sui_cluster_test/
cluster.rs1use super::config::{ClusterTestOpt, Env};
5use async_trait::async_trait;
6use std::path::Path;
7use sui_config::Config;
8use sui_config::{PersistedConfig, SUI_KEYSTORE_FILENAME, SUI_NETWORK_CONFIG};
9use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore};
10use sui_sdk::sui_client_config::{SuiClientConfig, SuiEnv};
11use sui_sdk::wallet_context::WalletContext;
12use sui_swarm::memory::Swarm;
13use sui_swarm_config::genesis_config::GenesisConfig;
14use sui_swarm_config::network_config::NetworkConfig;
15use sui_types::base_types::SuiAddress;
16use sui_types::crypto::KeypairTraits;
17use sui_types::crypto::SuiKeyPair;
18use sui_types::crypto::{AccountKeyPair, get_key_pair};
19use tempfile::tempdir;
20use test_cluster::{TestCluster, TestClusterBuilder};
21use tracing::info;
22
23const DEVNET_FAUCET_ADDR: &str = "https://faucet.devnet.sui.io:443";
24const STAGING_FAUCET_ADDR: &str = "https://faucet.staging.sui.io:443";
25const CONTINUOUS_FAUCET_ADDR: &str = "https://faucet.ci.sui.io:443";
26const CONTINUOUS_NOMAD_FAUCET_ADDR: &str = "https://faucet.nomad.ci.sui.io:443";
27const TESTNET_FAUCET_ADDR: &str = "https://faucet.testnet.sui.io:443";
28const DEVNET_FULLNODE_ADDR: &str = "https://rpc.devnet.sui.io:443";
29const STAGING_FULLNODE_ADDR: &str = "https://fullnode.staging.sui.io:443";
30const CONTINUOUS_FULLNODE_ADDR: &str = "https://fullnode.ci.sui.io:443";
31const CONTINUOUS_NOMAD_FULLNODE_ADDR: &str = "https://fullnode.nomad.ci.sui.io:443";
32const TESTNET_FULLNODE_ADDR: &str = "https://fullnode.testnet.sui.io:443";
33
34pub struct ClusterFactory;
35
36impl ClusterFactory {
37 pub async fn start(
38 options: &ClusterTestOpt,
39 ) -> Result<Box<dyn Cluster + Sync + Send>, anyhow::Error> {
40 Ok(match &options.env {
41 Env::NewLocal => Box::new(LocalNewCluster::start(options).await?),
42 _ => Box::new(RemoteRunningCluster::start(options).await?),
43 })
44 }
45}
46
47#[async_trait]
49pub trait Cluster {
50 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error>
51 where
52 Self: Sized;
53
54 fn fullnode_url(&self) -> &str;
55 fn user_key(&self) -> AccountKeyPair;
56
57 fn remote_faucet_url(&self) -> Option<&str>;
59
60 fn local_faucet_key(&self) -> Option<&AccountKeyPair>;
62
63 fn config_directory(&self) -> &Path;
65}
66
67pub struct RemoteRunningCluster {
69 fullnode_url: String,
70 faucet_url: String,
71 config_directory: tempfile::TempDir,
72}
73
74#[async_trait]
75impl Cluster for RemoteRunningCluster {
76 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
77 let (fullnode_url, faucet_url) = match options.env {
78 Env::Devnet => (
79 String::from(DEVNET_FULLNODE_ADDR),
80 String::from(DEVNET_FAUCET_ADDR),
81 ),
82 Env::Staging => (
83 String::from(STAGING_FULLNODE_ADDR),
84 String::from(STAGING_FAUCET_ADDR),
85 ),
86 Env::Ci => (
87 String::from(CONTINUOUS_FULLNODE_ADDR),
88 String::from(CONTINUOUS_FAUCET_ADDR),
89 ),
90 Env::CiNomad => (
91 String::from(CONTINUOUS_NOMAD_FULLNODE_ADDR),
92 String::from(CONTINUOUS_NOMAD_FAUCET_ADDR),
93 ),
94 Env::Testnet => (
95 String::from(TESTNET_FULLNODE_ADDR),
96 String::from(TESTNET_FAUCET_ADDR),
97 ),
98 Env::CustomRemote => (
99 options
100 .fullnode_address
101 .clone()
102 .expect("Expect 'fullnode_address' for Env::Custom"),
103 options
104 .faucet_address
105 .clone()
106 .expect("Expect 'faucet_address' for Env::Custom"),
107 ),
108 Env::NewLocal => unreachable!("NewLocal shouldn't use RemoteRunningCluster"),
109 };
110
111 Ok(Self {
114 fullnode_url,
115 faucet_url,
116 config_directory: tempfile::tempdir()?,
117 })
118 }
119
120 fn fullnode_url(&self) -> &str {
121 &self.fullnode_url
122 }
123
124 fn user_key(&self) -> AccountKeyPair {
125 get_key_pair().1
126 }
127
128 fn remote_faucet_url(&self) -> Option<&str> {
129 Some(&self.faucet_url)
130 }
131
132 fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
133 None
134 }
135
136 fn config_directory(&self) -> &Path {
137 self.config_directory.path()
138 }
139}
140
141pub struct LocalNewCluster {
143 test_cluster: TestCluster,
144 fullnode_url: String,
145 faucet_key: AccountKeyPair,
146 config_directory: tempfile::TempDir,
147 #[allow(unused)]
148 data_ingestion_path: tempfile::TempDir,
149}
150
151impl LocalNewCluster {
152 #[allow(unused)]
153 pub fn swarm(&self) -> &Swarm {
154 &self.test_cluster.swarm
155 }
156}
157
158#[async_trait]
159impl Cluster for LocalNewCluster {
160 async fn start(options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
161 let data_ingestion_path = tempdir()?;
162
163 let mut cluster_builder = TestClusterBuilder::new()
164 .enable_fullnode_events()
165 .with_data_ingestion_dir(data_ingestion_path.path().to_path_buf());
166
167 if let Some(config_dir) = options.config_dir.clone() {
169 assert!(options.epoch_duration_ms.is_none());
170 let network_config_path = config_dir.join(SUI_NETWORK_CONFIG);
172 let network_config: NetworkConfig = PersistedConfig::read(&network_config_path)
173 .map_err(|err| {
174 err.context(format!(
175 "Cannot open Sui network config file at {:?}",
176 network_config_path
177 ))
178 })?;
179
180 cluster_builder = cluster_builder.set_network_config(network_config);
181 cluster_builder = cluster_builder.with_config_dir(config_dir);
182 } else {
183 let genesis_config = GenesisConfig::custom_genesis(1, 100);
185 cluster_builder = cluster_builder.set_genesis_config(genesis_config);
187
188 if let Some(epoch_duration_ms) = options.epoch_duration_ms {
189 cluster_builder = cluster_builder.with_epoch_duration_ms(epoch_duration_ms);
190 }
191 }
192
193 let mut test_cluster = cluster_builder.build().await;
194
195 let faucet_key = test_cluster.swarm.config_mut().account_keys.swap_remove(0);
197 let faucet_address = SuiAddress::from(faucet_key.public());
198 info!(?faucet_address, "faucet_address");
199
200 let fullnode_url = test_cluster.fullnode_handle.rpc_url.clone();
202
203 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
205
206 Ok(Self {
207 test_cluster,
208 fullnode_url,
209 faucet_key,
210 config_directory: tempfile::tempdir()?,
211 data_ingestion_path,
212 })
213 }
214
215 fn fullnode_url(&self) -> &str {
216 &self.fullnode_url
217 }
218
219 fn user_key(&self) -> AccountKeyPair {
220 get_key_pair().1
221 }
222
223 fn remote_faucet_url(&self) -> Option<&str> {
224 None
225 }
226
227 fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
228 Some(&self.faucet_key)
229 }
230
231 fn config_directory(&self) -> &Path {
232 self.config_directory.path()
233 }
234}
235
236#[async_trait]
238impl Cluster for Box<dyn Cluster + Send + Sync> {
239 async fn start(_options: &ClusterTestOpt) -> Result<Self, anyhow::Error> {
240 unreachable!(
241 "If we already have a boxed Cluster trait object we wouldn't have to call this function"
242 );
243 }
244 fn fullnode_url(&self) -> &str {
245 (**self).fullnode_url()
246 }
247
248 fn user_key(&self) -> AccountKeyPair {
249 (**self).user_key()
250 }
251
252 fn remote_faucet_url(&self) -> Option<&str> {
253 (**self).remote_faucet_url()
254 }
255
256 fn local_faucet_key(&self) -> Option<&AccountKeyPair> {
257 (**self).local_faucet_key()
258 }
259
260 fn config_directory(&self) -> &Path {
261 (**self).config_directory()
262 }
263}
264
265pub async fn new_wallet_context_from_cluster(
266 cluster: &(dyn Cluster + Sync + Send),
267 key_pair: AccountKeyPair,
268) -> WalletContext {
269 let config_dir = cluster.config_directory();
270 let wallet_config_path = config_dir.join("client.yaml");
271 let fullnode_url = cluster.fullnode_url();
272 info!("Use RPC: {}", &fullnode_url);
273 let keystore_path = config_dir.join(SUI_KEYSTORE_FILENAME);
274 let mut keystore = Keystore::from(FileBasedKeystore::load_or_create(&keystore_path).unwrap());
275 let address: SuiAddress = key_pair.public().into();
276 keystore
277 .import(None, SuiKeyPair::Ed25519(key_pair))
278 .await
279 .unwrap();
280 SuiClientConfig {
281 keystore,
282 external_keys: None,
283 envs: vec![SuiEnv {
284 alias: "localnet".to_string(),
285 rpc: fullnode_url.into(),
286 ws: None,
287 basic_auth: None,
288 chain_id: None,
289 }],
290 active_address: Some(address),
291 active_env: Some("localnet".to_string()),
292 }
293 .persisted(&wallet_config_path)
294 .save()
295 .unwrap();
296
297 info!(
298 "Initialize wallet from config path: {:?}",
299 wallet_config_path
300 );
301
302 WalletContext::new(&wallet_config_path).unwrap_or_else(|e| {
303 panic!(
304 "Failed to init wallet context from path {:?}, error: {e}",
305 wallet_config_path
306 )
307 })
308}