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