Skip to main content

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, MovePackageExt},
24    object::{Object, Owner},
25    programmable_transaction_builder::ProgrammableTransactionBuilder,
26    transaction::{Argument, CallArg, SharedObjectRef},
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 [`CallArg`] 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<CallArg, 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(CallArg::Receiving(obj_ref));
120        }
121        Ok(match owner {
122            Owner::Shared(initial_shared_version) => CallArg::Shared(SharedObjectRef::new(
123                id,
124                initial_shared_version,
125                is_mutable_ref,
126            )),
127            Owner::Address(_) | Owner::Object(_) | Owner::Immutable => {
128                CallArg::ImmutableOrOwned(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    /// [`CallArg`] 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(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                // `p` is already BCS-encoded for the expected Move type.
318                ResolvedCallArg::Pure(p) => Ok(builder.pure_bytes(p, false)),
319                // Move View Functions can accept only immutable object references.
320                ResolvedCallArg::Object(id) => {
321                    fp_ensure!(
322                            matches!(expected_type, SignatureToken::Reference(_)),
323                            UserInputError::InvalidMoveViewFunction {
324                                error: format!("Found a function parameter which is not an immutable reference {expected_type:?}")
325                                    ,
326                            }
327                            .into()
328                        );
329                    builder.input(
330                        self.get_object_arg(
331                            id,
332                            // Setting false is safe because of fp_ensure! above
333                            false,
334                            &module,
335                            &expected_type,
336                        )
337                        .await?,
338                    )
339                }
340                // Move View Functions can not accept vector of object by value (this case).
341                ResolvedCallArg::ObjVec(_) => Err(UserInputError::InvalidMoveViewFunction {
342                    error: "Found a function parameter which is a vector of objects".to_owned(),
343                }
344                .into()),
345            }?);
346        }
347
348        Ok(args)
349    }
350
351    /// Convert provided JSON arguments for a move function to their
352    /// [`CallArg`] representation and check their validity.
353    ///
354    /// Note: For object vectors, each object is added as a separate
355    /// `CallArg::Object` entry.
356    pub async fn resolve_and_check_json_args_to_call_args(
357        &self,
358        package_id: ObjectID,
359        module: &Identifier,
360        function: &Identifier,
361        type_args: &[TypeTag],
362        call_args: Vec<IotaJsonValue>,
363    ) -> Result<Vec<CallArg>, anyhow::Error> {
364        let package = self.fetch_move_package(package_id).await?;
365        let module_compiled = package.deserialize_module(module, &BinaryConfig::standard())?;
366        let parameters = get_function_parameters(&module_compiled, function)?;
367        let expected_len = expected_arg_count(&module_compiled, parameters);
368
369        let mut arguments = Vec::with_capacity(expected_len);
370
371        for (idx, (value, param)) in call_args
372            .into_iter()
373            .zip(parameters.iter().take(expected_len))
374            .enumerate()
375        {
376            let resolved_arg = self
377                .resolve_json_value_to_call_arg(&module_compiled, type_args, value, param, idx)
378                .await?;
379
380            match resolved_arg {
381                ResolvedCallArgResult::CallArg(call_arg) => arguments.push(call_arg),
382                ResolvedCallArgResult::ObjVec(object_args) => {
383                    // For object vectors, add each object as a separate CallArg entry
384                    for obj_arg in object_args {
385                        arguments.push(obj_arg);
386                    }
387                }
388            }
389        }
390
391        Ok(arguments)
392    }
393
394    /// Get the latest object ref for an object.
395    pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
396        // TODO: we should add retrial to reduce the transaction building error rate
397        self.get_object_ref_and_type(object_id)
398            .await
399            .map(|(oref, _)| oref)
400    }
401
402    /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber,
403    /// ObjectDigest) and ObjectType for a provided ObjectID.
404    pub(crate) async fn get_object_ref_and_type(
405        &self,
406        object_id: ObjectID,
407    ) -> anyhow::Result<(ObjectRef, ObjectType)> {
408        let object = self
409            .0
410            .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type())
411            .await?
412            .into_object()?;
413
414        Ok((object.object_ref(), object.object_type()?))
415    }
416
417    /// Helper function to get a Move Package for a provided ObjectID.
418    async fn fetch_move_package(&self, package_id: ObjectID) -> Result<MovePackage, anyhow::Error> {
419        let object = self
420            .0
421            .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless())
422            .await?
423            .into_object()?;
424        let Some(IotaRawData::Package(package)) = object.bcs else {
425            bail!("Bcs field in object [{package_id}] is missing or not a package.");
426        };
427
428        Ok(MovePackage::new(
429            package.id,
430            object.version,
431            package
432                .module_map
433                .iter()
434                .map(|(k, v)| (Identifier::new_unchecked(k.as_str()), v.clone()))
435                .collect(),
436            ProtocolConfig::get_for_min_version().max_move_package_size(),
437            package.type_origin_table,
438            package.linkage_table,
439        )?)
440    }
441}
442
443/// Helper function to check if the provided function within a module has at
444/// least a return type.
445fn check_function_has_a_return(
446    module: &CompiledModule,
447    function_ident: &Identifier,
448) -> Result<(), anyhow::Error> {
449    let (_, fdef) = module
450        .find_function_def_by_name(function_ident.as_str())
451        .ok_or_else(|| {
452            anyhow!(
453                "Could not resolve function {} in module {}",
454                function_ident,
455                module.self_id()
456            )
457        })?;
458    let function_signature = module.function_handle_at(fdef.function);
459    fp_ensure!(
460        !&module.signature_at(function_signature.return_).is_empty(),
461        UserInputError::InvalidMoveViewFunction {
462            error: "No return type for this function".to_owned(),
463        }
464        .into()
465    );
466    Ok(())
467}
468
469/// Result of resolving a call argument, distinguishing between single
470/// [`CallArg`] and object vectors.
471enum ResolvedCallArgResult {
472    CallArg(CallArg),
473    ObjVec(Vec<CallArg>),
474}
475
476/// Get function parameters from a compiled module, excluding TxContext.
477fn get_function_parameters<'a>(
478    module: &'a CompiledModule,
479    function: &Identifier,
480) -> Result<&'a [SignatureToken], anyhow::Error> {
481    let function_str = function.as_str();
482    let function_def = module
483        .function_defs
484        .iter()
485        .find(|function_def| {
486            module
487                .identifier_at(module.function_handle_at(function_def.function).name)
488                .as_str()
489                == function_str
490        })
491        .ok_or_else(|| {
492            anyhow!(
493                "Could not resolve function {function} in module {}",
494                module.self_id()
495            )
496        })?;
497    let function_signature = module.function_handle_at(function_def.function);
498    Ok(&module.signature_at(function_signature.parameters).0)
499}
500
501/// Calculate expected argument count, excluding TxContext if present.
502fn expected_arg_count(module: &CompiledModule, parameters: &[SignatureToken]) -> usize {
503    match parameters.last() {
504        Some(param) if TxContext::kind(module, param) != TxContextKind::None => {
505            parameters.len() - 1
506        }
507        _ => parameters.len(),
508    }
509}