1use 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 Transaction::from_data(data.clone(), vec![sig])
324 }
325
326 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 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}