Skip to main content

fusionamm_sdk/
increase_liquidity.rs

1//
2// Copyright (c) Cryptic Dot
3//
4// Modification based on Orca Whirlpools (https://github.com/orca-so/whirlpools),
5// originally licensed under the Apache License, Version 2.0, prior to February 26, 2025.
6//
7// Modifications licensed under FusionAMM SDK Source-Available License v1.0
8// See the LICENSE file in the project root for license information.
9//
10
11use 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
41// TODO: support transfer hooks
42
43fn 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/// Represents the parameters for increasing liquidity in a position.
85///
86/// You must choose one of the variants (`TokenA`, `TokenB`, or `Liquidity`).
87/// The SDK will calculate the remaining values based on the provided input.
88#[derive(Debug, Clone)]
89pub enum IncreaseLiquidityParam {
90    /// Specifies the amount of token A to add to the position.
91    TokenA(u64),
92
93    /// Specifies the amount of token B to add to the position.
94    TokenB(u64),
95
96    /// Specifies the amount of liquidity to add to the position.
97    Liquidity(u128),
98}
99
100/// Represents the instructions and quote for increasing liquidity in a position.
101///
102/// This struct includes the necessary transaction instructions, as well as a detailed
103/// quote describing the liquidity increase.
104#[derive(Debug)]
105pub struct IncreaseLiquidityInstruction {
106    /// The computed quote for increasing liquidity, including:
107    /// - `liquidity_delta` - The change in liquidity.
108    /// - `token_est_a` - The estimated amount of token A required.
109    /// - `token_est_b` - The estimated amount of token B required.
110    /// - `token_max_a` - The maximum allowable amount of token A based on slippage tolerance.
111    /// - `token_max_b` - The maximum allowable amount of token B based on slippage tolerance.
112    pub quote: IncreaseLiquidityQuote,
113
114    /// A vector of `Instruction` objects required to execute the liquidity increase.
115    pub instructions: Vec<Instruction>,
116
117    /// A vector of `Keypair` objects representing additional signers required for the instructions.
118    pub additional_signers: Vec<Keypair>,
119}
120
121#[cfg(not(doctest))]
122/// Generates instructions to increase liquidity for an existing position.
123///
124/// This function computes the necessary quote and creates instructions to add liquidity
125/// to an existing pool position, specified by the position's mint address.
126///
127/// # Arguments
128///
129/// * `rpc` - A reference to a Solana RPC client for fetching necessary accounts and pool data.
130/// * `position_mint_address` - The public key of the NFT mint address representing the pool position.
131/// * `param` - A variant of `IncreaseLiquidityParam` specifying the liquidity addition method (by Token A, Token B, or liquidity amount).
132/// * `slippage_tolerance_bps` - An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
133/// * `authority` - An optional public key of the account authorizing the liquidity addition. Defaults to the global funder if not provided.
134///
135/// # Returns
136///
137/// A `Result` containing `IncreaseLiquidityInstruction` on success:
138///
139/// * `quote` - The computed quote for increasing liquidity, including liquidity delta, token estimates, and maximum tokens based on slippage tolerance.
140/// * `instructions` - A vector of `Instruction` objects required to execute the liquidity addition.
141/// * `additional_signers` - A vector of `Keypair` objects representing additional signers required for the instructions.
142///
143/// # Errors
144///
145/// This function will return an error if:
146/// - The `authority` account is invalid or missing.
147/// - The position or token mint accounts are not found or have invalid data.
148/// - Any RPC request to the blockchain fails.
149///
150/// # Example
151///
152/// ```rust
153/// use fusionamm_sdk::{ increase_liquidity_instructions, IncreaseLiquidityParam };
154/// use solana_client::nonblocking::rpc_client::RpcClient;
155/// use solana_pubkey::pubkey;
156/// use solana_keypair::Keypair;
157/// use solana_signer::Signer;
158///
159/// #[tokio::main]
160/// async fn main() {
161/// let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
162///     let wallet = Keypair::new(); // Load your wallet here
163///
164///     let position_mint_address = pubkey!("HqoV7Qv27REUtmd9UKSJGGmCRNx3531t33bDG1BUfo9K");
165///     let param = IncreaseLiquidityParam::TokenA(1_000_000);
166///
167///     let result = increase_liquidity_instructions(
168///         &rpc,
169///         position_mint_address,
170///         param,
171///         Some(100),
172///         Some(wallet.pubkey()),
173///     )
174///     .await
175///     .unwrap();
176///
177///     println!("Liquidity Increase Quote: {:?}", result.quote);
178///     println!("Number of Instructions: {}", result.instructions.len());
179/// }
180/// ```
181pub 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/// Represents the instructions and quote for opening a liquidity position.
289///
290/// This struct contains the instructions required to open a new position, along with detailed
291/// information about the liquidity increase, the cost of initialization, and the mint address
292/// of the position NFT.
293#[derive(Debug)]
294pub struct OpenPositionInstruction {
295    /// The public key of the position NFT that represents ownership of the newly opened position.
296    pub position_mint: Pubkey,
297
298    /// The computed quote for increasing liquidity, including liquidity delta, token estimates,
299    /// and maximum tokens based on slippage tolerance.
300    pub quote: IncreaseLiquidityQuote,
301
302    /// A vector of `Instruction` objects required to execute the position opening.
303    pub instructions: Vec<Instruction>,
304
305    /// A vector of `Keypair` objects representing additional signers required for the instructions.
306    pub additional_signers: Vec<Keypair>,
307
308    /// The cost of initializing the position, measured in lamports.
309    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))]
479/// Opens a full-range position in a liquidity pool.
480///
481/// This function creates a new position within the full price range for the specified pool,
482/// which is ideal for full-range liquidity provisioning.
483///
484/// # Arguments
485///
486/// * `rpc` - A reference to the Solana RPC client.
487/// * `pool_address` - The public key of the liquidity pool.
488/// * `param` - Parameters for increasing liquidity, specified as `IncreaseLiquidityParam`.
489/// * `slippage_tolerance_bps` - An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
490/// * `funder` - An optional public key of the funder account. Defaults to the global funder if not provided.
491///
492/// # Returns
493///
494/// Returns a `Result` containing an `OpenPositionInstruction` on success, which includes:
495/// * `position_mint` - The mint address of the position NFT.
496/// * `quote` - The computed liquidity quote, including liquidity delta, token estimates, and maximum tokens.
497/// * `instructions` - A vector of `Instruction` objects required for creating the position.
498/// * `additional_signers` - A vector of `Keypair` objects for additional transaction signers.
499/// * `initialization_cost` - The cost of initializing the position, in lamports.
500///
501/// # Errors
502///
503/// Returns an error if:
504/// - The funder account is invalid.
505/// - The pool or token mint accounts are not found or invalid.
506/// - Any RPC request fails.
507///
508/// # Example
509///
510/// ```rust
511/// use solana_client::nonblocking::rpc_client::RpcClient;
512/// use fusionamm_sdk::{open_full_range_position_instructions, IncreaseLiquidityParam};
513/// use solana_pubkey::pubkey;
514/// use solana_keypair::Keypair;
515/// use solana_signer::Signer;
516///
517/// #[tokio::main]
518/// async fn main() {
519/// let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
520///     let wallet = Keypair::new(); // Load your wallet here
521///
522///     let fusion_pool_pubkey = pubkey!("7VuKeevbvbQQcxz6N4SNLmuq6PYy4AcGQRDssoqo4t65");
523///     let param = IncreaseLiquidityParam::TokenA(1_000_000);
524///     let slippage_tolerance_bps = Some(100);
525///     let funder = Some(wallet.pubkey());
526///
527///     let result = open_full_range_position_instructions(
528///         &rpc,
529///         fusion_pool_pubkey,
530///         param,
531///         slippage_tolerance_bps,
532///         funder,
533///     ).await.unwrap();
534///
535///     println!("Position Mint: {:?}", result.position_mint);
536///     println!("Initialization Cost: {} lamports", result.initialization_cost);
537/// }
538/// ```
539pub 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))]
568/// Opens a position in a liquidity pool within a specific price range.
569///
570/// This function creates a new position in the specified price range for a given pool.
571/// It allows for providing liquidity in a targeted range, optimizing capital efficiency.
572///
573/// # Arguments
574///
575/// * `rpc` - A reference to the Solana RPC client.
576/// * `pool_address` - The public key of the liquidity pool.
577/// * `lower_price` - The lower bound of the price range for the position.
578/// * `upper_price` - The upper bound of the price range for the position.
579/// * `param` - Parameters for increasing liquidity, specified as `IncreaseLiquidityParam`.
580/// * `slippage_tolerance_bps` - An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
581/// * `funder` - An optional public key of the funder account. Defaults to the global funder if not provided.
582///
583/// # Returns
584///
585/// Returns a `Result` containing an `OpenPositionInstruction` on success, which includes:
586/// * `position_mint` - The mint address of the position NFT.
587/// * `quote` - The computed liquidity quote, including liquidity delta, token estimates, and maximum tokens.
588/// * `instructions` - A vector of `Instruction` objects required for creating the position.
589/// * `additional_signers` - A vector of `Keypair` objects for additional transaction signers.
590/// * `initialization_cost` - The cost of initializing the position, in lamports.
591///
592/// # Errors
593///
594/// Returns an error if:
595/// - The funder account is invalid.
596/// - The pool or token mint accounts are not found or invalid.
597/// - Any RPC request fails.
598///
599/// # Example
600///
601/// ```rust
602/// use fusionamm_sdk::{open_position_instructions, IncreaseLiquidityParam, PriceOrTickIndex};
603/// use solana_client::nonblocking::rpc_client::RpcClient;
604/// use solana_pubkey::pubkey;
605/// use solana_keypair::Keypair;
606/// use solana_signer::Signer;
607///
608/// #[tokio::main]
609/// async fn main() {
610///     let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
611///     let wallet = Keypair::new(); // Load your wallet here
612///
613///     let fusion_pool_pubkey = pubkey!("7VuKeevbvbQQcxz6N4SNLmuq6PYy4AcGQRDssoqo4t65");
614///     let lower_price = 0.00005;
615///     let upper_price = 0.00015;
616///     let param = IncreaseLiquidityParam::TokenA(1_000_000);
617///     let slippage_tolerance_bps = Some(100);
618///     let funder = Some(wallet.pubkey());
619///
620///     let result = open_position_instructions(
621///         &rpc,
622///         fusion_pool_pubkey,
623///         PriceOrTickIndex::Price(lower_price),
624///         PriceOrTickIndex::Price(upper_price),
625///         param,
626///         slippage_tolerance_bps,
627///         funder,
628///     ).await.unwrap();
629///
630///     println!("Position Mint: {:?}", result.position_mint);
631///     println!("Initialization Cost: {} lamports", result.initialization_cost);
632/// }
633/// ```
634pub 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            // prevent flaky test by ordering the tokens correctly by lexical order
848            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), // slippage
871                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), // slippage
902            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        // Attempt
932        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}