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::utils::fees;
16use crate::{
17 calculate_tuna_protocol_fee, HUNDRED_PERCENT, INVALID_ARGUMENTS, JUPITER_QUOTE_REQUEST_ERROR, JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR, TOKEN_A,
18 TOKEN_B,
19};
20use fusionamm_core::{
21 sqrt_price_to_price, swap_quote_by_input_token, swap_quote_by_output_token, try_get_max_amount_with_slippage_tolerance,
22 try_get_min_amount_with_slippage_tolerance, try_mul_div, CoreError, FusionPoolFacade, TickArrays, TokenPair,
23};
24use jup_ag::{PrioritizationFeeLamports, QuoteConfig, SwapMode, SwapRequest};
25use libm::{ceil, round};
26use solana_instruction::AccountMeta;
27use solana_pubkey::Pubkey;
28
29pub const DEFAULT_SLIPPAGE_TOLERANCE_BPS: u16 = 100;
30
31#[cfg_attr(feature = "wasm", wasm_expose)]
32pub struct SwapInstruction {
33 pub data: Vec<u8>,
34 pub accounts: Vec<AccountMeta>,
35 pub address_lookup_table_addresses: Vec<Pubkey>,
36}
37
38#[cfg_attr(feature = "wasm", wasm_expose)]
39pub struct IncreaseSpotPositionQuoteResult {
40 pub collateral: u64,
42 pub borrow: u64,
44 pub estimated_amount: u64,
46 pub swap_input_amount: u64,
48 pub min_swap_output_amount: u64,
50 pub protocol_fee_a: u64,
52 pub protocol_fee_b: u64,
54 pub price_impact: f64,
56 pub jupiter_swap_ix: Option<SwapInstruction>,
58}
59
60pub async fn get_increase_spot_position_quote(
78 increase_amount: u64,
79 collateral_token: u8,
80 position_token: u8,
81 leverage: f64,
82 slippage_tolerance_bps: Option<u16>,
83 protocol_fee_rate: u16,
84 protocol_fee_rate_on_collateral: u16,
85 mint_a: Pubkey,
86 mint_b: Pubkey,
87 fusion_pool: FusionPoolFacade,
88 tick_arrays: Option<TickArrays>,
89) -> Result<IncreaseSpotPositionQuoteResult, CoreError> {
90 if collateral_token > TOKEN_B || position_token > TOKEN_B {
91 return Err(INVALID_ARGUMENTS.into());
92 }
93
94 if leverage < 1.0 {
95 return Err(INVALID_ARGUMENTS.into());
96 }
97
98 let borrow: u64;
99 let mut collateral: u64;
100 let mut estimated_amount: u64 = 0;
101 let mut swap_input_amount: u64;
102 let mut min_swap_output_amount: u64 = 0;
103 let mut price_impact: f64 = 0.0;
104 let mut jupiter_swap_ix: Option<SwapInstruction> = None;
105
106 let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
107 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(DEFAULT_SLIPPAGE_TOLERANCE_BPS);
108
109 let borrowed_token = if position_token == TOKEN_A { TOKEN_B } else { TOKEN_A };
110 let swap_input_token_is_a = borrowed_token == TOKEN_A;
111
112 if borrowed_token == collateral_token {
113 borrow = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage) as u64;
114 collateral =
115 increase_amount - fees::apply_swap_fee(fees::apply_tuna_protocol_fee(borrow, protocol_fee_rate, false)?, fusion_pool.fee_rate, false)?;
116 collateral = fees::reverse_apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
117 collateral = fees::reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
118
119 swap_input_amount = collateral + borrow;
120 } else {
121 let position_to_borrowed_token_price = if collateral_token == TOKEN_A { price } else { 1.0 / price };
122 let borrow_in_position_token = ceil((increase_amount as f64 * (leverage - 1.0)) / leverage);
123
124 borrow = ceil(borrow_in_position_token * position_to_borrowed_token_price) as u64;
125
126 let borrow_in_position_token_with_fees_applied = fees::apply_swap_fee(
127 fees::apply_tuna_protocol_fee(borrow_in_position_token as u64, protocol_fee_rate, false)?,
128 fusion_pool.fee_rate,
129 false,
130 )?;
131
132 collateral = increase_amount - borrow_in_position_token_with_fees_applied;
133 collateral = fees::reverse_apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
134
135 swap_input_amount = borrow;
136 }
137
138 let protocol_fee = calculate_tuna_spot_position_protocol_fee(
139 collateral_token,
140 borrowed_token,
141 collateral,
142 borrow,
143 protocol_fee_rate_on_collateral,
144 protocol_fee_rate,
145 );
146
147 swap_input_amount -= if swap_input_token_is_a { protocol_fee.a } else { protocol_fee.b };
148
149 if position_token == collateral_token {
150 estimated_amount = collateral - if collateral_token == TOKEN_A { protocol_fee.a } else { protocol_fee.b };
151 }
152
153 if swap_input_amount > 0 {
154 if let Some(tick_arrays) = tick_arrays {
155 let quote = swap_quote_by_input_token(swap_input_amount, swap_input_token_is_a, 0, fusion_pool, tick_arrays, None, None)?;
156 estimated_amount += quote.token_est_out;
157 min_swap_output_amount = try_get_min_amount_with_slippage_tolerance(quote.token_est_out, slippage_tolerance_bps)?;
158 let new_price = sqrt_price_to_price(quote.next_sqrt_price.into(), 1, 1);
159 price_impact = (new_price / price - 1.0).abs();
160 } else {
161 let (input_mint, output_mint) = if swap_input_token_is_a { (mint_a, mint_b) } else { (mint_b, mint_a) };
162
163 let quote = jupiter_swap_quote(input_mint, output_mint, swap_input_amount, Some(slippage_tolerance_bps as u64)).await?;
164
165 estimated_amount += quote.out_amount;
166 price_impact = quote.price_impact_pct;
167 min_swap_output_amount = quote.other_amount_threshold;
168 jupiter_swap_ix = Some(SwapInstruction {
169 data: quote.instruction.data,
170 accounts: quote.instruction.accounts,
171 address_lookup_table_addresses: quote.instruction.address_lookup_table_addresses,
172 });
173 }
174 }
175
176 Ok(IncreaseSpotPositionQuoteResult {
177 collateral,
178 borrow,
179 estimated_amount,
180 swap_input_amount,
181 min_swap_output_amount,
182 protocol_fee_a: protocol_fee.a,
183 protocol_fee_b: protocol_fee.b,
184 price_impact,
185 jupiter_swap_ix,
186 })
187}
188
189#[cfg(feature = "wasm")]
207#[wasm_bindgen(js_name = "getIncreaseSpotPositionQuote", skip_jsdoc)]
208pub async fn wasm_get_increase_spot_position_quote(
209 increase_amount: u64,
210 collateral_token: u8,
211 position_token: u8,
212 leverage: f64,
213 slippage_tolerance_bps: Option<u16>,
214 protocol_fee_rate: u16,
215 protocol_fee_rate_on_collateral: u16,
216 mint_a: Pubkey,
217 mint_b: Pubkey,
218 fusion_pool: FusionPoolFacade,
219 tick_arrays: Option<TickArrays>,
220) -> Result<JsValue, JsValue> {
221 let result = get_increase_spot_position_quote(
222 increase_amount,
223 collateral_token,
224 position_token,
225 leverage,
226 slippage_tolerance_bps,
227 protocol_fee_rate,
228 protocol_fee_rate_on_collateral,
229 mint_a,
230 mint_b,
231 fusion_pool,
232 tick_arrays,
233 )
234 .await
235 .map_err(|e| JsValue::from_str(e))?;
236
237 let serializer = Serializer::new().serialize_maps_as_objects(true);
238 let js_value = result.serialize(&serializer).unwrap();
239
240 Ok(js_value)
241}
242
243#[cfg_attr(feature = "wasm", wasm_expose)]
244pub struct DecreaseSpotPositionQuoteResult {
245 pub decrease_percent: u32,
247 pub required_swap_amount: u64,
251 pub estimated_amount: u64,
253 pub estimated_payable_debt: u64,
255 pub estimated_collateral_to_be_withdrawn: u64,
257 pub price_impact: f64,
259 pub jupiter_swap_ix: Option<SwapInstruction>,
261}
262
263pub async fn get_decrease_spot_position_quote(
279 decrease_amount: u64,
280 collateral_token: u8,
281 leverage: f64,
282 slippage_tolerance_bps: Option<u16>,
283 position_token: u8,
284 position_amount: u64,
285 position_debt: u64,
286 mint_a: Pubkey,
287 mint_b: Pubkey,
288 fusion_pool: FusionPoolFacade,
289 tick_arrays: Option<TickArrays>,
290) -> Result<DecreaseSpotPositionQuoteResult, CoreError> {
291 if collateral_token > TOKEN_B || position_token > TOKEN_B {
292 return Err(INVALID_ARGUMENTS.into());
293 }
294
295 if leverage < 1.0 {
296 return Err(INVALID_ARGUMENTS.into());
297 }
298
299 let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
300 let position_to_borrowed_token_price = if position_token == TOKEN_A { price } else { 1.0 / price };
301 let borrowed_token = if position_token == TOKEN_A { TOKEN_B } else { TOKEN_A };
302 let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(DEFAULT_SLIPPAGE_TOLERANCE_BPS);
303
304 let mut required_swap_amount: u64 = 0;
305 let mut price_impact = 0.0;
306 let mut jupiter_swap_ix: Option<SwapInstruction> = None;
307
308 let mut decrease_amount_in_position_token = if collateral_token == position_token {
309 decrease_amount
310 } else {
311 round(decrease_amount as f64 / position_to_borrowed_token_price) as u64
312 };
313
314 decrease_amount_in_position_token = position_amount.min(decrease_amount_in_position_token);
315
316 let decrease_percent = ((decrease_amount_in_position_token * HUNDRED_PERCENT as u64 / position_amount) as u32).min(HUNDRED_PERCENT);
317
318 let estimated_amount = position_amount * (HUNDRED_PERCENT - decrease_percent) as u64 / HUNDRED_PERCENT as u64;
319 let estimated_payable_debt = try_mul_div(position_debt, decrease_percent as u128, HUNDRED_PERCENT as u128, true)?;
320 let mut estimated_collateral_to_be_withdrawn = 0;
321
322 if let Some(tick_arrays) = tick_arrays {
323 let mut next_sqrt_price = fusion_pool.sqrt_price;
324
325 if collateral_token == position_token {
326 if position_debt > 0 {
327 let amount_out = position_debt * decrease_percent as u64 / HUNDRED_PERCENT as u64;
328 let swap = swap_quote_by_output_token(amount_out, borrowed_token == TOKEN_A, 0, fusion_pool, tick_arrays, None, None)?;
329 next_sqrt_price = swap.next_sqrt_price;
330 required_swap_amount = try_get_max_amount_with_slippage_tolerance(swap.token_est_in, slippage_tolerance_bps)?;
331 estimated_collateral_to_be_withdrawn = position_amount.saturating_sub(swap.token_est_in).saturating_sub(estimated_amount);
332 } else {
333 estimated_collateral_to_be_withdrawn = position_amount - estimated_amount;
334 }
335 } else {
336 let amount_in = position_amount - estimated_amount;
337 let swap = swap_quote_by_input_token(amount_in, position_token == TOKEN_A, 0, fusion_pool, tick_arrays, None, None)?;
338 next_sqrt_price = swap.next_sqrt_price;
339 required_swap_amount = try_get_min_amount_with_slippage_tolerance(swap.token_est_out, slippage_tolerance_bps)?;
340 estimated_collateral_to_be_withdrawn = swap.token_est_out.saturating_sub(estimated_payable_debt);
341 }
342
343 let new_price = sqrt_price_to_price(next_sqrt_price.into(), 1, 1);
344 price_impact = (new_price / price - 1.0).abs();
345 } else {
346 let (input_mint, output_mint) = if position_token == TOKEN_A { (mint_a, mint_b) } else { (mint_b, mint_a) };
347
348 let amount_in = if collateral_token == position_token {
349 if position_debt > 0 {
350 let mut amount_in = if position_token == TOKEN_A {
351 (position_debt as f64 / price) as u64
352 } else {
353 (position_debt as f64 * price) as u64
354 };
355 amount_in = try_get_max_amount_with_slippage_tolerance(amount_in, slippage_tolerance_bps)?;
356 amount_in = amount_in.min(position_amount);
357 estimated_collateral_to_be_withdrawn = position_amount.saturating_sub(amount_in).saturating_sub(estimated_amount);
358 amount_in
359 } else {
360 estimated_collateral_to_be_withdrawn = position_amount - estimated_amount;
361 0
362 }
363 } else {
364 position_amount - estimated_amount
365 };
366
367 if amount_in > 0 {
368 let quote = jupiter_swap_quote(input_mint, output_mint, amount_in, Some(slippage_tolerance_bps as u64)).await?;
369
370 price_impact = quote.price_impact_pct;
371 required_swap_amount = quote.other_amount_threshold;
372 jupiter_swap_ix = Some(SwapInstruction {
373 data: quote.instruction.data,
374 accounts: quote.instruction.accounts,
375 address_lookup_table_addresses: quote.instruction.address_lookup_table_addresses,
376 });
377
378 if collateral_token != position_token {
379 estimated_collateral_to_be_withdrawn = quote.out_amount.saturating_sub(estimated_payable_debt);
380 }
381 }
382 }
383
384 Ok(DecreaseSpotPositionQuoteResult {
385 decrease_percent,
386 estimated_payable_debt,
387 estimated_collateral_to_be_withdrawn,
388 required_swap_amount,
389 estimated_amount,
390 price_impact,
391 jupiter_swap_ix,
392 })
393}
394
395#[cfg(feature = "wasm")]
411#[wasm_bindgen(js_name = "getDecreaseSpotPositionQuote", skip_jsdoc)]
412pub async fn wasm_get_decrease_spot_position_quote(
413 decrease_amount: u64,
414 collateral_token: u8,
415 leverage: f64,
416 slippage_tolerance_bps: Option<u16>,
417 position_token: u8,
418 position_amount: u64,
419 position_debt: u64,
420 mint_a: Pubkey,
421 mint_b: Pubkey,
422 fusion_pool: FusionPoolFacade,
423 tick_arrays: Option<TickArrays>,
424) -> Result<JsValue, JsValue> {
425 let result = get_decrease_spot_position_quote(
426 decrease_amount,
427 collateral_token,
428 leverage,
429 slippage_tolerance_bps,
430 position_token,
431 position_amount,
432 position_debt,
433 mint_a,
434 mint_b,
435 fusion_pool,
436 tick_arrays,
437 )
438 .await
439 .map_err(|e| JsValue::from_str(e))?;
440
441 let serializer = Serializer::new().serialize_maps_as_objects(true);
442 let js_value = result.serialize(&serializer).unwrap();
443
444 Ok(js_value)
445}
446
447#[cfg_attr(feature = "wasm", wasm_expose)]
458pub fn get_spot_position_liquidation_price(position_token: u8, amount: u64, debt: u64, liquidation_threshold: u32) -> Result<f64, CoreError> {
459 if liquidation_threshold >= HUNDRED_PERCENT {
460 return Err(INVALID_ARGUMENTS);
461 }
462
463 if debt == 0 || amount == 0 {
464 return Ok(0.0);
465 }
466
467 let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
468
469 if position_token == TOKEN_A {
470 Ok(debt as f64 / (amount as f64 * liquidation_threshold_f))
471 } else {
472 Ok((amount as f64 * liquidation_threshold_f) / debt as f64)
473 }
474}
475
476#[cfg_attr(feature = "wasm", wasm_expose)]
492pub fn get_tradable_amount(
493 collateral_token: u8,
494 available_balance: u64,
495 leverage: f64,
496 position_token: u8,
497 position_amount: u64,
498 protocol_fee_rate: u16,
499 protocol_fee_rate_on_collateral: u16,
500 fusion_pool: FusionPoolFacade,
501 increase: bool,
502) -> Result<u64, CoreError> {
503 if collateral_token > TOKEN_B || position_token > TOKEN_B {
504 return Err(INVALID_ARGUMENTS.into());
505 }
506
507 if leverage < 1.0 {
508 return Err(INVALID_ARGUMENTS.into());
509 }
510
511 let add_leverage = |collateral: u64| -> Result<u64, CoreError> {
515 let mut collateral = fees::apply_tuna_protocol_fee(collateral, protocol_fee_rate_on_collateral, false)?;
516 if collateral_token != position_token {
517 collateral = fees::apply_swap_fee(collateral, fusion_pool.fee_rate, false)?;
518 }
519
520 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);
521 let total = (collateral as f64 / (1.0 - (fee_multiplier * (leverage - 1.0)) / leverage)) as u64;
522 Ok(total)
523 };
524
525 let available_to_trade = if increase {
526 add_leverage(available_balance)?
527 } else {
528 let price = sqrt_price_to_price(fusion_pool.sqrt_price.into(), 1, 1);
529 let position_to_opposite_token_price = if position_token == TOKEN_A { price } else { 1.0 / price };
530
531 if collateral_token == position_token {
532 position_amount
533 } else {
534 round(position_amount as f64 * position_to_opposite_token_price) as u64
535 }
536 };
537
538 Ok(available_to_trade)
539}
540
541struct JupiterSwapResult {
542 pub instruction: SwapInstruction,
543 pub out_amount: u64,
544 pub other_amount_threshold: u64,
545 pub price_impact_pct: f64,
546}
547
548async fn jupiter_swap_quote(input_mint: Pubkey, output_mint: Pubkey, amount: u64, slippage_bps: Option<u64>) -> Result<JupiterSwapResult, CoreError> {
549 let quote_config = QuoteConfig {
550 slippage_bps,
551 swap_mode: Some(SwapMode::ExactIn),
552 dexes: None,
553 exclude_dexes: None,
554 only_direct_routes: false,
555 as_legacy_transaction: None,
556 platform_fee_bps: None,
557 max_accounts: None,
558 };
559
560 let quote = jup_ag::quote(input_mint, output_mint, amount, quote_config)
561 .await
562 .map_err(|_| JUPITER_QUOTE_REQUEST_ERROR)?;
563
564 #[allow(deprecated)]
565 let swap_request = SwapRequest {
566 user_public_key: Default::default(),
567 wrap_and_unwrap_sol: None,
568 use_shared_accounts: Some(true),
569 fee_account: None,
570 compute_unit_price_micro_lamports: None,
571 prioritization_fee_lamports: PrioritizationFeeLamports::Auto,
572 as_legacy_transaction: None,
573 use_token_ledger: None,
574 destination_token_account: None,
575 quote_response: quote.clone(),
576 };
577
578 let swap_response = jup_ag::swap_instructions(swap_request)
579 .await
580 .map_err(|_| JUPITER_SWAP_INSTRUCTIONS_REQUEST_ERROR)?;
581
582 Ok(JupiterSwapResult {
583 instruction: SwapInstruction {
584 data: swap_response.swap_instruction.data,
585 accounts: swap_response.swap_instruction.accounts,
586 address_lookup_table_addresses: swap_response.address_lookup_table_addresses,
587 },
588 out_amount: quote.out_amount,
589 other_amount_threshold: quote.other_amount_threshold,
590 price_impact_pct: quote.price_impact_pct,
591 })
592}
593
594#[cfg_attr(feature = "wasm", wasm_expose)]
595pub fn calculate_tuna_spot_position_protocol_fee(
596 collateral_token: u8,
597 borrowed_token: u8,
598 collateral: u64,
599 borrow: u64,
600 protocol_fee_rate_on_collateral: u16,
601 protocol_fee_rate: u16,
602) -> TokenPair {
603 let collateral_a = if collateral_token == TOKEN_A { collateral } else { 0 };
604 let collateral_b = if collateral_token == TOKEN_B { collateral } else { 0 };
605 let borrow_a = if borrowed_token == TOKEN_A { borrow } else { 0 };
606 let borrow_b = if borrowed_token == TOKEN_B { borrow } else { 0 };
607
608 let protocol_fee_a = calculate_tuna_protocol_fee(collateral_a, borrow_a, protocol_fee_rate_on_collateral, protocol_fee_rate);
609 let protocol_fee_b = calculate_tuna_protocol_fee(collateral_b, borrow_b, protocol_fee_rate_on_collateral, protocol_fee_rate);
610
611 TokenPair {
612 a: protocol_fee_a,
613 b: protocol_fee_b,
614 }
615}
616
617#[cfg(all(test, not(feature = "wasm")))]
618mod tests {
619 use super::*;
620 use crate::assert_approx_eq;
621 use fusionamm_core::{
622 get_tick_array_start_tick_index, price_to_sqrt_price, sqrt_price_to_tick_index, TickArrayFacade, TickFacade, TICK_ARRAY_SIZE,
623 };
624 use solana_pubkey::pubkey;
625
626 const NATIVE_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
627 const TUNA_MINT: Pubkey = pubkey!("TUNAfXDZEdQizTMTh3uEvNvYqJmqFHZbEJt8joP4cyx");
628
629 fn test_fusion_pool(sqrt_price: u128) -> FusionPoolFacade {
630 let tick_current_index = sqrt_price_to_tick_index(sqrt_price);
631 FusionPoolFacade {
632 tick_current_index,
633 fee_rate: 3000,
634 liquidity: 10000000000000,
635 sqrt_price,
636 tick_spacing: 2,
637 ..FusionPoolFacade::default()
638 }
639 }
640
641 fn test_tick(liquidity_net: i128) -> TickFacade {
642 TickFacade {
643 initialized: true,
644 liquidity_net,
645 ..TickFacade::default()
646 }
647 }
648
649 fn test_tick_array(start_tick_index: i32) -> TickArrayFacade {
650 TickArrayFacade {
651 start_tick_index,
652 ticks: [test_tick(0); TICK_ARRAY_SIZE],
653 }
654 }
655
656 fn test_tick_arrays(fusion_pool: FusionPoolFacade) -> TickArrays {
657 let tick_spacing = fusion_pool.tick_spacing;
658 let tick_current_index = sqrt_price_to_tick_index(fusion_pool.sqrt_price);
659 let tick_array_start_index = get_tick_array_start_tick_index(tick_current_index, tick_spacing);
660
661 [
662 test_tick_array(tick_array_start_index),
663 test_tick_array(tick_array_start_index + TICK_ARRAY_SIZE as i32 * tick_spacing as i32),
664 test_tick_array(tick_array_start_index + TICK_ARRAY_SIZE as i32 * tick_spacing as i32 * 2),
665 test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32),
666 test_tick_array(tick_array_start_index - TICK_ARRAY_SIZE as i32 * tick_spacing as i32 * 2),
667 ]
668 .into()
669 }
670
671 #[test]
672 fn test_get_liquidation_price() {
673 assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5, 0, HUNDRED_PERCENT * 85 / 100), Ok(0.0));
674 assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 0, 5, HUNDRED_PERCENT * 85 / 100), Ok(0.0));
675 assert_eq!(get_spot_position_liquidation_price(TOKEN_A, 5, 800, HUNDRED_PERCENT * 85 / 100), Ok(188.23529411764707));
676 assert_eq!(get_spot_position_liquidation_price(TOKEN_B, 1000, 4, HUNDRED_PERCENT * 85 / 100), Ok(212.5));
677 }
678
679 #[tokio::test]
680 async fn increase_long_position_providing_token_a() {
681 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
682 let fusion_pool = test_fusion_pool(sqrt_price);
683
684 let quote = get_increase_spot_position_quote(
685 5_000_000_000,
686 TOKEN_A,
687 TOKEN_A,
688 5.0,
689 Some(0),
690 (HUNDRED_PERCENT / 100) as u16,
691 (HUNDRED_PERCENT / 200) as u16,
692 NATIVE_MINT,
693 TUNA_MINT,
694 fusion_pool,
695 Some(test_tick_arrays(fusion_pool)),
696 )
697 .await
698 .unwrap();
699
700 assert_eq!(quote.collateral, 1057165829);
701 assert_eq!(quote.borrow, 800000000);
702 assert_eq!(quote.min_swap_output_amount, 3_947_423_011);
703 assert_eq!(quote.estimated_amount, 4_999_303_011);
704 assert_eq!(quote.protocol_fee_a, 5285829);
705 assert_eq!(quote.protocol_fee_b, 8000000);
706 assert_eq!(quote.price_impact, 0.00035316176257027543);
707 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);
708 }
709
710 #[tokio::test]
711 async fn increase_long_position_providing_token_b() {
712 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
713 let fusion_pool = test_fusion_pool(sqrt_price);
714
715 let quote = get_increase_spot_position_quote(
716 5000_000_000,
717 TOKEN_B,
718 TOKEN_A,
719 5.0,
720 Some(0),
721 (HUNDRED_PERCENT / 100) as u16,
722 (HUNDRED_PERCENT / 200) as u16,
723 NATIVE_MINT,
724 TUNA_MINT,
725 fusion_pool,
726 Some(test_tick_arrays(fusion_pool)),
727 )
728 .await
729 .unwrap();
730
731 assert_eq!(quote.collateral, 1060346869);
732 assert_eq!(quote.borrow, 4000000000);
733 assert_eq!(quote.estimated_amount, 24_972_080_293);
734 assert_eq!(quote.protocol_fee_a, 0);
735 assert_eq!(quote.protocol_fee_b, 45301734);
736 assert_eq!(quote.price_impact, 0.0022373179716579372);
737 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);
738 }
739
740 #[tokio::test]
741 async fn increase_short_position_providing_a() {
742 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
743 let fusion_pool = test_fusion_pool(sqrt_price);
744
745 let quote = get_increase_spot_position_quote(
746 5_000_000_000,
747 TOKEN_A,
748 TOKEN_B,
749 5.0,
750 Some(0),
751 (HUNDRED_PERCENT / 100) as u16,
752 (HUNDRED_PERCENT / 200) as u16,
753 NATIVE_MINT,
754 TUNA_MINT,
755 fusion_pool,
756 Some(test_tick_arrays(fusion_pool)),
757 )
758 .await
759 .unwrap();
760
761 assert_eq!(quote.collateral, 1060346869);
762 assert_eq!(quote.borrow, 4000000000);
763 assert_eq!(quote.estimated_amount, 999_776_441);
764 assert_eq!(quote.protocol_fee_a, 45301734);
765 assert_eq!(quote.protocol_fee_b, 0);
766 assert_eq!(quote.price_impact, 0.0004470636400017991);
767 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);
768 }
769
770 #[tokio::test]
771 async fn increase_short_position_providing_b() {
772 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
773 let fusion_pool = test_fusion_pool(sqrt_price);
774
775 let quote = get_increase_spot_position_quote(
776 5000_000_000,
777 TOKEN_B,
778 TOKEN_B,
779 5.0,
780 Some(0),
781 (HUNDRED_PERCENT / 100) as u16,
782 (HUNDRED_PERCENT / 200) as u16,
783 NATIVE_MINT,
784 TUNA_MINT,
785 fusion_pool,
786 Some(test_tick_arrays(fusion_pool)),
787 )
788 .await
789 .unwrap();
790
791 assert_eq!(quote.collateral, 1057165829);
792 assert_eq!(quote.borrow, 20000000000);
793 assert_eq!(quote.estimated_amount, 4996_517_564);
794 assert_eq!(quote.protocol_fee_a, 200000000);
795 assert_eq!(quote.protocol_fee_b, 5285829);
796 assert_eq!(quote.price_impact, 0.0017633175413067637);
797 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);
798 }
799
800 #[tokio::test]
801 async fn increase_quote_with_slippage() {
802 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
803 let fusion_pool = test_fusion_pool(sqrt_price);
804
805 let quote = get_increase_spot_position_quote(
807 200_000,
808 TOKEN_B,
809 TOKEN_A,
810 5.0,
811 Some(1000),
812 0,
813 0,
814 NATIVE_MINT,
815 TUNA_MINT,
816 fusion_pool,
817 Some(test_tick_arrays(fusion_pool)),
818 )
819 .await
820 .unwrap();
821 assert_eq!(quote.min_swap_output_amount, 899_994);
822
823 let quote = get_increase_spot_position_quote(
825 200_000,
826 TOKEN_B,
827 TOKEN_A,
828 5.0,
829 Some(0),
830 0,
831 0,
832 NATIVE_MINT,
833 TUNA_MINT,
834 fusion_pool,
835 Some(test_tick_arrays(fusion_pool)),
836 )
837 .await
838 .unwrap();
839 assert_eq!(quote.min_swap_output_amount, 999_994);
840 }
841
842 #[tokio::test]
843 async fn decrease_non_leveraged_long_position_providing_a() {
844 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
845 let fusion_pool = test_fusion_pool(sqrt_price);
846
847 let quote = get_decrease_spot_position_quote(
848 1_000_000_000,
849 TOKEN_A,
850 1.0,
851 Some(0),
852 TOKEN_A,
853 5_000_000_000, 0, NATIVE_MINT,
856 TUNA_MINT,
857 fusion_pool,
858 Some(test_tick_arrays(fusion_pool)),
859 )
860 .await
861 .unwrap();
862
863 assert_eq!(quote.decrease_percent, 200000);
864 assert_eq!(quote.estimated_amount, 4_000_000_000);
865 assert_eq!(quote.estimated_payable_debt, 0);
866 assert_eq!(quote.estimated_collateral_to_be_withdrawn, 1_000_000_000);
867 assert_eq!(quote.price_impact, 0.0);
868 }
869
870 #[tokio::test]
871 async fn decrease_non_leveraged_long_position_providing_b() {
872 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
873 let fusion_pool = test_fusion_pool(sqrt_price);
874
875 let quote = get_decrease_spot_position_quote(
876 200_000_000,
877 TOKEN_B,
878 1.0,
879 Some(0),
880 TOKEN_A,
881 5_000_000_000, 0, NATIVE_MINT,
884 TUNA_MINT,
885 fusion_pool,
886 Some(test_tick_arrays(fusion_pool)),
887 )
888 .await
889 .unwrap();
890
891 assert_eq!(quote.decrease_percent, 200000);
892 assert_eq!(quote.estimated_amount, 4_000_000_000);
893 assert_eq!(quote.estimated_payable_debt, 0);
894 assert_eq!(quote.estimated_collateral_to_be_withdrawn, 199_391_108);
895 assert_eq!(quote.price_impact, 0.00008916842709072448);
896 }
897
898 #[tokio::test]
899 async fn decrease_long_position_providing_a() {
900 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
901 let fusion_pool = test_fusion_pool(sqrt_price);
902
903 let quote = get_decrease_spot_position_quote(
904 1_000_000_000,
905 TOKEN_A,
906 5.0,
907 Some(0),
908 TOKEN_A,
909 5_000_000_000, 800_000_000, NATIVE_MINT,
912 TUNA_MINT,
913 fusion_pool,
914 Some(test_tick_arrays(fusion_pool)),
915 )
916 .await
917 .unwrap();
918
919 assert_eq!(quote.decrease_percent, 200000);
920 assert_eq!(quote.estimated_amount, 4_000_000_000);
921 assert_eq!(quote.estimated_payable_debt, 160_000_000);
922 assert_eq!(quote.estimated_collateral_to_be_withdrawn, 197_564_069);
923 assert_eq!(quote.price_impact, 0.00007155289528004705);
924
925 let quote = get_decrease_spot_position_quote(
926 6_000_000_000,
927 TOKEN_A,
928 5.0,
929 Some(0),
930 TOKEN_A,
931 5_000_000_000, 800_000_000, NATIVE_MINT,
934 TUNA_MINT,
935 fusion_pool,
936 Some(test_tick_arrays(fusion_pool)),
937 )
938 .await
939 .unwrap();
940
941 assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
942 assert_eq!(quote.estimated_amount, 0);
943 }
944
945 #[tokio::test]
946 async fn decrease_long_position_providing_b() {
947 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
948 let fusion_pool = test_fusion_pool(sqrt_price);
949
950 let quote = get_decrease_spot_position_quote(
951 200_000_000,
952 TOKEN_B,
953 5.0,
954 Some(0),
955 TOKEN_A,
956 5_000_000_000, 800_000_000, NATIVE_MINT,
959 TUNA_MINT,
960 fusion_pool,
961 Some(test_tick_arrays(fusion_pool)),
962 )
963 .await
964 .unwrap();
965
966 assert_eq!(quote.estimated_amount, 4000000000);
967 assert_eq!(quote.decrease_percent, 200000);
968 assert_eq!(quote.estimated_payable_debt, 160_000_000);
969 assert_eq!(quote.estimated_collateral_to_be_withdrawn, 39_391_108);
970 assert_eq!(quote.price_impact, 0.00008916842709072448);
971
972 let quote = get_decrease_spot_position_quote(
973 1200_000_000,
974 TOKEN_B,
975 5.0,
976 Some(0),
977 TOKEN_A,
978 5_000_000_000, 800_000_000, NATIVE_MINT,
981 TUNA_MINT,
982 fusion_pool,
983 Some(test_tick_arrays(fusion_pool)),
984 )
985 .await
986 .unwrap();
987
988 assert_eq!(quote.estimated_amount, 0);
989 assert_eq!(quote.decrease_percent, HUNDRED_PERCENT);
990 }
991
992 #[tokio::test]
993 async fn tradable_amount_for_1x_long_position_providing_b() {
994 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
995 let fusion_pool = test_fusion_pool(sqrt_price);
996 let tick_arrays = Some(test_tick_arrays(fusion_pool));
997
998 let collateral_token = TOKEN_B;
999 let position_token = TOKEN_A;
1000 let leverage = 1.0;
1001 let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1002 let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1003 let available_balance = 200_000_000;
1004
1005 let tradable_amount = get_tradable_amount(
1006 collateral_token,
1007 available_balance,
1008 leverage,
1009 position_token,
1010 0,
1011 protocol_fee_rate,
1012 protocol_fee_rate_on_collateral,
1013 fusion_pool,
1014 true,
1015 )
1016 .unwrap();
1017 assert_eq!(tradable_amount, 198403000);
1018
1019 let quote = get_increase_spot_position_quote(
1020 tradable_amount,
1021 collateral_token,
1022 position_token,
1023 leverage,
1024 Some(0),
1025 protocol_fee_rate,
1026 protocol_fee_rate_on_collateral,
1027 NATIVE_MINT,
1028 TUNA_MINT,
1029 fusion_pool,
1030 tick_arrays,
1031 )
1032 .await
1033 .unwrap();
1034 assert_eq!(quote.collateral, available_balance);
1035 }
1036
1037 #[tokio::test]
1038 async fn tradable_amount_for_5x_long_position_providing_b() {
1039 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1040 let fusion_pool = test_fusion_pool(sqrt_price);
1041 let tick_arrays = Some(test_tick_arrays(fusion_pool));
1042
1043 let collateral_token = TOKEN_B;
1044 let position_token = TOKEN_A;
1045 let leverage = 5.0;
1046 let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1047 let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1048 let available_balance = 10_000_000;
1049
1050 let tradable_amount = get_tradable_amount(
1051 collateral_token,
1052 available_balance,
1053 leverage,
1054 position_token,
1055 0,
1056 protocol_fee_rate,
1057 protocol_fee_rate_on_collateral,
1058 fusion_pool,
1059 true,
1060 )
1061 .unwrap();
1062 assert_eq!(tradable_amount, 47154380);
1063
1064 let quote = get_increase_spot_position_quote(
1065 tradable_amount,
1066 collateral_token,
1067 position_token,
1068 leverage,
1069 Some(0),
1070 protocol_fee_rate,
1071 protocol_fee_rate_on_collateral,
1072 NATIVE_MINT,
1073 TUNA_MINT,
1074 fusion_pool,
1075 tick_arrays,
1076 )
1077 .await
1078 .unwrap();
1079 assert_eq!(quote.collateral, available_balance + 1);
1081 }
1082
1083 #[tokio::test]
1084 async fn tradable_amount_for_5x_long_position_providing_a() {
1085 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1086 let fusion_pool = test_fusion_pool(sqrt_price);
1087 let tick_arrays = Some(test_tick_arrays(fusion_pool));
1088
1089 let collateral_token = TOKEN_A;
1090 let position_token = TOKEN_A;
1091 let leverage = 5.0;
1092 let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1093 let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1094 let available_balance = 1_000_000_000;
1095
1096 let tradable_amount = get_tradable_amount(
1097 collateral_token,
1098 available_balance,
1099 leverage,
1100 position_token,
1101 0,
1102 protocol_fee_rate,
1103 protocol_fee_rate_on_collateral,
1104 fusion_pool,
1105 true,
1106 )
1107 .unwrap();
1108 assert_eq!(tradable_amount, 4729626953);
1109
1110 let quote = get_increase_spot_position_quote(
1111 tradable_amount,
1112 collateral_token,
1113 position_token,
1114 leverage,
1115 Some(0),
1116 protocol_fee_rate,
1117 protocol_fee_rate_on_collateral,
1118 NATIVE_MINT,
1119 TUNA_MINT,
1120 fusion_pool,
1121 tick_arrays,
1122 )
1123 .await
1124 .unwrap();
1125 assert_eq!(quote.collateral, available_balance);
1126 }
1128
1129 #[tokio::test]
1130 async fn tradable_amount_for_5x_short_position_providing_b() {
1131 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1132 let fusion_pool = test_fusion_pool(sqrt_price);
1133 let tick_arrays = Some(test_tick_arrays(fusion_pool));
1134
1135 let collateral_token = TOKEN_B;
1136 let position_token = TOKEN_B;
1137 let leverage = 5.0;
1138 let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1139 let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1140 let available_balance = 200_000_000;
1141
1142 let tradable_amount = get_tradable_amount(
1143 collateral_token,
1144 available_balance,
1145 leverage,
1146 position_token,
1147 0,
1148 protocol_fee_rate,
1149 protocol_fee_rate_on_collateral,
1150 fusion_pool,
1151 true,
1152 )
1153 .unwrap();
1154 assert_eq!(tradable_amount, 945925390);
1155
1156 let quote = get_increase_spot_position_quote(
1157 tradable_amount,
1158 collateral_token,
1159 position_token,
1160 leverage,
1161 Some(0),
1162 protocol_fee_rate,
1163 protocol_fee_rate_on_collateral,
1164 NATIVE_MINT,
1165 TUNA_MINT,
1166 fusion_pool,
1167 tick_arrays,
1168 )
1169 .await
1170 .unwrap();
1171 assert_eq!(quote.collateral, available_balance + 1);
1173 }
1175
1176 #[tokio::test]
1177 async fn tradable_amount_for_reducing_existing_long_position() {
1178 let sqrt_price = price_to_sqrt_price(200.0, 9, 6);
1179 let fusion_pool = test_fusion_pool(sqrt_price);
1180 let tick_arrays = Some(test_tick_arrays(fusion_pool));
1181
1182 for i in 0..2 {
1183 let collateral_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1184 let position_token = if i == 0 { TOKEN_A } else { TOKEN_B };
1185 let leverage = 5.0;
1186 let position_amount = 5_000_000_000;
1187 let position_debt = 800_000_000;
1188 let protocol_fee_rate = (HUNDRED_PERCENT / 100) as u16;
1189 let protocol_fee_rate_on_collateral = (HUNDRED_PERCENT / 200) as u16;
1190 let available_balance = 50_000_000_000;
1191
1192 let tradable_amount = get_tradable_amount(
1193 collateral_token,
1194 available_balance,
1195 leverage,
1196 position_token,
1197 position_amount,
1198 protocol_fee_rate,
1199 protocol_fee_rate_on_collateral,
1200 fusion_pool,
1201 false,
1202 )
1203 .unwrap();
1204 assert_eq!(tradable_amount, 5_000_000_000);
1205
1206 let quote = get_decrease_spot_position_quote(
1207 tradable_amount,
1208 collateral_token,
1209 5.0,
1210 Some(0),
1211 position_token,
1212 position_amount,
1213 position_debt,
1214 NATIVE_MINT,
1215 TUNA_MINT,
1216 fusion_pool,
1217 tick_arrays.clone(),
1218 )
1219 .await
1220 .unwrap();
1221
1222 assert_eq!(quote.estimated_amount, 0);
1223 }
1224 }
1225}