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