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;
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_TESTNET_GAS_URL,
18    IOTA_TESTNET_GRAPHQL_URL, IOTA_TESTNET_URL, IotaClient, IotaClientBuilder,
19};
20
21/// Configuration for the IOTA client, containing a
22/// [`Keystore`](iota_keys::keystore::Keystore) and potentially multiple
23/// [`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 [`IotaEnv`]s.
47    pub fn with_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
48        self.set_envs(envs);
49        self
50    }
51
52    /// Set the [`IotaEnv`]s. Also sets the active env to the first in the list.
53    pub fn set_envs(&mut self, envs: impl IntoIterator<Item = IotaEnv>) {
54        self.envs = envs.into_iter().collect();
55        if let Some(env) = self.envs.first() {
56            self.set_active_env(env.alias().clone());
57        }
58    }
59
60    /// Set the active [`IotaEnv`] by its alias.
61    pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
62        self.set_active_env(env);
63        self
64    }
65
66    /// Set the active [`IotaEnv`] by its alias.
67    pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
68        self.active_env = env.into();
69    }
70
71    /// Set the active [`IotaAddress`].
72    pub fn with_active_address(mut self, address: impl Into<Option<IotaAddress>>) -> Self {
73        self.set_active_address(address);
74        self
75    }
76
77    /// Set the active [`IotaAddress`].
78    pub fn set_active_address(&mut self, address: impl Into<Option<IotaAddress>>) {
79        self.active_address = address.into();
80    }
81
82    /// Get an [`IotaEnv`] by its alias.
83    pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
84        self.envs.iter().find(|env| env.alias == alias)
85    }
86
87    /// Get the active [`IotaEnv`].
88    pub fn get_active_env(&self) -> Result<&IotaEnv, anyhow::Error> {
89        self.active_env
90            .as_ref()
91            .and_then(|alias| self.get_env(alias))
92            .ok_or_else(|| {
93                anyhow!(
94                    "Environment configuration not found for env [{}]",
95                    self.active_env.as_deref().unwrap_or("None")
96                )
97            })
98    }
99
100    /// Add an [`IotaEnv`].
101    pub fn add_env(&mut self, env: IotaEnv) {
102        if self.get_env(&env.alias).is_none() {
103            if self
104                .active_env
105                .as_ref()
106                .and_then(|env| self.get_env(env))
107                .is_none()
108            {
109                self.set_active_env(env.alias.clone());
110            }
111            self.envs.push(env);
112        }
113    }
114}
115
116/// IOTA environment configuration, containing the RPC URL, and optional
117/// websocket, basic auth and faucet options.
118#[derive(Debug, Clone, Serialize, Deserialize, Getters, MutGetters)]
119#[getset(get = "pub", get_mut = "pub")]
120pub struct IotaEnv {
121    pub(crate) alias: String,
122    pub(crate) rpc: String,
123    pub(crate) graphql: Option<String>,
124    pub(crate) ws: Option<String>,
125    /// Basic HTTP access authentication in the format of username:password, if
126    /// needed.
127    pub(crate) basic_auth: Option<String>,
128    pub(crate) faucet: Option<String>,
129}
130
131impl IotaEnv {
132    /// Create a new [`IotaEnv`] with the given alias and RPC URL such as <https://api.testnet.iota.cafe>.
133    pub fn new(alias: impl Into<String>, rpc: impl Into<String>) -> Self {
134        Self {
135            alias: alias.into(),
136            rpc: rpc.into(),
137            graphql: None,
138            ws: None,
139            basic_auth: None,
140            faucet: None,
141        }
142    }
143
144    /// Set a graphql URL.
145    pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
146        self.set_graphql(graphql);
147        self
148    }
149
150    /// Set a graphql URL.
151    pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
152        self.graphql = graphql.into();
153    }
154
155    /// Set a websocket URL.
156    pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
157        self.set_ws(ws);
158        self
159    }
160
161    /// Set a websocket URL.
162    pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
163        self.ws = ws.into();
164    }
165
166    /// Set basic authentication information in the format of username:password.
167    pub fn with_basic_auth(mut self, basic_auth: impl Into<Option<String>>) -> Self {
168        self.set_basic_auth(basic_auth);
169        self
170    }
171
172    /// Set basic authentication information in the format of username:password.
173    pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
174        self.basic_auth = basic_auth.into();
175    }
176
177    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
178    pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
179        self.set_faucet(faucet);
180        self
181    }
182
183    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
184    pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
185        self.faucet = faucet.into();
186    }
187
188    /// Create an [`IotaClient`] with the given request timeout, max
189    /// concurrent requests and possible configured websocket URL and basic
190    /// auth.
191    pub async fn create_rpc_client(
192        &self,
193        request_timeout: impl Into<Option<std::time::Duration>>,
194        max_concurrent_requests: impl Into<Option<u64>>,
195    ) -> Result<IotaClient, anyhow::Error> {
196        let request_timeout = request_timeout.into();
197        let max_concurrent_requests = max_concurrent_requests.into();
198        let mut builder = IotaClientBuilder::default();
199
200        if let Some(request_timeout) = request_timeout {
201            builder = builder.request_timeout(request_timeout);
202        }
203        if let Some(ws_url) = &self.ws {
204            builder = builder.ws_url(ws_url);
205        }
206        if let Some(basic_auth) = &self.basic_auth {
207            let fields: Vec<_> = basic_auth.split(':').collect();
208            if fields.len() != 2 {
209                return Err(anyhow!(
210                    "Basic auth should be in the format `username:password`"
211                ));
212            }
213            builder = builder.basic_auth(fields[0], fields[1]);
214        }
215
216        if let Some(max_concurrent_requests) = max_concurrent_requests {
217            builder = builder.max_concurrent_requests(max_concurrent_requests as usize);
218        }
219        Ok(builder.build(&self.rpc).await?)
220    }
221
222    /// Create the env with the default devnet configuration.
223    pub fn devnet() -> Self {
224        Self {
225            alias: "devnet".to_string(),
226            rpc: IOTA_DEVNET_URL.into(),
227            graphql: Some(IOTA_DEVNET_GRAPHQL_URL.into()),
228            ws: None,
229            basic_auth: None,
230            faucet: Some(IOTA_DEVNET_GAS_URL.into()),
231        }
232    }
233
234    /// Create the env with the default testnet configuration.
235    pub fn testnet() -> Self {
236        Self {
237            alias: "testnet".to_string(),
238            rpc: IOTA_TESTNET_URL.into(),
239            graphql: Some(IOTA_TESTNET_GRAPHQL_URL.into()),
240            ws: None,
241            basic_auth: None,
242            faucet: Some(IOTA_TESTNET_GAS_URL.into()),
243        }
244    }
245
246    /// Create the env with the default localnet configuration.
247    pub fn localnet() -> Self {
248        Self {
249            alias: "local".to_string(),
250            rpc: IOTA_LOCAL_NETWORK_URL.into(),
251            graphql: Some(IOTA_LOCAL_NETWORK_GRAPHQL_URL.into()),
252            ws: None,
253            basic_auth: None,
254            faucet: Some(IOTA_LOCAL_NETWORK_GAS_URL.into()),
255        }
256    }
257}
258
259impl Display for IotaEnv {
260    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
261        let mut writer = String::new();
262        writeln!(writer, "Active environment: {}", self.alias)?;
263        write!(writer, "RPC URL: {}", self.rpc)?;
264        if let Some(graphql) = &self.graphql {
265            writeln!(writer)?;
266            write!(writer, "GraphQL URL: {graphql}")?;
267        }
268        if let Some(ws) = &self.ws {
269            writeln!(writer)?;
270            write!(writer, "Websocket URL: {ws}")?;
271        }
272        if let Some(basic_auth) = &self.basic_auth {
273            writeln!(writer)?;
274            write!(writer, "Basic Auth: {basic_auth}")?;
275        }
276        if let Some(faucet) = &self.faucet {
277            writeln!(writer)?;
278            write!(writer, "Faucet URL: {faucet}")?;
279        }
280        write!(f, "{writer}")
281    }
282}
283
284impl Config for IotaClientConfig {}
285
286impl Display for IotaClientConfig {
287    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
288        let mut writer = String::new();
289
290        writeln!(
291            writer,
292            "Managed addresses: {}",
293            self.keystore.addresses().len()
294        )?;
295        write!(writer, "Active address: ")?;
296        match self.active_address {
297            Some(r) => writeln!(writer, "{}", r)?,
298            None => writeln!(writer, "None")?,
299        };
300        writeln!(writer, "{}", self.keystore)?;
301        if let Ok(env) = self.get_active_env() {
302            write!(writer, "{}", env)?;
303        }
304        write!(f, "{}", writer)
305    }
306}