1use std::{collections::BTreeSet, path::Path, sync::Arc};
6
7use anyhow::{anyhow, bail, ensure};
8use colored::Colorize;
9use futures::{StreamExt, TryStreamExt, future};
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, Keystore};
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#[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 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 if let Some(active_address) = &config.active_address {
59 let addresses = match &config.keystore {
60 Keystore::File(file) => file.addresses(),
61 Keystore::InMem(mem) => mem.addresses(),
62 };
63 ensure!(
64 addresses.contains(active_address),
65 "error in '{}': active address not found in the keystore",
66 config_path.display()
67 );
68 }
69
70 if let Some(active_env) = &config.active_env {
71 ensure!(
72 config.get_env(active_env).is_some(),
73 "error in '{}': active environment not found in the envs list",
74 config_path.display()
75 );
76 }
77
78 let config = config.persisted(config_path);
79 let context = Self {
80 config,
81 request_timeout: request_timeout.into(),
82 client: Default::default(),
83 max_concurrent_requests: max_concurrent_requests.into(),
84 };
85 Ok(context)
86 }
87
88 pub fn get_addresses(&self) -> Vec<IotaAddress> {
90 self.config.keystore.addresses()
91 }
92
93 pub async fn get_client(&self) -> Result<IotaClient, anyhow::Error> {
95 let read = self.client.read().await;
96
97 Ok(if let Some(client) = read.as_ref() {
98 client.clone()
99 } else {
100 drop(read);
101 let client = self
102 .active_env()?
103 .create_rpc_client(self.request_timeout, self.max_concurrent_requests)
104 .await?;
105 if let Err(e) = client.check_api_version() {
106 warn!("{e}");
107 eprintln!("{}", format!("[warn] {e}").yellow().bold());
108 }
109 self.client.write().await.insert(client).clone()
110 })
111 }
112
113 pub fn active_address(&self) -> Result<IotaAddress, anyhow::Error> {
116 if self.config.keystore.addresses().is_empty() {
117 bail!("No managed addresses. Create new address with the `new-address` command.");
118 }
119
120 Ok(if let Some(addr) = self.config.active_address() {
121 *addr
122 } else {
123 self.config.keystore().addresses()[0]
124 })
125 }
126
127 pub fn active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
130 if self.config.envs.is_empty() {
131 bail!("No managed environments. Create new environment with the `new-env` command.");
132 }
133
134 Ok(if self.config.active_env().is_some() {
135 self.config.get_active_env()?
136 } else {
137 &self.config.envs()[0]
138 })
139 }
140
141 pub async fn get_object_ref(&self, object_id: ObjectID) -> Result<ObjectRef, anyhow::Error> {
143 let client = self.get_client().await?;
144 Ok(client
145 .read_api()
146 .get_object_with_options(object_id, IotaObjectDataOptions::new())
147 .await?
148 .into_object()?
149 .object_ref())
150 }
151
152 pub async fn gas_objects(
154 &self,
155 address: IotaAddress,
156 ) -> Result<Vec<(u64, IotaObjectData)>, anyhow::Error> {
157 let client = self.get_client().await?;
158
159 let values_objects = PagedFn::stream(async |cursor| {
160 client
161 .read_api()
162 .get_owned_objects(
163 address,
164 IotaObjectResponseQuery::new(
165 Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
166 Some(IotaObjectDataOptions::full_content()),
167 ),
168 cursor,
169 None,
170 )
171 .await
172 })
173 .filter_map(|res| async {
174 match res {
175 Ok(res) => {
176 if let Some(o) = res.data {
177 match GasCoin::try_from(&o) {
178 Ok(gas_coin) => Some(Ok((gas_coin.value(), o.clone()))),
179 Err(e) => Some(Err(anyhow!("{e}"))),
180 }
181 } else {
182 None
183 }
184 }
185 Err(e) => Some(Err(anyhow!("{e}"))),
186 }
187 })
188 .try_collect::<Vec<_>>()
189 .await?;
190
191 Ok(values_objects)
192 }
193
194 pub async fn get_object_owner(&self, id: &ObjectID) -> Result<IotaAddress, anyhow::Error> {
196 let client = self.get_client().await?;
197 let object = client
198 .read_api()
199 .get_object_with_options(*id, IotaObjectDataOptions::new().with_owner())
200 .await?
201 .into_object()?;
202 Ok(object
203 .owner
204 .ok_or_else(|| anyhow!("Owner field is None"))?
205 .get_owner_address()?)
206 }
207
208 pub async fn try_get_object_owner(
210 &self,
211 id: &Option<ObjectID>,
212 ) -> Result<Option<IotaAddress>, anyhow::Error> {
213 if let Some(id) = id {
214 Ok(Some(self.get_object_owner(id).await?))
215 } else {
216 Ok(None)
217 }
218 }
219
220 pub async fn infer_sender(&mut self, gas: &[ObjectID]) -> Result<IotaAddress, anyhow::Error> {
224 if gas.is_empty() {
225 return self.active_address();
226 }
227
228 let owners = future::try_join_all(gas.iter().map(|id| self.get_object_owner(id))).await?;
230
231 let owner = owners[0];
233
234 ensure!(
235 owners.iter().all(|o| o == &owner),
236 "Cannot infer sender, not all gas objects have the same owner."
237 );
238
239 Ok(owner)
240 }
241
242 pub async fn gas_for_owner_budget(
244 &self,
245 address: IotaAddress,
246 budget: u64,
247 forbidden_gas_objects: BTreeSet<ObjectID>,
248 ) -> Result<(u64, IotaObjectData), anyhow::Error> {
249 for o in self.gas_objects(address).await? {
250 if o.0 >= budget && !forbidden_gas_objects.contains(&o.1.object_id) {
251 return Ok((o.0, o.1));
252 }
253 }
254 bail!(
255 "No non-argument gas objects found for this address with value >= budget {budget}. Run iota client gas to check for gas objects."
256 )
257 }
258
259 pub async fn get_all_gas_objects_owned_by_address(
262 &self,
263 address: IotaAddress,
264 ) -> anyhow::Result<Vec<ObjectRef>> {
265 self.get_gas_objects_owned_by_address(address, None).await
266 }
267
268 pub async fn get_gas_objects_owned_by_address(
272 &self,
273 address: IotaAddress,
274 limit: impl Into<Option<usize>>,
275 ) -> anyhow::Result<Vec<ObjectRef>> {
276 let client = self.get_client().await?;
277 let results: Vec<_> = client
278 .read_api()
279 .get_owned_objects(
280 address,
281 IotaObjectResponseQuery::new(
282 Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
283 Some(IotaObjectDataOptions::full_content()),
284 ),
285 None,
286 limit,
287 )
288 .await?
289 .data
290 .into_iter()
291 .filter_map(|r| r.data.map(|o| o.object_ref()))
292 .collect();
293 Ok(results)
294 }
295
296 pub async fn get_one_gas_object_owned_by_address(
300 &self,
301 address: IotaAddress,
302 ) -> anyhow::Result<Option<ObjectRef>> {
303 Ok(self
304 .get_gas_objects_owned_by_address(address, 1)
305 .await?
306 .pop())
307 }
308
309 pub async fn get_one_account(&self) -> anyhow::Result<(IotaAddress, Vec<ObjectRef>)> {
311 let address = self.get_addresses().pop().unwrap();
312 Ok((
313 address,
314 self.get_all_gas_objects_owned_by_address(address).await?,
315 ))
316 }
317
318 pub async fn get_one_gas_object(&self) -> anyhow::Result<Option<(IotaAddress, ObjectRef)>> {
320 for address in self.get_addresses() {
321 if let Some(gas_object) = self.get_one_gas_object_owned_by_address(address).await? {
322 return Ok(Some((address, gas_object)));
323 }
324 }
325 Ok(None)
326 }
327
328 pub async fn get_all_accounts_and_gas_objects(
331 &self,
332 ) -> anyhow::Result<Vec<(IotaAddress, Vec<ObjectRef>)>> {
333 let mut result = vec![];
334 for address in self.get_addresses() {
335 let objects = self
336 .gas_objects(address)
337 .await?
338 .into_iter()
339 .map(|(_, o)| o.object_ref())
340 .collect();
341 result.push((address, objects));
342 }
343 Ok(result)
344 }
345
346 pub async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
347 let client = self.get_client().await?;
348 let gas_price = client.governance_api().get_reference_gas_price().await?;
349 Ok(gas_price)
350 }
351
352 pub fn add_account(&mut self, alias: impl Into<Option<String>>, keypair: IotaKeyPair) {
354 self.config.keystore.add_key(alias.into(), keypair).unwrap();
355 }
356
357 pub fn sign_transaction(&self, data: &TransactionData) -> Transaction {
359 let sig = self
360 .config
361 .keystore
362 .sign_secure(&data.sender(), data, Intent::iota_transaction())
363 .unwrap();
364 Transaction::from_data(data.clone(), vec![sig])
366 }
367
368 pub async fn execute_transaction_must_succeed(
372 &self,
373 tx: Transaction,
374 ) -> IotaTransactionBlockResponse {
375 tracing::debug!("Executing transaction: {:?}", tx);
376 let response = self.execute_transaction_may_fail(tx).await.unwrap();
377 assert!(
378 response.status_ok().unwrap(),
379 "Transaction failed: {response:?}"
380 );
381 response
382 }
383
384 pub async fn execute_transaction_may_fail(
389 &self,
390 tx: Transaction,
391 ) -> anyhow::Result<IotaTransactionBlockResponse> {
392 let client = self.get_client().await?;
393 Ok(client
394 .quorum_driver_api()
395 .execute_transaction_block(
396 tx,
397 IotaTransactionBlockResponseOptions::new()
398 .with_effects()
399 .with_input()
400 .with_events()
401 .with_object_changes()
402 .with_balance_changes(),
403 iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution,
404 )
405 .await?)
406 }
407}