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_types::base_types::*;
12use serde::{Deserialize, Serialize};
13use serde_with::serde_as;
14
15use crate::{
16 IOTA_DEVNET_GAS_URL, IOTA_DEVNET_GRAPHQL_URL, IOTA_DEVNET_URL, IOTA_LOCAL_NETWORK_GAS_URL,
17 IOTA_LOCAL_NETWORK_GRAPHQL_URL, IOTA_LOCAL_NETWORK_URL, IOTA_MAINNET_GRAPHQL_URL,
18 IOTA_MAINNET_URL, IOTA_TESTNET_GAS_URL, IOTA_TESTNET_GRAPHQL_URL, IOTA_TESTNET_URL, IotaClient,
19 IotaClientBuilder,
20};
21
22#[serde_as]
25#[derive(Serialize, Deserialize, Getters, MutGetters)]
26#[getset(get = "pub", get_mut = "pub")]
27pub struct IotaClientConfig {
28 pub(crate) keystore: Keystore,
29 pub(crate) envs: Vec<IotaEnv>,
30 pub(crate) active_env: Option<String>,
31 pub(crate) active_address: Option<IotaAddress>,
32}
33
34impl IotaClientConfig {
35 pub fn new(keystore: impl Into<Keystore>) -> Self {
37 let keystore = keystore.into();
38 IotaClientConfig {
39 envs: Default::default(),
40 active_env: None,
41 active_address: keystore.addresses().first().copied(),
42 keystore,
43 }
44 }
45
46 pub fn with_default_envs(mut self) -> Self {
48 self.envs = vec![
50 IotaEnv::mainnet(),
51 IotaEnv::devnet(),
52 IotaEnv::testnet(),
53 IotaEnv::localnet(),
54 ];
55 self
56 }
57
58 pub fn with_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
60 self.set_envs(envs);
61 self
62 }
63
64 pub fn set_envs(&mut self, envs: impl IntoIterator<Item = IotaEnv>) {
66 self.envs = envs.into_iter().collect();
67 if let Some(env) = self.envs.first() {
68 self.set_active_env(env.alias().clone());
69 }
70 }
71
72 pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
74 self.set_active_env(env);
75 self
76 }
77
78 pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
80 self.active_env = env.into();
81 }
82
83 pub fn with_active_address(mut self, address: impl Into<Option<IotaAddress>>) -> Self {
85 self.set_active_address(address);
86 self
87 }
88
89 pub fn set_active_address(&mut self, address: impl Into<Option<IotaAddress>>) {
91 self.active_address = address.into();
92 }
93
94 pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
96 self.envs.iter().find(|env| env.alias == alias)
97 }
98
99 pub fn get_active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
101 self.active_env
102 .as_ref()
103 .and_then(|alias| self.get_env(alias))
104 .ok_or_else(|| {
105 anyhow!(
106 "Environment configuration not found for env [{}]",
107 self.active_env.as_deref().unwrap_or("None")
108 )
109 })
110 }
111
112 pub fn add_env(&mut self, env: IotaEnv) {
114 if self.get_env(&env.alias).is_none() {
115 if self
116 .active_env
117 .as_ref()
118 .and_then(|env| self.get_env(env))
119 .is_none()
120 {
121 self.set_active_env(env.alias.clone());
122 }
123 self.envs.push(env);
124 }
125 }
126
127 pub fn set_env(&mut self, env: IotaEnv) {
129 self.envs.retain(|e| e.alias != env.alias);
130 self.add_env(env);
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, Getters, MutGetters)]
137#[getset(get = "pub", get_mut = "pub")]
138pub struct IotaEnv {
139 pub(crate) alias: String,
140 pub(crate) rpc: String,
141 pub(crate) graphql: Option<String>,
142 pub(crate) ws: Option<String>,
143 pub(crate) basic_auth: Option<String>,
146 pub(crate) faucet: Option<String>,
147}
148
149impl IotaEnv {
150 pub fn new(alias: impl Into<String>, rpc: impl Into<String>) -> Self {
152 Self {
153 alias: alias.into(),
154 rpc: rpc.into(),
155 graphql: None,
156 ws: None,
157 basic_auth: None,
158 faucet: None,
159 }
160 }
161
162 pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
164 self.set_graphql(graphql);
165 self
166 }
167
168 pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
170 self.graphql = graphql.into();
171 }
172
173 pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
175 self.set_ws(ws);
176 self
177 }
178
179 pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
181 self.ws = ws.into();
182 }
183
184 pub fn with_basic_auth(mut self, basic_auth: impl Into<Option<String>>) -> Self {
186 self.set_basic_auth(basic_auth);
187 self
188 }
189
190 pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
192 self.basic_auth = basic_auth.into();
193 }
194
195 pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
197 self.set_faucet(faucet);
198 self
199 }
200
201 pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
203 self.faucet = faucet.into();
204 }
205
206 pub async fn create_rpc_client(
210 &self,
211 request_timeout: impl Into<Option<std::time::Duration>>,
212 max_concurrent_requests: impl Into<Option<u64>>,
213 ) -> Result<IotaClient, anyhow::Error> {
214 let request_timeout = request_timeout.into();
215 let max_concurrent_requests = max_concurrent_requests.into();
216 let mut builder = IotaClientBuilder::default();
217
218 if let Some(request_timeout) = request_timeout {
219 builder = builder.request_timeout(request_timeout);
220 }
221 if let Some(ws_url) = &self.ws {
222 builder = builder.ws_url(ws_url);
223 }
224 if let Some(basic_auth) = &self.basic_auth {
225 let fields: Vec<_> = basic_auth.split(':').collect();
226 if fields.len() != 2 {
227 bail!("Basic auth should be in the format `username:password`");
228 }
229 builder = builder.basic_auth(fields[0], fields[1]);
230 }
231
232 if let Some(max_concurrent_requests) = max_concurrent_requests {
233 builder = builder.max_concurrent_requests(max_concurrent_requests as usize);
234 }
235 Ok(builder.build(&self.rpc).await?)
236 }
237
238 pub fn mainnet() -> Self {
240 Self {
241 alias: "mainnet".to_string(),
242 rpc: IOTA_MAINNET_URL.into(),
243 graphql: Some(IOTA_MAINNET_GRAPHQL_URL.into()),
244 ws: None,
245 basic_auth: None,
246 faucet: None,
247 }
248 }
249
250 pub fn devnet() -> Self {
252 Self {
253 alias: "devnet".to_string(),
254 rpc: IOTA_DEVNET_URL.into(),
255 graphql: Some(IOTA_DEVNET_GRAPHQL_URL.into()),
256 ws: None,
257 basic_auth: None,
258 faucet: Some(IOTA_DEVNET_GAS_URL.into()),
259 }
260 }
261
262 pub fn testnet() -> Self {
264 Self {
265 alias: "testnet".to_string(),
266 rpc: IOTA_TESTNET_URL.into(),
267 graphql: Some(IOTA_TESTNET_GRAPHQL_URL.into()),
268 ws: None,
269 basic_auth: None,
270 faucet: Some(IOTA_TESTNET_GAS_URL.into()),
271 }
272 }
273
274 pub fn localnet() -> Self {
276 Self {
277 alias: "localnet".to_string(),
278 rpc: IOTA_LOCAL_NETWORK_URL.into(),
279 graphql: Some(IOTA_LOCAL_NETWORK_GRAPHQL_URL.into()),
280 ws: None,
281 basic_auth: None,
282 faucet: Some(IOTA_LOCAL_NETWORK_GAS_URL.into()),
283 }
284 }
285}
286
287impl Display for IotaEnv {
288 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
289 let mut writer = String::new();
290 writeln!(writer, "Active environment: {}", self.alias)?;
291 write!(writer, "RPC URL: {}", self.rpc)?;
292 if let Some(graphql) = &self.graphql {
293 writeln!(writer)?;
294 write!(writer, "GraphQL URL: {graphql}")?;
295 }
296 if let Some(ws) = &self.ws {
297 writeln!(writer)?;
298 write!(writer, "Websocket URL: {ws}")?;
299 }
300 if let Some(basic_auth) = &self.basic_auth {
301 writeln!(writer)?;
302 write!(writer, "Basic Auth: {basic_auth}")?;
303 }
304 if let Some(faucet) = &self.faucet {
305 writeln!(writer)?;
306 write!(writer, "Faucet URL: {faucet}")?;
307 }
308 write!(f, "{writer}")
309 }
310}
311
312impl Config for IotaClientConfig {}
313
314impl Display for IotaClientConfig {
315 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
316 let mut writer = String::new();
317
318 writeln!(
319 writer,
320 "Managed addresses: {}",
321 self.keystore.addresses().len()
322 )?;
323 write!(writer, "Active address: ")?;
324 match self.active_address {
325 Some(r) => writeln!(writer, "{r}")?,
326 None => writeln!(writer, "None")?,
327 };
328 writeln!(writer, "{}", self.keystore)?;
329 if let Ok(env) = self.get_active_env() {
330 write!(writer, "{env}")?;
331 }
332 write!(f, "{writer}")
333 }
334}