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