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, CoreError, HUNDRED_PERCENT, INVALID_ARGUMENTS, JUPITER_QUOTE_REQUEST_ERROR, JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR,
18    TOKEN_A, 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, 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 (decimal)
452/// - `debt`: Position total debt (decimal)
453/// - `liquidation_threshold`: Liquidation threshold of the market (decimal)
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: f64, debt: f64, liquidation_threshold: f64) -> Result<f64, CoreError> {
459    if debt < 0.0 || amount < 0.0 {
460        return Err(INVALID_ARGUMENTS);
461    }
462
463    if liquidation_threshold <= 0.0 || liquidation_threshold >= 1.0 {
464        return Err(INVALID_ARGUMENTS);
465    }
466
467    if debt == 0.0 || amount == 0.0 {
468        return Ok(0.0);
469    }
470
471    if position_token == TOKEN_A {
472        Ok(debt / (amount * liquidation_threshold))
473    } else {
474        Ok((amount * liquidation_threshold) / debt)
475    }
476}
477
478/// Calculates the maximum tradable amount in the collateral token.
479///
480/// # Parameters
481/// - `collateral_token`: Collateral token.
482/// - `available_balance`: Available wallet balance in the collateral_token.
483/// - `leverage`: Leverage (1.0 or higher).
484/// - `position_token`: Token of the existing position. Should be set to new_position_token if position_amount is zero.
485/// - `position_amount`: Existing position amount in the position_token.
486/// - `protocol_fee_rate`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
487/// - `protocol_fee_rate_on_collateral`: Protocol fee rate from a market account represented as hundredths of a basis point (0.01% = 100).
488/// - `fusion_pool`: Fusion pool.
489/// - `increase`: true if increasing the position
490///
491/// # Returns
492/// - `u64`: the maximum tradable amount
493#[cfg_attr(feature = "wasm", wasm_expose)]
494pub fn get_tradable_amount(
495    collateral_token: u8,
496    available_balance: u64,
497    leverage: f64,
498    position_token: u8,
499    position_amount: u64,
500    protocol_fee_rate: u16,
501    protocol_fee_rate_on_collateral: u16,
502    fusion_pool: FusionPoolFacade,
503    increase: bool,
504) -> Result<u64, CoreError> {
505    if collateral_token > TOKEN_B || position_token > TOKEN_B {
506        return Err(INVALID_ARGUMENTS.into());
507    }
508
509    if leverage < 1.0 {
510        return Err(INVALID_ARGUMENTS.into());
511    }
512
513    // T = Câ‹…Fcâ‹…Fs + Bâ‹…Fbâ‹…Fs, where: Fc/Fb/Fs - collateral/borrow/swap fee multiplier
514    // B = Tâ‹…(L - 1) / L
515    // => T = Câ‹…Fcâ‹…Fs / (1 - Fbâ‹…Fsâ‹…(L - 1) / L)
516    let add_leverage = |collateral: u64| -> Result<u64, CoreError> {
517        let mut collateral = fees::apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
518        if collateral_token != position_token {
519            collateral = fees::apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
520        }
521
522        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);
523        let total = (collateral as f64 / (1.0 - (fee_multiplier * (leverage - 1.0)) / leverage)) as u64;
524        Ok(total)
525    };
526
527    let available_to_trade = if increase {
528        add_leverage(available_balance)?
529    } else {
530        let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
531        let position_to_opposite_token_price = if position_token == TOKEN_A { price } else { 1.0 / price };
532
533        if collateral_token == position_token {
534            position_amount
535        } else {
536            round(position_amount as f64 * position_to_opposite_token_price) as u64
537        }
538    };
539
540    Ok(available_to_trade)
541}
542
543struct JupiterSwapResult {
544    pub instruction: SwapInstruction,
545    pub out_amount: u64,
546    pub other_amount_threshold: u64,
547    pub price_impact_pct: f64,
548}
549
550async fn jupiter_swap_quote(input_mint: Pubkey, output_mint: Pubkey, amount: u64, slippage_bps: Option<u64>) -> Result<JupiterSwapResult, CoreError> {
551    let quote_config = QuoteConfig {
552        slippage_bps,
553        swap_mode: Some(SwapMode::ExactIn),
554        dexes: None,
555        exclude_dexes: None,
556        only_direct_routes: false,
557        as_legacy_transaction: None,
558        platform_fee_bps: None,
559        max_accounts: None,
560    };
561
562    let quote = jup_ag::quote(input_mint, output_mint, amount, quote_config)
563        .await
564        .map_err(|_| JUPITER_QUOTE_REQUEST_ERROR)?;
565
566    #[allow(deprecated)]
567    let swap_request = SwapRequest {
568        user_public_key: Default::default(),
569        wrap_and_unwrap_sol: None,
570        use_shared_accounts: Some(true),
571        fee_account: None,
572        compute_unit_price_micro_lamports: None,
573        prioritization_fee_lamports: PrioritizationFeeLamports::Auto,
574        as_legacy_transaction: None,
575        use_token_ledger: None,
576        destination_token_account: None,
577        quote_response: quote.clone(),
578    };
579
580    let swap_response = jup_ag::swap_instructions(swap_request)
581        .await
582        .map_err(|_| JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR)?;
583
584    Ok(JupiterSwapResult {
585        instruction: SwapInstruction {
586            data: swap_response.swap_instruction.data,
587            accounts: swap_response.swap_instruction.accounts,
588            address_lookup_table_addresses: swap_response.address_lookup_table_addresses,
589        },
590        out_amount: quote.out_amount,
591        other_amount_threshold: quote.other_amount_threshold,
592        price_impact_pct: quote.price_impact_pct,
593    })
594}
595
596#[cfg_attr(feature = "wasm", wasm_expose)]
597pub fn calculate_tuna_spot_position_protocol_fee(
598    collateral_token: u8,
599    borrowed_token: u8,
600    collateral: u64,
601    borrow: u64,
602    protocol_fee_rate_on_collateral: u16,
603    protocol_fee_rate: u16,
604) -> TokenPair {
605    let collateral_a = if collateral_token == TOKEN_A { collateral } else { 0 };
606    let collateral_b = if collateral_token == TOKEN_B { collateral } else { 0 };
607    let borrow_a = if borrowed_token == TOKEN_A { borrow } else { 0 };
608    let borrow_b = if borrowed_token == TOKEN_B { borrow } else { 0 };
609
610    let protocol_fee_a = calculate_tuna_protocol_fee(collateral_a, borrow_a, protocol_fee_rate_on_collateral, protocol_fee_rate);
611    let protocol_fee_b = calculate_tuna_protocol_fee(collateral_b, borrow_b, protocol_fee_rate_on_collateral, protocol_fee_rate);
612
613    TokenPair {
614        a: protocol_fee_a,
615        b: protocol_fee_b,
616    }
617}
618
619#[cfg(all(test, not(feature = "wasm")))]
620mod tests {
621    use super::*;
622    use crate::assert_approx_eq;
623    use fusionamm_core::{
624        get_tick_array_start_tick_index, price_to_sqrt_price, sqrt_price_to_tick_index, TickArrayFacade, TickFacade, TICK_ARRAY_SIZE,
625    };
626    use solana_pubkey::pubkey;
627
628    const NATIVE_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
629    const TUNA_MINT: Pubkey = pubkey!("TUNAfXDZEdQizTMTh3uEvNvYqJmqFHZbEJt8joP4cyx");
630
631    fn test_fusion_pool(sqrt_price: u128) -> FusionPoolFacade {
632        let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
633        FusionPoolFacade {
634            tick_current_index,
635            fee_rate: 3000,
636            liquidity: 10000000000000,
637            sqrt_price,
638            tick_spacing: 2,
639            ..FusionPoolFacade::default()
640        }
641    }
642
643    fn test_tick(liquidity_net: i128) -> TickFacade {
644        TickFacade {
645            initialized: true,
646            liquidity_net,
647            ..TickFacade::default()
648        }
649    }
650
651    fn test_tick_array(start_tick_index: i32) -> TickArrayFacade {
652        TickArrayFacade {
653            start_tick_index,
654            ticks: [test_tick(0); TICK_ARRAY_SIZE],
655        }
656    }
657
658    fn test_tick_arrays(fusion_pool: FusionPoolFacade) -> TickArrays {
659        let tick_spacing = fusion_pool.tick_spacing;
660        let tick_current_index = sqrt_price_to_tick_index(fusion_pool.sqrt_price);
661        let tick_array_start_index = get_tick_array_start_tick_index(tick_current_index, tick_spacing);
662
663        [
664            test_tick_array(tick_array_start_index),
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            test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32),
668            test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32 * 2),
669        ]
670        .into()
671    }
672
673    #[test]
674    fn test_get_liquidation_price() {
675        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5.0, 0.0, 0.85), Ok(0.0));
676        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 0.0, 5.0, 0.85), Ok(0.0));
677        assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5.0, 800.0, 0.85), Ok(188.23529411764707));
678        assert_eq!(get_spot_position_liquidation_price(TOKEN_B, 1000.0, 4.0, 0.85), Ok(212.5));
679    }
680
681    #[tokio::test]
682    async fn increase_long_position_providing_token_a() {
683        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
684        let fusion_pool = test_fusion_pool(sqrt_price);
685
686        let quote = get_increase_spot_position_quote(
687            5_000_000_000,
688            TOKEN_A,
689            TOKEN_A,
690            5.0,
691            Some(0),
692            (HUNDRED_PERCENT / 100) as u16,
693            (HUNDRED_PERCENT / 200) as u16,
694            NATIVE_MINT,
695            TUNA_MINT,
696            fusion_pool,
697            Some(test_tick_arrays(fusion_pool)),
698        )
699        .await
700        .unwrap();
701
702        assert_eq!(quote.collateral, 1057165829);
703        assert_eq!(quote.borrow, 800000000);
704        assert_eq!(quote.min_swap_output_amount, 3_947_423_011);
705        assert_eq!(quote.estimated_amount, 4_999_303_011);
706        assert_eq!(quote.protocol_fee_a, 5285829);
707        assert_eq!(quote.protocol_fee_b, 8000000);
708        assert_eq!(quote.price_impact, 0.00035316176257027543);
709        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);
710    }
711
712    #[tokio::test]
713    async fn increase_long_position_providing_token_b() {
714        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
715        let fusion_pool = test_fusion_pool(sqrt_price);
716
717        let quote = get_increase_spot_position_quote(
718            5000_000_000,
719            TOKEN_B,
720            TOKEN_A,
721            5.0,
722            Some(0),
723            (HUNDRED_PERCENT / 100) as u16,
724            (HUNDRED_PERCENT / 200) as u16,
725            NATIVE_MINT,
726            TUNA_MINT,
727            fusion_pool,
728            Some(test_tick_arrays(fusion_pool)),
729        )
730        .await
731        .unwrap();
732
733        assert_eq!(quote.collateral, 1060346869);
734        assert_eq!(quote.borrow, 4000000000);
735        assert_eq!(quote.estimated_amount, 24_972_080_293);
736        assert_eq!(quote.protocol_fee_a, 0);
737        assert_eq!(quote.protocol_fee_b, 45301734);
738        assert_eq!(quote.price_impact, 0.0022373179716579372);
739        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);
740    }
741
742    #[tokio::test]
743    async fn increase_short_position_providing_a() {
744        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
745        let fusion_pool = test_fusion_pool(sqrt_price);
746
747        let quote = get_increase_spot_position_quote(
748            5_000_000_000,
749            TOKEN_A,
750            TOKEN_B,
751            5.0,
752            Some(0),
753            (HUNDRED_PERCENT / 100) as u16,
754            (HUNDRED_PERCENT / 200) as u16,
755            NATIVE_MINT,
756            TUNA_MINT,
757            fusion_pool,
758            Some(test_tick_arrays(fusion_pool)),
759        )
760        .await
761        .unwrap();
762
763        assert_eq!(quote.collateral, 1060346869);
764        assert_eq!(quote.borrow, 4000000000);
765        assert_eq!(quote.estimated_amount, 999_776_441);
766        assert_eq!(quote.protocol_fee_a, 45301734);
767        assert_eq!(quote.protocol_fee_b, 0);
768        assert_eq!(quote.price_impact, 0.0004470636400017991);
769        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);
770    }
771
772    #[tokio::test]
773    async fn increase_short_position_providing_b() {
774        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
775        let fusion_pool = test_fusion_pool(sqrt_price);
776
777        let quote = get_increase_spot_position_quote(
778            5000_000_000,
779            TOKEN_B,
780            TOKEN_B,
781            5.0,
782            Some(0),
783            (HUNDRED_PERCENT / 100) as u16,
784            (HUNDRED_PERCENT / 200) as u16,
785            NATIVE_MINT,
786            TUNA_MINT,
787            fusion_pool,
788            Some(test_tick_arrays(fusion_pool)),
789        )
790        .await
791        .unwrap();
792
793        assert_eq!(quote.collateral, 1057165829);
794        assert_eq!(quote.borrow, 20000000000);
795        assert_eq!(quote.estimated_amount, 4996_517_564);
796        assert_eq!(quote.protocol_fee_a, 200000000);
797        assert_eq!(quote.protocol_fee_b, 5285829);
798        assert_eq!(quote.price_impact, 0.0017633175413067637);
799        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);
800    }
801
802    #[tokio::test]
803    async fn increase_quote_with_slippage() {
804        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
805        let fusion_pool = test_fusion_pool(sqrt_price);
806
807        // with slippage 10%
808        let quote = get_increase_spot_position_quote(
809            200_000,
810            TOKEN_B,
811            TOKEN_A,
812            5.0,
813            Some(1000),
814            0,
815            0,
816            NATIVE_MINT,
817            TUNA_MINT,
818            fusion_pool,
819            Some(test_tick_arrays(fusion_pool)),
820        )
821        .await
822        .unwrap();
823        assert_eq!(quote.min_swap_output_amount, 899_994);
824
825        // without slippage
826        let quote = get_increase_spot_position_quote(
827            200_000,
828            TOKEN_B,
829            TOKEN_A,
830            5.0,
831            Some(0),
832            0,
833            0,
834            NATIVE_MINT,
835            TUNA_MINT,
836            fusion_pool,
837            Some(test_tick_arrays(fusion_pool)),
838        )
839        .await
840        .unwrap();
841        assert_eq!(quote.min_swap_output_amount, 999_994);
842    }
843
844    #[tokio::test]
845    async fn decrease_non_leveraged_long_position_providing_a() {
846        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
847        let fusion_pool = test_fusion_pool(sqrt_price);
848
849        let quote = get_decrease_spot_position_quote(
850            1_000_000_000,
851            TOKEN_A,
852            1.0,
853            Some(0),
854            TOKEN_A,
855            5_000_000_000, // A
856            0,             // B
857            NATIVE_MINT,
858            TUNA_MINT,
859            fusion_pool,
860            Some(test_tick_arrays(fusion_pool)),
861        )
862        .await
863        .unwrap();
864
865        assert_eq!(quote.decrease_percent, 200000);
866        assert_eq!(quote.estimated_amount, 4_000_000_000);
867        assert_eq!(quote.estimated_payable_debt, 0);
868        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 1_000_000_000);
869        assert_eq!(quote.price_impact, 0.0);
870    }
871
872    #[tokio::test]
873    async fn decrease_non_leveraged_long_position_providing_b() {
874        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
875        let fusion_pool = test_fusion_pool(sqrt_price);
876
877        let quote = get_decrease_spot_position_quote(
878            200_000_000,
879            TOKEN_B,
880            1.0,
881            Some(0),
882            TOKEN_A,
883            5_000_000_000, // A
884            0,             // B
885            NATIVE_MINT,
886            TUNA_MINT,
887            fusion_pool,
888            Some(test_tick_arrays(fusion_pool)),
889        )
890        .await
891        .unwrap();
892
893        assert_eq!(quote.decrease_percent, 200000);
894        assert_eq!(quote.estimated_amount, 4_000_000_000);
895        assert_eq!(quote.estimated_payable_debt, 0);
896        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 199_391_108);
897        assert_eq!(quote.price_impact, 0.00008916842709072448);
898    }
899
900    #[tokio::test]
901    async fn decrease_long_position_providing_a() {
902        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
903        let fusion_pool = test_fusion_pool(sqrt_price);
904
905        let quote = get_decrease_spot_position_quote(
906            1_000_000_000,
907            TOKEN_A,
908            5.0,
909            Some(0),
910            TOKEN_A,
911            5_000_000_000, // A
912            800_000_000,   // B
913            NATIVE_MINT,
914            TUNA_MINT,
915            fusion_pool,
916            Some(test_tick_arrays(fusion_pool)),
917        )
918        .await
919        .unwrap();
920
921        assert_eq!(quote.decrease_percent, 200000);
922        assert_eq!(quote.estimated_amount, 4_000_000_000);
923        assert_eq!(quote.estimated_payable_debt, 160_000_000);
924        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 197_564_069);
925        assert_eq!(quote.price_impact, 0.00007155289528004705);
926
927        let quote = get_decrease_spot_position_quote(
928            6_000_000_000,
929            TOKEN_A,
930            5.0,
931            Some(0),
932            TOKEN_A,
933            5_000_000_000, // A
934            800_000_000,   // B
935            NATIVE_MINT,
936            TUNA_MINT,
937            fusion_pool,
938            Some(test_tick_arrays(fusion_pool)),
939        )
940        .await
941        .unwrap();
942
943        assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
944        assert_eq!(quote.estimated_amount, 0);
945    }
946
947    #[tokio::test]
948    async fn decrease_long_position_providing_b() {
949        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
950        let fusion_pool = test_fusion_pool(sqrt_price);
951
952        let quote = get_decrease_spot_position_quote(
953            200_000_000,
954            TOKEN_B,
955            5.0,
956            Some(0),
957            TOKEN_A,
958            5_000_000_000, // A
959            800_000_000,   // B
960            NATIVE_MINT,
961            TUNA_MINT,
962            fusion_pool,
963            Some(test_tick_arrays(fusion_pool)),
964        )
965        .await
966        .unwrap();
967
968        assert_eq!(quote.estimated_amount, 4000000000);
969        assert_eq!(quote.decrease_percent, 200000);
970        assert_eq!(quote.estimated_payable_debt, 160_000_000);
971        assert_eq!(quote.estimated_collateral_to_be_withdrawn, 39_391_108);
972        assert_eq!(quote.price_impact, 0.00008916842709072448);
973
974        let quote = get_decrease_spot_position_quote(
975            1200_000_000,
976            TOKEN_B,
977            5.0,
978            Some(0),
979            TOKEN_A,
980            5_000_000_000, // A
981            800_000_000,   // B
982            NATIVE_MINT,
983            TUNA_MINT,
984            fusion_pool,
985            Some(test_tick_arrays(fusion_pool)),
986        )
987        .await
988        .unwrap();
989
990        assert_eq!(quote.estimated_amount, 0);
991        assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
992    }
993
994    #[tokio::test]
995    async fn tradable_amount_for_1x_long_position_providing_b() {
996        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
997        let fusion_pool = test_fusion_pool(sqrt_price);
998        let tick_arrays = Some(test_tick_arrays(fusion_pool));
999
1000        let collateral_token = TOKEN_B;
1001        let position_token = TOKEN_A;
1002        let leverage = 1.0;
1003        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1004        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1005        let available_balance = 200_000_000;
1006
1007        let tradable_amount = get_tradable_amount(
1008            collateral_token,
1009            available_balance,
1010            leverage,
1011            position_token,
1012            0,
1013            protocol_fee_rate,
1014            protocol_fee_rate_on_collateral,
1015            fusion_pool,
1016            true,
1017        )
1018        .unwrap();
1019        assert_eq!(tradable_amount, 198403000);
1020
1021        let quote = get_increase_spot_position_quote(
1022            tradable_amount,
1023            collateral_token,
1024            position_token,
1025            leverage,
1026            Some(0),
1027            protocol_fee_rate,
1028            protocol_fee_rate_on_collateral,
1029            NATIVE_MINT,
1030            TUNA_MINT,
1031            fusion_pool,
1032            tick_arrays,
1033        )
1034        .await
1035        .unwrap();
1036        assert_eq!(quote.collateral, available_balance);
1037    }
1038
1039    #[tokio::test]
1040    async fn tradable_amount_for_5x_long_position_providing_b() {
1041        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1042        let fusion_pool = test_fusion_pool(sqrt_price);
1043        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1044
1045        let collateral_token = TOKEN_B;
1046        let position_token = TOKEN_A;
1047        let leverage = 5.0;
1048        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1049        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1050        let available_balance = 10_000_000;
1051
1052        let tradable_amount = get_tradable_amount(
1053            collateral_token,
1054            available_balance,
1055            leverage,
1056            position_token,
1057            0,
1058            protocol_fee_rate,
1059            protocol_fee_rate_on_collateral,
1060            fusion_pool,
1061            true,
1062        )
1063        .unwrap();
1064        assert_eq!(tradable_amount, 47154380);
1065
1066        let quote = get_increase_spot_position_quote(
1067            tradable_amount,
1068            collateral_token,
1069            position_token,
1070            leverage,
1071            Some(0),
1072            protocol_fee_rate,
1073            protocol_fee_rate_on_collateral,
1074            NATIVE_MINT,
1075            TUNA_MINT,
1076            fusion_pool,
1077            tick_arrays,
1078        )
1079        .await
1080        .unwrap();
1081        // TODO: fix precision error
1082        assert_eq!(quote.collateral, available_balance + 1);
1083    }
1084
1085    #[tokio::test]
1086    async fn tradable_amount_for_5x_long_position_providing_a() {
1087        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1088        let fusion_pool = test_fusion_pool(sqrt_price);
1089        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1090
1091        let collateral_token = TOKEN_A;
1092        let position_token = TOKEN_A;
1093        let leverage = 5.0;
1094        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1095        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1096        let available_balance = 1_000_000_000;
1097
1098        let tradable_amount = get_tradable_amount(
1099            collateral_token,
1100            available_balance,
1101            leverage,
1102            position_token,
1103            0,
1104            protocol_fee_rate,
1105            protocol_fee_rate_on_collateral,
1106            fusion_pool,
1107            true,
1108        )
1109        .unwrap();
1110        assert_eq!(tradable_amount, 4729626953);
1111
1112        let quote = get_increase_spot_position_quote(
1113            tradable_amount,
1114            collateral_token,
1115            position_token,
1116            leverage,
1117            Some(0),
1118            protocol_fee_rate,
1119            protocol_fee_rate_on_collateral,
1120            NATIVE_MINT,
1121            TUNA_MINT,
1122            fusion_pool,
1123            tick_arrays,
1124        )
1125        .await
1126        .unwrap();
1127        assert_eq!(quote.collateral, available_balance);
1128        //assert_eq!(quote.estimated_amount, tradable_amount);
1129    }
1130
1131    #[tokio::test]
1132    async fn tradable_amount_for_5x_short_position_providing_b() {
1133        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1134        let fusion_pool = test_fusion_pool(sqrt_price);
1135        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1136
1137        let collateral_token = TOKEN_B;
1138        let position_token = TOKEN_B;
1139        let leverage = 5.0;
1140        let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1141        let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1142        let available_balance = 200_000_000;
1143
1144        let tradable_amount = get_tradable_amount(
1145            collateral_token,
1146            available_balance,
1147            leverage,
1148            position_token,
1149            0,
1150            protocol_fee_rate,
1151            protocol_fee_rate_on_collateral,
1152            fusion_pool,
1153            true,
1154        )
1155        .unwrap();
1156        assert_eq!(tradable_amount, 945925390);
1157
1158        let quote = get_increase_spot_position_quote(
1159            tradable_amount,
1160            collateral_token,
1161            position_token,
1162            leverage,
1163            Some(0),
1164            protocol_fee_rate,
1165            protocol_fee_rate_on_collateral,
1166            NATIVE_MINT,
1167            TUNA_MINT,
1168            fusion_pool,
1169            tick_arrays,
1170        )
1171        .await
1172        .unwrap();
1173        // TODO: fix precision error
1174        assert_eq!(quote.collateral, available_balance + 1);
1175        //assert_eq!(quote.estimated_amount, tradable_amount);
1176    }
1177
1178    #[tokio::test]
1179    async fn tradable_amount_for_reducing_existing_long_position() {
1180        let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1181        let fusion_pool = test_fusion_pool(sqrt_price);
1182        let tick_arrays = Some(test_tick_arrays(fusion_pool));
1183
1184        for i in 0..2 {
1185            let collateral_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1186            let position_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1187            let leverage = 5.0;
1188            let position_amount = 5_000_000_000;
1189            let position_debt = 800_000_000;
1190            let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1191            let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1192            let available_balance = 50_000_000_000;
1193
1194            let tradable_amount = get_tradable_amount(
1195                collateral_token,
1196                available_balance,
1197                leverage,
1198                position_token,
1199                position_amount,
1200                protocol_fee_rate,
1201                protocol_fee_rate_on_collateral,
1202                fusion_pool,
1203                false,
1204            )
1205            .unwrap();
1206            assert_eq!(tradable_amount, 5_000_000_000);
1207
1208            let quote = get_decrease_spot_position_quote(
1209                tradable_amount,
1210                collateral_token,
1211                5.0,
1212                Some(0),
1213                position_token,
1214                position_amount,
1215                position_debt,
1216                NATIVE_MINT,
1217                TUNA_MINT,
1218                fusion_pool,
1219                tick_arrays.clone(),
1220            )
1221            .await
1222            .unwrap();
1223
1224            assert_eq!(quote.estimated_amount, 0);
1225        }
1226    }
1227}