Skip to main content

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