Skip to main content

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_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/// Configuration for the IOTA client, containing a [`Keystore`] and potentially
24/// multiple [`IotaEnv`]s.
25#[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    /// Create a new [`IotaClientConfig`] with the given keystore.
37    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    /// Set the default [`IotaEnv`]s for mainnet, devnet, testnet, and localnet.
48    pub fn with_default_envs(mut self) -> Self {
49        // We don't want to set any particular one of the default networks as active.
50        self.envs = vec![
51            IotaEnv::mainnet(),
52            IotaEnv::devnet(),
53            IotaEnv::testnet(),
54            IotaEnv::localnet(),
55        ];
56        self
57    }
58
59    /// Set the [`IotaEnv`]s.
60    pub fn with_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
61        self.set_envs(envs);
62        self
63    }
64
65    /// Set the [`IotaEnv`]s. Also sets the active env to the first in the list.
66    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    /// Set the active [`IotaEnv`] by its alias.
74    pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
75        self.set_active_env(env);
76        self
77    }
78
79    /// Set the active [`IotaEnv`] by its alias.
80    pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
81        self.active_env = env.into();
82    }
83
84    /// Set the active [`Address`].
85    pub fn with_active_address(mut self, address: impl Into<Option<Address>>) -> Self {
86        self.set_active_address(address);
87        self
88    }
89
90    /// Set the active [`Address`].
91    pub fn set_active_address(&mut self, address: impl Into<Option<Address>>) {
92        self.active_address = address.into();
93    }
94
95    /// Get an [`IotaEnv`] by its alias.
96    pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
97        self.envs.iter().find(|env| env.alias == alias)
98    }
99
100    /// Get the active [`IotaEnv`].
101    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    /// Add an [`IotaEnv`] if there's no env with the same alias already.
114    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    /// Set an [`IotaEnv`]. Replaces any existing env with the same alias.
129    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/// IOTA environment configuration, containing the RPC URL, and optional
136/// websocket, basic auth and faucet options.
137#[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    /// Optional gRPC URL. Absent from older `client.yaml` files, so it
145    /// defaults to `None` on load.
146    #[serde(default)]
147    pub(crate) grpc: Option<String>,
148    /// Basic HTTP access authentication in the format of username:password, if
149    /// needed.
150    pub(crate) basic_auth: Option<String>,
151    pub(crate) faucet: Option<String>,
152}
153
154impl IotaEnv {
155    /// Create a new [`IotaEnv`] with the given alias and RPC URL such as <https://api.testnet.iota.cafe>.
156    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    /// Set a graphql URL.
169    pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
170        self.set_graphql(graphql);
171        self
172    }
173
174    /// Set a graphql URL.
175    pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
176        self.graphql = graphql.into();
177    }
178
179    /// Set a websocket URL.
180    pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
181        self.set_ws(ws);
182        self
183    }
184
185    /// Set a websocket URL.
186    pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
187        self.ws = ws.into();
188    }
189
190    /// Set a gRPC URL.
191    pub fn with_grpc(mut self, grpc: impl Into<Option<String>>) -> Self {
192        self.set_grpc(grpc);
193        self
194    }
195
196    /// Set a gRPC URL.
197    pub fn set_grpc(&mut self, grpc: impl Into<Option<String>>) {
198        self.grpc = grpc.into();
199    }
200
201    /// Set basic authentication information in the format of username:password.
202    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    /// Set basic authentication information in the format of username:password.
208    pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
209        self.basic_auth = basic_auth.into();
210    }
211
212    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
213    pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
214        self.set_faucet(faucet);
215        self
216    }
217
218    /// Set a faucet URL such as <https://faucet.testnet.iota.cafe/v1/gas>.
219    pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
220        self.faucet = faucet.into();
221    }
222
223    /// Create an [`IotaClient`] with the given request timeout, max
224    /// concurrent requests and possible configured websocket URL and basic
225    /// auth.
226    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    /// Create a [`iota_grpc_client::Client`] for this env's gRPC endpoint.
256    ///
257    /// Errors if the env has no `grpc` URL configured.
258    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    /// Create the env with the default mainnet configuration.
267    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    /// Create the env with the default devnet configuration.
280    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    /// Create the env with the default testnet configuration.
293    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    /// Create the env with the default localnet configuration.
306    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}