1use anyhow::Context;
9use indexmap::IndexMap;
10use iota_sdk_types::{Identifier, TypeTag};
11use serde::Serialize;
12
13use crate::{
14 base_types::{IotaAddress, ObjectID, ObjectRef},
15 transaction::{Argument, CallArg, Command, ProgrammableTransaction, SharedObjectRef},
16};
17
18#[derive(PartialEq, Eq, Hash)]
19enum BuilderArg {
20 Object(ObjectID),
21 Pure(Vec<u8>),
22 ForcedNonUniquePure(usize),
23}
24
25#[derive(Default)]
26pub struct ProgrammableTransactionBuilder {
27 inputs: IndexMap<BuilderArg, CallArg>,
28 commands: Vec<Command>,
29}
30
31impl ProgrammableTransactionBuilder {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 pub fn finish(self) -> ProgrammableTransaction {
37 let Self { inputs, commands } = self;
38 let inputs = inputs.into_values().collect();
39 ProgrammableTransaction { inputs, commands }
40 }
41
42 pub fn pure_bytes(&mut self, bytes: Vec<u8>, force_separate: bool) -> Argument {
43 let arg = if force_separate {
44 BuilderArg::ForcedNonUniquePure(self.inputs.len())
45 } else {
46 BuilderArg::Pure(bytes.clone())
47 };
48 let (i, _) = self.inputs.insert_full(arg, CallArg::Pure(bytes));
49 Argument::Input(i as u16)
50 }
51
52 pub fn pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
53 Ok(self.pure_bytes(
54 bcs::to_bytes(&value).context("Serializing pure argument.")?,
55 false,
57 ))
58 }
59
60 pub fn force_separate_pure<T: Serialize>(&mut self, value: T) -> anyhow::Result<Argument> {
62 Ok(self.pure_bytes(
63 bcs::to_bytes(&value).context("Serializing pure argument.")?,
64 true,
66 ))
67 }
68
69 pub fn obj(&mut self, obj_arg: impl Into<CallArg>) -> anyhow::Result<Argument> {
70 let obj_arg: CallArg = obj_arg.into();
71 let id = *obj_arg
72 .object_id_opt()
73 .ok_or_else(|| anyhow::anyhow!("expected object CallArg, found pure argument"))?;
74 let obj_arg = if let Some(old_value) = self.inputs.get(&BuilderArg::Object(id)) {
75 match (old_value.as_shared_opt(), obj_arg.as_shared_opt()) {
76 (
77 Some(&SharedObjectRef {
78 object_id: id1,
79 initial_shared_version: v1,
80 mutable: mut1,
81 }),
82 Some(&SharedObjectRef {
83 object_id: id2,
84 initial_shared_version: v2,
85 mutable: mut2,
86 }),
87 ) if v1 == v2 => {
88 anyhow::ensure!(
89 id1 == id2 && id == id2,
90 "invariant violation! object has id does not match call arg"
91 );
92 CallArg::Shared(SharedObjectRef {
93 object_id: id,
94 initial_shared_version: v2,
95 mutable: mut1 || mut2,
96 })
97 }
98 _ => {
99 anyhow::ensure!(
100 *old_value == obj_arg,
101 "Mismatched Object argument kind for object {id}. \
102 {old_value:?} is not compatible with {obj_arg:?}"
103 );
104 obj_arg
105 }
106 }
107 } else {
108 obj_arg
109 };
110 let (i, _) = self.inputs.insert_full(BuilderArg::Object(id), obj_arg);
111 Ok(Argument::Input(i as u16))
112 }
113
114 pub fn input(&mut self, call_arg: CallArg) -> anyhow::Result<Argument> {
115 match call_arg {
116 CallArg::Pure(value) => Ok(self.pure_bytes(value, false)),
117 CallArg::ImmutableOrOwned(_) | CallArg::Shared(_) | CallArg::Receiving(_) => {
118 self.obj(call_arg)
119 }
120 _ => unimplemented!("a new CallArg variant was added and needs to be handled"),
121 }
122 }
123
124 pub fn make_obj_vec<T: Into<CallArg>>(
125 &mut self,
126 objs: impl IntoIterator<Item = T>,
127 ) -> anyhow::Result<Argument> {
128 let make_vec_args = objs
129 .into_iter()
130 .map(|obj| self.obj(obj.into()))
131 .collect::<Result<_, _>>()?;
132 Ok(self.command(Command::new_make_move_vector(None, make_vec_args)))
133 }
134
135 pub fn command(&mut self, command: Command) -> Argument {
136 let i = self.commands.len();
137 self.commands.push(command);
138 Argument::Result(i as u16)
139 }
140
141 pub fn move_call(
143 &mut self,
144 package: ObjectID,
145 module: Identifier,
146 function: Identifier,
147 type_arguments: Vec<TypeTag>,
148 call_args: Vec<CallArg>,
149 ) -> anyhow::Result<()> {
150 let arguments = call_args
151 .into_iter()
152 .map(|a| self.input(a))
153 .collect::<Result<_, _>>()?;
154 self.command(Command::new_move_call(
155 package,
156 module,
157 function,
158 type_arguments,
159 arguments,
160 ));
161 Ok(())
162 }
163
164 pub fn programmable_move_call(
165 &mut self,
166 package: ObjectID,
167 module: Identifier,
168 function: Identifier,
169 type_arguments: Vec<TypeTag>,
170 arguments: Vec<Argument>,
171 ) -> Argument {
172 self.command(Command::new_move_call(
173 package,
174 module,
175 function,
176 type_arguments,
177 arguments,
178 ))
179 }
180
181 pub fn publish_upgradeable(
182 &mut self,
183 modules: Vec<Vec<u8>>,
184 dep_ids: Vec<ObjectID>,
185 ) -> Argument {
186 self.command(Command::new_publish(modules, dep_ids))
187 }
188
189 pub fn publish_immutable(&mut self, modules: Vec<Vec<u8>>, dep_ids: Vec<ObjectID>) {
190 let cap = self.publish_upgradeable(modules, dep_ids);
191 self.commands.push(Command::new_move_call(
192 ObjectID::FRAMEWORK,
193 Identifier::PACKAGE_MODULE,
194 Identifier::from_static("make_immutable"),
195 vec![],
196 vec![cap],
197 ));
198 }
199
200 pub fn upgrade(
201 &mut self,
202 current_package_object_id: ObjectID,
203 upgrade_ticket: Argument,
204 transitive_deps: Vec<ObjectID>,
205 modules: Vec<Vec<u8>>,
206 ) -> Argument {
207 self.command(Command::new_upgrade(
208 modules,
209 transitive_deps,
210 current_package_object_id,
211 upgrade_ticket,
212 ))
213 }
214
215 pub fn transfer_arg(&mut self, recipient: IotaAddress, arg: Argument) {
216 self.transfer_args(recipient, vec![arg])
217 }
218
219 pub fn transfer_args(&mut self, recipient: IotaAddress, args: Vec<Argument>) {
220 let rec_arg = self.pure(recipient).unwrap();
221 self.commands
222 .push(Command::new_transfer_objects(args, rec_arg));
223 }
224
225 pub fn transfer_object(
226 &mut self,
227 recipient: IotaAddress,
228 object_ref: ObjectRef,
229 ) -> anyhow::Result<()> {
230 let rec_arg = self.pure(recipient).unwrap();
231 let obj_arg = self.obj(CallArg::ImmutableOrOwned(object_ref))?;
232 self.commands
233 .push(Command::new_transfer_objects(vec![obj_arg], rec_arg));
234 Ok(())
235 }
236
237 pub fn transfer_iota(&mut self, recipient: IotaAddress, amount: Option<u64>) {
238 let rec_arg = self.pure(recipient).unwrap();
239 let coin_arg = if let Some(amount) = amount {
240 let amt_arg = self.pure(amount).unwrap();
241 self.command(Command::new_split_coins(Argument::Gas, vec![amt_arg]))
242 } else {
243 Argument::Gas
244 };
245 self.command(Command::new_transfer_objects(vec![coin_arg], rec_arg));
246 }
247
248 pub fn pay_all_iota(&mut self, recipient: IotaAddress) {
249 let rec_arg = self.pure(recipient).unwrap();
250 self.command(Command::new_transfer_objects(vec![Argument::Gas], rec_arg));
251 }
252
253 pub fn pay_iota(
256 &mut self,
257 recipients: Vec<IotaAddress>,
258 amounts: Vec<u64>,
259 ) -> anyhow::Result<()> {
260 self.pay_impl(recipients, amounts, Argument::Gas)
261 }
262
263 pub fn split_coin(&mut self, recipient: IotaAddress, coin: ObjectRef, amounts: Vec<u64>) {
264 let coin_arg = self.obj(CallArg::ImmutableOrOwned(coin)).unwrap();
265 let amounts_len = amounts.len();
266 let amt_args = amounts.into_iter().map(|a| self.pure(a).unwrap()).collect();
267 let result = self.command(Command::new_split_coins(coin_arg, amt_args));
268 let Argument::Result(result) = result else {
269 panic!("self.command should always give a Argument::Result");
270 };
271
272 let recipient = self.pure(recipient).unwrap();
273 self.command(Command::new_transfer_objects(
274 (0..amounts_len)
275 .map(|i| Argument::NestedResult(result, i as u16))
276 .collect(),
277 recipient,
278 ));
279 }
280
281 pub fn pay(
284 &mut self,
285 coins: Vec<ObjectRef>,
286 recipients: Vec<IotaAddress>,
287 amounts: Vec<u64>,
288 ) -> anyhow::Result<()> {
289 let mut coins = coins.into_iter();
290 let Some(coin) = coins.next() else {
291 anyhow::bail!("coins vector is empty");
292 };
293 let coin_arg = self.obj(CallArg::ImmutableOrOwned(coin))?;
294 let merge_args: Vec<_> = coins
295 .map(|c| self.obj(CallArg::ImmutableOrOwned(c)))
296 .collect::<Result<_, _>>()?;
297 if !merge_args.is_empty() {
298 self.command(Command::new_merge_coins(coin_arg, merge_args));
299 }
300 self.pay_impl(recipients, amounts, coin_arg)
301 }
302
303 fn pay_impl(
304 &mut self,
305 recipients: Vec<IotaAddress>,
306 amounts: Vec<u64>,
307 coin: Argument,
308 ) -> anyhow::Result<()> {
309 if recipients.len() != amounts.len() {
310 anyhow::bail!(
311 "Recipients and amounts mismatch. Got {} recipients but {} amounts",
312 recipients.len(),
313 amounts.len()
314 )
315 }
316 if amounts.is_empty() {
317 return Ok(());
318 }
319
320 let mut recipient_map: IndexMap<IotaAddress, Vec<usize>> = IndexMap::new();
323 let mut amt_args = Vec::with_capacity(recipients.len());
324 for (i, (recipient, amount)) in recipients.into_iter().zip(amounts).enumerate() {
325 recipient_map.entry(recipient).or_default().push(i);
326 amt_args.push(self.pure(amount)?);
327 }
328 let Argument::Result(split_primary) =
329 self.command(Command::new_split_coins(coin, amt_args))
330 else {
331 panic!("self.command should always give a Argument::Result")
332 };
333 for (recipient, split_secondaries) in recipient_map {
334 let rec_arg = self.pure(recipient).unwrap();
335 let coins = split_secondaries
336 .into_iter()
337 .map(|j| Argument::NestedResult(split_primary, j as u16))
338 .collect();
339 self.command(Command::new_transfer_objects(coins, rec_arg));
340 }
341 Ok(())
342 }
343}