diff --git a/Cargo.lock b/Cargo.lock index e29f27a5..9bf81814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9557,6 +9557,16 @@ dependencies = [ "solana-pubkey", ] +[[package]] +name = "spl-associated-token-account-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6bbe0794e532ac08428d3abf5bf8ae75bd81dfddd785c388e326c00c92c6f5" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + [[package]] name = "spl-discriminator" version = "0.4.0" @@ -9785,6 +9795,7 @@ dependencies = [ "serde_json", "serde_with", "serial_test", + "solana-account", "solana-account-decoder", "solana-borsh 3.0.0", "solana-clap-v3-utils", @@ -9812,6 +9823,7 @@ dependencies = [ "solana-transaction", "solana-transaction-status", "solana-vote-program", + "spl-associated-token-account-interface", "spl-single-pool", "spl-token", "spl-token-client", diff --git a/README.md b/README.md index 9b8bba71..de035c69 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,51 @@ -## Solana Program Library Single-Validator Stake Pool +# Single-Validator Stake Pool -The single-validator stake pool program is an upcoming SPL program that enables liquid staking with zero fees, no counterparty, and 100% capital efficiency. +Fully permissionless liquid staking. -The program defines a canonical pool for every vote account, which can be initialized permissionlessly, and mints tokens in exchange for stake delegated to its designated validator. +| Information | Account Address | +| --- | --- | +| Single Pool | `SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE` | -The program is a stripped-down adaptation of the existing multi-validator stake pool program, with approximately 80% less code, to minimize execution risk. +## Overview -On launch, users will only be able to deposit and withdraw active stake, but pending future stake program development, we hope to support instant sol deposits and withdrawals as well. +The Single-Validator Stake Pool is an onchain program that enables liquid staking with zero fees, no counterparty, and 100% capital efficiency. The program defines a canonical pool for every vote account, which can be initialized permissionlessly, and mints tokens in exchange for stake delegated to its designated validator. + +The program also allows permissionless harvesting of Jito tips and other MEV rewards, turning liquid sol paid into the stake account into active stake earning rewards, functionally distributing these earnings to all LST holders just like protocol staking rewards. + +Users can only deposit and withdraw active stake, but liquid sol deposit is coming in a future update. + +## Security Audits + +The Single Pool Program has received three external audits: + +* Zellic (2024-01-02) + - Review commit hash [`ef44df9`](https://github.com/solana-program/single-pool/commit/ef44df985e76a697ee9a8aabb3a223610e4cf1dc) + - Final report +* Neodyme (2023-08-08) + - Review commit hash [`735d729`](https://github.com/solana-program/single-pool/commit/735d7292e35d35101750a4452d2647bdbf848e8b) + - Final report +* Zellic (2023-06-21) + - Review commit hash [`9dbdc3b`](https://github.com/solana-program/single-pool/commit/9dbdc3bdae31dda1dcb35346aab2d879deecf194) + - Final report + +## Building and Verifying + +To build the Single Pool Program, you can run `cargo-build-sbf` or use the Makefile +command: + +```console +cargo build-sbf --manifest-path program/Cargo.toml +make build-sbf-program +``` + +The BPF program deployed on all clusters is built with [solana-verify](https://solana.com/developers/guides/advanced/verified-builds). It may be verified independently by comparing the output of: + +```console +solana-verify get-program-hash -um SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE +``` + +with: + +```console +solana-verify build --library-name spl_single_pool +``` diff --git a/clients/cli/Cargo.toml b/clients/cli/Cargo.toml index 1401c3de..2c0a491e 100644 --- a/clients/cli/Cargo.toml +++ b/clients/cli/Cargo.toml @@ -17,6 +17,7 @@ serde = "1.0.219" serde_derive = "1.0.103" serde_json = "1.0.142" serde_with = "3.13.0" +solana-account = "2.2" solana-account-decoder = "2.3.4" solana-borsh = "3.0" solana-clap-v3-utils = "2.3.4" @@ -43,6 +44,7 @@ solana-stake-program = "2.2" solana-transaction = "2.2" solana-transaction-status = "2.3.4" solana-vote-program = "2.2" +spl-associated-token-account-interface = "1.0.0" spl-token = { version = "8.0", features = ["no-entrypoint"] } spl-token-client = { version = "0.16.1" } spl-single-pool = { version = "2.0.0", path = "../../program", features = [ diff --git a/clients/cli/src/main.rs b/clients/cli/src/main.rs index 380fd330..e0c00007 100644 --- a/clients/cli/src/main.rs +++ b/clients/cli/src/main.rs @@ -2,6 +2,7 @@ use { clap::{ArgMatches, CommandFactory, Parser}, + solana_account::Account, solana_borsh::v1::try_from_slice_unchecked, solana_clap_v3_utils::{input_parsers::Amount, keypair::signer_from_source}, solana_client::{ @@ -16,6 +17,7 @@ use { solana_stake_interface as stake, solana_transaction::Transaction, solana_vote_program::{self as vote_program, vote_state::VoteState}, + spl_associated_token_account_interface::instruction::create_associated_token_account, spl_single_pool::{ self, find_default_deposit_account_address, find_pool_address, find_pool_mint_address, find_pool_onramp_address, find_pool_stake_address, instruction::SinglePoolInstruction, @@ -110,29 +112,25 @@ async fn command_initialize(config: &Config, command_config: InitializeCli) -> C ); // check if the vote account is valid - let vote_account = config - .program_client - .get_account(vote_account_address) - .await?; - if vote_account.is_none() || vote_account.unwrap().owner != vote_program::id() { - return Err(format!("{} is not a valid vote account", vote_account_address,).into()); + match get_initialized_account(config, vote_account_address).await? { + Some(vote_account) + if vote_account.owner == vote_program::id() + && VoteState::deserialize(&vote_account.data).is_ok() => {} + _ => return Err(format!("{} is not a valid vote account", vote_account_address).into()), } let pool_address = find_pool_address(&spl_single_pool::id(), &vote_account_address); - // check if the pool has already been initialized - if config - .program_client - .get_account(pool_address) - .await? - .is_some() - { + // the pool must not already be initialized + // we do not use `pool_is_initialized()` because that function is restrictive + // so its negation would be permissive + let None = get_initialized_account(config, pool_address).await? else { return Err(format!( "Pool {} for vote account {} already exists", pool_address, vote_account_address ) .into()); - } + }; let mut instructions = spl_single_pool::instruction::initialize( &spl_single_pool::id(), @@ -168,6 +166,7 @@ async fn command_initialize(config: &Config, command_config: InitializeCli) -> C pool_address, vote_account_address, available_stake: 0, + excess_lamports: 0, token_supply: 0, signature, }, @@ -187,12 +186,7 @@ async fn command_replenish_pool(config: &Config, command_config: ReplenishCli) - format!("Replenishing stake accounts for pool {}\n", pool_address), ); - let vote_account_address = - if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - }; + let vote_account_address = get_vote_address_from_pool(config, pool_address).await?; let instruction = spl_single_pool::instruction::replenish_pool(&spl_single_pool::id(), &vote_account_address); @@ -308,14 +302,7 @@ async fn command_deposit( ), ); - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } + pool_is_initialized(config, pool_address).await?; let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address); let pool_stake_active = quarantine::get_stake_info(config, &pool_stake_address) @@ -339,31 +326,37 @@ async fn command_deposit( payer.clone(), ); - // use token account provided, or get/create the associated account for the - // client keypair + let mut instructions = vec![]; + + // use token account provided, or get/create the associated account for the client keypair let token_account_address = if let Some(account) = command_config.token_account_address { account } else { - token - .get_or_create_associated_account_info(&owner.pubkey()) - .await?; - token.get_associated_token_address(&owner.pubkey()) + let address = token.get_associated_token_address(&owner.pubkey()); + if get_initialized_account(config, address).await?.is_none() { + instructions.push(create_associated_token_account( + &payer.pubkey(), + &owner.pubkey(), + &pool_mint_address, + &spl_token::id(), + )); + } + address }; - let previous_token_amount = token - .get_account_info(&token_account_address) - .await? - .base - .amount; + let previous_token_amount = match token.get_account_info(&token_account_address).await { + Ok(account) => account.base.amount, + Err(_) => 0, + }; - let instructions = spl_single_pool::instruction::deposit( + instructions.extend(spl_single_pool::instruction::deposit( &spl_single_pool::id(), &pool_address, &stake_account_address, &token_account_address, &lamport_recipient, &stake_authority.pubkey(), - ); + )); let mut signers = vec![]; for signer in [payer.clone(), stake_authority] { @@ -380,12 +373,19 @@ async fn command_deposit( ); let signature = process_transaction(config, transaction).await?; - let token_amount = token - .get_account_info(&token_account_address) - .await? - .base - .amount - - previous_token_amount; + + let token_amount = if config.dry_run { + None + } else { + Some( + token + .get_account_info(&token_account_address) + .await? + .base + .amount + - previous_token_amount, + ) + }; Ok(format_output( config, @@ -429,14 +429,7 @@ async fn command_withdraw( command_config.vote_account_address, ); - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } + pool_is_initialized(config, pool_address).await?; // now all the mint and token info let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address); @@ -535,12 +528,15 @@ async fn command_withdraw( ); let signature = process_transaction(config, transaction).await?; - let stake_amount = if let Some((_, stake)) = + + let stake_amount = if config.dry_run { + None + } else if let Some((_, stake)) = quarantine::get_stake_info(config, &stake_account_address).await? { - stake.delegation.stake + Some(stake.delegation.stake) } else { - 0 + Some(0) }; Ok(format_output( @@ -577,14 +573,7 @@ async fn command_create_metadata( ), ); - if config - .program_client - .get_account(pool_address) - .await? - .is_none() - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } + pool_is_initialized(config, pool_address).await?; // and... i guess thats it? @@ -641,12 +630,7 @@ async fn command_update_metadata( ); // we always need the vote account - let vote_account_address = - if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - }; + let vote_account_address = get_vote_address_from_pool(config, pool_address).await?; if let Some(vote_account_data) = config .program_client @@ -718,25 +702,23 @@ async fn command_create_stake(config: &Config, command_config: CreateStakeCli) - format!("Creating default stake account for pool {}\n", pool_address), ); - let vote_account_address = - if let Some(vote_account_address) = command_config.vote_account_address { - vote_account_address - } else if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - try_from_slice_unchecked::(&pool_data.data)?.vote_account_address - } else { - return Err(format!( - "Cannot determine vote account address from uninitialized pool {}", - pool_address, - ) - .into()); - }; + let vote_account_address = if let Some(vote_account_address) = + command_config.vote_account_address + { + vote_account_address + } else if let Ok(vote_account_address) = get_vote_address_from_pool(config, pool_address).await + { + vote_account_address + } else { + return Err(format!( + "Cannot determine vote account address from provided pool address {}", + pool_address, + ) + .into()); + }; if command_config.vote_account_address.is_some() - && config - .program_client - .get_account(pool_address) - .await? - .is_none() + && pool_is_initialized(config, pool_address).await.is_err() { eprintln_display( config, @@ -777,7 +759,8 @@ async fn command_create_stake(config: &Config, command_config: CreateStakeCli) - // display stake pool(s) async fn command_display(config: &Config, command_config: DisplayCli) -> CommandResult { - if command_config.all { + let minimum_pool_balance = quarantine::get_minimum_pool_balance(config).await?; + let pool_and_vote_addresses = if command_config.all { // the filter isn't necessary now but makes the cli forward-compatible let pools = config .rpc_client @@ -793,28 +776,79 @@ async fn command_display(config: &Config, command_config: DisplayCli) -> Command ) .await?; - let mut displays = vec![]; - for pool in pools { + let mut pool_and_vote_addresses = vec![]; + for pool in pools.into_iter() { let vote_account_address = try_from_slice_unchecked::(&pool.1.data)?.vote_account_address; - displays.push(get_pool_display(config, pool.0, Some(vote_account_address)).await?); + pool_and_vote_addresses.push((pool.0, vote_account_address)); } - Ok(format_output( - config, - "DisplayAll".to_string(), - StakePoolListOutput(displays), - )) + pool_and_vote_addresses } else { let pool_address = pool_address_from_args( command_config.pool_address, command_config.vote_account_address, ); + vec![( + pool_address, + get_vote_address_from_pool(config, pool_address).await?, + )] + }; + + if pool_and_vote_addresses.len() > 100 { + return Err( + "Displaying more than 100 pools is not implemented; if you see \ + this error, feel free to open an issue in the SVSP repo." + .into(), + ); + } + + let stake_addresses = pool_and_vote_addresses + .iter() + .map(|(pool_address, _)| find_pool_stake_address(&spl_single_pool::id(), pool_address)) + .collect::>(); + + let available_balances = + quarantine::get_available_balances(config, &stake_addresses, minimum_pool_balance).await?; + + let mint_addresses = pool_and_vote_addresses + .iter() + .map(|(pool_address, _)| find_pool_mint_address(&spl_single_pool::id(), pool_address)) + .collect::>(); + + let token_supplies = quarantine::get_token_supplies(config, &mint_addresses).await?; + + let mut displays = vec![]; + for ( + ((pool_address, vote_account_address), (available_stake, excess_lamports)), + token_supply, + ) in pool_and_vote_addresses + .into_iter() + .zip(available_balances) + .zip(token_supplies) + { + displays.push(StakePoolOutput { + pool_address, + vote_account_address, + available_stake, + excess_lamports, + token_supply, + signature: None, + }); + } + + if command_config.all { + Ok(format_output( + config, + "DisplayAll".to_string(), + StakePoolListOutput(displays), + )) + } else { Ok(format_output( config, "Display".to_string(), - get_pool_display(config, pool_address, None).await?, + displays.remove(0), )) } } @@ -837,20 +871,11 @@ async fn command_create_onramp(config: &Config, command_config: CreateOnRampCli) ), ); - if config - .program_client - .get_account(pool_address) - .await? - .is_none_or(|account| account.data.is_empty()) - { - return Err(format!("Pool {} has not been initialized", pool_address).into()); - } + pool_is_initialized(config, pool_address).await?; - if config - .program_client - .get_account(onramp_address) + if get_initialized_account(config, onramp_address) .await? - .is_some_and(|account| !account.data.is_empty()) + .is_some() { return Err(format!( "Pool {} onramp {} already exists", @@ -882,50 +907,44 @@ async fn command_create_onramp(config: &Config, command_config: CreateOnRampCli) )) } -async fn get_pool_display( +async fn get_initialized_account( + config: &Config, + pubkey: Pubkey, +) -> Result, Error> { + Ok(config + .program_client + .get_account(pubkey) + .await? + .filter(|account| !account.data.is_empty())) +} + +async fn get_vote_address_from_pool( config: &Config, pool_address: Pubkey, - maybe_vote_account: Option, -) -> Result { - let vote_account_address = if let Some(address) = maybe_vote_account { - address - } else if let Some(pool_data) = config.program_client.get_account(pool_address).await? { - if let Ok(data) = try_from_slice_unchecked::(&pool_data.data) { - data.vote_account_address - } else { - return Err(format!( - "Failed to parse account at {}; is this a pool?", - pool_address - ) - .into()); - } - } else { - return Err(format!("Pool {} does not exist", pool_address).into()); +) -> Result { + let Some(pool_account) = get_initialized_account(config, pool_address).await? else { + return Err(format!("Pool {} has not been initialized", pool_address).into()); }; - let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address); - let available_stake = - if let Some((_, stake)) = quarantine::get_stake_info(config, &pool_stake_address).await? { - stake.delegation.stake - quarantine::get_minimum_pool_balance(config).await? - } else { - unreachable!() - }; + if pool_account.owner != spl_single_pool::id() { + return Err(format!("{} is not owned by the SVSP program", pool_address).into()); + } - let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address); - let token_supply = config - .rpc_client - .get_token_supply(&pool_mint_address) - .await? - .amount - .parse::()?; + if let Ok(pool) = try_from_slice_unchecked::(&pool_account.data) { + Ok(pool.vote_account_address) + } else { + Err(format!( + "{} is owned by the SVSP program but not a valid pool account", + pool_address + ) + .into()) + } +} - Ok(StakePoolOutput { - pool_address, - vote_account_address, - available_stake, - token_supply, - signature: None, - }) +async fn pool_is_initialized(config: &Config, pool_address: Pubkey) -> Result<(), Error> { + get_vote_address_from_pool(config, pool_address) + .await + .map(|_| ()) } async fn process_transaction( diff --git a/clients/cli/src/output.rs b/clients/cli/src/output.rs index c1b58f04..2daa2489 100644 --- a/clients/cli/src/output.rs +++ b/clients/cli/src/output.rs @@ -94,6 +94,7 @@ pub struct StakePoolOutput { #[serde_as(as = "DisplayFromStr")] pub vote_account_address: Pubkey, pub available_stake: u64, + pub excess_lamports: u64, pub token_supply: u64, #[serde_as(as = "Option")] pub signature: Option, @@ -146,6 +147,7 @@ impl VerboseDisplay for StakePoolOutput { )?; writeln_name_value(w, " Available stake:", &self.available_stake.to_string())?; + writeln_name_value(w, " Excess lamports:", &self.excess_lamports.to_string())?; writeln_name_value(w, " Token supply:", &self.token_supply.to_string())?; if let Some(signature) = self.signature { @@ -219,7 +221,7 @@ impl Display for StakePoolListOutput { pub struct DepositOutput { #[serde_as(as = "DisplayFromStr")] pub pool_address: Pubkey, - pub token_amount: u64, + pub token_amount: Option, #[serde_as(as = "Option")] pub signature: Option, } @@ -231,7 +233,13 @@ impl Display for DepositOutput { fn fmt(&self, f: &mut Formatter<'_>) -> Result { writeln!(f)?; writeln_name_value(f, "Pool address:", &self.pool_address.to_string())?; - writeln_name_value(f, "Token amount:", &self.token_amount.to_string())?; + + let token_amount = if let Some(amount) = self.token_amount { + &amount.to_string() + } else { + "(cannot display in simulation)" + }; + writeln_name_value(f, "Token amount:", token_amount)?; if let Some(signature) = self.signature { writeln!(f)?; @@ -250,7 +258,7 @@ pub struct WithdrawOutput { pub pool_address: Pubkey, #[serde_as(as = "DisplayFromStr")] pub stake_account_address: Pubkey, - pub stake_amount: u64, + pub stake_amount: Option, #[serde_as(as = "Option")] pub signature: Option, } @@ -267,7 +275,13 @@ impl Display for WithdrawOutput { "Stake account address:", &self.stake_account_address.to_string(), )?; - writeln_name_value(f, "Stake amount:", &self.stake_amount.to_string())?; + + let stake_amount = if let Some(amount) = self.stake_amount { + &amount.to_string() + } else { + "(cannot display in simulation)" + }; + writeln_name_value(f, "Stake amount:", stake_amount)?; if let Some(signature) = self.signature { writeln!(f)?; diff --git a/clients/cli/src/quarantine.rs b/clients/cli/src/quarantine.rs index eab0538b..5fa43427 100644 --- a/clients/cli/src/quarantine.rs +++ b/clients/cli/src/quarantine.rs @@ -13,6 +13,7 @@ use { }, solana_system_interface::instruction as system_instruction, solana_sysvar as sysvar, + spl_token::{solana_program::program_pack::Pack, state::Mint}, }; pub async fn get_rent(config: &Config) -> Result { @@ -57,6 +58,66 @@ pub async fn get_stake_info( } } +pub async fn get_available_balances( + config: &Config, + stake_account_addresses: &[Pubkey], + minimum_pool_balance: u64, +) -> Result, Error> { + let stake_accounts = config + .rpc_client + .get_multiple_accounts(stake_account_addresses) + .await?; + + let mut delegations = vec![]; + for stake_account in &stake_accounts { + let delegation = if let Some(account) = stake_account { + match bincode::deserialize::(&account.data) { + Ok(StakeStateV2::Stake(meta, stake, _)) => ( + stake.delegation.stake.saturating_sub(minimum_pool_balance), + account + .lamports + .saturating_sub(stake.delegation.stake) + .saturating_sub(meta.rent_exempt_reserve), + ), + Ok(StakeStateV2::Initialized(meta)) => { + (0, account.lamports.saturating_sub(meta.rent_exempt_reserve)) + } + _ => unreachable!(), + } + } else { + (0, 0) + }; + delegations.push(delegation); + } + + Ok(delegations) +} + +pub async fn get_token_supplies( + config: &Config, + mint_addresses: &[Pubkey], +) -> Result, Error> { + let mint_accounts = config + .rpc_client + .get_multiple_accounts(mint_addresses) + .await?; + + let mut supplies = vec![]; + for mint_account in &mint_accounts { + let supply = if let Some(account) = mint_account { + match Mint::unpack(&account.data) { + Ok(mint) => mint.supply, + _ => 0, + } + } else { + 0 + }; + supplies.push(supply); + } + + Ok(supplies) +} + pub async fn create_uninitialized_stake_account_instruction( config: &Config, payer: &Pubkey, diff --git a/clients/cli/tests/test.rs b/clients/cli/tests/test.rs index 6a6f4832..9c0104e8 100644 --- a/clients/cli/tests/test.rs +++ b/clients/cli/tests/test.rs @@ -6,6 +6,7 @@ use { solana_cli_config::Config as SolanaConfig, solana_client::nonblocking::rpc_client::RpcClient, solana_clock::Epoch, + solana_commitment_config::CommitmentConfig, solana_epoch_schedule::{EpochSchedule, MINIMUM_SLOTS_PER_EPOCH}, solana_keypair::{write_keypair_file, Keypair}, solana_native_token::LAMPORTS_PER_SOL, @@ -26,20 +27,17 @@ use { id, instruction::{self as ixn, SinglePoolInstruction}, }, - spl_token_client::client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction}, std::{path::PathBuf, process::Command, str::FromStr, sync::Arc, time::Duration}, tempfile::NamedTempFile, test_case::test_case, tokio::time::sleep, }; -type PClient = Arc>; const SVSP_CLI: &str = "../../target/debug/spl-single-pool"; #[allow(dead_code)] pub struct Env { pub rpc_client: Arc, - pub program_client: PClient, pub payer: Keypair, pub keypair_file_path: String, pub config_file_path: String, @@ -55,11 +53,10 @@ async fn setup(raise_minimum_delegation: bool, initialize_pool: bool) -> Env { // start test validator let (validator, payer) = start_validator(raise_minimum_delegation).await; - // make clients - let rpc_client = Arc::new(validator.get_async_rpc_client()); - let program_client: PClient = Arc::new(ProgramRpcClient::new( - rpc_client.clone(), - ProgramRpcClientSendTransaction, + // make client + let rpc_client = Arc::new(RpcClient::new_with_commitment( + validator.rpc_url(), + CommitmentConfig::confirmed(), )); // write the payer to disk @@ -78,24 +75,14 @@ async fn setup(raise_minimum_delegation: bool, initialize_pool: bool) -> Env { solana_config.save(config_file_path).unwrap(); // make vote and stake accounts - let vote_account = create_vote_account(&program_client, &payer, &payer.pubkey()).await; - if initialize_pool { - let status = Command::new(SVSP_CLI) - .args([ - "manage", - "initialize", - "-C", - config_file_path, - &vote_account.to_string(), - ]) - .status() - .unwrap(); - assert!(status.success()); - } + let vote_account = if initialize_pool { + create_pool(&rpc_client, &payer, config_file_path).await + } else { + create_vote_account(&rpc_client, &payer, &payer.pubkey()).await + }; Env { rpc_client, - program_client, payer, keypair_file_path: keypair_file.path().to_str().unwrap().to_string(), config_file_path: config_file_path.to_string(), @@ -142,7 +129,7 @@ async fn wait_for_next_epoch(rpc_client: &RpcClient) -> Epoch { println!("current epoch {}, advancing to next...", current_epoch); loop { let epoch_info = rpc_client.get_epoch_info().await.unwrap(); - if epoch_info.epoch > current_epoch { + if epoch_info.epoch > current_epoch && epoch_info.slot_index > 0 { return epoch_info.epoch; } @@ -151,7 +138,7 @@ async fn wait_for_next_epoch(rpc_client: &RpcClient) -> Epoch { } async fn create_vote_account( - program_client: &PClient, + rpc_client: &RpcClient, payer: &Keypair, withdrawer: &Pubkey, ) -> Pubkey { @@ -159,17 +146,17 @@ async fn create_vote_account( let vote_account = Keypair::new(); let voter = Keypair::new(); - let zero_rent = program_client + let zero_rent = rpc_client .get_minimum_balance_for_rent_exemption(0) .await .unwrap(); - let vote_rent = program_client + let vote_rent = rpc_client .get_minimum_balance_for_rent_exemption(VoteState::size_of() * 2) .await .unwrap(); - let blockhash = program_client.get_latest_blockhash().await.unwrap(); + let blockhash = rpc_client.get_latest_blockhash().await.unwrap(); let mut instructions = vec![system_instruction::create_account( &payer.pubkey(), @@ -203,23 +190,43 @@ async fn create_vote_account( .try_partial_sign(&vec![&validator, &vote_account], blockhash) .unwrap(); - program_client.send_transaction(&transaction).await.unwrap(); + rpc_client + .send_and_confirm_transaction(&transaction) + .await + .unwrap(); vote_account.pubkey() } +async fn create_pool(rpc_client: &RpcClient, payer: &Keypair, config_file_path: &str) -> Pubkey { + let vote_account = create_vote_account(rpc_client, payer, &payer.pubkey()).await; + let status = Command::new(SVSP_CLI) + .args([ + "manage", + "initialize", + "-C", + config_file_path, + &vote_account.to_string(), + ]) + .status() + .unwrap(); + assert!(status.success()); + + vote_account +} + async fn create_and_delegate_stake_account( - program_client: &PClient, + rpc_client: &RpcClient, payer: &Keypair, vote_account: &Pubkey, ) -> Pubkey { let stake_account = Keypair::new(); - let stake_rent = program_client + let stake_rent = rpc_client .get_minimum_balance_for_rent_exemption(StakeStateV2::size_of()) .await .unwrap(); - let blockhash = program_client.get_latest_blockhash().await.unwrap(); + let blockhash = rpc_client.get_latest_blockhash().await.unwrap(); let mut transaction = Transaction::new_with_payer( &stake_instruction::create_account( @@ -239,7 +246,10 @@ async fn create_and_delegate_stake_account( .try_partial_sign(&vec![&stake_account], blockhash) .unwrap(); - program_client.send_transaction(&transaction).await.unwrap(); + rpc_client + .send_and_confirm_transaction(&transaction) + .await + .unwrap(); let mut transaction = Transaction::new_with_payer( &[stake_instruction::delegate_stake( @@ -252,7 +262,10 @@ async fn create_and_delegate_stake_account( transaction.sign(&vec![payer], blockhash); - program_client.send_transaction(&transaction).await.unwrap(); + rpc_client + .send_and_confirm_transaction(&transaction) + .await + .unwrap(); stake_account.pubkey() } @@ -303,7 +316,7 @@ async fn deposit(raise_minimum_delegation: bool, use_default: bool) { Pubkey::default() } else { - create_and_delegate_stake_account(&env.program_client, &env.payer, &env.vote_account).await + create_and_delegate_stake_account(&env.rpc_client, &env.payer, &env.vote_account).await }; wait_for_next_epoch(&env.rpc_client).await; @@ -335,7 +348,7 @@ async fn deposit(raise_minimum_delegation: bool, use_default: bool) { async fn withdraw(raise_minimum_delegation: bool) { let env = setup(raise_minimum_delegation, true).await; let stake_account = - create_and_delegate_stake_account(&env.program_client, &env.payer, &env.vote_account).await; + create_and_delegate_stake_account(&env.rpc_client, &env.payer, &env.vote_account).await; wait_for_next_epoch(&env.rpc_client).await; @@ -470,6 +483,47 @@ async fn display() { assert!(status.success()); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial] +async fn display_all() { + let env = setup(false, true).await; + + create_pool(&env.rpc_client, &env.payer, &env.config_file_path).await; + create_pool(&env.rpc_client, &env.payer, &env.config_file_path).await; + + let output = Command::new(SVSP_CLI) + .args(["display", "-C", &env.config_file_path, "--all"]) + .output() + .unwrap(); + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let pools = stdout + .lines() + .filter(|line| line.starts_with(" Pool address:")) + .count(); + assert_eq!(pools, 3); + + let output = Command::new(SVSP_CLI) + .args(["display", "-C", &env.config_file_path, "--all", "--verbose"]) + .output() + .unwrap(); + assert!(output.status.success()); + + let stdout = String::from_utf8_lossy(&output.stdout); + let pools = stdout + .lines() + .filter(|line| line.starts_with(" Pool address:")) + .count(); + assert_eq!(pools, 3); + + let stakes = stdout + .lines() + .filter(|line| line.starts_with(" Pool main stake account address:")) + .count(); + assert_eq!(stakes, 3); +} + #[test_case(false; "one_lamp")] #[test_case(true; "one_sol")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -489,15 +543,15 @@ async fn create_onramp(raise_minimum_delegation: bool) { .filter(|instruction| instruction.data != onramp_opcode) .collect::>(); - let blockhash = env.program_client.get_latest_blockhash().await.unwrap(); + let blockhash = env.rpc_client.get_latest_blockhash().await.unwrap(); let transaction = Transaction::new_signed_with_payer( &instructions, Some(&env.payer.pubkey()), &[&env.payer], blockhash, ); - env.program_client - .send_transaction(&transaction) + env.rpc_client + .send_and_confirm_transaction(&transaction) .await .unwrap();