sui_cluster_test/
cluster.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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/// Cluster Abstraction
48#[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    /// Returns faucet url in a remote cluster.
58    fn remote_faucet_url(&self) -> Option<&str>;
59
60    /// Returns faucet key in a local cluster.
61    fn local_faucet_key(&self) -> Option<&AccountKeyPair>;
62
63    /// Place to put config for the wallet, and any locally running services.
64    fn config_directory(&self) -> &Path;
65}
66
67/// Represents an up and running cluster deployed remotely.
68pub 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        // TODO: test connectivity before proceeding?
112
113        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
141/// Represents a local Cluster which starts per cluster test run.
142pub 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        // Check if we already have a config directory that is passed
168        if let Some(config_dir) = options.config_dir.clone() {
169            assert!(options.epoch_duration_ms.is_none());
170            // Load the config of the Sui authority.
171            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 the faucet account hold 1000 gas objects on genesis
184            let genesis_config = GenesisConfig::custom_genesis(1, 100);
185            // Custom genesis should be build here where we add the extra accounts
186            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        // Use the wealthy account for faucet
196        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        // This cluster has fullnode handle, safe to unwrap
201        let fullnode_url = test_cluster.fullnode_handle.rpc_url.clone();
202
203        // Let nodes connect to one another
204        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// Make linter happy
237#[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}