1use crate::get_rent;
12use crate::{
13 token::{get_current_transfer_fee, prepare_token_accounts_instructions, TokenAccountStrategy},
14 FUNDER, SLIPPAGE_TOLERANCE_BPS,
15};
16use fusionamm_client::{
17 get_position_address, get_tick_array_address, FusionPool, InitializeTickArray, InitializeTickArrayInstructionArgs, OpenPosition,
18 OpenPositionInstructionArgs, Position, TickArray, FP_NFT_UPDATE_AUTH,
19};
20use fusionamm_client::{IncreaseLiquidity, IncreaseLiquidityInstructionArgs};
21use fusionamm_core::{
22 get_full_range_tick_indexes, get_initializable_tick_index, get_tick_array_start_tick_index, increase_liquidity_quote, increase_liquidity_quote_a,
23 increase_liquidity_quote_b, order_tick_indexes, price_to_tick_index, IncreaseLiquidityQuote, TransferFee,
24};
25use solana_account::Account;
26use solana_client::nonblocking::rpc_client::RpcClient;
27use solana_instruction::Instruction;
28use solana_keypair::Keypair;
29use solana_program::program_pack::Pack;
30use solana_pubkey::Pubkey;
31use solana_signer::Signer;
32use spl_associated_token_account::get_associated_token_address_with_program_id;
33use spl_token_2022::state::Mint;
34use std::error::Error;
35
36pub enum PriceOrTickIndex {
37 Tick(i32),
38 Price(f64),
39}
40
41fn get_increase_liquidity_quote(
44 param: IncreaseLiquidityParam,
45 slippage_tolerance_bps: u16,
46 pool: &FusionPool,
47 tick_lower_index: i32,
48 tick_upper_index: i32,
49 transfer_fee_a: Option<TransferFee>,
50 transfer_fee_b: Option<TransferFee>,
51) -> Result<IncreaseLiquidityQuote, Box<dyn Error>> {
52 let result = match param {
53 IncreaseLiquidityParam::TokenA(amount) => increase_liquidity_quote_a(
54 amount,
55 slippage_tolerance_bps,
56 pool.sqrt_price,
57 tick_lower_index,
58 tick_upper_index,
59 transfer_fee_a,
60 transfer_fee_b,
61 ),
62 IncreaseLiquidityParam::TokenB(amount) => increase_liquidity_quote_b(
63 amount,
64 slippage_tolerance_bps,
65 pool.sqrt_price,
66 tick_lower_index,
67 tick_upper_index,
68 transfer_fee_a,
69 transfer_fee_b,
70 ),
71 IncreaseLiquidityParam::Liquidity(amount) => increase_liquidity_quote(
72 amount,
73 slippage_tolerance_bps,
74 pool.sqrt_price,
75 tick_lower_index,
76 tick_upper_index,
77 transfer_fee_a,
78 transfer_fee_b,
79 ),
80 }?;
81 Ok(result)
82}
83
84#[derive(Debug, Clone)]
89pub enum IncreaseLiquidityParam {
90 TokenA(u64),
92
93 TokenB(u64),
95
96 Liquidity(u128),
98}
99
100#[derive(Debug)]
105pub struct IncreaseLiquidityInstruction {
106 pub quote: IncreaseLiquidityQuote,
113
114 pub instructions: Vec<Instruction>,
116
117 pub additional_signers: Vec<Keypair>,
119}
120
121#[cfg(not(doctest))]
122pub async fn increase_liquidity_instructions(
182 rpc: &RpcClient,
183 position_mint_address: Pubkey,
184 param: IncreaseLiquidityParam,
185 slippage_tolerance_bps: Option<u16>,
186 authority: Option<Pubkey>,
187) -> Result<IncreaseLiquidityInstruction, Box<dyn Error>> {
188 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(*SLIPPAGE_TOLERANCE_BPS.try_lock()?);
189 let authority = authority.unwrap_or(*FUNDER.try_lock()?);
190 if authority == Pubkey::default() {
191 return Err("Authority must be provided".into());
192 }
193
194 let position_address = get_position_address(&position_mint_address)?.0;
195 let position_info = rpc.get_account(&position_address).await?;
196 let position = Position::from_bytes(&position_info.data)?;
197
198 let pool_info = rpc.get_account(&position.fusion_pool).await?;
199 let pool = FusionPool::from_bytes(&pool_info.data)?;
200
201 let mint_infos = rpc
202 .get_multiple_accounts(&[pool.token_mint_a, pool.token_mint_b, position_mint_address])
203 .await?;
204
205 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
206 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
207 let position_mint_info = mint_infos[2].as_ref().ok_or("Position mint info not found")?;
208
209 let current_epoch = rpc.get_epoch_info().await?.epoch;
210 let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), current_epoch);
211 let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), current_epoch);
212
213 let quote = get_increase_liquidity_quote(
214 param,
215 slippage_tolerance_bps,
216 &pool,
217 position.tick_lower_index,
218 position.tick_upper_index,
219 transfer_fee_a,
220 transfer_fee_b,
221 )?;
222
223 let mut instructions: Vec<Instruction> = Vec::new();
224
225 let lower_tick_array_start_index = get_tick_array_start_tick_index(position.tick_lower_index, pool.tick_spacing);
226 let upper_tick_array_start_index = get_tick_array_start_tick_index(position.tick_upper_index, pool.tick_spacing);
227
228 let position_token_account_address = get_associated_token_address_with_program_id(&authority, &position_mint_address, &position_mint_info.owner);
229 let lower_tick_array_address = get_tick_array_address(&position.fusion_pool, lower_tick_array_start_index)?.0;
230 let upper_tick_array_address = get_tick_array_address(&position.fusion_pool, upper_tick_array_start_index)?.0;
231
232 let token_accounts = prepare_token_accounts_instructions(
233 rpc,
234 authority,
235 vec![
236 TokenAccountStrategy::WithBalance(pool.token_mint_a, quote.token_max_a),
237 TokenAccountStrategy::WithBalance(pool.token_mint_b, quote.token_max_b),
238 ],
239 )
240 .await?;
241
242 instructions.extend(token_accounts.create_instructions);
243
244 let token_owner_account_a = token_accounts
245 .token_account_addresses
246 .get(&pool.token_mint_a)
247 .ok_or("Token A owner account not found")?;
248 let token_owner_account_b = token_accounts
249 .token_account_addresses
250 .get(&pool.token_mint_b)
251 .ok_or("Token B owner account not found")?;
252
253 instructions.push(
254 IncreaseLiquidity {
255 fusion_pool: position.fusion_pool,
256 token_program_a: mint_a_info.owner,
257 token_program_b: mint_b_info.owner,
258 memo_program: spl_memo::ID,
259 position_authority: authority,
260 position: position_address,
261 position_token_account: position_token_account_address,
262 token_mint_a: pool.token_mint_a,
263 token_mint_b: pool.token_mint_b,
264 token_owner_account_a: *token_owner_account_a,
265 token_owner_account_b: *token_owner_account_b,
266 token_vault_a: pool.token_vault_a,
267 token_vault_b: pool.token_vault_b,
268 tick_array_lower: lower_tick_array_address,
269 tick_array_upper: upper_tick_array_address,
270 }
271 .instruction(IncreaseLiquidityInstructionArgs {
272 liquidity_amount: quote.liquidity_delta,
273 token_max_a: quote.token_max_a,
274 token_max_b: quote.token_max_b,
275 remaining_accounts_info: None,
276 }),
277 );
278
279 instructions.extend(token_accounts.cleanup_instructions);
280
281 Ok(IncreaseLiquidityInstruction {
282 quote,
283 instructions,
284 additional_signers: token_accounts.additional_signers,
285 })
286}
287
288#[derive(Debug)]
294pub struct OpenPositionInstruction {
295 pub position_mint: Pubkey,
297
298 pub quote: IncreaseLiquidityQuote,
301
302 pub instructions: Vec<Instruction>,
304
305 pub additional_signers: Vec<Keypair>,
307
308 pub initialization_cost: u64,
310}
311
312#[allow(clippy::too_many_arguments)]
313async fn internal_open_position(
314 rpc: &RpcClient,
315 pool_address: Pubkey,
316 fusion_pool: FusionPool,
317 param: IncreaseLiquidityParam,
318 lower_tick_index: i32,
319 upper_tick_index: i32,
320 mint_a_info: &Account,
321 mint_b_info: &Account,
322 slippage_tolerance_bps: Option<u16>,
323 funder: Option<Pubkey>,
324) -> Result<OpenPositionInstruction, Box<dyn Error>> {
325 let funder = funder.unwrap_or(*FUNDER.try_lock()?);
326 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(*SLIPPAGE_TOLERANCE_BPS.try_lock()?);
327 let rent = get_rent(rpc).await?;
328 if funder == Pubkey::default() {
329 return Err("Funder must be provided".into());
330 }
331
332 let tick_range = order_tick_indexes(lower_tick_index, upper_tick_index);
333
334 let lower_initializable_tick_index = get_initializable_tick_index(tick_range.tick_lower_index, fusion_pool.tick_spacing, Some(false));
335
336 let upper_initializable_tick_index = get_initializable_tick_index(tick_range.tick_upper_index, fusion_pool.tick_spacing, Some(true));
337
338 let mut instructions: Vec<Instruction> = Vec::new();
339 let mut non_refundable_rent: u64 = 0;
340 let mut additional_signers: Vec<Keypair> = Vec::new();
341
342 let epoch = rpc.get_epoch_info().await?.epoch;
343 let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), epoch);
344 let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), epoch);
345
346 let quote = get_increase_liquidity_quote(
347 param,
348 slippage_tolerance_bps,
349 &fusion_pool,
350 lower_initializable_tick_index,
351 upper_initializable_tick_index,
352 transfer_fee_a,
353 transfer_fee_b,
354 )?;
355
356 additional_signers.push(Keypair::new());
357 let position_mint = additional_signers[0].pubkey();
358
359 let lower_tick_start_index = get_tick_array_start_tick_index(lower_initializable_tick_index, fusion_pool.tick_spacing);
360 let upper_tick_start_index = get_tick_array_start_tick_index(upper_initializable_tick_index, fusion_pool.tick_spacing);
361
362 let position_address = get_position_address(&position_mint)?.0;
363 let position_token_account_address = get_associated_token_address_with_program_id(&funder, &position_mint, &spl_token_2022::ID);
364 let lower_tick_array_address = get_tick_array_address(&pool_address, lower_tick_start_index)?.0;
365 let upper_tick_array_address = get_tick_array_address(&pool_address, upper_tick_start_index)?.0;
366
367 let token_accounts = prepare_token_accounts_instructions(
368 rpc,
369 funder,
370 vec![
371 TokenAccountStrategy::WithBalance(fusion_pool.token_mint_a, quote.token_max_a),
372 TokenAccountStrategy::WithBalance(fusion_pool.token_mint_b, quote.token_max_b),
373 ],
374 )
375 .await?;
376
377 instructions.extend(token_accounts.create_instructions);
378 additional_signers.extend(token_accounts.additional_signers);
379
380 let tick_array_infos = rpc.get_multiple_accounts(&[lower_tick_array_address, upper_tick_array_address]).await?;
381
382 if tick_array_infos[0].is_none() {
383 instructions.push(
384 InitializeTickArray {
385 fusion_pool: pool_address,
386 funder,
387 tick_array: lower_tick_array_address,
388 system_program: solana_program::system_program::id(),
389 }
390 .instruction(InitializeTickArrayInstructionArgs {
391 start_tick_index: lower_tick_start_index,
392 }),
393 );
394 non_refundable_rent += rent.minimum_balance(TickArray::MIN_LEN);
395 }
396
397 if tick_array_infos[1].is_none() && lower_tick_start_index != upper_tick_start_index {
398 instructions.push(
399 InitializeTickArray {
400 fusion_pool: pool_address,
401 funder,
402 tick_array: upper_tick_array_address,
403 system_program: solana_program::system_program::id(),
404 }
405 .instruction(InitializeTickArrayInstructionArgs {
406 start_tick_index: upper_tick_start_index,
407 }),
408 );
409 non_refundable_rent += rent.minimum_balance(TickArray::MIN_LEN);
410 }
411
412 let token_owner_account_a = token_accounts
413 .token_account_addresses
414 .get(&fusion_pool.token_mint_a)
415 .ok_or("Token A owner account not found")?;
416 let token_owner_account_b = token_accounts
417 .token_account_addresses
418 .get(&fusion_pool.token_mint_b)
419 .ok_or("Token B owner account not found")?;
420
421 instructions.push(
422 OpenPosition {
423 funder,
424 owner: funder,
425 position: position_address,
426 position_mint,
427 position_token_account: position_token_account_address,
428 fusion_pool: pool_address,
429 token2022_program: spl_token_2022::ID,
430 system_program: solana_program::system_program::id(),
431 associated_token_program: spl_associated_token_account::ID,
432 metadata_update_auth: FP_NFT_UPDATE_AUTH,
433 }
434 .instruction(OpenPositionInstructionArgs {
435 tick_lower_index: lower_initializable_tick_index,
436 tick_upper_index: upper_initializable_tick_index,
437 with_token_metadata_extension: true,
438 }),
439 );
440
441 instructions.push(
442 IncreaseLiquidity {
443 fusion_pool: pool_address,
444 token_program_a: mint_a_info.owner,
445 token_program_b: mint_b_info.owner,
446 memo_program: spl_memo::ID,
447 position_authority: funder,
448 position: position_address,
449 position_token_account: position_token_account_address,
450 token_mint_a: fusion_pool.token_mint_a,
451 token_mint_b: fusion_pool.token_mint_b,
452 token_owner_account_a: *token_owner_account_a,
453 token_owner_account_b: *token_owner_account_b,
454 token_vault_a: fusion_pool.token_vault_a,
455 token_vault_b: fusion_pool.token_vault_b,
456 tick_array_lower: lower_tick_array_address,
457 tick_array_upper: upper_tick_array_address,
458 }
459 .instruction(IncreaseLiquidityInstructionArgs {
460 liquidity_amount: quote.liquidity_delta,
461 token_max_a: quote.token_max_a,
462 token_max_b: quote.token_max_b,
463 remaining_accounts_info: None,
464 }),
465 );
466
467 instructions.extend(token_accounts.cleanup_instructions);
468
469 Ok(OpenPositionInstruction {
470 position_mint,
471 quote,
472 instructions,
473 additional_signers,
474 initialization_cost: non_refundable_rent,
475 })
476}
477
478#[cfg(not(doctest))]
479pub async fn open_full_range_position_instructions(
540 rpc: &RpcClient,
541 pool_address: Pubkey,
542 param: IncreaseLiquidityParam,
543 slippage_tolerance_bps: Option<u16>,
544 funder: Option<Pubkey>,
545) -> Result<OpenPositionInstruction, Box<dyn Error>> {
546 let fusion_pool_info = rpc.get_account(&pool_address).await?;
547 let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
548 let tick_range = get_full_range_tick_indexes(fusion_pool.tick_spacing);
549 let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
550 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
551 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
552 internal_open_position(
553 rpc,
554 pool_address,
555 fusion_pool,
556 param,
557 tick_range.tick_lower_index,
558 tick_range.tick_upper_index,
559 mint_a_info,
560 mint_b_info,
561 slippage_tolerance_bps,
562 funder,
563 )
564 .await
565}
566
567#[cfg(not(doctest))]
568pub async fn open_position_instructions(
635 rpc: &RpcClient,
636 pool_address: Pubkey,
637 lower_price_or_tick_index: PriceOrTickIndex,
638 upper_price_or_tick_index: PriceOrTickIndex,
639 param: IncreaseLiquidityParam,
640 slippage_tolerance_bps: Option<u16>,
641 funder: Option<Pubkey>,
642) -> Result<OpenPositionInstruction, Box<dyn Error>> {
643 let fusion_pool_info = rpc.get_account(&pool_address).await?;
644 let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
645 let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
646 let mint_a_info = mint_infos[0].as_ref().ok_or("Token A mint info not found")?;
647 let mint_a = Mint::unpack(&mint_a_info.data)?;
648 let mint_b_info = mint_infos[1].as_ref().ok_or("Token B mint info not found")?;
649 let mint_b = Mint::unpack(&mint_b_info.data)?;
650
651 let decimals_a = mint_a.decimals;
652 let decimals_b = mint_b.decimals;
653
654 let lower_tick_index = match lower_price_or_tick_index {
655 PriceOrTickIndex::Tick(tick_index) => tick_index,
656 PriceOrTickIndex::Price(price) => price_to_tick_index(price, decimals_a, decimals_b),
657 };
658
659 let upper_tick_index = match upper_price_or_tick_index {
660 PriceOrTickIndex::Tick(tick_index) => tick_index,
661 PriceOrTickIndex::Price(price) => price_to_tick_index(price, decimals_a, decimals_b),
662 };
663
664 internal_open_position(
665 rpc,
666 pool_address,
667 fusion_pool,
668 param,
669 lower_tick_index,
670 upper_tick_index,
671 mint_a_info,
672 mint_b_info,
673 slippage_tolerance_bps,
674 funder,
675 )
676 .await
677}
678
679#[cfg(test)]
680mod tests {
681 use std::collections::HashMap;
682 use std::error::Error;
683
684 use fusionamm_client::{get_position_address, Position};
685 use rstest::rstest;
686 use serial_test::serial;
687 use solana_program_test::tokio;
688 use spl_token::state::Account as TokenAccount;
689 use spl_token_2022::{extension::StateWithExtensionsOwned, state::Account as TokenAccount2022, ID as TOKEN_2022_PROGRAM_ID};
690
691 use crate::{
692 increase_liquidity_instructions,
693 tests::{
694 setup_ata_te, setup_ata_with_amount, setup_fusion_pool, setup_mint_te, setup_mint_te_fee, setup_mint_with_decimals, RpcContext,
695 SetupAtaConfig,
696 },
697 IncreaseLiquidityParam,
698 };
699
700 use crate::tests::setup_position;
701 use solana_client::nonblocking::rpc_client::RpcClient;
702 use solana_keypair::Keypair;
703 use solana_program::program_pack::Pack;
704 use solana_pubkey::Pubkey;
705 use solana_signer::Signer;
706
707 async fn fetch_position(rpc: &RpcClient, address: Pubkey) -> Result<Position, Box<dyn Error>> {
708 let account = rpc.get_account(&address).await?;
709 Position::from_bytes(&account.data).map_err(|e| e.into())
710 }
711
712 async fn get_token_balance(rpc: &RpcClient, address: Pubkey) -> Result<u64, Box<dyn Error>> {
713 let account_data = rpc.get_account(&address).await?;
714
715 if account_data.owner == TOKEN_2022_PROGRAM_ID {
716 let state = StateWithExtensionsOwned::<TokenAccount2022>::unpack(account_data.data)?;
717 Ok(state.base.amount)
718 } else {
719 let token_account = TokenAccount::unpack(&account_data.data)?;
720 Ok(token_account.amount)
721 }
722 }
723
724 async fn verify_increase_liquidity(
725 ctx: &RpcContext,
726 increase_ix: &crate::IncreaseLiquidityInstruction,
727 token_a_account: Pubkey,
728 token_b_account: Pubkey,
729 position_mint: Pubkey,
730 ) -> Result<(), Box<dyn Error>> {
731 let before_a = get_token_balance(&ctx.rpc, token_a_account).await?;
732 let before_b = get_token_balance(&ctx.rpc, token_b_account).await?;
733
734 let signers: Vec<&Keypair> = increase_ix.additional_signers.iter().collect();
735 ctx.send_transaction_with_signers(increase_ix.instructions.clone(), signers).await?;
736
737 let after_a = get_token_balance(&ctx.rpc, token_a_account).await?;
738 let after_b = get_token_balance(&ctx.rpc, token_b_account).await?;
739 let used_a = before_a.saturating_sub(after_a);
740 let used_b = before_b.saturating_sub(after_b);
741
742 let quote = &increase_ix.quote;
743 assert!(
744 used_a >= quote.token_est_a && used_a <= quote.token_max_a,
745 "Token A usage out of range: used={}, est={}..{}",
746 used_a,
747 quote.token_est_a,
748 quote.token_max_a
749 );
750 assert!(
751 used_b >= quote.token_est_b && used_b <= quote.token_max_b,
752 "Token B usage out of range: used={}, est={}..{}",
753 used_b,
754 quote.token_est_b,
755 quote.token_max_b
756 );
757
758 let position_pubkey = get_position_address(&position_mint)?.0;
759 let position_data = fetch_position(&ctx.rpc, position_pubkey).await?;
760 assert_eq!(
761 position_data.liquidity, quote.liquidity_delta,
762 "Position liquidity mismatch! expected={}, got={}",
763 quote.liquidity_delta, position_data.liquidity
764 );
765
766 Ok(())
767 }
768
769 async fn setup_all_mints(ctx: &RpcContext) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
770 let mint_a = setup_mint_with_decimals(ctx, 9).await?;
771 let mint_b = setup_mint_with_decimals(ctx, 9).await?;
772 let mint_te_a = setup_mint_te(ctx, &[]).await?;
773 let mint_te_b = setup_mint_te(ctx, &[]).await?;
774 let mint_te_fee = setup_mint_te_fee(ctx).await?;
775
776 let mut out = HashMap::new();
777 out.insert("A", mint_a);
778 out.insert("B", mint_b);
779 out.insert("TEA", mint_te_a);
780 out.insert("TEB", mint_te_b);
781 out.insert("TEFee", mint_te_fee);
782
783 Ok(out)
784 }
785
786 async fn setup_all_atas(ctx: &RpcContext, minted: &HashMap<&str, Pubkey>) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
787 let token_balance = 1_000_000_000;
788 let user_ata_a = setup_ata_with_amount(ctx, *minted.get("A").unwrap(), token_balance).await?;
789 let user_ata_b = setup_ata_with_amount(ctx, *minted.get("B").unwrap(), token_balance).await?;
790 let user_ata_te_a = setup_ata_te(ctx, *minted.get("TEA").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
791 let user_ata_te_b = setup_ata_te(ctx, *minted.get("TEB").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
792 let user_ata_tefee = setup_ata_te(ctx, *minted.get("TEFee").unwrap(), Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
793
794 let mut out = HashMap::new();
795 out.insert("A", user_ata_a);
796 out.insert("B", user_ata_b);
797 out.insert("TEA", user_ata_te_a);
798 out.insert("TEB", user_ata_te_b);
799 out.insert("TEFee", user_ata_tefee);
800
801 Ok(out)
802 }
803
804 pub fn parse_pool_name(pool_name: &str) -> (&'static str, &'static str) {
805 match pool_name {
806 "A-B" => ("A", "B"),
807 "A-TEA" => ("A", "TEA"),
808 "TEA-TEB" => ("TEA", "TEB"),
809 "A-TEFee" => ("A", "TEFee"),
810
811 _ => panic!("Unknown pool name: {}", pool_name),
812 }
813 }
814
815 #[rstest]
816 #[case("A-B", "equally centered", -100, 100)]
817 #[case("A-B", "one sided A", -100, -1)]
818 #[case("A-B", "one sided B", 1, 100)]
819 #[case("A-TEA", "equally centered", -100, 100)]
820 #[case("A-TEA", "one sided A", -100, -1)]
821 #[case("A-TEA", "one sided B", 1, 100)]
822 #[case("TEA-TEB", "equally centered", -100, 100)]
823 #[case("TEA-TEB", "one sided A", -100, -1)]
824 #[case("TEA-TEB", "one sided B", 1, 100)]
825 #[case("A-TEFee", "equally centered", -100, 100)]
826 #[case("A-TEFee", "one sided A", -100, -1)]
827 #[case("A-TEFee", "one sided B", 1, 100)]
828 #[serial]
829 fn test_increase_liquidity_cases(#[case] pool_name: &str, #[case] _position_name: &str, #[case] lower_tick: i32, #[case] upper_tick: i32) {
830 let rt = tokio::runtime::Runtime::new().unwrap();
831 rt.block_on(async {
832 let ctx = RpcContext::new().await;
833
834 let minted = setup_all_mints(&ctx).await.unwrap();
835 let user_atas = setup_all_atas(&ctx, &minted).await.unwrap();
836
837 let (mint_a_key, mint_b_key) = parse_pool_name(pool_name);
838 let pubkey_a = *minted.get(mint_a_key).unwrap();
839 let pubkey_b = *minted.get(mint_b_key).unwrap();
840
841 let (final_a, final_b) = if pubkey_a < pubkey_b {
842 (pubkey_a, pubkey_b)
843 } else {
844 (pubkey_b, pubkey_a)
845 };
846
847 let tick_spacing = 64;
849 let fee_rate = 300;
850 let swapped = pubkey_a > pubkey_b;
851 let pool_pubkey = setup_fusion_pool(&ctx, final_a, final_b, tick_spacing, fee_rate).await.unwrap();
852 let user_ata_for_token_a = if swapped {
853 user_atas.get(mint_b_key).unwrap()
854 } else {
855 user_atas.get(mint_a_key).unwrap()
856 };
857 let user_ata_for_token_b = if swapped {
858 user_atas.get(mint_a_key).unwrap()
859 } else {
860 user_atas.get(mint_b_key).unwrap()
861 };
862
863 let position_mint = setup_position(&ctx, pool_pubkey, Some((lower_tick, upper_tick)), None).await.unwrap();
864
865 let param = IncreaseLiquidityParam::Liquidity(10_000);
866 let inc_ix = increase_liquidity_instructions(
867 &ctx.rpc,
868 position_mint,
869 param,
870 Some(100), Some(ctx.signer.pubkey()),
872 )
873 .await
874 .unwrap();
875
876 verify_increase_liquidity(&ctx, &inc_ix, *user_ata_for_token_a, *user_ata_for_token_b, position_mint)
877 .await
878 .unwrap();
879 });
880 }
881
882 #[tokio::test]
883 #[serial]
884 async fn test_increase_liquidity_fails_if_authority_is_default() -> Result<(), Box<dyn Error>> {
885 let ctx = RpcContext::new().await;
886
887 let minted = setup_all_mints(&ctx).await?;
888 let _user_atas = setup_all_atas(&ctx, &minted).await?;
889
890 let mint_a_key = minted.get("A").unwrap();
891 let mint_b_key = minted.get("B").unwrap();
892 let pool_pubkey = setup_fusion_pool(&ctx, *mint_a_key, *mint_b_key, 64, 300).await?;
893
894 let position_mint = setup_position(&ctx, pool_pubkey, Some((-100, 100)), None).await?;
895
896 let param = IncreaseLiquidityParam::Liquidity(100_000);
897 let res = increase_liquidity_instructions(
898 &ctx.rpc,
899 position_mint,
900 param,
901 Some(100), Some(Pubkey::default()),
903 )
904 .await;
905
906 assert!(res.is_err(), "Should have failed with default authority");
907 let err_str = format!("{:?}", res.err().unwrap());
908 assert!(
909 err_str.contains("Authority must be provided") || err_str.contains("Signer must be provided"),
910 "Error string was: {}",
911 err_str
912 );
913
914 Ok(())
915 }
916
917 #[tokio::test]
918 #[serial]
919 async fn test_increase_liquidity_fails_if_deposit_exceeds_user_balance() -> Result<(), Box<dyn Error>> {
920 let ctx = RpcContext::new().await;
921
922 let minted = setup_all_mints(&ctx).await?;
923 let _user_atas = setup_all_atas(&ctx, &minted).await?;
924
925 let mint_a_key = minted.get("A").unwrap();
926 let mint_b_key = minted.get("B").unwrap();
927 let pool_pubkey = setup_fusion_pool(&ctx, *mint_a_key, *mint_b_key, 64, 300).await?;
928
929 let position_mint = setup_position(&ctx, pool_pubkey, Some((-100, 100)), None).await?;
930
931 let res = increase_liquidity_instructions(
933 &ctx.rpc,
934 position_mint,
935 IncreaseLiquidityParam::TokenA(2_000_000_000),
936 Some(100),
937 Some(ctx.signer.pubkey()),
938 )
939 .await;
940
941 assert!(res.is_err(), "Should fail if user tries depositing more than balance");
942 let err_str = format!("{:?}", res.err().unwrap());
943 assert!(
944 err_str.contains("Insufficient balance") || err_str.contains("Error processing Instruction 0"),
945 "Unexpected error message: {}",
946 err_str
947 );
948
949 Ok(())
950 }
951}