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