nautilus_model/defi/data/swap_trade_info.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use alloy_primitives::{U160, U256};
17use rust_decimal::prelude::ToPrimitive;
18use rust_decimal_macros::dec;
19
20use crate::{
21 defi::{
22 Token,
23 data::swap::RawSwapData,
24 tick_map::{
25 full_math::{DECIMAL_EXPONENT_MAX, FullMath},
26 sqrt_price_math::{decode_sqrt_price_x96_to_price_tokens_adjusted, price_from_u256},
27 },
28 },
29 enums::OrderSide,
30 types::{Price, Quantity, fixed::FIXED_PRECISION},
31};
32
33/// Trade information derived from raw swap data, normalized to market conventions.
34///
35/// This structure represents a Uniswap V3 swap translated into standard trading terminology
36/// (base/quote, buy/sell) for consistency with traditional financial data systems.
37///
38/// # Base/Quote Token Convention
39///
40/// Tokens are assigned base/quote roles based on their priority:
41/// - Higher priority token → base (asset being traded)
42/// - Lower priority token → quote (pricing currency)
43///
44/// This may differ from the pool's token0/token1 ordering. When token priority differs
45/// from pool ordering, we say the market is "inverted":
46/// - NOT inverted: token0=base, token1=quote
47/// - Inverted: token0=quote, token1=base
48///
49/// # Prices
50///
51/// - `spot_price`: Instantaneous pool price after the swap (from `sqrt_price_x96`)
52/// - `execution_price`: Average realized price for this swap (from amount ratio)
53///
54/// Both prices are in quote/base direction (e.g., USDC per WETH) and adjusted for token decimals.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct SwapTradeInfo {
57 /// The direction of the trade from the base token perspective.
58 pub order_side: OrderSide,
59 /// The absolute quantity of the base token involved in the swap.
60 pub quantity_base: Quantity,
61 /// The absolute quantity of the quote token involved in the swap.
62 pub quantity_quote: Quantity,
63 /// The instantaneous pool price after the swap (quote per base).
64 pub spot_price: Price,
65 /// The average realized execution price for this swap (quote per base).
66 pub execution_price: Price,
67 /// Whether the base/quote assignment differs from token0/token1 ordering.
68 pub is_inverted: bool,
69 /// The pool price before that swap executed(optional).
70 pub spot_price_before: Option<Price>,
71}
72
73impl SwapTradeInfo {
74 /// Sets the spot price before the swap for price impact and slippage calculations.
75 pub fn set_spot_price_before(&mut self, price: Price) {
76 self.spot_price_before = Some(price);
77 }
78
79 /// Calculates price impact in basis points (requires token references for decimal adjustment).
80 ///
81 /// Price impact measures the market movement caused by the swap size,
82 /// excluding fees. This is the percentage change in spot price from
83 /// before to after the swap.
84 ///
85 /// # Returns
86 /// Price impact in basis points (10000 = 100%)
87 ///
88 /// # Errors
89 ///
90 /// Returns an error if the spot price before the swap is not set or is zero.
91 pub fn get_price_impact_bps(&self) -> anyhow::Result<u32> {
92 if let Some(spot_price_before) = self.spot_price_before {
93 Self::check_spot_price_before(spot_price_before, PriceMetric::Impact)?;
94 let price_change = self.spot_price - spot_price_before;
95 let price_impact =
96 (price_change.as_decimal() / spot_price_before.as_decimal()).abs() * dec!(10_000);
97
98 Ok(price_impact.round().to_u32().unwrap_or(0))
99 } else {
100 anyhow::bail!("Cannot calculate price impact, the spot price before is not set");
101 }
102 }
103
104 /// Calculates slippage in basis points (requires token references for decimal adjustment).
105 ///
106 /// Slippage includes both price impact and fees, representing the total
107 /// deviation from the spot price before the swap. This measures the total
108 /// cost to the trader.
109 ///
110 /// # Returns
111 /// Total slippage in basis points (10000 = 100%)
112 ///
113 /// # Errors
114 ///
115 /// Returns an error if the spot price before the swap is not set or is zero.
116 pub fn get_slippage_bps(&self) -> anyhow::Result<u32> {
117 if let Some(spot_price_before) = self.spot_price_before {
118 Self::check_spot_price_before(spot_price_before, PriceMetric::Slippage)?;
119 let price_change = self.execution_price - spot_price_before;
120 let slippage =
121 (price_change.as_decimal() / spot_price_before.as_decimal()).abs() * dec!(10_000);
122
123 Ok(slippage.round().to_u32().unwrap_or(0))
124 } else {
125 anyhow::bail!("Cannot calculate slippage, the spot price before is not set")
126 }
127 }
128
129 fn check_spot_price_before(
130 spot_price_before: Price,
131 metric: PriceMetric,
132 ) -> anyhow::Result<()> {
133 let metric = metric.name();
134 anyhow::ensure!(
135 !spot_price_before.is_zero(),
136 "Cannot calculate {metric}, the spot price before is zero"
137 );
138 Ok(())
139 }
140}
141
142enum PriceMetric {
143 Impact,
144 Slippage,
145}
146
147impl PriceMetric {
148 const fn name(self) -> &'static str {
149 match self {
150 Self::Impact => "price impact",
151 Self::Slippage => "slippage",
152 }
153 }
154}
155
156/// Computation engine for deriving market-oriented trade info from raw swap data.
157///
158/// This calculator translates DEX's token0/token1 representation into standard
159/// trading terminology (base/quote, buy/sell) based on token priority.
160///
161/// # Token Priority and Inversion
162///
163/// The calculator determines which token is base vs quote by comparing token priorities.
164/// When the higher-priority token is token1 (not token0), the market is "inverted":
165///
166/// # Precision Handling
167///
168/// For tokens with more than 16 decimals, quantities and prices are automatically
169/// scaled down to `MAX_FLOAT_PRECISION` (16) to ensure safe f64 conversion while
170/// maintaining reasonable precision for practical trading purposes.
171#[derive(Debug)]
172pub struct SwapTradeInfoCalculator<'a> {
173 /// Reference to token0 from the pool.
174 token0: &'a Token,
175 /// Reference to token1 from the pool.
176 token1: &'a Token,
177 /// Whether the base/quote assignment differs from token0/token1 ordering.
178 ///
179 /// - `true`: token0=quote, token1=base (inverted)
180 /// - `false`: token0=base, token1=quote (normal)
181 pub is_inverted: bool,
182 /// Raw swap amounts and resulting sqrt price from the blockchain event.
183 raw_swap_data: RawSwapData,
184}
185
186impl<'a> SwapTradeInfoCalculator<'a> {
187 #[must_use]
188 pub fn new(token0: &'a Token, token1: &'a Token, raw_swap_data: RawSwapData) -> Self {
189 let is_inverted = token0.get_token_priority() < token1.get_token_priority();
190 Self {
191 token0,
192 token1,
193 is_inverted,
194 raw_swap_data,
195 }
196 }
197
198 /// Determines swap direction from amount signs.
199 ///
200 /// Returns `true` if swapping token0 for token1 (`zero_for_one`).
201 #[must_use]
202 pub fn zero_for_one(&self) -> bool {
203 self.raw_swap_data.amount0.is_positive()
204 }
205
206 /// Computes all trade information fields and returns a complete [`SwapTradeInfo`].
207 ///
208 /// Calculates order side, quantities, and prices from the raw swap data,
209 /// applying token priority rules and decimal adjustments. If the price before
210 /// the swap is provided, also computes price impact and slippage metrics.
211 ///
212 /// # Arguments
213 ///
214 /// * `sqrt_price_x96_before` - Optional square root price before the swap (Q96 format).
215 /// When provided, enables calculation of `spot_price_before`, price impact, and slippage.
216 ///
217 /// # Errors
218 ///
219 /// Returns an error if:
220 /// - A token decimal count exceeds `DECIMAL_EXPONENT_MAX` (77).
221 /// - A quantity or price calculation fails.
222 pub fn compute(&self, sqrt_price_x96_before: Option<U160>) -> anyhow::Result<SwapTradeInfo> {
223 let spot_price_before = if let Some(sqrt_price_x96_before) = sqrt_price_x96_before {
224 Some(decode_sqrt_price_x96_to_price_tokens_adjusted(
225 sqrt_price_x96_before,
226 self.token0.decimals,
227 self.token1.decimals,
228 self.is_inverted,
229 )?)
230 } else {
231 None
232 };
233
234 Ok(SwapTradeInfo {
235 order_side: self.order_side(),
236 quantity_base: self.quantity_base()?,
237 quantity_quote: self.quantity_quote()?,
238 spot_price: self.spot_price()?,
239 execution_price: self.execution_price()?,
240 is_inverted: self.is_inverted,
241 spot_price_before,
242 })
243 }
244
245 /// Determines the order side from the perspective of the determined base/quote tokens.
246 ///
247 /// Uses market convention where base is the asset being traded and quote is the pricing currency.
248 ///
249 /// # Returns
250 /// - `OrderSide::Buy` when buying base token (selling quote for base)
251 /// - `OrderSide::Sell` when selling base token (buying quote with base)
252 ///
253 /// # Logic
254 ///
255 /// The order side depends on:
256 /// 1. Which token is being bought/sold (from amount signs)
257 /// 2. Which token is base vs quote (from priority determination)
258 #[must_use]
259 pub fn order_side(&self) -> OrderSide {
260 let zero_for_one = self.zero_for_one();
261
262 if self.is_inverted {
263 // When inverted: token0=quote, token1=base
264 // - zero_for_one (sell token0/quote, buy token1/base) -> BUY base
265 // - one_for_zero (sell token1/base, buy token0/quote -> SELL base
266 if zero_for_one {
267 OrderSide::Buy
268 } else {
269 OrderSide::Sell
270 }
271 } else {
272 // When NOT inverted: token0=base, token1=quote
273 // - zero_for_one (sell token0/base, buy token1/quote) → SELL base
274 // - one_for_zero (sell token1/quote, buy token0/base) → BUY base
275 if zero_for_one {
276 OrderSide::Sell
277 } else {
278 OrderSide::Buy
279 }
280 }
281 }
282
283 /// Returns the quantity of the base token involved in the swap.
284 ///
285 /// This is always the amount of the base asset being traded,
286 /// regardless of whether it's token0 or token1 in the pool.
287 ///
288 /// # Returns
289 /// Absolute value of base token amount with proper decimals
290 ///
291 /// # Errors
292 ///
293 /// Returns an error if the amount cannot be converted to a valid `Quantity`.
294 pub fn quantity_base(&self) -> anyhow::Result<Quantity> {
295 let (amount, precision) = if self.is_inverted {
296 (
297 self.raw_swap_data.amount1.unsigned_abs(),
298 self.token1.decimals,
299 )
300 } else {
301 (
302 self.raw_swap_data.amount0.unsigned_abs(),
303 self.token0.decimals,
304 )
305 };
306
307 Quantity::from_u256(amount, precision).map_err(Into::into)
308 }
309
310 /// Returns the quantity of the quote token involved in the swap.
311 ///
312 /// This is always the amount of the quote (pricing) currency,
313 /// regardless of whether it's token0 or token1 in the pool.
314 ///
315 /// # Returns
316 /// Absolute value of quote token amount with proper decimals
317 ///
318 /// # Errors
319 ///
320 /// Returns an error if the amount cannot be converted to a valid `Quantity`.
321 pub fn quantity_quote(&self) -> anyhow::Result<Quantity> {
322 let (amount, precision) = if self.is_inverted {
323 (
324 self.raw_swap_data.amount0.unsigned_abs(),
325 self.token0.decimals,
326 )
327 } else {
328 (
329 self.raw_swap_data.amount1.unsigned_abs(),
330 self.token1.decimals,
331 )
332 };
333
334 Quantity::from_u256(amount, precision).map_err(Into::into)
335 }
336
337 /// Returns the human-readable spot price in base/quote (market) convention.
338 ///
339 /// This is the instantaneous market price after the swap, adjusted for token decimals
340 /// to provide a human-readable value. This price does NOT include fees or slippage.
341 ///
342 /// # Returns
343 /// Price adjusted for token decimals in quote/base direction (market convention).
344 ///
345 /// # Base/Quote Logic
346 /// - When `is_inverted=false`: token0=base, token1=quote → returns token1/token0 (quote/base)
347 /// - When `is_inverted=true`: token0=quote, token1=base → returns token0/token1 (quote/base)
348 ///
349 /// # Use Cases
350 /// - Displaying current market price to users
351 /// - Calculating price impact: `(spot_after - spot_before) / spot_before`
352 /// - Comparing market rate vs execution rate
353 /// - Real-time price feeds
354 fn spot_price(&self) -> anyhow::Result<Price> {
355 // Pool always stores token1/token0
356 // When is_inverted=false: token0=base, token1=quote → want token1/token0 (quote/base) → don't invert
357 // When is_inverted=true: token0=quote, token1=base → want token0/token1 (quote/base) → invert
358 decode_sqrt_price_x96_to_price_tokens_adjusted(
359 self.raw_swap_data.sqrt_price_x96,
360 self.token0.decimals,
361 self.token1.decimals,
362 self.is_inverted, // invert when base/quote differs from token0/token1
363 )
364 }
365
366 /// Calculates the average execution price for this swap (includes fees and slippage).
367 ///
368 /// This is the actual realized price paid/received in the swap, calculated from
369 /// the input and output amounts. This represents the true cost of the trade.
370 ///
371 /// # Returns
372 /// Price in quote/base direction (market convention), adjusted for token decimals.
373 ///
374 /// # Formula
375 /// ```text
376 /// price = (quote_amount / 10^quote_decimals) / (base_amount / 10^base_decimals)
377 /// = (quote_amount * 10^base_decimals) / (base_amount * 10^quote_decimals)
378 /// ```
379 ///
380 /// To preserve precision in U256 arithmetic, we scale by `10^FIXED_PRECISION`:
381 /// ```text
382 /// price_raw = (quote_amount * 10^base_decimals * 10^FIXED_PRECISION) / (base_amount * 10^quote_decimals)
383 /// ```
384 ///
385 /// # Base/Quote Logic
386 /// - When `is_inverted=false`: quote=token1, base=token0 → price = amount1/amount0
387 /// - When `is_inverted=true`: quote=token0, base=token1 → price = amount0/amount1
388 ///
389 /// # Use Cases
390 /// - Trade accounting and P&L calculation
391 /// - Comparing quoted vs executed prices
392 /// - Cost analysis (includes all fees and price impact)
393 /// - Performance reporting
394 fn execution_price(&self) -> anyhow::Result<Price> {
395 let amount0 = self.raw_swap_data.amount0.unsigned_abs();
396 let amount1 = self.raw_swap_data.amount1.unsigned_abs();
397
398 if amount0.is_zero() || amount1.is_zero() {
399 anyhow::bail!("Cannot calculate execution price with zero amounts");
400 }
401
402 // Determine base and quote amounts/decimals based on inversion
403 let (quote_amount, base_amount, quote_decimals, base_decimals) = if self.is_inverted {
404 // inverted: token0=quote, token1=base
405 (amount0, amount1, self.token0.decimals, self.token1.decimals)
406 } else {
407 // not inverted: token0=base, token1=quote
408 (amount1, amount0, self.token1.decimals, self.token0.decimals)
409 };
410
411 FullMath::check_decimal_exponent(base_decimals)?;
412 FullMath::check_decimal_exponent(quote_decimals)?;
413
414 let exponent =
415 i16::from(base_decimals) + i16::from(FIXED_PRECISION) - i16::from(quote_decimals);
416 let price_raw_u256 = if exponent >= 0 {
417 let exponent = u8::try_from(exponent)
418 .map_err(|_| anyhow::anyhow!("Decimal exponent {exponent} exceeds u8 range"))?;
419 let primary_exponent = exponent.min(DECIMAL_EXPONENT_MAX);
420 let secondary_exponent = exponent - primary_exponent;
421 let primary_scalar = FullMath::pow10(primary_exponent)?;
422 let secondary_scalar = FullMath::pow10(secondary_exponent)?;
423 FullMath::mul_div_scaled(
424 quote_amount,
425 U256::from(1),
426 base_amount,
427 &[primary_scalar, secondary_scalar],
428 )?
429 } else {
430 let divisor_exponent = u8::try_from(exponent.unsigned_abs())
431 .map_err(|_| anyhow::anyhow!("Decimal exponent {exponent} exceeds u8 range"))?;
432 let divisor = FullMath::pow10(divisor_exponent)?;
433 (quote_amount / base_amount) / divisor
434 };
435
436 price_from_u256(price_raw_u256)
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use std::str::FromStr;
443
444 use alloy_primitives::{I256, U160};
445 use rstest::{fixture, rstest};
446 use rust_decimal_macros::dec;
447
448 use super::*;
449 use crate::defi::{
450 stubs::{usdc, weth},
451 tick_map::{full_math::Q96_U160, tick_math::MAX_SQRT_RATIO},
452 };
453
454 #[fixture]
455 fn swap_trade_info() -> SwapTradeInfo {
456 SwapTradeInfo {
457 order_side: OrderSide::Buy,
458 quantity_base: Quantity::from("1"),
459 quantity_quote: Quantity::from("2"),
460 spot_price: Price::from_raw(2, FIXED_PRECISION),
461 execution_price: Price::from_raw(3, FIXED_PRECISION),
462 is_inverted: true,
463 spot_price_before: Some(Price::from_raw(1, FIXED_PRECISION)),
464 }
465 }
466
467 #[rstest]
468 fn test_get_price_impact_bps_rejects_zero_spot_price_before(
469 mut swap_trade_info: SwapTradeInfo,
470 ) {
471 swap_trade_info.spot_price_before = Some(Price::zero(FIXED_PRECISION));
472
473 let error = swap_trade_info.get_price_impact_bps().unwrap_err();
474
475 assert_eq!(
476 error.to_string(),
477 "Cannot calculate price impact, the spot price before is zero"
478 );
479 }
480
481 #[rstest]
482 fn test_get_slippage_bps_rejects_zero_spot_price_before(mut swap_trade_info: SwapTradeInfo) {
483 swap_trade_info.spot_price_before = Some(Price::zero(FIXED_PRECISION));
484
485 let error = swap_trade_info.get_slippage_bps().unwrap_err();
486
487 assert_eq!(
488 error.to_string(),
489 "Cannot calculate slippage, the spot price before is zero"
490 );
491 }
492
493 #[rstest]
494 fn test_get_price_impact_bps_accepts_smallest_positive_spot_price_before(
495 swap_trade_info: SwapTradeInfo,
496 ) {
497 assert_eq!(swap_trade_info.get_price_impact_bps().unwrap(), 10_000);
498 }
499
500 #[rstest]
501 fn test_get_slippage_bps_accepts_smallest_positive_spot_price_before(
502 swap_trade_info: SwapTradeInfo,
503 ) {
504 assert_eq!(swap_trade_info.get_slippage_bps().unwrap(), 20_000);
505 }
506
507 #[rstest]
508 fn test_swap_trade_info_calculator_calculations_buy(weth: Token, usdc: Token) {
509 // Real Arbitrum transaction: https://arbiscan.io/tx/0xb9af1fd5eefe82650a5e0f8ff10b3a5e1c7f05f44f255e1335360df97bd1645a
510 let raw_data = RawSwapData::new(
511 I256::from_str("-466341596920355889").unwrap(),
512 I256::from_str("1656236893").unwrap(),
513 U160::from_str("4720799958938693700000000").unwrap(),
514 );
515
516 let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
517 let result = calculator.compute(None).unwrap();
518 // Its not inverted first is WETH(base) and second USDC(quote) as stablecoin
519 assert!(!calculator.is_inverted);
520 // Its buy, as amount0(WETH) < 0 (we received WETH, pool outflow) and amount1 > 0 (USDC sent, pool inflow)
521 assert_eq!(result.order_side, OrderSide::Buy);
522 assert_eq!(
523 result.quantity_base.as_decimal(),
524 dec!(0.466341596920355889)
525 );
526 assert_eq!(result.quantity_quote.as_decimal(), dec!(1656.236893));
527 assert_eq!(result.spot_price.as_decimal(), dec!(3550.3570265047994091));
528 assert_eq!(
529 result.execution_price.as_decimal(),
530 dec!(3551.5529902061477063)
531 );
532 }
533
534 #[rstest]
535 fn test_swap_trade_info_calculator_calculations_sell(weth: Token, usdc: Token) {
536 //Real Arbitrum transaction: https://arbiscan.io/tx/0x1fbedacf4a1cc7f76174d905c93d2f56d42335cadb4a782e2d74e3019107286b
537 let raw_data = RawSwapData::new(
538 I256::from_str("193450074461093702").unwrap(),
539 I256::from_str("-691892530").unwrap(),
540 U160::from_str("4739235524363817533004858").unwrap(),
541 );
542
543 let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
544 let result = calculator.compute(None).unwrap();
545 // Its sell as amount0(WETH) > 0 (we send WETH, pool inflow) and amount1 <0 (USDC received, pool outflow)
546 assert_eq!(result.order_side, OrderSide::Sell);
547 assert_eq!(
548 result.quantity_base.as_decimal(),
549 dec!(0.193450074461093702)
550 );
551 assert_eq!(result.quantity_quote.as_decimal(), dec!(691.89253));
552 assert_eq!(result.spot_price.as_decimal(), dec!(3578.1407251651610105));
553 assert_eq!(
554 result.execution_price.as_decimal(),
555 dec!(3576.5947980503469024)
556 );
557 }
558
559 #[rstest]
560 fn test_swap_trade_info_calculator_spot_price_overflow_is_recoverable(
561 weth: Token,
562 usdc: Token,
563 ) {
564 // A near-MAX_SQRT_RATIO swap overflows spot-price decoding, so compute must return a
565 // recoverable error rather than panic, letting the sync keep the swap with empty metadata.
566 let raw_data = RawSwapData::new(
567 I256::from_str("1").unwrap(),
568 I256::from_str("-1").unwrap(),
569 MAX_SQRT_RATIO - U160::from(1),
570 );
571
572 let calculator = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data);
573
574 assert!(calculator.compute(None).is_err());
575 }
576
577 #[rstest]
578 fn test_execution_price_scales_distinct_decimals_in_both_directions(weth: Token, usdc: Token) {
579 let normal_data = RawSwapData::new(
580 I256::from_str("-2000000000000000000").unwrap(),
581 I256::from_str("5000000").unwrap(),
582 Q96_U160,
583 );
584 let inverted_data = RawSwapData::new(
585 I256::from_str("5000000").unwrap(),
586 I256::from_str("-2000000000000000000").unwrap(),
587 Q96_U160,
588 );
589
590 let normal = SwapTradeInfoCalculator::new(&weth, &usdc, normal_data)
591 .execution_price()
592 .unwrap();
593 let inverted = SwapTradeInfoCalculator::new(&usdc, &weth, inverted_data)
594 .execution_price()
595 .unwrap();
596 let expected = Price::from_raw(25_000_000_000_000_000, FIXED_PRECISION);
597
598 assert_eq!(normal, expected);
599 assert_eq!(inverted, expected);
600 }
601
602 #[rstest]
603 fn test_execution_price_scales_negative_net_exponent(mut weth: Token, mut usdc: Token) {
604 weth.decimals = 0;
605 usdc.decimals = 18;
606 let raw_data = RawSwapData::new(
607 I256::from_str("1").unwrap(),
608 I256::from_str("-100").unwrap(),
609 Q96_U160,
610 );
611
612 let result = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
613 .execution_price()
614 .unwrap();
615
616 assert_eq!(result, Price::from_raw(1, FIXED_PRECISION));
617 }
618
619 #[rstest]
620 fn test_execution_price_accepts_largest_decimal_exponent(mut weth: Token, mut usdc: Token) {
621 weth.decimals = DECIMAL_EXPONENT_MAX;
622 usdc.decimals = 0;
623 let raw_data = RawSwapData::new(
624 I256::from_raw(FullMath::pow10(76).unwrap()),
625 I256::from_str("-1").unwrap(),
626 Q96_U160,
627 );
628
629 let result = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
630 .execution_price()
631 .unwrap();
632
633 assert_eq!(
634 result,
635 Price::from_raw(100_000_000_000_000_000, FIXED_PRECISION)
636 );
637 }
638
639 #[rstest]
640 fn test_execution_price_rejects_first_unsupported_decimal_exponent(
641 mut weth: Token,
642 mut usdc: Token,
643 ) {
644 weth.decimals = DECIMAL_EXPONENT_MAX + 1;
645 usdc.decimals = 0;
646 let raw_data = RawSwapData::new(
647 I256::from_str("1").unwrap(),
648 I256::from_str("-1").unwrap(),
649 Q96_U160,
650 );
651
652 let error = SwapTradeInfoCalculator::new(&weth, &usdc, raw_data)
653 .execution_price()
654 .unwrap_err();
655
656 assert_eq!(
657 error.to_string(),
658 "Decimal exponent 78 exceeds supported maximum 77"
659 );
660 }
661}