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::{collections::BTreeMap, result::Result};
6
7use anyhow::{Ok, anyhow, bail};
8use futures::future::join_all;
9use iota_json::{
10    IotaJsonValue, ResolvedCallArg, is_receiving_argument, resolve_move_function_args,
11};
12use iota_json_rpc_types::{IotaData, IotaObjectDataOptions, IotaRawData};
13use iota_protocol_config::ProtocolConfig;
14use iota_types::{
15    base_types::{IotaAddress, ObjectID, ObjectRef, ObjectType},
16    gas_coin::GasCoin,
17    move_package::MovePackage,
18    object::{Object, Owner},
19    programmable_transaction_builder::ProgrammableTransactionBuilder,
20    transaction::{Argument, CallArg, ObjectArg},
21};
22use move_binary_format::{
23    CompiledModule, binary_config::BinaryConfig, file_format::SignatureToken,
24};
25use move_core_types::{identifier::Identifier, language_storage::TypeTag};
26
27use crate::TransactionBuilder;
28
29impl TransactionBuilder {
30    /// Select a gas coin for the provided gas budget.
31    pub(crate) async fn select_gas(
32        &self,
33        signer: IotaAddress,
34        input_gas: impl Into<Option<ObjectID>>,
35        gas_budget: u64,
36        input_objects: Vec<ObjectID>,
37        gas_price: u64,
38    ) -> Result<ObjectRef, anyhow::Error> {
39        if gas_budget < gas_price {
40            bail!(
41                "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}."
42            )
43        }
44        if let Some(gas) = input_gas.into() {
45            self.get_object_ref(gas).await
46        } else {
47            let gas_objs = self.0.get_owned_objects(signer, GasCoin::type_()).await?;
48
49            for obj in gas_objs {
50                let response = self
51                    .0
52                    .get_object_with_options(obj.object_id, IotaObjectDataOptions::new().with_bcs())
53                    .await?;
54                let obj = response.object()?;
55                let gas: GasCoin = bcs::from_bytes(
56                    &obj.bcs
57                        .as_ref()
58                        .ok_or_else(|| anyhow!("bcs field is unexpectedly empty"))?
59                        .try_as_move()
60                        .ok_or_else(|| anyhow!("Cannot parse move object to gas object"))?
61                        .bcs_bytes,
62                )?;
63                if !input_objects.contains(&obj.object_id) && gas.value() >= gas_budget {
64                    return Ok(obj.object_ref());
65                }
66            }
67            Err(anyhow!(
68                "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."
69            ))
70        }
71    }
72
73    /// Get the object references for a list of object IDs
74    pub async fn input_refs(&self, obj_ids: &[ObjectID]) -> Result<Vec<ObjectRef>, anyhow::Error> {
75        let handles: Vec<_> = obj_ids.iter().map(|id| self.get_object_ref(*id)).collect();
76        let obj_refs = join_all(handles)
77            .await
78            .into_iter()
79            .collect::<anyhow::Result<Vec<ObjectRef>>>()?;
80        Ok(obj_refs)
81    }
82
83    /// Resolve a provided [`ObjectID`] to the required [`ObjectArg`] for a
84    /// given move module.
85    async fn get_object_arg(
86        &self,
87        id: ObjectID,
88        objects: &mut BTreeMap<ObjectID, Object>,
89        is_mutable_ref: bool,
90        view: &CompiledModule,
91        arg_type: &SignatureToken,
92    ) -> Result<ObjectArg, anyhow::Error> {
93        let response = self
94            .0
95            .get_object_with_options(id, IotaObjectDataOptions::bcs_lossless())
96            .await?;
97
98        let obj: Object = response.into_object()?.try_into()?;
99        let obj_ref = obj.compute_object_reference();
100        let owner = obj.owner;
101        objects.insert(id, obj);
102        if is_receiving_argument(view, arg_type) {
103            return Ok(ObjectArg::Receiving(obj_ref));
104        }
105        Ok(match owner {
106            Owner::Shared {
107                initial_shared_version,
108            } => ObjectArg::SharedObject {
109                id,
110                initial_shared_version,
111                mutable: is_mutable_ref,
112            },
113            Owner::AddressOwner(_) | Owner::ObjectOwner(_) | Owner::Immutable => {
114                ObjectArg::ImmOrOwnedObject(obj_ref)
115            }
116        })
117    }
118
119    /// Convert provided JSON arguments for a move function to their
120    /// [`Argument`] representation and check their validity.
121    pub async fn resolve_and_checks_json_args(
122        &self,
123        builder: &mut ProgrammableTransactionBuilder,
124        package_id: ObjectID,
125        module: &Identifier,
126        function: &Identifier,
127        type_args: &[TypeTag],
128        json_args: Vec<IotaJsonValue>,
129    ) -> Result<Vec<Argument>, anyhow::Error> {
130        let object = self
131            .0
132            .get_object_with_options(package_id, IotaObjectDataOptions::bcs_lossless())
133            .await?
134            .into_object()?;
135        let Some(IotaRawData::Package(package)) = object.bcs else {
136            bail!(
137                "Bcs field in object [{}] is missing or not a package.",
138                package_id
139            );
140        };
141        let package: MovePackage = MovePackage::new(
142            package.id,
143            object.version,
144            package.module_map,
145            ProtocolConfig::get_for_min_version().max_move_package_size(),
146            package.type_origin_table,
147            package.linkage_table,
148        )?;
149
150        let json_args_and_tokens = resolve_move_function_args(
151            &package,
152            module.clone(),
153            function.clone(),
154            type_args,
155            json_args,
156        )?;
157
158        let mut args = Vec::new();
159        let mut objects = BTreeMap::new();
160        let module = package.deserialize_module(module, &BinaryConfig::standard())?;
161        for (arg, expected_type) in json_args_and_tokens {
162            args.push(match arg {
163                ResolvedCallArg::Pure(p) => builder.input(CallArg::Pure(p)),
164
165                ResolvedCallArg::Object(id) => builder.input(CallArg::Object(
166                    self.get_object_arg(
167                        id,
168                        &mut objects,
169                        // Is mutable if passed by mutable reference or by value
170                        matches!(expected_type, SignatureToken::MutableReference(_))
171                            || !expected_type.is_reference(),
172                        &module,
173                        &expected_type,
174                    )
175                    .await?,
176                )),
177
178                ResolvedCallArg::ObjVec(v) => {
179                    let mut object_ids = vec![];
180                    for id in v {
181                        object_ids.push(
182                            self.get_object_arg(
183                                id,
184                                &mut objects,
185                                // is_mutable_ref
186                                false,
187                                &module,
188                                &expected_type,
189                            )
190                            .await?,
191                        )
192                    }
193                    builder.make_obj_vec(object_ids)
194                }
195            }?);
196        }
197
198        Ok(args)
199    }
200
201    /// Get the latest object ref for an object.
202    pub async fn get_object_ref(&self, object_id: ObjectID) -> anyhow::Result<ObjectRef> {
203        // TODO: we should add retrial to reduce the transaction building error rate
204        self.get_object_ref_and_type(object_id)
205            .await
206            .map(|(oref, _)| oref)
207    }
208
209    /// Helper function to get the latest ObjectRef (ObjectID, SequenceNumber,
210    /// ObjectDigest) and ObjectType for a provided ObjectID.
211    pub(crate) async fn get_object_ref_and_type(
212        &self,
213        object_id: ObjectID,
214    ) -> anyhow::Result<(ObjectRef, ObjectType)> {
215        let object = self
216            .0
217            .get_object_with_options(object_id, IotaObjectDataOptions::new().with_type())
218            .await?
219            .into_object()?;
220
221        Ok((object.object_ref(), object.object_type()?))
222    }
223}