iota_sdk/
wallet_context.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::BTreeSet, path::Path, sync::Arc};
6
7use anyhow::{anyhow, bail};
8use colored::Colorize;
9use futures::{StreamExt, TryStreamExt};
10use getset::{Getters, MutGetters};
11use iota_config::{Config, PersistedConfig};
12use iota_json_rpc_types::{
13    IotaObjectData, IotaObjectDataFilter, IotaObjectDataOptions, IotaObjectResponseQuery,
14    IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions,
15};
16use iota_keys::keystore::AccountKeystore;
17use iota_types::{
18    base_types::{IotaAddress, ObjectID, ObjectRef},
19    crypto::IotaKeyPair,
20    gas_coin::GasCoin,
21    transaction::{Transaction, TransactionData, TransactionDataAPI},
22};
23use shared_crypto::intent::Intent;
24use tokio::sync::RwLock;
25use tracing::warn;
26
27use crate::{
28    IotaClient, PagedFn,
29    iota_client_config::{IotaClientConfig, IotaEnv},
30};
31
32/// Wallet for managing accounts, objects, and interact with client APIs.
33// Mainly used in the CLI and tests.
34#[derive(Getters, MutGetters)]
35#[getset(get = "pub", get_mut = "pub")]
36pub struct WalletContext {
37    config: PersistedConfig<IotaClientConfig>,
38    request_timeout: Option<std::time::Duration>,
39    client: Arc<RwLock<Option<IotaClient>>>,
40    max_concurrent_requests: Option<u64>,
41}
42
43impl WalletContext {
44    /// Create a new [`WalletContext`] with the config path to an existing
45    /// [`IotaClientConfig`] and optional parameters for the client.
46    pub fn new(
47        config_path: &Path,
48        request_timeout: impl Into<Option<std::time::Duration>>,
49        max_concurrent_requests: impl Into<Option<u64>>,
50    ) -> Result<Self, anyhow::Error> {
51        let config: IotaClientConfig = PersistedConfig::read(config_path).map_err(|err| {
52            anyhow!(
53                "Cannot open wallet config file at {:?}. Err: {err}",
54                config_path
55            )
56        })?;
57
58        let config = config.persisted(config_path);
59        let context = Self {
60            config,
61            request_timeout: request_timeout.into(),
62            client: Default::default(),
63            max_concurrent_requests: max_concurrent_requests.into(),
64        };
65        Ok(context)
66    }
67
68    /// Get all addresses from the keystore.
69    pub fn get_addresses(&self) -> Vec<IotaAddress> {
70        self.config.keystore.addresses()
71    }
72
73    /// Get the configured [`IotaClient`].
74    pub async fn get_client(&self) -> Result<IotaClient, anyhow::Error> {
75        let read = self.client.read().await;
76
77        Ok(if let Some(client) = read.as_ref() {
78            client.clone()
79        } else {
80            drop(read);
81            let client = self
82                .active_env()?
83                .create_rpc_client(self.request_timeout, self.max_concurrent_requests)
84                .await?;
85            if let Err(e) = client.check_api_version() {
86                warn!("{e}");
87                eprintln!("{}", format!("[warn] {e}").yellow().bold());
88            }
89            self.client.write().await.insert(client).clone()
90        })
91    }
92
93    /// Get the active [`IotaAddress`].
94    /// If not set, defaults to the first address in the keystore.
95    pub fn active_address(&self) -> Result<IotaAddress, anyhow::Error> {
96        if self.config.keystore.addresses().is_empty() {
97            bail!("No managed addresses. Create new address with the `new-address` command.");
98        }
99
100        Ok(if let Some(addr) = self.config.active_address() {
101            *addr
102        } else {
103            self.config.keystore().addresses()[0]
104        })
105    }
106
107    /// Get the active [`IotaEnv`].
108    /// If not set, defaults to the first environment in the config.
109    pub fn active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
110        if self.config.envs.is_empty() {
111            bail!("No managed environments. Create new environment with the `new-env` command.");
112        }
113
114        Ok(if self.config.active_env().is_some() {
115            self.config.get_active_env()?
116        } else {
117            &self.config.envs()[0]
118        })
119    }
120
121    /// Get the latest object reference given a object id.
122    pub async fn get_object_ref(&self, object_id: ObjectID) -> Result<ObjectRef, anyhow::Error> {
123        let client = self.get_client().await?;
124        Ok(client
125            .read_api()
126            .get_object_with_options(object_id, IotaObjectDataOptions::new())
127            .await?
128            .into_object()?
129            .object_ref())
130    }
131
132    /// Get all the gas objects (and conveniently, gas amounts) for the address.
133    pub async fn gas_objects(
134        &self,
135        address: IotaAddress,
136    ) -> Result<Vec<(u64, IotaObjectData)>, anyhow::Error> {
137        let client = self.get_client().await?;
138
139        let values_objects = PagedFn::stream(async |cursor| {
140            client
141                .read_api()
142                .get_owned_objects(
143                    address,
144                    IotaObjectResponseQuery::new(
145                        Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
146                        Some(IotaObjectDataOptions::full_content()),
147                    ),
148                    cursor,
149                    None,
150                )
151                .await
152        })
153        .filter_map(|res| async {
154            match res {
155                Ok(res) => {
156                    if let Some(o) = res.data {
157                        match GasCoin::try_from(&o) {
158                            Ok(gas_coin) => Some(Ok((gas_coin.value(), o.clone()))),
159                            Err(e) => Some(Err(anyhow!("{e}"))),
160                        }
161                    } else {
162                        None
163                    }
164                }
165                Err(e) => Some(Err(anyhow!("{e}"))),
166            }
167        })
168        .try_collect::<Vec<_>>()
169        .await?;
170
171        Ok(values_objects)
172    }
173
174    /// Get the address that owns the object of the provided [`ObjectID`].
175    pub async fn get_object_owner(&self, id: &ObjectID) -> Result<IotaAddress, anyhow::Error> {
176        let client = self.get_client().await?;
177        let object = client
178            .read_api()
179            .get_object_with_options(*id, IotaObjectDataOptions::new().with_owner())
180            .await?
181            .into_object()?;
182        Ok(object
183            .owner
184            .ok_or_else(|| anyhow!("Owner field is None"))?
185            .get_owner_address()?)
186    }
187
188    /// Get the address that owns the object, if an [`ObjectID`] is provided.
189    pub async fn try_get_object_owner(
190        &self,
191        id: &Option<ObjectID>,
192    ) -> Result<Option<IotaAddress>, anyhow::Error> {
193        if let Some(id) = id {
194            Ok(Some(self.get_object_owner(id).await?))
195        } else {
196            Ok(None)
197        }
198    }
199
200    /// Find a gas object which fits the budget.
201    pub async fn gas_for_owner_budget(
202        &self,
203        address: IotaAddress,
204        budget: u64,
205        forbidden_gas_objects: BTreeSet<ObjectID>,
206    ) -> Result<(u64, IotaObjectData), anyhow::Error> {
207        for o in self.gas_objects(address).await? {
208            if o.0 >= budget && !forbidden_gas_objects.contains(&o.1.object_id) {
209                return Ok((o.0, o.1));
210            }
211        }
212        bail!(
213            "No non-argument gas objects found for this address with value >= budget {budget}. Run iota client gas to check for gas objects."
214        )
215    }
216
217    /// Get the [`ObjectRef`] for gas objects owned by the provided address.
218    /// Maximum is RPC_QUERY_MAX_RESULT_LIMIT (50 by default).
219    pub async fn get_all_gas_objects_owned_by_address(
220        &self,
221        address: IotaAddress,
222    ) -> anyhow::Result<Vec<ObjectRef>> {
223        self.get_gas_objects_owned_by_address(address, None).await
224    }
225
226    /// Get a limited amount of [`ObjectRef`]s for gas objects owned by the
227    /// provided address. Max limit is RPC_QUERY_MAX_RESULT_LIMIT (50 by
228    /// default).
229    pub async fn get_gas_objects_owned_by_address(
230        &self,
231        address: IotaAddress,
232        limit: impl Into<Option<usize>>,
233    ) -> anyhow::Result<Vec<ObjectRef>> {
234        let client = self.get_client().await?;
235        let results: Vec<_> = client
236            .read_api()
237            .get_owned_objects(
238                address,
239                IotaObjectResponseQuery::new(
240                    Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
241                    Some(IotaObjectDataOptions::full_content()),
242                ),
243                None,
244                limit,
245            )
246            .await?
247            .data
248            .into_iter()
249            .filter_map(|r| r.data.map(|o| o.object_ref()))
250            .collect();
251        Ok(results)
252    }
253
254    /// Given an address, return one gas object owned by this address.
255    /// The actual implementation just returns the first one returned by the
256    /// read api.
257    pub async fn get_one_gas_object_owned_by_address(
258        &self,
259        address: IotaAddress,
260    ) -> anyhow::Result<Option<ObjectRef>> {
261        Ok(self
262            .get_gas_objects_owned_by_address(address, 1)
263            .await?
264            .pop())
265    }
266
267    /// Return one address and all gas objects owned by that address.
268    pub async fn get_one_account(&self) -> anyhow::Result<(IotaAddress, Vec<ObjectRef>)> {
269        let address = self.get_addresses().pop().unwrap();
270        Ok((
271            address,
272            self.get_all_gas_objects_owned_by_address(address).await?,
273        ))
274    }
275
276    /// Return a gas object owned by an arbitrary address managed by the wallet.
277    pub async fn get_one_gas_object(&self) -> anyhow::Result<Option<(IotaAddress, ObjectRef)>> {
278        for address in self.get_addresses() {
279            if let Some(gas_object) = self.get_one_gas_object_owned_by_address(address).await? {
280                return Ok(Some((address, gas_object)));
281            }
282        }
283        Ok(None)
284    }
285
286    /// Return all the account addresses managed by the wallet and their owned
287    /// gas objects.
288    pub async fn get_all_accounts_and_gas_objects(
289        &self,
290    ) -> anyhow::Result<Vec<(IotaAddress, Vec<ObjectRef>)>> {
291        let mut result = vec![];
292        for address in self.get_addresses() {
293            let objects = self
294                .gas_objects(address)
295                .await?
296                .into_iter()
297                .map(|(_, o)| o.object_ref())
298                .collect();
299            result.push((address, objects));
300        }
301        Ok(result)
302    }
303
304    pub async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
305        let client = self.get_client().await?;
306        let gas_price = client.governance_api().get_reference_gas_price().await?;
307        Ok(gas_price)
308    }
309
310    /// Add an account.
311    pub fn add_account(&mut self, alias: impl Into<Option<String>>, keypair: IotaKeyPair) {
312        self.config.keystore.add_key(alias.into(), keypair).unwrap();
313    }
314
315    /// Sign a transaction with a key currently managed by the WalletContext.
316    pub fn sign_transaction(&self, data: &TransactionData) -> Transaction {
317        let sig = self
318            .config
319            .keystore
320            .sign_secure(&data.sender(), data, Intent::iota_transaction())
321            .unwrap();
322        // TODO: To support sponsored transaction, we should also look at the gas owner.
323        Transaction::from_data(data.clone(), vec![sig])
324    }
325
326    /// Execute a transaction and wait for it to be locally executed on the
327    /// fullnode. Also expects the effects status to be
328    /// ExecutionStatus::Success.
329    pub async fn execute_transaction_must_succeed(
330        &self,
331        tx: Transaction,
332    ) -> IotaTransactionBlockResponse {
333        tracing::debug!("Executing transaction: {:?}", tx);
334        let response = self.execute_transaction_may_fail(tx).await.unwrap();
335        assert!(
336            response.status_ok().unwrap(),
337            "Transaction failed: {:?}",
338            response
339        );
340        response
341    }
342
343    /// Execute a transaction and wait for it to be locally executed on the
344    /// fullnode. The transaction execution is not guaranteed to succeed and
345    /// may fail. This is usually only needed in non-test environment or the
346    /// caller is explicitly testing some failure behavior.
347    pub async fn execute_transaction_may_fail(
348        &self,
349        tx: Transaction,
350    ) -> anyhow::Result<IotaTransactionBlockResponse> {
351        let client = self.get_client().await?;
352        Ok(client
353            .quorum_driver_api()
354            .execute_transaction_block(
355                tx,
356                IotaTransactionBlockResponseOptions::new()
357                    .with_effects()
358                    .with_input()
359                    .with_events()
360                    .with_object_changes()
361                    .with_balance_changes(),
362                iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution,
363            )
364            .await?)
365    }
366}