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