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