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