tycho_common/simulation/swap.rs
1use std::{collections::HashMap, fmt, fmt::Debug, sync::Arc};
2
3use itertools::Itertools;
4use num_bigint::BigUint;
5
6use crate::{
7 dto::ProtocolStateDelta,
8 models::{protocol::ProtocolComponent, token::Token},
9 simulation::{
10 errors::{SimulationError, TransitionError},
11 indicatively_priced::IndicativelyPriced,
12 protocol_sim::{Balances, BlockContext, Price, ProtocolSim},
13 },
14 Bytes,
15};
16
17/// Result type for swap simulation operations that may fail with a `SimulationError`.
18pub type SimulationResult<T> = Result<T, SimulationError>;
19
20/// Type alias for token addresses, represented as raw bytes.
21pub type TokenAddress = Bytes;
22
23/// Macro that generates parameter structs with embedded blockchain context.
24///
25/// This macro creates structs that automatically include a `Context` field and provides
26/// methods for managing blockchain context (block number and timestamp). All parameter
27/// structs used in swap simulations should be created with this macro to ensure consistent
28/// context handling.
29///
30/// # Generated Methods
31/// - `with_context(context: Context) -> Self` - Sets the blockchain context
32/// - `context() -> &Context` - Gets a reference to the current context
33///
34/// # Example
35/// ```rust
36/// params_with_context! {
37/// pub struct MyParams {
38/// token: TokenAddress,
39/// amount: BigUint,
40/// }
41/// }
42/// ```
43macro_rules! params_with_context {
44 (
45 $(#[$meta:meta])*
46 $vis:vis struct $name:ident $(<$($gen:tt),*>)? {
47 $($field:ident: $ty:ty),* $(,)?
48 }
49 ) => {
50 $(#[$meta])*
51 #[derive(Debug, Clone)]
52 $vis struct $name $(<$($gen),*>)? {
53 context: Context,
54 $($field: $ty,)*
55 }
56
57 impl $(<$($gen),*>)? $name $(<$($gen),*>)? {
58
59 pub fn with_context(mut self, context: Context) -> Self {
60 self.context = context;
61 self
62 }
63
64 pub fn context(&self) -> &Context {
65 &self.context
66 }
67 }
68 };
69}
70
71/// Blockchain context information used in swap simulations.
72///
73/// Carries the block a quote is expected to execute in. `None` means the caller did not state one,
74/// and time-sensitive protocols fall back to whatever the state was last pointed at.
75#[derive(Debug, Clone, Default)]
76pub struct Context {
77 block: Option<BlockContext>,
78}
79
80impl Context {
81 /// Targets the quote at `block`.
82 pub fn with_block(mut self, block: BlockContext) -> Self {
83 self.block = Some(block);
84 self
85 }
86
87 /// The block a quote is expected to execute in, if the caller stated one.
88 pub fn block(&self) -> Option<&BlockContext> {
89 self.block.as_ref()
90 }
91}
92
93params_with_context! {
94/// Parameters for requesting a swap quote from a pool.
95///
96/// Contains the tokens to swap between, the input amount, and whether the
97/// simulation should return a modified state to reflect the swap execution.
98pub struct QuoteParams<'a>{
99 token_in: &'a TokenAddress,
100 token_out: &'a TokenAddress,
101 amount: QuoteAmount,
102 return_new_state: bool,
103 }
104}
105
106#[derive(Debug, Clone)]
107pub enum QuoteAmount {
108 FixedIn(BigUint),
109 FixedOut(BigUint),
110}
111
112impl<'a> QuoteParams<'a> {
113 /// Creates new fixed input parameters with default settings (no state modification).
114 ///
115 /// # Arguments
116 /// * `token_in` - The token to sell
117 /// * `token_out` - The token to buy
118 /// * `amount` - The amount of input token to sell
119 pub fn fixed_in(
120 token_in: &'a TokenAddress,
121 token_out: &'a TokenAddress,
122 amount: BigUint,
123 ) -> SimulationResult<Self> {
124 if token_out == token_in {
125 return Err(SimulationError::InvalidInput(
126 "Quote tokens have to differ!".to_string(),
127 None,
128 ));
129 }
130 Ok(Self {
131 context: Context::default(),
132 token_in,
133 token_out,
134 amount: QuoteAmount::FixedIn(amount),
135 return_new_state: false,
136 })
137 }
138
139 /// Creates new fixed output parameters with default settings (no state modification).
140 ///
141 /// # Arguments
142 /// * `token_in` - The token to sell
143 /// * `token_out` - The token to buy
144 /// * `amount` - The amount of output token to buy
145 pub fn fixed_out(
146 token_in: &'a TokenAddress,
147 token_out: &'a TokenAddress,
148 amount: BigUint,
149 ) -> SimulationResult<Self> {
150 if token_out == token_in {
151 return Err(SimulationError::InvalidInput(
152 "Quote tokens have to differ!".to_string(),
153 None,
154 ));
155 }
156
157 Ok(Self {
158 context: Context::default(),
159 token_in,
160 token_out,
161 amount: QuoteAmount::FixedOut(amount),
162 return_new_state: false,
163 })
164 }
165
166 pub fn amount(&self) -> &QuoteAmount {
167 &self.amount
168 }
169
170 /// Configures the quote to modify the state during simulation.
171 ///
172 /// When enabled, the quote simulation will return an updated state
173 /// as if the swap was actually executed.
174 pub fn with_new_state(mut self) -> Self {
175 self.return_new_state = true;
176 self
177 }
178
179 /// Returns the input token address.
180 pub fn token_in(&self) -> &TokenAddress {
181 self.token_in
182 }
183
184 /// Returns the output token address.
185 pub fn token_out(&self) -> &TokenAddress {
186 self.token_out
187 }
188
189 /// Returns whether the simulation should return the post-swap state.
190 pub fn should_return_new_state(&self) -> bool {
191 self.return_new_state
192 }
193}
194
195params_with_context! {
196/// Parameters for querying swap limits from a pool.
197///
198/// Used to determine the minimum and maximum amounts that can be traded
199/// between two tokens in the pool.
200pub struct LimitsParams<'a> {
201 token_in: &'a TokenAddress,
202 token_out: &'a TokenAddress,
203}
204}
205
206impl<'a> LimitsParams<'a> {
207 /// Creates new parameters for querying swap limits.
208 ///
209 /// # Arguments
210 /// * `token_in` - The input token address
211 /// * `token_out` - The output token address
212 pub fn new(token_in: &'a TokenAddress, token_out: &'a TokenAddress) -> Self {
213 Self { context: Context::default(), token_in, token_out }
214 }
215
216 pub fn token_in(&self) -> &TokenAddress {
217 self.token_in
218 }
219
220 pub fn token_out(&self) -> &TokenAddress {
221 self.token_out
222 }
223}
224
225params_with_context! {
226/// Parameters for querying the spot price between two tokens.
227///
228/// Used to get the current marginal price for infinitesimally small trades
229/// between two tokens in a pool.
230pub struct MarginalPriceParams<'a> {
231 token_in: &'a TokenAddress,
232 token_out: &'a TokenAddress,
233}
234}
235
236impl<'a> MarginalPriceParams<'a> {
237 /// Creates new parameters for querying marginal price.
238 ///
239 /// # Arguments
240 /// * `token_in` - The input token address
241 /// * `token_out` - The output token address
242 pub fn new(token_in: &'a TokenAddress, token_out: &'a TokenAddress) -> Self {
243 Self { context: Context::default(), token_in, token_out }
244 }
245 pub fn token_in(&self) -> &TokenAddress {
246 self.token_in
247 }
248
249 pub fn token_out(&self) -> &TokenAddress {
250 self.token_out
251 }
252}
253
254/// Represents a swap fee as a fraction.
255///
256/// The fee is typically expressed as a decimal (e.g., 0.003 for 0.3%).
257pub struct SwapFee {
258 fee: f64,
259}
260
261impl SwapFee {
262 /// Creates a new swap fee.
263 ///
264 /// # Arguments
265 /// * `fee` - The fee as a decimal fraction (e.g., 0.003 for 0.3%)
266 pub fn new(fee: f64) -> Self {
267 Self { fee }
268 }
269
270 /// Returns the fee as a decimal fraction.
271 pub fn fee(&self) -> f64 {
272 self.fee
273 }
274}
275
276/// Represents the marginal price at which the next infinitesimal trade would execute.
277///
278/// This is the instantaneous price for very small trades at the current pool state,
279/// often different from the effective price of larger trades due to slippage.
280pub struct MarginalPrice {
281 price: f64,
282}
283
284impl MarginalPrice {
285 /// Creates a new marginal price.
286 ///
287 /// # Arguments
288 /// * `price` - The marginal price as token_out/token_in ratio
289 pub fn new(price: f64) -> Self {
290 Self { price }
291 }
292
293 /// Returns the marginal price value.
294 pub fn price(&self) -> f64 {
295 self.price
296 }
297}
298
299/// Result of a swap quote calculation.
300///
301/// Contains the expected output amount, gas cost, and optionally the new pool state
302/// if the quote was requested with state modification enabled.
303pub struct Quote {
304 amount_out: BigUint,
305 gas: BigUint,
306 new_state: Option<Arc<dyn SwapQuoter>>,
307}
308
309impl Quote {
310 /// Creates a new quote result.
311 ///
312 /// # Arguments
313 /// * `amount_out` - The amount of output tokens that would be received
314 /// * `gas` - The estimated gas cost for executing this swap, excluding token transfers cost
315 /// * `new_state` - The new pool state after the swap (if state modification was requested)
316 pub fn new(amount_out: BigUint, gas: BigUint, new_state: Option<Arc<dyn SwapQuoter>>) -> Self {
317 Self { amount_out, gas, new_state }
318 }
319
320 /// Returns the amount of output tokens.
321 pub fn amount_out(&self) -> &BigUint {
322 &self.amount_out
323 }
324
325 /// Returns the estimated swap gas cost excluding including token transfer cost.
326 pub fn gas(&self) -> &BigUint {
327 &self.gas
328 }
329
330 /// Returns the new pool state after the swap, if available.
331 pub fn new_state(&self) -> Option<Arc<dyn SwapQuoter>> {
332 self.new_state.clone()
333 }
334}
335
336/// Represents a numeric range with lower and upper bounds.
337///
338/// Used for specifying trading limits and constraints.
339pub struct Range {
340 lower: BigUint,
341 upper: BigUint,
342}
343
344impl Range {
345 /// Creates a new range with validation.
346 ///
347 /// # Arguments
348 /// * `lower` - The lower bound (must be <= upper)
349 /// * `upper` - The upper bound (must be >= lower)
350 ///
351 /// # Errors
352 /// Returns `SimulationError::InvalidInput` if lower > upper.
353 pub fn new(lower: BigUint, upper: BigUint) -> SimulationResult<Self> {
354 if lower > upper {
355 return Err(SimulationError::InvalidInput(
356 "Invalid range! Argument lower > upper".to_string(),
357 None,
358 ));
359 }
360 Ok(Self { lower, upper })
361 }
362
363 /// Returns the lower bound.
364 pub fn lower(&self) -> &BigUint {
365 &self.lower
366 }
367
368 /// Returns the upper bound.
369 pub fn upper(&self) -> &BigUint {
370 &self.upper
371 }
372}
373
374/// Defines the trading limits for input and output amounts in a swap.
375///
376/// Specifies the minimum and maximum amounts that can be traded through a pool
377/// for both input and output tokens.
378pub struct SwapLimits {
379 range_in: Range,
380 range_out: Range,
381}
382
383impl SwapLimits {
384 /// Creates new swap limits.
385 ///
386 /// # Arguments
387 /// * `range_in` - The valid range for input token amounts
388 /// * `range_out` - The valid range for output token amounts
389 pub fn new(range_in: Range, range_out: Range) -> Self {
390 Self { range_in, range_out }
391 }
392
393 /// Returns the input amount limits.
394 pub fn range_in(&self) -> &Range {
395 &self.range_in
396 }
397
398 /// Returns the output amount limits.
399 pub fn range_out(&self) -> &Range {
400 &self.range_out
401 }
402}
403
404params_with_context! {
405/// Parameters for applying protocol state transitions.
406///
407/// Contains the state delta and associated data needed to transition
408/// a pool's state in response to blockchain events.
409pub struct TransitionParams<'a> {
410 delta: ProtocolStateDelta,
411 tokens: &'a HashMap<Bytes, Token>,
412 balances: &'a Balances,
413 }
414}
415
416impl<'a> TransitionParams<'a> {
417 /// Creates new parameters for state transition.
418 ///
419 /// # Arguments
420 /// * `delta` - The protocol state change to apply
421 /// * `tokens` - Map of token addresses to token metadata
422 /// * `balances` - Current token balances in the system
423 pub fn new(
424 delta: ProtocolStateDelta,
425 tokens: &'a HashMap<Bytes, Token>,
426 balances: &'a Balances,
427 ) -> Self {
428 Self { context: Context::default(), delta, tokens, balances }
429 }
430
431 pub fn delta(&self) -> &ProtocolStateDelta {
432 &self.delta
433 }
434
435 pub fn tokens(&self) -> &HashMap<Bytes, Token> {
436 self.tokens
437 }
438
439 pub fn balances(&self) -> &Balances {
440 self.balances
441 }
442}
443
444/// Result of applying a state transition to a pool.
445///
446/// Currently, a placeholder struct that may be extended in the future
447/// to contain transition metadata or validation results.
448pub struct Transition {}
449
450impl Default for Transition {
451 /// Creates a new transition result.
452 fn default() -> Self {
453 Self {}
454 }
455}
456
457/// Defines constraints for advanced quote calculations.
458///
459/// These constraints allow sophisticated trading strategies by limiting swaps
460/// based on price thresholds or targeting specific pool states.
461#[derive(Debug, Clone, PartialEq)]
462pub enum SwapConstraint {
463 /// This mode will calculate the maximum trade that this pool can execute while respecting a
464 /// trade limit price.
465 #[non_exhaustive]
466 TradeLimitPrice {
467 /// The minimum acceptable price for the resulting trade, as a [Price] struct. The
468 /// resulting amount_out / amount_in must be >= trade_limit_price
469 limit: Price,
470 /// The tolerance as a fraction to be applied on top of (increasing) the trade
471 /// limit price, raising the acceptance threshold. This is used to loosen the acceptance
472 /// criteria for implementations of this method, but will never allow violating the trade
473 /// limit price itself.
474 tolerance: f64,
475 /// The minimum amount of token_in that must be used for this trade.
476 min_amount_in: Option<BigUint>,
477 /// The maximum amount of token_in that can be used for this trade.
478 max_amount_in: Option<BigUint>,
479 },
480
481 /// This mode will calculate the amount of token_in required to move the pool's marginal price
482 /// down to a target price, and the amount of token_out received.
483 ///
484 /// # Edge Cases and Limitations
485 ///
486 /// Computing the exact amount to move a pool's marginal price to a target has several
487 /// challenges:
488 /// - The definition of marginal price varies between protocols. It is usually not an attribute
489 /// of the pool but a consequence of its liquidity distribution and current state.
490 /// - For protocols with concentrated liquidity, the marginal price is discrete, meaning we
491 /// can't always find an exact trade amount to reach the target price.
492 /// - Not all protocols support analytical solutions for this problem, requiring numerical
493 /// methods.
494 #[non_exhaustive]
495 PoolTargetPrice {
496 /// The marginal price we want the pool to be after the trade, as a [Price] struct. The
497 /// pool's price will move down to this level as token_in is sold into it
498 target: Price,
499 /// The tolerance as a fraction of the resulting pool marginal price. After trading, the
500 /// pool's price will decrease to the interval `[target, target * (1 +
501 /// tolerance)]`.
502 tolerance: f64,
503 /// The lower bound for searching algorithms.
504 min_amount_in: Option<BigUint>,
505 /// The upper bound for searching algorithms.
506 max_amount_in: Option<BigUint>,
507 },
508}
509
510impl SwapConstraint {
511 /// Creates a trade limit price constraint.
512 ///
513 /// This constraint finds the maximum trade size while respecting a minimum price
514 /// threshold. See `SwapConstraint::TradeLimitPrice` for details.
515 ///
516 /// # Arguments
517 /// * `limit` - The minimum acceptable price for the trade
518 /// * `tolerance` - Additional tolerance as a fraction to loosen the constraint
519 pub fn trade_limit_price(limit: Price, tolerance: f64) -> Self {
520 SwapConstraint::TradeLimitPrice {
521 limit,
522 tolerance,
523 min_amount_in: None,
524 max_amount_in: None,
525 }
526 }
527
528 /// Creates a pool target price constraint.
529 ///
530 /// This constraint calculates the trade needed to move the pool's price to a
531 /// target level. See `SwapConstraint::PoolTargetPrice` for details.`
532 ///
533 /// # Arguments
534 /// * `target` - The desired final marginal price of the pool
535 /// * `tolerance` - Acceptable variance from the target price as a fraction
536 pub fn pool_target_price(target: Price, tolerance: f64) -> Self {
537 SwapConstraint::PoolTargetPrice {
538 target,
539 tolerance,
540 min_amount_in: None,
541 max_amount_in: None,
542 }
543 }
544
545 /// Adds a lower bound to the constraint's search range.
546 ///
547 /// # Arguments
548 /// * `lower` - The minimum amount_in to consider
549 ///
550 /// # Returns
551 /// The modified constraint with the lower bound applied.
552 pub fn with_lower_bound(mut self, lower: BigUint) -> SimulationResult<Self> {
553 match &mut self {
554 SwapConstraint::PoolTargetPrice { min_amount_in, .. } => {
555 *min_amount_in = Some(lower);
556 Ok(self)
557 }
558 SwapConstraint::TradeLimitPrice { min_amount_in, .. } => {
559 *min_amount_in = Some(lower);
560 Ok(self)
561 }
562 }
563 }
564
565 /// Adds an upper bound to the constraint's search range.
566 ///
567 /// # Arguments
568 /// * `upper` - The maximum amount_in to consider
569 ///
570 /// # Returns
571 /// The modified constraint with the upper bound applied.
572 pub fn with_upper_bound(mut self, upper: BigUint) -> SimulationResult<Self> {
573 match &mut self {
574 SwapConstraint::PoolTargetPrice { max_amount_in, .. } => {
575 *max_amount_in = Some(upper);
576 Ok(self)
577 }
578 SwapConstraint::TradeLimitPrice { max_amount_in, .. } => {
579 *max_amount_in = Some(upper);
580 Ok(self)
581 }
582 }
583 }
584}
585
586params_with_context! {
587/// Parameters for advanced swap queries with constraints.
588///
589/// Used for sophisticated swap calculations that respect price limits or target
590/// prices instead of given amount values.
591pub struct QuerySwapParams<'a> {
592 token_in: &'a TokenAddress,
593 token_out: &'a TokenAddress,
594 swap_constraint: SwapConstraint,
595}
596}
597
598impl<'a> QuerySwapParams<'a> {
599 /// Creates new parameters for constrained swap queries.
600 ///
601 /// # Arguments
602 /// * `token_in` - The input token address
603 /// * `token_out` - The output token metadata
604 /// * `swap_constraint` - The constraint to apply to the swap calculation
605 pub fn new(
606 token_in: &'a TokenAddress,
607 token_out: &'a TokenAddress,
608 swap_constraint: SwapConstraint,
609 ) -> Self {
610 Self { context: Context::default(), token_in, token_out, swap_constraint }
611 }
612
613 pub fn token_in(&self) -> &'a TokenAddress {
614 self.token_in
615 }
616
617 pub fn token_out(&self) -> &'a TokenAddress {
618 self.token_out
619 }
620
621 pub fn swap_constraint(&self) -> &SwapConstraint {
622 &self.swap_constraint
623 }
624}
625
626/// Result of an advanced swap calculation with constraints.
627///
628/// Contains the calculated swap amounts, optionally the new pool state,
629/// and price points traversed during calculation for optimization purposes.
630pub struct Swap {
631 /// The amount of token_in sold to the component
632 amount_in: BigUint,
633 /// The amount of token_out bought from the component
634 amount_out: BigUint,
635 /// The new state of the component after the swap
636 new_state: Option<Arc<dyn SwapQuoter>>,
637 /// Optional price points that the pool was transitioned through while computing this swap.
638 /// The values are tuples of (amount_in, amount_out, price). This is useful for repeated calls
639 /// by providing good bounds for the next call.
640 price_points: Option<Vec<PricePoint>>,
641}
642
643/// A point on the AMM price curve.
644///
645/// Collected during iterative numerical search algorithms.
646/// These points can be reused as bounds for subsequent searches, improving convergence speed.
647#[derive(Debug, Clone)]
648pub struct PricePoint {
649 /// The amount of token_in in atomic units (wei).
650 amount_in: BigUint,
651 /// The amount of token_out in atomic units (wei).
652 amount_out: BigUint,
653 /// The price in units of `[token_out/token_in]` scaled by decimals.
654 ///
655 /// Computed as `(amount_out / 10^token_out_decimals) / (amount_in / 10^token_in_decimals)`.
656 price: f64,
657}
658
659impl PricePoint {
660 pub fn amount_in(&self) -> &BigUint {
661 &self.amount_in
662 }
663
664 pub fn amount_out(&self) -> &BigUint {
665 &self.amount_out
666 }
667
668 pub fn price(&self) -> f64 {
669 self.price
670 }
671}
672
673impl Swap {
674 /// Creates a new swap result.
675 ///
676 /// # Arguments
677 /// * `amount_in` - The amount of input tokens used
678 /// * `amount_out` - The amount of output tokens received
679 /// * `new_state` - The new pool state after the swap (if calculated)
680 /// * `price_points` - Optional price trajectory data for optimization
681 pub fn new(
682 amount_in: BigUint,
683 amount_out: BigUint,
684 new_state: Option<Arc<dyn SwapQuoter>>,
685 price_points: Option<Vec<PricePoint>>,
686 ) -> Self {
687 Self { amount_in, amount_out, new_state, price_points }
688 }
689
690 /// Returns the amount of input tokens used.
691 pub fn amount_in(&self) -> &BigUint {
692 &self.amount_in
693 }
694
695 /// Returns the amount of output tokens received.
696 pub fn amount_out(&self) -> &BigUint {
697 &self.amount_out
698 }
699
700 /// Returns the new pool state after the swap, if calculated.
701 pub fn new_state(&self) -> Option<Arc<dyn SwapQuoter>> {
702 self.new_state.clone()
703 }
704
705 /// Returns the price points traversed during calculation.
706 ///
707 /// Each tuple contains (amount_in, amount_out, price) at various points
708 /// during the swap calculation, useful for optimizing subsequent calls.
709 pub fn price_points(&self) -> &Option<Vec<PricePoint>> {
710 &self.price_points
711 }
712}
713
714/// Core trait for implementing swap quote functionality.
715///
716/// This trait defines the interface that all liquidity sources must implement
717/// to participate in swap quotes. It provides methods for price discovery,
718/// quote calculation, state transitions, and advanced swap queries.
719///
720/// Implementations should be thread-safe and support cloning for parallel simulations.
721#[typetag::serde(tag = "protocol", content = "state")]
722pub trait SwapQuoter: fmt::Debug + Send + Sync + 'static {
723 /// Returns the [`ProtocolComponent`] describing the protocol instance this quoter
724 /// is associated with.
725 ///
726 /// The component provides **structural and descriptive metadata** about the protocol,
727 /// such as the set of involved tokens and protocol-specific configuration, but does not
728 /// represent a mutable simulation state.
729 ///
730 /// # Semantics
731 ///
732 /// - The returned component is expected to be **stable for the lifetime of the quoter**.
733 /// - Multiple quoter instances may share the same component instance; callers should not assume
734 /// unique ownership (e.g. quoter instances may represent the state at different points in
735 /// time)
736 /// - The component is used for discovery and introspection (e.g. determining supported tokens
737 /// or deriving default quotable pairs), not for executing swaps.
738 ///
739 /// # Ownership
740 ///
741 /// This method returns an `Arc` to allow cheap cloning and shared access without
742 /// constraining the internal storage strategy of the implementation.
743 ///
744 /// # Intended use
745 ///
746 /// Typical uses include:
747 /// - Inspecting the tokens and configuration exposed by the protocol
748 /// - Deriving default [`quotable_pairs`](Self::quotable_pairs)
749 /// - Identifying or grouping quoters by protocol metadata
750 fn component(&self) -> Arc<ProtocolComponent<Arc<Token>>>;
751
752 /// Returns the set of **directed token pairs** for which this quoter can produce swap quotes.
753 ///
754 /// Each `(base, quote)` pair indicates that a swap from `base` to `quote` is supported.
755 /// Direction matters: `(A, B)` and `(B, A)` are considered distinct pairs and might not
756 /// both be present.
757 ///
758 /// # Semantics
759 ///
760 /// - The returned set represents **capability**, not liquidity or pricing guarantees. A pair
761 /// being present does not imply that a quote will be favorable or even currently executable,
762 /// only that the quoter understands how to price it.
763 /// - The set may be **computed dynamically** and is not required to be stable across calls,
764 /// though most implementations are expected to return the same result unless the underlying
765 /// protocol configuration changes.
766 ///
767 /// # Default behavior
768 ///
769 /// The default implementation derives the pairs from the tokens exposed by
770 /// [`component()`](Self::component), returning all ordered pairs `(a, b)` where `a != b`.
771 /// Protocols with restricted or asymmetric support (e.g. RFQ-based or single-sided
772 /// designs) should override this method.
773 ///
774 ///
775 /// # Intended use
776 ///
777 /// This method is primarily intended for routing, discovery, and validation logic,
778 /// allowing callers to determine whether a quote request is meaningful before invoking
779 /// [`quote()`](Self::quote).
780 fn quotable_pairs(&self) -> Vec<(Arc<Token>, Arc<Token>)> {
781 let component = self.component();
782 component
783 .tokens
784 .iter()
785 .permutations(2)
786 .map(|token| (token[0].clone(), token[1].clone()))
787 .collect()
788 }
789
790 /// Computes the protocol fee applicable to a prospective swap described by `params`.
791 ///
792 /// This method evaluates the fee that would be charged by the protocol for the given
793 /// swap parameters, without performing the swap or mutating any internal state.
794 ///
795 /// # Semantics
796 ///
797 /// - The returned [`SwapFee`] represents the **protocol-defined fee component** of the swap
798 /// (e.g. LP fee, protocol fee, or RFQ spread), as understood by this quoter.
799 /// - Fee computation is **pure and side-effect free**; calling this method must not modify the
800 /// internal state of the quoter.
801 /// - The fee may depend on the full set of quote parameters (including direction, amount, or
802 /// other protocol-specific inputs).
803 ///
804 /// # Relation to quoting
805 ///
806 /// Implementations may internally reuse logic from [`quote()`](Self::quote), but this method
807 /// exists to allow callers to:
808 /// - Inspect or decompose pricing components
809 /// - Perform fee-aware routing or optimization
810 /// - Estimate costs without requesting a full quote
811 ///
812 /// # Errors
813 ///
814 /// Returns an error if the fee cannot be determined for the given parameters (e.g. the
815 /// pair is not quotable or required inputs are missing)
816 fn fee(&self, params: QuoteParams) -> SimulationResult<SwapFee>;
817
818 /// Computes the **marginal (infinitesimal) price** for a swap described by `params`,
819 /// with **all protocol fees included**.
820 ///
821 /// The marginal price represents the instantaneous exchange rate at the current
822 /// protocol state, evaluated at an infinitesimally small trade size. It reflects the
823 /// derivative of output amount with respect to input amount for the specified swap
824 /// direction, inclusive of any protocol-defined fees or spreads.
825 ///
826 /// # Semantics
827 ///
828 /// - Fees are **always included** in the returned [`MarginalPrice`].
829 /// - The price is evaluated **at the margin** and does not represent an executable price for a
830 /// finite trade.
831 /// - This method is **pure and side-effect free**; it must not mutate internal state.
832 /// - For sufficiently small trade sizes, the marginal price should be consistent with
833 /// [`quote()`](Self::quote), up to numerical precision.
834 ///
835 /// # Use cases
836 ///
837 /// Typical uses include:
838 /// - Price display and monitoring
839 /// - Slippage estimation and sensitivity analysis
840 /// - Routing heuristics and initial path selection
841 ///
842 /// # Errors
843 ///
844 /// Returns an error if the marginal price is undefined or cannot be computed for the
845 /// given parameters (e.g. unsupported pair, zero liquidity, or missing inputs).
846 fn marginal_price(&self, params: MarginalPriceParams) -> SimulationResult<MarginalPrice>;
847
848 /// Produces a swap quote for the trade described by `params`, with **all protocol fees
849 /// included**.
850 ///
851 /// The returned [`Quote`] represents the effective execution terms of the swap as
852 /// understood by this quoter, including any protocol-defined fees, spreads, or
853 /// adjustments. Calling this method does not perform the swap and does not mutate
854 /// internal state.
855 ///
856 /// # Semantics
857 ///
858 /// - Fees are **always included** in the quoted price and amounts. Callers should not apply
859 /// additional protocol fees on top of the returned quote.
860 /// - Fees mentioned above **do not include** chain fees such as gas costs.
861 /// - The quote reflects a **finite-size trade** and therefore accounts for price impact where
862 /// applicable.
863 /// - This method is **pure and side-effect free**; it must not mutate internal state.
864 /// - For sufficiently small trade sizes, the quote should be consistent with
865 /// [`marginal_price()`](Self::marginal_price), up to numerical precision.
866 ///
867 /// # Intended use
868 ///
869 /// This method is the primary entry point for consumers of [`SwapQuoter`] and is
870 /// intended for:
871 /// - User-facing price discovery
872 /// - Routing and optimization across multiple quoters
873 /// - Simulation and what-if analysis
874 ///
875 /// # Errors
876 ///
877 /// Returns an error if a quote cannot be produced for the given parameters (e.g. the
878 /// pair is not quotable, required inputs are missing, or the quote is undefined).
879 fn quote(&self, params: QuoteParams) -> SimulationResult<Quote>;
880
881 /// Returns the valid execution limits for a prospective quote
882 ///
883 /// The returned [`SwapLimits`] describes the bounds within which a swap can be quoted or
884 /// simulated, such as minimum and maximum input or output amounts, given the current
885 /// protocol state.
886 ///
887 /// # Semantics
888 ///
889 /// - Limits are evaluated **at the current state** of the quoter and do not imply that a quote
890 /// will succeed outside the returned bounds.
891 /// - The limits may depend on swap direction, fees, liquidity constraints, or protocol-specific
892 /// rules.
893 /// - This method is **pure and side-effect free**; it must not mutate internal state.
894 /// - Limits are expressed in **fee-inclusive terms**, consistent with [`quote()`](Self::quote)
895 /// and [`marginal_price()`](Self::marginal_price).
896 ///
897 /// # Intended use
898 ///
899 /// Typical uses include:
900 /// - Pre-validating quote requests before quoting
901 /// - Bounding search spaces for routing and optimization
902 /// - UI validation and input clamping
903 ///
904 /// # Errors
905 ///
906 /// Returns an error if limits cannot be determined for the given parameters (e.g. the
907 /// pair is not quotable or required inputs are missing).
908 fn swap_limits(&self, params: LimitsParams) -> SimulationResult<SwapLimits>;
909
910 /// Produces an **advanced, price-constraint-based quote**
911 ///
912 /// Unlike [`quote()`](Self::quote), which prices a swap for a fixed input or output amount,
913 /// `query_swap` solves for a swap that satisfies a higher-level [`SwapConstraint`],
914 /// such as a minimum execution price or a target post-trade pool price.
915 ///
916 /// The returned [`Swap`] describes the swap that best satisfies the given constraint
917 /// under the current protocol state, with **all protocol fees included**.
918 ///
919 /// # Semantics
920 ///
921 /// - The method is **read-only** and does not mutate internal state.
922 /// - All amounts and prices in the returned [`Swap`] are **fee-inclusive**, consistent with
923 /// [`quote()`](Self::quote) and [`marginal_price()`](Self::marginal_price).
924 /// - The constraint defines *what is solved for* (e.g. maximum trade size, target price),
925 /// rather than supplying an explicit trade amount.
926 /// - Implementations may use analytical or numerical methods to satisfy the constraint, subject
927 /// to the provided tolerances and bounds.
928 ///
929 /// # Supported constraints
930 ///
931 /// The behavior of this method is defined by the [`SwapConstraint`] provided in
932 /// `params`, including:
933 ///
934 /// - **Trade limit price**: computes the maximum executable trade size whose effective price
935 /// (amount_out / amount_in) meets or exceeds a specified limit.
936 /// - **Pool target price**: computes the trade required to move the pool’s marginal price down
937 /// to a target level, within a specified tolerance.
938 ///
939 /// Bounds on the search space (minimum and maximum `amount_in`) are respected when
940 /// provided.
941 ///
942 /// # Intended use
943 ///
944 /// Typical uses include:
945 /// - Price-impact-aware execution planning
946 /// - Strategy-driven routing (e.g. price-capped or price-targeting trades)
947 /// - Liquidity probing and pool sensitivity analysis
948 ///
949 /// # Errors
950 ///
951 /// Returns an error if:
952 /// - The constraint cannot be satisfied within the provided bounds or tolerances
953 /// - The token pair is not quotable
954 /// - The swap is undefined under the current protocol state
955 fn query_swap(&self, params: QuerySwapParams) -> SimulationResult<Swap>;
956
957 /// Applies a **protocol state delta**
958 ///
959 /// This method updates the internal protocol state of the quoter by applying the
960 /// incremental changes described by `params`, typically derived from an external
961 /// indexer or on-chain data source. It is used to keep the quoter’s local view of the
962 /// protocol state in sync with the latest block.
963 ///
964 /// # Semantics
965 ///
966 /// - The provided delta is assumed to represent a **valid, externally observed state
967 /// transition** (e.g. from an indexer service) and is not re-validated as a swap.
968 /// - Calling this method **mutates internal state**; all subsequent quotes, prices, and limits
969 /// will reflect the updated state.
970 /// - The transition is applied **incrementally** and is expected to be composable with previous
971 /// deltas.
972 ///
973 /// # Intended use
974 ///
975 /// Typical uses include:
976 /// - Applying per-block protocol updates received from an indexer
977 /// - Advancing local state during historical replay or backfilling
978 /// - Keeping multiple quoters synchronized with chain state
979 ///
980 /// # Errors
981 ///
982 /// Returns an error if the delta cannot be applied to the current state (e.g. it is
983 /// incompatible, malformed, or violates protocol invariants).
984 fn delta_transition(&mut self, params: TransitionParams)
985 -> Result<Transition, TransitionError>;
986
987 /// Clones the protocol state as a trait object.
988 ///
989 /// This method enables cloning when the pool is used as a boxed trait object,
990 /// which is necessary for parallel simulations and state management.
991 ///
992 /// # Returns
993 /// A new boxed instance with the same state as this pool.
994 fn clone_box(&self) -> Box<dyn SwapQuoter>;
995
996 /// Attempts to cast this pool to an indicatively priced pool.
997 ///
998 /// This is used for RFQ (Request for Quote) protocols that provide
999 /// indicative pricing rather than deterministic calculations.
1000 ///
1001 /// # Returns
1002 /// A reference to the `IndicativelyPriced` implementation, or an error
1003 /// if this pool type doesn't support indicative pricing.
1004 ///
1005 /// # Default Implementation
1006 /// Returns an error indicating that indicative pricing is not supported.
1007 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
1008 Err(SimulationError::FatalError("Pool State does not implement IndicativelyPriced".into()))
1009 }
1010
1011 #[deprecated(note = "ProtocolSim is deprecated. This method will be removed in v1.0.0")]
1012 fn to_protocol_sim(&self) -> Box<dyn ProtocolSim>;
1013}
1014
1015/// Testing extension trait for SwapQuoter implementations.
1016///
1017/// Provides additional methods needed for testing and validation
1018/// that are not part of the main SwapQuoter interface.
1019#[cfg(test)]
1020pub trait SwapQuoterTestExt {
1021 /// Compares this pool state with another for equality.
1022 ///
1023 /// This method is used in tests to verify that pool states
1024 /// are equivalent after various operations.
1025 ///
1026 /// # Arguments
1027 /// * `other` - Another SwapQuoter to compare against
1028 ///
1029 /// # Returns
1030 /// `true` if the pool states are equivalent, `false` otherwise.
1031 fn eq(&self, other: &dyn SwapQuoter) -> bool;
1032}