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