Skip to content

Commit f6464c9

Browse files
committed
cli
1 parent 3d45f59 commit f6464c9

4 files changed

Lines changed: 247 additions & 3 deletions

File tree

clients/cli/src/cli.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ pub enum Command {
8686

8787
/// Display info for one or all single-validator stake pool(s)
8888
Display(DisplayCli),
89+
90+
/// Deposit liquid sol into a pool in exchange for pool tokens, less a one percent
91+
/// fee.
92+
DepositSol(DepositSolCli),
8993
}
9094

9195
#[derive(Clone, Debug, Parser)]
@@ -305,6 +309,30 @@ pub struct CreateOnRampCli {
305309
pub vote_account_address: Option<Pubkey>,
306310
}
307311

312+
#[derive(Clone, Debug, Args)]
313+
#[clap(group(pool_source_group()))]
314+
pub struct DepositSolCli {
315+
/// Lamports to deposit into pool
316+
pub lamports: u64,
317+
318+
/// The pool to withdraw from
319+
#[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))]
320+
pub pool_address: Option<Pubkey>,
321+
322+
/// The vote account corresponding to the pool to withdraw from
323+
#[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))]
324+
pub vote_account_address: Option<Pubkey>,
325+
326+
/// The wallet to deposit lamports from. Defaults to the client
327+
/// keypair
328+
#[clap(long, id = "DEPOSIT_SOURCE_KEYPAIR", value_parser = SignerSourceParserBuilder::default().allow_all().build())]
329+
pub from: Option<SignerSource>,
330+
331+
/// The token account to mint to. Defaults to the client keypair's
332+
/// associated token account
333+
#[clap(long = "token-account", value_parser = |p: &str| parse_address(p, "token_account_address"))]
334+
pub token_account_address: Option<Pubkey>,
335+
}
308336
fn pool_source_group() -> ArgGroup<'static> {
309337
ArgGroup::new("pool-source")
310338
.required(true)
@@ -358,3 +386,12 @@ pub fn pool_address_from_args(maybe_pool: Option<Pubkey>, maybe_vote: Option<Pub
358386
unreachable!()
359387
}
360388
}
389+
390+
#[cfg(test)]
391+
mod tests {
392+
// if this test fails, we changed the fee. fix the comment on Command::DepositSol
393+
#[test]
394+
fn test_deposit_sol_fee() {
395+
assert_eq!(spl_single_pool::DEPOSIT_SOL_FEE_BPS, 100);
396+
}
397+
}

clients/cli/src/main.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ impl Command {
9191
command_withdraw(config, command_config, matches, wallet_manager).await
9292
}
9393
Command::Display(command_config) => command_display(config, command_config).await,
94+
Command::DepositSol(command_config) => {
95+
command_deposit_sol(config, command_config, matches, wallet_manager).await
96+
}
9497
}
9598
}
9699
}
@@ -822,6 +825,189 @@ async fn command_create_onramp(config: &Config, command_config: CreateOnRampCli)
822825
))
823826
}
824827

828+
// deposit liquid sol
829+
async fn command_deposit_sol(
830+
config: &Config,
831+
command_config: DepositSolCli,
832+
matches: &ArgMatches,
833+
wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
834+
) -> CommandResult {
835+
let payer = config.fee_payer()?;
836+
let owner = config.default_signer()?;
837+
838+
let deposit_source = command_config
839+
.from
840+
.and_then(|source| {
841+
signer_from_source(matches, &source, "from", wallet_manager)
842+
.ok()
843+
.map(Arc::from)
844+
})
845+
.unwrap_or(owner.clone());
846+
847+
let deposit_amount = command_config.lamports;
848+
849+
let pool_address = pool_address_from_args(
850+
command_config.pool_address,
851+
command_config.vote_account_address,
852+
);
853+
854+
let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address);
855+
let onramp_address = find_pool_onramp_address(&spl_single_pool::id(), &pool_address);
856+
let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address);
857+
858+
pool_is_initialized(config, pool_address).await?;
859+
860+
let vote_account_address = get_vote_address_from_pool(config, pool_address).await?;
861+
862+
if get_initialized_account(config, onramp_address)
863+
.await?
864+
.is_none()
865+
{
866+
return Err(format!(
867+
"Pool {} onramp {} does not exist",
868+
pool_address, onramp_address
869+
)
870+
.into());
871+
}
872+
873+
let current_epoch = config.rpc_client.get_epoch_info().await?.epoch;
874+
875+
if let Some((_, stake)) = quarantine::get_stake_info(config, &pool_stake_address).await? {
876+
if stake.delegation.activation_epoch >= current_epoch {
877+
return Err(format!(
878+
"Pool {} stake {} is still activating; must be fully active",
879+
pool_address, pool_stake_address
880+
)
881+
.into());
882+
}
883+
884+
if stake.delegation.deactivation_epoch < u64::MAX {
885+
return Err(format!(
886+
"Pool {} stake {} is deactivating or deactivated",
887+
pool_address, pool_stake_address
888+
)
889+
.into());
890+
}
891+
} else {
892+
// pool existence already validated and pool exists => stake exists
893+
unreachable!();
894+
};
895+
896+
{
897+
let deposit_source_balance = config
898+
.program_client
899+
.get_account(deposit_source.pubkey())
900+
.await?
901+
.map(|account| account.lamports)
902+
.unwrap_or(0);
903+
904+
if deposit_source_balance < deposit_amount {
905+
return Err(format!(
906+
"Insufficient lamports in {} for deposit: has {}, needs {}",
907+
deposit_source.pubkey(),
908+
deposit_source_balance,
909+
deposit_amount,
910+
)
911+
.into());
912+
}
913+
}
914+
915+
println_display(
916+
config,
917+
format!(
918+
"Depositing liquid sol from account {} into pool {}\n",
919+
deposit_source.pubkey(),
920+
pool_address
921+
),
922+
);
923+
924+
let token = Token::new(
925+
config.program_client.clone(),
926+
&spl_token::id(),
927+
&pool_mint_address,
928+
None,
929+
payer.clone(),
930+
);
931+
932+
let mut instructions = vec![];
933+
934+
// use token account provided, or get/create the associated account for the client keypair
935+
let token_account_address = if let Some(account) = command_config.token_account_address {
936+
account
937+
} else {
938+
let address = token.get_associated_token_address(&owner.pubkey());
939+
if get_initialized_account(config, address).await?.is_none() {
940+
instructions.push(create_associated_token_account(
941+
&payer.pubkey(),
942+
&owner.pubkey(),
943+
&pool_mint_address,
944+
&spl_token::id(),
945+
));
946+
}
947+
address
948+
};
949+
950+
let previous_token_amount = match token.get_account_info(&token_account_address).await {
951+
Ok(account) => account.base.amount,
952+
Err(_) => 0,
953+
};
954+
955+
// use escrow account for lamports to avoid exposing wallet signer to program
956+
let escrow_deposit_account = Keypair::new();
957+
958+
instructions.extend(spl_single_pool::instruction::deposit_liquid(
959+
&spl_single_pool::id(),
960+
&vote_account_address,
961+
&deposit_source.pubkey(),
962+
&escrow_deposit_account.pubkey(),
963+
&token_account_address,
964+
deposit_amount,
965+
));
966+
967+
let mut signers = vec![];
968+
for signer in [
969+
payer.clone(),
970+
deposit_source,
971+
Arc::new(escrow_deposit_account),
972+
] {
973+
if !signers.contains(&signer) {
974+
signers.push(signer);
975+
}
976+
}
977+
978+
let transaction = Transaction::new_signed_with_payer(
979+
&instructions,
980+
Some(&payer.pubkey()),
981+
&signers,
982+
config.program_client.get_latest_blockhash().await?,
983+
);
984+
985+
let signature = process_transaction(config, transaction).await?;
986+
987+
let token_amount = if config.dry_run {
988+
None
989+
} else {
990+
Some(
991+
token
992+
.get_account_info(&token_account_address)
993+
.await?
994+
.base
995+
.amount
996+
- previous_token_amount,
997+
)
998+
};
999+
1000+
Ok(format_output(
1001+
config,
1002+
"DepositSol".to_string(),
1003+
DepositOutput {
1004+
pool_address,
1005+
token_amount,
1006+
signature,
1007+
},
1008+
))
1009+
}
1010+
8251011
async fn get_initialized_account(
8261012
config: &Config,
8271013
pubkey: Pubkey,

clients/cli/tests/test.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,3 +543,23 @@ async fn create_onramp(raise_minimum_delegation: bool) {
543543
.unwrap();
544544
assert!(status.success());
545545
}
546+
547+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
548+
#[serial]
549+
async fn deposit_sol() {
550+
let env = setup(true, true).await;
551+
552+
wait_for_next_epoch(&env.rpc_client).await;
553+
554+
let args = vec![
555+
"deposit-sol".to_string(),
556+
"-C".to_string(),
557+
env.config_file_path,
558+
"--vote-account".to_string(),
559+
env.vote_account.to_string(),
560+
"100".to_string(),
561+
];
562+
563+
let status = Command::new(SVSP_CLI).args(&args).status().unwrap();
564+
assert!(status.success());
565+
}

program/src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ use {solana_native_token::LAMPORTS_PER_SOL, solana_pubkey::Pubkey};
1515

1616
solana_pubkey::declare_id!("SVSPxpvHdN29nkVg9rPapPNDddN5DipNLRUFhyjFThE");
1717

18+
/// Fee charged for the `DepositSol` instruction.
19+
pub const DEPOSIT_SOL_FEE_BPS: u64 = 100;
20+
const MAX_BPS: u64 = 10_000;
21+
1822
const POOL_PREFIX: &[u8] = b"pool";
1923
const POOL_STAKE_PREFIX: &[u8] = b"stake";
2024
const POOL_ONRAMP_PREFIX: &[u8] = b"onramp";
@@ -27,9 +31,6 @@ const PHANTOM_TOKEN_AMOUNT: u64 = LAMPORTS_PER_SOL;
2731
const MINT_DECIMALS: u8 = 9;
2832
const PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH: Option<u64> = Some(0);
2933

30-
const DEPOSIT_SOL_FEE_BPS: u64 = 100;
31-
const MAX_BPS: u64 = 10_000;
32-
3334
const VOTE_STATE_DISCRIMINATOR_END: usize = 4;
3435
const VOTE_STATE_AUTHORIZED_WITHDRAWER_START: usize = 36;
3536
const VOTE_STATE_AUTHORIZED_WITHDRAWER_END: usize = 68;

0 commit comments

Comments
 (0)