diff --git a/rust-sdk/whirlpool/src/position.rs b/rust-sdk/whirlpool/src/position.rs index 0e4c67517..8e12171b3 100644 --- a/rust-sdk/whirlpool/src/position.rs +++ b/rust-sdk/whirlpool/src/position.rs @@ -8,6 +8,7 @@ use orca_whirlpools_core::POSITION_BUNDLE_SIZE; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::account::Account; use solana_sdk::pubkey::Pubkey; +use solana_sdk::signer::Signer; use crate::{get_token_accounts_for_owner, ParsedTokenAccount}; @@ -295,3 +296,100 @@ pub async fn fetch_positions_in_whirlpool( let filters = vec![PositionFilter::Whirlpool(whirlpool)]; fetch_all_position_with_filter(rpc, filters).await } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{ + setup_ata_with_amount, setup_mint_with_decimals, setup_position, setup_position_bundle, + setup_te_position, setup_whirlpool, RpcContext, + }; + use serial_test::serial; + use solana_program_test::tokio; + use std::error::Error; + + const DEFAULT_TICK_RANGE: (i32, i32) = (-100, 100); + + #[tokio::test] + #[serial] + #[ignore = "Skipped until solana-bankrun supports gpa"] + async fn test_fetch_positions_for_owner_no_positions() -> Result<(), Box> { + let ctx = RpcContext::new().await; + let owner = ctx.signer.pubkey(); + let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?; + assert!( + positions.is_empty(), + "No positions should exist for a new owner" + ); + Ok(()) + } + + #[tokio::test] + #[serial] + #[ignore = "Skipped until solana-bankrun supports gpa"] + async fn test_fetch_positions_for_owner_with_position() -> Result<(), Box> { + let ctx = RpcContext::new().await; + let mint_a = setup_mint_with_decimals(&ctx, 9).await?; + let mint_b = setup_mint_with_decimals(&ctx, 9).await?; + setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?; + setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?; + + let whirlpool = setup_whirlpool(&ctx, mint_a, mint_b, 64).await?; + let normal_position_pubkey = setup_position(whirlpool).await?; + + // 1) Add a te_position (uses token-2022) + let te_position_pubkey = setup_te_position(whirlpool).await?; + + // 2) Add a position bundle, optionally with multiple bundled positions + let position_bundle_pubkey = setup_position_bundle(whirlpool, Some(vec![(), ()])).await?; + + let owner = ctx.signer.pubkey(); + let positions = fetch_positions_for_owner(&ctx.rpc, owner).await?; + + // Expect at least 3: normal, te_position, and a bundle + assert!( + positions.len() >= 3, + "Did not find all positions for the owner (expected normal, te_position, bundle)" + ); + + // Existing checks remain... + match &positions[0] { + PositionOrBundle::Position(pos) => { + assert_eq!(pos.address, normal_position_pubkey); + } + _ => panic!("Expected a single position, but found a bundle!"), + } + + Ok(()) + } + + #[tokio::test] + #[serial] + #[ignore = "Skipped until solana-bankrun supports gpa"] + async fn test_fetch_positions_in_whirlpool() -> Result<(), Box> { + let ctx = RpcContext::new().await; + let mint_a = setup_mint_with_decimals(&ctx, 9).await?; + let mint_b = setup_mint_with_decimals(&ctx, 9).await?; + setup_ata_with_amount(&ctx, mint_a, 1_000_000_000).await?; + setup_ata_with_amount(&ctx, mint_b, 1_000_000_000).await?; + + let whirlpool = setup_whirlpool(&ctx, mint_a, mint_b, 64).await?; + let _normal_position_pubkey = setup_position(whirlpool).await?; + + // 1) te_position + let _te_position_pubkey = setup_te_position(whirlpool).await?; + + // 2) position bundle + let _position_bundle_pubkey = setup_position_bundle(whirlpool, Some(vec![(), ()])).await?; + + let positions = fetch_positions_in_whirlpool(&ctx.rpc, whirlpool).await?; + + // Expect at least 3: normal + te_position + bundle + assert!( + positions.len() >= 3, + "Should find multiple positions in this whirlpool, including te_position & bundle" + ); + + Ok(()) + } +} diff --git a/rust-sdk/whirlpool/src/tests/program.rs b/rust-sdk/whirlpool/src/tests/program.rs index 81d735c44..a1b58d358 100644 --- a/rust-sdk/whirlpool/src/tests/program.rs +++ b/rust-sdk/whirlpool/src/tests/program.rs @@ -1,17 +1,32 @@ -use solana_sdk::{pubkey::Pubkey, signer::Signer, system_program}; -use std::error::Error; - use orca_whirlpools_client::{ - get_fee_tier_address, get_token_badge_address, get_whirlpool_address, InitializePoolV2, - InitializePoolV2InstructionArgs, + get_bundled_position_address, get_fee_tier_address, get_position_address, + get_position_bundle_address, get_token_badge_address, get_whirlpool_address, InitializePoolV2, + InitializePoolV2InstructionArgs, InitializePositionBundle, OpenBundledPosition, + OpenBundledPositionInstructionArgs, OpenPosition, OpenPositionInstructionArgs, }; use orca_whirlpools_core::tick_index_to_sqrt_price; +use solana_program::program_pack::Pack; use solana_program::sysvar::rent::ID as RENT_PROGRAM_ID; +use solana_sdk::{ + pubkey::Pubkey, + signer::{keypair::Keypair, Signer}, + system_instruction, system_program, +}; +use spl_associated_token_account::{ + get_associated_token_address, get_associated_token_address_with_program_id, + instruction::create_associated_token_account, +}; +use spl_token::{state::Mint, ID as TOKEN_PROGRAM_ID}; +use spl_token_2022::{state::Mint as Token2022Mint, ID as TOKEN_2022_PROGRAM_ID}; +use std::error::Error; use crate::WHIRLPOOLS_CONFIG_ADDRESS; use super::rpc::RpcContext; +use crate::tests::token::{setup_ata, setup_mint_with_decimals}; +use crate::tests::token_extensions::setup_mint_te; + pub async fn setup_whirlpool( ctx: &RpcContext, token_a: Pubkey, @@ -60,14 +75,137 @@ pub async fn setup_whirlpool( Ok(whirlpool) } -pub async fn setup_position(_whirlpool: Pubkey) -> Result> { - todo!() +pub async fn setup_position(whirlpool: Pubkey) -> Result> { + let ctx = RpcContext::new().await; + + let position_mint = ctx.get_next_keypair(); + + let (position_pubkey, position_bump) = get_position_address(&position_mint.pubkey())?; + + let position_token_account = get_associated_token_address_with_program_id( + &ctx.signer.pubkey(), + &position_mint.pubkey(), + &TOKEN_PROGRAM_ID, + ); + + let open_position_ix = OpenPosition { + funder: ctx.signer.pubkey(), + owner: ctx.signer.pubkey(), + position: position_pubkey, + position_mint: position_mint.pubkey(), + position_token_account, + whirlpool, + token_program: TOKEN_PROGRAM_ID, + system_program: system_program::id(), + associated_token_program: spl_associated_token_account::id(), + rent: RENT_PROGRAM_ID, + } + .instruction(OpenPositionInstructionArgs { + tick_lower_index: -128, + tick_upper_index: 128, + position_bump, + }); + + ctx.send_transaction_with_signers(vec![open_position_ix], vec![&position_mint]) + .await?; + + Ok(position_pubkey) } -pub async fn setup_te_position(_whirlpool: Pubkey) -> Result> { - todo!() +pub async fn setup_te_position(whirlpool: Pubkey) -> Result> { + let ctx = RpcContext::new().await; + + let te_position_mint = ctx.get_next_keypair(); + + let (position_pubkey, position_bump) = get_position_address(&te_position_mint.pubkey())?; + + let position_token_account = get_associated_token_address_with_program_id( + &ctx.signer.pubkey(), + &te_position_mint.pubkey(), + &TOKEN_2022_PROGRAM_ID, + ); + + let open_position_ix = OpenPosition { + funder: ctx.signer.pubkey(), + owner: ctx.signer.pubkey(), + position: position_pubkey, + position_mint: te_position_mint.pubkey(), + position_token_account: Pubkey::default(), + whirlpool, + token_program: TOKEN_2022_PROGRAM_ID, + system_program: system_program::id(), + associated_token_program: spl_associated_token_account::id(), + rent: RENT_PROGRAM_ID, + } + .instruction(OpenPositionInstructionArgs { + tick_lower_index: -128, + tick_upper_index: 128, + position_bump, + }); + + ctx.send_transaction_with_signers(vec![open_position_ix], vec![&te_position_mint]) + .await?; + + Ok(position_pubkey) } -pub async fn setup_position_bundle(_whirlpool: Pubkey) -> Result> { - todo!() +pub async fn setup_position_bundle( + whirlpool: Pubkey, + bundle_positions: Option>, +) -> Result> { + let ctx = RpcContext::new().await; + + let position_bundle_mint = ctx.get_next_keypair(); + let (position_bundle_address, _bundle_bump) = + get_position_bundle_address(&position_bundle_mint.pubkey())?; + + let position_token_account = get_associated_token_address_with_program_id( + &ctx.signer.pubkey(), + &position_bundle_mint.pubkey(), + &TOKEN_PROGRAM_ID, + ); + + let open_bundle_ix = InitializePositionBundle { + funder: ctx.signer.pubkey(), + position_bundle: position_bundle_address, + position_bundle_mint: position_bundle_mint.pubkey(), + position_bundle_token_account: position_token_account, + position_bundle_owner: ctx.signer.pubkey(), + token_program: TOKEN_PROGRAM_ID, + system_program: system_program::id(), + associated_token_program: spl_associated_token_account::id(), + rent: RENT_PROGRAM_ID, + } + .instruction(); + + ctx.send_transaction_with_signers(vec![open_bundle_ix], vec![&position_bundle_mint]) + .await?; + + if let Some(positions) = bundle_positions { + for (i, _) in positions.iter().enumerate() { + let bundle_index = i as u16; + let (bundled_position_address, _) = + get_bundled_position_address(&position_bundle_mint.pubkey(), bundle_index as u8)?; + + let open_bundled_ix = OpenBundledPosition { + funder: ctx.signer.pubkey(), + bundled_position: bundled_position_address, + position_bundle: position_bundle_address, + position_bundle_authority: ctx.signer.pubkey(), + position_bundle_token_account: Pubkey::default(), + whirlpool, + system_program: system_program::id(), + rent: RENT_PROGRAM_ID, + } + .instruction(OpenBundledPositionInstructionArgs { + tick_lower_index: -128, + tick_upper_index: 128, + bundle_index, + }); + + ctx.send_transaction(vec![open_bundled_ix]).await?; + } + } + + Ok(position_bundle_address) }