1use std::{net::SocketAddr, path::Path};
6
7use async_trait::async_trait;
8use iota_config::{Config, IOTA_GENESIS_FILENAME, IOTA_KEYSTORE_FILENAME, node::Genesis};
9use iota_graphql_rpc::{
10 config::ConnectionConfig, test_infra::cluster::start_graphql_server_with_fn_rpc,
11};
12use iota_indexer::test_utils::{IndexerTypeConfig, start_test_indexer};
13use iota_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore};
14use iota_sdk::{
15 iota_client_config::{IotaClientConfig, IotaEnv},
16 wallet_context::WalletContext,
17};
18use iota_swarm::memory::Swarm;
19use iota_swarm_config::{genesis_config::GenesisConfig, network_config::PersistedNetworkConfig};
20use iota_types::crypto::AccountPrivateKey;
21use tempfile::tempdir;
22use test_cluster::{TestCluster, TestClusterBuilder};
23use tracing::info;
24
25use super::config::{ClusterTestOpt, Env};
26
27const DEVNET_FAUCET_ADDR: &str = "https://faucet.devnet.iota.cafe:443";
28const TESTNET_FAUCET_ADDR: &str = "https://faucet.testnet.iota.cafe:443";
29const DEVNET_FULLNODE_ADDR: &str = "https://api.devnet.iota.cafe:443";
30const TESTNET_FULLNODE_ADDR: &str = "https://api.testnet.iota.cafe:443";
31
32pub struct ClusterFactory;
33
34impl ClusterFactory {
35 pub async fn start(
36 options: &ClusterTestOpt,
37 ) -> Result<Box<dyn Cluster + Sync + Send>, anyhow::Error> {
38 Ok(match &options.env {
39 Env::NewLocal => Box::new(LocalNewCluster::start(options).await?) as Box<_>,
40 _ => Box::new(RemoteRunningCluster::start(options).await?) as Box<_>,
41 })
42 }
43}
44
45#[async_trait]
47pub trait Cluster {
48 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error>
49 where
50 Self: Sized;
51
52 fn fullnode_url(&self) -> &str;
53
54 fn grpc_url(&self) -> Option<&str>;
56
57 fn user_key(&self) -> AccountPrivateKey;
58 fn indexer_url(&self) -> &Option<String>;
59
60 fn remote_faucet_url(&self) -> Option<&str>;
62
63 fn local_faucet_key(&self) -> Option<&AccountPrivateKey>;
65
66 fn config_directory(&self) -> &Path;
68}
69
70pub struct RemoteRunningCluster {
72 fullnode_url: String,
73 faucet_url: String,
74 config_directory: tempfile::TempDir,
75}
76
77#[async_trait]
78impl Cluster for RemoteRunningCluster {
79 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
80 let (fullnode_url, faucet_url) = match options.env {
81 Env::Devnet => (
82 String::from(DEVNET_FULLNODE_ADDR),
83 String::from(DEVNET_FAUCET_ADDR),
84 ),
85 Env::Testnet => (
86 String::from(TESTNET_FULLNODE_ADDR),
87 String::from(TESTNET_FAUCET_ADDR),
88 ),
89 Env::CustomRemote => (
90 options
91 .fullnode_address
92 .clone()
93 .expect("expect 'fullnode_address' for Env::Custom"),
94 options
95 .faucet_address
96 .clone()
97 .expect("expect 'faucet_address' for Env::Custom"),
98 ),
99 Env::NewLocal => {
100 unreachable!("the NewLocal variant shouldn't use RemoteRunningCluster")
101 }
102 };
103
104 Ok(Self {
107 fullnode_url,
108 faucet_url,
109 config_directory: tempfile::tempdir()?,
110 })
111 }
112
113 fn fullnode_url(&self) -> &str {
114 &self.fullnode_url
115 }
116
117 fn grpc_url(&self) -> Option<&str> {
118 None
119 }
120
121 fn indexer_url(&self) -> &Option<String> {
122 &None
123 }
124
125 fn user_key(&self) -> AccountPrivateKey {
126 AccountPrivateKey::random()
127 }
128
129 fn remote_faucet_url(&self) -> Option<&str> {
130 Some(&self.faucet_url)
131 }
132
133 fn local_faucet_key(&self) -> Option<&AccountPrivateKey> {
134 None
135 }
136
137 fn config_directory(&self) -> &Path {
138 self.config_directory.path()
139 }
140}
141
142pub struct LocalNewCluster {
144 test_cluster: TestCluster,
145 fullnode_url: String,
146 grpc_url: String,
147 indexer_url: Option<String>,
148 faucet_key: AccountPrivateKey,
149 config_directory: tempfile::TempDir,
150}
151
152impl LocalNewCluster {
153 #[allow(unused)]
154 pub fn swarm(&self) -> &Swarm {
155 &self.test_cluster.swarm
156 }
157}
158
159#[async_trait]
160impl Cluster for LocalNewCluster {
161 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
162 let data_ingestion_path = tempdir()?.keep();
163 let fullnode_rpc_addr = options.fullnode_address.as_ref().map(|addr| {
165 addr.parse::<SocketAddr>()
166 .expect("unable to parse fullnode address")
167 });
168
169 let indexer_address = options.indexer_address.as_ref().map(|addr| {
170 addr.parse::<SocketAddr>()
171 .expect("unable to parse indexer address")
172 });
173
174 let mut cluster_builder = TestClusterBuilder::new()
175 .enable_fullnode_events()
176 .with_data_ingestion_dir(data_ingestion_path.clone())
177 .disable_fullnode_pruning();
180
181 if let Some(config_dir) = options.config_dir.clone() {
183 assert!(options.epoch_duration_ms.is_none());
184 let network_config = PersistedNetworkConfig::read(&config_dir)?.into_network_config(
186 &config_dir,
187 Genesis::new_from_file(config_dir.join(IOTA_GENESIS_FILENAME)),
188 )?;
189 cluster_builder = cluster_builder.set_network_config(network_config);
190
191 cluster_builder = cluster_builder.with_config_dir(config_dir);
192 } else {
193 let genesis_config = GenesisConfig::custom_genesis(1, 100);
195 cluster_builder = cluster_builder.set_genesis_config(genesis_config);
197
198 if let Some(epoch_duration_ms) = options.epoch_duration_ms {
199 cluster_builder = cluster_builder.with_epoch_duration_ms(epoch_duration_ms);
200 }
201 }
202
203 if let Some(fullnode_rpc_addr) = fullnode_rpc_addr {
204 cluster_builder = cluster_builder.with_fullnode_rpc_addr(fullnode_rpc_addr);
205 }
206
207 let mut test_cluster = cluster_builder.build().await;
208
209 let faucet_key = test_cluster.swarm.config_mut().account_keys.swap_remove(0);
211 let faucet_address = faucet_key.public_key().derive_address();
212 info!(?faucet_address, "faucet_address");
213
214 let fullnode_url = test_cluster.fullnode_handle.rpc_url.clone();
216 let grpc_url = test_cluster.grpc_url();
217
218 if let (Some(pg_address), Some(indexer_address)) =
219 (options.pg_address.clone(), indexer_address)
220 {
221 start_test_indexer(
223 pg_address.clone(),
224 true,
226 None,
227 test_cluster.grpc_url(),
228 IndexerTypeConfig::writer_mode(None),
229 Some(data_ingestion_path.clone()),
230 )
231 .await;
232
233 start_test_indexer(
235 pg_address,
236 false,
237 None,
238 test_cluster.grpc_url(),
239 IndexerTypeConfig::reader_mode(indexer_address.to_string()),
240 Some(data_ingestion_path),
241 )
242 .await;
243 }
244
245 if let Some(graphql_address) = &options.graphql_address {
246 let graphql_address = graphql_address.parse::<SocketAddr>()?;
247 let graphql_connection_config = ConnectionConfig::new(
248 Some(graphql_address.port()),
249 Some(graphql_address.ip().to_string()),
250 options.pg_address.clone(),
251 None,
252 None,
253 None,
254 None,
255 None,
256 );
257
258 start_graphql_server_with_fn_rpc(
259 graphql_connection_config.clone(),
260 Some(test_cluster.grpc_url()),
261 None,
263 None,
265 )
266 .await;
267 }
268
269 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
271
272 Ok(Self {
274 test_cluster,
275 fullnode_url,
276 grpc_url,
277 faucet_key,
278 config_directory: tempfile::tempdir()?,
279 indexer_url: options.indexer_address.clone(),
280 })
281 }
282
283 fn fullnode_url(&self) -> &str {
284 &self.fullnode_url
285 }
286
287 fn grpc_url(&self) -> Option<&str> {
288 Some(&self.grpc_url)
289 }
290
291 fn indexer_url(&self) -> &Option<String> {
292 &self.indexer_url
293 }
294
295 fn user_key(&self) -> AccountPrivateKey {
296 AccountPrivateKey::random()
297 }
298
299 fn remote_faucet_url(&self) -> Option<&str> {
300 None
301 }
302
303 fn local_faucet_key(&self) -> Option<&AccountPrivateKey> {
304 Some(&self.faucet_key)
305 }
306
307 fn config_directory(&self) -> &Path {
308 self.config_directory.path()
309 }
310}
311
312#[async_trait]
314impl Cluster for Box<dyn Cluster + Send + Sync> {
315 async fn start(_options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
316 unreachable!(
317 "if we already have a boxed Cluster trait object we wouldn't have to call this function"
318 );
319 }
320 fn fullnode_url(&self) -> &str {
321 (**self).fullnode_url()
322 }
323 fn grpc_url(&self) -> Option<&str> {
324 (**self).grpc_url()
325 }
326 fn indexer_url(&self) -> &Option<String> {
327 (**self).indexer_url()
328 }
329
330 fn user_key(&self) -> AccountPrivateKey {
331 (**self).user_key()
332 }
333
334 fn remote_faucet_url(&self) -> Option<&str> {
335 (**self).remote_faucet_url()
336 }
337
338 fn local_faucet_key(&self) -> Option<&AccountPrivateKey> {
339 (**self).local_faucet_key()
340 }
341
342 fn config_directory(&self) -> &Path {
343 (**self).config_directory()
344 }
345}
346
347pub fn new_wallet_context_from_cluster(
348 cluster: &(dyn Cluster + Sync + Send),
349 private_key: AccountPrivateKey,
350) -> WalletContext {
351 let config_dir = cluster.config_directory();
352 let wallet_config_path = config_dir.join("client.yaml");
353 let fullnode_url = cluster.fullnode_url();
354 info!("Use RPC: {fullnode_url}");
355 let keystore_path = config_dir.join(IOTA_KEYSTORE_FILENAME);
356 let mut keystore = Keystore::from(FileBasedKeystore::new(&keystore_path).unwrap());
357 let address = private_key.public_key().derive_address();
358 keystore.add_key(None, private_key).unwrap();
359 IotaClientConfig::new(keystore)
360 .with_envs([IotaEnv::new("localnet", fullnode_url)])
361 .with_active_address(address)
362 .with_active_env("localnet".to_string())
363 .persisted(&wallet_config_path)
364 .save()
365 .unwrap();
366
367 info!(
368 "Initialize wallet from config path: {:?}",
369 wallet_config_path
370 );
371
372 WalletContext::new(&wallet_config_path).unwrap_or_else(|e| {
373 panic!("failed to init wallet context from path {wallet_config_path:?}, error: {e}")
374 })
375}