diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f7fb6f4c7c9a..82a960208e2ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -145,6 +145,7 @@ jobs: if: ${{ env.s3_archive_exist == '' }} shell: bash run: | + [ -f ~/.cargo/env ] && source ~/.cargo/env ; cargo build --bin sui --release --features indexer && mv target/release/sui target/release/sui-pg [ -f ~/.cargo/env ] && source ~/.cargo/env ; cargo build --release && cargo build --profile=dev --bin sui - name: Rename binaries for ${{ matrix.os }} @@ -159,6 +160,10 @@ jobs: mv ./target/release/${binary}${{ env.extention }} ${{ env.TMP_BUILD_DIR }}/${binary}${{ env.extention }} done + # sui-pg is a special binary that is built with the indexer feature for sui start cmd + export binary='sui-pg' | tr -d $'\r') + mv ./target/release/${binary}${{ env.extention }} ${{ env.TMP_BUILD_DIR }}/${binary}${{ env.extention }} + mv ./target/debug/sui${{ env.extention }} ${{ env.TMP_BUILD_DIR }}/sui-debug${{ env.extention }} tar -cvzf ./tmp/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz -C ${{ env.TMP_BUILD_DIR }} . [[ ${{ env.sui_tag }} == *"testnet"* ]] && aws s3 cp ./tmp/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz s3://sui-releases/releases/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz || true diff --git a/Cargo.lock b/Cargo.lock index f31dca3a259fc..567422cdea2b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11694,6 +11694,7 @@ dependencies = [ "assert_cmd", "async-recursion", "async-trait", + "axum", "bcs", "bin-version", "bip32", @@ -11702,11 +11703,13 @@ dependencies = [ "colored", "csv", "datatest-stable", + "diesel", "expect-test", "fastcrypto", "fastcrypto-zkp", "fs_extra", "futures", + "http", "im", "inquire", "insta", @@ -11739,9 +11742,13 @@ dependencies = [ "shlex", "signature 1.6.4", "sui-bridge", + "sui-cluster-test", "sui-config", "sui-execution", + "sui-faucet", "sui-genesis-builder", + "sui-graphql-rpc", + "sui-indexer", "sui-json", "sui-json-rpc-types", "sui-keys", @@ -11767,8 +11774,12 @@ dependencies = [ "thiserror", "tokio", "toml 0.7.4", + "tower", + "tower-http", "tracing", "unescape", + "url", + "uuid 1.2.2", ] [[package]] @@ -12201,7 +12212,6 @@ dependencies = [ "reqwest", "serde_json", "shared-crypto", - "sui", "sui-config", "sui-core", "sui-faucet", @@ -12587,7 +12597,6 @@ dependencies = [ "scopeguard", "serde", "shared-crypto", - "sui", "sui-config", "sui-json-rpc-types", "sui-keys", diff --git a/binary-build-list.json b/binary-build-list.json index 4bb16bb391a6f..4855c1b2a220b 100644 --- a/binary-build-list.json +++ b/binary-build-list.json @@ -4,11 +4,11 @@ "sui-node", "sui-tool", "sui-faucet", - "sui-test-validator", "sui-data-ingestion", "sui-bridge", "sui-bridge-cli", "sui-graphql-rpc", + "sui-test-validator", "move-analyzer" ], "internal_binaries": [ diff --git a/crates/sui-cluster-test/Cargo.toml b/crates/sui-cluster-test/Cargo.toml index 7bf3bbe69de83..a6477761d408b 100644 --- a/crates/sui-cluster-test/Cargo.toml +++ b/crates/sui-cluster-test/Cargo.toml @@ -29,7 +29,6 @@ sui-indexer.workspace = true sui-faucet.workspace = true sui-graphql-rpc.workspace = true sui-swarm.workspace = true -sui.workspace = true sui-swarm-config.workspace = true sui-json-rpc-types.workspace = true sui-sdk.workspace = true diff --git a/crates/sui-config/src/lib.rs b/crates/sui-config/src/lib.rs index 6432b2338c244..abe14139b08c2 100644 --- a/crates/sui-config/src/lib.rs +++ b/crates/sui-config/src/lib.rs @@ -52,6 +52,22 @@ pub fn sui_config_dir() -> Result { }) } +/// Check if the genesis blob exists in the given directory or the default directory. +pub fn genesis_blob_exists(config_dir: Option) -> bool { + if let Some(dir) = config_dir { + dir.join(SUI_GENESIS_FILENAME).exists() + } else if let Some(config_env) = std::env::var_os("SUI_CONFIG_DIR") { + Path::new(&config_env).join(SUI_GENESIS_FILENAME).exists() + } else if let Some(home) = dirs::home_dir() { + let mut config = PathBuf::new(); + config.push(&home); + config.extend([SUI_DIR, SUI_CONFIG_DIR, SUI_GENESIS_FILENAME]); + config.exists() + } else { + false + } +} + pub fn validator_config_file(address: Multiaddr, i: usize) -> String { multiaddr_to_filename(address).unwrap_or(format!("validator-config-{}.yaml", i)) } diff --git a/crates/sui-faucet/Cargo.toml b/crates/sui-faucet/Cargo.toml index 7f2cb4d76adc6..40c8642d9257d 100644 --- a/crates/sui-faucet/Cargo.toml +++ b/crates/sui-faucet/Cargo.toml @@ -29,7 +29,6 @@ rocksdb.workspace = true tempfile.workspace = true parking_lot.workspace = true -sui.workspace = true sui-json-rpc-types.workspace = true sui-types.workspace = true sui-config.workspace = true diff --git a/crates/sui-faucet/src/faucet/mod.rs b/crates/sui-faucet/src/faucet/mod.rs index a1f890f22f868..1483123803318 100644 --- a/crates/sui-faucet/src/faucet/mod.rs +++ b/crates/sui-faucet/src/faucet/mod.rs @@ -10,7 +10,7 @@ mod simple_faucet; mod write_ahead_log; pub use self::simple_faucet::SimpleFaucet; use clap::Parser; -use std::{net::Ipv4Addr, path::PathBuf}; +use std::{net::Ipv4Addr, path::PathBuf, sync::Arc}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FaucetReceipt { @@ -44,6 +44,17 @@ pub enum BatchSendStatusType { DISCARDED, } +pub struct AppState> { + pub faucet: F, + pub config: FaucetConfig, +} + +impl AppState { + pub fn new(faucet: F, config: FaucetConfig) -> Self { + Self { faucet, config } + } +} + #[async_trait] pub trait Faucet { /// Send `Coin` of the specified amount to the recipient diff --git a/crates/sui-faucet/src/faucet/simple_faucet.rs b/crates/sui-faucet/src/faucet/simple_faucet.rs index f93b4cfa5ba6c..0bdbb8641d764 100644 --- a/crates/sui-faucet/src/faucet/simple_faucet.rs +++ b/crates/sui-faucet/src/faucet/simple_faucet.rs @@ -1086,15 +1086,45 @@ pub async fn batch_transfer_gases( #[cfg(test)] mod tests { - use sui::{ - client_commands::{Opts, OptsWithGas, SuiClientCommandResult, SuiClientCommands}, - key_identity::KeyIdentity, - }; + use super::*; + use anyhow::*; + use shared_crypto::intent::Intent; use sui_json_rpc_types::SuiExecutionStatus; + use sui_json_rpc_types::SuiTransactionBlockEffects; use sui_sdk::wallet_context::WalletContext; + use sui_types::transaction::SenderSignedData; + use sui_types::transaction::TransactionDataAPI; use test_cluster::TestClusterBuilder; - use super::*; + async fn execute_tx( + ctx: &mut WalletContext, + tx_data: TransactionData, + ) -> Result { + let signature = ctx.config.keystore.sign_secure( + &tx_data.sender(), + &tx_data, + Intent::sui_transaction(), + )?; + let sender_signed_data = SenderSignedData::new_from_sender_signature(tx_data, signature); + let transaction = Transaction::new(sender_signed_data); + let response = ctx.execute_transaction_may_fail(transaction).await?; + let result_effects = response.clone().effects; + + if let Some(effects) = result_effects { + if matches!(effects.status(), SuiExecutionStatus::Failure { .. }) { + Err(anyhow!( + "Error executing transaction: {:#?}", + effects.status() + )) + } else { + Ok(effects) + } + } else { + Err(anyhow!( + "Effects from SuiTransactionBlockResult should not be empty" + )) + } + } #[tokio::test] async fn simple_faucet_basic_interface_should_work() { @@ -1106,17 +1136,25 @@ mod tests { let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; - // Split some extra gas coins so that we can test batch queue - SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: None, - count: Some(10), - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await - .expect("split failed"); + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind(gas_coins.first().unwrap().0, None, Some(10)) + .await + .unwrap(); + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); + + execute_tx(&mut context, tx_data).await.unwrap(); let faucet = SimpleFaucet::new( context, @@ -1144,9 +1182,12 @@ mod tests { async fn test_init_gas_queue() { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); - let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; - let gases = HashSet::from_iter(gases.into_iter().map(|gas| *gas.id())); + let context = test_cluster.wallet; + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + let gas_coins = HashSet::from_iter(gas_coins.into_iter().map(|gas| gas.0)); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); @@ -1163,13 +1204,13 @@ mod tests { let available = faucet.metrics.total_available_coins.get(); let faucet_unwrapped = &mut Arc::try_unwrap(faucet).unwrap(); - let candidates = faucet_unwrapped.drain_gas_queue(gases.len()).await; + let candidates = faucet_unwrapped.drain_gas_queue(gas_coins.len()).await; assert_eq!(available as usize, candidates.len()); assert_eq!( - candidates, gases, + candidates, gas_coins, "gases: {:?}, candidates: {:?}", - gases, candidates + gas_coins, candidates ); } @@ -1177,10 +1218,12 @@ mod tests { async fn test_transfer_state() { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); - let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; - - let gases = HashSet::from_iter(gases.into_iter().map(|gas| *gas.id())); + let context = test_cluster.wallet; + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + let gas_coins = HashSet::from_iter(gas_coins.into_iter().map(|gas| gas.0)); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); @@ -1194,7 +1237,7 @@ mod tests { .await .unwrap(); - let number_of_coins = gases.len(); + let number_of_coins = gas_coins.len(); let amounts = &vec![1; number_of_coins]; let _ = futures::future::join_all((0..number_of_coins).map(|_| { faucet.send( @@ -1213,12 +1256,12 @@ mod tests { faucet.shutdown_batch_send_task(); let faucet_unwrapped: &mut SimpleFaucet = &mut Arc::try_unwrap(faucet).unwrap(); - let candidates = faucet_unwrapped.drain_gas_queue(gases.len()).await; + let candidates = faucet_unwrapped.drain_gas_queue(gas_coins.len()).await; assert_eq!(available as usize, candidates.len()); assert_eq!( - candidates, gases, + candidates, gas_coins, "gases: {:?}, candidates: {:?}", - gases, candidates + gas_coins, candidates ); } @@ -1231,17 +1274,25 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; - // Split some extra gas coins so that we can test batch queue - SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: None, - count: Some(10), - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await - .expect("split failed"); + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind(gas_coins.first().unwrap().0, None, Some(10)) + .await + .unwrap(); + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); + + execute_tx(&mut context, tx_data).await.unwrap(); let faucet = SimpleFaucet::new( context, @@ -1367,15 +1418,20 @@ mod tests { async fn test_discard_invalid_gas() { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); - let mut context = test_cluster.wallet; - let mut gases = get_current_gases(address, &mut context).await; + let context = test_cluster.wallet; + let mut gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); - let bad_gas = gases.swap_remove(0); - let gases = HashSet::from_iter(gases.into_iter().map(|gas| *gas.id())); + let bad_gas = gas_coins.swap_remove(0); + let gas_coins = HashSet::from_iter(gas_coins.into_iter().map(|gas| gas.0)); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); let config = FaucetConfig::default(); + + let client = context.get_client().await.unwrap(); let faucet = SimpleFaucet::new( context, &prom_registry, @@ -1385,29 +1441,23 @@ mod tests { .await .unwrap(); faucet.shutdown_batch_send_task(); - let faucet: &mut SimpleFaucet = &mut Arc::try_unwrap(faucet).unwrap(); // Now we transfer one gas out - let res = SuiClientCommands::PayAllSui { - input_coins: vec![*bad_gas.id()], - recipient: KeyIdentity::Address(SuiAddress::random_for_testing_only()), - opts: Opts::for_testing(2_000_000), - } - .execute(faucet.wallet_mut()) - .await - .unwrap(); - - if let SuiClientCommandResult::TransactionBlock(response) = res { - assert!(matches!( - response.effects.unwrap().status(), - SuiExecutionStatus::Success - )); - } else { - panic!("PayAllSui command did not return SuiClientCommandResult::TransactionBlock"); - }; + let gas_budget = 50_000_000; + let tx_data = client + .transaction_builder() + .pay_all_sui( + address, + vec![bad_gas.0], + SuiAddress::random_for_testing_only(), + gas_budget, + ) + .await + .unwrap(); + execute_tx(faucet.wallet_mut(), tx_data).await.unwrap(); - let number_of_coins = gases.len(); + let number_of_coins = gas_coins.len(); let amounts = &vec![1; number_of_coins]; // We traverse the list twice, which must trigger the transferred gas to be kicked out futures::future::join_all((0..2).map(|_| { @@ -1423,13 +1473,13 @@ mod tests { // Note `gases` does not contain the bad gas. let available = faucet.metrics.total_available_coins.get(); let discarded = faucet.metrics.total_discarded_coins.get(); - let candidates = faucet.drain_gas_queue(gases.len()).await; + let candidates = faucet.drain_gas_queue(gas_coins.len()).await; assert_eq!(available as usize, candidates.len()); assert_eq!(discarded, 1); assert_eq!( - candidates, gases, + candidates, gas_coins, "gases: {:?}, candidates: {:?}", - gases, candidates + gas_coins, candidates ); } @@ -1504,39 +1554,45 @@ mod tests { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); // split out a coin that has a very small balance such that // this coin will be not used later on. This is the new default amount for faucet due to gas changes let config = FaucetConfig::default(); let tiny_value = (config.num_coins as u64 * config.amount) + 1; - let res = SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: Some(vec![tiny_value]), - count: None, - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await; + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind(gas_coins.first().unwrap().0, Some(vec![tiny_value]), None) + .await + .unwrap(); + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); - let tiny_coin_id = if let SuiClientCommandResult::TransactionBlock(resp) = res.unwrap() { - resp.effects.as_ref().unwrap().created()[0] - .reference - .object_id - } else { - panic!("SplitCoin command did not return SuiClientCommandResult::TransactionBlock"); - }; + let effects = execute_tx(&mut context, tx_data).await.unwrap(); + + let tiny_coin_id = effects.created()[0].reference.object_id; // Get the latest list of gas - let gases = get_current_gases(address, &mut context).await; - let tiny_amount = gases + let gas_coins = context.gas_objects(address).await.unwrap(); + + let tiny_amount = gas_coins .iter() - .find(|gas| gas.id() == &tiny_coin_id) + .find(|gas| gas.1.object_id == tiny_coin_id) .unwrap() - .value(); + .0; assert_eq!(tiny_amount, tiny_value); - let gases: HashSet = HashSet::from_iter(gases.into_iter().map(|gas| *gas.id())); + let gas_coins: HashSet = + HashSet::from_iter(gas_coins.into_iter().map(|gas| gas.1.object_id)); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); @@ -1553,7 +1609,7 @@ mod tests { let faucet: &mut SimpleFaucet = &mut Arc::try_unwrap(faucet).unwrap(); // Ask for a value higher than tiny coin + DEFAULT_GAS_COMPUTATION_BUCKET - let number_of_coins = gases.len(); + let number_of_coins = gas_coins.len(); let amounts = &vec![tiny_value + 1; number_of_coins]; // We traverse the list ten times, which must trigger the tiny gas to be examined and then discarded futures::future::join_all((0..10).map(|_| { @@ -1576,7 +1632,7 @@ mod tests { let discarded = faucet.metrics.total_discarded_coins.get(); info!("discarded: {:?}", discarded); - let candidates = faucet.drain_gas_queue(gases.len() - 1).await; + let candidates = faucet.drain_gas_queue(gas_coins.len() - 1).await; assert_eq!(discarded, 1); assert!(candidates.get(&tiny_coin_id).is_none()); @@ -1587,38 +1643,50 @@ mod tests { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); let config = FaucetConfig::default(); // The coin that is split off stays because we don't try to refresh the coin vector let reasonable_value = (config.num_coins as u64 * config.amount) * 10; - SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: Some(vec![reasonable_value]), - count: None, - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await - .expect("split failed"); + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind( + gas_coins.first().unwrap().0, + Some(vec![reasonable_value]), + None, + ) + .await + .unwrap(); + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); + execute_tx(&mut context, tx_data).await.unwrap(); let destination_address = SuiAddress::random_for_testing_only(); // Transfer all valid gases away except for 1 - for gas in gases.iter().take(gases.len() - 1) { - SuiClientCommands::TransferSui { - to: KeyIdentity::Address(destination_address), - sui_coin_object_id: *gas.id(), - amount: None, - opts: Opts::for_testing(50_000_000), - } - .execute(&mut context) - .await - .expect("transfer failed"); + for gas in gas_coins.iter().take(gas_coins.len() - 1) { + let tx_data = client + .transaction_builder() + .transfer_sui(address, gas.0, gas_budget, destination_address, None) + .await + .unwrap(); + execute_tx(&mut context, tx_data).await.unwrap(); } // Assert that the coins were transferred away successfully to destination address - let gases = get_current_gases(destination_address, &mut context).await; - assert!(!gases.is_empty()); + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + assert!(!gas_coins.is_empty()); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); @@ -1656,37 +1724,49 @@ mod tests { let test_cluster = TestClusterBuilder::new().build().await; let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); let config = FaucetConfig::default(); let tiny_value = (config.num_coins as u64 * config.amount) + 1; - let _res = SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: Some(vec![tiny_value]), - count: None, - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await; + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind(gas_coins.first().unwrap().0, Some(vec![tiny_value]), None) + .await + .unwrap(); + + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); + + execute_tx(&mut context, tx_data).await.unwrap(); let destination_address = SuiAddress::random_for_testing_only(); // Transfer all valid gases away - for gas in gases { - SuiClientCommands::TransferSui { - to: KeyIdentity::Address(destination_address), - sui_coin_object_id: *gas.id(), - amount: None, - opts: Opts::for_testing(50_000_000), - } - .execute(&mut context) - .await - .expect("transfer failed"); + for gas in gas_coins { + let tx_data = client + .transaction_builder() + .transfer_sui(address, gas.0, gas_budget, destination_address, None) + .await + .unwrap(); + execute_tx(&mut context, tx_data).await.unwrap(); } // Assert that the coins were transferred away successfully to destination address - let gases = get_current_gases(destination_address, &mut context).await; - assert!(!gases.is_empty()); + let gas_coins = context + .get_all_gas_objects_owned_by_address(destination_address) + .await + .unwrap(); + assert!(!gas_coins.is_empty()); let tmp = tempfile::tempdir().unwrap(); let prom_registry = Registry::new(); @@ -1787,17 +1867,24 @@ mod tests { let config: FaucetConfig = Default::default(); let address = test_cluster.get_address_0(); let mut context = test_cluster.wallet; - let gases = get_current_gases(address, &mut context).await; - // Split some extra gas coins so that we can test batch queue - SuiClientCommands::SplitCoin { - coin_id: *gases[0].id(), - amounts: None, - count: Some(10), - opts: OptsWithGas::for_testing(None, 50_000_000), - } - .execute(&mut context) - .await - .expect("split failed"); + let gas_coins = context + .get_all_gas_objects_owned_by_address(address) + .await + .unwrap(); + let client = context.get_client().await.unwrap(); + let tx_kind = client + .transaction_builder() + .split_coin_tx_kind(gas_coins.first().unwrap().0, None, Some(10)) + .await + .unwrap(); + let gas_budget = 50_000_000; + let rgp = context.get_reference_gas_price().await.unwrap(); + let tx_data = client + .transaction_builder() + .tx_data(address, tx_kind, gas_budget, rgp, vec![], None) + .await + .unwrap(); + execute_tx(&mut context, tx_data).await.unwrap(); let prom_registry = Registry::new(); let tmp = tempfile::tempdir().unwrap(); @@ -1898,18 +1985,4 @@ mod tests { actual_amounts.sort_unstable(); assert_eq!(actual_amounts, amounts); } - - async fn get_current_gases(address: SuiAddress, context: &mut WalletContext) -> Vec { - // Get the latest list of gas - let results = SuiClientCommands::Gas { - address: Some(KeyIdentity::Address(address)), - } - .execute(context) - .await - .unwrap(); - match results { - SuiClientCommandResult::Gas(gases) => gases, - other => panic!("Expect SuiClientCommandResult::Gas, but got {:?}", other), - } - } } diff --git a/crates/sui-faucet/src/lib.rs b/crates/sui-faucet/src/lib.rs index 0b1c4dd4c9d73..b6947a8891b22 100644 --- a/crates/sui-faucet/src/lib.rs +++ b/crates/sui-faucet/src/lib.rs @@ -6,6 +6,7 @@ mod faucet; mod metrics; mod requests; mod responses; +mod server; pub mod metrics_layer; pub use metrics_layer::*; @@ -14,3 +15,4 @@ pub use errors::FaucetError; pub use faucet::*; pub use requests::*; pub use responses::*; +pub use server::{create_wallet_context, start_faucet}; diff --git a/crates/sui-faucet/src/main.rs b/crates/sui-faucet/src/main.rs index a8899b54fdb08..89d49cf34c6a9 100644 --- a/crates/sui-faucet/src/main.rs +++ b/crates/sui-faucet/src/main.rs @@ -1,42 +1,15 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use axum::{ - error_handling::HandleErrorLayer, - extract::Path, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, - BoxError, Extension, Json, Router, -}; use clap::Parser; -use http::Method; -use mysten_metrics::spawn_monitored_task; use std::env; -use std::{ - borrow::Cow, - net::{IpAddr, SocketAddr}, - sync::Arc, - time::Duration, -}; -use sui_config::{sui_config_dir, SUI_CLIENT_CONFIG}; -use sui_faucet::{ - BatchFaucetResponse, BatchStatusFaucetResponse, Faucet, FaucetConfig, FaucetError, - FaucetRequest, FaucetResponse, RequestMetricsLayer, SimpleFaucet, -}; -use sui_sdk::wallet_context::WalletContext; -use tower::{limit::RateLimitLayer, ServiceBuilder}; -use tower_http::cors::{Any, CorsLayer}; -use tracing::{info, warn}; -use uuid::Uuid; +use std::sync::Arc; +use sui_config::sui_config_dir; +use sui_faucet::{create_wallet_context, start_faucet, AppState}; +use sui_faucet::{FaucetConfig, SimpleFaucet}; +use tracing::info; const CONCURRENCY_LIMIT: usize = 30; - -struct AppState> { - faucet: F, - config: FaucetConfig, -} - const PROM_PORT_ADDR: &str = "0.0.0.0:9184"; #[tokio::main] @@ -46,25 +19,20 @@ async fn main() -> Result<(), anyhow::Error> { .with_env() .init(); - let max_concurrency = match env::var("MAX_CONCURRENCY") { - Ok(val) => val.parse::().unwrap(), - _ => CONCURRENCY_LIMIT, - }; - info!("Max concurrency: {max_concurrency}."); - let config: FaucetConfig = FaucetConfig::parse(); let FaucetConfig { - port, - host_ip, - request_buffer_size, - max_request_per_second, wallet_client_timeout_secs, ref write_ahead_log, - wal_retry_interval, .. } = config; - let context = create_wallet_context(wallet_client_timeout_secs)?; + let context = create_wallet_context(wallet_client_timeout_secs, sui_config_dir()?)?; + + let max_concurrency = match env::var("MAX_CONCURRENCY") { + Ok(val) => val.parse::().unwrap(), + _ => CONCURRENCY_LIMIT, + }; + info!("Max concurrency: {max_concurrency}."); let prom_binding = PROM_PORT_ADDR.parse().unwrap(); info!("Starting Prometheus HTTP endpoint at {}", prom_binding); @@ -82,229 +50,5 @@ async fn main() -> Result<(), anyhow::Error> { config, }); - // TODO: restrict access if needed - let cors = CorsLayer::new() - .allow_methods(vec![Method::GET, Method::POST]) - .allow_headers(Any) - .allow_origin(Any); - - let app = Router::new() - .route("/", get(health)) - .route("/gas", post(request_gas)) - .route("/v1/gas", post(batch_request_gas)) - .route("/v1/status/:task_id", get(request_status)) - .layer( - ServiceBuilder::new() - .layer(HandleErrorLayer::new(handle_error)) - .layer(RequestMetricsLayer::new(&prometheus_registry)) - .layer(cors) - .load_shed() - .buffer(request_buffer_size) - .layer(RateLimitLayer::new( - max_request_per_second, - Duration::from_secs(1), - )) - .concurrency_limit(max_concurrency) - .layer(Extension(app_state.clone())) - .into_inner(), - ); - - spawn_monitored_task!(async move { - info!("Starting task to clear WAL."); - loop { - // Every config.wal_retry_interval (Default: 300 seconds) we try to clear the wal coins - tokio::time::sleep(Duration::from_secs(wal_retry_interval)).await; - app_state.faucet.retry_wal_coins().await.unwrap(); - } - }); - - let addr = SocketAddr::new(IpAddr::V4(host_ip), port); - info!("listening on {}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await?; - Ok(()) -} - -/// basic handler that responds with a static string -async fn health() -> &'static str { - "OK" -} - -/// handler for batch_request_gas requests -async fn batch_request_gas( - Extension(state): Extension>, - Json(payload): Json, -) -> impl IntoResponse { - let id = Uuid::new_v4(); - // ID for traceability - info!(uuid = ?id, "Got new gas request."); - - let FaucetRequest::FixedAmountRequest(request) = payload else { - return ( - StatusCode::BAD_REQUEST, - Json(BatchFaucetResponse::from(FaucetError::Internal( - "Input Error.".to_string(), - ))), - ); - }; - - if state.config.batch_enabled { - let result = spawn_monitored_task!(async move { - state - .faucet - .batch_send( - id, - request.recipient, - &vec![state.config.amount; state.config.num_coins], - ) - .await - }) - .await - .unwrap(); - - match result { - Ok(v) => { - info!(uuid =?id, "Request is successfully served"); - (StatusCode::ACCEPTED, Json(BatchFaucetResponse::from(v))) - } - Err(v) => { - warn!(uuid =?id, "Failed to request gas: {:?}", v); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(BatchFaucetResponse::from(v)), - ) - } - } - } else { - // TODO (jian): remove this feature gate when batch has proven to be baked long enough - info!(uuid = ?id, "Falling back to v1 implementation"); - let result = spawn_monitored_task!(async move { - state - .faucet - .send( - id, - request.recipient, - &vec![state.config.amount; state.config.num_coins], - ) - .await - }) - .await - .unwrap(); - - match result { - Ok(_) => { - info!(uuid =?id, "Request is successfully served"); - (StatusCode::ACCEPTED, Json(BatchFaucetResponse::from(id))) - } - Err(v) => { - warn!(uuid =?id, "Failed to request gas: {:?}", v); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(BatchFaucetResponse::from(v)), - ) - } - } - } -} - -/// handler for batch_get_status requests -async fn request_status( - Extension(state): Extension>, - Path(id): Path, -) -> impl IntoResponse { - match Uuid::parse_str(&id) { - Ok(task_id) => { - let result = state.faucet.get_batch_send_status(task_id).await; - match result { - Ok(v) => ( - StatusCode::CREATED, - Json(BatchStatusFaucetResponse::from(v)), - ), - Err(v) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(BatchStatusFaucetResponse::from(v)), - ), - } - } - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(BatchStatusFaucetResponse::from(FaucetError::Internal( - e.to_string(), - ))), - ), - } -} - -/// handler for all the request_gas requests -async fn request_gas( - Extension(state): Extension>, - Json(payload): Json, -) -> impl IntoResponse { - // ID for traceability - let id = Uuid::new_v4(); - info!(uuid = ?id, "Got new gas request."); - let result = match payload { - FaucetRequest::FixedAmountRequest(requests) => { - // We spawn a tokio task for this such that connection drop will not interrupt - // it and impact the recycling of coins - spawn_monitored_task!(async move { - state - .faucet - .send( - id, - requests.recipient, - &vec![state.config.amount; state.config.num_coins], - ) - .await - }) - .await - .unwrap() - } - _ => { - return ( - StatusCode::BAD_REQUEST, - Json(FaucetResponse::from(FaucetError::Internal( - "Input Error.".to_string(), - ))), - ) - } - }; - match result { - Ok(v) => { - info!(uuid =?id, "Request is successfully served"); - (StatusCode::CREATED, Json(FaucetResponse::from(v))) - } - Err(v) => { - warn!(uuid =?id, "Failed to request gas: {:?}", v); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(FaucetResponse::from(v)), - ) - } - } -} - -fn create_wallet_context(timeout_secs: u64) -> Result { - let wallet_conf = sui_config_dir()?.join(SUI_CLIENT_CONFIG); - info!("Initialize wallet from config path: {:?}", wallet_conf); - WalletContext::new( - &wallet_conf, - Some(Duration::from_secs(timeout_secs)), - Some(1000), - ) -} - -async fn handle_error(error: BoxError) -> impl IntoResponse { - if error.is::() { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Cow::from("service is overloaded, please try again later"), - ); - } - - ( - StatusCode::INTERNAL_SERVER_ERROR, - Cow::from(format!("Unhandled internal error: {}", error)), - ) + start_faucet(app_state, max_concurrency, &prometheus_registry).await } diff --git a/crates/sui-faucet/src/server.rs b/crates/sui-faucet/src/server.rs new file mode 100644 index 0000000000000..4b4f2400f6d13 --- /dev/null +++ b/crates/sui-faucet/src/server.rs @@ -0,0 +1,278 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + AppState, BatchFaucetResponse, BatchStatusFaucetResponse, FaucetConfig, FaucetError, + FaucetRequest, FaucetResponse, RequestMetricsLayer, +}; + +use axum::{ + error_handling::HandleErrorLayer, + extract::Path, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + BoxError, Extension, Json, Router, +}; +use http::Method; +use mysten_metrics::spawn_monitored_task; +use prometheus::Registry; +use std::{ + borrow::Cow, + net::{IpAddr, SocketAddr}, + path::PathBuf, + sync::Arc, + time::Duration, +}; +use sui_config::SUI_CLIENT_CONFIG; +use sui_sdk::wallet_context::WalletContext; +use tower::{limit::RateLimitLayer, ServiceBuilder}; +use tower_http::cors::{Any, CorsLayer}; +use tracing::{info, warn}; +use uuid::Uuid; + +use crate::faucet::Faucet; + +pub async fn start_faucet( + app_state: Arc, + concurrency_limit: usize, + prometheus_registry: &Registry, +) -> Result<(), anyhow::Error> { + // TODO: restrict access if needed + let cors = CorsLayer::new() + .allow_methods(vec![Method::GET, Method::POST]) + .allow_headers(Any) + .allow_origin(Any); + + let FaucetConfig { + port, + host_ip, + request_buffer_size, + max_request_per_second, + wal_retry_interval, + .. + } = app_state.config; + + let app = Router::new() + .route("/", get(health)) + .route("/gas", post(request_gas)) + .route("/v1/gas", post(batch_request_gas)) + .route("/v1/status/:task_id", get(request_status)) + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(handle_error)) + .layer(RequestMetricsLayer::new(prometheus_registry)) + .layer(cors) + .load_shed() + .buffer(request_buffer_size) + .layer(RateLimitLayer::new( + max_request_per_second, + Duration::from_secs(1), + )) + .concurrency_limit(concurrency_limit) + .layer(Extension(app_state.clone())) + .into_inner(), + ); + + spawn_monitored_task!(async move { + info!("Starting task to clear WAL."); + loop { + // Every config.wal_retry_interval (Default: 300 seconds) we try to clear the wal coins + tokio::time::sleep(Duration::from_secs(wal_retry_interval)).await; + app_state.faucet.retry_wal_coins().await.unwrap(); + } + }); + + let addr = SocketAddr::new(IpAddr::V4(host_ip), port); + info!("listening on {}", addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +/// basic handler that responds with a static string +async fn health() -> &'static str { + "OK" +} + +/// handler for batch_request_gas requests +async fn batch_request_gas( + Extension(state): Extension>, + Json(payload): Json, +) -> impl IntoResponse { + let id = Uuid::new_v4(); + // ID for traceability + info!(uuid = ?id, "Got new gas request."); + + let FaucetRequest::FixedAmountRequest(request) = payload else { + return ( + StatusCode::BAD_REQUEST, + Json(BatchFaucetResponse::from(FaucetError::Internal( + "Input Error.".to_string(), + ))), + ); + }; + + if state.config.batch_enabled { + let result = spawn_monitored_task!(async move { + state + .faucet + .batch_send( + id, + request.recipient, + &vec![state.config.amount; state.config.num_coins], + ) + .await + }) + .await + .unwrap(); + + match result { + Ok(v) => { + info!(uuid =?id, "Request is successfully served"); + (StatusCode::ACCEPTED, Json(BatchFaucetResponse::from(v))) + } + Err(v) => { + warn!(uuid =?id, "Failed to request gas: {:?}", v); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(BatchFaucetResponse::from(v)), + ) + } + } + } else { + // TODO (jian): remove this feature gate when batch has proven to be baked long enough + info!(uuid = ?id, "Falling back to v1 implementation"); + let result = spawn_monitored_task!(async move { + state + .faucet + .send( + id, + request.recipient, + &vec![state.config.amount; state.config.num_coins], + ) + .await + }) + .await + .unwrap(); + + match result { + Ok(_) => { + info!(uuid =?id, "Request is successfully served"); + (StatusCode::ACCEPTED, Json(BatchFaucetResponse::from(id))) + } + Err(v) => { + warn!(uuid =?id, "Failed to request gas: {:?}", v); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(BatchFaucetResponse::from(v)), + ) + } + } + } +} + +/// handler for batch_get_status requests +async fn request_status( + Extension(state): Extension>, + Path(id): Path, +) -> impl IntoResponse { + match Uuid::parse_str(&id) { + Ok(task_id) => { + let result = state.faucet.get_batch_send_status(task_id).await; + match result { + Ok(v) => ( + StatusCode::CREATED, + Json(BatchStatusFaucetResponse::from(v)), + ), + Err(v) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(BatchStatusFaucetResponse::from(v)), + ), + } + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(BatchStatusFaucetResponse::from(FaucetError::Internal( + e.to_string(), + ))), + ), + } +} + +/// handler for all the request_gas requests +async fn request_gas( + Extension(state): Extension>, + Json(payload): Json, +) -> impl IntoResponse { + // ID for traceability + let id = Uuid::new_v4(); + info!(uuid = ?id, "Got new gas request."); + let result = match payload { + FaucetRequest::FixedAmountRequest(requests) => { + // We spawn a tokio task for this such that connection drop will not interrupt + // it and impact the recycling of coins + spawn_monitored_task!(async move { + state + .faucet + .send( + id, + requests.recipient, + &vec![state.config.amount; state.config.num_coins], + ) + .await + }) + .await + .unwrap() + } + _ => { + return ( + StatusCode::BAD_REQUEST, + Json(FaucetResponse::from(FaucetError::Internal( + "Input Error.".to_string(), + ))), + ) + } + }; + match result { + Ok(v) => { + info!(uuid =?id, "Request is successfully served"); + (StatusCode::CREATED, Json(FaucetResponse::from(v))) + } + Err(v) => { + warn!(uuid =?id, "Failed to request gas: {:?}", v); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(FaucetResponse::from(v)), + ) + } + } +} + +pub fn create_wallet_context( + timeout_secs: u64, + config_dir: PathBuf, +) -> Result { + let wallet_conf = config_dir.join(SUI_CLIENT_CONFIG); + info!("Initialize wallet from config path: {:?}", wallet_conf); + WalletContext::new( + &wallet_conf, + Some(Duration::from_secs(timeout_secs)), + Some(1000), + ) +} + +async fn handle_error(error: BoxError) -> impl IntoResponse { + if error.is::() { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Cow::from("service is overloaded, please try again later"), + ); + } + + ( + StatusCode::INTERNAL_SERVER_ERROR, + Cow::from(format!("Unhandled internal error: {}", error)), + ) +} diff --git a/crates/sui-sdk/examples/utils.rs b/crates/sui-sdk/examples/utils.rs index f05754504434a..90068eaa8079d 100644 --- a/crates/sui-sdk/examples/utils.rs +++ b/crates/sui-sdk/examples/utils.rs @@ -40,7 +40,6 @@ struct FaucetResponse { pub const SUI_FAUCET: &str = "https://faucet.testnet.sui.io/v1/gas"; // testnet faucet -// if you use the sui-test-validator and use the local network; if it does not work, try with port 5003. // const SUI_FAUCET: &str = "http://127.0.0.1:9123/gas"; /// Return a sui client to interact with the APIs, diff --git a/crates/sui-sdk/src/lib.rs b/crates/sui-sdk/src/lib.rs index c8ba5b1a8bf73..d94b3fe5874a4 100644 --- a/crates/sui-sdk/src/lib.rs +++ b/crates/sui-sdk/src/lib.rs @@ -108,6 +108,7 @@ pub mod wallet_context; pub const SUI_COIN_TYPE: &str = "0x2::sui::SUI"; pub const SUI_LOCAL_NETWORK_URL: &str = "http://127.0.0.1:9000"; +pub const SUI_LOCAL_NETWORK_URL_0: &str = "http://0.0.0.0:9000"; pub const SUI_LOCAL_NETWORK_GAS_URL: &str = "http://127.0.0.1:5003/gas"; pub const SUI_DEVNET_URL: &str = "https://fullnode.devnet.sui.io:443"; pub const SUI_TESTNET_URL: &str = "https://fullnode.testnet.sui.io:443"; diff --git a/crates/sui-swarm-config/src/node_config_builder.rs b/crates/sui-swarm-config/src/node_config_builder.rs index c29c17ade2455..e39eeab0dc0c9 100644 --- a/crates/sui-swarm-config/src/node_config_builder.rs +++ b/crates/sui-swarm-config/src/node_config_builder.rs @@ -273,6 +273,7 @@ pub struct FullnodeConfigBuilder { run_with_range: Option, policy_config: Option, fw_config: Option, + data_ingestion_dir: Option, } impl FullnodeConfigBuilder { @@ -380,6 +381,11 @@ impl FullnodeConfigBuilder { self } + pub fn with_data_ingestion_dir(mut self, path: Option) -> Self { + self.data_ingestion_dir = path; + self + } + pub fn build( self, rng: &mut R, @@ -443,6 +449,11 @@ impl FullnodeConfigBuilder { format!("{}:{}", ip, rpc_port).parse().unwrap() }); + let checkpoint_executor_config = CheckpointExecutorConfig { + data_ingestion_dir: self.data_ingestion_dir, + ..Default::default() + }; + NodeConfig { protocol_key_pair: AuthorityKeyPairWithPath::new(validator_config.key_pair), account_key_pair: KeyPairWithPath::new(validator_config.account_key_pair), @@ -477,7 +488,7 @@ impl FullnodeConfigBuilder { authority_store_pruning_config: AuthorityStorePruningConfig::default(), end_of_epoch_broadcast_channel_capacity: default_end_of_epoch_broadcast_channel_capacity(), - checkpoint_executor_config: Default::default(), + checkpoint_executor_config, metrics: None, supported_protocol_versions: self.supported_protocol_versions, db_checkpoint_config: self.db_checkpoint_config.unwrap_or_default(), diff --git a/crates/sui-swarm/src/memory/swarm.rs b/crates/sui-swarm/src/memory/swarm.rs index 053f2353e1154..b03cf664312fb 100644 --- a/crates/sui-swarm/src/memory/swarm.rs +++ b/crates/sui-swarm/src/memory/swarm.rs @@ -306,6 +306,8 @@ impl SwarmBuilder { SwarmDirectory::new_temporary() }; + let ingest_data = self.data_ingestion_dir.clone(); + let network_config = self.network_config.unwrap_or_else(|| { let mut config_builder = ConfigBuilder::new(dir.as_ref()); @@ -371,7 +373,9 @@ impl SwarmBuilder { .with_db_checkpoint_config(self.db_checkpoint_config.clone()) .with_run_with_range(self.fullnode_run_with_range) .with_policy_config(self.fullnode_policy_config) + .with_data_ingestion_dir(ingest_data) .with_fw_config(self.fullnode_fw_config); + if let Some(spvc) = &self.fullnode_supported_protocol_versions_config { let supported_versions = match spvc { ProtocolVersionsConfig::Default => SupportedProtocolVersions::SYSTEM_DEFAULT, diff --git a/crates/sui/Cargo.toml b/crates/sui/Cargo.toml index a323ce3e90152..9c06cb2893505 100644 --- a/crates/sui/Cargo.toml +++ b/crates/sui/Cargo.toml @@ -7,44 +7,55 @@ publish = false edition = "2021" [dependencies] -json_to_table.workspace = true -tabled.workspace = true anemo.workspace = true anyhow.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_yaml.workspace = true -signature.workspace = true -camino.workspace = true -tokio = { workspace = true, features = ["full"] } +async-recursion.workspace = true async-trait.workspace = true -tracing.workspace = true +axum.workspace = true bcs.workspace = true -clap.workspace = true +bin-version.workspace = true bip32.workspace = true -rand.workspace = true -tap.workspace = true +camino.workspace = true +clap.workspace = true +datatest-stable.workspace = true +futures.workspace = true +http.workspace = true +im.workspace = true inquire.workspace = true -rusoto_core.workspace = true -rusoto_kms.workspace = true -prometheus.workspace = true -bin-version.workspace = true +insta.workspace = true +json_to_table.workspace = true +miette.workspace = true num-bigint.workspace = true +prometheus.workspace = true +rand.workspace = true regex.workspace = true reqwest.workspace = true -im.workspace = true -async-recursion.workspace = true -thiserror.workspace = true -miette.workspace = true -datatest-stable.workspace = true -insta.workspace = true +rusoto_core.workspace = true +rusoto_kms.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true +serde.workspace = true shlex.workspace = true -futures.workspace = true +signature.workspace = true +tabled.workspace = true +tap.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["full"] } +tower.workspace = true +tower-http.workspace = true +tracing.workspace = true +diesel.workspace = true +uuid.workspace = true +url.workspace = true sui-config.workspace = true sui-bridge.workspace = true +sui-cluster-test.workspace = true sui-execution = { path = "../../sui-execution" } +sui-faucet.workspace = true sui-swarm-config.workspace = true +sui-graphql-rpc = {workspace = true, optional = true} +sui-indexer = { workspace = true, optional = true } sui-genesis-builder.workspace = true sui-types.workspace = true sui-json.workspace = true @@ -61,6 +72,7 @@ shared-crypto.workspace = true sui-replay.workspace = true sui-transaction-builder.workspace = true move-binary-format.workspace = true +test-cluster.workspace = true fastcrypto.workspace = true fastcrypto-zkp.workspace = true @@ -119,3 +131,4 @@ gas-profiler = [ "sui-types/gas-profiler", "sui-execution/gas-profiler", ] +indexer = ["dep:sui-indexer", "dep:sui-graphql-rpc"] diff --git a/crates/sui/src/client_commands.rs b/crates/sui/src/client_commands.rs index b72713ecc15f4..f0182776e4e2e 100644 --- a/crates/sui/src/client_commands.rs +++ b/crates/sui/src/client_commands.rs @@ -57,7 +57,8 @@ use sui_sdk::{ apis::ReadApi, sui_client_config::{SuiClientConfig, SuiEnv}, wallet_context::WalletContext, - SuiClient, SUI_COIN_TYPE, SUI_DEVNET_URL, SUI_LOCAL_NETWORK_URL, SUI_TESTNET_URL, + SuiClient, SUI_COIN_TYPE, SUI_DEVNET_URL, SUI_LOCAL_NETWORK_URL, SUI_LOCAL_NETWORK_URL_0, + SUI_TESTNET_URL, }; use sui_types::{ base_types::{ObjectID, SequenceNumber, SuiAddress}, @@ -1385,8 +1386,7 @@ impl SuiClientCommands { let network = match env.rpc.as_str() { SUI_DEVNET_URL => "https://faucet.devnet.sui.io/v1/gas", SUI_TESTNET_URL => "https://faucet.testnet.sui.io/v1/gas", - // TODO when using sui-test-validator, and 5003 when using sui start - SUI_LOCAL_NETWORK_URL => "http://127.0.0.1:9123/gas", + SUI_LOCAL_NETWORK_URL | SUI_LOCAL_NETWORK_URL_0 => "http://127.0.0.1:9123/gas", _ => bail!("Cannot recognize the active network. Please provide the gas faucet full URL.") }; network.to_string() diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 1908e56f7622c..757d10631fc1b 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -7,7 +7,7 @@ use crate::fire_drill::{run_fire_drill, FireDrill}; use crate::genesis_ceremony::{run, Ceremony}; use crate::keytool::KeyToolCommand; use crate::validator_commands::SuiValidatorCommand; -use anyhow::{anyhow, bail}; +use anyhow::{anyhow, bail, ensure}; use clap::*; use fastcrypto::traits::KeyPair; use move_analyzer::analyzer; @@ -16,6 +16,7 @@ use rand::rngs::OsRng; use std::io::{stderr, stdout, Write}; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::{fs, io}; use sui_bridge::config::BridgeCommitteeConfig; use sui_bridge::sui_client::SuiBridgeClient; @@ -23,12 +24,19 @@ use sui_bridge::sui_transaction_builder::build_committee_register_transaction; use sui_config::node::Genesis; use sui_config::p2p::SeedPeer; use sui_config::{ - sui_config_dir, Config, PersistedConfig, FULL_NODE_DB_PATH, SUI_CLIENT_CONFIG, - SUI_FULLNODE_CONFIG, SUI_NETWORK_CONFIG, + genesis_blob_exists, sui_config_dir, Config, PersistedConfig, FULL_NODE_DB_PATH, + SUI_CLIENT_CONFIG, SUI_FULLNODE_CONFIG, SUI_NETWORK_CONFIG, }; use sui_config::{ SUI_BENCHMARK_GENESIS_GAS_KEYSTORE_FILENAME, SUI_GENESIS_FILENAME, SUI_KEYSTORE_FILENAME, }; +use sui_faucet::{create_wallet_context, start_faucet, AppState, FaucetConfig, SimpleFaucet}; +#[cfg(feature = "indexer")] +use sui_graphql_rpc::{ + config::ConnectionConfig, test_infra::cluster::start_graphql_server_with_fn_rpc, +}; +#[cfg(feature = "indexer")] +use sui_indexer::test_utils::{start_test_indexer, ReaderWriterConfig}; use sui_keys::keypair_file::read_key; use sui_keys::keystore::{AccountKeystore, FileBasedKeystore, Keystore}; use sui_move::{self, execute_move_command}; @@ -42,17 +50,138 @@ use sui_swarm_config::network_config_builder::ConfigBuilder; use sui_swarm_config::node_config_builder::FullnodeConfigBuilder; use sui_types::base_types::SuiAddress; use sui_types::crypto::{SignatureScheme, SuiKeyPair, ToFromBytes}; +use tempfile::tempdir; +use tracing; use tracing::info; +const CONCURRENCY_LIMIT: usize = 30; +const DEFAULT_EPOCH_DURATION_MS: u64 = 60_000; +const DEFAULT_FAUCET_NUM_COINS: usize = 5; // 5 coins per request was the default in sui-test-validator +const DEFAULT_FAUCET_MIST_AMOUNT: u64 = 200_000_000_000; // 200 SUI + +#[cfg(feature = "indexer")] +#[derive(Args)] +pub struct IndexerFeatureArgs { + /// Start an indexer with default host and port: 0.0.0.0:9124, or on the port provided. + /// When providing a specific value, please use the = sign between the flag and value: + /// `--with-indexer=6124`. + /// The indexer will be started in writer mode and reader mode. + #[clap(long, + default_missing_value = "9124", + num_args = 0..=1, + require_equals = true, + value_name = "INDEXER_PORT" + )] + with_indexer: Option, + + /// Start a GraphQL server on localhost and port: 127.0.0.1:9125, or on the port provided. + /// When providing a specific value, please use the = sign between the flag and value: + /// `--with-graphql=6125`. + /// Note that GraphQL requires a running indexer, which will be enabled by default if the + /// `--with-indexer` flag is not set. + #[clap( + long, + default_missing_value = "9125", + num_args = 0..=1, + require_equals = true, + value_name = "GRAPHQL_PORT" + )] + with_graphql: Option, + + /// Port for the Indexer Postgres DB. Default port is 5432. + #[clap(long, default_value = "5432")] + pg_port: u16, + + /// Hostname for the Indexer Postgres DB. Default host is localhost. + #[clap(long, default_value = "localhost")] + pg_host: String, + + /// DB name for the Indexer Postgres DB. Default DB name is sui_indexer. + #[clap(long, default_value = "sui_indexer")] + pg_db_name: String, + + /// DB username for the Indexer Postgres DB. Default username is postgres. + #[clap(long, default_value = "postgres")] + pg_user: String, + + /// DB password for the Indexer Postgres DB. Default password is postgrespw. + #[clap(long, default_value = "postgrespw")] + pg_password: String, +} + +#[cfg(feature = "indexer")] +impl IndexerFeatureArgs { + pub fn for_testing() -> Self { + Self { + with_indexer: None, + with_graphql: None, + pg_port: 5432, + pg_host: "localhost".to_string(), + pg_db_name: "sui_indexer".to_string(), + pg_user: "postgres".to_string(), + pg_password: "postgrespw".to_string(), + } + } +} + #[allow(clippy::large_enum_variant)] #[derive(Parser)] #[clap(rename_all = "kebab-case")] pub enum SuiCommand { - /// Start sui network. + /// Start a local network in two modes: saving state between re-runs and not saving state + /// between re-runs. Please use (--help) to see the full description. + /// + /// By default, sui start will start a local network from the genesis blob that exists in + /// the Sui config default dir or in the config_dir that was passed. If the default directory + /// does not exist and the config_dir is not passed, it will generate a new default directory, + /// generate the genesis blob, and start the network. + /// + /// Note that if you want to start an indexer, Postgres DB is required. #[clap(name = "start")] Start { + /// Config directory that will be used to store network config, node db, keystore + /// sui genesis -f --with-faucet generates a genesis config that can be used to start this + /// proces. Use with caution as the `-f` flag will overwrite the existing config directory. + /// We can use any config dir that is generated by the `sui genesis`. #[clap(long = "network.config")] - config: Option, + config_dir: Option, + + /// A new genesis is created each time this flag is set, and state is not persisted between + /// runs. Only use this flag when you want to start the network from scratch every time you + /// run this command. + /// + /// To run with persisted state, do not pass this flag and use the `sui genesis` command + /// to generate a genesis that can be used to start the network with. + #[clap(long)] + force_regenesis: bool, + + /// Start a faucet with default host and port: 127.0.0.1:9123, or on the port provided. + /// When providing a specific value, please use the = sign between the flag and value: + /// `--with-faucet=6123`. + #[clap( + long, + default_missing_value = "9123", + num_args = 0..=1, + require_equals = true, + value_name = "FAUCET_PORT" + )] + with_faucet: Option, + + #[cfg(feature = "indexer")] + #[clap(flatten)] + indexer_feature_args: IndexerFeatureArgs, + + /// Port to start the Fullnode RPC server on. Default port is 9000. + #[clap(long, default_value = "9000")] + fullnode_rpc_port: u16, + + /// Set the epoch duration. Can only be used when `--force-regenesis` flag is passed or if + /// there's no genesis config and one will be auto-generated. When this flag is not set but + /// `--force-regenesis` is set, the epoch duration will be set to 60 seconds. + #[clap(long)] + epoch_duration_ms: Option, + + /// Start the network without a fullnode #[clap(long = "no-full-node")] no_full_node: bool, }, @@ -89,7 +218,7 @@ pub enum SuiCommand { benchmark_ips: Option>, #[clap( long, - help = "Creates an extra faucet configuration for sui-test-validator persisted runs." + help = "Creates an extra faucet configuration for sui persisted runs." )] with_faucet: bool, }, @@ -183,60 +312,6 @@ impl SuiCommand { pub async fn execute(self) -> Result<(), anyhow::Error> { move_package::package_hooks::register_package_hooks(Box::new(SuiPackageHooks)); match self { - SuiCommand::Start { - config, - no_full_node, - } => { - // Auto genesis if path is none and sui directory doesn't exists. - if config.is_none() && !sui_config_dir()?.join(SUI_NETWORK_CONFIG).exists() { - genesis(None, None, None, false, None, None, false).await?; - } - - // Load the config of the Sui authority. - let network_config_path = config - .clone() - .unwrap_or(sui_config_dir()?.join(SUI_NETWORK_CONFIG)); - let network_config: NetworkConfig = PersistedConfig::read(&network_config_path) - .map_err(|err| { - err.context(format!( - "Cannot open Sui network config file at {:?}", - network_config_path - )) - })?; - let mut swarm_builder = Swarm::builder() - .dir(sui_config_dir()?) - .with_network_config(network_config); - if no_full_node { - swarm_builder = swarm_builder.with_fullnode_count(0); - } else { - swarm_builder = swarm_builder - .with_fullnode_count(1) - .with_fullnode_rpc_addr(sui_config::node::default_json_rpc_address()); - } - let mut swarm = swarm_builder.build(); - swarm.launch().await?; - - let mut interval = tokio::time::interval(std::time::Duration::from_secs(3)); - let mut unhealthy_cnt = 0; - loop { - for node in swarm.validator_nodes() { - if let Err(err) = node.health_check(true).await { - unhealthy_cnt += 1; - if unhealthy_cnt > 3 { - // The network could temporarily go down during reconfiguration. - // If we detect a failed validator 3 times in a row, give up. - return Err(err.into()); - } - // Break the inner loop so that we could retry latter. - break; - } else { - unhealthy_cnt = 0; - } - } - - interval.tick().await; - } - } SuiCommand::Network { config, dump_addresses, @@ -260,6 +335,30 @@ impl SuiCommand { } Ok(()) } + SuiCommand::Start { + config_dir, + force_regenesis, + with_faucet, + #[cfg(feature = "indexer")] + indexer_feature_args, + fullnode_rpc_port, + no_full_node, + epoch_duration_ms, + } => { + start( + config_dir.clone(), + with_faucet, + #[cfg(feature = "indexer")] + indexer_feature_args, + force_regenesis, + epoch_duration_ms, + fullnode_rpc_port, + no_full_node, + ) + .await?; + + Ok(()) + } SuiCommand::Genesis { working_dir, force, @@ -430,6 +529,258 @@ impl SuiCommand { } } +/// Starts a local network with the given configuration. +async fn start( + config: Option, + with_faucet: Option, + #[cfg(feature = "indexer")] indexer_feature_args: IndexerFeatureArgs, + force_regenesis: bool, + epoch_duration_ms: Option, + fullnode_rpc_port: u16, + no_full_node: bool, +) -> Result<(), anyhow::Error> { + if force_regenesis { + ensure!( + config.is_none(), + "Cannot pass `--force-regenesis` and `--network.config` at the same time." + ); + } + + #[cfg(feature = "indexer")] + let IndexerFeatureArgs { + mut with_indexer, + with_graphql, + pg_port, + pg_host, + pg_db_name, + pg_user, + pg_password, + } = indexer_feature_args; + + #[cfg(feature = "indexer")] + if with_graphql.is_some() { + with_indexer = Some(with_indexer.unwrap_or_default()); + } + + #[cfg(feature = "indexer")] + if with_indexer.is_some() { + ensure!( + !no_full_node, + "Cannot start the indexer without a fullnode." + ); + } + + if epoch_duration_ms.is_some() && genesis_blob_exists(config.clone()) && !force_regenesis { + bail!( + "Epoch duration can only be set when passing the `--force-regenesis` flag, or when \ + there is no genesis configuration in the default Sui configuration folder or the given \ + network.config argument.", + ); + } + + #[cfg(feature = "indexer")] + if let Some(indexer_rpc_port) = with_indexer { + tracing::info!("Starting the indexer service at 0.0.0.0:{indexer_rpc_port}"); + } + #[cfg(feature = "indexer")] + if let Some(graphql_port) = with_graphql { + tracing::info!("Starting the GraphQL service at 127.0.0.1:{graphql_port}"); + } + if let Some(faucet_port) = with_faucet { + tracing::info!("Starting the faucet service at 127.0.0.1:{faucet_port}"); + } + + let mut swarm_builder = Swarm::builder(); + // If this is set, then no data will be persisted between runs, and a new genesis will be + // generated each run. + if force_regenesis { + swarm_builder = + swarm_builder.committee_size(NonZeroUsize::new(DEFAULT_NUMBER_OF_AUTHORITIES).unwrap()); + let genesis_config = GenesisConfig::custom_genesis(1, 100); + swarm_builder = swarm_builder.with_genesis_config(genesis_config); + let epoch_duration_ms = epoch_duration_ms.unwrap_or(DEFAULT_EPOCH_DURATION_MS); + swarm_builder = swarm_builder.with_epoch_duration_ms(epoch_duration_ms); + } else { + // load from config dir that was passed, or generate a new genesis if there is no config + // dir passed and there is no config_dir in the default location + // and if a dir exists, then use that one + if let Some(config) = config.clone() { + swarm_builder = swarm_builder.dir(config); + } else if config.is_none() && !sui_config_dir()?.join(SUI_NETWORK_CONFIG).exists() { + genesis(None, None, None, false, epoch_duration_ms, None, false).await?; + swarm_builder = swarm_builder.dir(sui_config_dir()?); + } else { + swarm_builder = swarm_builder.dir(sui_config_dir()?); + } + // Load the config of the Sui authority. + let network_config_path = config + .clone() + .unwrap_or(sui_config_dir()?.join(SUI_NETWORK_CONFIG)); + let network_config: NetworkConfig = + PersistedConfig::read(&network_config_path).map_err(|err| { + err.context(format!( + "Cannot open Sui network config file at {:?}", + network_config_path + )) + })?; + + swarm_builder = swarm_builder.with_network_config(network_config); + } + + #[cfg(feature = "indexer")] + let data_ingestion_path = tempdir()?.into_path(); + + // the indexer requires to set the fullnode's data ingestion directory + // note that this overrides the default configuration that is set when running the genesis + // command, which sets data_ingestion_dir to None. + #[cfg(feature = "indexer")] + if with_indexer.is_some() { + swarm_builder = swarm_builder.with_data_ingestion_dir(data_ingestion_path.clone()); + } + + let mut fullnode_url = sui_config::node::default_json_rpc_address(); + fullnode_url.set_port(fullnode_rpc_port); + + if no_full_node { + swarm_builder = swarm_builder.with_fullnode_count(0); + } else { + swarm_builder = swarm_builder + .with_fullnode_count(1) + .with_fullnode_rpc_addr(fullnode_url); + } + + let mut swarm = swarm_builder.build(); + swarm.launch().await?; + // Let nodes connect to one another + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + info!("Cluster started"); + + // the indexer requires a fullnode url with protocol specified + let fullnode_url = format!("http://{}", fullnode_url); + info!("Fullnode URL: {}", fullnode_url); + #[cfg(feature = "indexer")] + let pg_address = format!("postgres://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_db_name}"); + + #[cfg(feature = "indexer")] + if with_indexer.is_some() { + let indexer_address = format!("0.0.0.0:{}", with_indexer.unwrap_or_default()); + // Start in writer mode + start_test_indexer::( + Some(pg_address.clone()), + fullnode_url.clone(), + ReaderWriterConfig::writer_mode(None), + data_ingestion_path.clone(), + ) + .await; + info!("Indexer in writer mode started"); + + // Start in reader mode + start_test_indexer::( + Some(pg_address.clone()), + fullnode_url.clone(), + ReaderWriterConfig::reader_mode(indexer_address.to_string()), + data_ingestion_path, + ) + .await; + info!("Indexer in reader mode started"); + } + + #[cfg(feature = "indexer")] + if with_graphql.is_some() { + let graphql_connection_config = ConnectionConfig::new( + Some(with_graphql.unwrap_or_default()), + None, + Some(pg_address), + None, + None, + None, + ); + start_graphql_server_with_fn_rpc( + graphql_connection_config, + Some(fullnode_url.clone()), + None, // it will be initialized by default + ) + .await; + info!("GraphQL started"); + } + if with_faucet.is_some() { + let config_dir = if force_regenesis { + tempdir()?.into_path() + } else { + match config { + Some(config) => config, + None => sui_config_dir()?, + } + }; + + let config = FaucetConfig { + port: with_faucet.unwrap_or_default(), + num_coins: DEFAULT_FAUCET_NUM_COINS, + amount: DEFAULT_FAUCET_MIST_AMOUNT, + ..Default::default() + }; + let prometheus_registry = prometheus::Registry::new(); + if force_regenesis { + let kp = swarm.config_mut().account_keys.swap_remove(0); + let keystore_path = config_dir.join(SUI_KEYSTORE_FILENAME); + let mut keystore = Keystore::from(FileBasedKeystore::new(&keystore_path).unwrap()); + let address: SuiAddress = kp.public().into(); + keystore.add_key(None, SuiKeyPair::Ed25519(kp)).unwrap(); + SuiClientConfig { + keystore, + envs: vec![SuiEnv { + alias: "localnet".to_string(), + rpc: fullnode_url, + ws: None, + basic_auth: None, + }], + active_address: Some(address), + active_env: Some("localnet".to_string()), + } + .persisted(config_dir.join(SUI_CLIENT_CONFIG).as_path()) + .save() + .unwrap(); + } + let faucet_wal = config_dir.join("faucet.wal"); + let simple_faucet = SimpleFaucet::new( + create_wallet_context(config.wallet_client_timeout_secs, config_dir)?, + &prometheus_registry, + faucet_wal.as_path(), + config.clone(), + ) + .await + .unwrap(); + + let app_state = Arc::new(AppState { + faucet: simple_faucet, + config, + }); + + start_faucet(app_state, CONCURRENCY_LIMIT, &prometheus_registry).await?; + } + + let mut interval = tokio::time::interval(std::time::Duration::from_secs(3)); + let mut unhealthy_cnt = 0; + loop { + for node in swarm.validator_nodes() { + if let Err(err) = node.health_check(true).await { + unhealthy_cnt += 1; + if unhealthy_cnt > 3 { + // The network could temporarily go down during reconfiguration. + // If we detect a failed validator 3 times in a row, give up. + return Err(err.into()); + } + // Break the inner loop so that we could retry latter. + break; + } else { + unhealthy_cnt = 0; + } + } + + interval.tick().await; + } +} + async fn genesis( from_config: Option, write_config: Option, diff --git a/crates/sui/tests/cli_tests.rs b/crates/sui/tests/cli_tests.rs index 924ea8892f925..7f0deb7791b1c 100644 --- a/crates/sui/tests/cli_tests.rs +++ b/crates/sui/tests/cli_tests.rs @@ -14,6 +14,8 @@ use move_package::{lock_file::schema::ManagedPackage, BuildConfig as MoveBuildCo use serde_json::json; use sui::client_ptb::ptb::PTB; use sui::key_identity::{get_identity_address, KeyIdentity}; +#[cfg(feature = "indexer")] +use sui::sui_commands::IndexerFeatureArgs; use sui_sdk::SuiClient; use sui_test_transaction_builder::batch_make_transfer_transactions; use sui_types::object::Owner; @@ -66,8 +68,14 @@ async fn test_genesis() -> Result<(), anyhow::Error> { // Start network without authorities let start = SuiCommand::Start { - config: Some(config), + config_dir: Some(config), + force_regenesis: false, + with_faucet: None, + fullnode_rpc_port: 9000, + epoch_duration_ms: None, no_full_node: false, + #[cfg(feature = "indexer")] + indexer_feature_args: IndexerFeatureArgs::for_testing(), } .execute() .await; diff --git a/scripts/sui-test-validator.sh b/scripts/sui-test-validator.sh new file mode 100755 index 0000000000000..ec1209358e646 --- /dev/null +++ b/scripts/sui-test-validator.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Copyright (c) Mysten Labs, Inc. +# SPDX-License-Identifier: Apache-2.0 + +echo "sui-test-validator binary has been deprecated in favor of sui start, which is a more powerful command that allows you to start the local network with more options. +This script offers backward compatibiltiy, but ideally, you should migrate to sui start instead. Use sui start --help to see all the flags and options. + +To recreate the exact basic functionality of sui-test-validator, you must use the following options: + * --with-faucet --> to start the faucet server on the default host and port + * --force-regenesis --> to start the local network without persisting the state and from a new genesis + +You can also use the following options to start the local network with more features: + * --with-indexer --> to start the indexer on the default host and port. Note that this requires a Postgres database to be running locally, or you need to set the different options to connect to a remote indexer database. + * --with-graphql --> to start the GraphQL server on the default host and port" + +# holds the args names +named_args=() +config_dir=false; +indexer_port_set=false; +with_indexer=false; + +# Iterate over all arguments +while [[ $# -gt 0 ]]; do + case $1 in + + --with-indexer) + with_indexer=true + shift # Remove argument from processing + ;; + --config-dir=*) + value="${1#*=}" + named_args+=("--network.config=$value") + config_dir=true + shift # Remove argument from processing + ;; + --config-dir) + if [[ -n $2 && $2 != --* ]]; then + named_args+=("--network.config=$2") + config_dir=true + shift # Remove value from processing + fi + shift # Remove argument from processing + ;; + --faucet-port=*) + port_value="${1#*=}" + named_args+=("--with-faucet=$port_value") + shift # Remove argument from processing + ;; + --faucet-port) + if [[ -n $2 && $2 != --* ]]; then + named_args+=("--with-faucet=$2") + shift # Remove value from processing + fi + shift # Remove argument from processing + ;; + --indexer-rpc-port=*) + port_value="${1#*=}" + named_args+=("--with-indexer=$port_value") + indexer_port_set=true + shift # Remove argument from processing + ;; + --indexer-rpc-port) + if [[ -n $2 && $2 != --* ]]; then + named_args+=("--with-indexer=$2") + indexer_port_set=true + shift # Remove value from processing + fi + shift # Remove argument from processing + ;; + --graphql-port=*) + port_value="${1#*=}" + named_args+=("--with-graphql=$port_value") + shift # Remove argument from processing + ;; + --graphql-port) + if [[ -n $2 && $2 != --* ]]; then + named_args+=("--with-graphql=$2") + shift # Remove value from processing + fi + shift # Remove argument from processing + ;; + *) + named_args+=("$1") + shift # Remove unknown arguments from processing + ;; + esac +done + +if [[ $indexer_port_set = false ]] && [[ $with_indexer = true ]]; then + named_args+=("--with-indexer") +fi + +# Basic command that replicates the command line arguments of sui-test-validator +cmd="sui start --with-faucet --force-regenesis" + + +# To maintain compatibility, when passing a network configuration in a directory, --force-regenesis cannot be passed. +if [ "$config_dir" = true ]; then + echo "Starting with the provided network configuration." + cmd="sui start --with-faucet" +fi + +echo "Running command: $cmd ${named_args[@]}" +$cmd "${named_args[@]}" +