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}