1use crate::sui_client_config::{SuiClientConfig, SuiEnv};
5use anyhow::{anyhow, ensure};
6use futures::future;
7use futures::stream::TryStreamExt;
8use shared_crypto::intent::Intent;
9use std::collections::BTreeSet;
10use std::path::{Path, PathBuf};
11use sui_config::{Config, PersistedConfig};
12use sui_keys::key_identity::KeyIdentity;
13use sui_keys::keystore::{AccountKeystore, Alias, Keystore};
14use sui_rpc_api::client::ExecutedTransaction;
15use sui_types::base_types::{FullObjectRef, ObjectID, ObjectRef, SuiAddress};
16use sui_types::crypto::{Signature, SuiKeyPair};
17use sui_types::effects::TransactionEffectsAPI;
18use sui_types::object::Object;
19
20use std::sync::OnceLock;
21use sui_rpc_api::Client;
22use sui_types::gas_coin::GasCoin;
23use sui_types::transaction::{Transaction, TransactionData, TransactionDataAPI};
24use tracing::info;
25
26pub struct WalletContext {
27 pub config: PersistedConfig<SuiClientConfig>,
28 request_timeout: Option<std::time::Duration>,
29 grpc: OnceLock<Client>,
30 max_concurrent_requests: Option<u64>,
31 env_override: Option<String>,
32}
33
34impl WalletContext {
35 pub fn new(config_path: &Path) -> Result<Self, anyhow::Error> {
36 let config: SuiClientConfig = PersistedConfig::read(config_path).map_err(|err| {
37 anyhow!(
38 "Cannot open wallet config file at {:?}. Err: {err}",
39 config_path
40 )
41 })?;
42
43 let config = config.persisted(config_path);
44 let context = Self {
45 config,
46 request_timeout: None,
47 grpc: OnceLock::new(),
48 max_concurrent_requests: None,
49 env_override: None,
50 };
51 Ok(context)
52 }
53
54 pub fn new_for_tests(
55 keystore: Keystore,
56 external: Option<Keystore>,
57 path: Option<PathBuf>,
58 ) -> Self {
59 let mut config = SuiClientConfig::new(keystore)
60 .persisted(&path.unwrap_or(PathBuf::from("test_config.yaml")));
61 config.external_keys = external;
62 Self {
63 config,
64 request_timeout: None,
65 grpc: OnceLock::new(),
66 max_concurrent_requests: None,
67 env_override: None,
68 }
69 }
70
71 pub fn with_request_timeout(mut self, request_timeout: std::time::Duration) -> Self {
72 self.request_timeout = Some(request_timeout);
73 self
74 }
75
76 pub fn with_max_concurrent_requests(mut self, max_concurrent_requests: u64) -> Self {
77 self.max_concurrent_requests = Some(max_concurrent_requests);
78 self
79 }
80
81 pub fn with_env_override(mut self, env_override: String) -> Self {
82 self.env_override = Some(env_override);
83 self
84 }
85
86 pub fn get_addresses(&self) -> Vec<SuiAddress> {
87 let mut addresses = self.config.keystore.addresses();
88
89 if let Some(external_keys) = &self.config.external_keys {
90 addresses.extend(external_keys.addresses());
91 }
92
93 addresses
94 }
95
96 pub fn addresses_with_alias(&self) -> Vec<(&SuiAddress, &Alias)> {
97 let mut addresses = self.config.keystore.addresses_with_alias();
98
99 if let Some(external_keys) = &self.config.external_keys {
100 addresses.extend(external_keys.addresses_with_alias());
101 }
102
103 addresses
104 }
105
106 pub fn get_env_override(&self) -> Option<String> {
107 self.env_override.clone()
108 }
109
110 pub fn get_identity_address(
111 &mut self,
112 input: Option<KeyIdentity>,
113 ) -> Result<SuiAddress, anyhow::Error> {
114 if let Some(key_identity) = input {
115 if let Ok(address) = self.config.keystore.get_by_identity(&key_identity) {
116 return Ok(address);
117 }
118 if let Some(address) = self
119 .config
120 .external_keys
121 .as_ref()
122 .and_then(|external_keys| external_keys.get_by_identity(&key_identity).ok())
123 {
124 return Ok(address);
125 }
126
127 Err(anyhow!(
128 "No address found for the provided key identity: {key_identity}"
129 ))
130 } else {
131 self.active_address()
132 }
133 }
134
135 pub fn grpc_client(&self) -> Result<Client, anyhow::Error> {
136 if let Some(client) = self.grpc.get() {
137 Ok(client.clone())
138 } else {
139 let client = self.get_active_env()?.create_grpc_client()?;
140 Ok(self.grpc.get_or_init(move || client).clone())
141 }
142 }
143
144 pub async fn load_or_cache_chain_id(&self) -> Result<String, anyhow::Error> {
149 self.internal_load_or_cache_chain_id(false).await
150 }
151
152 pub async fn try_load_chain_id_from_cache(
154 &self,
155 env: Option<String>,
156 ) -> Result<String, anyhow::Error> {
157 let env = if let Some(env) = env {
158 self.config
159 .get_env(&Some(env.to_string()))
160 .ok_or_else(|| anyhow!("Environment configuration not found for env [{}]", env))?
161 } else {
162 self.get_active_env()?
163 };
164 if let Some(chain_id) = &env.chain_id {
165 Ok(chain_id.clone())
166 } else {
167 Err(anyhow!(
168 "No cached chain ID found for env {}. Please pass `-e env_name` to your command",
169 env.alias
170 ))
171 }
172 }
173
174 pub async fn cache_chain_id(&self) -> Result<String, anyhow::Error> {
177 self.internal_load_or_cache_chain_id(true).await
178 }
179
180 async fn internal_load_or_cache_chain_id(
181 &self,
182 force_recache: bool,
183 ) -> Result<String, anyhow::Error> {
184 let env = self.get_active_env()?;
185 if !force_recache && env.chain_id.is_some() {
186 let chain_id = env.chain_id.as_ref().unwrap();
187 info!("Found cached chain ID for env {}: {}", env.alias, chain_id);
188 return Ok(chain_id.clone());
189 }
190 let chain_id = self.grpc_client()?.get_chain_identifier().await?;
191 let path = self.config.path();
192 let mut config_result = SuiClientConfig::load_with_lock(path)?;
193
194 config_result.update_env_chain_id(&env.alias, chain_id.to_string())?;
195 config_result.save_with_lock(path)?;
196 Ok(chain_id.to_string())
197 }
198
199 pub fn get_active_env(&self) -> Result<&SuiEnv, anyhow::Error> {
200 if self.env_override.is_some() {
201 self.config.get_env(&self.env_override).ok_or_else(|| {
202 anyhow!(
203 "Environment configuration not found for env [{}]",
204 self.env_override.as_deref().unwrap_or("None")
205 )
206 })
207 } else {
208 self.config.get_active_env()
209 }
210 }
211
212 pub fn active_address(&mut self) -> Result<SuiAddress, anyhow::Error> {
214 if self.config.keystore.entries().is_empty() {
215 return Err(anyhow!(
216 "No managed addresses. Create new address with `new-address` command."
217 ));
218 }
219
220 self.config.active_address = Some(
223 self.config
224 .active_address
225 .unwrap_or(*self.config.keystore.addresses().first().unwrap()),
226 );
227
228 Ok(self.config.active_address.unwrap())
229 }
230
231 pub async fn get_object_ref(&self, object_id: ObjectID) -> Result<ObjectRef, anyhow::Error> {
233 Ok(self
234 .grpc_client()?
235 .get_object(object_id)
236 .await?
237 .compute_object_reference())
238 }
239
240 pub async fn get_full_object_ref(
242 &self,
243 object_id: ObjectID,
244 ) -> Result<FullObjectRef, anyhow::Error> {
245 Ok(self
246 .grpc_client()?
247 .get_object(object_id)
248 .await?
249 .compute_full_object_reference())
250 }
251
252 pub async fn gas_objects(
254 &self,
255 owner: SuiAddress,
256 ) -> Result<Vec<(u64, Object)>, anyhow::Error> {
257 let client = self.grpc_client()?;
258
259 client
260 .list_owned_objects(owner, Some(GasCoin::type_()))
261 .map_err(Into::into)
262 .and_then(|object| async move {
263 let gas_coin = GasCoin::try_from(&object)?;
264
265 Ok((gas_coin.value(), object))
266 })
267 .try_collect()
268 .await
269 }
270
271 pub async fn get_object_owner(&self, id: &ObjectID) -> Result<SuiAddress, anyhow::Error> {
272 self.grpc_client()?
273 .get_object(*id)
274 .await?
275 .owner()
276 .get_owner_address()
277 .map_err(Into::into)
278 }
279
280 pub async fn try_get_object_owner(
281 &self,
282 id: &Option<ObjectID>,
283 ) -> Result<Option<SuiAddress>, anyhow::Error> {
284 if let Some(id) = id {
285 Ok(Some(self.get_object_owner(id).await?))
286 } else {
287 Ok(None)
288 }
289 }
290
291 pub async fn infer_sender(&mut self, gas: &[ObjectID]) -> Result<SuiAddress, anyhow::Error> {
294 if gas.is_empty() {
295 return self.active_address();
296 }
297
298 let owners = future::try_join_all(gas.iter().map(|id| self.get_object_owner(id))).await?;
300
301 let owner = owners.first().copied().unwrap();
303
304 ensure!(
305 owners.iter().all(|o| o == &owner),
306 "Cannot infer sender, not all gas objects have the same owner."
307 );
308
309 Ok(owner)
310 }
311
312 pub async fn gas_for_owner_budget(
314 &self,
315 address: SuiAddress,
316 budget: u64,
317 forbidden_gas_objects: BTreeSet<ObjectID>,
318 ) -> Result<(u64, Object), anyhow::Error> {
319 for o in self.gas_objects(address).await? {
320 if o.0 >= budget && !forbidden_gas_objects.contains(&o.1.id()) {
321 return Ok((o.0, o.1));
322 }
323 }
324 Err(anyhow!(
325 "No non-argument gas objects found for this address with value >= budget {budget}. Run sui client gas to check for gas objects."
326 ))
327 }
328
329 pub async fn get_all_gas_objects_owned_by_address(
330 &self,
331 address: SuiAddress,
332 ) -> anyhow::Result<Vec<ObjectRef>> {
333 self.get_gas_objects_owned_by_address(address, None).await
334 }
335
336 pub async fn get_gas_objects_owned_by_address(
337 &self,
338 owner: SuiAddress,
339 page_size: Option<u32>,
340 ) -> anyhow::Result<Vec<ObjectRef>> {
341 let page = self
342 .grpc_client()?
343 .get_owned_objects(owner, Some(GasCoin::type_()), page_size, None)
344 .await?;
345
346 Ok(page
347 .items
348 .into_iter()
349 .map(|o| o.compute_object_reference())
350 .collect())
351 }
352
353 pub async fn get_one_gas_object_owned_by_address(
356 &self,
357 address: SuiAddress,
358 ) -> anyhow::Result<Option<ObjectRef>> {
359 Ok(self
360 .get_gas_objects_owned_by_address(address, Some(1))
361 .await?
362 .pop())
363 }
364
365 pub async fn get_one_account(&self) -> anyhow::Result<(SuiAddress, Vec<ObjectRef>)> {
367 let address = self.get_addresses().pop().unwrap();
368 Ok((
369 address,
370 self.get_all_gas_objects_owned_by_address(address).await?,
371 ))
372 }
373
374 pub async fn get_one_gas_object(&self) -> anyhow::Result<Option<(SuiAddress, ObjectRef)>> {
376 for address in self.get_addresses() {
377 if let Some(gas_object) = self.get_one_gas_object_owned_by_address(address).await? {
378 return Ok(Some((address, gas_object)));
379 }
380 }
381 Ok(None)
382 }
383
384 pub async fn get_all_accounts_and_gas_objects(
386 &self,
387 ) -> anyhow::Result<Vec<(SuiAddress, Vec<ObjectRef>)>> {
388 let mut result = vec![];
389 for address in self.get_addresses() {
390 let objects = self
391 .gas_objects(address)
392 .await?
393 .into_iter()
394 .map(|(_, o)| o.compute_object_reference())
395 .collect();
396 result.push((address, objects));
397 }
398 Ok(result)
399 }
400
401 pub async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
402 self.grpc_client()?
403 .get_reference_gas_price()
404 .await
405 .map_err(Into::into)
406 }
407
408 pub async fn get_current_epoch(&self) -> Result<u64, anyhow::Error> {
409 self.grpc_client()?
410 .get_current_epoch()
411 .await
412 .map_err(Into::into)
413 }
414
415 pub async fn get_chain_identifier(
416 &self,
417 ) -> Result<sui_types::digests::ChainIdentifier, anyhow::Error> {
418 self.grpc_client()?
419 .get_chain_identifier()
420 .await
421 .map_err(Into::into)
422 }
423
424 pub async fn add_account(&mut self, alias: Option<String>, keypair: SuiKeyPair) {
426 self.config.keystore.import(alias, keypair).await.unwrap();
427 }
428
429 pub fn get_keystore_by_identity(
430 &self,
431 key_identity: &KeyIdentity,
432 ) -> Result<&Keystore, anyhow::Error> {
433 if self
434 .config
435 .keystore
436 .get_by_identity(key_identity)
437 .map(|address| self.config.keystore.addresses().contains(&address))
438 .unwrap_or(false)
439 {
440 return Ok(&self.config.keystore);
441 }
442
443 if let Some(external_keys) = self.config.external_keys.as_ref()
444 && external_keys
445 .get_by_identity(key_identity)
446 .map(|address| external_keys.addresses().contains(&address))
447 .unwrap_or(false)
448 {
449 return Ok(external_keys);
450 }
451
452 Err(anyhow!(
453 "No keystore found for the provided key identity: {key_identity}"
454 ))
455 }
456
457 pub fn get_keystore_by_identity_mut(
458 &mut self,
459 key_identity: &KeyIdentity,
460 ) -> Result<&mut Keystore, anyhow::Error> {
461 if self.config.keystore.get_by_identity(key_identity).is_ok() {
462 return Ok(&mut self.config.keystore);
463 }
464
465 if let Some(external_keys) = self.config.external_keys.as_mut()
466 && external_keys.get_by_identity(key_identity).is_ok()
467 {
468 return Ok(external_keys);
469 }
470
471 Err(anyhow!(
472 "No keystore found for the provided key identity: {key_identity}"
473 ))
474 }
475
476 pub async fn sign_secure(
477 &self,
478 key_identity: &KeyIdentity,
479 data: &TransactionData,
480 intent: Intent,
481 ) -> Result<Signature, anyhow::Error> {
482 let keystore = self.get_keystore_by_identity(key_identity)?;
483 let address = keystore.get_by_identity(key_identity).map_err(|_| {
484 anyhow!("No address found for the provided key identity: {key_identity}")
485 })?;
486 let sig = keystore.sign_secure(&address, data, intent).await?;
487 Ok(sig)
488 }
489
490 pub async fn sign_transaction(&self, data: &TransactionData) -> Transaction {
492 let sig = self
493 .config
494 .keystore
495 .sign_secure(&data.sender(), data, Intent::sui_transaction())
496 .await
497 .unwrap();
498 Transaction::from_data(data.clone(), vec![sig])
500 }
501
502 pub async fn execute_transaction_must_succeed(&self, tx: Transaction) -> ExecutedTransaction {
505 tracing::debug!("Executing transaction: {:?}", tx);
506 let response = self.execute_transaction_may_fail(tx).await.unwrap();
507 assert!(
508 response.effects.status().is_ok(),
509 "Transaction failed: {:?}",
510 response
511 );
512 response
513 }
514
515 pub async fn execute_transaction_may_fail(
519 &self,
520 tx: Transaction,
521 ) -> anyhow::Result<ExecutedTransaction> {
522 self.grpc_client()?
523 .execute_transaction_and_wait_for_checkpoint(&tx)
524 .await
525 .map_err(Into::into)
526 }
527}