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 gas_objs = self.0.get_owned_objects(signer, GasCoin::type_()).await?;
51
52            for obj in gas_objs {
53                let response = self
54                    .0
55                    .get_object_with_options(obj.object_id, IotaObjectDataOptions::new().with_bcs())
56                    .await?;
57                let obj = response.object()?;
58                let gas: GasCoin = bcs::from_bytes(
59                    &obj.bcs
60                        .as_ref()
61                        .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))?
62                        .try_as_move()
63                        .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))?
64                        .bcs_bytes,
65                )?;
66                if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget {
67                    return Ok(obj.object_ref());
68                }
69            }
70            Err(anyhow!(
71                "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."
72            ))
73        }
74    }
75
76    /// Get the object references for a list of object IDs
77    pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result<Vec<ObjectRef>, anyhow::Error> {
78        let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect();
79        let obj_refs = join_all(handles)
80            .await
81            .into_iter()
82            .collect::<anyhow::Result<Vec<ObjectRef>>>()?;
83        Ok(obj_refs)
84    }
85
86    /// Resolve a provided [`ObjectID`] to the required [`ObjectArg`] for a
87    /// given move module.
88    async fn get_object_arg(
89        &self,
90        id: ObjectID,
91        is_mutable_ref: bool,
92        view: &CompiledModule,
93        arg_type: &SignatureToken,
94    ) -> Result<ObjectArg, anyhow::Error> {
95        let response = self
96            .0
97            .get_object_with_options(id, IotaObjectDataOptions::bcs_lossless())
98            .await?;
99
100        let obj: Object = response.into_object()?.try_into()?;
101        let obj_ref = obj.compute_object_reference();
102        let owner = obj.owner;
103        if is_receiving_argument(view, arg_type) {
104            return Ok(ObjectArg::Receiving(obj_ref));
105        }
106        Ok(match owner {
107            Owner::Shared {
108                initial_shared_version,
109            } => ObjectArg::SharedObject {
110                id,
111                initial_shared_version,
112                mutable: is_mutable_ref,
113            },
114            Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
115                ObjectArg::ImmOrOwnedObject(obj_ref)
116            }
117        })
118    }
119
120    /// Convert provided JSON arguments for a move function to their
121    /// [`Argument`] representation and check their validity.
122    pub async fn resolve_and_checks_json_args(
123        &self,
124        builder: &mut ProgrammableTransactionBuilder,
125        package_id: ObjectID,
126        module_ident: &Identifier,
127        function_ident: &Identifier,
128        type_args: &[TypeTag],
129        json_args: Vec<IotaJsonValue>,
130    ) -> Result<Vec<Argument>, anyhow::Error> {
131        // Fetch the move package for the given package ID.
132        let package = self.fetch_move_package(package_id).await?;
133        let module = package.deserialize_module(module_ident, &BinaryConfig::standard())?;
134
135        // Then resolve the function parameters type.
136        let json_args_and_tokens = resolve_move_function_args(
137            &package,
138            module_ident.clone(),
139            function_ident.clone(),
140            type_args,
141            json_args,
142        )?;
143
144        // Finally construct the input arguments for the builder.
145        let mut args = Vec::new();
146        for (arg, expected_type) in json_args_and_tokens {
147            args.push(match arg {
148                ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)),
149                ResolvedCallArg::Object(id) => builder.input(CallArg::Object(
150                    self.get_object_arg(
151                        id,
152                        // Is mutable if passed by mutable reference or by value
153                        matches!(expected_type, SignatureToken::MutableReference(_))
154                            || !expected_type.is_reference(),
155                        &module,
156                        &expected_type,
157                    )
158                    .await?,
159                )),
160                ResolvedCallArg::ObjVec(v) => {
161                    let mut object_ids = vec![];
162                    for id in v {
163                        object_ids.push(
164                            self.get_object_arg(
165                                id,
166                                // is_mutable_ref
167                                false,
168                                &module,
169                                &expected_type,
170                            )
171                            .await?,
172                        )
173                    }
174                    builder.make_obj_vec(object_ids)
175                }
176            }?);
177        }
178
179        Ok(args)
180    }
181
182    /// Convert provided PtbInput's for a move function to their
183    /// [`Argument`] representation and check their validity.
184    pub async fn resolve_and_check_call_args(
185        &self,
186        builder: &mut ProgrammableTransactionBuilder,
187        package_id: ObjectID,
188        module: &Identifier,
189        function: &Identifier,
190        type_args: &[TypeTag],
191        call_args: Vec<PtbInput>,
192    ) -> Result<Vec<Argument>, anyhow::Error> {
193        let package = self.fetch_move_package(package_id).await?;
194
195        let module_compiled = package.deserialize_module(module, &BinaryConfig::standard())?;
196        let function_str = function.as_ident_str();
197        let function_def = module_compiled
198            .function_defs
199            .iter()
200            .find(|function_def| {
201                module_compiled.identifier_at(
202                    module_compiled
203                        .function_handle_at(function_def.function)
204                        .name,
205                ) == function_str
206            })
207            .ok_or_else(|| anyhow!("Could not resolve function {function} in module {module}"))?;
208        let function_signature = module_compiled.function_handle_at(function_def.function);
209        let parameters = &module_compiled
210            .signature_at(function_signature.parameters)
211            .0;
212
213        let expected_len = match parameters.last() {
214            Some(param) if TxContext::kind(&module_compiled, param) != TxContextKind::None => {
215                parameters.len() - 1
216            }
217            _ => parameters.len(),
218        };
219
220        if call_args.len() != expected_len {
221            bail!("Expected {expected_len} args, found {}", call_args.len());
222        }
223
224        let mut arguments = Vec::with_capacity(expected_len);
225
226        for (idx, (arg, param)) in call_args
227            .into_iter()
228            .zip(parameters.iter().take(expected_len))
229            .enumerate()
230        {
231            let argument = match arg {
232                PtbInput::CallArg(value) => {
233                    let json_slice = [value];
234                    let param_slice = [param.clone()];
235                    let resolved =
236                        resolve_call_args(&module_compiled, type_args, &json_slice, &param_slice)?;
237                    let resolved_arg = resolved
238                        .into_iter()
239                        .next()
240                        .ok_or_else(|| anyhow!("Unable to resolve pure argument at index {idx}"))?;
241
242                    match resolved_arg {
243                        ResolvedCallArg::Pure(bytes) => builder.input(CallArg::Pure(bytes))?,
244                        ResolvedCallArg::Object(id) => {
245                            let is_mutable = match param {
246                                SignatureToken::MutableReference(_) => true,
247                                _ => !param.is_reference(),
248                            };
249                            let object_arg = self
250                                .get_object_arg(id, is_mutable, &module_compiled, param)
251                                .await?;
252                            builder.input(CallArg::Object(object_arg))?
253                        }
254                        ResolvedCallArg::ObjVec(vec_ids) => {
255                            let mut object_args = Vec::with_capacity(vec_ids.len());
256                            for id in vec_ids {
257                                object_args.push(
258                                    self.get_object_arg(id, false, &module_compiled, param)
259                                        .await?,
260                                );
261                            }
262                            builder.make_obj_vec(object_args)?
263                        }
264                    }
265                }
266                PtbInput::PtbRef(iota_arg) => match iota_arg {
267                    IotaArgument::GasCoin => Argument::GasCoin,
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                                    .to_owned(),
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    /// Get the latest object ref for an object.
351    pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
352        // TODO: we should add retrial to reduce the transaction building error rate
353        self.get_object_ref_and_type(object_id)
354            .await
355            .map(|(oref, _)| oref)
356    }
357
358    /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber,
359    /// ObjectDigest) and ObjectType for a provided ObjectID.
360    pub(crate) async fn get_object_ref_and_type(
361        &self,
362        object_id: ObjectID,
363    ) -> anyhow::Result<(ObjectRef, ObjectType)> {
364        let object = self
365            .0
366            .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type())
367            .await?
368            .into_object()?;
369
370        Ok((object.object_ref(), object.object_type()?))
371    }
372
373    /// Helper function to get a Move Package for a provided ObjectID.
374    async fn fetch_move_package(&self, package_id: ObjectID) -> Result<MovePackage, anyhow::Error> {
375        let object = self
376            .0
377            .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless())
378            .await?
379            .into_object()?;
380        let Some(IotaRawData::Package(package)) = object.bcs else {
381            bail!("Bcs field in object [{package_id}] is missing or not a package.");
382        };
383        Ok(MovePackage::new(
384            package.id,
385            object.version,
386            package.module_map,
387            ProtocolConfig::get_for_min_version().max_move_package_size(),
388            package.type_origin_table,
389            package.linkage_table,
390        )?)
391    }
392}
393
394/// Helper function to check if the provided function within a module has at
395/// least a return type.
396fn check_function_has_a_return(
397    module: &CompiledModule,
398    function_ident: &Identifier,
399) -> Result<(), anyhow::Error> {
400    let (_, fdef) = module
401        .find_function_def_by_name(function_ident.as_str())
402        .ok_or_else(|| {
403            anyhow!(
404                "Could not resolve function {} in module {}",
405                function_ident,
406                module.self_id()
407            )
408        })?;
409    let function_signature = module.function_handle_at(fdef.function);
410    fp_ensure!(
411        !&module.signature_at(function_signature.return_).is_empty(),
412        UserInputError::InvalidMoveViewFunction {
413            error: "No return type for this function".to_owned(),
414        }
415        .into()
416    );
417    Ok(())
418}