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_sdk_crypto::simple::SimpleKeypair;
18use iota_sdk_types::{Address, ObjectId, ObjectReference, StructTag, Transaction, crypto::Intent};
19use iota_types::{
20 gas_coin::GasCoin,
21 transaction::{TransactionAPI, TransactionEnvelope},
22};
23use tokio::sync::RwLock;
24use tracing::warn;
25
26use crate::{
27 IotaClient, PagedFn,
28 iota_client_config::{IotaClientConfig, IotaEnv},
29};
30
31#[derive(Getters, MutGetters)]
34#[getset(get = "pub", get_mut = "pub")]
35pub struct WalletContext {
36 config: PersistedConfig<IotaClientConfig>,
37 request_timeout: Option<std::time::Duration>,
38 client: Arc<RwLock<Option<IotaClient>>>,
39 grpc_client: Arc<RwLock<Option<iota_grpc_client::Client>>>,
40 max_concurrent_requests: Option<u64>,
41 env_override: Option<String>,
42}
43
44impl WalletContext {
45 pub fn new(config_path: &Path) -> Result<Self, anyhow::Error> {
48 let config: IotaClientConfig = PersistedConfig::read(config_path).map_err(|err| {
49 anyhow!("Cannot open wallet config file at {config_path:?}. Err: {err}")
50 })?;
51
52 if let Some(active_address) = &config.active_address {
53 let addresses = match &config.keystore {
54 Keystore::File(file) => file.addresses(),
55 Keystore::InMem(mem) => mem.addresses(),
56 };
57 ensure!(
58 addresses.contains(active_address),
59 "error in '{}': active address not found in the keystore",
60 config_path.display()
61 );
62 }
63
64 if let Some(active_env) = &config.active_env {
65 ensure!(
66 config.get_env(active_env).is_some(),
67 "error in '{}': active environment not found in the envs list",
68 config_path.display()
69 );
70 }
71
72 let config = config.persisted(config_path);
73 let context = Self {
74 config,
75 request_timeout: None,
76 client: Default::default(),
77 grpc_client: Default::default(),
78 max_concurrent_requests: None,
79 env_override: None,
80 };
81 Ok(context)
82 }
83
84 pub fn with_request_timeout(mut self, request_timeout: std::time::Duration) -> Self {
85 self.request_timeout = Some(request_timeout);
86 self
87 }
88
89 pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: u64) -> Self {
90 self.max_concurrent_requests = Some(max_concurrent_requests);
91 self
92 }
93
94 pub fn with_env_override(mut self, env_override: String) -> Self {
95 self.env_override = Some(env_override);
96 self
97 }
98
99 pub fn get_addresses(&self) -> Vec<Address> {
101 self.config.keystore.addresses()
102 }
103
104 pub fn get_env_override(&self) -> Option<String> {
105 self.env_override.clone()
106 }
107
108 pub async fn get_client(&self) -> Result<IotaClient, anyhow::Error> {
110 let read = self.client.read().await;
111
112 Ok(if let Some(client) = read.as_ref() {
113 client.clone()
114 } else {
115 drop(read);
116 let client = self
117 .active_env()?
118 .create_rpc_client(self.request_timeout, self.max_concurrent_requests)
119 .await?;
120 if let Err(e) = client.check_api_version() {
121 warn!("{e}");
122 eprintln!("{}", format!("[warn] {e}").yellow().bold());
123 }
124 self.client.write().await.insert(client).clone()
125 })
126 }
127
128 pub async fn get_grpc_client(&self) -> Result<iota_grpc_client::Client, anyhow::Error> {
131 let read = self.grpc_client.read().await;
132
133 Ok(if let Some(client) = read.as_ref() {
134 client.clone()
135 } else {
136 drop(read);
137 let client = self.active_env()?.create_grpc_client()?;
138 self.grpc_client.write().await.insert(client).clone()
139 })
140 }
141
142 pub fn active_address(&self) -> Result<Address, anyhow::Error> {
145 if self.config.keystore.addresses().is_empty() {
146 bail!("No managed addresses. Create new address with the `new-address` command.");
147 }
148
149 Ok(if let Some(addr) = self.config.active_address() {
150 *addr
151 } else {
152 self.config.keystore().addresses()[0]
153 })
154 }
155
156 pub fn active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
159 if self.config.envs.is_empty() {
160 bail!("No managed environments. Create new environment with the `new-env` command.");
161 }
162
163 if let Some(env_override) = &self.env_override {
164 self.config.get_env(env_override).ok_or_else(|| {
165 anyhow!("Environment configuration not found for env [{env_override}]")
166 })
167 } else {
168 Ok(if self.config.active_env().is_some() {
169 self.config.get_active_env()?
170 } else {
171 &self.config.envs()[0]
172 })
173 }
174 }
175
176 pub async fn get_object_ref(
178 &self,
179 object_id: ObjectId,
180 ) -> Result<ObjectReference, anyhow::Error> {
181 let client = self.get_client().await?;
182 Ok(client
183 .read_api()
184 .get_object_with_options(object_id, IotaObjectDataOptions::new())
185 .await?
186 .into_object()?
187 .object_ref())
188 }
189
190 pub async fn gas_objects(
192 &self,
193 address: Address,
194 ) -> Result<Vec<(u64, IotaObjectData)>, anyhow::Error> {
195 let client = self.get_client().await?;
196
197 let values_objects = PagedFn::stream(async |cursor| {
198 client
199 .read_api()
200 .get_owned_objects(
201 address,
202 IotaObjectResponseQuery::new(
203 Some(IotaObjectDataFilter::StructType(StructTag::new_gas_coin())),
204 Some(IotaObjectDataOptions::full_content()),
205 ),
206 cursor,
207 None,
208 )
209 .await
210 })
211 .filter_map(|res| async {
212 match res {
213 Ok(res) => {
214 if let Some(o) = res.data {
215 match GasCoin::try_from(&o) {
216 Ok(gas_coin) => Some(Ok((gas_coin.value(), o))),
217 Err(e) => Some(Err(anyhow!("{e}"))),
218 }
219 } else {
220 None
221 }
222 }
223 Err(e) => Some(Err(anyhow!("{e}"))),
224 }
225 })
226 .try_collect::<Vec<_>>()
227 .await?;
228
229 Ok(values_objects)
230 }
231
232 pub async fn get_object_owner(&self, id: &ObjectId) -> Result<Address, anyhow::Error> {
234 let client = self.get_client().await?;
235 let object = client
236 .read_api()
237 .get_object_with_options(*id, IotaObjectDataOptions::new().with_owner())
238 .await?
239 .into_object()?;
240 Ok(*object
241 .owner
242 .ok_or_else(|| anyhow!("Owner field is None"))?
243 .address_or_object()
244 .ok_or_else(|| anyhow::anyhow!("not an address or object owner"))?)
245 }
246
247 pub async fn try_get_object_owner(
249 &self,
250 id: &Option<ObjectId>,
251 ) -> Result<Option<Address>, anyhow::Error> {
252 if let Some(id) = id {
253 Ok(Some(self.get_object_owner(id).await?))
254 } else {
255 Ok(None)
256 }
257 }
258
259 pub async fn infer_sender(&mut self, gas: &[ObjectId]) -> Result<Address, anyhow::Error> {
263 if gas.is_empty() {
264 return self.active_address();
265 }
266
267 let owners = future::try_join_all(gas.iter().map(|id| self.get_object_owner(id))).await?;
269
270 let owner = owners[0];
272
273 ensure!(
274 owners.iter().all(|o| o == &owner),
275 "Cannot infer sender, not all gas objects have the same owner."
276 );
277
278 Ok(owner)
279 }
280
281 pub async fn gas_for_owner_budget(
283 &self,
284 address: Address,
285 budget: u64,
286 forbidden_gas_objects: BTreeSet<ObjectId>,
287 ) -> Result<(u64, IotaObjectData), anyhow::Error> {
288 for o in self.gas_objects(address).await? {
289 if o.0 >= budget && !forbidden_gas_objects.contains(&o.1.object_id) {
290 return Ok((o.0, o.1));
291 }
292 }
293 bail!(
294 "No non-argument gas objects found for this address with value >= budget {budget}. Run iota client gas to check for gas objects."
295 )
296 }
297
298 pub async fn get_all_gas_objects_owned_by_address(
301 &self,
302 address: Address,
303 ) -> anyhow::Result<Vec<ObjectReference>> {
304 self.get_gas_objects_owned_by_address(address, None).await
305 }
306
307 pub async fn get_gas_objects_owned_by_address(
311 &self,
312 address: Address,
313 limit: impl Into<Option<usize>>,
314 ) -> anyhow::Result<Vec<ObjectReference>> {
315 let client = self.get_client().await?;
316 let results: Vec<_> = client
317 .read_api()
318 .get_owned_objects(
319 address,
320 IotaObjectResponseQuery::new(
321 Some(IotaObjectDataFilter::StructType(StructTag::new_gas_coin())),
322 Some(IotaObjectDataOptions::full_content()),
323 ),
324 None,
325 limit,
326 )
327 .await?
328 .data
329 .into_iter()
330 .filter_map(|r| r.data.map(|o| o.object_ref()))
331 .collect();
332 Ok(results)
333 }
334
335 pub async fn get_one_gas_object_owned_by_address(
339 &self,
340 address: Address,
341 ) -> anyhow::Result<Option<ObjectReference>> {
342 Ok(self
343 .get_gas_objects_owned_by_address(address, 1)
344 .await?
345 .pop())
346 }
347
348 pub async fn get_one_account(&self) -> anyhow::Result<(Address, Vec<ObjectReference>)> {
350 let address = self.get_addresses().pop().unwrap();
351 Ok((
352 address,
353 self.get_all_gas_objects_owned_by_address(address).await?,
354 ))
355 }
356
357 pub async fn get_one_gas_object(&self) -> anyhow::Result<Option<(Address, ObjectReference)>> {
359 for address in self.get_addresses() {
360 if let Some(gas_object) = self.get_one_gas_object_owned_by_address(address).await? {
361 return Ok(Some((address, gas_object)));
362 }
363 }
364 Ok(None)
365 }
366
367 pub async fn get_all_accounts_and_gas_objects(
370 &self,
371 ) -> anyhow::Result<Vec<(Address, Vec<ObjectReference>)>> {
372 let mut result = vec![];
373 for address in self.get_addresses() {
374 let objects = self
375 .gas_objects(address)
376 .await?
377 .into_iter()
378 .map(|(_, o)| o.object_ref())
379 .collect();
380 result.push((address, objects));
381 }
382 Ok(result)
383 }
384
385 pub async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
386 let client = self.get_client().await?;
387 let gas_price = client.governance_api().get_reference_gas_price().await?;
388 Ok(gas_price)
389 }
390
391 pub fn add_account(&mut self, alias: impl Into<Option<String>>, keypair: SimpleKeypair) {
393 self.config.keystore.add_key(alias.into(), keypair).unwrap();
394 }
395
396 pub fn sign_transaction(&self, tx: &Transaction) -> TransactionEnvelope {
398 let sig = self
399 .config
400 .keystore
401 .sign_secure(&tx.sender(), tx, Intent::iota_transaction())
402 .unwrap();
403 TransactionEnvelope::from_data(tx.clone(), vec![sig])
405 }
406
407 pub async fn execute_transaction_must_succeed(
411 &self,
412 tx: TransactionEnvelope,
413 ) -> IotaTransactionBlockResponse {
414 tracing::debug!("Executing transaction: {:?}", tx);
415 let response = self.execute_transaction_may_fail(tx).await.unwrap();
416 assert!(
417 response.status_ok().unwrap(),
418 "Transaction failed: {response:?}"
419 );
420 response
421 }
422
423 pub async fn execute_transaction_may_fail(
428 &self,
429 tx: TransactionEnvelope,
430 ) -> anyhow::Result<IotaTransactionBlockResponse> {
431 let client = self.get_client().await?;
432 Ok(client
433 .quorum_driver_api()
434 .execute_transaction_block(
435 tx,
436 IotaTransactionBlockResponseOptions::new()
437 .with_effects()
438 .with_input()
439 .with_events()
440 .with_object_changes()
441 .with_balance_changes(),
442 iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution,
443 )
444 .await?)
445 }
446}