1use std::{collections::BTreeSet, path::Path, sync::Arc};
6
7use anyhow::anyhow;
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#[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 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 pub fn get_addresses(&self) -> Vec<IotaAddress> {
70 self.config.keystore.addresses()
71 }
72
73 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 pub fn active_address(&self) -> Result<IotaAddress, anyhow::Error> {
96 if self.config.keystore.addresses().is_empty() {
97 return Err(anyhow!(
98 "No managed addresses. Create new address with the `new-address` command."
99 ));
100 }
101
102 Ok(if let Some(addr) = self.config.active_address() {
103 *addr
104 } else {
105 self.config.keystore().addresses()[0]
106 })
107 }
108
109 pub fn active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
112 if self.config.envs.is_empty() {
113 return Err(anyhow!(
114 "No managed environments. Create new environment with the `new-env` command."
115 ));
116 }
117
118 Ok(if self.config.active_env().is_some() {
119 self.config.get_active_env()?
120 } else {
121 &self.config.envs()[0]
122 })
123 }
124
125 pub async fn get_object_ref(&self, object_id: ObjectID) -> Result<ObjectRef, anyhow::Error> {
127 let client = self.get_client().await?;
128 Ok(client
129 .read_api()
130 .get_object_with_options(object_id, IotaObjectDataOptions::new())
131 .await?
132 .into_object()?
133 .object_ref())
134 }
135
136 pub async fn gas_objects(
138 &self,
139 address: IotaAddress,
140 ) -> Result<Vec<(u64, IotaObjectData)>, anyhow::Error> {
141 let client = self.get_client().await?;
142
143 let values_objects = PagedFn::stream(async |cursor| {
144 client
145 .read_api()
146 .get_owned_objects(
147 address,
148 IotaObjectResponseQuery::new(
149 Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
150 Some(IotaObjectDataOptions::full_content()),
151 ),
152 cursor,
153 None,
154 )
155 .await
156 })
157 .filter_map(|res| async {
158 match res {
159 Ok(res) => {
160 if let Some(o) = res.data {
161 match GasCoin::try_from(&o) {
162 Ok(gas_coin) => Some(Ok((gas_coin.value(), o.clone()))),
163 Err(e) => Some(Err(anyhow::anyhow!("{e}"))),
164 }
165 } else {
166 None
167 }
168 }
169 Err(e) => Some(Err(anyhow::anyhow!("{e}"))),
170 }
171 })
172 .try_collect::<Vec<_>>()
173 .await?;
174
175 Ok(values_objects)
176 }
177
178 pub async fn get_object_owner(&self, id: &ObjectID) -> Result<IotaAddress, anyhow::Error> {
180 let client = self.get_client().await?;
181 let object = client
182 .read_api()
183 .get_object_with_options(*id, IotaObjectDataOptions::new().with_owner())
184 .await?
185 .into_object()?;
186 Ok(object
187 .owner
188 .ok_or_else(|| anyhow!("Owner field is None"))?
189 .get_owner_address()?)
190 }
191
192 pub async fn try_get_object_owner(
194 &self,
195 id: &Option<ObjectID>,
196 ) -> Result<Option<IotaAddress>, anyhow::Error> {
197 if let Some(id) = id {
198 Ok(Some(self.get_object_owner(id).await?))
199 } else {
200 Ok(None)
201 }
202 }
203
204 pub async fn gas_for_owner_budget(
206 &self,
207 address: IotaAddress,
208 budget: u64,
209 forbidden_gas_objects: BTreeSet<ObjectID>,
210 ) -> Result<(u64, IotaObjectData), anyhow::Error> {
211 for o in self.gas_objects(address).await? {
212 if o.0 >= budget && !forbidden_gas_objects.contains(&o.1.object_id) {
213 return Ok((o.0, o.1));
214 }
215 }
216 Err(anyhow!(
217 "No non-argument gas objects found for this address with value >= budget {budget}. Run iota client gas to check for gas objects."
218 ))
219 }
220
221 pub async fn get_all_gas_objects_owned_by_address(
224 &self,
225 address: IotaAddress,
226 ) -> anyhow::Result<Vec<ObjectRef>> {
227 self.get_gas_objects_owned_by_address(address, None).await
228 }
229
230 pub async fn get_gas_objects_owned_by_address(
234 &self,
235 address: IotaAddress,
236 limit: impl Into<Option<usize>>,
237 ) -> anyhow::Result<Vec<ObjectRef>> {
238 let client = self.get_client().await?;
239 let results: Vec<_> = client
240 .read_api()
241 .get_owned_objects(
242 address,
243 IotaObjectResponseQuery::new(
244 Some(IotaObjectDataFilter::StructType(GasCoin::type_())),
245 Some(IotaObjectDataOptions::full_content()),
246 ),
247 None,
248 limit,
249 )
250 .await?
251 .data
252 .into_iter()
253 .filter_map(|r| r.data.map(|o| o.object_ref()))
254 .collect();
255 Ok(results)
256 }
257
258 pub async fn get_one_gas_object_owned_by_address(
262 &self,
263 address: IotaAddress,
264 ) -> anyhow::Result<Option<ObjectRef>> {
265 Ok(self
266 .get_gas_objects_owned_by_address(address, 1)
267 .await?
268 .pop())
269 }
270
271 pub async fn get_one_account(&self) -> anyhow::Result<(IotaAddress, Vec<ObjectRef>)> {
273 let address = self.get_addresses().pop().unwrap();
274 Ok((
275 address,
276 self.get_all_gas_objects_owned_by_address(address).await?,
277 ))
278 }
279
280 pub async fn get_one_gas_object(&self) -> anyhow::Result<Option<(IotaAddress, ObjectRef)>> {
282 for address in self.get_addresses() {
283 if let Some(gas_object) = self.get_one_gas_object_owned_by_address(address).await? {
284 return Ok(Some((address, gas_object)));
285 }
286 }
287 Ok(None)
288 }
289
290 pub async fn get_all_accounts_and_gas_objects(
293 &self,
294 ) -> anyhow::Result<Vec<(IotaAddress, Vec<ObjectRef>)>> {
295 let mut result = vec![];
296 for address in self.get_addresses() {
297 let objects = self
298 .gas_objects(address)
299 .await?
300 .into_iter()
301 .map(|(_, o)| o.object_ref())
302 .collect();
303 result.push((address, objects));
304 }
305 Ok(result)
306 }
307
308 pub async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
309 let client = self.get_client().await?;
310 let gas_price = client.governance_api().get_reference_gas_price().await?;
311 Ok(gas_price)
312 }
313
314 pub fn add_account(&mut self, alias: impl Into<Option<String>>, keypair: IotaKeyPair) {
316 self.config.keystore.add_key(alias.into(), keypair).unwrap();
317 }
318
319 pub fn sign_transaction(&self, data: &TransactionData) -> Transaction {
321 let sig = self
322 .config
323 .keystore
324 .sign_secure(&data.sender(), data, Intent::iota_transaction())
325 .unwrap();
326 Transaction::from_data(data.clone(), vec![sig])
328 }
329
330 pub async fn execute_transaction_must_succeed(
334 &self,
335 tx: Transaction,
336 ) -> IotaTransactionBlockResponse {
337 tracing::debug!("Executing transaction: {:?}", tx);
338 let response = self.execute_transaction_may_fail(tx).await.unwrap();
339 assert!(
340 response.status_ok().unwrap(),
341 "Transaction failed: {:?}",
342 response
343 );
344 response
345 }
346
347 pub async fn execute_transaction_may_fail(
352 &self,
353 tx: Transaction,
354 ) -> anyhow::Result<IotaTransactionBlockResponse> {
355 let client = self.get_client().await?;
356 Ok(client
357 .quorum_driver_api()
358 .execute_transaction_block(
359 tx,
360 IotaTransactionBlockResponseOptions::new()
361 .with_effects()
362 .with_input()
363 .with_events()
364 .with_object_changes()
365 .with_balance_changes(),
366 iota_types::quorum_driver_types::ExecuteTransactionRequestType::WaitForLocalExecution,
367 )
368 .await?)
369 }
370}