iota_sdk/
iota_client_config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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/// Configuration for the IOTA client, containing a [`Keystore`] and potentially
23/// multiple [`IotaEnv`]s.
24#[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    /// Create a new [`IotaClientConfig`] with the given keystore.
36    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    /// Set the default [`IotaEnv`]s for mainnet, devnet, testnet, and localnet.
47    pub fn with_default_envs(mut self) -> Self {
48        // We don't want to set any particular one of the default networks as active.
49        self.envs = vec![
50            IotaEnv::mainnet(),
51            IotaEnv::devnet(),
52            IotaEnv::testnet(),
53            IotaEnv::localnet(),
54        ];
55        self
56    }
57
58    /// Set the [`IotaEnv`]s.
59    pub fn with_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
60        self.set_envs(envs);
61        self
62    }
63
64    /// Set the [`IotaEnv`]s. Also sets the active env to the first in the list.
65    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    /// Set the active [`IotaEnv`] by its alias.
73    pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
74        self.set_active_env(env);
75        self
76    }
77
78    /// Set the active [`IotaEnv`] by its alias.
79    pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
80        self.active_env = env.into();
81    }
82
83    /// Set the active [`IotaAddress`].
84    pub fn with_active_address(mut self, address: impl Into<Option<IotaAddress>>) -> Self {
85        self.set_active_address(address);
86        self
87    }
88
89    /// Set the active [`IotaAddress`].
90    pub fn set_active_address(&mut self, address: impl Into<Option<IotaAddress>>) {
91        self.active_address = address.into();
92    }
93
94    /// Get an [`IotaEnv`] by its alias.
95    pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
96        self.envs.iter().find(|env| env.alias == alias)
97    }
98
99    /// Get the active [`IotaEnv`].
100    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    /// Add an [`IotaEnv`] if there's no env with the same alias already.
113    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    /// Set an [`IotaEnv`]. Replaces any existing env with the same alias.
128    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/// IOTA environment configuration, containing the RPC URL, and optional
135/// websocket, basic auth and faucet options.
136#[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    /// Basic HTTP access authentication in the format of username:password, if
144    /// needed.
145    pub(crate) basic_auth: Option<String>,
146    pub(crate) faucet: Option<String>,
147}
148
149impl IotaEnv {
150    /// Create a new [`IotaEnv`] with the given alias and RPC URL such as <https://api.testnet.iota.cafe>.
151    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    /// Set a graphql URL.
163    pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
164        self.set_graphql(graphql);
165        self
166    }
167
168    /// Set a graphql URL.
169    pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
170        self.graphql = graphql.into();
171    }
172
173    /// Set a websocket URL.
174    pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
175        self.set_ws(ws);
176        self
177    }
178
179    /// Set a websocket URL.
180    pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
181        self.ws = ws.into();
182    }
183
184    /// Set basic authentication information in the format of username:password.
185    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    /// Set basic authentication information in the format of username:password.
191    pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
192        self.basic_auth = basic_auth.into();
193    }
194
195    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
196    pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
197        self.set_faucet(faucet);
198        self
199    }
200
201    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
202    pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
203        self.faucet = faucet.into();
204    }
205
206    /// Create an [`IotaClient`] with the given request timeout, max
207    /// concurrent requests and possible configured websocket URL and basic
208    /// auth.
209    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    /// Create the env with the default mainnet configuration.
239    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    /// Create the env with the default devnet configuration.
251    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    /// Create the env with the default testnet configuration.
263    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    /// Create the env with the default localnet configuration.
275    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}