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 price_impact: f64,
241 pub jupiter_swap_ix: Option<SwapInstruction>,
243}
244
245pub 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#[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#[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#[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 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 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 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, 0, 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, 0, 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, 800_000_000, 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, 800_000_000, 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, 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.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, 800_000_000, 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 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 }
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 assert_eq!(quote.collateral, available_balance + 1);
1152 }
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}