1use 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 pub keystore: Keystore,
28 pub external_keys: Option<Keystore>,
30 pub envs: Vec<SuiEnv>,
32 pub active_env: Option<String>,
34 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 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 pub basic_auth: Option<String>,
99 #[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}