1use anyhow::{Context, bail};
8use indexmap::IndexMap;
9use move_core_types::{ident_str, identifier::Identifier, language_storage::TypeTag};
10use serde::Serialize;
11
12use crate::{
13 SUI_FRAMEWORK_PACKAGE_ID,
14 balance::{
15 BALANCE_MODULE_NAME, BALANCE_REDEEM_FUNDS_FUNCTION_NAME, BALANCE_SEND_FUNDS_FUNCTION_NAME,
16 },
17 base_types::{FullObjectID, FullObjectRef, ObjectID, ObjectRef, SuiAddress},
18 move_package::PACKAGE_MODULE_NAME,
19 transaction::{
20 Argument, CallArg, Command, FundsWithdrawalArg, ObjectArg, ProgrammableTransaction,
21 SharedObjectMutability,
22 },
23 type_input::TypeInput,
24};
25
26#[cfg(test)]
27#[path = "unit_tests/programmable_transaction_builder_tests.rs"]
28mod programmable_transaction_builder_tests;
29
30#[derive(PartialEq, Eq, Hash)]
31enum BuilderArg {
32 Object(ObjectID),
33 Pure(Vec<u8>),
34 ForcedNonUniquePure(usize),
35 FundsWithdraw(usize),
36}
37
38#[derive(Default)]
39pub struct ProgrammableTransactionBuilder {
40 inputs: IndexMap<BuilderArg, CallArg>,
41 commands: Vec<Command>,
42}
43
44impl ProgrammableTransactionBuilder {
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn finish(self) -> ProgrammableTransaction {
50 let Self { inputs, commands } = self;
51 let inputs = inputs.into_values().collect();
52 ProgrammableTransaction { inputs, commands }
53 }
54
55 pub fn pure_bytes(&mut self, bytes: Vec<u8>, force_separate: bool) -> Argument {
56 let arg = if force_separate {
57 BuilderArg::ForcedNonUniquePure(self.inputs.len())
58 } else {
59 BuilderArg::Pure(bytes.clone())
60 };
61 let (i, _) = self.inputs.insert_full(arg, CallArg::Pure(bytes));
62 Argument::Input(i as u16)
63 }
64
65 pub fn pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
66 Ok(self.pure_bytes(
67 bcs::to_bytes(&value).context("Serializing pure argument.")?,
68 false,
69 ))
70 }
71
72 pub fn force_separate_pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
74 Ok(self.pure_bytes(
75 bcs::to_bytes(&value).context("Serializing pure argument.")?,
76 true,
77 ))
78 }
79
80 pub fn obj(&mut self, obj_arg: ObjectArg) -> anyhow::Result<Argument> {
81 let id = obj_arg.id();
82 let obj_arg = if let Some(old_value) = self.inputs.get(&BuilderArg::Object(id)) {
83 let old_obj_arg = match old_value {
84 CallArg::Pure(_) => anyhow::bail!("invariant violation! object has pure argument"),
85 CallArg::Object(arg) => arg,
86 CallArg::FundsWithdrawal(_) => {
87 anyhow::bail!("invariant violation! object has balance withdraw argument")
88 }
89 };
90 match (old_obj_arg, obj_arg) {
91 (
92 ObjectArg::SharedObject {
93 id: id1,
94 initial_shared_version: v1,
95 mutability: mut1,
96 },
97 ObjectArg::SharedObject {
98 id: id2,
99 initial_shared_version: v2,
100 mutability: mut2,
101 },
102 ) if v1 == &v2 => {
103 anyhow::ensure!(
104 id1 == &id2 && id == id2,
105 "invariant violation! object has id does not match call arg"
106 );
107 ObjectArg::SharedObject {
108 id,
109 initial_shared_version: v2,
110 mutability: if mut1 == &SharedObjectMutability::Mutable
111 || mut2 == SharedObjectMutability::Mutable
112 {
113 SharedObjectMutability::Mutable
114 } else {
115 mut2
116 },
117 }
118 }
119 (old_obj_arg, obj_arg) => {
120 anyhow::ensure!(
121 old_obj_arg == &obj_arg,
122 "Mismatched Object argument kind for object {id}. \
123 {old_value:?} is not compatible with {obj_arg:?}"
124 );
125 obj_arg
126 }
127 }
128 } else {
129 obj_arg
130 };
131 let (i, _) = self
132 .inputs
133 .insert_full(BuilderArg::Object(id), CallArg::Object(obj_arg));
134 Ok(Argument::Input(i as u16))
135 }
136
137 pub fn funds_withdrawal(&mut self, arg: FundsWithdrawalArg) -> anyhow::Result<Argument> {
138 let (i, _) = self.inputs.insert_full(
139 BuilderArg::FundsWithdraw(self.inputs.len()),
140 CallArg::FundsWithdrawal(arg),
141 );
142 Ok(Argument::Input(i as u16))
143 }
144
145 pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result<Argument> {
146 match call_arg {
147 CallArg::Pure(bytes) => Ok(self.pure_bytes(bytes, false)),
148 CallArg::Object(obj) => self.obj(obj),
149 CallArg::FundsWithdrawal(arg) => self.funds_withdrawal(arg),
150 }
151 }
152
153 pub fn make_obj_vec(
154 &mut self,
155 objs: impl IntoIterator<Item = ObjectArg>,
156 ) -> anyhow::Result<Argument> {
157 let make_vec_args = objs
158 .into_iter()
159 .map(|obj| self.obj(obj))
160 .collect::<Result<_, _>>()?;
161 Ok(self.command(Command::MakeMoveVec(None, make_vec_args)))
162 }
163
164 pub fn command(&mut self, command: Command) -> Argument {
165 let i = self.commands.len();
166 self.commands.push(command);
167 Argument::Result(i as u16)
168 }
169
170 pub fn move_call(
172 &mut self,
173 package: ObjectID,
174 module: Identifier,
175 function: Identifier,
176 type_arguments: Vec<TypeTag>,
177 call_args: Vec<CallArg>,
178 ) -> anyhow::Result<()> {
179 let arguments = call_args
180 .into_iter()
181 .map(|a| self.input(a))
182 .collect::<Result<_, _>>()?;
183 self.command(Command::move_call(
184 package,
185 module,
186 function,
187 type_arguments,
188 arguments,
189 ));
190 Ok(())
191 }
192
193 pub fn programmable_move_call(
194 &mut self,
195 package: ObjectID,
196 module: Identifier,
197 function: Identifier,
198 type_arguments: Vec<TypeTag>,
199 arguments: Vec<Argument>,
200 ) -> Argument {
201 self.command(Command::move_call(
202 package,
203 module,
204 function,
205 type_arguments,
206 arguments,
207 ))
208 }
209
210 pub fn publish_upgradeable(
211 &mut self,
212 modules: Vec<Vec<u8>>,
213 dep_ids: Vec<ObjectID>,
214 ) -> Argument {
215 self.command(Command::Publish(modules, dep_ids))
216 }
217
218 pub fn publish_immutable(&mut self, modules: Vec<Vec<u8>>, dep_ids: Vec<ObjectID>) {
219 let cap = self.publish_upgradeable(modules, dep_ids);
220 self.commands.push(Command::move_call(
221 SUI_FRAMEWORK_PACKAGE_ID,
222 PACKAGE_MODULE_NAME.to_owned(),
223 ident_str!("make_immutable").to_owned(),
224 vec![],
225 vec![cap],
226 ));
227 }
228
229 pub fn upgrade(
230 &mut self,
231 current_package_object_id: ObjectID,
232 upgrade_ticket: Argument,
233 transitive_deps: Vec<ObjectID>,
234 modules: Vec<Vec<u8>>,
235 ) -> Argument {
236 self.command(Command::Upgrade(
237 modules,
238 transitive_deps,
239 current_package_object_id,
240 upgrade_ticket,
241 ))
242 }
243
244 pub fn transfer_arg(&mut self, recipient: SuiAddress, arg: Argument) {
245 self.transfer_args(recipient, vec![arg])
246 }
247
248 pub fn transfer_args(&mut self, recipient: SuiAddress, args: Vec<Argument>) {
249 let rec_arg = self.pure(recipient).unwrap();
250 self.commands.push(Command::TransferObjects(args, rec_arg));
251 }
252
253 pub fn transfer_object(
254 &mut self,
255 recipient: SuiAddress,
256 full_object_ref: FullObjectRef,
257 ) -> anyhow::Result<()> {
258 let rec_arg = self.pure(recipient).unwrap();
259 let obj_arg = self.obj(match full_object_ref.0 {
260 FullObjectID::Fastpath(_) => {
261 ObjectArg::ImmOrOwnedObject(full_object_ref.as_object_ref())
262 }
263 FullObjectID::Consensus((id, initial_shared_version)) => ObjectArg::SharedObject {
264 id,
265 initial_shared_version,
266 mutability: SharedObjectMutability::Mutable,
267 },
268 });
269 self.commands
270 .push(Command::TransferObjects(vec![obj_arg?], rec_arg));
271 Ok(())
272 }
273
274 pub fn transfer_sui(&mut self, recipient: SuiAddress, amount: Option<u64>) {
275 let rec_arg = self.pure(recipient).unwrap();
276 let coin_arg = if let Some(amount) = amount {
277 let amt_arg = self.pure(amount).unwrap();
278 self.command(Command::SplitCoins(Argument::GasCoin, vec![amt_arg]))
279 } else {
280 Argument::GasCoin
281 };
282 self.command(Command::TransferObjects(vec![coin_arg], rec_arg));
283 }
284
285 pub fn redeem_funds(&mut self, amount: u64, type_arg: TypeTag) -> anyhow::Result<Argument> {
286 let withdrawal_arg =
287 FundsWithdrawalArg::balance_from_sender(amount, TypeInput::from(type_arg.clone()));
288 let withdrawal_arg = self.funds_withdrawal(withdrawal_arg)?;
289 Ok(self.programmable_move_call(
290 SUI_FRAMEWORK_PACKAGE_ID,
291 BALANCE_MODULE_NAME.to_owned(),
292 BALANCE_REDEEM_FUNDS_FUNCTION_NAME.to_owned(),
293 vec![type_arg],
294 vec![withdrawal_arg],
295 ))
296 }
297
298 pub fn transfer_balance(
299 &mut self,
300 recipient: SuiAddress,
301 amount: u64,
302 type_arg: TypeTag,
303 ) -> anyhow::Result<()> {
304 let rec_arg = self.pure(recipient).unwrap();
305 let balance = self.redeem_funds(amount, type_arg.clone())?;
306
307 self.programmable_move_call(
308 SUI_FRAMEWORK_PACKAGE_ID,
309 BALANCE_MODULE_NAME.to_owned(),
310 BALANCE_SEND_FUNDS_FUNCTION_NAME.to_owned(),
311 vec![type_arg],
312 vec![balance, rec_arg],
313 );
314 Ok(())
315 }
316
317 pub fn pay_all_sui(&mut self, recipient: SuiAddress) {
318 let rec_arg = self.pure(recipient).unwrap();
319 self.command(Command::TransferObjects(vec![Argument::GasCoin], rec_arg));
320 }
321
322 pub fn pay_sui(
324 &mut self,
325 recipients: Vec<SuiAddress>,
326 amounts: Vec<u64>,
327 ) -> anyhow::Result<()> {
328 self.pay_impl(recipients, amounts, Argument::GasCoin)
329 }
330
331 pub fn split_coin(&mut self, recipient: SuiAddress, coin: ObjectRef, amounts: Vec<u64>) {
332 let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap();
333 let amounts_len = amounts.len();
334 let amt_args = amounts.into_iter().map(|a| self.pure(a).unwrap()).collect();
335 let result = self.command(Command::SplitCoins(coin_arg, amt_args));
336 let Argument::Result(result) = result else {
337 panic!("self.command should always give a Argument::Result");
338 };
339
340 let recipient = self.pure(recipient).unwrap();
341 self.command(Command::TransferObjects(
342 (0..amounts_len)
343 .map(|i| Argument::NestedResult(result, i as u16))
344 .collect(),
345 recipient,
346 ));
347 }
348
349 pub fn merge_coins(&mut self, target: ObjectRef, coins: Vec<ObjectRef>) -> anyhow::Result<()> {
351 let target_arg = self.obj(ObjectArg::ImmOrOwnedObject(target))?;
352 let coin_args = coins
353 .into_iter()
354 .map(|coin| self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap())
355 .collect::<Vec<_>>();
356 self.command(Command::MergeCoins(target_arg, coin_args));
357 Ok(())
358 }
359
360 pub fn smash_coins(&mut self, coins: Vec<ObjectRef>) -> anyhow::Result<Argument> {
363 let mut coins = coins.into_iter();
364 let Some(target) = coins.next() else {
365 bail!("coins vector is empty");
366 };
367 let target_arg = self.obj(ObjectArg::ImmOrOwnedObject(target))?;
368 let coin_args = coins
369 .map(|coin| self.obj(ObjectArg::ImmOrOwnedObject(coin)).unwrap())
370 .collect::<Vec<_>>();
371 self.command(Command::MergeCoins(target_arg, coin_args));
372 Ok(target_arg)
373 }
374
375 pub fn pay(
378 &mut self,
379 coins: Vec<ObjectRef>,
380 recipients: Vec<SuiAddress>,
381 amounts: Vec<u64>,
382 ) -> anyhow::Result<()> {
383 let mut coins = coins.into_iter();
384 let Some(coin) = coins.next() else {
385 anyhow::bail!("coins vector is empty");
386 };
387 let coin_arg = self.obj(ObjectArg::ImmOrOwnedObject(coin))?;
388 let merge_args: Vec<_> = coins
389 .map(|c| self.obj(ObjectArg::ImmOrOwnedObject(c)))
390 .collect::<Result<_, _>>()?;
391 if !merge_args.is_empty() {
392 self.command(Command::MergeCoins(coin_arg, merge_args));
393 }
394 self.pay_impl(recipients, amounts, coin_arg)
395 }
396
397 fn pay_impl(
398 &mut self,
399 recipients: Vec<SuiAddress>,
400 amounts: Vec<u64>,
401 coin: Argument,
402 ) -> anyhow::Result<()> {
403 if recipients.len() != amounts.len() {
404 anyhow::bail!(
405 "Recipients and amounts mismatch. Got {} recipients but {} amounts",
406 recipients.len(),
407 amounts.len()
408 )
409 }
410 if amounts.is_empty() {
411 return Ok(());
412 }
413
414 let mut recipient_map: IndexMap<SuiAddress, Vec<usize>> = IndexMap::new();
417 let mut amt_args = Vec::with_capacity(recipients.len());
418 for (i, (recipient, amount)) in recipients.into_iter().zip(amounts).enumerate() {
419 recipient_map.entry(recipient).or_default().push(i);
420 amt_args.push(self.pure(amount)?);
421 }
422 let Argument::Result(split_primary) = self.command(Command::SplitCoins(coin, amt_args))
423 else {
424 panic!("self.command should always give a Argument::Result")
425 };
426 for (recipient, split_secondaries) in recipient_map {
427 let rec_arg = self.pure(recipient).unwrap();
428 let coins = split_secondaries
429 .into_iter()
430 .map(|j| Argument::NestedResult(split_primary, j as u16))
431 .collect();
432 self.command(Command::TransferObjects(coins, rec_arg));
433 }
434 Ok(())
435 }
436}