sui_transaction_builder/intent/
coin_with_balance.rs

1use crate::Argument;
2use crate::Function;
3use crate::ObjectInput;
4use crate::TransactionBuilder;
5use crate::builder::ResolvedArgument;
6use crate::intent::BoxError;
7use crate::intent::Intent;
8use crate::intent::IntentResolver;
9use crate::intent::MAX_ARGUMENTS;
10use crate::intent::MAX_GAS_OBJECTS;
11use std::collections::BTreeMap;
12use sui_sdk_types::Address;
13use sui_sdk_types::Identifier;
14use sui_sdk_types::StructTag;
15
16pub struct CoinWithBalance {
17    coin_type: StructTag,
18    balance: u64,
19    use_gas_coin: bool,
20}
21
22impl CoinWithBalance {
23    pub fn new(coin_type: StructTag, balance: u64) -> Self {
24        Self {
25            coin_type,
26            balance,
27            use_gas_coin: true,
28        }
29    }
30
31    pub fn sui(balance: u64) -> Self {
32        Self {
33            coin_type: StructTag::sui(),
34            balance,
35            use_gas_coin: true,
36        }
37    }
38
39    // Used to explicitly opt out of using the gas coin.
40    // This parameter is only respected when coin type is `SUI`.
41    pub fn with_use_gas_coin(self, use_gas_coin: bool) -> Self {
42        Self {
43            use_gas_coin,
44            ..self
45        }
46    }
47}
48
49impl Intent for CoinWithBalance {
50    fn register(self, builder: &mut TransactionBuilder) -> Argument {
51        builder.register_resolver(CoinWithBalanceResolver);
52        builder.unresolved(self)
53    }
54}
55
56#[derive(Debug)]
57struct CoinWithBalanceResolver;
58
59#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
60enum CoinType {
61    Gas,
62    Coin(StructTag),
63}
64
65#[async_trait::async_trait]
66impl IntentResolver for CoinWithBalanceResolver {
67    async fn resolve(
68        &self,
69        builder: &mut TransactionBuilder,
70        client: &mut sui_rpc::Client,
71    ) -> Result<(), BoxError> {
72        // Collect all the requests
73        let mut requests: BTreeMap<CoinType, Vec<(usize, u64)>> = BTreeMap::new();
74        let mut zero_values = Vec::new();
75
76        for (id, intent) in builder.intents.extract_if(.., |_id, intent| {
77            intent.downcast_ref::<CoinWithBalance>().is_some()
78        }) {
79            let request = intent.downcast_ref::<CoinWithBalance>().unwrap();
80
81            if request.balance == 0 {
82                zero_values.push((id, request.coin_type.clone()));
83            } else {
84                let coin_type = if request.coin_type == StructTag::sui() && request.use_gas_coin {
85                    CoinType::Gas
86                } else {
87                    CoinType::Coin(request.coin_type.clone())
88                };
89                requests
90                    .entry(coin_type)
91                    .or_default()
92                    .push((id, request.balance));
93            }
94        }
95
96        for (id, coin_type) in zero_values {
97            CoinWithBalanceResolver::resolve_zero_balance_coin(builder, coin_type, id);
98        }
99
100        for (coin_type, requests) in requests {
101            match coin_type {
102                CoinType::Gas => {
103                    CoinWithBalanceResolver::resolve_gas_coin(builder, client, &requests).await?;
104                }
105                CoinType::Coin(coin_type) => {
106                    CoinWithBalanceResolver::resolve_coin_type(
107                        builder, client, &coin_type, &requests,
108                    )
109                    .await?;
110                }
111            }
112        }
113
114        Ok(())
115    }
116}
117
118impl CoinWithBalanceResolver {
119    fn resolve_zero_balance_coin(
120        builder: &mut TransactionBuilder,
121        coin_type: StructTag,
122        request_id: usize,
123    ) {
124        let coin = builder.move_call(
125            Function::new(
126                Address::TWO,
127                Identifier::from_static("coin"),
128                Identifier::from_static("zero"),
129            )
130            .with_type_args(vec![coin_type.into()]),
131            vec![],
132        );
133
134        *builder.arguments.get_mut(&request_id).unwrap() = ResolvedArgument::ReplaceWith(coin);
135    }
136
137    async fn resolve_coin_type(
138        builder: &mut TransactionBuilder,
139        client: &mut sui_rpc::Client,
140        coin_type: &StructTag,
141        requests: &[(usize, u64)],
142    ) -> Result<(), BoxError> {
143        let sender = builder
144            .sender()
145            .ok_or("Sender must be set to resolve CoinWithBalance")?;
146
147        if requests.is_empty() {
148            return Err("BUG: requests is empty".into());
149        }
150
151        let sum = requests.iter().map(|(_, balance)| *balance).sum();
152
153        let coins = client
154            //TODO populate excludes
155            .select_coins(&sender, &(coin_type.clone().into()), sum, &[])
156            .await?;
157
158        // For SUI need to handle working with gas coin
159        let split_coin_args = if let [first, rest @ ..] = coins
160            .into_iter()
161            .map(|coin| ObjectInput::try_from_object_proto(&coin).map(|coin| builder.object(coin)))
162            .collect::<Result<Vec<_>, _>>()?
163            .as_slice()
164        {
165            let mut deps = Vec::new();
166            for chunk in rest.chunks(MAX_ARGUMENTS) {
167                builder.merge_coins(*first, chunk.to_vec());
168                deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
169            }
170
171            let amounts = requests
172                .iter()
173                .map(|(_, balance)| builder.pure(balance))
174                .collect();
175            let coin_outputs = builder.split_coins(*first, amounts);
176            if !deps.is_empty() {
177                builder
178                    .commands
179                    .last_entry()
180                    .unwrap()
181                    .get_mut()
182                    .dependencies
183                    .extend(deps);
184            }
185            coin_outputs
186        } else {
187            return Err(format!("unable to find sufficient coins of type {}", coin_type).into());
188        };
189
190        for (coin, request_index) in split_coin_args
191            .into_iter()
192            .zip(requests.iter().map(|(index, _)| *index))
193        {
194            *builder.arguments.get_mut(&request_index).unwrap() =
195                ResolvedArgument::ReplaceWith(coin);
196        }
197
198        Ok(())
199    }
200
201    async fn resolve_gas_coin(
202        builder: &mut TransactionBuilder,
203        client: &mut sui_rpc::Client,
204        requests: &[(usize, u64)],
205    ) -> Result<(), BoxError> {
206        let sender = builder
207            .sender()
208            .ok_or("Sender must be set to resolve CoinWithBalance")?;
209
210        if requests.is_empty() {
211            return Err("BUG: requests is empty".into());
212        }
213
214        let sum = requests.iter().map(|(_, balance)| *balance).sum();
215
216        let mut coins = client
217            //TODO populate excludes
218            .select_coins(&sender, &(StructTag::sui().into()), sum, &[])
219            .await?
220            .into_iter()
221            .map(|coin| ObjectInput::try_from_object_proto(&coin))
222            .collect::<Result<Vec<_>, _>>()?
223            .into_iter();
224
225        let gas = builder.gas();
226        let mut deps = Vec::new();
227
228        // Append to gas coin up to 250 coins
229        builder
230            .add_gas_objects((&mut coins).take(MAX_GAS_OBJECTS.saturating_sub(builder.gas.len())));
231
232        // Any remaining do a merge coins
233        let remaining = coins.map(|coin| builder.object(coin)).collect::<Vec<_>>();
234
235        for chunk in remaining.chunks(MAX_ARGUMENTS) {
236            builder.merge_coins(gas, chunk.to_vec());
237            deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
238        }
239
240        let amounts = requests
241            .iter()
242            .map(|(_, balance)| builder.pure(balance))
243            .collect();
244        let split_coin_args = builder.split_coins(gas, amounts);
245        if !deps.is_empty() {
246            builder
247                .commands
248                .last_entry()
249                .unwrap()
250                .get_mut()
251                .dependencies
252                .extend(deps);
253        }
254
255        for (coin, request_index) in split_coin_args
256            .into_iter()
257            .zip(requests.iter().map(|(index, _)| *index))
258        {
259            *builder.arguments.get_mut(&request_index).unwrap() =
260                ResolvedArgument::ReplaceWith(coin);
261        }
262
263        Ok(())
264    }
265}