diff --git a/crates/sui-cluster-test/src/cluster.rs b/crates/sui-cluster-test/src/cluster.rs index 6c391d12a56c2..d978b27b14baa 100644 --- a/crates/sui-cluster-test/src/cluster.rs +++ b/crates/sui-cluster-test/src/cluster.rs @@ -377,6 +377,7 @@ pub async fn new_wallet_context_from_cluster( rpc: fullnode_url.into(), ws: None, basic_auth: None, + chain_id: None, }], active_address: Some(address), active_env: Some("localnet".to_string()), diff --git a/crates/sui-config/src/lib.rs b/crates/sui-config/src/lib.rs index ed31377ea8c4c..18e75b441e8a4 100644 --- a/crates/sui-config/src/lib.rs +++ b/crates/sui-config/src/lib.rs @@ -25,6 +25,7 @@ pub mod verifier_signing_config; pub use node::{ConsensusConfig, ExecutionCacheConfig, NodeConfig}; pub use rpc_config::{RpcConfig, RpcIndexInitConfig, RpcTlsConfig}; use sui_types::multiaddr::Multiaddr; +use tracing::debug; const SUI_DIR: &str = ".sui"; pub const SUI_CONFIG_DIR: &str = "sui_config"; @@ -117,6 +118,47 @@ where .with_context(|| format!("Unable to save config to {}", path.display()))?; Ok(()) } + + /// Load the config from the given path, acquiring a shared lock on the file during the read. + fn load_with_lock>(path: P) -> Result { + let path = path.as_ref(); + debug!("Reading config with lock from {}", path.display()); + let file = fs::File::open(path) + .with_context(|| format!("Unable to load config from {}", path.display()))?; + file.lock_shared()?; + let config: Self = serde_yaml::from_reader(&file)?; + file.unlock()?; + Ok(config) + } + + /// Save the config to the given path, acquiring an exclusive lock on the file during the + /// write. + fn save_with_lock>(&self, path: P) -> Result<(), anyhow::Error> { + let path = path.as_ref(); + debug!("Writing config with lock to {}", path.display()); + let config_str = serde_yaml::to_string(&self)?; + + let file = fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .with_context(|| { + format!( + "Unable to open config file for writing at {}", + path.display() + ) + })?; + + file.lock() + .with_context(|| format!("Unable to acquire exclusive lock on {}", path.display()))?; + + fs::write(path, config_str) + .with_context(|| format!("Unable to save config to {}", path.display()))?; + + file.unlock()?; + Ok(()) + } } pub struct PersistedConfig { @@ -136,6 +178,10 @@ where self.inner.save(&self.path) } + pub fn save_with_lock(&self) -> Result<(), anyhow::Error> { + self.inner.save_with_lock(&self.path) + } + pub fn into_inner(self) -> C { self.inner } diff --git a/crates/sui-sdk/src/sui_client_config.rs b/crates/sui-sdk/src/sui_client_config.rs index d02bed7f3d87f..ba2509cfe8fae 100644 --- a/crates/sui-sdk/src/sui_client_config.rs +++ b/crates/sui-sdk/src/sui_client_config.rs @@ -64,6 +64,21 @@ impl SuiClientConfig { self.envs.push(env) } } + + /// Update the cached chain ID for the specified environment. + pub fn update_env_chain_id( + &mut self, + alias: &str, + chain_id: String, + ) -> Result<(), anyhow::Error> { + let env = self + .envs + .iter_mut() + .find(|env| env.alias == alias) + .ok_or_else(|| anyhow!("Environment {} not found", alias))?; + env.chain_id = Some(chain_id); + Ok(()) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -73,6 +88,9 @@ pub struct SuiEnv { pub ws: Option, /// Basic HTTP access authentication in the format of username:password, if needed. pub basic_auth: Option, + /// Cached chain identifier for this environment. + #[serde(skip_serializing_if = "Option::is_none")] + pub chain_id: Option, } impl SuiEnv { @@ -110,6 +128,7 @@ impl SuiEnv { rpc: SUI_DEVNET_URL.into(), ws: None, basic_auth: None, + chain_id: None, } } pub fn testnet() -> Self { @@ -118,6 +137,7 @@ impl SuiEnv { rpc: SUI_TESTNET_URL.into(), ws: None, basic_auth: None, + chain_id: None, } } @@ -127,6 +147,7 @@ impl SuiEnv { rpc: SUI_LOCAL_NETWORK_URL.into(), ws: None, basic_auth: None, + chain_id: None, } } } @@ -144,6 +165,10 @@ impl Display for SuiEnv { writeln!(writer)?; write!(writer, "Basic Auth: {}", basic_auth)?; } + if let Some(chain_id) = &self.chain_id { + writeln!(writer)?; + write!(writer, "Chain ID: {}", chain_id)?; + } write!(f, "{}", writer) } } diff --git a/crates/sui-sdk/src/wallet_context.rs b/crates/sui-sdk/src/wallet_context.rs index 02f2aa0dee36a..5b60bee66359b 100644 --- a/crates/sui-sdk/src/wallet_context.rs +++ b/crates/sui-sdk/src/wallet_context.rs @@ -22,6 +22,7 @@ use sui_types::crypto::{Signature, SuiKeyPair}; use sui_types::gas_coin::GasCoin; use sui_types::transaction::{Transaction, TransactionData, TransactionDataAPI}; use tokio::sync::RwLock; +use tracing::info; pub struct WalletContext { pub config: PersistedConfig, @@ -127,10 +128,48 @@ impl WalletContext { .get_active_env()? .create_rpc_client(self.request_timeout, self.max_concurrent_requests) .await?; + self.client.write().await.insert(client).clone() }) } + /// Load the chain ID corresponding to the active environment, or fetch and cache it if not + /// present. + /// + /// The chain ID is cached in the `client.yaml` file to avoid redundant network requests. + pub async fn load_or_cache_chain_id( + &self, + client: &SuiClient, + ) -> Result { + self.internal_load_or_cache_chain_id(client, false).await + } + + /// Cache (or recache) chain ID for the active environment by fetching it from the + /// network + pub async fn cache_chain_id(&self, client: &SuiClient) -> Result { + self.internal_load_or_cache_chain_id(client, true).await + } + + async fn internal_load_or_cache_chain_id( + &self, + client: &SuiClient, + force_recache: bool, + ) -> Result { + let env = self.get_active_env()?; + if !force_recache && env.chain_id.is_some() { + let chain_id = env.chain_id.as_ref().unwrap(); + info!("Found cached chain ID for env {}: {}", env.alias, chain_id); + return Ok(chain_id.clone()); + } + let chain_id = client.read_api().get_chain_identifier().await?; + let path = self.config.path(); + let mut config_result = SuiClientConfig::load_with_lock(path)?; + + config_result.update_env_chain_id(&env.alias, chain_id.clone())?; + config_result.save_with_lock(path)?; + Ok(chain_id) + } + pub fn get_active_env(&self) -> Result<&SuiEnv, anyhow::Error> { if self.env_override.is_some() { self.config.get_env(&self.env_override).ok_or_else(|| { diff --git a/crates/sui/src/client_commands.rs b/crates/sui/src/client_commands.rs index 56a81abfea849..91f3722a13c4b 100644 --- a/crates/sui/src/client_commands.rs +++ b/crates/sui/src/client_commands.rs @@ -770,6 +770,7 @@ impl SuiClientCommands { } => { let address = context.get_identity_address(address)?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let mut objects: Vec = Vec::new(); let mut cursor = None; @@ -840,6 +841,7 @@ impl SuiClientCommands { SuiClientCommands::DynamicFieldQuery { id, cursor, limit } => { let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let df_read = client .read_api() .get_dynamic_fields(id, cursor, Some(limit)) @@ -861,6 +863,7 @@ impl SuiClientCommands { } => { let sender = context.infer_sender(&payment.gas).await?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let read_api = client.read_api(); let chain_id = read_api.get_chain_identifier().await.ok(); @@ -1023,6 +1026,7 @@ impl SuiClientCommands { let sender = context.infer_sender(&payment.gas).await?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let read_api = client.read_api(); let chain_id = read_api.get_chain_identifier().await.ok(); @@ -1116,6 +1120,7 @@ impl SuiClientCommands { build_config, } => { let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let read_api = client.read_api(); let protocol_version = protocol_version.map_or(ProtocolVersion::MAX, ProtocolVersion::new); @@ -1200,6 +1205,7 @@ impl SuiClientCommands { SuiClientCommands::Object { id, bcs } => { // Fetch the object ref let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; if !bcs { let object_read = client .read_api() @@ -1217,6 +1223,7 @@ impl SuiClientCommands { SuiClientCommands::TransactionBlock { digest } => { let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_read = client .read_api() .get_transaction_with_options( @@ -1258,6 +1265,7 @@ impl SuiClientCommands { .collect::>(); let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client .transaction_builder() @@ -1291,6 +1299,7 @@ impl SuiClientCommands { let signer = context.get_object_owner(&object_id).await?; let to = context.get_identity_address(Some(to))?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client .transaction_builder() @@ -1323,6 +1332,7 @@ impl SuiClientCommands { let signer = context.get_object_owner(&object_id).await?; let to = context.get_identity_address(Some(to))?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client .transaction_builder() @@ -1375,6 +1385,7 @@ impl SuiClientCommands { .map_err(|e| anyhow!("{e}"))?; let signer = context.get_object_owner(&input_coins[0]).await?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client .transaction_builder() .pay_tx_kind(input_coins.clone(), recipients.clone(), amounts.clone()) @@ -1431,6 +1442,7 @@ impl SuiClientCommands { .map_err(|e| anyhow!("{e}"))?; let signer = context.get_object_owner(&input_coins[0]).await?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client .transaction_builder() @@ -1465,6 +1477,7 @@ impl SuiClientCommands { let recipient = context.get_identity_address(Some(recipient))?; let signer = context.get_object_owner(&input_coins[0]).await?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let tx_kind = client.transaction_builder().pay_all_sui_tx_kind(recipient); let gas_payment = client @@ -1486,6 +1499,7 @@ impl SuiClientCommands { SuiClientCommands::Objects { address } => { let address = context.get_identity_address(address)?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let mut objects: Vec = Vec::new(); let mut cursor = None; loop { @@ -1558,6 +1572,7 @@ impl SuiClientCommands { // Ok to unwrap() since `get_gas_objects` guarantees gas .map(|(_val, object)| GasCoin::try_from(object).unwrap()) .collect(); + let _ = context.cache_chain_id(&context.get_client().await?).await?; SuiClientCommandResult::Gas(coins) } SuiClientCommands::Faucet { address, url } => { @@ -1592,15 +1607,12 @@ impl SuiClientCommands { } }; request_tokens_from_faucet(address, url).await?; + let _ = context.cache_chain_id(&context.get_client().await?).await?; SuiClientCommandResult::NoOutput } SuiClientCommands::ChainIdentifier => { - let ci = context - .get_client() - .await? - .read_api() - .get_chain_identifier() - .await?; + let client = context.get_client().await?; + let ci = context.cache_chain_id(&client).await?; SuiClientCommandResult::ChainIdentifier(ci) } SuiClientCommands::SplitCoin { @@ -1619,6 +1631,7 @@ impl SuiClientCommands { } let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let signer = context.get_object_owner(&coin_id).await?; let tx_kind = client @@ -1649,6 +1662,7 @@ impl SuiClientCommands { processing, } => { let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let signer = context.get_object_owner(&primary_coin).await?; let tx_kind = client @@ -1811,17 +1825,20 @@ impl SuiClientCommands { "Environment config with name [{alias}] already exists." )); } - let env = SuiEnv { + let mut env = SuiEnv { alias, rpc, ws, basic_auth, + chain_id: None, }; // Check urls are valid and server is reachable env.create_rpc_client(None, None).await?; context.config.envs.push(env.clone()); context.config.save()?; + let chain_id = context.cache_chain_id(&context.get_client().await?).await?; + env.chain_id = Some(chain_id); SuiClientCommandResult::NewEnv(env) } SuiClientCommands::ActiveEnv => SuiClientCommandResult::ActiveEnv( @@ -1852,12 +1869,7 @@ impl SuiClientCommands { build_config.implicit_dependencies = implicit_deps(latest_system_packages()); let build_config = resolve_lock_file_path(build_config, Some(&package_path))?; - let chain_id = context - .get_client() - .await? - .read_api() - .get_chain_identifier() - .await?; + let chain_id = context.cache_chain_id(&context.get_client().await?).await?; let compiled_package = BuildConfig { config: build_config, run_bytecode_verifier: true, @@ -1883,6 +1895,7 @@ impl SuiClientCommands { let signer = context.get_object_owner(&object_id).await?; let to = context.get_identity_address(Some(to))?; let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; let transaction_builder = client.transaction_builder(); let (full_obj_ref, object_type) = transaction_builder @@ -1939,6 +1952,8 @@ impl SuiClientCommands { .await? } SuiClientCommands::PTB(ptb) => { + let client = context.get_client().await?; + let _ = context.cache_chain_id(&client).await?; ptb.execute(context).await?; SuiClientCommandResult::NoOutput } diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index c038cca8895af..2de249a1e67f6 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1203,6 +1203,7 @@ async fn start( rpc: fullnode_rpc_url, ws: None, basic_auth: None, + chain_id: None, }], active_address: Some(address), active_env: Some("localnet".to_string()), @@ -1507,6 +1508,7 @@ async fn genesis( ), ws: None, basic_auth: None, + chain_id: None, }); client_config.add_env(SuiEnv::devnet()); @@ -1532,6 +1534,7 @@ async fn prompt_if_no_config( rpc: v.into_string().unwrap(), ws: None, basic_auth: None, + chain_id: None, }), None => { if accept_defaults { @@ -1571,6 +1574,7 @@ async fn prompt_if_no_config( rpc: url, ws: None, basic_auth: None, + chain_id: None, } }) } else { @@ -1631,6 +1635,9 @@ async fn prompt_if_no_config( } .persisted(wallet_conf_path) .save()?; + + let context = WalletContext::new(wallet_conf_path)?; + let _ = context.cache_chain_id(&context.get_client().await?).await?; } } Ok(()) diff --git a/crates/test-cluster/src/lib.rs b/crates/test-cluster/src/lib.rs index a1bb2408a1358..2629dd29c5907 100644 --- a/crates/test-cluster/src/lib.rs +++ b/crates/test-cluster/src/lib.rs @@ -1207,6 +1207,7 @@ impl TestClusterBuilder { rpc: rpc_url, ws: None, basic_auth: None, + chain_id: None, }); wallet_conf.active_env = Some("localnet".to_string());