1use crate::{
12 token::{get_current_transfer_fee, prepare_token_accounts_instructions, TokenAccountStrategy},
13 FUNDER, SLIPPAGE_TOLERANCE_BPS,
14};
15use fusionamm_client::{get_position_address, get_tick_array_address, FusionPool, Position, TickArray};
16use fusionamm_client::{ClosePosition, CollectFees, CollectFeesInstructionArgs, DecreaseLiquidity, DecreaseLiquidityInstructionArgs};
17use fusionamm_core::{
18 collect_fees_quote, decrease_liquidity_quote, decrease_liquidity_quote_a, decrease_liquidity_quote_b, get_tick_array_start_tick_index,
19 get_tick_index_in_array, CollectFeesQuote, DecreaseLiquidityQuote,
20};
21use solana_client::nonblocking::rpc_client::RpcClient;
22use solana_instruction::Instruction;
23use solana_keypair::Keypair;
24use solana_pubkey::Pubkey;
25use spl_associated_token_account::get_associated_token_address_with_program_id;
26use std::{collections::HashSet, error::Error};
27#[derive(Debug, Clone)]
34pub enum DecreaseLiquidityParam {
35 TokenA(u64),
37 TokenB(u64),
39 Liquidity(u128),
41}
42
43#[derive(Debug)]
45pub struct DecreaseLiquidityInstruction {
46 pub quote: DecreaseLiquidityQuote,
51
52 pub instructions: Vec<Instruction>,
54
55 pub additional_signers: Vec<Keypair>,
57}
58
59#[cfg(not(doctest))]
60pub async fn decrease_liquidity_instructions(
117 rpc: &RpcClient,
118 position_mint_address: Pubkey,
119 param: DecreaseLiquidityParam,
120 slippage_tolerance_bps: Option<u16>,
121 authority: Option<Pubkey>,
122) -> Result<DecreaseLiquidityInstruction, Box<dyn Error>> {
123 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(*SLIPPAGE_TOLERANCE_BPS.try_lock()?);
124 let authority = authority.unwrap_or(*FUNDER.try_lock()?);
125 if authority == Pubkey::default() {
126 return Err("Authority must be provided".into());
127 }
128
129 let position_address = get_position_address(&position_mint_address)?.0;
130 let position_info = rpc.get_account(&position_address).await?;
131 let position = Position::from_bytes(&position_info.data)?;
132
133 let pool_info = rpc.get_account(&position.fusion_pool).await?;
134 let pool = FusionPool::from_bytes(&pool_info.data)?;
135
136 let mint_infos = rpc
137 .get_multiple_accounts(&[pool.token_mint_a, pool.token_mint_b, position_mint_address])
138 .await?;
139
140 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
141 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
142 let position_mint_info = mint_infos[2].as_ref().ok_or("Position mint info not found")?;
143
144 let current_epoch = rpc.get_epoch_info().await?.epoch;
145 let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), current_epoch);
146 let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), current_epoch);
147
148 let quote = match param {
149 DecreaseLiquidityParam::TokenA(amount) => decrease_liquidity_quote_a(
150 amount,
151 slippage_tolerance_bps,
152 pool.sqrt_price,
153 position.tick_lower_index,
154 position.tick_upper_index,
155 transfer_fee_a,
156 transfer_fee_b,
157 ),
158 DecreaseLiquidityParam::TokenB(amount) => decrease_liquidity_quote_b(
159 amount,
160 slippage_tolerance_bps,
161 pool.sqrt_price,
162 position.tick_lower_index,
163 position.tick_upper_index,
164 transfer_fee_a,
165 transfer_fee_b,
166 ),
167 DecreaseLiquidityParam::Liquidity(amount) => decrease_liquidity_quote(
168 amount,
169 slippage_tolerance_bps,
170 pool.sqrt_price,
171 position.tick_lower_index,
172 position.tick_upper_index,
173 transfer_fee_a,
174 transfer_fee_b,
175 ),
176 }?;
177
178 let mut instructions: Vec<Instruction> = Vec::new();
179
180 let lower_tick_array_start_index = get_tick_array_start_tick_index(position.tick_lower_index, pool.tick_spacing);
181 let upper_tick_array_start_index = get_tick_array_start_tick_index(position.tick_upper_index, pool.tick_spacing);
182
183 let position_token_account_address = get_associated_token_address_with_program_id(&authority, &position_mint_address, &position_mint_info.owner);
184 let lower_tick_array_address = get_tick_array_address(&position.fusion_pool, lower_tick_array_start_index)?.0;
185 let upper_tick_array_address = get_tick_array_address(&position.fusion_pool, upper_tick_array_start_index)?.0;
186
187 let token_accounts = prepare_token_accounts_instructions(
188 rpc,
189 authority,
190 vec![
191 TokenAccountStrategy::WithoutBalance(pool.token_mint_a),
192 TokenAccountStrategy::WithoutBalance(pool.token_mint_b),
193 ],
194 )
195 .await?;
196
197 instructions.extend(token_accounts.create_instructions);
198
199 let token_owner_account_a = token_accounts
200 .token_account_addresses
201 .get(&pool.token_mint_a)
202 .ok_or("Token A owner account not found")?;
203 let token_owner_account_b = token_accounts
204 .token_account_addresses
205 .get(&pool.token_mint_b)
206 .ok_or("Token B owner account not found")?;
207
208 instructions.push(
209 DecreaseLiquidity {
210 fusion_pool: position.fusion_pool,
211 token_program_a: mint_a_info.owner,
212 token_program_b: mint_b_info.owner,
213 memo_program: spl_memo::ID,
214 position_authority: authority,
215 position: position_address,
216 position_token_account: position_token_account_address,
217 token_mint_a: pool.token_mint_a,
218 token_mint_b: pool.token_mint_b,
219 token_owner_account_a: *token_owner_account_a,
220 token_owner_account_b: *token_owner_account_b,
221 token_vault_a: pool.token_vault_a,
222 token_vault_b: pool.token_vault_b,
223 tick_array_lower: lower_tick_array_address,
224 tick_array_upper: upper_tick_array_address,
225 }
226 .instruction(DecreaseLiquidityInstructionArgs {
227 liquidity_amount: quote.liquidity_delta,
228 token_min_a: quote.token_min_a,
229 token_min_b: quote.token_min_b,
230 remaining_accounts_info: None,
231 }),
232 );
233
234 instructions.extend(token_accounts.cleanup_instructions);
235
236 Ok(DecreaseLiquidityInstruction {
237 quote,
238 instructions,
239 additional_signers: token_accounts.additional_signers,
240 })
241}
242
243#[derive(Debug)]
248pub struct ClosePositionInstruction {
249 pub instructions: Vec<Instruction>,
251
252 pub additional_signers: Vec<Keypair>,
254
255 pub quote: DecreaseLiquidityQuote,
257
258 pub fees_quote: CollectFeesQuote,
262}
263
264#[cfg(not(doctest))]
265pub async fn close_position_instructions(
327 rpc: &RpcClient,
328 position_mint_address: Pubkey,
329 slippage_tolerance_bps: Option<u16>,
330 authority: Option<Pubkey>,
331) -> Result<ClosePositionInstruction, Box<dyn Error>> {
332 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(*SLIPPAGE_TOLERANCE_BPS.try_lock()?);
333 let authority = authority.unwrap_or(*FUNDER.try_lock()?);
334 if authority == Pubkey::default() {
335 return Err("Authority must be provided".into());
336 }
337
338 let position_address = get_position_address(&position_mint_address)?.0;
339 let position_info = rpc.get_account(&position_address).await?;
340 let position = Position::from_bytes(&position_info.data)?;
341
342 let pool_info = rpc.get_account(&position.fusion_pool).await?;
343 let pool = FusionPool::from_bytes(&pool_info.data)?;
344
345 let mint_infos = rpc
346 .get_multiple_accounts(&[pool.token_mint_a, pool.token_mint_b, position_mint_address])
347 .await?;
348
349 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
350 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
351 let position_mint_info = mint_infos[2].as_ref().ok_or("Position mint info not found")?;
352
353 let current_epoch = rpc.get_epoch_info().await?.epoch;
354 let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), current_epoch);
355 let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), current_epoch);
356
357 let quote = decrease_liquidity_quote(
358 position.liquidity,
359 slippage_tolerance_bps,
360 pool.sqrt_price,
361 position.tick_lower_index,
362 position.tick_upper_index,
363 transfer_fee_a,
364 transfer_fee_b,
365 )?;
366
367 let lower_tick_array_start_index = get_tick_array_start_tick_index(position.tick_lower_index, pool.tick_spacing);
368 let upper_tick_array_start_index = get_tick_array_start_tick_index(position.tick_upper_index, pool.tick_spacing);
369
370 let position_token_account_address = get_associated_token_address_with_program_id(&authority, &position_mint_address, &position_mint_info.owner);
371 let lower_tick_array_address = get_tick_array_address(&position.fusion_pool, lower_tick_array_start_index)?.0;
372 let upper_tick_array_address = get_tick_array_address(&position.fusion_pool, upper_tick_array_start_index)?.0;
373
374 let tick_array_infos = rpc.get_multiple_accounts(&[lower_tick_array_address, upper_tick_array_address]).await?;
375
376 let lower_tick_array_info = tick_array_infos[0].as_ref().ok_or("Lower tick array info not found")?;
377 let lower_tick_array = TickArray::from_bytes(&lower_tick_array_info.data)?;
378 let lower_tick =
379 &lower_tick_array.ticks[get_tick_index_in_array(position.tick_lower_index, lower_tick_array_start_index, pool.tick_spacing)? as usize];
380
381 let upper_tick_array_info = tick_array_infos[1].as_ref().ok_or("Upper tick array info not found")?;
382 let upper_tick_array = TickArray::from_bytes(&upper_tick_array_info.data)?;
383 let upper_tick =
384 &upper_tick_array.ticks[get_tick_index_in_array(position.tick_upper_index, upper_tick_array_start_index, pool.tick_spacing)? as usize];
385
386 let fees_quote = collect_fees_quote(
387 pool.clone().into(),
388 position.clone().into(),
389 lower_tick.clone().into(),
390 upper_tick.clone().into(),
391 transfer_fee_a,
392 transfer_fee_b,
393 )?;
394
395 let mut required_mints: HashSet<TokenAccountStrategy> = HashSet::new();
396
397 if quote.liquidity_delta > 0 || fees_quote.fee_owed_a > 0 || fees_quote.fee_owed_b > 0 {
398 required_mints.insert(TokenAccountStrategy::WithoutBalance(pool.token_mint_a));
399 required_mints.insert(TokenAccountStrategy::WithoutBalance(pool.token_mint_b));
400 }
401
402 let token_accounts = prepare_token_accounts_instructions(rpc, authority, required_mints.into_iter().collect()).await?;
403
404 let mut instructions: Vec<Instruction> = Vec::new();
405 instructions.extend(token_accounts.create_instructions);
406
407 let token_owner_account_a = token_accounts
408 .token_account_addresses
409 .get(&pool.token_mint_a)
410 .ok_or("Token A owner account not found")?;
411 let token_owner_account_b = token_accounts
412 .token_account_addresses
413 .get(&pool.token_mint_b)
414 .ok_or("Token B owner account not found")?;
415
416 if quote.liquidity_delta > 0 {
417 instructions.push(
418 DecreaseLiquidity {
419 fusion_pool: position.fusion_pool,
420 token_program_a: mint_a_info.owner,
421 token_program_b: mint_b_info.owner,
422 memo_program: spl_memo::ID,
423 position_authority: authority,
424 position: position_address,
425 position_token_account: position_token_account_address,
426 token_mint_a: pool.token_mint_a,
427 token_mint_b: pool.token_mint_b,
428 token_owner_account_a: *token_owner_account_a,
429 token_owner_account_b: *token_owner_account_b,
430 token_vault_a: pool.token_vault_a,
431 token_vault_b: pool.token_vault_b,
432 tick_array_lower: lower_tick_array_address,
433 tick_array_upper: upper_tick_array_address,
434 }
435 .instruction(DecreaseLiquidityInstructionArgs {
436 liquidity_amount: quote.liquidity_delta,
437 token_min_a: quote.token_min_a,
438 token_min_b: quote.token_min_b,
439 remaining_accounts_info: None,
440 }),
441 );
442 }
443
444 if fees_quote.fee_owed_a > 0 || fees_quote.fee_owed_b > 0 {
445 instructions.push(
446 CollectFees {
447 fusion_pool: position.fusion_pool,
448 position_authority: authority,
449 position: position_address,
450 position_token_account: position_token_account_address,
451 token_owner_account_a: *token_owner_account_a,
452 token_owner_account_b: *token_owner_account_b,
453 token_vault_a: pool.token_vault_a,
454 token_vault_b: pool.token_vault_b,
455 token_mint_a: pool.token_mint_a,
456 token_mint_b: pool.token_mint_b,
457 token_program_a: mint_a_info.owner,
458 token_program_b: mint_b_info.owner,
459 memo_program: spl_memo::ID,
460 }
461 .instruction(CollectFeesInstructionArgs {
462 remaining_accounts_info: None,
463 }),
464 );
465 }
466
467 match position_mint_info.owner {
468 spl_token_2022::ID => {
469 instructions.push(
470 ClosePosition {
471 position_authority: authority,
472 position: position_address,
473 position_token_account: position_token_account_address,
474 position_mint: position_mint_address,
475 receiver: authority,
476 token2022_program: spl_token_2022::ID,
477 }
478 .instruction(),
479 );
480 }
481 _ => {
482 return Err("Unsupported token program".into());
483 }
484 }
485
486 instructions.extend(token_accounts.cleanup_instructions);
487
488 Ok(ClosePositionInstruction {
489 instructions,
490 additional_signers: token_accounts.additional_signers,
491 quote,
492 fees_quote,
493 })
494}
495
496#[cfg(test)]
497mod tests {
498 use std::collections::HashMap;
499 use std::error::Error;
500
501 use rstest::rstest;
502 use serial_test::serial;
503 use solana_client::nonblocking::rpc_client::RpcClient;
504 use solana_keypair::Keypair;
505 use solana_program::program_pack::Pack;
506 use solana_program_test::tokio;
507 use solana_pubkey::Pubkey;
508 use solana_signer::Signer;
509 use spl_token::state::Account as TokenAccount;
510 use spl_token_2022::{extension::StateWithExtensionsOwned, state::Account as TokenAccount2022, ID as TOKEN_2022_PROGRAM_ID};
511
512 use crate::tests::setup_position;
513 use crate::{
514 close_position_instructions, decrease_liquidity_instructions, increase_liquidity_instructions, swap_instructions,
515 tests::{
516 setup_ata_te, setup_ata_with_amount, setup_fusion_pool, setup_mint_te, setup_mint_te_fee, setup_mint_with_decimals, RpcContext,
517 SetupAtaConfig,
518 },
519 DecreaseLiquidityParam, IncreaseLiquidityParam, SwapType,
520 };
521 use fusionamm_client::{get_position_address, Position};
522
523 async fn get_token_balance(rpc: &RpcClient, address: Pubkey) -> Result<u64, Box<dyn Error>> {
524 let account_data = rpc.get_account(&address).await?;
525 if account_data.owner == TOKEN_2022_PROGRAM_ID {
526 let parsed = StateWithExtensionsOwned::<TokenAccount2022>::unpack(account_data.data)?;
527 Ok(parsed.base.amount)
528 } else {
529 let parsed = TokenAccount::unpack(&account_data.data)?;
530 Ok(parsed.amount)
531 }
532 }
533
534 async fn maybe_fetch_position(rpc: &RpcClient, position_pubkey: Pubkey) -> Result<Option<Position>, Box<dyn Error>> {
535 match rpc.get_account(&position_pubkey).await {
536 Ok(acc) => {
537 let p = Position::from_bytes(&acc.data)?;
538 Ok(Some(p))
539 }
540 Err(_) => Ok(None),
541 }
542 }
543
544 async fn fetch_position(rpc: &RpcClient, position_pubkey: Pubkey) -> Result<Position, Box<dyn Error>> {
545 let account = rpc.get_account(&position_pubkey).await?;
546 Ok(Position::from_bytes(&account.data)?)
547 }
548
549 async fn verify_decrease_liquidity(
550 ctx: &RpcContext,
551 decrease_ix: &crate::DecreaseLiquidityInstruction,
552 token_a_account: Pubkey,
553 token_b_account: Pubkey,
554 position_mint: Pubkey,
555 ) -> Result<(), Box<dyn Error>> {
556 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
558 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
559
560 let signers: Vec<&Keypair> = decrease_ix.additional_signers.iter().collect();
562 ctx.send_transaction_with_signers(decrease_ix.instructions.clone(), signers).await?;
563
564 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
566 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
567 let gained_a = after_a.saturating_sub(before_a);
568 let gained_b = after_b.saturating_sub(before_b);
569
570 let quote = &decrease_ix.quote;
572 assert!(
573 gained_a >= quote.token_min_a && gained_a <= quote.token_est_a,
574 "Token A gain out of range: gained={}, expected={}..{}",
575 gained_a,
576 quote.token_min_a,
577 quote.token_est_a
578 );
579 assert!(
580 gained_b >= quote.token_min_b && gained_b <= quote.token_est_b,
581 "Token B gain out of range: gained={}, expected={}..{}",
582 gained_b,
583 quote.token_min_b,
584 quote.token_est_b
585 );
586
587 let position_pubkey = get_position_address(&position_mint)?.0;
589 let position_data = fetch_position(&ctx.rpc, position_pubkey).await?;
590 assert_eq!(
591 position_data.liquidity, quote.liquidity_delta,
592 "Position liquidity mismatch! expected={}, got={}",
593 quote.liquidity_delta, position_data.liquidity
594 );
595
596 Ok(())
597 }
598
599 async fn setup_all_mints(ctx: &RpcContext) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
600 let mint_a = setup_mint_with_decimals(ctx, 9).await?;
601 let mint_b = setup_mint_with_decimals(ctx, 9).await?;
602 let mint_te_a = setup_mint_te(ctx, &[]).await?;
603 let mint_te_b = setup_mint_te(ctx, &[]).await?;
604 let mint_tefee = setup_mint_te_fee(ctx).await?;
605
606 let mut out = HashMap::new();
607 out.insert("A", mint_a);
608 out.insert("B", mint_b);
609 out.insert("TEA", mint_te_a);
610 out.insert("TEB", mint_te_b);
611 out.insert("TEFee", mint_tefee);
612 Ok(out)
613 }
614
615 async fn setup_all_atas(ctx: &RpcContext, minted: &HashMap<&'static str, Pubkey>) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
616 let token_balance = 1_000_000;
617 let ata_a = setup_ata_with_amount(ctx, minted["A"], token_balance).await?;
618 let ata_b = setup_ata_with_amount(ctx, minted["B"], token_balance).await?;
619 let ata_te_a = setup_ata_te(ctx, minted["TEA"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
620 let ata_te_b = setup_ata_te(ctx, minted["TEB"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
621 let ata_tefee = setup_ata_te(ctx, minted["TEFee"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
622
623 let mut out = HashMap::new();
624 out.insert("A", ata_a);
625 out.insert("B", ata_b);
626 out.insert("TEA", ata_te_a);
627 out.insert("TEB", ata_te_b);
628 out.insert("TEFee", ata_tefee);
629 Ok(out)
630 }
631
632 fn parse_pool_name(pool_name: &str) -> (&'static str, &'static str) {
633 match pool_name {
634 "A-B" => ("A", "B"),
635 "A-TEA" => ("A", "TEA"),
636 "TEA-TEB" => ("TEA", "TEB"),
637 "A-TEFee" => ("A", "TEFee"),
638 _ => panic!("Unknown combo: {}", pool_name),
639 }
640 }
641
642 #[rstest]
643 #[case("A-B", "equally centered", -100, 100)]
644 #[case("A-B", "one sided A", -100, -1)]
645 #[case("A-B", "one sided B", 1, 100)]
646 #[case("A-TEA", "equally centered", -100, 100)]
647 #[case("A-TEA", "one sided A", -100, -1)]
648 #[case("A-TEA", "one sided B", 1, 100)]
649 #[case("TEA-TEB","equally centered", -100, 100)]
650 #[case("TEA-TEB","one sided A", -100, -1)]
651 #[case("TEA-TEB", "one sided B", 1, 100)]
652 #[case("A-TEFee","equally centered", -100, 100)]
653 #[case("A-TEFee","one sided A", -100, -1)]
654 #[case("A-TEFee", "one sided B", 1, 100)]
655 #[serial]
656 fn test_decrease_liquidity_cases(#[case] pool_name: &str, #[case] _position_name: &str, #[case] lower_tick: i32, #[case] upper_tick: i32) {
657 let rt = tokio::runtime::Runtime::new().unwrap();
658 rt.block_on(async {
659 let ctx = RpcContext::new().await;
660
661 let minted = setup_all_mints(&ctx).await.unwrap();
662 let user_atas = setup_all_atas(&ctx, &minted).await.unwrap();
663
664 let (mkey_a, mkey_b) = parse_pool_name(pool_name);
665 let pubkey_a = minted[mkey_a];
666 let pubkey_b = minted[mkey_b];
667
668 let swapped = pubkey_a > pubkey_b;
669 let (final_a, final_b) = if pubkey_a < pubkey_b {
670 (pubkey_a, pubkey_b)
671 } else {
672 (pubkey_b, pubkey_a)
673 };
674
675 let tick_spacing = 64;
676 let fee_rate = 300;
677 let pool_pubkey = setup_fusion_pool(&ctx, final_a, final_b, tick_spacing, fee_rate).await.unwrap();
678
679 let position_mint = setup_position(&ctx, pool_pubkey, Some((lower_tick, upper_tick)), None).await.unwrap();
680
681 let inc_ix = increase_liquidity_instructions(
682 &ctx.rpc,
683 position_mint,
684 IncreaseLiquidityParam::Liquidity(100_000),
685 Some(100),
686 Some(ctx.signer.pubkey()),
687 )
688 .await
689 .unwrap();
690 ctx.send_transaction_with_signers(inc_ix.instructions, vec![]).await.unwrap();
691
692 let dec_ix = decrease_liquidity_instructions(
693 &ctx.rpc,
694 position_mint,
695 DecreaseLiquidityParam::Liquidity(50_000),
696 Some(100),
697 Some(ctx.signer.pubkey()),
698 )
699 .await
700 .unwrap();
701
702 let user_ata_for_token_a = if swapped { user_atas[mkey_b] } else { user_atas[mkey_a] };
703 let user_ata_for_token_b = if swapped { user_atas[mkey_a] } else { user_atas[mkey_b] };
704
705 verify_decrease_liquidity(&ctx, &dec_ix, user_ata_for_token_a, user_ata_for_token_b, position_mint)
706 .await
707 .unwrap();
708 });
709 }
710
711 #[rstest]
712 #[case("A-B", "equally centered", -100, 100)]
713 #[case("A-B", "one sided A", -100, -1)]
714 #[case("A-TEA", "equally centered", -100, 100)]
715 #[case("A-TEA", "one sided A", -100, -1)]
716 #[case("TEA-TEB","equally centered", -100, 100)]
717 #[case("TEA-TEB","one sided A", -100, -1)]
718 #[case("A-TEFee","equally centered", -100, 100)]
719 #[case("A-TEFee","one sided A", -100, -1)]
720 #[tokio::test]
721 #[serial]
722 async fn test_close_position_cases(
723 #[case] pool_name: &str,
724 #[case] range_name: &str,
725 #[case] lower_tick: i32,
726 #[case] upper_tick: i32,
727 ) -> Result<(), Box<dyn Error>> {
728 let ctx = RpcContext::new().await;
729 let minted = setup_all_mints(&ctx).await?;
730 let user_atas = setup_all_atas(&ctx, &minted).await?;
731
732 let (mkey_a, mkey_b) = parse_pool_name(pool_name);
733 let pubkey_a = minted[mkey_a];
734 let pubkey_b = minted[mkey_b];
735 let swapped = pubkey_a > pubkey_b;
736 let (final_a, final_b) = if pubkey_a < pubkey_b {
737 (pubkey_a, pubkey_b)
738 } else {
739 (pubkey_b, pubkey_a)
740 };
741
742 let tick_spacing = 64;
743 let fee_rate = 300;
744 let pool_pubkey = setup_fusion_pool(&ctx, final_a, final_b, tick_spacing, fee_rate).await?;
745 let position_mint = setup_position(&ctx, pool_pubkey, Some((lower_tick, upper_tick)), None).await?;
746
747 let inc_ix = increase_liquidity_instructions(
748 &ctx.rpc,
749 position_mint,
750 IncreaseLiquidityParam::Liquidity(100_000),
751 Some(100),
752 Some(ctx.signer.pubkey()),
753 )
754 .await?;
755 ctx.send_transaction_with_signers(inc_ix.instructions, vec![]).await?;
756
757 let swap_ix = swap_instructions(&ctx.rpc, pool_pubkey, 100, final_a, SwapType::ExactIn, Some(100), Some(ctx.signer.pubkey())).await?;
758 ctx.send_transaction_with_signers(swap_ix.instructions, swap_ix.additional_signers.iter().collect())
759 .await?;
760
761 let before_a = get_token_balance(&ctx.rpc, if swapped { user_atas[mkey_b] } else { user_atas[mkey_a] }).await?;
762 let before_b = get_token_balance(&ctx.rpc, if swapped { user_atas[mkey_a] } else { user_atas[mkey_b] }).await?;
763
764 let close_ix = close_position_instructions(&ctx.rpc, position_mint, Some(100), Some(ctx.signer.pubkey())).await?;
765 let signers: Vec<&Keypair> = close_ix.additional_signers.iter().collect();
766 ctx.send_transaction_with_signers(close_ix.instructions.clone(), signers).await?;
767
768 let position_address = get_position_address(&position_mint)?.0;
769 let position_after = maybe_fetch_position(&ctx.rpc, position_address).await?;
770 assert!(position_after.is_none(), "[{} {}] position={} was not closed!", pool_name, range_name, position_mint);
771
772 let after_a = get_token_balance(&ctx.rpc, if swapped { user_atas[mkey_b] } else { user_atas[mkey_a] }).await?;
773 let after_b = get_token_balance(&ctx.rpc, if swapped { user_atas[mkey_a] } else { user_atas[mkey_b] }).await?;
774 let gained_a = after_a.saturating_sub(before_a);
775 let gained_b = after_b.saturating_sub(before_b);
776
777 let total_expected_a = close_ix.quote.token_est_a + close_ix.fees_quote.fee_owed_a;
778 let total_expected_b = close_ix.quote.token_est_b + close_ix.fees_quote.fee_owed_b;
779
780 assert_eq!(
781 gained_a, total_expected_a,
782 "[{} {}] position={} token A mismatch: gained={}, expected={}",
783 pool_name, range_name, position_mint, gained_a, total_expected_a
784 );
785 assert_eq!(
786 gained_b, total_expected_b,
787 "[{} {}] position={} token B mismatch: gained={}, expected={}",
788 pool_name, range_name, position_mint, gained_b, total_expected_b
789 );
790 Ok(())
791 }
792
793 #[tokio::test]
794 #[serial]
795 async fn test_close_position_fails_if_missing_mint() -> Result<(), Box<dyn Error>> {
796 let ctx = RpcContext::new().await;
797
798 let bogus_mint = Pubkey::new_unique();
799
800 let res = close_position_instructions(&ctx.rpc, bogus_mint, Some(100), Some(ctx.signer.pubkey())).await;
801
802 assert!(res.is_err(), "Expected error when position mint doesn't exist");
803
804 Ok(())
805 }
806}