defituna_core/quote/
tuna_spot_position.rs

1#![allow(clippy::collapsible_else_if)]
2#![allow(clippy::too_many_arguments)]
3
4#[cfg(feature = "wasm")]
5use fusionamm_macros::wasm_expose;
6#[cfg(feature = "wasm")]
7use serde::Serialize;
8#[cfg(feature = "wasm")]
9use serde_wasm_bindgen::Serializer;
10#[cfg(feature = "wasm")]
11use wasm_bindgen::prelude::wasm_bindgen;
12#[cfg(feature = "wasm")]
13use wasm_bindgen::JsValue;
14
15use crate::utils::fees;
16use crate::{
17    calculate_tuna_protocol_fee, HUNDRED_PERCENT, INVALID_ARGUMENTS, JUPITER_QUOTE_REQUEST_ERROR, JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR, TOKEN_A,
18    TOKEN_B,
19};
20use fusionamm_core::{
21    sqrt_price_to_price, swap_quote_by_input_token, swap_quote_by_output_token, try_get_max_amount_with_slippage_tolerance,
22    try_get_min_amount_with_slippage_tolerance, try_mul_div, CoreError, FusionPoolFacade, TickArrays, TokenPair,
23};
24use jup_ag::{PrioritizationFeeLamports, QuoteConfig, SwapMode, SwapRequest};
25use libm::{ceil, round};
26use solana_instruction::AccountMeta;
27use solana_pubkey::Pubkey;
28
29pub const DEFAULT_SLIPPAGE_TOLERANCE_BPS: u16 = 100;
30
31#[cfg_attr(feature = "wasm", wasm_expose)]
32pub struct SwapInstruction {
33    pub data: Vec<u8>,
34    pub accounts: Vec<AccountMeta>,
35    pub address_lookup_table_addresses: Vec<Pubkey>,
36}
37
38#[cfg_attr(feature = "wasm", wasm_expose)]
39pub struct IncreaseSpotPositionQuoteResult {
40    /** Required collateral amount */
41    pub collateral: u64,
42    /** Required amount to borrow */
43    pub borrow: u64,
44    /** Estimated position size in the position token. */
45    pub estimated_amount: u64,
46    /** Swap input amount. */
47    pub swap_input_amount: u64,
48    /** Minimum swap output amount according to the provided slippage. */
49    pub min_swap_output_amount: u64,
50    /** Protocol fee in token A */
51    pub protocol_fee_a: u64,
52    /** Protocol fee in token B */
53    pub protocol_fee_b: u64,
54    /** Price impact in percents (100% = 1.0) */
55    pub price_impact: f64,
56    /** Optional jupiter swap instruction data and accounts. */
57    pub jupiter_swap_ix: Option<SwapInstruction>,
58}
59
60/// Spot position increase quote
61///
62/// # Parameters
63/// - `increase_amount`: Position total size in the collateral_token.
64/// - `collateral_token`: Collateral token.
65/// - `position_token`: Token of the position.
66/// - `leverage`: Leverage (1.0 or higher).
67/// - `slippage_tolerance_bps`: An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
68/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
69/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
70/// - `mint_a`: Token A mint address
71/// - `mint_b`: Token B mint address
72/// - `fusion_pool`: Fusion pool.
73/// - `tick_arrays`: Optional five tick arrays around the current pool price. If not provided, the quote will be calculated using the Jupiter Aggregator.
74///
75/// # Returns
76/// - `IncreaseSpotPositionQuoteResult`: quote result
77pub async fn get_increase_spot_position_quote(
78    increase_amount: u64,
79    collateral_token: u8,
80    position_token: u8,
81    leverage: f64,
82    slippage_tolerance_bps: Option<u16>,
83    protocol_fee_rate: u16,
84    protocol_fee_rate_on_collateral: u16,
85    mint_a: Pubkey,
86    mint_b: Pubkey,
87    fusion_pool: FusionPoolFacade,
88    tick_arrays: Option<TickArrays>,
89) -> Result<IncreaseSpotPositionQuoteResult, CoreError> {
90    if collateral_token > TOKEN_B || position_token > TOKEN_B {
91        return Err(INVALID_ARGUMENTS.into());
92    }
93
94    if leverage < 1.0 {
95        return Err(INVALID_ARGUMENTS.into());
96    }
97
98    let borrow: u64;
99    let mut collateral: u64;
100    let mut estimated_amount: u64 = 0;
101    let mut swap_input_amount: u64;
102    let mut min_swap_output_amount: u64 = 0;
103    let mut price_impact: f64 = 0.0;
104    let mut jupiter_swap_ix: Option<SwapInstruction> = None;
105
106    let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
107    let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(DEFAULT_SLIPPAGE_TOLERANCE_BPS);
108
109    let borrowed_token = if position_token == TOKEN_A { TOKEN_B } else { TOKEN_A };
110    let swap_input_token_is_a = borrowed_token == TOKEN_A;
111
112    if borrowed_token == collateral_token {
113        borrow = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage) as u64;
114        collateral =
115            increase_amount - fees::apply_swap_fee(fees::apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
116        collateral = fees::reverse_apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
117        collateral = fees::reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
118
119        swap_input_amount = collateral + borrow;
120    } else {
121        let position_to_borrowed_token_price = if collateral_token == TOKEN_A { price } else { 1.0 / price };
122        let borrow_in_position_token = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage);
123
124        borrow = ceil(borrow_in_position_token * position_to_borrowed_token_price) as u64;
125
126        let borrow_in_position_token_with_fees_applied = fees::apply_swap_fee(
127            fees::apply_tuna_protocol_fee(borrow_in_position_token as u64, protocol_fee_rate, false)?,
128            fusion_pool.fee_rate,
129            false,
130        )?;
131
132        collateral = increase_amount - borrow_in_position_token_with_fees_applied;
133        collateral = fees::reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
134
135        swap_input_amount = borrow;
136    }
137
138    let protocol_fee = calculate_tuna_spot_position_protocol_fee(
139        collateral_token,
140        borrowed_token,
141        collateral,
142        borrow,
143        protocol_fee_rate_on_collateral,
144        protocol_fee_rate,
145    );
146
147    swap_input_amount -= if swap_input_token_is_a { protocol_fee.a } else { protocol_fee.b };
148
149    if position_token == collateral_token {
150        estimated_amount = collateral - if collateral_token == TOKEN_A { protocol_fee.a } else { protocol_fee.b };
151    }
152
153    if swap_input_amount > 0 {
154        if let Some(tick_arrays) = tick_arrays {
155            let quote = swap_quote_by_input_token(swap_input_amount, swap_input_token_is_a, 0, fusion_pool, tick_arrays, None, None)?;
156            estimated_amount += quote.token_est_out;
157            min_swap_output_amount = try_get_min_amount_with_slippage_tolerance(quote.token_est_out, slippage_tolerance_bps)?;
158            let new_price = sqrt_price_to_price(quote.next_sqrt_price.into(), 1, 1);
159            price_impact = (new_price / price - 1.0).abs();
160        } else {
161            let (input_mint, output_mint) = if swap_input_token_is_a { (mint_a, mint_b) } else { (mint_b, mint_a) };
162
163            let quote = jupiter_swap_quote(input_mint, output_mint, swap_input_amount, Some(slippage_tolerance_bps as u64)).await?;
164
165            estimated_amount += quote.out_amount;
166            price_impact = quote.price_impact_pct;
167            min_swap_output_amount = quote.other_amount_threshold;
168            jupiter_swap_ix = Some(SwapInstruction {
169                data: quote.instruction.data,
170                accounts: quote.instruction.accounts,
171                address_lookup_table_addresses: quote.instruction.address_lookup_table_addresses,
172            });
173        }
174    }
175
176    Ok(IncreaseSpotPositionQuoteResult {
177        collateral,
178        borrow,
179        estimated_amount,
180        swap_input_amount,
181        min_swap_output_amount,
182        protocol_fee_a: protocol_fee.a,
183        protocol_fee_b: protocol_fee.b,
184        price_impact,
185        jupiter_swap_ix,
186    })
187}
188
189/// Spot position increase quote
190///
191/// # Parameters
192/// - `increase_amount`: Position total size in the collateral_token.
193/// - `collateral_token`: Collateral token.
194/// - `position_token`: Token of the position.
195/// - `leverage`: Leverage (1.0 or higher).
196/// - `slippage_tolerance_bps`: An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
197/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
198/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
199/// - `mint_a`: Token A mint address
200/// - `mint_b`: Token B mint address
201/// - `fusion_pool`: Fusion pool.
202/// - `tick_arrays`: Optional five tick arrays around the current pool price. If not provided, the quote will be calculated using the Jupiter Aggregator.
203///
204/// # Returns
205/// - `IncreaseSpotPositionQuoteResult`: quote result
206#[cfg(feature = "wasm")]
207#[wasm_bindgen(js_name = "getIncreaseSpotPositionQuote", skip_jsdoc)]
208pub async fn wasm_get_increase_spot_position_quote(
209    increase_amount: u64,
210    collateral_token: u8,
211    position_token: u8,
212    leverage: f64,
213    slippage_tolerance_bps: Option<u16>,
214    protocol_fee_rate: u16,
215    protocol_fee_rate_on_collateral: u16,
216    mint_a: Pubkey,
217    mint_b: Pubkey,
218    fusion_pool: FusionPoolFacade,
219    tick_arrays: Option<TickArrays>,
220) -> Result<JsValue, JsValue> {
221    let result = get_increase_spot_position_quote(
222        increase_amount,
223        collateral_token,
224        position_token,
225        leverage,
226        slippage_tolerance_bps,
227        protocol_fee_rate,
228        protocol_fee_rate_on_collateral,
229        mint_a,
230        mint_b,
231        fusion_pool,
232        tick_arrays,
233    )
234    .await
235    .map_err(|e| JsValue::from_str(e))?;
236
237    let serializer = Serializer::new().serialize_maps_as_objects(true);
238    let js_value = result.serialize(&serializer).unwrap();
239
240    Ok(js_value)
241}
242
243#[cfg_attr(feature = "wasm", wasm_expose)]
244pub struct DecreaseSpotPositionQuoteResult {
245    /** Position decrease percentage */
246    pub decrease_percent: u32,
247    /** The maximum acceptable swap input amount for position decrease according to the provided slippage
248     * (if collateral_token == position_token) OR the minimum swap output amount (if collateral_token != position_token).
249     */
250    pub required_swap_amount: u64,
251    /** Estimated total amount of the adjusted position. */
252    pub estimated_amount: u64,
253    /** Estimated value of a debt that will be repaid. */
254    pub estimated_payable_debt: u64,
255    /** Estimated collateral that will be withdrawn from the position. */
256    pub estimated_collateral_to_be_withdrawn: u64,
257    /** Price impact in percents (100% = 1.0) */
258    pub price_impact: f64,
259    /** Optional jupiter swap instruction data and accounts. */
260    pub jupiter_swap_ix: Option<SwapInstruction>,
261}
262
263/// Spot position decrease quote
264///
265/// # Parameters
266/// - `decrease_amount`: Position total decrease size in the collateral_token.
267/// - `collateral_token`: Collateral token.
268/// - `leverage`: Leverage (1.0 or higher).
269/// - `slippage_tolerance_bps`: An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
270/// - `position_token`: Token of the existing position.
271/// - `position_amount`: Existing position amount in the position_token.
272/// - `position_debt`: Existing position debt in the token opposite to the position_token.
273/// - `fusion_pool`: Fusion pool.
274/// - `tick_arrays`: Optional five tick arrays around the current pool price. If not provided, the quote will be calculated using the Jupiter Aggregator.
275///
276/// # Returns
277/// - `DecreaseSpotPositionQuoteResult`: quote result
278pub async fn get_decrease_spot_position_quote(
279    decrease_amount: u64,
280    collateral_token: u8,
281    leverage: f64,
282    slippage_tolerance_bps: Option<u16>,
283    position_token: u8,
284    position_amount: u64,
285    position_debt: u64,
286    mint_a: Pubkey,
287    mint_b: Pubkey,
288    fusion_pool: FusionPoolFacade,
289    tick_arrays: Option<TickArrays>,
290) -> Result<DecreaseSpotPositionQuoteResult, CoreError> {
291    if collateral_token > TOKEN_B || position_token > TOKEN_B {
292        return Err(INVALID_ARGUMENTS.into());
293    }
294
295    if leverage < 1.0 {
296        return Err(INVALID_ARGUMENTS.into());
297    }
298
299    let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
300    let position_to_borrowed_token_price = if position_token == TOKEN_A { price } else { 1.0 / price };
301    let borrowed_token = if position_token == TOKEN_A { TOKEN_B } else { TOKEN_A };
302    let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(DEFAULT_SLIPPAGE_TOLERANCE_BPS);
303
304    let mut required_swap_amount: u64 = 0;
305    let mut price_impact = 0.0;
306    let mut jupiter_swap_ix: Option<SwapInstruction> = None;
307
308    let mut decrease_amount_in_position_token = if collateral_token == position_token {
309        decrease_amount
310    } else {
311        round(decrease_amount as f64 / position_to_borrowed_token_price) as u64
312    };
313
314    decrease_amount_in_position_token = position_amount.min(decrease_amount_in_position_token);
315
316    let decrease_percent = ((decrease_amount_in_position_token * HUNDRED_PERCENT as u64 / position_amount) as u32).min(HUNDRED_PERCENT);
317
318    let estimated_amount = position_amount * (HUNDRED_PERCENT - decrease_percent) as u64 / HUNDRED_PERCENT as u64;
319    let estimated_payable_debt = try_mul_div(position_debt, decrease_percent as u128, HUNDRED_PERCENT as u128, true)?;
320    let mut estimated_collateral_to_be_withdrawn = 0;
321
322    if let Some(tick_arrays) = tick_arrays {
323        let mut next_sqrt_price = fusion_pool.sqrt_price;
324
325        if collateral_token == position_token {
326            if position_debt > 0 {
327                let amount_out = position_debt * decrease_percent as u64 / HUNDRED_PERCENT as u64;
328                let swap = swap_quote_by_output_token(amount_out, borrowed_token == TOKEN_A, 0, fusion_pool, tick_arrays, None, None)?;
329                next_sqrt_price = swap.next_sqrt_price;
330                required_swap_amount = try_get_max_amount_with_slippage_tolerance(swap.token_est_in, slippage_tolerance_bps)?;
331                estimated_collateral_to_be_withdrawn = position_amount.saturating_sub(swap.token_est_in).saturating_sub(estimated_amount);
332            } else {
333                estimated_collateral_to_be_withdrawn = position_amount - estimated_amount;
334            }
335        } else {
336            let amount_in = position_amount - estimated_amount;
337            let swap = swap_quote_by_input_token(amount_in, position_token == TOKEN_A, 0, fusion_pool, tick_arrays, None, None)?;
338            next_sqrt_price = swap.next_sqrt_price;
339            required_swap_amount = try_get_min_amount_with_slippage_tolerance(swap.token_est_out, slippage_tolerance_bps)?;
340            estimated_collateral_to_be_withdrawn = swap.token_est_out.saturating_sub(estimated_payable_debt);
341        }
342
343        let new_price = sqrt_price_to_price(next_sqrt_price.into(), 1, 1);
344        price_impact = (new_price / price - 1.0).abs();
345    } else {
346        let (input_mint, output_mint) = if position_token == TOKEN_A { (mint_a, mint_b) } else { (mint_b, mint_a) };
347
348        let amount_in = if collateral_token == position_token {
349            if position_debt > 0 {
350                let mut amount_in = if position_token == TOKEN_A {
351                    (position_debt as f64 / price) as u64
352                } else {
353                    (position_debt as f64 * price) as u64
354                };
355                amount_in = try_get_max_amount_with_slippage_tolerance(amount_in, slippage_tolerance_bps)?;
356                amount_in = amount_in.min(position_amount);
357                estimated_collateral_to_be_withdrawn = position_amount.saturating_sub(amount_in).saturating_sub(estimated_amount);
358                amount_in
359            } else {
360                estimated_collateral_to_be_withdrawn = position_amount - estimated_amount;
361                0
362            }
363        } else {
364            position_amount - estimated_amount
365        };
366
367        if amount_in > 0 {
368            let quote = jupiter_swap_quote(input_mint, output_mint, amount_in, Some(slippage_tolerance_bps as u64)).await?;
369
370            price_impact = quote.price_impact_pct;
371            required_swap_amount = quote.other_amount_threshold;
372            jupiter_swap_ix = Some(SwapInstruction {
373                data: quote.instruction.data,
374                accounts: quote.instruction.accounts,
375                address_lookup_table_addresses: quote.instruction.address_lookup_table_addresses,
376            });
377
378            if collateral_token != position_token {
379                estimated_collateral_to_be_withdrawn = quote.out_amount.saturating_sub(estimated_payable_debt);
380            }
381        }
382    }
383
384    Ok(DecreaseSpotPositionQuoteResult {
385        decrease_percent,
386        estimated_payable_debt,
387        estimated_collateral_to_be_withdrawn,
388        required_swap_amount,
389        estimated_amount,
390        price_impact,
391        jupiter_swap_ix,
392    })
393}
394
395/// Spot position decrease quote
396///
397/// # Parameters
398/// - `decrease_amount`: Position total decrease size in the collateral_token.
399/// - `collateral_token`: Collateral token.
400/// - `leverage`: Leverage (1.0 or higher).
401/// - `slippage_tolerance_bps`: An optional slippage tolerance in basis points. Defaults to the global slippage tolerance if not provided.
402/// - `position_token`: Token of the existing position.
403/// - `position_amount`: Existing position amount in the position_token.
404/// - `position_debt`: Existing position debt in the token opposite to the position_token.
405/// - `fusion_pool`: Fusion pool.
406/// - `tick_arrays`: Optional five tick arrays around the current pool price. If not provided, the quote will be calculated using the Jupiter Aggregator.
407///
408/// # Returns
409/// - `DecreaseSpotPositionQuoteResult`: quote result
410#[cfg(feature = "wasm")]
411#[wasm_bindgen(js_name = "getDecreaseSpotPositionQuote", skip_jsdoc)]
412pub async fn wasm_get_decrease_spot_position_quote(
413    decrease_amount: u64,
414    collateral_token: u8,
415    leverage: f64,
416    slippage_tolerance_bps: Option<u16>,
417    position_token: u8,
418    position_amount: u64,
419    position_debt: u64,
420    mint_a: Pubkey,
421    mint_b: Pubkey,
422    fusion_pool: FusionPoolFacade,
423    tick_arrays: Option<TickArrays>,
424) -> Result<JsValue, JsValue> {
425    let result = get_decrease_spot_position_quote(
426        decrease_amount,
427        collateral_token,
428        leverage,
429        slippage_tolerance_bps,
430        position_token,
431        position_amount,
432        position_debt,
433        mint_a,
434        mint_b,
435        fusion_pool,
436        tick_arrays,
437    )
438    .await
439    .map_err(|e| JsValue::from_str(e))?;
440
441    let serializer = Serializer::new().serialize_maps_as_objects(true);
442    let js_value = result.serialize(&serializer).unwrap();
443
444    Ok(js_value)
445}
446
447/// Returns the liquidation price
448///
449/// # Parameters
450/// - `position_token`: Token of the position
451/// - `amount`: Position total size
452/// - `debt`: Position total debt
453/// - `liquidation_threshold`: Liquidation threshold of a market
454///
455/// # Returns
456/// - `f64`: Decimal liquidation price
457#[cfg_attr(feature = "wasm", wasm_expose)]
458pub fn get_spot_position_liquidation_price(position_token: u8, amount: u64, debt: u64, liquidation_threshold: u32) -> Result<f64, CoreError> {
459    if liquidation_threshold >= HUNDRED_PERCENT {
460        return Err(INVALID_ARGUMENTS);
461    }
462
463    if debt == 0 || amount == 0 {
464        return Ok(0.0);
465    }
466
467    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
468
469    if position_token == TOKEN_A {
470        Ok(debt as f64 / (amount as f64 * liquidation_threshold_f))
471    } else {
472        Ok((amount as f64 * liquidation_threshold_f) / debt as f64)
473    }
474}
475
476/// Calculates the maximum tradable amount in the collateral token.
477///
478/// # Parameters
479/// - `collateral_token`: Collateral token.
480/// - `available_balance`: Available wallet balance in the collateral_token.
481/// - `leverage`: Leverage (1.0 or higher).
482/// - `position_token`: Token of the existing position. Should be set to new_position_token if position_amount is zero.
483/// - `position_amount`: Existing position amount in the position_token.
484/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
485/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
486/// - `fusion_pool`: Fusion pool.
487/// - `increase`: true if increasing the position
488///
489/// # Returns
490/// - `u64`: the maximum tradable amount
491#[cfg_attr(feature = "wasm", wasm_expose)]
492pub fn get_tradable_amount(
493    collateral_token: u8,
494    available_balance: u64,
495    leverage: f64,
496    position_token: u8,
497    position_amount: u64,
498    protocol_fee_rate: u16,
499    protocol_fee_rate_on_collateral: u16,
500    fusion_pool: FusionPoolFacade,
501    increase: bool,
502) -> Result<u64, CoreError> {
503    if collateral_token > TOKEN_B || position_token > TOKEN_B {
504        return Err(INVALID_ARGUMENTS.into());
505    }
506
507    if leverage < 1.0 {
508        return Err(INVALID_ARGUMENTS.into());
509    }
510
511    // T = Câ‹…Fcâ‹…Fs + Bâ‹…Fbâ‹…Fs, where: Fc/Fb/Fs - collateral/borrow/swap fee multiplier
512    // B = Tâ‹…(L - 1) / L
513    // => T = Câ‹…Fcâ‹…Fs / (1 - Fbâ‹…Fsâ‹…(L - 1) / L)
514    let add_leverage = |collateral: u64| -> Result<u64, CoreError> {
515        let mut collateral = fees::apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
516        if collateral_token != position_token {
517            collateral = fees::apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
518        }
519
520        let fee_multiplier = (1.0 - protocol_fee_rate as f64 / HUNDRED_PERCENT as f64) * (1.0 - fusion_pool.fee_rate as f64 / 1_000_000.0);
521        let total = (collateral as f64 / (1.0 - (fee_multiplier * (leverage - 1.0)) / leverage)) as u64;
522        Ok(total)
523    };
524
525    let available_to_trade = if increase {
526        add_leverage(available_balance)?
527    } else {
528        let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
529        let position_to_opposite_token_price = if position_token == TOKEN_A { price } else { 1.0 / price };
530
531        if collateral_token == position_token {
532            position_amount
533        } else {
534            round(position_amount as f64 * position_to_opposite_token_price) as u64
535        }
536    };
537
538    Ok(available_to_trade)
539}
540
541struct JupiterSwapResult {
542    pub instruction: SwapInstruction,
543    pub out_amount: u64,
544    pub other_amount_threshold: u64,
545    pub price_impact_pct: f64,
546}
547
548async fn jupiter_swap_quote(input_mint: Pubkey, output_mint: Pubkey, amount: u64, slippage_bps: Option<u64>) -> Result<JupiterSwapResult, CoreError> {
549    let quote_config = QuoteConfig {
550        slippage_bps,
551        swap_mode: Some(SwapMode::ExactIn),
552        dexes: None,
553        exclude_dexes: None,
554        only_direct_routes: false,
555        as_legacy_transaction: None,
556        platform_fee_bps: None,
557        max_accounts: None,
558    };
559
560    let quote = jup_ag::quote(input_mint, output_mint, amount, quote_config)
561        .await
562        .map_err(|_| JUPITER_QUOTE_REQUEST_ERROR)?;
563
564    #[allow(deprecated)]
565    let swap_request = SwapRequest {
566        user_public_key: Default::default(),
567        wrap_and_unwrap_sol: None,
568        use_shared_accounts: Some(true),
569        fee_account: None,
570        compute_unit_price_micro_lamports: None,
571        prioritization_fee_lamports: PrioritizationFeeLamports::Auto,
572        as_legacy_transaction: None,
573        use_token_ledger: None,
574        destination_token_account: None,
575        quote_response: quote.clone(),
576    };
577
578    let swap_response = jup_ag::swap_instructions(swap_request)
579        .await
580        .map_err(|_| JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR)?;
581
582    Ok(JupiterSwapResult {
583        instruction: SwapInstruction {
584            data: swap_response.swap_instruction.data,
585            accounts: swap_response.swap_instruction.accounts,
586            address_lookup_table_addresses: swap_response.address_lookup_table_addresses,
587        },
588        out_amount: quote.out_amount,
589        other_amount_threshold: quote.other_amount_threshold,
590        price_impact_pct: quote.price_impact_pct,
591    })
592}
593
594#[cfg_attr(feature = "wasm", wasm_expose)]
595pub fn calculate_tuna_spot_position_protocol_fee(
596    collateral_token: u8,
597    borrowed_token: u8,
598    collateral: u64,
599    borrow: u64,
600    protocol_fee_rate_on_collateral: u16,
601    protocol_fee_rate: u16,
602) -> TokenPair {
603    let collateral_a = if collateral_token == TOKEN_A { collateral } else { 0 };
604    let collateral_b = if collateral_token == TOKEN_B { collateral } else { 0 };
605    let borrow_a = if borrowed_token == TOKEN_A { borrow } else { 0 };
606    let borrow_b = if borrowed_token == TOKEN_B { borrow } else { 0 };
607
608    let protocol_fee_a = calculate_tuna_protocol_fee(collateral_a, borrow_a, protocol_fee_rate_on_collateral, protocol_fee_rate);
609    let protocol_fee_b = calculate_tuna_protocol_fee(collateral_b, borrow_b, protocol_fee_rate_on_collateral, protocol_fee_rate);
610
611    TokenPair {
612        a: protocol_fee_a,
613        b: protocol_fee_b,
614    }
615}
616
617#[cfg(all(test, not(feature = "wasm")))]
618mod tests {
619    use super::*;
620    use crate::assert_approx_eq;
621    use fusionamm_core::{
622        get_tick_array_start_tick_index, price_to_sqrt_price, sqrt_price_to_tick_index, TickArrayFacade, TickFacade, TICK_ARRAY_SIZE,
623    };
624    use solana_pubkey::pubkey;
625
626    const NATIVE_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
627    const TUNA_MINT: Pubkey = pubkey!("TUNAfXDZEdQizTMTh3uEvNvYqJmqFHZbEJt8joP4cyx");
628
629    fn test_fusion_pool(sqrt_price: u128) -> FusionPoolFacade {
630        let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
631        FusionPoolFacade {
632            tick_current_index,
633            fee_rate: 3000,
634            liquidity: 10000000000000,
635            sqrt_price,
636            tick_spacing: 2,
637            ..FusionPoolFacade::default()
638        }
639    }
640
641    fn test_tick(liquidity_net: i128) -> TickFacade {
642        TickFacade {
643            initialized: true,
644            liquidity_net,
645            ..TickFacade::default()
646        }
647    }
648
649    fn test_tick_array(start_tick_index: i32) -> TickArrayFacade {
650        TickArrayFacade {
651            start_tick_index,
652            ticks: [test_tick(0); TICK_ARRAY_SIZE],
653        }
654    }
655
656    fn test_tick_arrays(fusion_pool: FusionPoolFacade) -> TickArrays {
657        let tick_spacing = fusion_pool.tick_spacing;
658        let tick_current_index = sqrt_price_to_tick_index(fusion_pool.sqrt_price);
659        let tick_array_start_index = get_tick_array_start_tick_index(tick_current_index, tick_spacing);
660
661        [
662            test_tick_array(tick_array_start_index),
663            test_tick_array(tick_array_start_index + TICK_ARRAY_SIZE as i32 * tick_spacing as i32),
664            test_tick_array(tick_array_start_index + TICK_ARRAY_SIZE as i32 * tick_spacing as i32 * 2),
665            test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32),
666            test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32 * 2),
667        ]
668        .into()
669    }
670
671    #[test]
672    fn test_get_liquidation_price() {
673        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5, 0, HUNDRED_PERCENT * 85 / 100), Ok(0.0));
674        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 0, 5, HUNDRED_PERCENT * 85 / 100), Ok(0.0));
675        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5, 800, HUNDRED_PERCENT * 85 / 100), Ok(188.23529411764707));
676        assert_eq!(get_spot_position_liquidation_price(TOKEN_B, 1000, 4, HUNDRED_PERCENT * 85 / 100), Ok(212.5));
677    }
678
679    #[tokio::test]
680    async fn increase_long_position_providing_token_a() {
681        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
682        let fusion_pool = test_fusion_pool(sqrt_price);
683
684        let quote = get_increase_spot_position_quote(
685            5_000_000_000,
686            TOKEN_A,
687            TOKEN_A,
688            5.0,
689            Some(0),
690            (HUNDRED_PERCENT / 100) as u16,
691            (HUNDRED_PERCENT / 200) as u16,
692            NATIVE_MINT,
693            TUNA_MINT,
694            fusion_pool,
695            Some(test_tick_arrays(fusion_pool)),
696        )
697        .await
698        .unwrap();
699
700        assert_eq!(quote.collateral, 1057165829);
701        assert_eq!(quote.borrow, 800000000);
702        assert_eq!(quote.min_swap_output_amount, 3_947_423_011);
703        assert_eq!(quote.estimated_amount, 4_999_303_011);
704        assert_eq!(quote.protocol_fee_a, 5285829);
705        assert_eq!(quote.protocol_fee_b, 8000000);
706        assert_eq!(quote.price_impact, 0.00035316176257027543);
707        assert_approx_eq!(quote.estimated_amount as f64 / (quote.estimated_amount as f64 - (quote.borrow as f64 * 1000.0) / 200.0), 5.0, 0.1);
708    }
709
710    #[tokio::test]
711    async fn increase_long_position_providing_token_b() {
712        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
713        let fusion_pool = test_fusion_pool(sqrt_price);
714
715        let quote = get_increase_spot_position_quote(
716            5000_000_000,
717            TOKEN_B,
718            TOKEN_A,
719            5.0,
720            Some(0),
721            (HUNDRED_PERCENT / 100) as u16,
722            (HUNDRED_PERCENT / 200) as u16,
723            NATIVE_MINT,
724            TUNA_MINT,
725            fusion_pool,
726            Some(test_tick_arrays(fusion_pool)),
727        )
728        .await
729        .unwrap();
730
731        assert_eq!(quote.collateral, 1060346869);
732        assert_eq!(quote.borrow, 4000000000);
733        assert_eq!(quote.estimated_amount, 24_972_080_293);
734        assert_eq!(quote.protocol_fee_a, 0);
735        assert_eq!(quote.protocol_fee_b, 45301734);
736        assert_eq!(quote.price_impact, 0.0022373179716579372);
737        assert_approx_eq!(quote.estimated_amount as f64 / (quote.estimated_amount as f64 - (quote.borrow as f64 * 1000.0) / 200.0), 5.0, 0.1);
738    }
739
740    #[tokio::test]
741    async fn increase_short_position_providing_a() {
742        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
743        let fusion_pool = test_fusion_pool(sqrt_price);
744
745        let quote = get_increase_spot_position_quote(
746            5_000_000_000,
747            TOKEN_A,
748            TOKEN_B,
749            5.0,
750            Some(0),
751            (HUNDRED_PERCENT / 100) as u16,
752            (HUNDRED_PERCENT / 200) as u16,
753            NATIVE_MINT,
754            TUNA_MINT,
755            fusion_pool,
756            Some(test_tick_arrays(fusion_pool)),
757        )
758        .await
759        .unwrap();
760
761        assert_eq!(quote.collateral, 1060346869);
762        assert_eq!(quote.borrow, 4000000000);
763        assert_eq!(quote.estimated_amount, 999_776_441);
764        assert_eq!(quote.protocol_fee_a, 45301734);
765        assert_eq!(quote.protocol_fee_b, 0);
766        assert_eq!(quote.price_impact, 0.0004470636400017991);
767        assert_approx_eq!(quote.estimated_amount as f64 / (quote.estimated_amount as f64 - (quote.borrow as f64 / 1000.0) * 200.0), 5.0, 0.1);
768    }
769
770    #[tokio::test]
771    async fn increase_short_position_providing_b() {
772        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
773        let fusion_pool = test_fusion_pool(sqrt_price);
774
775        let quote = get_increase_spot_position_quote(
776            5000_000_000,
777            TOKEN_B,
778            TOKEN_B,
779            5.0,
780            Some(0),
781            (HUNDRED_PERCENT / 100) as u16,
782            (HUNDRED_PERCENT / 200) as u16,
783            NATIVE_MINT,
784            TUNA_MINT,
785            fusion_pool,
786            Some(test_tick_arrays(fusion_pool)),
787        )
788        .await
789        .unwrap();
790
791        assert_eq!(quote.collateral, 1057165829);
792        assert_eq!(quote.borrow, 20000000000);
793        assert_eq!(quote.estimated_amount, 4996_517_564);
794        assert_eq!(quote.protocol_fee_a, 200000000);
795        assert_eq!(quote.protocol_fee_b, 5285829);
796        assert_eq!(quote.price_impact, 0.0017633175413067637);
797        assert_approx_eq!(quote.estimated_amount as f64 / (quote.estimated_amount as f64 - (quote.borrow as f64 / 1000.0) * 200.0), 5.0, 0.1);
798    }
799
800    #[tokio::test]
801    async fn increase_quote_with_slippage() {
802        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
803        let fusion_pool = test_fusion_pool(sqrt_price);
804
805        // with slippage 10%
806        let quote = get_increase_spot_position_quote(
807            200_000,
808            TOKEN_B,
809            TOKEN_A,
810            5.0,
811            Some(1000),
812            0,
813            0,
814            NATIVE_MINT,
815            TUNA_MINT,
816            fusion_pool,
817            Some(test_tick_arrays(fusion_pool)),
818        )
819        .await
820        .unwrap();
821        assert_eq!(quote.min_swap_output_amount, 899_994);
822
823        // without slippage
824        let quote = get_increase_spot_position_quote(
825            200_000,
826            TOKEN_B,
827            TOKEN_A,
828            5.0,
829            Some(0),
830            0,
831            0,
832            NATIVE_MINT,
833            TUNA_MINT,
834            fusion_pool,
835            Some(test_tick_arrays(fusion_pool)),
836        )
837        .await
838        .unwrap();
839        assert_eq!(quote.min_swap_output_amount, 999_994);
840    }
841
842    #[tokio::test]
843    async fn decrease_non_leveraged_long_position_providing_a() {
844        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
845        let fusion_pool = test_fusion_pool(sqrt_price);
846
847        let quote = get_decrease_spot_position_quote(
848            1_000_000_000,
849            TOKEN_A,
850            1.0,
851            Some(0),
852            TOKEN_A,
853            5_000_000_000, // A
854            0,             // B
855            NATIVE_MINT,
856            TUNA_MINT,
857            fusion_pool,
858            Some(test_tick_arrays(fusion_pool)),
859        )
860        .await
861        .unwrap();
862
863        assert_eq!(quote.decrease_percent, 200000);
864        assert_eq!(quote.estimated_amount, 4_000_000_000);
865        assert_eq!(quote.estimated_payable_debt, 0);
866        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 1_000_000_000);
867        assert_eq!(quote.price_impact, 0.0);
868    }
869
870    #[tokio::test]
871    async fn decrease_non_leveraged_long_position_providing_b() {
872        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
873        let fusion_pool = test_fusion_pool(sqrt_price);
874
875        let quote = get_decrease_spot_position_quote(
876            200_000_000,
877            TOKEN_B,
878            1.0,
879            Some(0),
880            TOKEN_A,
881            5_000_000_000, // A
882            0,             // B
883            NATIVE_MINT,
884            TUNA_MINT,
885            fusion_pool,
886            Some(test_tick_arrays(fusion_pool)),
887        )
888        .await
889        .unwrap();
890
891        assert_eq!(quote.decrease_percent, 200000);
892        assert_eq!(quote.estimated_amount, 4_000_000_000);
893        assert_eq!(quote.estimated_payable_debt, 0);
894        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 199_391_108);
895        assert_eq!(quote.price_impact, 0.00008916842709072448);
896    }
897
898    #[tokio::test]
899    async fn decrease_long_position_providing_a() {
900        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
901        let fusion_pool = test_fusion_pool(sqrt_price);
902
903        let quote = get_decrease_spot_position_quote(
904            1_000_000_000,
905            TOKEN_A,
906            5.0,
907            Some(0),
908            TOKEN_A,
909            5_000_000_000, // A
910            800_000_000,   // B
911            NATIVE_MINT,
912            TUNA_MINT,
913            fusion_pool,
914            Some(test_tick_arrays(fusion_pool)),
915        )
916        .await
917        .unwrap();
918
919        assert_eq!(quote.decrease_percent, 200000);
920        assert_eq!(quote.estimated_amount, 4_000_000_000);
921        assert_eq!(quote.estimated_payable_debt, 160_000_000);
922        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 197_564_069);
923        assert_eq!(quote.price_impact, 0.00007155289528004705);
924
925        let quote = get_decrease_spot_position_quote(
926            6_000_000_000,
927            TOKEN_A,
928            5.0,
929            Some(0),
930            TOKEN_A,
931            5_000_000_000, // A
932            800_000_000,   // B
933            NATIVE_MINT,
934            TUNA_MINT,
935            fusion_pool,
936            Some(test_tick_arrays(fusion_pool)),
937        )
938        .await
939        .unwrap();
940
941        assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
942        assert_eq!(quote.estimated_amount, 0);
943    }
944
945    #[tokio::test]
946    async fn decrease_long_position_providing_b() {
947        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
948        let fusion_pool = test_fusion_pool(sqrt_price);
949
950        let quote = get_decrease_spot_position_quote(
951            200_000_000,
952            TOKEN_B,
953            5.0,
954            Some(0),
955            TOKEN_A,
956            5_000_000_000, // A
957            800_000_000,   // B
958            NATIVE_MINT,
959            TUNA_MINT,
960            fusion_pool,
961            Some(test_tick_arrays(fusion_pool)),
962        )
963        .await
964        .unwrap();
965
966        assert_eq!(quote.estimated_amount, 4000000000);
967        assert_eq!(quote.decrease_percent, 200000);
968        assert_eq!(quote.estimated_payable_debt, 160_000_000);
969        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 39_391_108);
970        assert_eq!(quote.price_impact, 0.00008916842709072448);
971
972        let quote = get_decrease_spot_position_quote(
973            1200_000_000,
974            TOKEN_B,
975            5.0,
976            Some(0),
977            TOKEN_A,
978            5_000_000_000, // A
979            800_000_000,   // B
980            NATIVE_MINT,
981            TUNA_MINT,
982            fusion_pool,
983            Some(test_tick_arrays(fusion_pool)),
984        )
985        .await
986        .unwrap();
987
988        assert_eq!(quote.estimated_amount, 0);
989        assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
990    }
991
992    #[tokio::test]
993    async fn tradable_amount_for_1x_long_position_providing_b() {
994        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
995        let fusion_pool = test_fusion_pool(sqrt_price);
996        let tick_arrays = Some(test_tick_arrays(fusion_pool));
997
998        let collateral_token = TOKEN_B;
999        let position_token = TOKEN_A;
1000        let leverage = 1.0;
1001        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1002        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1003        let available_balance = 200_000_000;
1004
1005        let tradable_amount = get_tradable_amount(
1006            collateral_token,
1007            available_balance,
1008            leverage,
1009            position_token,
1010            0,
1011            protocol_fee_rate,
1012            protocol_fee_rate_on_collateral,
1013            fusion_pool,
1014            true,
1015        )
1016        .unwrap();
1017        assert_eq!(tradable_amount, 198403000);
1018
1019        let quote = get_increase_spot_position_quote(
1020            tradable_amount,
1021            collateral_token,
1022            position_token,
1023            leverage,
1024            Some(0),
1025            protocol_fee_rate,
1026            protocol_fee_rate_on_collateral,
1027            NATIVE_MINT,
1028            TUNA_MINT,
1029            fusion_pool,
1030            tick_arrays,
1031        )
1032        .await
1033        .unwrap();
1034        assert_eq!(quote.collateral, available_balance);
1035    }
1036
1037    #[tokio::test]
1038    async fn tradable_amount_for_5x_long_position_providing_b() {
1039        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1040        let fusion_pool = test_fusion_pool(sqrt_price);
1041        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1042
1043        let collateral_token = TOKEN_B;
1044        let position_token = TOKEN_A;
1045        let leverage = 5.0;
1046        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1047        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1048        let available_balance = 10_000_000;
1049
1050        let tradable_amount = get_tradable_amount(
1051            collateral_token,
1052            available_balance,
1053            leverage,
1054            position_token,
1055            0,
1056            protocol_fee_rate,
1057            protocol_fee_rate_on_collateral,
1058            fusion_pool,
1059            true,
1060        )
1061        .unwrap();
1062        assert_eq!(tradable_amount, 47154380);
1063
1064        let quote = get_increase_spot_position_quote(
1065            tradable_amount,
1066            collateral_token,
1067            position_token,
1068            leverage,
1069            Some(0),
1070            protocol_fee_rate,
1071            protocol_fee_rate_on_collateral,
1072            NATIVE_MINT,
1073            TUNA_MINT,
1074            fusion_pool,
1075            tick_arrays,
1076        )
1077        .await
1078        .unwrap();
1079        // TODO: fix precision error
1080        assert_eq!(quote.collateral, available_balance + 1);
1081    }
1082
1083    #[tokio::test]
1084    async fn tradable_amount_for_5x_long_position_providing_a() {
1085        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1086        let fusion_pool = test_fusion_pool(sqrt_price);
1087        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1088
1089        let collateral_token = TOKEN_A;
1090        let position_token = TOKEN_A;
1091        let leverage = 5.0;
1092        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1093        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1094        let available_balance = 1_000_000_000;
1095
1096        let tradable_amount = get_tradable_amount(
1097            collateral_token,
1098            available_balance,
1099            leverage,
1100            position_token,
1101            0,
1102            protocol_fee_rate,
1103            protocol_fee_rate_on_collateral,
1104            fusion_pool,
1105            true,
1106        )
1107        .unwrap();
1108        assert_eq!(tradable_amount, 4729626953);
1109
1110        let quote = get_increase_spot_position_quote(
1111            tradable_amount,
1112            collateral_token,
1113            position_token,
1114            leverage,
1115            Some(0),
1116            protocol_fee_rate,
1117            protocol_fee_rate_on_collateral,
1118            NATIVE_MINT,
1119            TUNA_MINT,
1120            fusion_pool,
1121            tick_arrays,
1122        )
1123        .await
1124        .unwrap();
1125        assert_eq!(quote.collateral, available_balance);
1126        //assert_eq!(quote.estimated_amount, tradable_amount);
1127    }
1128
1129    #[tokio::test]
1130    async fn tradable_amount_for_5x_short_position_providing_b() {
1131        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1132        let fusion_pool = test_fusion_pool(sqrt_price);
1133        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1134
1135        let collateral_token = TOKEN_B;
1136        let position_token = TOKEN_B;
1137        let leverage = 5.0;
1138        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1139        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1140        let available_balance = 200_000_000;
1141
1142        let tradable_amount = get_tradable_amount(
1143            collateral_token,
1144            available_balance,
1145            leverage,
1146            position_token,
1147            0,
1148            protocol_fee_rate,
1149            protocol_fee_rate_on_collateral,
1150            fusion_pool,
1151            true,
1152        )
1153        .unwrap();
1154        assert_eq!(tradable_amount, 945925390);
1155
1156        let quote = get_increase_spot_position_quote(
1157            tradable_amount,
1158            collateral_token,
1159            position_token,
1160            leverage,
1161            Some(0),
1162            protocol_fee_rate,
1163            protocol_fee_rate_on_collateral,
1164            NATIVE_MINT,
1165            TUNA_MINT,
1166            fusion_pool,
1167            tick_arrays,
1168        )
1169        .await
1170        .unwrap();
1171        // TODO: fix precision error
1172        assert_eq!(quote.collateral, available_balance + 1);
1173        //assert_eq!(quote.estimated_amount, tradable_amount);
1174    }
1175
1176    #[tokio::test]
1177    async fn tradable_amount_for_reducing_existing_long_position() {
1178        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1179        let fusion_pool = test_fusion_pool(sqrt_price);
1180        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1181
1182        for i in 0..2 {
1183            let collateral_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1184            let position_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1185            let leverage = 5.0;
1186            let position_amount = 5_000_000_000;
1187            let position_debt = 800_000_000;
1188            let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1189            let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1190            let available_balance = 50_000_000_000;
1191
1192            let tradable_amount = get_tradable_amount(
1193                collateral_token,
1194                available_balance,
1195                leverage,
1196                position_token,
1197                position_amount,
1198                protocol_fee_rate,
1199                protocol_fee_rate_on_collateral,
1200                fusion_pool,
1201                false,
1202            )
1203            .unwrap();
1204            assert_eq!(tradable_amount, 5_000_000_000);
1205
1206            let quote = get_decrease_spot_position_quote(
1207                tradable_amount,
1208                collateral_token,
1209                5.0,
1210                Some(0),
1211                position_token,
1212                position_amount,
1213                position_debt,
1214                NATIVE_MINT,
1215                TUNA_MINT,
1216                fusion_pool,
1217                tick_arrays.clone(),
1218            )
1219            .await
1220            .unwrap();
1221
1222            assert_eq!(quote.estimated_amount, 0);
1223        }
1224    }
1225}