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