Skip to main content

fusionamm_sdk/
limit_order.rs

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    /// The public key of the limit order NFT that represents ownership of the newly opened order.
26    pub limit_order_mint: Pubkey,
27
28    /// A vector of `Instruction` objects required to execute the limit order opening.
29    pub instructions: Vec<Instruction>,
30
31    /// A vector of `Keypair` objects representing additional signers required for the instructions.
32    pub additional_signers: Vec<Keypair>,
33
34    /// The amount of required token A
35    pub quote_a: u64,
36
37    /// The amount of required token B
38    pub quote_b: u64,
39
40    /// The cost of initializing the limit order, measured in lamports.
41    pub initialization_cost: u64,
42}
43
44#[derive(Debug)]
45pub struct IncreaseLimitOrderInstruction {
46    /// A vector of `Instruction` objects required to execute the limit order increasing.
47    pub instructions: Vec<Instruction>,
48
49    /// The amount of required token A
50    pub quote_a: u64,
51
52    /// The amount of required token B
53    pub quote_b: u64,
54
55    /// A vector of `Keypair` objects representing additional signers required for the instructions.
56    pub additional_signers: Vec<Keypair>,
57}
58
59#[derive(Debug)]
60pub struct DecreaseLimitOrderInstruction {
61    /// A vector of `Instruction` objects required to execute the limit order decreasing or closing.
62    pub instructions: Vec<Instruction>,
63
64    /// The limit order decrease quote.
65    pub quote: LimitOrderDecreaseQuote,
66
67    /// A vector of `Keypair` objects representing additional signers required for the instructions.
68    pub additional_signers: Vec<Keypair>,
69}
70
71#[cfg(not(doctest))]
72/// Opens a limit order in a liquidity pool at a specific tick index.
73///
74/// # Arguments
75///
76/// * `rpc` - A reference to the Solana RPC client.
77/// * `pool_address` - The public key of the liquidity pool.
78/// * `amount` - The limit order input token amount.
79/// * `price_or_tick_index` - The price or tick index for the limit order.
80/// * `a_to_b` - The limit order swap direction.
81/// * `funder` - An optional public key of the funder account. Defaults to the global funder if not provided.
82///
83/// # Returns
84///
85/// Returns a `Result` containing an `OpenLimitOrderInstruction` on success, which includes:
86/// * `limit_order_mint` - The mint address of the limit order NFT.
87/// * `instructions` - A vector of `Instruction` objects required for opening the limit order.
88/// * `additional_signers` - A vector of `Keypair` objects for additional transaction signers.
89/// * `initialization_cost` - The cost of initializing the limit order, in lamports.
90///
91/// # Errors
92///
93/// Returns an error if:
94/// - The funder account is invalid.
95/// - The pool account is not found or invalid.
96/// - Any RPC request fails.
97///
98/// # Example
99///
100/// ```rust
101/// use fusionamm_sdk::{open_limit_order_instructions, PriceOrTickIndex};
102/// use solana_client::nonblocking::rpc_client::RpcClient;
103/// use solana_pubkey::pubkey;
104/// use solana_keypair::Keypair;
105/// use solana_signer::Signer;
106///
107/// #[tokio::main]
108/// async fn main() {
109///     let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
110///     let wallet = Keypair::new(); // Load your wallet here
111///
112///     let fusion_pool_pubkey = pubkey!("7VuKeevbvbQQcxz6N4SNLmuq6PYy4AcGQRDssoqo4t65");
113///     let amount = 1_000_000;
114///     let limit_order_price = 100.5;
115///     let a_to_b = true;
116///
117///     let funder = Some(wallet.pubkey());
118///
119///     let result = open_limit_order_instructions(
120///         &rpc,
121///         fusion_pool_pubkey,
122///         amount,
123///         PriceOrTickIndex::Price(limit_order_price),
124///         a_to_b,
125///         funder,
126///     )
127///     .await
128///     .unwrap();
129///
130///     println!("Limit Order Mint: {:?}", result.limit_order_mint);
131///     println!("Initialization Cost: {} lamports", result.initialization_cost);
132/// }
133/// ```
134pub 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    // Use 'unpack_from_slice' instead of 'unpack' because the account length might be larger than Mint::LEN due to extensions.
154    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
278/// Increases a limit order.
279/// The limit order can't be increased if it's partially filled.
280///
281/// # Arguments
282///
283/// * `rpc` - A reference to the Solana RPC client.
284/// * `limit_order_mint` - The public key of the NFT mint address representing the limit order to be increased.
285/// * `amount` - The amount of input tokens by which to increase the limit order.
286/// * `authority` - An optional public key of the account authorizing the transaction. Defaults to the global funder if not provided.
287///
288/// # Returns
289///
290/// Returns a `Result` containing an `OpenLimitOrderInstruction` on success, which includes:
291/// * `instructions` - A vector of `Instruction` objects required for increasing the limit order.
292/// * `additional_signers` - A vector of `Keypair` objects for additional transaction signers.
293///
294/// # Errors
295///
296/// Returns an error if:
297/// - The funder account is invalid.
298/// - The pool or token mint accounts are not found or invalid.
299/// - Any RPC request fails.
300pub 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))]
387/// Generates instructions to close a limit order.
388///
389/// This function removes any remaining liquidity, and closes the limit order.
390///
391/// # Arguments
392///
393/// * `rpc` - A reference to a Solana RPC client for fetching accounts and pool data.
394/// * `limit_order_mint` - The public key of the NFT mint address representing the limit order to be closed.
395/// * `authority` - An optional public key of the account authorizing the transaction. Defaults to the global funder if not provided.
396///
397/// # Returns
398///
399/// A `Result` containing `DecreaseLimitOrderInstruction` on success:
400///
401/// * `instructions` - A vector of `Instruction` objects required to execute the limit order closure.
402/// * `additional_signers` - A vector of `Keypair` objects representing additional signers required for the instructions.
403///
404/// # Errors
405///
406/// This function will return an error if:
407/// - The `authority` account is invalid or missing.
408/// - The limit order account is not found or have invalid data.
409/// - Any RPC request to the blockchain fails.
410///
411/// # Example
412///
413/// ```rust
414/// use fusionamm_sdk::close_limit_order_instructions;
415/// use solana_client::nonblocking::rpc_client::RpcClient;
416/// use solana_pubkey::pubkey;
417/// use solana_keypair::Keypair;
418/// use solana_signer::Signer;
419///
420/// #[tokio::main]
421/// async fn main() {
422///     let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
423///     let wallet = Keypair::new(); // Load your wallet here
424///
425///     let limit_order_mint_address = pubkey!("HqoV7Qv27REUtmd9UKSJGGmCRNx3531t33bDG1BUfo9K");
426///     let authority = Some(wallet.pubkey());
427///
428///     let result = close_limit_order_instructions(
429///         &rpc,
430///         limit_order_mint_address,
431///         authority,
432///     )
433///     .await
434///     .unwrap();
435///
436///     println!("Number of Instructions: {}", result.instructions.len());
437/// }
438/// ```
439pub 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))]
448/// Generates instructions to decrease a limit order.
449///
450/// Decrease the existing limit order in a concentrated liquidity pool.
451/// Both input and output tokens are removed proportionally.
452///
453/// # Arguments
454///
455/// * `rpc` - A reference to a Solana RPC client for fetching accounts and pool data.
456/// * `amount` - The share by which the limit order needs to be reduced.
457/// * `limit_order_mint` - The public key of the NFT mint address representing the limit order to be decreased.
458/// * `authority` - An optional public key of the account authorizing the transaction. Defaults to the global funder if not provided.
459///
460/// # Returns
461///
462/// A `Result` containing `DecreaseLimitOrderInstruction` on success:
463///
464/// * `instructions` - A vector of `Instruction` objects required to execute the limit order closure.
465/// * `additional_signers` - A vector of `Keypair` objects representing additional signers required for the instructions.
466///
467/// # Errors
468///
469/// This function will return an error if:
470/// - The `authority` account is invalid or missing.
471/// - The limit order account is not found or have invalid data.
472/// - Any RPC request to the blockchain fails.
473///
474/// # Example
475///
476/// ```rust
477/// use fusionamm_sdk::decrease_limit_order_instructions;
478/// use solana_client::nonblocking::rpc_client::RpcClient;
479/// use solana_pubkey::pubkey;
480/// use solana_keypair::Keypair;
481/// use solana_signer::Signer;
482///
483/// #[tokio::main]
484/// async fn main() {
485///     let rpc = RpcClient::new("https://api.mainnet.solana.com".to_string());
486///     let wallet = Keypair::new(); // Load your wallet here
487///
488///     let limit_order_mint_address = pubkey!("HqoV7Qv27REUtmd9UKSJGGmCRNx3531t33bDG1BUfo9K");
489///     let authority = Some(wallet.pubkey());
490///     let amount = 1_000_000;
491///
492///     let result = decrease_limit_order_instructions(
493///         &rpc,
494///         limit_order_mint_address,
495///         amount,
496///         authority,
497///     )
498///     .await
499///     .unwrap();
500///
501///     println!("Number of Instructions: {}", result.instructions.len());
502/// }
503/// ```
504pub 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            // prevent flaky test by ordering the tokens correctly by lexical order
878            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            // Open
897            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            // Increase
912            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            // Partially decrease
920            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            // Decrease and close
928            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}