Skip to main content

cow_chains/
params.rs

1//! High-level executor configuration for `CoW` Protocol swap operations.
2//!
3//! Provides [`CowSwapConfig`] which bundles all the parameters needed to
4//! submit orders (chain, environment, tokens, slippage, TTL) and
5//! [`TokenRegistry`] which maps ticker symbols to on-chain addresses and
6//! decimal counts.
7
8use std::fmt;
9
10use alloy_primitives::Address;
11use foldhash::HashMap;
12
13use super::chain::{Env, SupportedChainId};
14
15/// Internal storage entry: `(token_address, decimals)`.
16type TokenEntry = (Address, u8);
17
18/// A registry mapping asset ticker symbols to their ERC-20 token metadata.
19///
20/// Stores both address and decimal precision so the executor can convert
21/// human-readable quantities to token atoms without assuming 18 decimals.
22///
23/// # Example
24///
25/// ```
26/// use alloy_primitives::Address;
27/// use cow_chains::TokenRegistry;
28///
29/// let mut reg = TokenRegistry::new_with_decimals([
30///     ("USDC", Address::ZERO, 6u8),
31///     ("WETH", Address::ZERO, 18u8),
32/// ]);
33/// assert_eq!(reg.get_decimals("USDC"), Some(6));
34/// assert!(reg.contains("WETH"));
35/// assert!(!reg.contains("DAI"));
36///
37/// reg.insert("DAI", Address::ZERO);
38/// assert!(reg.contains("DAI"));
39/// assert_eq!(reg.len(), 3);
40/// ```
41#[derive(Debug)]
42pub struct TokenRegistry {
43    inner: HashMap<String, TokenEntry>,
44}
45
46impl TokenRegistry {
47    /// Create a new registry from `(symbol, address)` pairs.
48    ///
49    /// All tokens registered this way are assumed to have **18 decimals**.
50    /// Use [`new_with_decimals`](Self::new_with_decimals) when tokens have
51    /// non-standard decimal counts (e.g. USDC = 6, WBTC = 8).
52    ///
53    /// # Parameters
54    ///
55    /// * `entries` — an iterator of `(symbol, address)` pairs.
56    ///
57    /// # Returns
58    ///
59    /// A new [`TokenRegistry`] with all entries set to 18 decimals.
60    #[must_use]
61    pub fn new(entries: impl IntoIterator<Item = (impl Into<String>, Address)>) -> Self {
62        Self { inner: entries.into_iter().map(|(k, v)| (k.into(), (v, 18))).collect() }
63    }
64
65    /// Create a new registry from `(symbol, address, decimals)` tuples.
66    ///
67    /// Use this when tokens have non-standard decimal counts (e.g. USDC = 6,
68    /// WBTC = 8).
69    ///
70    /// # Parameters
71    ///
72    /// * `entries` — an iterator of `(symbol, address, decimals)` tuples.
73    ///
74    /// # Returns
75    ///
76    /// A new [`TokenRegistry`] with explicit decimal counts per token.
77    #[must_use]
78    pub fn new_with_decimals(
79        entries: impl IntoIterator<Item = (impl Into<String>, Address, u8)>,
80    ) -> Self {
81        Self { inner: entries.into_iter().map(|(k, v, d)| (k.into(), (v, d))).collect() }
82    }
83
84    /// Look up the [`Address`] for a given asset symbol, e.g. `"WETH"`.
85    ///
86    /// # Arguments
87    ///
88    /// * `asset` — the ticker symbol to look up.
89    ///
90    /// # Returns
91    ///
92    /// `Some(address)` if the symbol is registered, `None` otherwise.
93    #[must_use]
94    pub fn get(&self, asset: &str) -> Option<Address> {
95        self.inner.get(asset).map(|&(addr, _)| addr)
96    }
97
98    /// Look up the decimal count for a given asset symbol.
99    ///
100    /// # Arguments
101    ///
102    /// * `asset` — the ticker symbol to look up.
103    ///
104    /// # Returns
105    ///
106    /// `Some(decimals)` when the symbol is registered, `None` otherwise.
107    #[must_use]
108    pub fn get_decimals(&self, asset: &str) -> Option<u8> {
109        self.inner.get(asset).map(|&(_, decimals)| decimals)
110    }
111
112    /// Register a token with 18 decimals (or update an existing entry).
113    ///
114    /// # Arguments
115    ///
116    /// * `symbol` — the ticker symbol to register.
117    /// * `address` — the ERC-20 contract [`Address`].
118    pub fn insert(&mut self, symbol: impl Into<String>, address: Address) {
119        self.inner.insert(symbol.into(), (address, 18));
120    }
121
122    /// Register a token with explicit decimals (or update an existing entry).
123    ///
124    /// # Arguments
125    ///
126    /// * `symbol` — the ticker symbol to register.
127    /// * `address` — the ERC-20 contract [`Address`].
128    /// * `decimals` — the token's decimal precision.
129    pub fn insert_with_decimals(
130        &mut self,
131        symbol: impl Into<String>,
132        address: Address,
133        decimals: u8,
134    ) {
135        self.inner.insert(symbol.into(), (address, decimals));
136    }
137
138    /// Returns `true` if `asset` is registered in this registry.
139    ///
140    /// # Arguments
141    ///
142    /// * `asset` — the ticker symbol to check.
143    ///
144    /// # Returns
145    ///
146    /// `true` when the symbol exists in the registry.
147    #[must_use]
148    pub fn contains(&self, asset: &str) -> bool {
149        self.inner.contains_key(asset)
150    }
151
152    /// Returns the number of registered tokens.
153    ///
154    /// # Returns
155    ///
156    /// The count of tokens in this registry.
157    #[must_use]
158    pub fn len(&self) -> usize {
159        self.inner.len()
160    }
161
162    /// Returns `true` if no tokens are registered.
163    ///
164    /// # Returns
165    ///
166    /// `true` when the registry contains zero tokens.
167    #[must_use]
168    pub fn is_empty(&self) -> bool {
169        self.inner.is_empty()
170    }
171
172    /// Look up both the address and decimal count for a given asset symbol.
173    ///
174    /// Returns `Some((address, decimals))` when registered, `None` otherwise.
175    ///
176    /// ```
177    /// use alloy_primitives::Address;
178    /// use cow_chains::TokenRegistry;
179    ///
180    /// let reg = TokenRegistry::new_with_decimals([("USDC", Address::ZERO, 6u8)]);
181    /// assert_eq!(reg.get_entry("USDC"), Some((Address::ZERO, 6)));
182    /// assert_eq!(reg.get_entry("WETH"), None);
183    /// ```
184    #[must_use]
185    pub fn get_entry(&self, asset: &str) -> Option<(Address, u8)> {
186        self.inner.get(asset).copied()
187    }
188
189    /// Remove a token from the registry.
190    ///
191    /// # Arguments
192    ///
193    /// * `asset` — the ticker symbol to remove.
194    ///
195    /// # Returns
196    ///
197    /// `Some((address, decimals))` if the symbol was registered, `None` otherwise.
198    pub fn remove(&mut self, asset: &str) -> Option<(Address, u8)> {
199        self.inner.remove(asset)
200    }
201}
202
203impl fmt::Display for TokenRegistry {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "registry({} tokens)", self.inner.len())
206    }
207}
208
209/// Configuration for the `CoW` Protocol swap executor.
210///
211/// Bundles all parameters needed to submit orders: target chain,
212/// environment, sell token, slippage, TTL, and a [`TokenRegistry`] mapping
213/// strategy symbols to on-chain addresses.
214///
215/// Construct via [`prod`](Self::prod) or [`staging`](Self::staging), then
216/// customise with the `with_*` builder methods.
217///
218/// # Example
219///
220/// ```
221/// use alloy_primitives::Address;
222/// use cow_chains::{CowSwapConfig, SupportedChainId, TokenRegistry};
223///
224/// let empty: Vec<(&str, Address)> = vec![];
225/// let config = CowSwapConfig::prod(
226///     SupportedChainId::Mainnet,
227///     Address::ZERO, // sell token
228///     TokenRegistry::new(empty),
229///     50,   // 0.5% slippage
230///     1800, // 30 min TTL
231/// );
232/// assert!(config.env.is_prod());
233/// assert_eq!(config.slippage_bps, 50);
234/// ```
235#[derive(Debug)]
236pub struct CowSwapConfig {
237    /// Target chain.
238    pub chain_id: SupportedChainId,
239    /// API environment (`Prod` or `Staging`).
240    pub env: Env,
241    /// The token used as the quote / sell currency (e.g. USDC on Sepolia).
242    pub sell_token: Address,
243    /// Decimal count for [`Self::sell_token`] (e.g. `6` for USDC, `18` for WETH).
244    pub sell_token_decimals: u8,
245    /// Registry mapping strategy asset symbols to their on-chain token addresses
246    /// and decimal counts.
247    pub tokens: TokenRegistry,
248    /// Slippage tolerance in basis points (e.g. `50` = 0.5 %).
249    pub slippage_bps: u32,
250    /// Default order TTL in seconds (e.g. `1800` = 30 min).
251    pub order_valid_secs: u32,
252    /// Optional override for the buy-token receiver address.
253    ///
254    /// When `None` the order receiver defaults to the signing wallet address.
255    pub receiver: Option<Address>,
256}
257
258impl CowSwapConfig {
259    /// Convenience constructor defaulting to the production environment.
260    ///
261    /// [`Self::sell_token_decimals`] defaults to `18`; use
262    /// [`with_sell_token_decimals`](Self::with_sell_token_decimals) for
263    /// tokens such as USDC (`6`) or WBTC (`8`).
264    ///
265    /// # Parameters
266    ///
267    /// * `chain_id` — the target [`SupportedChainId`].
268    /// * `sell_token` — the ERC-20 [`Address`] of the sell (quote) currency.
269    /// * `tokens` — the [`TokenRegistry`] mapping strategy symbols to tokens.
270    /// * `slippage_bps` — slippage tolerance in basis points (e.g. `50` = 0.5 %).
271    /// * `order_valid_secs` — order TTL in seconds (e.g. `1800` = 30 min).
272    ///
273    /// # Returns
274    ///
275    /// A new [`CowSwapConfig`] targeting [`Env::Prod`] with no custom receiver.
276    #[must_use]
277    pub const fn prod(
278        chain_id: SupportedChainId,
279        sell_token: Address,
280        tokens: TokenRegistry,
281        slippage_bps: u32,
282        order_valid_secs: u32,
283    ) -> Self {
284        Self {
285            chain_id,
286            env: Env::Prod,
287            sell_token,
288            sell_token_decimals: 18,
289            tokens,
290            slippage_bps,
291            order_valid_secs,
292            receiver: None,
293        }
294    }
295
296    /// Convenience constructor defaulting to the staging (barn) environment.
297    ///
298    /// Same parameters as [`prod`](Self::prod) but targets [`Env::Staging`]
299    /// (`barn.api.cow.fi`). [`Self::sell_token_decimals`] defaults to `18`.
300    ///
301    /// # Parameters
302    ///
303    /// See [`prod`](Self::prod) for parameter descriptions.
304    ///
305    /// # Returns
306    ///
307    /// A new [`CowSwapConfig`] targeting [`Env::Staging`].
308    #[must_use]
309    pub const fn staging(
310        chain_id: SupportedChainId,
311        sell_token: Address,
312        tokens: TokenRegistry,
313        slippage_bps: u32,
314        order_valid_secs: u32,
315    ) -> Self {
316        Self {
317            chain_id,
318            env: Env::Staging,
319            sell_token,
320            sell_token_decimals: 18,
321            tokens,
322            slippage_bps,
323            order_valid_secs,
324            receiver: None,
325        }
326    }
327
328    /// Override the sell token address.
329    ///
330    /// # Arguments
331    ///
332    /// * `token` — the new sell token [`Address`].
333    ///
334    /// # Returns
335    ///
336    /// `self` with the updated sell token.
337    #[must_use]
338    pub const fn with_sell_token(mut self, token: Address) -> Self {
339        self.sell_token = token;
340        self
341    }
342
343    /// Override the chain ID.
344    ///
345    /// # Arguments
346    ///
347    /// * `chain_id` — the new target [`SupportedChainId`].
348    ///
349    /// # Returns
350    ///
351    /// `self` with the updated chain ID.
352    #[must_use]
353    pub const fn with_chain_id(mut self, chain_id: SupportedChainId) -> Self {
354        self.chain_id = chain_id;
355        self
356    }
357
358    /// Override the API environment (`Prod` or `Staging`).
359    ///
360    /// # Arguments
361    ///
362    /// * `env` — the new [`Env`] value.
363    ///
364    /// # Returns
365    ///
366    /// `self` with the updated environment.
367    #[must_use]
368    pub const fn with_env(mut self, env: Env) -> Self {
369        self.env = env;
370        self
371    }
372
373    /// Override the slippage tolerance in basis points.
374    ///
375    /// # Arguments
376    ///
377    /// * `slippage_bps` — the new slippage in basis points (e.g. `50` = 0.5 %).
378    ///
379    /// # Returns
380    ///
381    /// `self` with the updated slippage.
382    #[must_use]
383    pub const fn with_slippage_bps(mut self, slippage_bps: u32) -> Self {
384        self.slippage_bps = slippage_bps;
385        self
386    }
387
388    /// Override the default order TTL in seconds.
389    ///
390    /// # Arguments
391    ///
392    /// * `secs` — the new TTL in seconds (e.g. `1800` = 30 min).
393    ///
394    /// # Returns
395    ///
396    /// `self` with the updated order TTL.
397    #[must_use]
398    pub const fn with_order_valid_secs(mut self, secs: u32) -> Self {
399        self.order_valid_secs = secs;
400        self
401    }
402
403    /// Override the decimal count for `sell_token` (defaults to `18`).
404    ///
405    /// # Arguments
406    ///
407    /// * `decimals` — the decimal precision of the sell token.
408    ///
409    /// # Returns
410    ///
411    /// `self` with the updated decimal count.
412    #[must_use]
413    pub const fn with_sell_token_decimals(mut self, decimals: u8) -> Self {
414        self.sell_token_decimals = decimals;
415        self
416    }
417
418    /// Override the order receiver address.
419    ///
420    /// # Arguments
421    ///
422    /// * `receiver` — the custom receiver [`Address`].
423    ///
424    /// # Returns
425    ///
426    /// `self` with the receiver override set.
427    #[must_use]
428    pub const fn with_receiver(mut self, receiver: Address) -> Self {
429        self.receiver = Some(receiver);
430        self
431    }
432
433    /// Returns `true` if a custom receiver address has been set.
434    ///
435    /// When `false`, the executor uses the signing wallet address as receiver.
436    ///
437    /// # Returns
438    ///
439    /// `true` when a receiver override is present.
440    #[must_use]
441    pub const fn has_custom_receiver(&self) -> bool {
442        self.receiver.is_some()
443    }
444
445    /// Return the effective receiver: the override if set, otherwise `default`.
446    ///
447    /// # Example
448    ///
449    /// ```
450    /// use alloy_primitives::{Address, address};
451    /// use cow_chains::{CowSwapConfig, SupportedChainId, TokenRegistry};
452    ///
453    /// let wallet = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
454    /// let empty: Vec<(&str, Address)> = vec![];
455    /// let config = CowSwapConfig::prod(
456    ///     SupportedChainId::Mainnet,
457    ///     Address::ZERO,
458    ///     TokenRegistry::new(empty),
459    ///     50,
460    ///     1800,
461    /// );
462    /// assert_eq!(config.effective_receiver(wallet), wallet);
463    ///
464    /// let override_addr = address!("0000000000000000000000000000000000000001");
465    /// let with_recv = config.with_receiver(override_addr);
466    /// assert_eq!(with_recv.effective_receiver(wallet), override_addr);
467    /// ```
468    #[must_use]
469    pub fn effective_receiver(&self, default: Address) -> Address {
470        self.receiver.map_or(default, |r| r)
471    }
472}
473
474impl fmt::Display for CowSwapConfig {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        write!(f, "config({}, {}, sell={:#x})", self.chain_id, self.env, self.sell_token)
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    // ── TokenRegistry ───────────────────────────────────────────────────
485
486    #[test]
487    fn token_registry_new_defaults_to_18_decimals() {
488        let reg = TokenRegistry::new([("WETH", Address::ZERO)]);
489        assert_eq!(reg.get_decimals("WETH"), Some(18));
490    }
491
492    #[test]
493    fn token_registry_new_with_decimals() {
494        let reg = TokenRegistry::new_with_decimals([("USDC", Address::ZERO, 6u8)]);
495        assert_eq!(reg.get_decimals("USDC"), Some(6));
496        assert_eq!(reg.get("USDC"), Some(Address::ZERO));
497    }
498
499    #[test]
500    fn token_registry_insert_and_contains() {
501        let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
502        assert!(reg.is_empty());
503        assert_eq!(reg.len(), 0);
504        assert!(!reg.contains("DAI"));
505
506        reg.insert("DAI", Address::ZERO);
507        assert!(reg.contains("DAI"));
508        assert_eq!(reg.len(), 1);
509        assert!(!reg.is_empty());
510    }
511
512    #[test]
513    fn token_registry_insert_with_decimals() {
514        let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
515        reg.insert_with_decimals("WBTC", Address::ZERO, 8);
516        assert_eq!(reg.get_decimals("WBTC"), Some(8));
517    }
518
519    #[test]
520    fn token_registry_get_entry() {
521        let reg = TokenRegistry::new_with_decimals([("USDC", Address::ZERO, 6u8)]);
522        assert_eq!(reg.get_entry("USDC"), Some((Address::ZERO, 6)));
523        assert_eq!(reg.get_entry("NONEXISTENT"), None);
524    }
525
526    #[test]
527    fn token_registry_remove() {
528        let mut reg = TokenRegistry::new([("WETH", Address::ZERO)]);
529        assert!(reg.contains("WETH"));
530        let removed = reg.remove("WETH");
531        assert!(removed.is_some());
532        assert!(!reg.contains("WETH"));
533        assert!(reg.is_empty());
534    }
535
536    #[test]
537    fn token_registry_remove_nonexistent() {
538        let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
539        assert!(reg.remove("WETH").is_none());
540    }
541
542    #[test]
543    fn token_registry_get_missing_returns_none() {
544        let reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
545        assert_eq!(reg.get("WETH"), None);
546        assert_eq!(reg.get_decimals("WETH"), None);
547    }
548
549    #[test]
550    fn token_registry_display() {
551        let reg = TokenRegistry::new([("A", Address::ZERO), ("B", Address::ZERO)]);
552        let s = format!("{reg}");
553        assert!(s.contains("2 tokens"));
554    }
555
556    // ── CowSwapConfig ───────────────────────────────────────────────────
557
558    fn empty_registry() -> TokenRegistry {
559        TokenRegistry::new(std::iter::empty::<(&str, Address)>())
560    }
561
562    #[test]
563    fn config_prod_defaults() {
564        let cfg = CowSwapConfig::prod(
565            SupportedChainId::Mainnet,
566            Address::ZERO,
567            empty_registry(),
568            50,
569            1800,
570        );
571        assert!(cfg.env.is_prod());
572        assert_eq!(cfg.slippage_bps, 50);
573        assert_eq!(cfg.order_valid_secs, 1800);
574        assert_eq!(cfg.sell_token_decimals, 18);
575        assert!(!cfg.has_custom_receiver());
576    }
577
578    #[test]
579    fn config_staging_defaults() {
580        let cfg = CowSwapConfig::staging(
581            SupportedChainId::Sepolia,
582            Address::ZERO,
583            empty_registry(),
584            100,
585            900,
586        );
587        assert!(cfg.env.is_staging());
588        assert_eq!(cfg.slippage_bps, 100);
589    }
590
591    #[test]
592    fn config_builder_methods() {
593        let cfg = CowSwapConfig::prod(
594            SupportedChainId::Mainnet,
595            Address::ZERO,
596            empty_registry(),
597            50,
598            1800,
599        )
600        .with_slippage_bps(100)
601        .with_order_valid_secs(600)
602        .with_sell_token_decimals(6)
603        .with_chain_id(SupportedChainId::Sepolia)
604        .with_env(Env::Staging);
605
606        assert_eq!(cfg.slippage_bps, 100);
607        assert_eq!(cfg.order_valid_secs, 600);
608        assert_eq!(cfg.sell_token_decimals, 6);
609        assert_eq!(cfg.chain_id, SupportedChainId::Sepolia);
610        assert!(cfg.env.is_staging());
611    }
612
613    #[test]
614    fn config_with_receiver() {
615        let recv = Address::new([0x01; 20]);
616        let wallet = Address::new([0x02; 20]);
617        let cfg = CowSwapConfig::prod(
618            SupportedChainId::Mainnet,
619            Address::ZERO,
620            empty_registry(),
621            50,
622            1800,
623        )
624        .with_receiver(recv);
625        assert!(cfg.has_custom_receiver());
626        assert_eq!(cfg.effective_receiver(wallet), recv);
627    }
628
629    #[test]
630    fn config_effective_receiver_defaults_to_wallet() {
631        let wallet = Address::new([0x02; 20]);
632        let cfg = CowSwapConfig::prod(
633            SupportedChainId::Mainnet,
634            Address::ZERO,
635            empty_registry(),
636            50,
637            1800,
638        );
639        assert_eq!(cfg.effective_receiver(wallet), wallet);
640    }
641
642    #[test]
643    fn config_with_sell_token() {
644        let token = Address::new([0xaa; 20]);
645        let cfg = CowSwapConfig::prod(
646            SupportedChainId::Mainnet,
647            Address::ZERO,
648            empty_registry(),
649            50,
650            1800,
651        )
652        .with_sell_token(token);
653        assert_eq!(cfg.sell_token, token);
654    }
655
656    #[test]
657    fn config_display() {
658        let cfg = CowSwapConfig::prod(
659            SupportedChainId::Mainnet,
660            Address::ZERO,
661            empty_registry(),
662            50,
663            1800,
664        );
665        let s = format!("{cfg}");
666        assert!(s.contains("config("));
667    }
668}