iota_transaction_builder/
utils.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::result::Result;
6
7use anyhow::{anyhow, bail};
8use futures::future::join_all;
9use iota_json::{
10    IotaJsonValue, ResolvedCallArg, is_receiving_argument, resolve_call_args,
11    resolve_move_function_args,
12};
13use iota_json_rpc_types::{IotaArgument, IotaData, IotaObjectDataOptions, IotaRawData, PtbInput};
14use iota_protocol_config::ProtocolConfig;
15use iota_types::{
16    base_types::{
17        Identifier, IotaAddress, ObjectID, ObjectRef, ObjectType, StructTag, TxContext,
18        TxContextKind, TypeTag,
19    },
20    error::UserInputError,
21    fp_ensure,
22    gas_coin::GasCoin,
23    move_package::MovePackage,
24    object::{Object, Owner},
25    programmable_transaction_builder::ProgrammableTransactionBuilder,
26    transaction::{Argument, CallArg, ObjectArg},
27};
28use move_binary_format::{
29    CompiledModule, binary_config::BinaryConfig, file_format::SignatureToken,
30};
31
32use crate::TransactionBuilder;
33
34impl TransactionBuilder {
35    /// Select a gas coin for the provided gas budget.
36    pub async fn select_gas(
37        &self,
38        signer: IotaAddress,
39        input_gas: impl Into<Option<ObjectID>>,
40        gas_budget: u64,
41        input_objects: Vec<ObjectID>,
42        gas_price: u64,
43    ) -> Result<ObjectRef, anyhow::Error> {
44        if gas_budget < gas_price {
45            bail!(
46                "Gas budget {gas_budget} is less than the reference gas price {gas_price}. The gas budget must be at least the current reference gas price of {gas_price}."
47            )
48        }
49        if let Some(gas) = input_gas.into() {
50            self.get_object_ref(gas).await
51        } else {
52            let mut cursor = None;
53            // Paginate through all gas coins owned by the signer
54            loop {
55                let page = self
56                    .0
57                    .get_owned_objects(
58                        signer,
59                        StructTag::new_gas_coin(),
60                        cursor,
61                        None,
62                        IotaObjectDataOptions::new().with_bcs(),
63                    )
64                    .await?;
65                for response in &page.data {
66                    let obj = response.object()?;
67                    let gas: GasCoin = bcs::from_bytes(
68                        &obj.bcs
69                            .as_ref()
70                            .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))?
71                            .try_as_move()
72                            .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))?
73                            .bcs_bytes,
74                    )?;
75                    if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget {
76                        return Ok(obj.object_ref());
77                    }
78                }
79                if !page.has_next_page {
80                    break;
81                }
82                cursor = page.next_cursor;
83            }
84
85            Err(anyhow!(
86                "Cannot find gas coin for signer address {signer} with amount sufficient for the required gas budget {gas_budget}. If you are using the pay or transfer commands, you can use the pay-iota command instead, which will use the only object as gas payment."
87            ))
88        }
89    }
90
91    /// Get the object references for a list of object IDs
92    pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result<Vec<ObjectRef>, anyhow::Error> {
93        let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect();
94        let obj_refs = join_all(handles)
95            .await
96            .into_iter()
97            .collect::<anyhow::Result<Vec<ObjectRef>>>()?;
98        Ok(obj_refs)
99    }
100
101    /// Resolve a provided [`ObjectID`] to the required [`ObjectArg`] for a
102    /// given move module.
103    async fn get_object_arg(
104        &self,
105        id: ObjectID,
106        is_mutable_ref: bool,
107        view: &CompiledModule,
108        arg_type: &SignatureToken,
109    ) -> Result<ObjectArg, anyhow::Error> {
110        let response = self
111            .0
112            .get_object_with_options(id, IotaObjectDataOptions::bcs_lossless())
113            .await?;
114
115        let obj: Object = response.into_object()?.try_into()?;
116        let obj_ref = obj.compute_object_reference();
117        let owner = obj.owner;
118        if is_receiving_argument(view, arg_type) {
119            return Ok(ObjectArg::Receiving(obj_ref));
120        }
121        Ok(match owner {
122            Owner::Shared(initial_shared_version) => ObjectArg::SharedObject {
123                id,
124                initial_shared_version,
125                mutable: is_mutable_ref,
126            },
127            Owner::Address(_) | Owner::Object(_) | Owner::Immutable => {
128                ObjectArg::ImmOrOwnedObject(obj_ref)
129            }
130            _ => unimplemented!("a new Owner enum variant was added and needs to be handled"),
131        })
132    }
133
134    /// Resolve a [`ResolvedCallArg`] to a [`CallArg`] or a list of
135    /// [`ObjectArg`] for object vectors.
136    async fn resolved_call_arg_to_call_arg(
137        &self,
138        resolved_arg: ResolvedCallArg,
139        param: &SignatureToken,
140        module: &CompiledModule,
141    ) -> Result<ResolvedCallArgResult, anyhow::Error> {
142        match resolved_arg {
143            ResolvedCallArg::Pure(bytes) => {
144                Ok(ResolvedCallArgResult::CallArg(CallArg::Pure(bytes)))
145            }
146            ResolvedCallArg::Object(id) => {
147                let is_mutable =
148                    matches!(param, SignatureToken::MutableReference(_)) || !param.is_reference();
149                let object_arg = self.get_object_arg(id, is_mutable, module, param).await?;
150                Ok(ResolvedCallArgResult::CallArg(CallArg::Object(object_arg)))
151            }
152            ResolvedCallArg::ObjVec(vec_ids) => {
153                let mut object_args = Vec::with_capacity(vec_ids.len());
154                for id in vec_ids {
155                    object_args.push(self.get_object_arg(id, false, module, param).await?);
156                }
157                Ok(ResolvedCallArgResult::ObjVec(object_args))
158            }
159        }
160    }
161
162    /// Resolve a single JSON value to a [`ResolvedCallArgResult`].
163    async fn resolve_json_value_to_call_arg(
164        &self,
165        module: &CompiledModule,
166        type_args: &[TypeTag],
167        value: IotaJsonValue,
168        param: &SignatureToken,
169        idx: usize,
170    ) -> Result<ResolvedCallArgResult, anyhow::Error> {
171        let json_slice = [value];
172        let param_slice = [param.clone()];
173        let resolved = resolve_call_args(module, type_args, &json_slice, &param_slice)?;
174        let resolved_arg = resolved
175            .into_iter()
176            .next()
177            .ok_or_else(|| anyhow!("Unable to resolve argument at index {idx}"))?;
178        self.resolved_call_arg_to_call_arg(resolved_arg, param, module)
179            .await
180    }
181
182    /// Convert provided JSON arguments for a move function to their
183    /// [`Argument`] representation and check their validity.
184    pub async fn resolve_and_checks_json_args(
185        &self,
186        builder: &mut ProgrammableTransactionBuilder,
187        package_id: ObjectID,
188        module_ident: &Identifier,
189        function_ident: &Identifier,
190        type_args: &[TypeTag],
191        json_args: Vec<IotaJsonValue>,
192    ) -> Result<Vec<Argument>, anyhow::Error> {
193        // Fetch the move package for the given package ID.
194        let package = self.fetch_move_package(package_id).await?;
195        let module = package.deserialize_module(module_ident, &BinaryConfig::standard())?;
196
197        // Then resolve the function parameters type.
198        let json_args_and_tokens = resolve_move_function_args(
199            &package,
200            module_ident.to_owned(),
201            function_ident.to_owned(),
202            type_args,
203            json_args,
204        )?;
205
206        // Finally construct the input arguments for the builder.
207        let mut args = Vec::new();
208        for (arg, expected_type) in json_args_and_tokens {
209            let result = self
210                .resolved_call_arg_to_call_arg(arg, &expected_type, &module)
211                .await?;
212            args.push(match result {
213                ResolvedCallArgResult::CallArg(call_arg) => builder.input(call_arg)?,
214                ResolvedCallArgResult::ObjVec(object_args) => builder.make_obj_vec(object_args)?,
215            });
216        }
217
218        Ok(args)
219    }
220
221    /// Convert provided PtbInput's for a move function to their
222    /// [`Argument`] representation and check their validity.
223    pub async fn resolve_and_check_call_args(
224        &self,
225        builder: &mut ProgrammableTransactionBuilder,
226        package_id: ObjectID,
227        module: &Identifier,
228        function: &Identifier,
229        type_args: &[TypeTag],
230        call_args: Vec<PtbInput>,
231    ) -> Result<Vec<Argument>, anyhow::Error> {
232        let package = self.fetch_move_package(package_id).await?;
233        let module_compiled = package.deserialize_module(module, &BinaryConfig::standard())?;
234        let parameters = get_function_parameters(&module_compiled, function)?;
235        let expected_len = expected_arg_count(&module_compiled, parameters);
236
237        if call_args.len() != expected_len {
238            bail!("Expected {expected_len} args, found {}", call_args.len());
239        }
240
241        let mut arguments = Vec::with_capacity(expected_len);
242
243        for (idx, (arg, param)) in call_args
244            .into_iter()
245            .zip(parameters.iter().take(expected_len))
246            .enumerate()
247        {
248            let argument = match arg {
249                PtbInput::CallArg(value) => {
250                    let resolved_arg = self
251                        .resolve_json_value_to_call_arg(
252                            &module_compiled,
253                            type_args,
254                            value,
255                            param,
256                            idx,
257                        )
258                        .await?;
259                    match resolved_arg {
260                        ResolvedCallArgResult::CallArg(call_arg) => builder.input(call_arg)?,
261                        ResolvedCallArgResult::ObjVec(object_args) => {
262                            builder.make_obj_vec(object_args)?
263                        }
264                    }
265                }
266                PtbInput::PtbRef(iota_arg) => match iota_arg {
267                    IotaArgument::GasCoin => Argument::Gas,
268                    IotaArgument::Input(idx) => Argument::Input(idx),
269                    IotaArgument::Result(idx) => Argument::Result(idx),
270                    IotaArgument::NestedResult(idx, nested_idx) => {
271                        Argument::NestedResult(idx, nested_idx)
272                    }
273                },
274            };
275
276            arguments.push(argument);
277        }
278
279        Ok(arguments)
280    }
281
282    /// Convert provided JSON arguments for a move function to their
283    /// [`Argument`] representation and check their validity. Also, check that
284    /// the passed function is compliant to the Move View
285    /// Function specification.
286    pub async fn resolve_and_checks_json_view_args(
287        &self,
288        builder: &mut ProgrammableTransactionBuilder,
289        package_id: ObjectID,
290        module_ident: &Identifier,
291        function_ident: &Identifier,
292        type_args: &[TypeTag],
293        json_args: Vec<IotaJsonValue>,
294    ) -> Result<Vec<Argument>, anyhow::Error> {
295        // Fetch the move package for the given package ID.
296        let package = self.fetch_move_package(package_id).await?;
297        let module = package.deserialize_module(module_ident, &BinaryConfig::standard())?;
298
299        // Extract the expected function signature and check the return type.
300        // If the function is a view function, it MUST return at least a value.
301        check_function_has_a_return(&module, function_ident)?;
302
303        // Then resolve the function parameters type.
304        let json_args_and_tokens = resolve_move_function_args(
305            &package,
306            module_ident.clone(),
307            function_ident.clone(),
308            type_args,
309            json_args,
310        )?;
311
312        // Finally construct the input arguments for the builder.
313        let mut args = Vec::new();
314        for (arg, expected_type) in json_args_and_tokens {
315            args.push(match arg {
316                // Move View Functions can accept pure arguments.
317                ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)),
318                // Move View Functions can accept only immutable object references.
319                ResolvedCallArg::Object(id) => {
320                    fp_ensure!(
321                            matches!(expected_type, SignatureToken::Reference(_)),
322                            UserInputError::InvalidMoveViewFunction {
323                                error: format!("Found a function parameter which is not an immutable reference {expected_type:?}")
324                                    ,
325                            }
326                            .into()
327                        );
328                    builder.input(CallArg::Object(
329                        self.get_object_arg(
330                            id,
331                            // Setting false is safe because of fp_ensure! above
332                            false,
333                            &module,
334                            &expected_type,
335                        )
336                        .await?,
337                    ))
338                }
339                // Move View Functions can not accept vector of object by value (this case).
340                ResolvedCallArg::ObjVec(_) => Err(UserInputError::InvalidMoveViewFunction {
341                    error: "Found a function parameter which is a vector of objects".to_owned(),
342                }
343                .into()),
344            }?);
345        }
346
347        Ok(args)
348    }
349
350    /// Convert provided JSON arguments for a move function to their
351    /// [`CallArg`] representation and check their validity.
352    ///
353    /// Note: For object vectors, each object is added as a separate
354    /// `CallArg::Object` entry.
355    pub async fn resolve_and_check_json_args_to_call_args(
356        &self,
357        package_id: ObjectID,
358        module: &Identifier,
359        function: &Identifier,
360        type_args: &[TypeTag],
361        call_args: Vec<IotaJsonValue>,
362    ) -> Result<Vec<CallArg>, anyhow::Error> {
363        let package = self.fetch_move_package(package_id).await?;
364        let module_compiled = package.deserialize_module(module, &BinaryConfig::standard())?;
365        let parameters = get_function_parameters(&module_compiled, function)?;
366        let expected_len = expected_arg_count(&module_compiled, parameters);
367
368        let mut arguments = Vec::with_capacity(expected_len);
369
370        for (idx, (value, param)) in call_args
371            .into_iter()
372            .zip(parameters.iter().take(expected_len))
373            .enumerate()
374        {
375            let resolved_arg = self
376                .resolve_json_value_to_call_arg(&module_compiled, type_args, value, param, idx)
377                .await?;
378
379            match resolved_arg {
380                ResolvedCallArgResult::CallArg(call_arg) => arguments.push(call_arg),
381                ResolvedCallArgResult::ObjVec(object_args) => {
382                    // For object vectors, add each object as a separate CallArg::Object entry
383                    for obj_arg in object_args {
384                        arguments.push(CallArg::Object(obj_arg));
385                    }
386                }
387            }
388        }
389
390        Ok(arguments)
391    }
392
393    /// Get the latest object ref for an object.
394    pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
395        // TODO: we should add retrial to reduce the transaction building error rate
396        self.get_object_ref_and_type(object_id)
397            .await
398            .map(|(oref, _)| oref)
399    }
400
401    /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber,
402    /// ObjectDigest) and ObjectType for a provided ObjectID.
403    pub(crate) async fn get_object_ref_and_type(
404        &self,
405        object_id: ObjectID,
406    ) -> anyhow::Result<(ObjectRef, ObjectType)> {
407        let object = self
408            .0
409            .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type())
410            .await?
411            .into_object()?;
412
413        Ok((object.object_ref(), object.object_type()?))
414    }
415
416    /// Helper function to get a Move Package for a provided ObjectID.
417    async fn fetch_move_package(&self, package_id: ObjectID) -> Result<MovePackage, anyhow::Error> {
418        let object = self
419            .0
420            .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless())
421            .await?
422            .into_object()?;
423        let Some(IotaRawData::Package(package)) = object.bcs else {
424            bail!("Bcs field in object [{package_id}] is missing or not a package.");
425        };
426        Ok(MovePackage::new(
427            package.id,
428            object.version,
429            package.module_map,
430            ProtocolConfig::get_for_min_version().max_move_package_size(),
431            package.type_origin_table,
432            package.linkage_table,
433        )?)
434    }
435}
436
437/// Helper function to check if the provided function within a module has at
438/// least a return type.
439fn check_function_has_a_return(
440    module: &CompiledModule,
441    function_ident: &Identifier,
442) -> Result<(), anyhow::Error> {
443    let (_, fdef) = module
444        .find_function_def_by_name(function_ident.as_str())
445        .ok_or_else(|| {
446            anyhow!(
447                "Could not resolve function {} in module {}",
448                function_ident,
449                module.self_id()
450            )
451        })?;
452    let function_signature = module.function_handle_at(fdef.function);
453    fp_ensure!(
454        !&module.signature_at(function_signature.return_).is_empty(),
455        UserInputError::InvalidMoveViewFunction {
456            error: "No return type for this function".to_owned(),
457        }
458        .into()
459    );
460    Ok(())
461}
462
463/// Result of resolving a call argument, distinguishing between single
464/// [`CallArg`] and object vectors.
465enum ResolvedCallArgResult {
466    CallArg(CallArg),
467    ObjVec(Vec<ObjectArg>),
468}
469
470/// Get function parameters from a compiled module, excluding TxContext.
471fn get_function_parameters<'a>(
472    module: &'a CompiledModule,
473    function: &Identifier,
474) -> Result<&'a [SignatureToken], anyhow::Error> {
475    let function_str = function.as_str();
476    let function_def = module
477        .function_defs
478        .iter()
479        .find(|function_def| {
480            module
481                .identifier_at(module.function_handle_at(function_def.function).name)
482                .as_str()
483                == function_str
484        })
485        .ok_or_else(|| {
486            anyhow!(
487                "Could not resolve function {function} in module {}",
488                module.self_id()
489            )
490        })?;
491    let function_signature = module.function_handle_at(function_def.function);
492    Ok(&module.signature_at(function_signature.parameters).0)
493}
494
495/// Calculate expected argument count, excluding TxContext if present.
496fn expected_arg_count(module: &CompiledModule, parameters: &[SignatureToken]) -> usize {
497    match parameters.last() {
498        Some(param) if TxContext::kind(module, param) != TxContextKind::None => {
499            parameters.len() - 1
500        }
501        _ => parameters.len(),
502    }
503}