sui_transaction_builder/intent/
coin_with_balance.rs1use 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 futures::StreamExt;
12use std::collections::BTreeMap;
13use std::collections::HashSet;
14use sui_rpc::field::FieldMask;
15use sui_rpc::field::FieldMaskUtil;
16use sui_rpc::proto::sui::rpc::v2::GetBalanceRequest;
17use sui_rpc::proto::sui::rpc::v2::ListOwnedObjectsRequest;
18use sui_sdk_types::Address;
19use sui_sdk_types::Identifier;
20use sui_sdk_types::StructTag;
21
22pub struct CoinWithBalance {
46 coin_type: StructTag,
47 balance: u64,
48 use_gas_coin: bool,
49}
50
51impl CoinWithBalance {
52 pub fn new(coin_type: StructTag, balance: u64) -> Self {
62 Self {
63 coin_type,
64 balance,
65 use_gas_coin: true,
66 }
67 }
68
69 pub fn sui(balance: u64) -> Self {
80 Self {
81 coin_type: StructTag::sui(),
82 balance,
83 use_gas_coin: true,
84 }
85 }
86
87 pub fn with_use_gas_coin(self, use_gas_coin: bool) -> Self {
99 Self {
100 use_gas_coin,
101 ..self
102 }
103 }
104}
105
106impl Intent for CoinWithBalance {
107 fn register(self, builder: &mut TransactionBuilder) -> Argument {
108 builder.register_resolver(CoinWithBalanceResolver);
109 builder.unresolved(self)
110 }
111}
112
113#[derive(Debug)]
114struct CoinWithBalanceResolver;
115
116#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
117enum CoinType {
118 Gas,
119 Coin(StructTag),
120}
121
122#[async_trait::async_trait]
123impl IntentResolver for CoinWithBalanceResolver {
124 async fn resolve(
125 &self,
126 builder: &mut TransactionBuilder,
127 client: &mut sui_rpc::Client,
128 ) -> Result<(), BoxError> {
129 let mut requests: BTreeMap<CoinType, Vec<(usize, u64)>> = BTreeMap::new();
131 let mut zero_values = Vec::new();
132
133 for (id, intent) in builder.intents.extract_if(.., |_id, intent| {
134 intent.downcast_ref::<CoinWithBalance>().is_some()
135 }) {
136 let request = intent.downcast_ref::<CoinWithBalance>().unwrap();
137
138 if request.balance == 0 {
139 zero_values.push((id, request.coin_type.clone()));
140 } else {
141 let coin_type = if request.coin_type == StructTag::sui() && request.use_gas_coin {
142 CoinType::Gas
143 } else {
144 CoinType::Coin(request.coin_type.clone())
145 };
146 requests
147 .entry(coin_type)
148 .or_default()
149 .push((id, request.balance));
150 }
151 }
152
153 for (id, coin_type) in zero_values {
154 CoinWithBalanceResolver::resolve_zero_balance_coin(builder, coin_type, id);
155 }
156
157 for (coin_type, requests) in requests {
158 match coin_type {
159 CoinType::Gas => {
160 CoinWithBalanceResolver::resolve_gas_coin(builder, client, &requests).await?;
161 }
162 CoinType::Coin(coin_type) => {
163 CoinWithBalanceResolver::resolve_coin_type(
164 builder, client, &coin_type, &requests,
165 )
166 .await?;
167 }
168 }
169 }
170
171 Ok(())
172 }
173}
174
175impl CoinWithBalanceResolver {
176 fn resolve_zero_balance_coin(
177 builder: &mut TransactionBuilder,
178 coin_type: StructTag,
179 request_id: usize,
180 ) {
181 let coin = builder.move_call(
182 Function::new(
183 Address::TWO,
184 Identifier::from_static("coin"),
185 Identifier::from_static("zero"),
186 )
187 .with_type_args(vec![coin_type.into()]),
188 vec![],
189 );
190
191 *builder.arguments.get_mut(&request_id).unwrap() = ResolvedArgument::ReplaceWith(coin);
192 }
193
194 async fn resolve_coin_type(
195 builder: &mut TransactionBuilder,
196 client: &mut sui_rpc::Client,
197 coin_type: &StructTag,
198 requests: &[(usize, u64)],
199 ) -> Result<(), BoxError> {
200 let sender = builder
201 .sender()
202 .ok_or("Sender must be set to resolve CoinWithBalance")?;
203
204 if requests.is_empty() {
205 return Err("BUG: requests is empty".into());
206 }
207
208 let sum = requests.iter().map(|(_, balance)| *balance).sum();
209
210 let balance = client
211 .state_client()
212 .get_balance(
213 GetBalanceRequest::default()
214 .with_owner(sender)
215 .with_coin_type(coin_type),
216 )
217 .await?
218 .into_inner()
219 .balance
220 .take()
221 .unwrap_or_default();
222
223 if balance.balance() < sum {
225 return Err(format!(
226 "address {} does not have sufficient balance of {}: requested {} available {}",
227 sender,
228 coin_type,
229 sum,
230 balance.balance()
231 )
232 .into());
233 }
234
235 let excludes = builder.used_object_ids();
236 let (coins, remaining) =
237 Self::select_coins(client, &sender, coin_type, sum, &excludes).await?;
238
239 if balance.address_balance() < remaining {
242 return Err(format!(
243 "unable to find sufficient coins of type {}. requested {} found {} and AB of {} is insufficient to cover difference.",
244 coin_type,
245 sum,
246 sum - remaining,
247 balance.address_balance(),
248 )
249 .into());
250 }
251
252 let split_coin_args = if let [first, rest @ ..] = coins
253 .into_iter()
254 .map(|coin| builder.object(coin))
255 .collect::<Vec<_>>()
256 .as_slice()
257 {
258 let mut deps = Vec::new();
261 for chunk in rest.chunks(MAX_ARGUMENTS) {
262 builder.merge_coins(*first, chunk.to_vec());
263 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
264 }
265
266 if remaining > 0 {
269 let ab_coin = builder.funds_withdrawal_coin(coin_type.clone().into(), remaining);
270 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
271 builder.merge_coins(*first, vec![ab_coin]);
272 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
273 }
274
275 let amounts = requests
276 .iter()
277 .map(|(_, balance)| builder.pure(balance))
278 .collect();
279 let coin_outputs = builder.split_coins(*first, amounts);
280 if !deps.is_empty() {
281 builder
282 .commands
283 .last_entry()
284 .unwrap()
285 .get_mut()
286 .dependencies
287 .extend(deps);
288 }
289
290 coin_outputs
293 } else {
294 requests
296 .iter()
297 .map(|(_, balance)| {
298 builder.funds_withdrawal_coin(coin_type.clone().into(), *balance)
299 })
300 .collect()
301 };
302
303 for (coin, request_index) in split_coin_args
304 .into_iter()
305 .zip(requests.iter().map(|(index, _)| *index))
306 {
307 *builder.arguments.get_mut(&request_index).unwrap() =
308 ResolvedArgument::ReplaceWith(coin);
309 }
310
311 Ok(())
312 }
313
314 async fn resolve_gas_coin(
315 builder: &mut TransactionBuilder,
316 client: &mut sui_rpc::Client,
317 requests: &[(usize, u64)],
318 ) -> Result<(), BoxError> {
319 let sender = builder
320 .sender()
321 .ok_or("Sender must be set to resolve CoinWithBalance")?;
322
323 if requests.is_empty() {
324 return Err("BUG: requests is empty".into());
325 }
326
327 let coin_type = StructTag::sui();
328
329 let sum = requests.iter().map(|(_, balance)| *balance).sum();
330
331 let balance = client
332 .state_client()
333 .get_balance(
334 GetBalanceRequest::default()
335 .with_owner(sender)
336 .with_coin_type(&coin_type),
337 )
338 .await?
339 .into_inner()
340 .balance
341 .take()
342 .unwrap_or_default();
343
344 if balance.balance() < sum {
346 return Err(format!(
347 "address {} does not have sufficient balance of {}: requested {} available {}",
348 sender,
349 coin_type,
350 sum,
351 balance.balance()
352 )
353 .into());
354 }
355
356 let excludes = builder.used_object_ids();
357 let (coins, remaining) =
358 Self::select_coins(client, &sender, &coin_type, sum, &excludes).await?;
359
360 if balance.address_balance() < remaining {
363 return Err(format!(
364 "unable to find sufficient coins of type {}. requested {} found {} and AB of {} is insufficient to cover difference.",
365 coin_type,
366 sum,
367 sum - remaining,
368 balance.address_balance(),
369 )
370 .into());
371 }
372
373 let coin_args = if coins.is_empty() {
374 requests
376 .iter()
377 .map(|(_, balance)| {
378 builder.funds_withdrawal_coin(coin_type.clone().into(), *balance)
379 })
380 .collect()
381 } else {
382 let gas = builder.gas();
383 let mut deps = Vec::new();
384
385 let (use_as_gas, remaining_coins) = coins.split_at(std::cmp::min(
387 coins.len(),
388 MAX_GAS_OBJECTS.saturating_sub(builder.gas.len()),
389 ));
390 builder.add_gas_objects(use_as_gas.iter().cloned());
391
392 for chunk in remaining_coins
394 .iter()
395 .map(|coin| builder.object(coin.clone()))
396 .collect::<Vec<_>>()
397 .chunks(MAX_ARGUMENTS)
398 {
399 builder.merge_coins(gas, chunk.to_vec());
400 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
401 }
402
403 if remaining > 0 {
406 let ab_coin = builder
408 .funds_withdrawal_coin(coin_type.clone().into(), remaining + 500_000_000);
409 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
410 builder.merge_coins(gas, vec![ab_coin]);
411 deps.push(Argument::new(*builder.commands.last_key_value().unwrap().0));
412 }
413
414 let amounts = requests
415 .iter()
416 .map(|(_, balance)| builder.pure(balance))
417 .collect();
418 let split_coin_args = builder.split_coins(gas, amounts);
419 if !deps.is_empty() {
420 builder
421 .commands
422 .last_entry()
423 .unwrap()
424 .get_mut()
425 .dependencies
426 .extend(deps);
427 }
428
429 split_coin_args
432 };
433
434 for (coin, request_index) in coin_args
435 .into_iter()
436 .zip(requests.iter().map(|(index, _)| *index))
437 {
438 *builder.arguments.get_mut(&request_index).unwrap() =
439 ResolvedArgument::ReplaceWith(coin);
440 }
441
442 Ok(())
443 }
444
445 async fn select_coins(
446 client: &mut sui_rpc::Client,
447 owner_address: &Address,
448 coin_type: &StructTag,
449 amount: u64,
450 excludes: &HashSet<Address>,
451 ) -> Result<(Vec<ObjectInput>, u64), BoxError> {
452 let coin_struct = StructTag::coin(coin_type.clone().into());
453 let list_request = ListOwnedObjectsRequest::default()
454 .with_owner(owner_address)
455 .with_object_type(&coin_struct)
456 .with_page_size(500u32)
457 .with_read_mask(FieldMask::from_paths([
458 "object_id",
459 "version",
460 "digest",
461 "balance",
462 "owner",
463 ]));
464
465 let mut coin_stream = Box::pin(client.list_owned_objects(list_request));
466 let mut selected_coins = Vec::new();
467 let mut remaining = amount;
468
469 while let Some(object_result) = coin_stream.next().await {
470 let object = object_result?;
471 let coin = ObjectInput::try_from_object_proto(&object)?;
472
473 if excludes.contains(&coin.object_id()) {
474 continue;
475 }
476
477 remaining = remaining.saturating_sub(object.balance());
478 selected_coins.push(coin);
479
480 if remaining == 0 && selected_coins.len() >= 500 {
482 break;
483 }
484 }
485
486 Ok((selected_coins, remaining))
487 }
488}