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