1use 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#[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_envs(mut self, envs: impl IntoIterator<Item = IotaEnv>) -> Self {
48 self.set_envs(envs);
49 self
50 }
51
52 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 pub fn with_active_env(mut self, env: impl Into<Option<String>>) -> Self {
62 self.set_active_env(env);
63 self
64 }
65
66 pub fn set_active_env(&mut self, env: impl Into<Option<String>>) {
68 self.active_env = env.into();
69 }
70
71 pub fn with_active_address(mut self, address: impl Into<Option<IotaAddress>>) -> Self {
73 self.set_active_address(address);
74 self
75 }
76
77 pub fn set_active_address(&mut self, address: impl Into<Option<IotaAddress>>) {
79 self.active_address = address.into();
80 }
81
82 pub fn get_env(&self, alias: &str) -> Option<&IotaEnv> {
84 self.envs.iter().find(|env| env.alias == alias)
85 }
86
87 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 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#[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 pub(crate) basic_auth: Option<String>,
128 pub(crate) faucet: Option<String>,
129}
130
131impl IotaEnv {
132 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 pub fn with_graphql(mut self, graphql: impl Into<Option<String>>) -> Self {
146 self.set_graphql(graphql);
147 self
148 }
149
150 pub fn set_graphql(&mut self, graphql: impl Into<Option<String>>) {
152 self.graphql = graphql.into();
153 }
154
155 pub fn with_ws(mut self, ws: impl Into<Option<String>>) -> Self {
157 self.set_ws(ws);
158 self
159 }
160
161 pub fn set_ws(&mut self, ws: impl Into<Option<String>>) {
163 self.ws = ws.into();
164 }
165
166 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 pub fn set_basic_auth(&mut self, basic_auth: impl Into<Option<String>>) {
174 self.basic_auth = basic_auth.into();
175 }
176
177 pub fn with_faucet(mut self, faucet: impl Into<Option<String>>) -> Self {
179 self.set_faucet(faucet);
180 self
181 }
182
183 pub fn set_faucet(&mut self, faucet: impl Into<Option<String>>) {
185 self.faucet = faucet.into();
186 }
187
188 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 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 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 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}