1use crate::account::get_rent;
2use crate::token::{get_current_transfer_fee, prepare_token_accounts_instructions, TokenAccountStrategy};
3use crate::{PriceOrTickIndex, FUNDER};
4use fusionamm_client::{
5 get_limit_order_address, get_tick_array_address, CloseLimitOrder, DecreaseLimitOrder, DecreaseLimitOrderInstructionArgs, FusionPool,
6 IncreaseLimitOrder, IncreaseLimitOrderInstructionArgs, InitializeTickArray, InitializeTickArrayInstructionArgs, LimitOrder, OpenLimitOrder,
7 OpenLimitOrderInstructionArgs, TickArray, FP_NFT_UPDATE_AUTH,
8};
9use fusionamm_core::{
10 decrease_limit_order_quote, get_initializable_tick_index, get_tick_array_start_tick_index, price_to_tick_index, try_reverse_apply_transfer_fee,
11 LimitOrderDecreaseQuote,
12};
13use solana_client::nonblocking::rpc_client::RpcClient;
14use solana_keypair::Keypair;
15use solana_program::instruction::Instruction;
16use solana_program::program_pack::Pack;
17use solana_program::pubkey::Pubkey;
18use solana_signer::Signer;
19use spl_associated_token_account::get_associated_token_address_with_program_id;
20use spl_token_2022::state::Mint;
21use std::error::Error;
22
23#[derive(Debug)]
24pub struct OpenLimitOrderInstruction {
25 pub limit_order_mint: Pubkey,
27
28 pub instructions: Vec<Instruction>,
30
31 pub additional_signers: Vec<Keypair>,
33
34 pub quote_a: u64,
36
37 pub quote_b: u64,
39
40 pub initialization_cost: u64,
42}
43
44#[derive(Debug)]
45pub struct IncreaseLimitOrderInstruction {
46 pub instructions: Vec<Instruction>,
48
49 pub quote_a: u64,
51
52 pub quote_b: u64,
54
55 pub additional_signers: Vec<Keypair>,
57}
58
59#[derive(Debug)]
60pub struct DecreaseLimitOrderInstruction {
61 pub instructions: Vec<Instruction>,
63
64 pub quote: LimitOrderDecreaseQuote,
66
67 pub additional_signers: Vec<Keypair>,
69}
70
71#[cfg(not(doctest))]
72pub async fn open_limit_order_instructions(
135 rpc: &RpcClient,
136 pool_address: Pubkey,
137 amount: u64,
138 price_or_tick_index: PriceOrTickIndex,
139 a_to_b: bool,
140 funder: Option<Pubkey>,
141) -> Result<OpenLimitOrderInstruction, Box<dyn Error>> {
142 let funder = funder.unwrap_or(*FUNDER.try_lock()?);
143 let rent = get_rent(rpc).await?;
144 if funder == Pubkey::default() {
145 return Err("Funder must be provided".into());
146 }
147
148 let fusion_pool_info = rpc.get_account(&pool_address).await?;
149 let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
150
151 let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
152
153 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
155 if mint_a_info.data.len() < Mint::LEN {
156 return Err("Wrong token A mint account length".into());
157 }
158 let mint_a = Mint::unpack_from_slice(&mint_a_info.data).expect("Failed to unpack token A mint");
159 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
160 if mint_b_info.data.len() < Mint::LEN {
161 return Err("Wrong token B mint account length".into());
162 }
163 let mint_b = Mint::unpack_from_slice(&mint_b_info.data).expect("Failed to unpack token B mint");
164
165 let tick_index = match price_or_tick_index {
166 PriceOrTickIndex::Tick(tick_index) => tick_index,
167 PriceOrTickIndex::Price(price) => price_to_tick_index(price, mint_a.decimals, mint_b.decimals),
168 };
169
170 let (mint_address, mint_info) = if a_to_b {
171 (fusion_pool.token_mint_a, mint_a_info)
172 } else {
173 (fusion_pool.token_mint_b, mint_b_info)
174 };
175
176 let mut instructions: Vec<Instruction> = Vec::new();
177 let mut non_refundable_rent: u64 = 0;
178 let mut additional_signers: Vec<Keypair> = Vec::new();
179
180 let initializable_tick_index = get_initializable_tick_index(tick_index, fusion_pool.tick_spacing, Some(false));
181
182 let epoch = rpc.get_epoch_info().await?.epoch;
183 let transfer_fee = get_current_transfer_fee(Some(mint_info), epoch);
184 let amount_with_fee = if transfer_fee.is_some() {
185 try_reverse_apply_transfer_fee(amount, transfer_fee.unwrap_or_default())?
186 } else {
187 amount
188 };
189
190 additional_signers.push(Keypair::new());
191 let limit_order_mint = additional_signers[0].pubkey();
192
193 let tick_array_start_index = get_tick_array_start_tick_index(initializable_tick_index, fusion_pool.tick_spacing);
194
195 let limit_order_address = get_limit_order_address(&limit_order_mint)?.0;
196 let limit_order_token_account_address = get_associated_token_address_with_program_id(&funder, &limit_order_mint, &spl_token_2022::ID);
197 let tick_array_address = get_tick_array_address(&pool_address, tick_array_start_index)?.0;
198
199 let token_accounts =
200 prepare_token_accounts_instructions(rpc, funder, vec![TokenAccountStrategy::WithBalance(mint_address, amount_with_fee)]).await?;
201
202 instructions.extend(token_accounts.create_instructions);
203 additional_signers.extend(token_accounts.additional_signers);
204
205 let tick_array_info = rpc.get_account(&tick_array_address).await;
206
207 if tick_array_info.is_err() {
208 instructions.push(
209 InitializeTickArray {
210 fusion_pool: pool_address,
211 funder,
212 tick_array: tick_array_address,
213 system_program: solana_program::system_program::id(),
214 }
215 .instruction(InitializeTickArrayInstructionArgs {
216 start_tick_index: tick_array_start_index,
217 }),
218 );
219 non_refundable_rent += rent.minimum_balance(TickArray::MIN_LEN);
220 }
221
222 let token_owner_account = token_accounts
223 .token_account_addresses
224 .get(&mint_address)
225 .ok_or("Token owner account not found")?;
226
227 instructions.push(
228 OpenLimitOrder {
229 funder,
230 owner: funder,
231 limit_order: limit_order_address,
232 limit_order_mint,
233 limit_order_token_account: limit_order_token_account_address,
234 fusion_pool: pool_address,
235 token2022_program: spl_token_2022::ID,
236 system_program: solana_program::system_program::id(),
237 associated_token_program: spl_associated_token_account::ID,
238 metadata_update_auth: FP_NFT_UPDATE_AUTH,
239 }
240 .instruction(OpenLimitOrderInstructionArgs {
241 tick_index,
242 a_to_b,
243 with_token_metadata_extension: true,
244 }),
245 );
246
247 instructions.push(
248 IncreaseLimitOrder {
249 limit_order_authority: funder,
250 fusion_pool: pool_address,
251 limit_order: limit_order_address,
252 limit_order_token_account: limit_order_token_account_address,
253 token_mint: mint_address,
254 token_owner_account: *token_owner_account,
255 token_vault: if a_to_b { fusion_pool.token_vault_a } else { fusion_pool.token_vault_b },
256 tick_array: tick_array_address,
257 token_program: mint_info.owner,
258 memo_program: spl_memo::ID,
259 }
260 .instruction(IncreaseLimitOrderInstructionArgs {
261 amount,
262 remaining_accounts_info: None,
263 }),
264 );
265
266 instructions.extend(token_accounts.cleanup_instructions);
267
268 Ok(OpenLimitOrderInstruction {
269 limit_order_mint,
270 instructions,
271 additional_signers,
272 quote_a: if a_to_b { amount_with_fee } else { 0 },
273 quote_b: if a_to_b { 0 } else { amount_with_fee },
274 initialization_cost: non_refundable_rent,
275 })
276}
277
278pub async fn increase_limit_order_instructions(
301 rpc: &RpcClient,
302 limit_order_mint: Pubkey,
303 amount: u64,
304 authority: Option<Pubkey>,
305) -> Result<IncreaseLimitOrderInstruction, Box<dyn Error>> {
306 let funder = authority.unwrap_or(*FUNDER.try_lock()?);
307 if funder == Pubkey::default() {
308 return Err("Funder must be provided".into());
309 }
310
311 let mut instructions: Vec<Instruction> = Vec::new();
312
313 let limit_order_address = get_limit_order_address(&limit_order_mint)?.0;
314 let limit_order_info = rpc.get_account(&limit_order_address).await?;
315 let limit_order = LimitOrder::from_bytes(&limit_order_info.data)?;
316
317 let fusion_pool_info = rpc.get_account(&limit_order.fusion_pool).await?;
318 let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
319
320 let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
321 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
322 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
323
324 let (mint_address, mint_info) = if limit_order.a_to_b {
325 (fusion_pool.token_mint_a, mint_a_info)
326 } else {
327 (fusion_pool.token_mint_b, mint_b_info)
328 };
329
330 let tick_array_start_index = get_tick_array_start_tick_index(limit_order.tick_index, fusion_pool.tick_spacing);
331
332 let limit_order_token_account_address = get_associated_token_address_with_program_id(&funder, &limit_order_mint, &spl_token_2022::ID);
333 let tick_array_address = get_tick_array_address(&limit_order.fusion_pool, tick_array_start_index)?.0;
334
335 let epoch = rpc.get_epoch_info().await?.epoch;
336 let transfer_fee = get_current_transfer_fee(Some(mint_info), epoch);
337 let amount_with_fee = if transfer_fee.is_some() {
338 try_reverse_apply_transfer_fee(amount, transfer_fee.unwrap_or_default())?
339 } else {
340 amount
341 };
342
343 let token_accounts =
344 prepare_token_accounts_instructions(rpc, funder, vec![TokenAccountStrategy::WithBalance(mint_address, amount_with_fee)]).await?;
345
346 instructions.extend(token_accounts.create_instructions);
347
348 let token_owner_account = token_accounts
349 .token_account_addresses
350 .get(&mint_address)
351 .ok_or("Token owner account not found")?;
352
353 instructions.push(
354 IncreaseLimitOrder {
355 limit_order_authority: funder,
356 fusion_pool: limit_order.fusion_pool,
357 limit_order: limit_order_address,
358 limit_order_token_account: limit_order_token_account_address,
359 token_mint: mint_address,
360 token_owner_account: *token_owner_account,
361 token_vault: if limit_order.a_to_b {
362 fusion_pool.token_vault_a
363 } else {
364 fusion_pool.token_vault_b
365 },
366 tick_array: tick_array_address,
367 token_program: mint_info.owner,
368 memo_program: spl_memo::ID,
369 }
370 .instruction(IncreaseLimitOrderInstructionArgs {
371 amount,
372 remaining_accounts_info: None,
373 }),
374 );
375
376 instructions.extend(token_accounts.cleanup_instructions);
377
378 Ok(IncreaseLimitOrderInstruction {
379 instructions,
380 additional_signers: token_accounts.additional_signers,
381 quote_a: if limit_order.a_to_b { amount_with_fee } else { 0 },
382 quote_b: if limit_order.a_to_b { 0 } else { amount_with_fee },
383 })
384}
385
386#[cfg(not(doctest))]
387pub async fn close_limit_order_instructions(
440 rpc: &RpcClient,
441 limit_order_mint: Pubkey,
442 authority: Option<Pubkey>,
443) -> Result<DecreaseLimitOrderInstruction, Box<dyn Error>> {
444 internal_decrease_and_close_limit_order_instructions(rpc, limit_order_mint, None, authority).await
445}
446
447#[cfg(not(doctest))]
448pub async fn decrease_limit_order_instructions(
505 rpc: &RpcClient,
506 limit_order_mint: Pubkey,
507 amount: u64,
508 authority: Option<Pubkey>,
509) -> Result<DecreaseLimitOrderInstruction, Box<dyn Error>> {
510 internal_decrease_and_close_limit_order_instructions(rpc, limit_order_mint, Some(amount), authority).await
511}
512
513async fn internal_decrease_and_close_limit_order_instructions(
514 rpc: &RpcClient,
515 limit_order_mint: Pubkey,
516 amount: Option<u64>,
517 authority: Option<Pubkey>,
518) -> Result<DecreaseLimitOrderInstruction, Box<dyn Error>> {
519 let funder = authority.unwrap_or(*FUNDER.try_lock()?);
520 if funder == Pubkey::default() {
521 return Err("Funder must be provided".into());
522 }
523
524 let mut instructions: Vec<Instruction> = Vec::new();
525
526 let limit_order_address = get_limit_order_address(&limit_order_mint)?.0;
527 let limit_order_info = rpc.get_account(&limit_order_address).await?;
528 let limit_order = LimitOrder::from_bytes(&limit_order_info.data)?;
529
530 let fusion_pool_info = rpc.get_account(&limit_order.fusion_pool).await?;
531 let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
532
533 let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
534 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
535 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
536
537 let tick_array_start_index = get_tick_array_start_tick_index(limit_order.tick_index, fusion_pool.tick_spacing);
538
539 let limit_order_token_account_address = get_associated_token_address_with_program_id(&funder, &limit_order_mint, &spl_token_2022::ID);
540 let tick_array_address = get_tick_array_address(&limit_order.fusion_pool, tick_array_start_index)?.0;
541
542 let tick_array_info = rpc.get_account(&tick_array_address).await?;
543 let tick_array = TickArray::from_bytes(&tick_array_info.data)?;
544 let tick = &tick_array.ticks[((limit_order.tick_index - tick_array_start_index) / fusion_pool.tick_spacing as i32) as usize];
545
546 let decrease_amount = match amount {
547 None => limit_order.amount,
548 Some(amount) => amount,
549 };
550
551 let current_epoch = rpc.get_epoch_info().await?.epoch;
552 let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), current_epoch);
553 let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), current_epoch);
554
555 let quote = decrease_limit_order_quote(
556 fusion_pool.clone().into(),
557 limit_order.clone().into(),
558 tick.clone().into(),
559 decrease_amount,
560 transfer_fee_a,
561 transfer_fee_b,
562 )?;
563
564 let token_accounts = prepare_token_accounts_instructions(
565 rpc,
566 funder,
567 vec![
568 TokenAccountStrategy::WithoutBalance(fusion_pool.token_mint_a),
569 TokenAccountStrategy::WithoutBalance(fusion_pool.token_mint_b),
570 ],
571 )
572 .await?;
573
574 instructions.extend(token_accounts.create_instructions);
575
576 instructions.push(
577 DecreaseLimitOrder {
578 limit_order_authority: funder,
579 fusion_pool: limit_order.fusion_pool,
580 limit_order: limit_order_address,
581 limit_order_token_account: limit_order_token_account_address,
582 token_mint_a: fusion_pool.token_mint_a,
583 token_mint_b: fusion_pool.token_mint_b,
584 token_owner_account_a: *token_accounts.token_account_addresses.get(&fusion_pool.token_mint_a).unwrap(),
585 token_owner_account_b: *token_accounts.token_account_addresses.get(&fusion_pool.token_mint_b).unwrap(),
586 token_vault_a: fusion_pool.token_vault_a,
587 token_vault_b: fusion_pool.token_vault_b,
588 tick_array: tick_array_address,
589 token_program_a: mint_a_info.owner,
590 token_program_b: mint_b_info.owner,
591 memo_program: spl_memo::ID,
592 }
593 .instruction(DecreaseLimitOrderInstructionArgs {
594 amount: decrease_amount,
595 remaining_accounts_info: None,
596 }),
597 );
598
599 if amount.is_none() {
600 instructions.push(
601 CloseLimitOrder {
602 limit_order_authority: funder,
603 receiver: funder,
604 limit_order: limit_order_address,
605 limit_order_mint,
606 limit_order_token_account: limit_order_token_account_address,
607 token2022_program: spl_token_2022::ID,
608 }
609 .instruction(),
610 );
611 }
612
613 instructions.extend(token_accounts.cleanup_instructions);
614
615 Ok(DecreaseLimitOrderInstruction {
616 instructions,
617 quote,
618 additional_signers: token_accounts.additional_signers,
619 })
620}
621
622#[cfg(test)]
623mod tests {
624 use crate::{
625 close_limit_order_instructions, decrease_limit_order_instructions, increase_limit_order_instructions, open_limit_order_instructions,
626 tests::{
627 setup_ata_te, setup_ata_with_amount, setup_fusion_pool, setup_mint_te, setup_mint_te_fee, setup_mint_with_decimals, RpcContext,
628 SetupAtaConfig,
629 },
630 DecreaseLimitOrderInstruction, IncreaseLimitOrderInstruction, OpenLimitOrderInstruction, PriceOrTickIndex,
631 };
632 use fusionamm_client::{get_limit_order_address, LimitOrder};
633 use rstest::rstest;
634 use serial_test::serial;
635 use solana_client::nonblocking::rpc_client::RpcClient;
636 use solana_keypair::Keypair;
637 use solana_program::program_pack::Pack;
638 use solana_program_test::tokio;
639 use solana_pubkey::Pubkey;
640 use solana_signer::Signer;
641 use spl_token::state::Account as TokenAccount;
642 use spl_token_2022::{extension::StateWithExtensionsOwned, state::Account as TokenAccount2022, ID as TOKEN_2022_PROGRAM_ID};
643 use std::collections::HashMap;
644 use std::error::Error;
645
646 async fn fetch_limit_order(rpc: &RpcClient, address: Pubkey) -> Result<LimitOrder, Box<dyn Error>> {
647 let account = rpc.get_account(&address).await?;
648 LimitOrder::from_bytes(&account.data).map_err(|e| e.into())
649 }
650
651 async fn get_token_balance(rpc: &RpcClient, address: Pubkey) -> Result<u64, Box<dyn Error>> {
652 let account_data = rpc.get_account(&address).await?;
653
654 if account_data.owner == TOKEN_2022_PROGRAM_ID {
655 let state = StateWithExtensionsOwned::<TokenAccount2022>::unpack(account_data.data)?;
656 Ok(state.base.amount)
657 } else {
658 let token_account = TokenAccount::unpack(&account_data.data)?;
659 Ok(token_account.amount)
660 }
661 }
662
663 async fn verify_open_limit_order(
664 ctx: &RpcContext,
665 open_ix: &OpenLimitOrderInstruction,
666 amount: u64,
667 token_a_account: Pubkey,
668 token_b_account: Pubkey,
669 ) -> Result<(), Box<dyn Error>> {
670 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
671 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
672
673 let signers: Vec<&Keypair> = open_ix.additional_signers.iter().collect();
674 ctx.send_transaction_with_signers(open_ix.instructions.clone(), signers).await?;
675
676 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
677 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
678 let used_a = before_a.saturating_sub(after_a);
679 let used_b = before_b.saturating_sub(after_b);
680 assert!(used_a == open_ix.quote_a, "Token A usage mismatch! expected={}, got={}", open_ix.quote_a, used_a);
681 assert!(used_b == open_ix.quote_b, "Token B usage mismatch! expected={}, got={}", open_ix.quote_b, used_b);
682
683 let limit_order_address = get_limit_order_address(&open_ix.limit_order_mint)?.0;
684 let limit_order = fetch_limit_order(&ctx.rpc, limit_order_address).await?;
685 assert_eq!(limit_order.amount, amount, "Limit order amount mismatch! expected={}, got={}", amount, limit_order.amount);
686
687 Ok(())
688 }
689
690 async fn verify_increase_limit_order(
691 ctx: &RpcContext,
692 open_ix: &IncreaseLimitOrderInstruction,
693 limit_order_mint: Pubkey,
694 amount: u64,
695 token_a_account: Pubkey,
696 token_b_account: Pubkey,
697 ) -> Result<(), Box<dyn Error>> {
698 let limit_order_address = get_limit_order_address(&limit_order_mint)?.0;
699 let limit_order_before = fetch_limit_order(&ctx.rpc, limit_order_address).await?;
700
701 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
702 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
703
704 let signers: Vec<&Keypair> = open_ix.additional_signers.iter().collect();
705 ctx.send_transaction_with_signers(open_ix.instructions.clone(), signers).await?;
706
707 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
708 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
709 let used_a = before_a.saturating_sub(after_a);
710 let used_b = before_b.saturating_sub(after_b);
711 assert!(used_a == open_ix.quote_a, "Token A usage mismatch! expected={}, got={}", open_ix.quote_a, used_a);
712 assert!(used_b == open_ix.quote_b, "Token B usage mismatch! expected={}, got={}", open_ix.quote_b, used_b);
713
714 let limit_order_after = fetch_limit_order(&ctx.rpc, limit_order_address).await?;
715 assert_eq!(
716 limit_order_after.amount - limit_order_before.amount,
717 amount,
718 "Limit order amount increase mismatch! expected={}, got={}",
719 amount,
720 limit_order_after.amount - limit_order_before.amount
721 );
722
723 Ok(())
724 }
725
726 async fn verify_decrease_limit_order(
727 ctx: &RpcContext,
728 decrese_ix: &DecreaseLimitOrderInstruction,
729 limit_order_mint: Pubkey,
730 amount: u64,
731 token_a_account: Pubkey,
732 token_b_account: Pubkey,
733 ) -> Result<(), Box<dyn Error>> {
734 let limit_order_address = get_limit_order_address(&limit_order_mint)?.0;
735 let limit_order_before = fetch_limit_order(&ctx.rpc, limit_order_address).await?;
736 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
737 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
738
739 let signers: Vec<&Keypair> = decrese_ix.additional_signers.iter().collect();
740 ctx.send_transaction_with_signers(decrese_ix.instructions.clone(), signers).await?;
741
742 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
743 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
744 let used_a = after_a - before_a;
745 let used_b = after_b - before_b;
746 assert_eq!(
747 used_a, decrese_ix.quote.amount_out_a,
748 "Token A withdraw mismatch! expected={}, got={}",
749 decrese_ix.quote.amount_out_a, used_a
750 );
751 assert_eq!(
752 used_b, decrese_ix.quote.amount_out_b,
753 "Token B withdraw mismatch! expected={}, got={}",
754 decrese_ix.quote.amount_out_b, used_b
755 );
756
757 let limit_order_after = fetch_limit_order(&ctx.rpc, limit_order_address).await?;
758 assert_eq!(
759 limit_order_before.amount - limit_order_after.amount,
760 amount,
761 "Limit order amount decrease mismatch! expected={}, got={}",
762 amount,
763 limit_order_before.amount - limit_order_after.amount
764 );
765
766 Ok(())
767 }
768
769 async fn verify_close_limit_order(
770 ctx: &RpcContext,
771 close_ix: &DecreaseLimitOrderInstruction,
772 token_a_account: Pubkey,
773 token_b_account: Pubkey,
774 ) -> Result<(), Box<dyn Error>> {
775 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
776 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
777
778 let signers: Vec<&Keypair> = close_ix.additional_signers.iter().collect();
779 ctx.send_transaction_with_signers(close_ix.instructions.clone(), signers).await?;
780
781 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
782 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
783 let used_a = after_a - before_a;
784 let used_b = after_b - before_b;
785 assert_eq!(
786 used_a, close_ix.quote.amount_out_a,
787 "Token A withdraw mismatch! expected={}, got={}",
788 close_ix.quote.amount_out_a, used_a
789 );
790 assert_eq!(
791 used_b, close_ix.quote.amount_out_b,
792 "Token B withdraw mismatch! expected={}, got={}",
793 close_ix.quote.amount_out_b, used_b
794 );
795
796 Ok(())
797 }
798
799 async fn setup_all_mints(ctx: &RpcContext) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
800 let mint_a = setup_mint_with_decimals(ctx, 9).await?;
801 let mint_b = setup_mint_with_decimals(ctx, 9).await?;
802 let mint_te_a = setup_mint_te(ctx, &[]).await?;
803 let mint_te_b = setup_mint_te(ctx, &[]).await?;
804 let mint_te_fee = setup_mint_te_fee(ctx).await?;
805
806 let mut out = HashMap::new();
807 out.insert("A", mint_a);
808 out.insert("B", mint_b);
809 out.insert("TEA", mint_te_a);
810 out.insert("TEB", mint_te_b);
811 out.insert("TEFee", mint_te_fee);
812
813 Ok(out)
814 }
815
816 async fn setup_all_atas(ctx: &RpcContext, minted: &HashMap<&str, Pubkey>) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
817 let token_balance = 1_000_000_000;
818 let user_ata_a = setup_ata_with_amount(ctx, *minted.get("A").unwrap(), token_balance).await?;
819 let user_ata_b = setup_ata_with_amount(ctx, *minted.get("B").unwrap(), token_balance).await?;
820 let user_ata_te_a = setup_ata_te(ctx, *minted.get("TEA").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
821 let user_ata_te_b = setup_ata_te(ctx, *minted.get("TEB").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
822 let user_ata_tefee = setup_ata_te(ctx, *minted.get("TEFee").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
823
824 let mut out = HashMap::new();
825 out.insert("A", user_ata_a);
826 out.insert("B", user_ata_b);
827 out.insert("TEA", user_ata_te_a);
828 out.insert("TEB", user_ata_te_b);
829 out.insert("TEFee", user_ata_tefee);
830
831 Ok(out)
832 }
833
834 pub fn parse_pool_name(pool_name: &str) -> (&'static str, &'static str) {
835 match pool_name {
836 "A-B" => ("A", "B"),
837 "A-TEA" => ("A", "TEA"),
838 "TEA-TEB" => ("TEA", "TEB"),
839 "A-TEFee" => ("A", "TEFee"),
840 _ => panic!("Unknown pool name: {}", pool_name),
841 }
842 }
843
844 #[rstest]
845 #[case("A-B", "input amount in A", 128, true)]
846 #[case("A-B", "input amount in B", -128, false)]
847 #[case("A-TEA", "input amount in A", 128, true)]
848 #[case("A-TEA", "input amount in B", -128, false)]
849 #[case("TEA-TEB", "input amount in A", 128, true)]
850 #[case("TEA-TEB", "input amount in B", -128, false)]
851 #[case("A-TEFee", "input amount in A", 128, true)]
852 #[case("A-TEFee", "input amount in B", -128, false)]
853 #[serial]
854 fn test_open_increase_decrease_and_close_limit_order_cases(
855 #[case] pool_name: &str,
856 #[case] _limit_order_name: &str,
857 #[case] tick_index: i32,
858 #[case] a_to_b: bool,
859 ) {
860 let rt = tokio::runtime::Runtime::new().unwrap();
861 rt.block_on(async {
862 let ctx = RpcContext::new().await;
863
864 let minted = setup_all_mints(&ctx).await.unwrap();
865 let user_atas = setup_all_atas(&ctx, &minted).await.unwrap();
866
867 let (mint_a_key, mint_b_key) = parse_pool_name(pool_name);
868 let pubkey_a = *minted.get(mint_a_key).unwrap();
869 let pubkey_b = *minted.get(mint_b_key).unwrap();
870
871 let (final_a, final_b) = if pubkey_a < pubkey_b {
872 (pubkey_a, pubkey_b)
873 } else {
874 (pubkey_b, pubkey_a)
875 };
876
877 let tick_spacing = 64;
879 let fee_rate = 300;
880 let swapped = pubkey_a > pubkey_b;
881 let pool_pubkey = setup_fusion_pool(&ctx, final_a, final_b, tick_spacing, fee_rate).await.unwrap();
882 let user_ata_for_token_a = if swapped {
883 user_atas.get(mint_b_key).unwrap()
884 } else {
885 user_atas.get(mint_a_key).unwrap()
886 };
887 let user_ata_for_token_b = if swapped {
888 user_atas.get(mint_a_key).unwrap()
889 } else {
890 user_atas.get(mint_b_key).unwrap()
891 };
892
893 let initial_amount = 2_000_000;
894 let modify_amount = 1_000_000;
895
896 let open_ix = open_limit_order_instructions(
898 &ctx.rpc,
899 pool_pubkey,
900 initial_amount,
901 PriceOrTickIndex::Tick(tick_index),
902 a_to_b,
903 Some(ctx.signer.pubkey()),
904 )
905 .await
906 .unwrap();
907 verify_open_limit_order(&ctx, &open_ix, initial_amount, *user_ata_for_token_a, *user_ata_for_token_b)
908 .await
909 .unwrap();
910
911 let increase_ix = increase_limit_order_instructions(&ctx.rpc, open_ix.limit_order_mint, modify_amount, Some(ctx.signer.pubkey()))
913 .await
914 .unwrap();
915 verify_increase_limit_order(&ctx, &increase_ix, open_ix.limit_order_mint, modify_amount, *user_ata_for_token_a, *user_ata_for_token_b)
916 .await
917 .unwrap();
918
919 let decrease_ix = decrease_limit_order_instructions(&ctx.rpc, open_ix.limit_order_mint, modify_amount, Some(ctx.signer.pubkey()))
921 .await
922 .unwrap();
923 verify_decrease_limit_order(&ctx, &decrease_ix, open_ix.limit_order_mint, modify_amount, *user_ata_for_token_a, *user_ata_for_token_b)
924 .await
925 .unwrap();
926
927 let close_ix = close_limit_order_instructions(&ctx.rpc, open_ix.limit_order_mint, Some(ctx.signer.pubkey()))
929 .await
930 .unwrap();
931 verify_close_limit_order(&ctx, &close_ix, *user_ata_for_token_a, *user_ata_for_token_b)
932 .await
933 .unwrap();
934 });
935 }
936}