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