diff --git a/rust-sdk/whirlpool/src/decrease_liquidity.rs b/rust-sdk/whirlpool/src/decrease_liquidity.rs index 15a2c99f..c07d5998 100644 --- a/rust-sdk/whirlpool/src/decrease_liquidity.rs +++ b/rust-sdk/whirlpool/src/decrease_liquidity.rs @@ -18,6 +18,7 @@ use orca_whirlpools_core::{ get_tick_index_in_array, CollectFeesQuote, CollectRewardsQuote, DecreaseLiquidityQuote, }; use solana_client::nonblocking::rpc_client::RpcClient; +use solana_sdk::program_pack::Pack; use solana_sdk::{account::Account, instruction::Instruction, pubkey::Pubkey, signature::Keypair}; use spl_associated_token_account::get_associated_token_address_with_program_id; @@ -226,20 +227,20 @@ pub async fn decrease_liquidity_instructions( instructions.push( DecreaseLiquidityV2 { whirlpool: position.whirlpool, - token_program_a: mint_a_info.owner, - token_program_b: mint_b_info.owner, - memo_program: spl_memo::ID, position_authority: authority, position: position_address, position_token_account: position_token_account_address, - token_mint_a: pool.token_mint_a, - token_mint_b: pool.token_mint_b, token_owner_account_a: *token_owner_account_a, token_owner_account_b: *token_owner_account_b, token_vault_a: pool.token_vault_a, token_vault_b: pool.token_vault_b, + token_mint_a: pool.token_mint_a, + token_mint_b: pool.token_mint_b, + token_program_a: mint_a_info.owner, + token_program_b: mint_b_info.owner, tick_array_lower: lower_tick_array_address, tick_array_upper: upper_tick_array_address, + memo_program: spl_memo::ID, } .instruction(DecreaseLiquidityV2InstructionArgs { liquidity_amount: quote.liquidity_delta, @@ -516,20 +517,20 @@ pub async fn close_position_instructions( instructions.push( DecreaseLiquidityV2 { whirlpool: position.whirlpool, - token_program_a: mint_a_info.owner, - token_program_b: mint_b_info.owner, - memo_program: spl_memo::ID, position_authority: authority, position: position_address, position_token_account: position_token_account_address, - token_mint_a: pool.token_mint_a, - token_mint_b: pool.token_mint_b, token_owner_account_a: *token_owner_account_a, token_owner_account_b: *token_owner_account_b, token_vault_a: pool.token_vault_a, token_vault_b: pool.token_vault_b, + token_mint_a: pool.token_mint_a, + token_mint_b: pool.token_mint_b, + token_program_a: mint_a_info.owner, + token_program_b: mint_b_info.owner, tick_array_lower: lower_tick_array_address, tick_array_upper: upper_tick_array_address, + memo_program: spl_memo::ID, } .instruction(DecreaseLiquidityV2InstructionArgs { liquidity_amount: quote.liquidity_delta, @@ -581,8 +582,8 @@ pub async fn close_position_instructions( position: position_address, position_token_account: position_token_account_address, reward_owner_account: *reward_owner, - reward_vault: pool.reward_infos[i].vault, reward_mint: pool.reward_infos[i].mint, + reward_vault: pool.reward_infos[i].vault, reward_token_program: reward_info.owner, memo_program: spl_memo::ID, } @@ -635,3 +636,163 @@ pub async fn close_position_instructions( rewards_quote, }) } + +#[cfg(test)] +mod tests { + use super::*; + + // Helper functions + async fn fetch_token(rpc: &RpcClient, address: Pubkey) -> Result> { + let account = rpc.get_account(&address).await?; + TokenAccount::unpack(&account.data).map_err(|e| e.into()) + } + + async fn fetch_position(rpc: &RpcClient, address: Pubkey) -> Result> { + let account = rpc.get_account(&address).await?; + Position::from_bytes(&account.data).map_err(|e| e.into()) + } + + use crate::tests::{ + setup_ata_te, setup_ata_with_amount, setup_config_and_fee_tiers, setup_mint_te_fee, + setup_mint_with_decimals, setup_position, setup_te_position, setup_whirlpool, RpcContext, + SetupAtaConfig, + }; + use serial_test::serial; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::signature::{Keypair, Signer}; + use spl_associated_token_account::get_associated_token_address_with_program_id; + use spl_token::state::Account as TokenAccount; + use std::error::Error; + + const TEST_SLIPPAGE_TOLERANCE: u16 = 100; // 1% + + #[tokio::test] + #[serial] + async fn test_decrease_liquidity() -> Result<(), Box> { + let ctx = RpcContext::new().await; + + // Setup and initialize + setup_config_and_fee_tiers(&ctx).await?; + let mint_a_pubkey = setup_mint_with_decimals(&ctx, 9).await?; + let mint_b_pubkey = setup_mint_with_decimals(&ctx, 9).await?; + let token_balance: u64 = 1_000_000_000; + setup_ata_with_amount(&ctx, mint_a_pubkey, token_balance).await?; + setup_ata_with_amount(&ctx, mint_b_pubkey, token_balance).await?; + + // Setup pool and position + let tick_spacing = 64; + let pool_pubkey = + setup_whirlpool(&ctx, ctx.config, mint_a_pubkey, mint_b_pubkey, tick_spacing).await?; + + let position_mint = setup_position( + &ctx, + pool_pubkey, + Some((-128, 128)), + Some(ctx.signer.pubkey()), + ) + .await?; + + // Increase liquidity first to have liquidity to decrease + let increase_param = crate::increase_liquidity::IncreaseLiquidityParam::Liquidity(100_000); + let increase_ix = crate::increase_liquidity::increase_liquidity_instructions( + &ctx.rpc, + position_mint, + increase_param, + Some(TEST_SLIPPAGE_TOLERANCE), + Some(ctx.signer.pubkey()), + ) + .await?; + + ctx.send_transaction_with_signers(increase_ix.instructions, vec![]) + .await?; + + // Test Decrease Liquidity + let decrease_param = DecreaseLiquidityParam::Liquidity(50_000); + let decrease_ix = decrease_liquidity_instructions( + &ctx.rpc, + position_mint, + decrease_param, + Some(TEST_SLIPPAGE_TOLERANCE), + Some(ctx.signer.pubkey()), + ) + .await?; + + // Send transaction + ctx.send_transaction_with_signers(decrease_ix.instructions, vec![]) + .await?; + + // Fetch the updated position and check liquidity + let position_address = get_position_address(&position_mint)?.0; + let updated_position = fetch_position(&ctx.rpc, position_address).await?; + + assert_eq!(updated_position.liquidity, 50_000); + + Ok(()) + } + + // Additional test cases based on decreaseLiquidity.test.ts + + #[tokio::test] + #[serial] + async fn test_decrease_liquidity_to_zero() -> Result<(), Box> { + let ctx = RpcContext::new().await; + + // Setup and initialize + setup_config_and_fee_tiers(&ctx).await?; + let mint_a_pubkey = setup_mint_with_decimals(&ctx, 9).await?; + let mint_b_pubkey = setup_mint_with_decimals(&ctx, 9).await?; + let token_balance: u64 = 1_000_000_000; + setup_ata_with_amount(&ctx, mint_a_pubkey, token_balance).await?; + setup_ata_with_amount(&ctx, mint_b_pubkey, token_balance).await?; + + // Setup pool and position + let tick_spacing = 64; + let pool_pubkey = + setup_whirlpool(&ctx, ctx.config, mint_a_pubkey, mint_b_pubkey, tick_spacing).await?; + + let position_mint = setup_position( + &ctx, + pool_pubkey, + Some((-128, 128)), + Some(ctx.signer.pubkey()), + ) + .await?; + + // Increase liquidity first to have liquidity to decrease + let increase_param = crate::increase_liquidity::IncreaseLiquidityParam::Liquidity(100_000); + let increase_ix = crate::increase_liquidity::increase_liquidity_instructions( + &ctx.rpc, + position_mint, + increase_param, + Some(TEST_SLIPPAGE_TOLERANCE), + Some(ctx.signer.pubkey()), + ) + .await?; + + ctx.send_transaction_with_signers(increase_ix.instructions, vec![]) + .await?; + + // Decrease liquidity to zero + let decrease_param = DecreaseLiquidityParam::Liquidity(100_000); + let decrease_ix = decrease_liquidity_instructions( + &ctx.rpc, + position_mint, + decrease_param, + Some(TEST_SLIPPAGE_TOLERANCE), + Some(ctx.signer.pubkey()), + ) + .await?; + + // Send transaction + ctx.send_transaction_with_signers(decrease_ix.instructions, vec![]) + .await?; + + // Fetch the updated position and check liquidity + let position_address = get_position_address(&position_mint)?.0; + let updated_position = fetch_position(&ctx.rpc, position_address).await?; + + assert_eq!(updated_position.liquidity, 0); + + Ok(()) + } +} diff --git a/rust-sdk/whirlpool/src/tests/program.rs b/rust-sdk/whirlpool/src/tests/program.rs index aab9ea20..15f3cdee 100644 --- a/rust-sdk/whirlpool/src/tests/program.rs +++ b/rust-sdk/whirlpool/src/tests/program.rs @@ -17,9 +17,9 @@ use solana_sdk::{ system_instruction, system_program, }; use spl_associated_token_account::{ - get_associated_token_address, instruction::create_associated_token_account, + get_associated_token_address, get_associated_token_address_with_program_id, + instruction::create_associated_token_account, }; -use spl_token::instruction::initialize_mint2; use spl_token::ID as TOKEN_PROGRAM_ID; use spl_token_2022::{state::Mint as Token2022Mint, ID as TOKEN_2022_PROGRAM_ID}; use std::error::Error; @@ -43,31 +43,38 @@ pub async fn setup_whirlpool( ) -> Result> { let fee_tier = get_fee_tier_address(&config, tick_spacing)?.0; let whirlpool = get_whirlpool_address(&config, &token_a, &token_b, tick_spacing)?.0; - let token_badge_a = get_token_badge_address(&config, &token_a)?.0; - let token_badge_b = get_token_badge_address(&config, &token_b)?.0; + let (token_badge_a, _) = get_token_badge_address(&config, &token_a)?; + let (token_badge_b, _) = get_token_badge_address(&config, &token_b)?; let vault_a = ctx.get_next_keypair(); let vault_b = ctx.get_next_keypair(); - let mint_a_info = ctx.rpc.get_account(&token_a).await?; - let mint_b_info = ctx.rpc.get_account(&token_b).await?; + let mint_infos = ctx.rpc.get_multiple_accounts(&[token_a, token_b]).await?; + let mint_a_info = mint_infos[0] + .as_ref() + .ok_or("Token A mint info not found")?; + let mint_b_info = mint_infos[1] + .as_ref() + .ok_or("Token B mint info not found")?; + + let token_program_a = mint_a_info.owner; + let token_program_b = mint_b_info.owner; - // Default initial price of 1.0 let sqrt_price = tick_index_to_sqrt_price(0); let instructions = vec![InitializePoolV2 { - whirlpool, - fee_tier, + whirlpools_config: config, token_mint_a: token_a, token_mint_b: token_b, - whirlpools_config: config, + token_badge_a: token_badge_a, + token_badge_b: token_badge_b, funder: ctx.signer.pubkey(), + whirlpool, token_vault_a: vault_a.pubkey(), token_vault_b: vault_b.pubkey(), - token_badge_a, - token_badge_b, - token_program_a: mint_a_info.owner, - token_program_b: mint_b_info.owner, + fee_tier: fee_tier, + token_program_a: token_program_a, + token_program_b: token_program_b, system_program: system_program::id(), rent: RENT_PROGRAM_ID, } @@ -174,27 +181,34 @@ pub async fn setup_te_position( tick_range: Option<(i32, i32)>, owner: Option, ) -> Result> { + println!("Starting setup_te_position"); let owner = owner.unwrap_or_else(|| ctx.signer.pubkey()); let whirlpool_data = ctx.rpc.get_account(&whirlpool).await?; let whirlpool_account = Whirlpool::from_bytes(&whirlpool_data.data)?; // Get tick range - let (tick_lower, tick_upper) = tick_range.unwrap_or((-100, 100)); - - // Get initializable tick indexes - let lower_tick_index = - get_initializable_tick_index(tick_lower, whirlpool_account.tick_spacing, None); - let upper_tick_index = - get_initializable_tick_index(tick_upper, whirlpool_account.tick_spacing, None); + let (tick_lower, tick_upper) = tick_range.unwrap_or((-128, 128)); + println!("Tick range: lower={}, upper={}", tick_lower, tick_upper); + + // Tick Index를 Tick Spacing에 맞게 정렬 + let tick_spacing = whirlpool_account.tick_spacing as i32; + let tick_lower_aligned = (tick_lower / tick_spacing) * tick_spacing; + let tick_upper_aligned = (tick_upper / tick_spacing) * tick_spacing; + println!( + "Aligned ticks: lower={}, upper={}", + tick_lower_aligned, tick_upper_aligned + ); - // Initialize tick arrays if needed (재사용) + // Initialize tick arrays if needed let tick_arrays = [ - get_tick_array_start_tick_index(lower_tick_index, whirlpool_account.tick_spacing), - get_tick_array_start_tick_index(upper_tick_index, whirlpool_account.tick_spacing), + get_tick_array_start_tick_index(tick_lower_aligned, whirlpool_account.tick_spacing), + get_tick_array_start_tick_index(tick_upper_aligned, whirlpool_account.tick_spacing), ]; + println!("Tick array start indices: {:?}", tick_arrays); for start_tick in tick_arrays.iter() { let (tick_array_address, _) = get_tick_array_address(&whirlpool, *start_tick)?; + println!("Processing tick array at index {}", start_tick); let account_result = ctx.rpc.get_account(&tick_array_address).await; let needs_init = match account_result { @@ -203,6 +217,7 @@ pub async fn setup_te_position( }; if needs_init { + println!("Initializing tick array at index {}", start_tick); let init_tick_array_ix = InitializeTickArray { whirlpool, funder: ctx.signer.pubkey(), @@ -214,10 +229,14 @@ pub async fn setup_te_position( }); ctx.send_transaction(vec![init_tick_array_ix]).await?; + println!("Tick array initialized successfully"); + } else { + println!("Tick array already initialized"); } } // Create Token-2022 position + println!("Creating Token-2022 position"); let position_mint = Keypair::new(); let lamports = ctx .rpc @@ -240,8 +259,11 @@ pub async fn setup_te_position( 0, )?; - let position_token_account = - get_associated_token_address(&ctx.signer.pubkey(), &position_mint.pubkey()); + let position_token_account = get_associated_token_address_with_program_id( + &ctx.signer.pubkey(), + &position_mint.pubkey(), + &TOKEN_2022_PROGRAM_ID, + ); let create_ata_ix = create_associated_token_account( &ctx.signer.pubkey(), @@ -251,6 +273,7 @@ pub async fn setup_te_position( ); let (position_pubkey, position_bump) = get_position_address(&position_mint.pubkey())?; + println!("Position PDA: {}", position_pubkey); let open_position_ix = OpenPosition { funder: ctx.signer.pubkey(), @@ -265,8 +288,8 @@ pub async fn setup_te_position( rent: RENT_PROGRAM_ID, } .instruction(OpenPositionInstructionArgs { - tick_lower_index: lower_tick_index, - tick_upper_index: upper_tick_index, + tick_lower_index: tick_lower_aligned, + tick_upper_index: tick_upper_aligned, position_bump, }); @@ -281,6 +304,7 @@ pub async fn setup_te_position( vec![&position_mint], ) .await?; + println!("Transaction completed successfully"); Ok(position_pubkey) }