Skip to main content

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}