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 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 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 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 .select_coins(&sender, &(coin_type.clone().into()), sum, &[])
156 .await?;
157
158 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 .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 builder
230 .add_gas_objects((&mut coins).take(MAX_GAS_OBJECTS.saturating_sub(builder.gas.len())));
231
232 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}