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