1use std::fmt::{Display, Formatter, Write};
6
7use anyhow::{anyhow, bail};
8use getset::{Getters, MutGetters};
9use iota_config::Config;
10use iota_keys::keystore::{AccountKeystore, Keystore};
11use iota_sdk_types::Address;
12use serde::{Deserialize, Serialize};
13use serde_with::serde_as;
14
15use crate::{
16 IOTA_DEVNET_GAS_URL, IOTA_DEVNET_GRAPHQL_URL, IOTA_DEVNET_GRPC_URL, IOTA_DEVNET_URL,
17 IOTA_LOCAL_NETWORK_GAS_URL, IOTA_LOCAL_NETWORK_GRAPHQL_URL, IOTA_LOCAL_NETWORK_GRPC_URL,
18 IOTA_LOCAL_NETWORK_URL, IOTA_MAINNET_GRAPHQL_URL, IOTA_MAINNET_GRPC_URL, IOTA_MAINNET_URL,
19 IOTA_TESTNET_GAS_URL, IOTA_TESTNET_GRAPHQL_URL, IOTA_TESTNET_GRPC_URL, IOTA_TESTNET_URL,
20 IotaClient, IotaClientBuilder,
21};
22
23#[serde_as]
26#[derive(Serialize, Deserialize, Getters, MutGetters)]
27#[getset(get = "pub", get_mut = "pub")]
28pub struct IotaClientConfig {
29 pub(crate) keystore: Keystore,
30 pub(crate) envs: Vec<IotaEnv>,
31 pub(crate) active_env: Option<String>,
32 pub(crate) active_address: Option<Address>,
33}
34
35impl IotaClientConfig {
36 pub fn new(keystore: impl Into<Keystore>) -> Self {
38 let keystore = keystore.into();
39 IotaClientConfig {
40 envs: Default::default(),
41 active_env: None,
42 active_address: keystore.addresses().first().copied(),
43 keystore,
44 }
45 }
46
47 pub fn with_default_envs(mut self) -> Self {
49 self.envs = vec![
51 IotaEnv::mainnet(),
52 IotaEnv::devnet(),
53 IotaEnv::testnet(),
54 IotaEnv::localnet(),
55 ];
56 self
57 }
58
59 pub fn with_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
61 self.set_envs(envs);
62 self
63 }
64
65 pub fn set_envs(&mut self, envs: impl IntoIterator<Item = IotaEnv>) {
67 self.envs = envs.into_iter().collect();
68 if let Some(env) = self.envs.first() {
69 self.set_active_env(env.alias().clone());
70 }
71 }
72
73 pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
75 self.set_active_env(env);
76 self
77 }
78
79 pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
81 self.active_env = env.into();
82 }
83
84 pub fn with_active_address(mut self, address: impl Into<Option<Address>>) -> Self {
86 self.set_active_address(address);
87 self
88 }
89
90 pub fn set_active_address(&mut self, address: impl Into<Option<Address>>) {
92 self.active_address = address.into();
93 }
94
95 pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
97 self.envs.iter().find(|env| env.alias == alias)
98 }
99
100 pub fn get_active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
102 self.active_env
103 .as_ref()
104 .and_then(|alias| self.get_env(alias))
105 .ok_or_else(|| {
106 anyhow!(
107 "Environment configuration not found for env [{}]",
108 self.active_env.as_deref().unwrap_or("None")
109 )
110 })
111 }
112
113 pub fn add_env(&mut self, env: IotaEnv) {
115 if self.get_env(&env.alias).is_none() {
116 if self
117 .active_env
118 .as_ref()
119 .and_then(|env| self.get_env(env))
120 .is_none()
121 {
122 self.set_active_env(env.alias.clone());
123 }
124 self.envs.push(env);
125 }
126 }
127
128 pub fn set_env(&mut self, env: IotaEnv) {
130 self.envs.retain(|e| e.alias != env.alias);
131 self.add_env(env);
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, Getters, MutGetters)]
138#[getset(get = "pub", get_mut = "pub")]
139pub struct IotaEnv {
140 pub(crate) alias: String,
141 pub(crate) rpc: String,
142 pub(crate) graphql: Option<String>,
143 pub(crate) ws: Option<String>,
144 #[serde(default)]
147 pub(crate) grpc: Option<String>,
148 pub(crate) basic_auth: Option<String>,
151 pub(crate) faucet: Option<String>,
152}
153
154impl IotaEnv {
155 pub fn new(alias: impl Into<String>, rpc: impl Into<String>) -> Self {
157 Self {
158 alias: alias.into(),
159 rpc: rpc.into(),
160 graphql: None,
161 ws: None,
162 grpc: None,
163 basic_auth: None,
164 faucet: None,
165 }
166 }
167
168 pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
170 self.set_graphql(graphql);
171 self
172 }
173
174 pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
176 self.graphql = graphql.into();
177 }
178
179 pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
181 self.set_ws(ws);
182 self
183 }
184
185 pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
187 self.ws = ws.into();
188 }
189
190 pub fn with_grpc(mut self, grpc: impl Into<Option<String>>) -> Self {
192 self.set_grpc(grpc);
193 self
194 }
195
196 pub fn set_grpc(&mut self, grpc: impl Into<Option<String>>) {
198 self.grpc = grpc.into();
199 }
200
201 pub fn with_basic_auth(mut self, basic_auth: impl Into<Option<String>>) -> Self {
203 self.set_basic_auth(basic_auth);
204 self
205 }
206
207 pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
209 self.basic_auth = basic_auth.into();
210 }
211
212 pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
214 self.set_faucet(faucet);
215 self
216 }
217
218 pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
220 self.faucet = faucet.into();
221 }
222
223 pub async fn create_rpc_client(
227 &self,
228 request_timeout: impl Into<Option<std::time::Duration>>,
229 max_concurrent_requests: impl Into<Option<u64>>,
230 ) -> Result<IotaClient, anyhow::Error> {
231 let request_timeout = request_timeout.into();
232 let max_concurrent_requests = max_concurrent_requests.into();
233 let mut builder = IotaClientBuilder::default();
234
235 if let Some(request_timeout) = request_timeout {
236 builder = builder.request_timeout(request_timeout);
237 }
238 if let Some(ws_url) = &self.ws {
239 builder = builder.ws_url(ws_url);
240 }
241 if let Some(basic_auth) = &self.basic_auth {
242 let fields: Vec<_> = basic_auth.split(':').collect();
243 if fields.len() != 2 {
244 bail!("Basic auth should be in the format `username:password`");
245 }
246 builder = builder.basic_auth(fields[0], fields[1]);
247 }
248
249 if let Some(max_concurrent_requests) = max_concurrent_requests {
250 builder = builder.max_concurrent_requests(max_concurrent_requests as usize);
251 }
252 Ok(builder.build(&self.rpc).await?)
253 }
254
255 pub fn create_grpc_client(&self) -> Result<iota_grpc_client::Client, anyhow::Error> {
259 let grpc_url = self
260 .grpc
261 .as_deref()
262 .ok_or_else(|| anyhow!("gRPC is not configured for environment [{}]", self.alias))?;
263 Ok(iota_grpc_client::Client::new(grpc_url)?)
264 }
265
266 pub fn mainnet() -> Self {
268 Self {
269 alias: "mainnet".to_string(),
270 rpc: IOTA_MAINNET_URL.into(),
271 graphql: Some(IOTA_MAINNET_GRAPHQL_URL.into()),
272 ws: None,
273 grpc: Some(IOTA_MAINNET_GRPC_URL.into()),
274 basic_auth: None,
275 faucet: None,
276 }
277 }
278
279 pub fn devnet() -> Self {
281 Self {
282 alias: "devnet".to_string(),
283 rpc: IOTA_DEVNET_URL.into(),
284 graphql: Some(IOTA_DEVNET_GRAPHQL_URL.into()),
285 ws: None,
286 grpc: Some(IOTA_DEVNET_GRPC_URL.into()),
287 basic_auth: None,
288 faucet: Some(IOTA_DEVNET_GAS_URL.into()),
289 }
290 }
291
292 pub fn testnet() -> Self {
294 Self {
295 alias: "testnet".to_string(),
296 rpc: IOTA_TESTNET_URL.into(),
297 graphql: Some(IOTA_TESTNET_GRAPHQL_URL.into()),
298 ws: None,
299 grpc: Some(IOTA_TESTNET_GRPC_URL.into()),
300 basic_auth: None,
301 faucet: Some(IOTA_TESTNET_GAS_URL.into()),
302 }
303 }
304
305 pub fn localnet() -> Self {
307 Self {
308 alias: "localnet".to_string(),
309 rpc: IOTA_LOCAL_NETWORK_URL.into(),
310 graphql: Some(IOTA_LOCAL_NETWORK_GRAPHQL_URL.into()),
311 ws: None,
312 grpc: Some(IOTA_LOCAL_NETWORK_GRPC_URL.into()),
313 basic_auth: None,
314 faucet: Some(IOTA_LOCAL_NETWORK_GAS_URL.into()),
315 }
316 }
317}
318
319impl Display for IotaEnv {
320 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
321 let mut writer = String::new();
322 writeln!(writer, "Active environment: {}", self.alias)?;
323 write!(writer, "RPC URL: {}", self.rpc)?;
324 if let Some(graphql) = &self.graphql {
325 writeln!(writer)?;
326 write!(writer, "GraphQL URL: {graphql}")?;
327 }
328 if let Some(ws) = &self.ws {
329 writeln!(writer)?;
330 write!(writer, "Websocket URL: {ws}")?;
331 }
332 if let Some(grpc) = &self.grpc {
333 writeln!(writer)?;
334 write!(writer, "gRPC URL: {grpc}")?;
335 }
336 if let Some(basic_auth) = &self.basic_auth {
337 writeln!(writer)?;
338 write!(writer, "Basic Auth: {basic_auth}")?;
339 }
340 if let Some(faucet) = &self.faucet {
341 writeln!(writer)?;
342 write!(writer, "Faucet URL: {faucet}")?;
343 }
344 write!(f, "{writer}")
345 }
346}
347
348impl Config for IotaClientConfig {}
349
350impl Display for IotaClientConfig {
351 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
352 let mut writer = String::new();
353
354 writeln!(
355 writer,
356 "Managed addresses: {}",
357 self.keystore.addresses().len()
358 )?;
359 write!(writer, "Active address: ")?;
360 match self.active_address {
361 Some(r) => writeln!(writer, "{r}")?,
362 None => writeln!(writer, "None")?,
363 };
364 writeln!(writer, "{}", self.keystore)?;
365 if let Ok(env) = self.get_active_env() {
366 write!(writer, "{env}")?;
367 }
368 write!(f, "{writer}")
369 }
370}