Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions clients/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ pub enum Command {

/// Display info for one or all single-validator stake pool(s)
Display(DisplayCli),

/// Deposit liquid sol into a pool in exchange for pool tokens, less a one percent
/// fee.
DepositSol(DepositSolCli),
}

#[derive(Clone, Debug, Parser)]
Expand Down Expand Up @@ -285,6 +289,30 @@ pub struct CreateOnRampCli {
pub vote_account_address: Option<Pubkey>,
}

#[derive(Clone, Debug, Args)]
#[clap(group(pool_source_group()))]
pub struct DepositSolCli {
/// Lamports to deposit into pool
pub lamports: u64,

/// The pool to deposit into
#[clap(short, long = "pool", value_parser = |p: &str| parse_address(p, "pool_address"))]
pub pool_address: Option<Pubkey>,

/// The vote account corresponding to the pool to deposit into
#[clap(long = "vote-account", value_parser = |p: &str| parse_address(p, "vote_account_address"))]
pub vote_account_address: Option<Pubkey>,

/// The wallet to deposit lamports from. Defaults to the client
/// keypair
#[clap(long, id = "DEPOSIT_SOURCE_KEYPAIR", value_parser = SignerSourceParserBuilder::default().allow_all().build())]
pub from: Option<SignerSource>,

/// The token account to mint to. Defaults to the client keypair's
/// associated token account
#[clap(long = "token-account", value_parser = |p: &str| parse_address(p, "token_account_address"))]
pub token_account_address: Option<Pubkey>,
}
fn pool_source_group() -> ArgGroup<'static> {
ArgGroup::new("pool-source")
.required(true)
Expand Down Expand Up @@ -338,3 +366,12 @@ pub fn pool_address_from_args(maybe_pool: Option<Pubkey>, maybe_vote: Option<Pub
unreachable!()
}
}

#[cfg(test)]
mod tests {
// if this test fails, we changed the fee. fix the comment on Command::DepositSol
#[test]
fn test_deposit_sol_fee() {
assert_eq!(spl_single_pool::DEPOSIT_SOL_FEE_BPS, 100);
}
}
187 changes: 187 additions & 0 deletions clients/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ impl Command {
command_withdraw(config, command_config, matches, wallet_manager).await
}
Command::Display(command_config) => command_display(config, command_config).await,
Command::DepositSol(command_config) => {
command_deposit_sol(config, command_config, matches, wallet_manager).await
}
}
}
}
Expand Down Expand Up @@ -822,6 +825,190 @@ async fn command_create_onramp(config: &Config, command_config: CreateOnRampCli)
))
}

// deposit liquid sol
async fn command_deposit_sol(
config: &Config,
command_config: DepositSolCli,
matches: &ArgMatches,
wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
) -> CommandResult {
let payer = config.fee_payer()?;
let owner = config.default_signer()?;

let deposit_source = command_config
.from
.and_then(|source| {
signer_from_source(matches, &source, "from", wallet_manager)
.ok()
.map(Arc::from)
})
.unwrap_or(owner.clone());

let deposit_amount = command_config.lamports;

let pool_address = pool_address_from_args(
command_config.pool_address,
command_config.vote_account_address,
);

let pool_stake_address = find_pool_stake_address(&spl_single_pool::id(), &pool_address);
let onramp_address = find_pool_onramp_address(&spl_single_pool::id(), &pool_address);
let pool_mint_address = find_pool_mint_address(&spl_single_pool::id(), &pool_address);

pool_is_initialized(config, pool_address).await?;

let vote_account_address = get_vote_address_from_pool(config, pool_address).await?;

if get_initialized_account(config, onramp_address)
.await?
.is_none()
{
return Err(format!(
"Pool {} onramp {} does not exist; run `spl-single-pool manage create-on-ramp ...` \
to create it",
pool_address, onramp_address
)
.into());
}

let current_epoch = config.rpc_client.get_epoch_info().await?.epoch;

if let Some((_, stake)) = quarantine::get_stake_info(config, &pool_stake_address).await? {
if stake.delegation.activation_epoch >= current_epoch {
return Err(format!(
"Pool {} stake {} is still activating; must be fully active",
pool_address, pool_stake_address
)
.into());
}

if stake.delegation.deactivation_epoch < u64::MAX {
return Err(format!(
"Pool {} stake {} is deactivating or deactivated",
pool_address, pool_stake_address
)
.into());
}
} else {
// pool existence already validated and pool exists => stake exists
unreachable!();
};

{
let deposit_source_balance = config
.program_client
.get_account(deposit_source.pubkey())
.await?
.map(|account| account.lamports)
.unwrap_or(0);

if deposit_source_balance < deposit_amount {
return Err(format!(
"Insufficient lamports in {} for deposit: has {}, needs {}",
deposit_source.pubkey(),
deposit_source_balance,
deposit_amount,
)
.into());
}
}

println_display(
config,
format!(
"Depositing liquid sol from account {} into pool {}\n",
deposit_source.pubkey(),
pool_address
),
);

let token = Token::new(
config.program_client.clone(),
&spl_token::id(),
&pool_mint_address,
None,
payer.clone(),
);

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 {
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 = match token.get_account_info(&token_account_address).await {
Ok(account) => account.base.amount,
Err(_) => 0,
};

// use escrow account for lamports to avoid exposing wallet signer to program
let escrow_deposit_account = Keypair::new();

instructions.extend(spl_single_pool::instruction::deposit_liquid(
&spl_single_pool::id(),
&vote_account_address,
&deposit_source.pubkey(),
&escrow_deposit_account.pubkey(),
&token_account_address,
deposit_amount,
));

let mut signers = vec![];
for signer in [
payer.clone(),
deposit_source,
Arc::new(escrow_deposit_account),
] {
if !signers.contains(&signer) {
signers.push(signer);
}
}

let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&signers,
config.program_client.get_latest_blockhash().await?,
);

let signature = process_transaction(config, transaction).await?;

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,
"DepositSol".to_string(),
DepositOutput {
pool_address,
token_amount,
signature,
},
))
}

async fn get_initialized_account(
config: &Config,
pubkey: Pubkey,
Expand Down
20 changes: 20 additions & 0 deletions clients/cli/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,23 @@ async fn create_onramp(raise_minimum_delegation: bool) {
.unwrap();
assert!(status.success());
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[serial]
async fn deposit_sol() {
let env = setup(true, true).await;

wait_for_next_epoch(&env.rpc_client).await;

let args = vec![
"deposit-sol".to_string(),
"-C".to_string(),
env.config_file_path,
"--vote-account".to_string(),
env.vote_account.to_string(),
"100".to_string(),
];

let status = Command::new(SVSP_CLI).args(&args).status().unwrap();
assert!(status.success());
}
15 changes: 15 additions & 0 deletions clients/js-legacy/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,19 @@ export class SinglePoolInstruction {
);
return modernInstructionToLegacy(instruction);
}

static async depositSol(
voteAccount: PublicKey,
userWallet: PublicKey,
userTokenAccount: PublicKey,
lamports: number | bigint,
): Promise<TransactionInstruction> {
const instruction = await PoolInstructionModern.depositSol(
voteAccount.toBase58() as VoteAccountAddress,
userWallet.toBase58() as Address,
userTokenAccount.toBase58() as Address,
BigInt(lamports),
);
return modernInstructionToLegacy(instruction);
}
}
15 changes: 15 additions & 0 deletions clients/js-legacy/src/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ interface WithdrawParams {
userTokenAuthority?: PublicKey;
}

interface DepositSolParams {
connection: Connection;
voteAccount: PublicKey;
userWallet: PublicKey;
lamports: number | bigint;
userTokenAccount?: PublicKey;
}

export class SinglePoolProgram {
static programId: PublicKey = new PublicKey(PoolProgramModern.programAddress);
static space: number = Number(PoolProgramModern.space);
Expand Down Expand Up @@ -105,4 +113,11 @@ export class SinglePoolProgram {

return modernTransactionToLegacy(modernTransaction);
}

static async depositSol(params: DepositSolParams) {
const modernParams = paramsToModern(params);
const modernTransaction = await PoolProgramModern.depositSol(modernParams);

return modernTransactionToLegacy(modernTransaction);
}
}
33 changes: 33 additions & 0 deletions clients/js-legacy/tests/transactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,3 +448,36 @@ test('get vote account address', async (t) => {
const chainVoteAccount = await getVoteAccountAddressForPool(connection, poolAddress);
t.true(chainVoteAccount.equals(voteAccountAddress), 'got correct vote account');
});

test('deposit sol', async (t) => {
const context = await startWithContext();
const svm = context.svm;
const payer = context.payer;
const connection = new LiteConnection(svm, payer);

const voteAccountAddress = new PublicKey(voteAccount.pubkey);
const poolAddress = await findPoolAddress(SinglePoolProgram.programId, voteAccountAddress);
const onrampAddress = await findPoolOnRampAddress(SinglePoolProgram.programId, poolAddress);

// initialize pool
let transaction = await SinglePoolProgram.initialize(
connection,
voteAccountAddress,
payer.publicKey,
);
await processTransaction(context, transaction);
context.advanceEpoch();

// deposit sol
transaction = await SinglePoolProgram.depositSol({
connection,
voteAccount: voteAccountAddress,
userWallet: payer.publicKey,
lamports: LAMPORTS_PER_SOL,
});
await processTransaction(context, transaction);

const stakeRent = await connection.getMinimumBalanceForRentExemption(StakeProgram.space);
const onrampAccount = svm.getAccount(onrampAddress);
t.is(onrampAccount.lamports, LAMPORTS_PER_SOL + stakeRent, 'sol has been deposited');
});
Loading
Loading