Skip to main content

cow_rs/bridging/
types.rs

1//! Cross-chain bridging types.
2
3use foldhash::HashMap;
4
5use alloy_primitives::{Address, U256};
6use serde::{Deserialize, Serialize};
7
8use crate::app_data::CowHook;
9
10// ── Provider type ─────────────────────────────────────────────────────────────
11
12/// Type of bridge provider — either hook-based or receiver-account-based.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum BridgeProviderType {
15    /// Provider relies on a post-hook to initiate the bridge.
16    HookBridgeProvider,
17    /// Provider sends tokens to a specific deposit account.
18    ReceiverAccountBridgeProvider,
19}
20
21impl BridgeProviderType {
22    /// Returns `true` if this is a [`HookBridgeProvider`](Self::HookBridgeProvider).
23    ///
24    /// Equivalent to the `TypeScript` `isHookBridgeProvider` type guard.
25    #[must_use]
26    pub const fn is_hook_bridge_provider(self) -> bool {
27        matches!(self, Self::HookBridgeProvider)
28    }
29
30    /// Returns `true` if this is a
31    /// [`ReceiverAccountBridgeProvider`](Self::ReceiverAccountBridgeProvider).
32    ///
33    /// Equivalent to the `TypeScript` `isReceiverAccountBridgeProvider` type guard.
34    #[must_use]
35    pub const fn is_receiver_account_bridge_provider(self) -> bool {
36        matches!(self, Self::ReceiverAccountBridgeProvider)
37    }
38}
39
40/// Metadata about a bridge provider.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BridgeProviderInfo {
43    /// Provider display name.
44    pub name: String,
45    /// URL to the provider's logo.
46    pub logo_url: String,
47    /// Unique dApp identifier (e.g. `"cow-sdk://bridging/providers/across"`).
48    pub dapp_id: String,
49    /// Provider website URL.
50    pub website: String,
51    /// Type of bridge provider.
52    pub provider_type: BridgeProviderType,
53}
54
55impl BridgeProviderInfo {
56    /// Returns `true` if this provider uses hooks to initiate the bridge.
57    ///
58    /// Delegates to [`BridgeProviderType::is_hook_bridge_provider`] on the
59    /// inner `provider_type` field.
60    ///
61    /// # Returns
62    ///
63    /// `true` when `provider_type` is [`BridgeProviderType::HookBridgeProvider`],
64    /// `false` otherwise.
65    #[must_use]
66    pub const fn is_hook_bridge_provider(&self) -> bool {
67        self.provider_type.is_hook_bridge_provider()
68    }
69
70    /// Returns `true` if this provider sends tokens to a deposit account.
71    ///
72    /// Delegates to [`BridgeProviderType::is_receiver_account_bridge_provider`]
73    /// on the inner `provider_type` field.
74    ///
75    /// # Returns
76    ///
77    /// `true` when `provider_type` is
78    /// [`BridgeProviderType::ReceiverAccountBridgeProvider`], `false` otherwise.
79    #[must_use]
80    pub const fn is_receiver_account_bridge_provider(&self) -> bool {
81        self.provider_type.is_receiver_account_bridge_provider()
82    }
83}
84
85// ── Bridge status ─────────────────────────────────────────────────────────────
86
87/// Status of a cross-chain bridge transaction.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub enum BridgeStatus {
90    /// The bridge transaction is still in progress.
91    InProgress,
92    /// The bridge transaction was successfully executed.
93    Executed,
94    /// The bridge transaction has expired.
95    Expired,
96    /// The bridge transaction was refunded.
97    Refund,
98    /// The bridge status is unknown.
99    Unknown,
100}
101
102/// Result of querying a bridge transaction's status.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct BridgeStatusResult {
105    /// Current status of the bridge.
106    pub status: BridgeStatus,
107    /// Time in seconds for the fill to complete, if available.
108    pub fill_time_in_seconds: Option<u64>,
109    /// Transaction hash of the deposit on the origin chain.
110    pub deposit_tx_hash: Option<String>,
111    /// Transaction hash of the fill on the destination chain.
112    pub fill_tx_hash: Option<String>,
113}
114
115impl BridgeStatusResult {
116    /// Create a status result with only a status.
117    ///
118    /// All optional fields (`fill_time_in_seconds`, `deposit_tx_hash`,
119    /// `fill_tx_hash`) are set to `None`.
120    ///
121    /// # Arguments
122    ///
123    /// * `status` - The current [`BridgeStatus`] of the bridge transaction.
124    ///
125    /// # Returns
126    ///
127    /// A new [`BridgeStatusResult`] with only the status populated.
128    #[must_use]
129    pub const fn new(status: BridgeStatus) -> Self {
130        Self { status, fill_time_in_seconds: None, deposit_tx_hash: None, fill_tx_hash: None }
131    }
132}
133
134// ── Quote request / response ──────────────────────────────────────────────────
135
136/// Request for a cross-chain bridge quote.
137#[derive(Debug, Clone)]
138pub struct QuoteBridgeRequest {
139    /// Chain ID of the source chain.
140    pub sell_chain_id: u64,
141    /// Chain ID of the destination chain.
142    pub buy_chain_id: u64,
143    /// Token address on the source chain.
144    pub sell_token: Address,
145    /// Token decimals on the source chain.
146    pub sell_token_decimals: u8,
147    /// Token address on the destination chain.
148    pub buy_token: Address,
149    /// Token decimals on the destination chain.
150    pub buy_token_decimals: u8,
151    /// Amount of `sell_token` to bridge (in atoms).
152    pub sell_amount: U256,
153    /// Address of the user initiating the bridge.
154    pub account: Address,
155    /// Optional owner address.
156    pub owner: Option<Address>,
157    /// Optional receiver address on the destination chain.
158    pub receiver: Option<String>,
159    /// Optional bridge recipient (may be non-EVM, e.g. Solana/BTC).
160    pub bridge_recipient: Option<String>,
161    /// Slippage tolerance in basis points for the swap leg.
162    pub slippage_bps: u32,
163    /// Optional bridge-specific slippage tolerance in basis points.
164    pub bridge_slippage_bps: Option<u32>,
165    /// Whether this is a sell or buy order.
166    pub kind: crate::OrderKind,
167}
168
169/// Amounts (sell and buy) at various stages of a bridge quote.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct BridgeAmounts {
172    /// Amount being sold (source chain atoms).
173    pub sell_amount: U256,
174    /// Amount being received (destination chain atoms).
175    pub buy_amount: U256,
176}
177
178/// Costs associated with bridging.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct BridgeCosts {
181    /// Bridging fee information.
182    pub bridging_fee: BridgingFee,
183}
184
185/// Fee breakdown for a bridge transaction.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct BridgingFee {
188    /// Fee in basis points.
189    pub fee_bps: u32,
190    /// Fee amount denominated in the sell token.
191    pub amount_in_sell_currency: U256,
192    /// Fee amount denominated in the buy token.
193    pub amount_in_buy_currency: U256,
194}
195
196/// Full amounts-and-costs breakdown for a bridge quote.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct BridgeQuoteAmountsAndCosts {
199    /// Costs of the bridging.
200    pub costs: BridgeCosts,
201    /// Amounts before fees.
202    pub before_fee: BridgeAmounts,
203    /// Amounts after fees.
204    pub after_fee: BridgeAmounts,
205    /// Amounts after slippage tolerance (minimum the user will receive).
206    pub after_slippage: BridgeAmounts,
207    /// Slippage tolerance in basis points.
208    pub slippage_bps: u32,
209}
210
211/// Fee limits for a bridge deposit.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct BridgeLimits {
214    /// Minimum deposit amount in token atoms.
215    pub min_deposit: U256,
216    /// Maximum deposit amount in token atoms.
217    pub max_deposit: U256,
218}
219
220/// Fee amounts charged by the bridge.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BridgeFees {
223    /// Fee to cover relayer capital costs (in token atoms).
224    pub bridge_fee: U256,
225    /// Fee to cover destination chain gas costs (in token atoms).
226    pub destination_gas_fee: U256,
227}
228
229/// A bridge quote from a single provider.
230#[derive(Debug, Clone)]
231pub struct QuoteBridgeResponse {
232    /// Bridge provider identifier (e.g. `"bungee"`).
233    pub provider: String,
234    /// Input amount on the source chain.
235    pub sell_amount: U256,
236    /// Minimum output amount on the destination chain.
237    pub buy_amount: U256,
238    /// Fee charged by the bridge (in `buy_token` atoms).
239    pub fee_amount: U256,
240    /// Estimated seconds for the bridge to complete.
241    pub estimated_secs: u64,
242    /// Optional pre-interaction hook that triggers the bridge.
243    pub bridge_hook: Option<CowHook>,
244}
245
246impl QuoteBridgeResponse {
247    /// Returns `true` if a bridge hook is attached.
248    ///
249    /// Hook-based bridge providers attach a [`CowHook`] that is executed as a
250    /// post-interaction to initiate the bridge transfer.
251    ///
252    /// # Returns
253    ///
254    /// `true` when `bridge_hook` is `Some`, `false` otherwise.
255    #[must_use]
256    pub const fn has_bridge_hook(&self) -> bool {
257        self.bridge_hook.is_some()
258    }
259
260    /// Return a reference to the provider name.
261    ///
262    /// # Returns
263    ///
264    /// A string slice of the provider identifier (e.g. `"across"`, `"bungee"`).
265    #[must_use]
266    pub fn provider_ref(&self) -> &str {
267        &self.provider
268    }
269
270    /// Net buy amount after subtracting the fee.
271    ///
272    /// Uses saturating subtraction: returns zero if `fee_amount > buy_amount`.
273    #[must_use]
274    pub const fn net_buy_amount(&self) -> U256 {
275        self.buy_amount.saturating_sub(self.fee_amount)
276    }
277}
278
279/// Result of a bridge quote with full cost details.
280#[derive(Debug, Clone)]
281pub struct BridgeQuoteResult {
282    /// Unique ID of the quote.
283    pub id: Option<String>,
284    /// Provider quote signature (for `ReceiverAccountBridgeProvider`).
285    pub signature: Option<String>,
286    /// Attestation signature from the bridge provider.
287    pub attestation_signature: Option<String>,
288    /// Stringified JSON of the provider-specific quote body.
289    pub quote_body: Option<String>,
290    /// Whether this is a sell order.
291    pub is_sell: bool,
292    /// Full amounts and costs breakdown.
293    pub amounts_and_costs: BridgeQuoteAmountsAndCosts,
294    /// Estimated fill time in seconds.
295    pub expected_fill_time_seconds: Option<u64>,
296    /// Quote creation timestamp (UNIX seconds).
297    pub quote_timestamp: u64,
298    /// Bridge fees.
299    pub fees: BridgeFees,
300    /// Deposit limits.
301    pub limits: BridgeLimits,
302}
303
304/// Extended bridge quote results with provider and trade context.
305#[derive(Debug, Clone)]
306pub struct BridgeQuoteResults {
307    /// Bridge provider info.
308    pub provider_info: BridgeProviderInfo,
309    /// The bridge quote result.
310    pub quote: BridgeQuoteResult,
311    /// Bridge call details (for hook-based providers).
312    pub bridge_call_details: Option<BridgeCallDetails>,
313    /// Override receiver address (for receiver-account providers).
314    pub bridge_receiver_override: Option<String>,
315}
316
317/// Details about a bridge hook call.
318#[derive(Debug, Clone)]
319pub struct BridgeCallDetails {
320    /// Unsigned call to initiate the bridge.
321    pub unsigned_bridge_call: crate::config::EvmCall,
322    /// Pre-authorized bridging hook.
323    pub pre_authorized_bridging_hook: BridgeHook,
324}
325
326/// A signed bridge hook ready for inclusion in a `CoW` Protocol order.
327#[derive(Debug, Clone)]
328pub struct BridgeHook {
329    /// The post-hook to include in the order's app data.
330    pub post_hook: CowHook,
331    /// The recipient address for the bridged funds.
332    pub recipient: String,
333}
334
335/// Parameters extracted from a bridging deposit event.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct BridgingDepositParams {
338    /// Input token address.
339    pub input_token_address: Address,
340    /// Output token address.
341    pub output_token_address: Address,
342    /// Amount of input tokens deposited.
343    pub input_amount: U256,
344    /// Expected output amount (may be `None` if unknown).
345    pub output_amount: Option<U256>,
346    /// Address of the depositor.
347    pub owner: Address,
348    /// Quote timestamp used for fee computation.
349    pub quote_timestamp: Option<u64>,
350    /// Fill deadline as a UNIX timestamp.
351    pub fill_deadline: Option<u64>,
352    /// Recipient of bridged funds on the destination chain.
353    pub recipient: Address,
354    /// Source chain ID.
355    pub source_chain_id: u64,
356    /// Destination chain ID.
357    pub destination_chain_id: u64,
358    /// Provider-specific bridging identifier.
359    pub bridging_id: String,
360}
361
362/// A resolved cross-chain order with bridging details.
363#[derive(Debug, Clone)]
364pub struct CrossChainOrder {
365    /// Chain ID where the order was settled.
366    pub chain_id: u64,
367    /// Bridging status result.
368    pub status_result: BridgeStatusResult,
369    /// Bridging deposit parameters.
370    pub bridging_params: BridgingDepositParams,
371    /// Settlement transaction hash.
372    pub trade_tx_hash: String,
373    /// Bridge explorer URL for tracking.
374    pub explorer_url: Option<String>,
375}
376
377/// Result from a single provider in a multi-quote request.
378#[derive(Debug, Clone)]
379pub struct MultiQuoteResult {
380    /// The provider's dApp ID.
381    pub provider_dapp_id: String,
382    /// The bridge quote, if successful.
383    pub quote: Option<BridgeQuoteAmountsAndCosts>,
384    /// Error message, if the provider failed.
385    pub error: Option<String>,
386}
387
388// ── Errors ────────────────────────────────────────────────────────────────────
389
390/// Errors specific to bridging operations.
391#[derive(Debug, thiserror::Error)]
392pub enum BridgeError {
393    /// No bridge providers are registered.
394    #[error("no providers available")]
395    NoProviders,
396    /// None of the registered providers returned a quote for this route.
397    #[error("no quote available for this route")]
398    NoQuote,
399    /// Attempted a cross-chain operation on same-chain tokens.
400    #[error("sell and buy chains must be different for cross-chain bridging")]
401    SameChain,
402    /// Only sell orders are supported for bridging.
403    #[error("bridging only supports SELL orders")]
404    OnlySellOrderSupported,
405    /// No intermediate tokens available for the requested route.
406    #[error("no intermediate tokens available")]
407    NoIntermediateTokens,
408    /// The bridge API returned an error.
409    #[error("bridge API error: {0}")]
410    ApiError(String),
411    /// Invalid API response format.
412    #[error("invalid API JSON response: {0}")]
413    InvalidApiResponse(String),
414    /// Error building the bridge transaction.
415    #[error("transaction build error: {0}")]
416    TxBuildError(String),
417    /// General quote error.
418    #[error("quote error: {0}")]
419    QuoteError(String),
420    /// No routes found.
421    #[error("no routes available")]
422    NoRoutes,
423    /// Invalid bridge configuration.
424    #[error("invalid bridge: {0}")]
425    InvalidBridge(String),
426    /// Quote does not match expected deposit address.
427    #[error("quote does not match deposit address")]
428    QuoteDoesNotMatchDepositAddress,
429    /// Sell amount is below the minimum threshold.
430    #[error("sell amount too small")]
431    SellAmountTooSmall,
432    /// Provider with the given dApp ID was not found.
433    #[error("provider not found: {dapp_id}")]
434    ProviderNotFound {
435        /// The requested dApp ID.
436        dapp_id: String,
437    },
438    /// Provider request timed out.
439    #[error("provider request timed out")]
440    Timeout,
441    /// A `CoW` Protocol API error.
442    #[error(transparent)]
443    Cow(#[from] crate::CowError),
444}
445
446/// Priority of bridge quote errors for selecting the best error to surface.
447///
448/// Higher values indicate errors that are more relevant to the user.
449/// When multiple providers fail, the error with the highest priority is
450/// shown so that the most actionable message reaches the caller.
451///
452/// # Arguments
453///
454/// * `error` - The [`BridgeError`] to evaluate.
455///
456/// # Returns
457///
458/// A numeric priority (`u32`). Currently `10` for
459/// [`BridgeError::SellAmountTooSmall`], `9` for
460/// [`BridgeError::OnlySellOrderSupported`], and `1` for all other variants.
461#[must_use]
462pub const fn bridge_error_priority(error: &BridgeError) -> u32 {
463    match error {
464        BridgeError::SellAmountTooSmall => 10,
465        BridgeError::OnlySellOrderSupported => 9,
466        BridgeError::NoProviders |
467        BridgeError::NoQuote |
468        BridgeError::SameChain |
469        BridgeError::NoIntermediateTokens |
470        BridgeError::ApiError(_) |
471        BridgeError::InvalidApiResponse(_) |
472        BridgeError::TxBuildError(_) |
473        BridgeError::QuoteError(_) |
474        BridgeError::NoRoutes |
475        BridgeError::InvalidBridge(_) |
476        BridgeError::QuoteDoesNotMatchDepositAddress |
477        BridgeError::ProviderNotFound { .. } |
478        BridgeError::Timeout |
479        BridgeError::Cow(_) => 1,
480    }
481}
482
483// ── Across-specific types ─────────────────────────────────────────────────────
484
485/// A percentage fee as returned by the Across API.
486///
487/// `pct` is expressed in Across format: 1% = 1e16, 100% = 1e18.
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct AcrossPctFee {
490    /// Percentage as a string in contract format (1e18 = 100%).
491    pub pct: String,
492    /// Total fee amount as a string.
493    pub total: String,
494}
495
496/// Deposit size limits from the Across API.
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct AcrossSuggestedFeesLimits {
499    /// Minimum deposit size in token units.
500    pub min_deposit: String,
501    /// Maximum deposit size in token units.
502    pub max_deposit: String,
503    /// Maximum instant-fill deposit size.
504    pub max_deposit_instant: String,
505    /// Maximum short-delay deposit size.
506    pub max_deposit_short_delay: String,
507    /// Recommended instant deposit size.
508    pub recommended_deposit_instant: String,
509}
510
511/// Full response from the Across suggested-fees endpoint.
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct AcrossSuggestedFeesResponse {
514    /// Total relay fee (inclusive of LP fee).
515    pub total_relay_fee: AcrossPctFee,
516    /// Relayer capital fee component.
517    pub relayer_capital_fee: AcrossPctFee,
518    /// Relayer gas fee component.
519    pub relayer_gas_fee: AcrossPctFee,
520    /// LP fee component.
521    pub lp_fee: AcrossPctFee,
522    /// Quote timestamp for LP fee computation.
523    pub timestamp: String,
524    /// Whether the amount is below the minimum.
525    pub is_amount_too_low: bool,
526    /// Block number associated with the quote.
527    pub quote_block: String,
528    /// Spoke pool contract address.
529    pub spoke_pool_address: String,
530    /// Suggested exclusive relayer address.
531    pub exclusive_relayer: String,
532    /// Exclusivity deadline in seconds.
533    pub exclusivity_deadline: String,
534    /// Estimated fill time in seconds.
535    pub estimated_fill_time_sec: String,
536    /// Recommended fill deadline as a UNIX timestamp.
537    pub fill_deadline: String,
538    /// Deposit size limits.
539    pub limits: AcrossSuggestedFeesLimits,
540}
541
542/// Status of an Across deposit.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub enum AcrossDepositStatus {
546    /// Deposit has been filled.
547    Filled,
548    /// Slow fill was requested.
549    SlowFillRequested,
550    /// Deposit is still pending.
551    Pending,
552    /// Deposit has expired.
553    Expired,
554    /// Deposit was refunded.
555    Refunded,
556}
557
558/// Response from the Across deposit status endpoint.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct AcrossDepositStatusResponse {
561    /// Current deposit status.
562    pub status: AcrossDepositStatus,
563    /// Origin chain ID.
564    pub origin_chain_id: String,
565    /// Unique deposit identifier.
566    pub deposit_id: String,
567    /// Deposit transaction hash on the origin chain.
568    pub deposit_tx_hash: Option<String>,
569    /// Fill transaction hash on the destination chain.
570    pub fill_tx: Option<String>,
571    /// Destination chain ID.
572    pub destination_chain_id: Option<String>,
573    /// Refund transaction hash.
574    pub deposit_refund_tx_hash: Option<String>,
575}
576
577/// An Across deposit event parsed from transaction logs.
578#[derive(Debug, Clone)]
579pub struct AcrossDepositEvent {
580    /// Input token address.
581    pub input_token: Address,
582    /// Output token address.
583    pub output_token: Address,
584    /// Amount of input tokens.
585    pub input_amount: U256,
586    /// Expected output amount.
587    pub output_amount: U256,
588    /// Destination chain ID.
589    pub destination_chain_id: u64,
590    /// Unique deposit identifier.
591    pub deposit_id: U256,
592    /// Quote timestamp for fee computation.
593    pub quote_timestamp: u32,
594    /// Fill deadline as a UNIX timestamp.
595    pub fill_deadline: u32,
596    /// Exclusivity deadline.
597    pub exclusivity_deadline: u32,
598    /// Depositor address.
599    pub depositor: Address,
600    /// Recipient address.
601    pub recipient: Address,
602    /// Exclusive relayer address.
603    pub exclusive_relayer: Address,
604}
605
606/// Chain-specific token configuration for Across.
607#[derive(Debug, Clone)]
608pub struct AcrossChainConfig {
609    /// Chain ID.
610    pub chain_id: u64,
611    /// Token symbol to address mapping.
612    pub tokens: HashMap<String, Address>,
613}
614
615// ── Bungee-specific types ─────────────────────────────────────────────────────
616
617/// Supported Bungee bridge variants.
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
619pub enum BungeeBridge {
620    /// Across bridge via Bungee.
621    Across,
622    /// Circle CCTP bridge via Bungee.
623    CircleCctp,
624    /// Gnosis native bridge.
625    GnosisNative,
626}
627
628impl BungeeBridge {
629    /// Return the API string identifier for this bridge.
630    ///
631    /// # Returns
632    ///
633    /// A static string used in Bungee API calls:
634    /// - [`Across`](Self::Across) -> `"across"`
635    /// - [`CircleCctp`](Self::CircleCctp) -> `"cctp"`
636    /// - [`GnosisNative`](Self::GnosisNative) -> `"gnosis-native-bridge"`
637    ///
638    /// # Examples
639    ///
640    /// ```
641    /// use cow_rs::bridging::types::BungeeBridge;
642    ///
643    /// assert_eq!(BungeeBridge::Across.as_str(), "across");
644    /// assert_eq!(BungeeBridge::CircleCctp.as_str(), "cctp");
645    /// ```
646    #[must_use]
647    pub const fn as_str(&self) -> &'static str {
648        match self {
649            Self::Across => "across",
650            Self::CircleCctp => "cctp",
651            Self::GnosisNative => "gnosis-native-bridge",
652        }
653    }
654
655    /// Try to parse a bridge from its display name.
656    ///
657    /// This is the inverse of [`display_name`](Self::display_name).
658    ///
659    /// # Arguments
660    ///
661    /// * `name` - A human-readable bridge name (e.g. `"Across"`, `"Circle CCTP"`, `"Gnosis
662    ///   Native"`).
663    ///
664    /// # Returns
665    ///
666    /// `Some(BungeeBridge)` if the name matches a known variant, `None`
667    /// otherwise.
668    #[must_use]
669    pub fn from_display_name(name: &str) -> Option<Self> {
670        match name {
671            "Across" => Some(Self::Across),
672            "Circle CCTP" => Some(Self::CircleCctp),
673            "Gnosis Native" => Some(Self::GnosisNative),
674            _ => None,
675        }
676    }
677
678    /// Return the human-readable display name.
679    ///
680    /// This is the inverse of [`from_display_name`](Self::from_display_name).
681    ///
682    /// # Returns
683    ///
684    /// A static, human-readable label for this bridge variant (e.g.
685    /// `"Across"`, `"Circle CCTP"`, `"Gnosis Native"`).
686    #[must_use]
687    pub const fn display_name(&self) -> &'static str {
688        match self {
689            Self::Across => "Across",
690            Self::CircleCctp => "Circle CCTP",
691            Self::GnosisNative => "Gnosis Native",
692        }
693    }
694}
695
696/// Bungee event status.
697#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
699pub enum BungeeEventStatus {
700    /// Event is complete.
701    Completed,
702    /// Event is still pending.
703    Pending,
704}
705
706/// Bridge name as used in Bungee events.
707#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(rename_all = "lowercase")]
709pub enum BungeeBridgeName {
710    /// Across bridge.
711    Across,
712    /// Circle CCTP bridge.
713    Cctp,
714}
715
716/// A Bungee bridge event from the events API.
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct BungeeEvent {
719    /// Event identifier.
720    pub identifier: String,
721    /// Source transaction hash (None when pending).
722    pub src_transaction_hash: Option<String>,
723    /// Bridge name used.
724    pub bridge_name: BungeeBridgeName,
725    /// Origin chain ID.
726    pub from_chain_id: u64,
727    /// Whether this is a `CoW` Swap trade.
728    pub is_cowswap_trade: bool,
729    /// `CoW` Protocol order ID.
730    pub order_id: String,
731    /// Source transaction status.
732    pub src_tx_status: BungeeEventStatus,
733    /// Destination transaction status.
734    pub dest_tx_status: BungeeEventStatus,
735    /// Destination transaction hash (None when pending).
736    pub dest_transaction_hash: Option<String>,
737}
738
739/// Byte offset indices for decoding Bungee transaction data.
740#[derive(Debug, Clone, Copy)]
741pub struct BungeeTxDataBytesIndex {
742    /// Byte start offset in the raw calldata.
743    pub bytes_start_index: usize,
744    /// Byte length.
745    pub bytes_length: usize,
746    /// Character start offset in the hex string (including `0x` prefix).
747    pub bytes_string_start_index: usize,
748    /// Character length in the hex string.
749    pub bytes_string_length: usize,
750}
751
752/// Decoded result from Bungee transaction data.
753#[derive(Debug, Clone)]
754pub struct DecodedBungeeTxData {
755    /// Route ID (first 4 bytes).
756    pub route_id: String,
757    /// Encoded function data (after route ID).
758    pub encoded_function_data: String,
759    /// Function selector (first 4 bytes of function data).
760    pub function_selector: String,
761}
762
763/// Decoded amounts from Bungee transaction data.
764#[derive(Debug, Clone)]
765pub struct DecodedBungeeAmounts {
766    /// Raw input amount bytes as hex string.
767    pub input_amount_bytes: String,
768    /// Parsed input amount as U256.
769    pub input_amount: U256,
770}
771
772#[cfg(test)]
773mod tests {
774    use super::*;
775
776    // ── BridgeProviderType ──────────────────────────────────────────────
777
778    #[test]
779    fn hook_bridge_provider_is_hook() {
780        assert!(BridgeProviderType::HookBridgeProvider.is_hook_bridge_provider());
781        assert!(!BridgeProviderType::HookBridgeProvider.is_receiver_account_bridge_provider());
782    }
783
784    #[test]
785    fn receiver_account_bridge_provider_is_receiver() {
786        assert!(
787            BridgeProviderType::ReceiverAccountBridgeProvider.is_receiver_account_bridge_provider()
788        );
789        assert!(!BridgeProviderType::ReceiverAccountBridgeProvider.is_hook_bridge_provider());
790    }
791
792    // ── BridgeProviderInfo delegation ───────────────────────────────────
793
794    #[test]
795    fn bridge_provider_info_delegates_hook() {
796        let info = BridgeProviderInfo {
797            name: "test".into(),
798            logo_url: String::new(),
799            dapp_id: String::new(),
800            website: String::new(),
801            provider_type: BridgeProviderType::HookBridgeProvider,
802        };
803        assert!(info.is_hook_bridge_provider());
804        assert!(!info.is_receiver_account_bridge_provider());
805    }
806
807    #[test]
808    fn bridge_provider_info_delegates_receiver() {
809        let info = BridgeProviderInfo {
810            name: "test".into(),
811            logo_url: String::new(),
812            dapp_id: String::new(),
813            website: String::new(),
814            provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
815        };
816        assert!(info.is_receiver_account_bridge_provider());
817        assert!(!info.is_hook_bridge_provider());
818    }
819
820    // ── BridgeStatus ────────────────────────────────────────────────────
821
822    #[test]
823    fn bridge_status_variants_are_distinct() {
824        let statuses = [
825            BridgeStatus::InProgress,
826            BridgeStatus::Executed,
827            BridgeStatus::Expired,
828            BridgeStatus::Refund,
829            BridgeStatus::Unknown,
830        ];
831        for (i, a) in statuses.iter().enumerate() {
832            for (j, b) in statuses.iter().enumerate() {
833                if i == j {
834                    assert_eq!(a, b);
835                } else {
836                    assert_ne!(a, b);
837                }
838            }
839        }
840    }
841
842    // ── BridgeStatusResult ──────────────────────────────────────────────
843
844    #[test]
845    fn bridge_status_result_new_sets_status_only() {
846        let r = BridgeStatusResult::new(BridgeStatus::Executed);
847        assert_eq!(r.status, BridgeStatus::Executed);
848        assert!(r.fill_time_in_seconds.is_none());
849        assert!(r.deposit_tx_hash.is_none());
850        assert!(r.fill_tx_hash.is_none());
851    }
852
853    #[test]
854    fn bridge_status_result_new_all_statuses() {
855        for status in [
856            BridgeStatus::InProgress,
857            BridgeStatus::Executed,
858            BridgeStatus::Expired,
859            BridgeStatus::Refund,
860            BridgeStatus::Unknown,
861        ] {
862            let r = BridgeStatusResult::new(status);
863            assert_eq!(r.status, status);
864        }
865    }
866
867    // ── QuoteBridgeResponse ─────────────────────────────────────────────
868
869    fn make_quote(hook: Option<CowHook>, fee: U256) -> QuoteBridgeResponse {
870        QuoteBridgeResponse {
871            provider: "across".into(),
872            sell_amount: U256::from(1000u64),
873            buy_amount: U256::from(950u64),
874            fee_amount: fee,
875            estimated_secs: 60,
876            bridge_hook: hook,
877        }
878    }
879
880    #[test]
881    fn has_bridge_hook_true_when_some() {
882        let hook = CowHook {
883            target: "0xdead".into(),
884            call_data: "0x".into(),
885            gas_limit: "100000".into(),
886            dapp_id: None,
887        };
888        let q = make_quote(Some(hook), U256::ZERO);
889        assert!(q.has_bridge_hook());
890    }
891
892    #[test]
893    fn has_bridge_hook_false_when_none() {
894        let q = make_quote(None, U256::ZERO);
895        assert!(!q.has_bridge_hook());
896    }
897
898    #[test]
899    fn provider_ref_returns_provider_name() {
900        let q = make_quote(None, U256::ZERO);
901        assert_eq!(q.provider_ref(), "across");
902    }
903
904    #[test]
905    fn net_buy_amount_subtracts_fee() {
906        let q = make_quote(None, U256::from(50u64));
907        assert_eq!(q.net_buy_amount(), U256::from(900u64));
908    }
909
910    #[test]
911    fn net_buy_amount_saturates_at_zero() {
912        let q = make_quote(None, U256::from(2000u64));
913        assert_eq!(q.net_buy_amount(), U256::ZERO);
914    }
915
916    #[test]
917    fn net_buy_amount_zero_fee() {
918        let q = make_quote(None, U256::ZERO);
919        assert_eq!(q.net_buy_amount(), U256::from(950u64));
920    }
921
922    // ── BungeeBridge ────────────────────────────────────────────────────
923
924    #[test]
925    fn bungee_bridge_as_str() {
926        assert_eq!(BungeeBridge::Across.as_str(), "across");
927        assert_eq!(BungeeBridge::CircleCctp.as_str(), "cctp");
928        assert_eq!(BungeeBridge::GnosisNative.as_str(), "gnosis-native-bridge");
929    }
930
931    #[test]
932    fn bungee_bridge_display_name() {
933        assert_eq!(BungeeBridge::Across.display_name(), "Across");
934        assert_eq!(BungeeBridge::CircleCctp.display_name(), "Circle CCTP");
935        assert_eq!(BungeeBridge::GnosisNative.display_name(), "Gnosis Native");
936    }
937
938    #[test]
939    fn bungee_bridge_from_display_name_valid() {
940        assert_eq!(BungeeBridge::from_display_name("Across"), Some(BungeeBridge::Across));
941        assert_eq!(BungeeBridge::from_display_name("Circle CCTP"), Some(BungeeBridge::CircleCctp));
942        assert_eq!(
943            BungeeBridge::from_display_name("Gnosis Native"),
944            Some(BungeeBridge::GnosisNative)
945        );
946    }
947
948    #[test]
949    fn bungee_bridge_from_display_name_invalid() {
950        assert_eq!(BungeeBridge::from_display_name("across"), None);
951        assert_eq!(BungeeBridge::from_display_name(""), None);
952        assert_eq!(BungeeBridge::from_display_name("Unknown"), None);
953    }
954
955    #[test]
956    fn bungee_bridge_roundtrip_display_name() {
957        for bridge in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
958            let name = bridge.display_name();
959            assert_eq!(BungeeBridge::from_display_name(name), Some(bridge));
960        }
961    }
962
963    // ── BridgeError priority ────────────────────────────────────────────
964
965    #[test]
966    fn sell_amount_too_small_has_highest_priority() {
967        assert_eq!(bridge_error_priority(&BridgeError::SellAmountTooSmall), 10);
968    }
969
970    #[test]
971    fn only_sell_order_supported_has_second_priority() {
972        assert_eq!(bridge_error_priority(&BridgeError::OnlySellOrderSupported), 9);
973    }
974
975    #[test]
976    fn other_errors_have_base_priority() {
977        let base_errors: Vec<BridgeError> = vec![
978            BridgeError::NoProviders,
979            BridgeError::NoQuote,
980            BridgeError::SameChain,
981            BridgeError::NoIntermediateTokens,
982            BridgeError::ApiError("test".into()),
983            BridgeError::InvalidApiResponse("test".into()),
984            BridgeError::TxBuildError("test".into()),
985            BridgeError::QuoteError("test".into()),
986            BridgeError::NoRoutes,
987            BridgeError::InvalidBridge("test".into()),
988            BridgeError::QuoteDoesNotMatchDepositAddress,
989            BridgeError::ProviderNotFound { dapp_id: "test".into() },
990            BridgeError::Timeout,
991        ];
992        for e in &base_errors {
993            assert_eq!(bridge_error_priority(e), 1, "expected priority 1 for {e}");
994        }
995    }
996
997    // ── BridgeError Display ─────────────────────────────────────────────
998
999    #[test]
1000    fn bridge_error_display_messages() {
1001        assert_eq!(BridgeError::NoProviders.to_string(), "no providers available");
1002        assert_eq!(BridgeError::NoQuote.to_string(), "no quote available for this route");
1003        assert_eq!(
1004            BridgeError::SameChain.to_string(),
1005            "sell and buy chains must be different for cross-chain bridging"
1006        );
1007        assert_eq!(
1008            BridgeError::OnlySellOrderSupported.to_string(),
1009            "bridging only supports SELL orders"
1010        );
1011        assert_eq!(
1012            BridgeError::NoIntermediateTokens.to_string(),
1013            "no intermediate tokens available"
1014        );
1015        assert_eq!(BridgeError::ApiError("oops".into()).to_string(), "bridge API error: oops");
1016        assert_eq!(
1017            BridgeError::InvalidApiResponse("bad".into()).to_string(),
1018            "invalid API JSON response: bad"
1019        );
1020        assert_eq!(
1021            BridgeError::TxBuildError("fail".into()).to_string(),
1022            "transaction build error: fail"
1023        );
1024        assert_eq!(BridgeError::QuoteError("nope".into()).to_string(), "quote error: nope");
1025        assert_eq!(BridgeError::NoRoutes.to_string(), "no routes available");
1026        assert_eq!(BridgeError::InvalidBridge("x".into()).to_string(), "invalid bridge: x");
1027        assert_eq!(
1028            BridgeError::QuoteDoesNotMatchDepositAddress.to_string(),
1029            "quote does not match deposit address"
1030        );
1031        assert_eq!(BridgeError::SellAmountTooSmall.to_string(), "sell amount too small");
1032        assert_eq!(
1033            BridgeError::ProviderNotFound { dapp_id: "foo".into() }.to_string(),
1034            "provider not found: foo"
1035        );
1036        assert_eq!(BridgeError::Timeout.to_string(), "provider request timed out");
1037    }
1038
1039    // ── Serde roundtrips ────────────────────────────────────────────────
1040
1041    #[test]
1042    fn bridge_provider_type_serde_roundtrip() {
1043        for v in [
1044            BridgeProviderType::HookBridgeProvider,
1045            BridgeProviderType::ReceiverAccountBridgeProvider,
1046        ] {
1047            let json = serde_json::to_string(&v).unwrap();
1048            let back: BridgeProviderType = serde_json::from_str(&json).unwrap();
1049            assert_eq!(v, back);
1050        }
1051    }
1052
1053    #[test]
1054    fn bridge_status_serde_roundtrip() {
1055        for v in [
1056            BridgeStatus::InProgress,
1057            BridgeStatus::Executed,
1058            BridgeStatus::Expired,
1059            BridgeStatus::Refund,
1060            BridgeStatus::Unknown,
1061        ] {
1062            let json = serde_json::to_string(&v).unwrap();
1063            let back: BridgeStatus = serde_json::from_str(&json).unwrap();
1064            assert_eq!(v, back);
1065        }
1066    }
1067
1068    #[test]
1069    fn bungee_bridge_serde_roundtrip() {
1070        for v in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
1071            let json = serde_json::to_string(&v).unwrap();
1072            let back: BungeeBridge = serde_json::from_str(&json).unwrap();
1073            assert_eq!(v, back);
1074        }
1075    }
1076
1077    #[test]
1078    fn across_deposit_status_serde_roundtrip() {
1079        for v in [
1080            AcrossDepositStatus::Filled,
1081            AcrossDepositStatus::SlowFillRequested,
1082            AcrossDepositStatus::Pending,
1083            AcrossDepositStatus::Expired,
1084            AcrossDepositStatus::Refunded,
1085        ] {
1086            let json = serde_json::to_string(&v).unwrap();
1087            let back: AcrossDepositStatus = serde_json::from_str(&json).unwrap();
1088            assert_eq!(v, back);
1089        }
1090    }
1091
1092    #[test]
1093    fn across_deposit_status_camel_case_serialization() {
1094        assert_eq!(serde_json::to_string(&AcrossDepositStatus::Filled).unwrap(), "\"filled\"");
1095        assert_eq!(
1096            serde_json::to_string(&AcrossDepositStatus::SlowFillRequested).unwrap(),
1097            "\"slowFillRequested\""
1098        );
1099        assert_eq!(serde_json::to_string(&AcrossDepositStatus::Pending).unwrap(), "\"pending\"");
1100    }
1101
1102    #[test]
1103    fn bungee_event_status_screaming_snake_case() {
1104        assert_eq!(serde_json::to_string(&BungeeEventStatus::Completed).unwrap(), "\"COMPLETED\"");
1105        assert_eq!(serde_json::to_string(&BungeeEventStatus::Pending).unwrap(), "\"PENDING\"");
1106    }
1107
1108    #[test]
1109    fn bungee_bridge_name_lowercase_serialization() {
1110        assert_eq!(serde_json::to_string(&BungeeBridgeName::Across).unwrap(), "\"across\"");
1111        assert_eq!(serde_json::to_string(&BungeeBridgeName::Cctp).unwrap(), "\"cctp\"");
1112    }
1113}