sui_sdk/
sui_client_config.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::{Display, Formatter, Write};
5
6use anyhow::anyhow;
7use serde::{Deserialize, Serialize};
8use serde_with::serde_as;
9
10use crate::{
11    SUI_DEVNET_URL, SUI_LOCAL_NETWORK_URL, SUI_MAINNET_URL, SUI_TESTNET_URL, SuiClient,
12    SuiClientBuilder,
13};
14use sui_config::Config;
15use sui_keys::keystore::{AccountKeystore, Keystore};
16use sui_rpc_api::Client;
17use sui_rpc_api::client::HeadersInterceptor;
18use sui_types::{
19    base_types::*,
20    digests::{get_mainnet_chain_identifier, get_testnet_chain_identifier},
21};
22
23#[serde_as]
24#[derive(Serialize, Deserialize)]
25pub struct SuiClientConfig {
26    /// The keystore that holds the user's private keys, typically filebased keystore
27    pub keystore: Keystore,
28    /// Optional external keystore for managing keys that are not stored in the main keystore.
29    pub external_keys: Option<Keystore>,
30    /// List of environments that the client can connect to.
31    pub envs: Vec<SuiEnv>,
32    /// The alias of the currently active environment.
33    pub active_env: Option<String>,
34    /// The address that is currently active in the keystore.
35    pub active_address: Option<SuiAddress>,
36}
37
38impl SuiClientConfig {
39    pub fn new(keystore: Keystore) -> Self {
40        SuiClientConfig {
41            keystore,
42            external_keys: None,
43            envs: vec![],
44            active_env: None,
45            active_address: None,
46        }
47    }
48
49    pub fn get_env(&self, alias: &Option<String>) -> Option<&SuiEnv> {
50        if let Some(alias) = alias {
51            self.envs.iter().find(|env| &env.alias == alias)
52        } else {
53            self.envs.first()
54        }
55    }
56
57    pub fn get_active_env(&self) -> Result<&SuiEnv, anyhow::Error> {
58        self.get_env(&self.active_env).ok_or_else(|| {
59            anyhow!(
60                "Environment configuration not found for env [{}]",
61                self.active_env.as_deref().unwrap_or("None")
62            )
63        })
64    }
65
66    pub fn add_env(&mut self, env: SuiEnv) {
67        if !self
68            .envs
69            .iter()
70            .any(|other_env| other_env.alias == env.alias)
71        {
72            self.envs.push(env)
73        }
74    }
75
76    /// Update the cached chain ID for the specified environment.
77    pub fn update_env_chain_id(
78        &mut self,
79        alias: &str,
80        chain_id: String,
81    ) -> Result<(), anyhow::Error> {
82        let env = self
83            .envs
84            .iter_mut()
85            .find(|env| env.alias == alias)
86            .ok_or_else(|| anyhow!("Environment {} not found", alias))?;
87        env.chain_id = Some(chain_id);
88        Ok(())
89    }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SuiEnv {
94    pub alias: String,
95    pub rpc: String,
96    pub ws: Option<String>,
97    /// Basic HTTP access authentication in the format of username:password, if needed.
98    pub basic_auth: Option<String>,
99    /// Cached chain identifier for this environment.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub chain_id: Option<String>,
102}
103
104impl SuiEnv {
105    pub async fn create_rpc_client(
106        &self,
107        request_timeout: Option<std::time::Duration>,
108        max_concurrent_requests: Option<u64>,
109    ) -> Result<SuiClient, anyhow::Error> {
110        let mut builder = SuiClientBuilder::default();
111        if let Some(request_timeout) = request_timeout {
112            builder = builder.request_timeout(request_timeout);
113        }
114        if let Some(ws_url) = &self.ws {
115            builder = builder.ws_url(ws_url);
116        }
117        if let Some(basic_auth) = &self.basic_auth {
118            let fields: Vec<_> = basic_auth.split(':').collect();
119            if fields.len() != 2 {
120                return Err(anyhow!(
121                    "Basic auth should be in the format `username:password`"
122                ));
123            }
124            builder = builder.basic_auth(fields[0], fields[1]);
125        }
126
127        if let Some(max_concurrent_requests) = max_concurrent_requests {
128            builder = builder.max_concurrent_requests(max_concurrent_requests as usize);
129        }
130        Ok(builder.build(&self.rpc).await?)
131    }
132
133    pub fn create_grpc_client(&self) -> Result<Client, anyhow::Error> {
134        let mut client = Client::new(&self.rpc)?;
135
136        if let Some(basic_auth) = &self.basic_auth {
137            let fields: Vec<_> = basic_auth.split(':').collect();
138            if fields.len() != 2 {
139                return Err(anyhow!(
140                    "Basic auth should be in the format `username:password`"
141                ));
142            }
143            let mut headers = HeadersInterceptor::new();
144            headers.basic_auth(fields[0], Some(fields[1]));
145            client = client.with_headers(headers);
146        }
147
148        Ok(client)
149    }
150
151    pub fn devnet() -> Self {
152        Self {
153            alias: "devnet".to_string(),
154            rpc: SUI_DEVNET_URL.into(),
155            ws: None,
156            basic_auth: None,
157            chain_id: None,
158        }
159    }
160    pub fn testnet() -> Self {
161        Self {
162            alias: "testnet".to_string(),
163            rpc: SUI_TESTNET_URL.into(),
164            ws: None,
165            basic_auth: None,
166            chain_id: Some(get_testnet_chain_identifier().to_string()),
167        }
168    }
169
170    pub fn localnet() -> Self {
171        Self {
172            alias: "local".to_string(),
173            rpc: SUI_LOCAL_NETWORK_URL.into(),
174            ws: None,
175            basic_auth: None,
176            chain_id: None,
177        }
178    }
179
180    pub fn mainnet() -> Self {
181        Self {
182            alias: "mainnet".to_string(),
183            rpc: SUI_MAINNET_URL.into(),
184            ws: None,
185            basic_auth: None,
186            chain_id: Some(get_mainnet_chain_identifier().to_string()),
187        }
188    }
189}
190
191impl Display for SuiEnv {
192    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
193        let mut writer = String::new();
194        writeln!(writer, "Active environment : {}", self.alias)?;
195        write!(writer, "RPC URL: {}", self.rpc)?;
196        if let Some(ws) = &self.ws {
197            writeln!(writer)?;
198            write!(writer, "Websocket URL: {ws}")?;
199        }
200        if let Some(basic_auth) = &self.basic_auth {
201            writeln!(writer)?;
202            write!(writer, "Basic Auth: {}", basic_auth)?;
203        }
204        if let Some(chain_id) = &self.chain_id {
205            writeln!(writer)?;
206            write!(writer, "Chain ID: {}", chain_id)?;
207        }
208        write!(f, "{}", writer)
209    }
210}
211
212impl Config for SuiClientConfig {}
213
214impl Display for SuiClientConfig {
215    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
216        let mut writer = String::new();
217
218        writeln!(
219            writer,
220            "Managed addresses : {}",
221            self.keystore.addresses().len()
222        )?;
223        write!(writer, "Active address: ")?;
224        match self.active_address {
225            Some(r) => writeln!(writer, "{}", r)?,
226            None => writeln!(writer, "None")?,
227        };
228        writeln!(writer, "{}", self.keystore)?;
229        if let Ok(env) = self.get_active_env() {
230            write!(writer, "{}", env)?;
231        }
232        write!(f, "{}", writer)
233    }
234}