Skip to main content

cow_bridging/
types.rs

1//! Cross-chain bridging types.
2
3use foldhash::HashMap;
4
5use alloy_primitives::{Address, U256};
6use serde::{Deserialize, Serialize};
7
8use cow_types::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// ── Token address ─────────────────────────────────────────────────────────────
135
136/// Address of a token, either on an EVM chain (20-byte Ethereum-style
137/// address) or on a non-EVM chain (raw string — e.g. a Solana mint,
138/// Bitcoin bech32 address, or the chain-specific native-coin sentinel
139/// used by the NEAR Intents API).
140///
141/// EVM callers can keep passing an `alloy_primitives::Address` thanks
142/// to the `From<Address> for TokenAddress` impl; non-EVM callers use
143/// `TokenAddress::Raw("...".into())` explicitly.
144///
145/// The serde encoding is externally-tagged (`{"kind": "Evm", "value":
146/// "0x..."}`) so the TS SDK can discriminate the two variants.
147#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[serde(tag = "kind", content = "value")]
149pub enum TokenAddress {
150    /// EVM address (20 bytes).
151    Evm(Address),
152    /// Raw, non-EVM identifier — a Solana mint, a Bitcoin address, or
153    /// any chain-specific string the bridge provider needs to round-
154    /// trip back to its API.
155    Raw(String),
156}
157
158impl TokenAddress {
159    /// Return the inner [`Address`] if this is an EVM variant.
160    #[must_use]
161    pub const fn as_evm(&self) -> Option<&Address> {
162        match self {
163            Self::Evm(a) => Some(a),
164            Self::Raw(_) => None,
165        }
166    }
167
168    /// Return the inner [`Address`] *by value* if this is an EVM
169    /// variant — convenience for callers that want to pass it into
170    /// EVM-only APIs without juggling references.
171    #[must_use]
172    pub const fn to_evm(&self) -> Option<Address> {
173        match self {
174            Self::Evm(a) => Some(*a),
175            Self::Raw(_) => None,
176        }
177    }
178
179    /// Return the inner raw string if this is a non-EVM variant.
180    #[must_use]
181    pub const fn as_raw(&self) -> Option<&str> {
182        match self {
183            Self::Evm(_) => None,
184            Self::Raw(s) => Some(s.as_str()),
185        }
186    }
187
188    /// Returns `true` if this is an [`Evm`](Self::Evm) variant.
189    #[must_use]
190    pub const fn is_evm(&self) -> bool {
191        matches!(self, Self::Evm(_))
192    }
193
194    /// Returns `true` if this is a [`Raw`](Self::Raw) variant.
195    #[must_use]
196    pub const fn is_raw(&self) -> bool {
197        matches!(self, Self::Raw(_))
198    }
199}
200
201impl From<Address> for TokenAddress {
202    fn from(value: Address) -> Self {
203        Self::Evm(value)
204    }
205}
206
207impl From<&Address> for TokenAddress {
208    fn from(value: &Address) -> Self {
209        Self::Evm(*value)
210    }
211}
212
213impl PartialEq<Address> for TokenAddress {
214    fn eq(&self, other: &Address) -> bool {
215        matches!(self, Self::Evm(a) if a == other)
216    }
217}
218
219impl PartialEq<TokenAddress> for Address {
220    fn eq(&self, other: &TokenAddress) -> bool {
221        other == self
222    }
223}
224
225// ── Quote request / response ──────────────────────────────────────────────────
226
227/// Request for a cross-chain bridge quote.
228#[derive(Debug, Clone)]
229pub struct QuoteBridgeRequest {
230    /// Chain ID of the source chain.
231    pub sell_chain_id: u64,
232    /// Chain ID of the destination chain.
233    pub buy_chain_id: u64,
234    /// Token address on the source chain.
235    pub sell_token: Address,
236    /// Token decimals on the source chain.
237    pub sell_token_decimals: u8,
238    /// Token address on the destination chain.
239    pub buy_token: TokenAddress,
240    /// Token decimals on the destination chain.
241    pub buy_token_decimals: u8,
242    /// Amount of `sell_token` to bridge (in atoms).
243    pub sell_amount: U256,
244    /// Address of the user initiating the bridge.
245    pub account: Address,
246    /// Optional owner address.
247    pub owner: Option<Address>,
248    /// Optional receiver address on the destination chain.
249    pub receiver: Option<String>,
250    /// Optional bridge recipient (may be non-EVM, e.g. Solana/BTC).
251    pub bridge_recipient: Option<String>,
252    /// Slippage tolerance in basis points for the swap leg.
253    pub slippage_bps: u32,
254    /// Optional bridge-specific slippage tolerance in basis points.
255    pub bridge_slippage_bps: Option<u32>,
256    /// Whether this is a sell or buy order.
257    pub kind: cow_types::OrderKind,
258}
259
260/// Amounts (sell and buy) at various stages of a bridge quote.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct BridgeAmounts {
263    /// Amount being sold (source chain atoms).
264    pub sell_amount: U256,
265    /// Amount being received (destination chain atoms).
266    pub buy_amount: U256,
267}
268
269/// Costs associated with bridging.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct BridgeCosts {
272    /// Bridging fee information.
273    pub bridging_fee: BridgingFee,
274}
275
276/// Fee breakdown for a bridge transaction.
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct BridgingFee {
279    /// Fee in basis points.
280    pub fee_bps: u32,
281    /// Fee amount denominated in the sell token.
282    pub amount_in_sell_currency: U256,
283    /// Fee amount denominated in the buy token.
284    pub amount_in_buy_currency: U256,
285}
286
287/// Full amounts-and-costs breakdown for a bridge quote.
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct BridgeQuoteAmountsAndCosts {
290    /// Costs of the bridging.
291    pub costs: BridgeCosts,
292    /// Amounts before fees.
293    pub before_fee: BridgeAmounts,
294    /// Amounts after fees.
295    pub after_fee: BridgeAmounts,
296    /// Amounts after slippage tolerance (minimum the user will receive).
297    pub after_slippage: BridgeAmounts,
298    /// Slippage tolerance in basis points.
299    pub slippage_bps: u32,
300}
301
302/// Fee limits for a bridge deposit.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct BridgeLimits {
305    /// Minimum deposit amount in token atoms.
306    pub min_deposit: U256,
307    /// Maximum deposit amount in token atoms.
308    pub max_deposit: U256,
309}
310
311/// Fee amounts charged by the bridge.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct BridgeFees {
314    /// Fee to cover relayer capital costs (in token atoms).
315    pub bridge_fee: U256,
316    /// Fee to cover destination chain gas costs (in token atoms).
317    pub destination_gas_fee: U256,
318}
319
320/// A bridge quote from a single provider.
321#[derive(Debug, Clone)]
322pub struct QuoteBridgeResponse {
323    /// Bridge provider identifier (e.g. `"bungee"`).
324    pub provider: String,
325    /// Input amount on the source chain.
326    pub sell_amount: U256,
327    /// Minimum output amount on the destination chain.
328    pub buy_amount: U256,
329    /// Fee charged by the bridge (in `buy_token` atoms).
330    pub fee_amount: U256,
331    /// Estimated seconds for the bridge to complete.
332    pub estimated_secs: u64,
333    /// Optional pre-interaction hook that triggers the bridge.
334    pub bridge_hook: Option<CowHook>,
335}
336
337impl QuoteBridgeResponse {
338    /// Returns `true` if a bridge hook is attached.
339    ///
340    /// Hook-based bridge providers attach a [`CowHook`] that is executed as a
341    /// post-interaction to initiate the bridge transfer.
342    ///
343    /// # Returns
344    ///
345    /// `true` when `bridge_hook` is `Some`, `false` otherwise.
346    #[must_use]
347    pub const fn has_bridge_hook(&self) -> bool {
348        self.bridge_hook.is_some()
349    }
350
351    /// Return a reference to the provider name.
352    ///
353    /// # Returns
354    ///
355    /// A string slice of the provider identifier (e.g. `"across"`, `"bungee"`).
356    #[must_use]
357    pub fn provider_ref(&self) -> &str {
358        &self.provider
359    }
360
361    /// Net buy amount after subtracting the fee.
362    ///
363    /// Uses saturating subtraction: returns zero if `fee_amount > buy_amount`.
364    #[must_use]
365    pub const fn net_buy_amount(&self) -> U256 {
366        self.buy_amount.saturating_sub(self.fee_amount)
367    }
368}
369
370/// Result of a bridge quote with full cost details.
371#[derive(Debug, Clone)]
372pub struct BridgeQuoteResult {
373    /// Unique ID of the quote.
374    pub id: Option<String>,
375    /// Provider quote signature (for `ReceiverAccountBridgeProvider`).
376    pub signature: Option<String>,
377    /// Attestation signature from the bridge provider.
378    pub attestation_signature: Option<String>,
379    /// Stringified JSON of the provider-specific quote body.
380    pub quote_body: Option<String>,
381    /// Whether this is a sell order.
382    pub is_sell: bool,
383    /// Full amounts and costs breakdown.
384    pub amounts_and_costs: BridgeQuoteAmountsAndCosts,
385    /// Estimated fill time in seconds.
386    pub expected_fill_time_seconds: Option<u64>,
387    /// Quote creation timestamp (UNIX seconds).
388    pub quote_timestamp: u64,
389    /// Bridge fees.
390    pub fees: BridgeFees,
391    /// Deposit limits.
392    pub limits: BridgeLimits,
393}
394
395/// Extended bridge quote results with provider and trade context.
396#[derive(Debug, Clone)]
397pub struct BridgeQuoteResults {
398    /// Bridge provider info.
399    pub provider_info: BridgeProviderInfo,
400    /// The bridge quote result.
401    pub quote: BridgeQuoteResult,
402    /// Bridge call details (for hook-based providers).
403    pub bridge_call_details: Option<BridgeCallDetails>,
404    /// Override receiver address (for receiver-account providers).
405    pub bridge_receiver_override: Option<String>,
406}
407
408/// Details about a bridge hook call.
409#[derive(Debug, Clone)]
410pub struct BridgeCallDetails {
411    /// Unsigned call to initiate the bridge.
412    pub unsigned_bridge_call: cow_chains::EvmCall,
413    /// Pre-authorized bridging hook.
414    pub pre_authorized_bridging_hook: BridgeHook,
415}
416
417/// A signed bridge hook ready for inclusion in a `CoW` Protocol order.
418#[derive(Debug, Clone)]
419pub struct BridgeHook {
420    /// The post-hook to include in the order's app data.
421    pub post_hook: CowHook,
422    /// The recipient address for the bridged funds.
423    pub recipient: String,
424}
425
426/// Parameters extracted from a bridging deposit event.
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct BridgingDepositParams {
429    /// Input token address.
430    pub input_token_address: Address,
431    /// Output token address.
432    pub output_token_address: Address,
433    /// Amount of input tokens deposited.
434    pub input_amount: U256,
435    /// Expected output amount (may be `None` if unknown).
436    pub output_amount: Option<U256>,
437    /// Address of the depositor.
438    pub owner: Address,
439    /// Quote timestamp used for fee computation.
440    pub quote_timestamp: Option<u64>,
441    /// Fill deadline as a UNIX timestamp.
442    pub fill_deadline: Option<u64>,
443    /// Recipient of bridged funds on the destination chain.
444    pub recipient: Address,
445    /// Source chain ID.
446    pub source_chain_id: u64,
447    /// Destination chain ID.
448    pub destination_chain_id: u64,
449    /// Provider-specific bridging identifier.
450    pub bridging_id: String,
451}
452
453/// A resolved cross-chain order with bridging details.
454#[derive(Debug, Clone)]
455pub struct CrossChainOrder {
456    /// Chain ID where the order was settled.
457    pub chain_id: u64,
458    /// Bridging status result.
459    pub status_result: BridgeStatusResult,
460    /// Bridging deposit parameters.
461    pub bridging_params: BridgingDepositParams,
462    /// Settlement transaction hash.
463    pub trade_tx_hash: String,
464    /// Bridge explorer URL for tracking.
465    pub explorer_url: Option<String>,
466}
467
468/// Result from a single provider in a multi-quote request.
469#[derive(Debug, Clone)]
470pub struct MultiQuoteResult {
471    /// The provider's dApp ID.
472    pub provider_dapp_id: String,
473    /// The bridge quote, if successful.
474    pub quote: Option<BridgeQuoteAmountsAndCosts>,
475    /// Error message, if the provider failed.
476    pub error: Option<String>,
477}
478
479// ── Intermediate token info ───────────────────────────────────────────────────
480
481/// Information about a token that can serve as an intermediate hop during
482/// cross-chain bridging.
483///
484/// Mirrors `IntermediateTokenInfo` from the `TypeScript` SDK, which is
485/// structurally identical to `TokenInfo` from `@cowprotocol/sdk-config`.
486/// The Rust side is a dedicated struct so the role is explicit at the
487/// trait boundary.
488#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489pub struct IntermediateTokenInfo {
490    /// EIP-155 chain ID this token lives on.
491    pub chain_id: u64,
492    /// Token address on `chain_id` — an EVM address for ERC-20 tokens,
493    /// or a raw non-EVM identifier (Solana mint, Bitcoin native
494    /// sentinel, etc.) for non-EVM destinations.
495    pub address: TokenAddress,
496    /// Token decimals.
497    pub decimals: u8,
498    /// Token symbol (e.g. `"USDC"`).
499    pub symbol: String,
500    /// Token name (e.g. `"USD Coin"`).
501    pub name: String,
502    /// Logo URL, if available.
503    pub logo_url: Option<String>,
504}
505
506// ── Provider queries ──────────────────────────────────────────────────────────
507
508/// Parameters passed to
509/// [`BridgeProvider::get_buy_tokens`](crate::provider::BridgeProvider::get_buy_tokens)
510/// to enumerate the tokens a provider can deliver on a destination chain.
511///
512/// Mirrors `BuyTokensParams` from the `TypeScript` SDK.
513#[derive(Debug, Clone)]
514pub struct BuyTokensParams {
515    /// Source chain the user is bridging from.
516    pub sell_chain_id: u64,
517    /// Destination chain to list buyable tokens for.
518    pub buy_chain_id: u64,
519    /// Optional sell-token filter — when set, providers may restrict the
520    /// result to tokens reachable from this specific sell token.
521    pub sell_token_address: Option<Address>,
522}
523
524/// Tokens a provider can deliver on a given destination chain.
525///
526/// Mirrors `GetProviderBuyTokens` from the `TypeScript` SDK.
527#[derive(Debug, Clone)]
528pub struct GetProviderBuyTokens {
529    /// Provider metadata — mirrors the `providerInfo` field returned by the
530    /// `TypeScript` helper.
531    pub provider_info: BridgeProviderInfo,
532    /// The buyable tokens on the destination chain.
533    pub tokens: Vec<IntermediateTokenInfo>,
534}
535
536// ── Bridge deposit (decoded from hook metadata) ───────────────────────────────
537
538/// Decoded representation of a bridge deposit, typically reconstructed from
539/// `appData.metadata.bridging` and the settlement log events.
540///
541/// Mirrors `BridgeDeposit` from the `TypeScript` SDK (`types.ts`). Captures
542/// the provider's opaque `bridging_id` alongside the source/destination
543/// chain identifiers so a caller can re-query status or explorer URLs
544/// without re-parsing the original hook.
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct BridgeDeposit {
547    /// dApp identifier of the provider that produced the deposit.
548    pub provider_id: String,
549    /// Source chain where the deposit transaction was settled.
550    pub source_chain_id: u64,
551    /// Destination chain the bridge is delivering to.
552    pub destination_chain_id: u64,
553    /// Provider-specific bridging identifier (deposit address, quote ID, …).
554    pub bridging_id: String,
555    /// Settlement transaction hash on the source chain.
556    pub deposit_tx_hash: String,
557    /// Fill transaction hash on the destination chain, once known.
558    pub fill_tx_hash: Option<String>,
559    /// Amount of input tokens (in sell-token atoms).
560    pub amount_in: U256,
561    /// Amount of output tokens (in buy-token atoms), if the bridge
562    /// announced it ahead of time (otherwise populated post-fill).
563    pub amount_out: Option<U256>,
564}
565
566// ── Errors ────────────────────────────────────────────────────────────────────
567
568/// Errors specific to bridging operations.
569#[derive(Debug, thiserror::Error)]
570pub enum BridgeError {
571    /// No bridge providers are registered.
572    #[error("no providers available")]
573    NoProviders,
574    /// None of the registered providers returned a quote for this route.
575    #[error("no quote available for this route")]
576    NoQuote,
577    /// Attempted a cross-chain operation on same-chain tokens.
578    #[error("sell and buy chains must be different for cross-chain bridging")]
579    SameChain,
580    /// Only sell orders are supported for bridging.
581    #[error("bridging only supports SELL orders")]
582    OnlySellOrderSupported,
583    /// No intermediate tokens available for the requested route.
584    #[error("no intermediate tokens available")]
585    NoIntermediateTokens,
586    /// The bridge API returned an error.
587    #[error("bridge API error: {0}")]
588    ApiError(String),
589    /// Invalid API response format.
590    #[error("invalid API JSON response: {0}")]
591    InvalidApiResponse(String),
592    /// Error building the bridge transaction.
593    #[error("transaction build error: {0}")]
594    TxBuildError(String),
595    /// General quote error.
596    #[error("quote error: {0}")]
597    QuoteError(String),
598    /// No routes found.
599    #[error("no routes available")]
600    NoRoutes,
601    /// Invalid bridge configuration.
602    #[error("invalid bridge: {0}")]
603    InvalidBridge(String),
604    /// Quote does not match expected deposit address.
605    #[error("quote does not match deposit address")]
606    QuoteDoesNotMatchDepositAddress,
607    /// Sell amount is below the minimum threshold.
608    #[error("sell amount too small")]
609    SellAmountTooSmall,
610    /// Provider with the given dApp ID was not found.
611    #[error("provider not found: {dapp_id}")]
612    ProviderNotFound {
613        /// The requested dApp ID.
614        dapp_id: String,
615    },
616    /// Provider request timed out.
617    #[error("provider request timed out")]
618    Timeout,
619    /// A `CoW` Protocol API error.
620    #[error(transparent)]
621    Cow(#[from] cow_errors::CowError),
622}
623
624/// Priority of bridge quote errors for selecting the best error to surface.
625///
626/// Higher values indicate errors that are more relevant to the user.
627/// When multiple providers fail, the error with the highest priority is
628/// shown so that the most actionable message reaches the caller.
629///
630/// # Arguments
631///
632/// * `error` - The [`BridgeError`] to evaluate.
633///
634/// # Returns
635///
636/// A numeric priority (`u32`). Currently `10` for
637/// [`BridgeError::SellAmountTooSmall`], `9` for
638/// [`BridgeError::OnlySellOrderSupported`], and `1` for all other variants.
639#[must_use]
640pub const fn bridge_error_priority(error: &BridgeError) -> u32 {
641    match error {
642        BridgeError::SellAmountTooSmall => 10,
643        BridgeError::OnlySellOrderSupported => 9,
644        BridgeError::NoProviders |
645        BridgeError::NoQuote |
646        BridgeError::SameChain |
647        BridgeError::NoIntermediateTokens |
648        BridgeError::ApiError(_) |
649        BridgeError::InvalidApiResponse(_) |
650        BridgeError::TxBuildError(_) |
651        BridgeError::QuoteError(_) |
652        BridgeError::NoRoutes |
653        BridgeError::InvalidBridge(_) |
654        BridgeError::QuoteDoesNotMatchDepositAddress |
655        BridgeError::ProviderNotFound { .. } |
656        BridgeError::Timeout |
657        BridgeError::Cow(_) => 1,
658    }
659}
660
661// ── Across-specific types ─────────────────────────────────────────────────────
662
663/// A percentage fee as returned by the Across API.
664///
665/// `pct` is expressed in Across format: 1% = 1e16, 100% = 1e18.
666#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct AcrossPctFee {
668    /// Percentage as a string in contract format (1e18 = 100%).
669    pub pct: String,
670    /// Total fee amount as a string.
671    pub total: String,
672}
673
674/// Deposit size limits from the Across API.
675#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct AcrossSuggestedFeesLimits {
677    /// Minimum deposit size in token units.
678    pub min_deposit: String,
679    /// Maximum deposit size in token units.
680    pub max_deposit: String,
681    /// Maximum instant-fill deposit size.
682    pub max_deposit_instant: String,
683    /// Maximum short-delay deposit size.
684    pub max_deposit_short_delay: String,
685    /// Recommended instant deposit size.
686    pub recommended_deposit_instant: String,
687}
688
689/// Full response from the Across suggested-fees endpoint.
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct AcrossSuggestedFeesResponse {
692    /// Total relay fee (inclusive of LP fee).
693    pub total_relay_fee: AcrossPctFee,
694    /// Relayer capital fee component.
695    pub relayer_capital_fee: AcrossPctFee,
696    /// Relayer gas fee component.
697    pub relayer_gas_fee: AcrossPctFee,
698    /// LP fee component.
699    pub lp_fee: AcrossPctFee,
700    /// Quote timestamp for LP fee computation.
701    pub timestamp: String,
702    /// Whether the amount is below the minimum.
703    pub is_amount_too_low: bool,
704    /// Block number associated with the quote.
705    pub quote_block: String,
706    /// Spoke pool contract address.
707    pub spoke_pool_address: String,
708    /// Suggested exclusive relayer address.
709    pub exclusive_relayer: String,
710    /// Exclusivity deadline in seconds.
711    pub exclusivity_deadline: String,
712    /// Estimated fill time in seconds.
713    pub estimated_fill_time_sec: String,
714    /// Recommended fill deadline as a UNIX timestamp.
715    pub fill_deadline: String,
716    /// Deposit size limits.
717    pub limits: AcrossSuggestedFeesLimits,
718}
719
720/// Status of an Across deposit.
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase")]
723pub enum AcrossDepositStatus {
724    /// Deposit has been filled.
725    Filled,
726    /// Slow fill was requested.
727    SlowFillRequested,
728    /// Deposit is still pending.
729    Pending,
730    /// Deposit has expired.
731    Expired,
732    /// Deposit was refunded.
733    Refunded,
734}
735
736/// Response from the Across deposit status endpoint.
737#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct AcrossDepositStatusResponse {
739    /// Current deposit status.
740    pub status: AcrossDepositStatus,
741    /// Origin chain ID.
742    pub origin_chain_id: String,
743    /// Unique deposit identifier.
744    pub deposit_id: String,
745    /// Deposit transaction hash on the origin chain.
746    pub deposit_tx_hash: Option<String>,
747    /// Fill transaction hash on the destination chain.
748    pub fill_tx: Option<String>,
749    /// Destination chain ID.
750    pub destination_chain_id: Option<String>,
751    /// Refund transaction hash.
752    pub deposit_refund_tx_hash: Option<String>,
753}
754
755/// An Across deposit event parsed from transaction logs.
756#[derive(Debug, Clone)]
757pub struct AcrossDepositEvent {
758    /// Input token address.
759    pub input_token: Address,
760    /// Output token address.
761    pub output_token: Address,
762    /// Amount of input tokens.
763    pub input_amount: U256,
764    /// Expected output amount.
765    pub output_amount: U256,
766    /// Destination chain ID.
767    pub destination_chain_id: u64,
768    /// Unique deposit identifier.
769    pub deposit_id: U256,
770    /// Quote timestamp for fee computation.
771    pub quote_timestamp: u32,
772    /// Fill deadline as a UNIX timestamp.
773    pub fill_deadline: u32,
774    /// Exclusivity deadline.
775    pub exclusivity_deadline: u32,
776    /// Depositor address.
777    pub depositor: Address,
778    /// Recipient address.
779    pub recipient: Address,
780    /// Exclusive relayer address.
781    pub exclusive_relayer: Address,
782}
783
784/// Chain-specific token configuration for Across.
785#[derive(Debug, Clone)]
786pub struct AcrossChainConfig {
787    /// Chain ID.
788    pub chain_id: u64,
789    /// Token symbol to address mapping.
790    pub tokens: HashMap<String, Address>,
791}
792
793// ── Bungee-specific types ─────────────────────────────────────────────────────
794
795/// Supported Bungee bridge variants.
796#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
797pub enum BungeeBridge {
798    /// Across bridge via Bungee.
799    Across,
800    /// Circle CCTP bridge via Bungee.
801    CircleCctp,
802    /// Gnosis native bridge.
803    GnosisNative,
804}
805
806impl BungeeBridge {
807    /// Return the API string identifier for this bridge.
808    ///
809    /// # Returns
810    ///
811    /// A static string used in Bungee API calls:
812    /// - [`Across`](Self::Across) -> `"across"`
813    /// - [`CircleCctp`](Self::CircleCctp) -> `"cctp"`
814    /// - [`GnosisNative`](Self::GnosisNative) -> `"gnosis-native-bridge"`
815    ///
816    /// # Examples
817    ///
818    /// ```
819    /// use cow_bridging::types::BungeeBridge;
820    ///
821    /// assert_eq!(BungeeBridge::Across.as_str(), "across");
822    /// assert_eq!(BungeeBridge::CircleCctp.as_str(), "cctp");
823    /// ```
824    #[must_use]
825    pub const fn as_str(&self) -> &'static str {
826        match self {
827            Self::Across => "across",
828            Self::CircleCctp => "cctp",
829            Self::GnosisNative => "gnosis-native-bridge",
830        }
831    }
832
833    /// Try to parse a bridge from its display name.
834    ///
835    /// This is the inverse of [`display_name`](Self::display_name).
836    ///
837    /// # Arguments
838    ///
839    /// * `name` - A human-readable bridge name (e.g. `"Across"`, `"Circle CCTP"`, `"Gnosis
840    ///   Native"`).
841    ///
842    /// # Returns
843    ///
844    /// `Some(BungeeBridge)` if the name matches a known variant, `None`
845    /// otherwise.
846    #[must_use]
847    pub fn from_display_name(name: &str) -> Option<Self> {
848        match name {
849            "Across" => Some(Self::Across),
850            "Circle CCTP" => Some(Self::CircleCctp),
851            "Gnosis Native" => Some(Self::GnosisNative),
852            _ => None,
853        }
854    }
855
856    /// Return the human-readable display name.
857    ///
858    /// This is the inverse of [`from_display_name`](Self::from_display_name).
859    ///
860    /// # Returns
861    ///
862    /// A static, human-readable label for this bridge variant (e.g.
863    /// `"Across"`, `"Circle CCTP"`, `"Gnosis Native"`).
864    #[must_use]
865    pub const fn display_name(&self) -> &'static str {
866        match self {
867            Self::Across => "Across",
868            Self::CircleCctp => "Circle CCTP",
869            Self::GnosisNative => "Gnosis Native",
870        }
871    }
872}
873
874/// Bungee event status.
875#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
876#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
877pub enum BungeeEventStatus {
878    /// Event is complete.
879    Completed,
880    /// Event is still pending.
881    Pending,
882}
883
884/// Bridge name as used in Bungee events.
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
886#[serde(rename_all = "lowercase")]
887pub enum BungeeBridgeName {
888    /// Across bridge.
889    Across,
890    /// Circle CCTP bridge.
891    Cctp,
892}
893
894/// A Bungee bridge event from the events API.
895#[derive(Debug, Clone, Serialize, Deserialize)]
896pub struct BungeeEvent {
897    /// Event identifier.
898    pub identifier: String,
899    /// Source transaction hash (None when pending).
900    pub src_transaction_hash: Option<String>,
901    /// Bridge name used.
902    pub bridge_name: BungeeBridgeName,
903    /// Origin chain ID.
904    pub from_chain_id: u64,
905    /// Whether this is a `CoW` Swap trade.
906    pub is_cowswap_trade: bool,
907    /// `CoW` Protocol order ID.
908    pub order_id: String,
909    /// Source transaction status.
910    pub src_tx_status: BungeeEventStatus,
911    /// Destination transaction status.
912    pub dest_tx_status: BungeeEventStatus,
913    /// Destination transaction hash (None when pending).
914    pub dest_transaction_hash: Option<String>,
915}
916
917/// Byte offset indices for decoding Bungee transaction data.
918#[derive(Debug, Clone, Copy)]
919pub struct BungeeTxDataBytesIndex {
920    /// Byte start offset in the raw calldata.
921    pub bytes_start_index: usize,
922    /// Byte length.
923    pub bytes_length: usize,
924    /// Character start offset in the hex string (including `0x` prefix).
925    pub bytes_string_start_index: usize,
926    /// Character length in the hex string.
927    pub bytes_string_length: usize,
928}
929
930/// Decoded result from Bungee transaction data.
931#[derive(Debug, Clone)]
932pub struct DecodedBungeeTxData {
933    /// Route ID (first 4 bytes).
934    pub route_id: String,
935    /// Encoded function data (after route ID).
936    pub encoded_function_data: String,
937    /// Function selector (first 4 bytes of function data).
938    pub function_selector: String,
939}
940
941/// Decoded amounts from Bungee transaction data.
942#[derive(Debug, Clone)]
943pub struct DecodedBungeeAmounts {
944    /// Raw input amount bytes as hex string.
945    pub input_amount_bytes: String,
946    /// Parsed input amount as U256.
947    pub input_amount: U256,
948}
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953
954    // ── BridgeProviderType ──────────────────────────────────────────────
955
956    #[test]
957    fn hook_bridge_provider_is_hook() {
958        assert!(BridgeProviderType::HookBridgeProvider.is_hook_bridge_provider());
959        assert!(!BridgeProviderType::HookBridgeProvider.is_receiver_account_bridge_provider());
960    }
961
962    #[test]
963    fn receiver_account_bridge_provider_is_receiver() {
964        assert!(
965            BridgeProviderType::ReceiverAccountBridgeProvider.is_receiver_account_bridge_provider()
966        );
967        assert!(!BridgeProviderType::ReceiverAccountBridgeProvider.is_hook_bridge_provider());
968    }
969
970    // ── BridgeProviderInfo delegation ───────────────────────────────────
971
972    #[test]
973    fn bridge_provider_info_delegates_hook() {
974        let info = BridgeProviderInfo {
975            name: "test".into(),
976            logo_url: String::new(),
977            dapp_id: String::new(),
978            website: String::new(),
979            provider_type: BridgeProviderType::HookBridgeProvider,
980        };
981        assert!(info.is_hook_bridge_provider());
982        assert!(!info.is_receiver_account_bridge_provider());
983    }
984
985    #[test]
986    fn bridge_provider_info_delegates_receiver() {
987        let info = BridgeProviderInfo {
988            name: "test".into(),
989            logo_url: String::new(),
990            dapp_id: String::new(),
991            website: String::new(),
992            provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
993        };
994        assert!(info.is_receiver_account_bridge_provider());
995        assert!(!info.is_hook_bridge_provider());
996    }
997
998    // ── IntermediateTokenInfo / BuyTokensParams / GetProviderBuyTokens ──
999
1000    #[test]
1001    fn intermediate_token_info_roundtrips_through_serde() {
1002        let token = IntermediateTokenInfo {
1003            chain_id: 1,
1004            address: Address::repeat_byte(0x11).into(),
1005            decimals: 6,
1006            symbol: "USDC".into(),
1007            name: "USD Coin".into(),
1008            logo_url: Some("https://example.com/usdc.png".into()),
1009        };
1010        let json = serde_json::to_string(&token).unwrap();
1011        let decoded: IntermediateTokenInfo = serde_json::from_str(&json).unwrap();
1012        assert_eq!(token, decoded);
1013    }
1014
1015    #[test]
1016    fn token_address_evm_and_raw_roundtrip_through_serde() {
1017        let evm = TokenAddress::Evm(Address::repeat_byte(0xaa));
1018        let raw = TokenAddress::Raw("bc1qexample000000000000000000000000000000".into());
1019        let evm_json = serde_json::to_string(&evm).unwrap();
1020        let raw_json = serde_json::to_string(&raw).unwrap();
1021        assert!(evm_json.contains("\"kind\":\"Evm\""));
1022        assert!(raw_json.contains("\"kind\":\"Raw\""));
1023        let evm_decoded: TokenAddress = serde_json::from_str(&evm_json).unwrap();
1024        let raw_decoded: TokenAddress = serde_json::from_str(&raw_json).unwrap();
1025        assert_eq!(evm, evm_decoded);
1026        assert_eq!(raw, raw_decoded);
1027    }
1028
1029    #[test]
1030    fn token_address_partial_eq_with_evm_address() {
1031        let a = Address::repeat_byte(0x11);
1032        let token: TokenAddress = a.into();
1033        assert!(token == a);
1034        assert!(a == token);
1035        let raw = TokenAddress::Raw("anything".into());
1036        assert!(raw != a);
1037    }
1038
1039    #[test]
1040    fn token_address_extractors_and_discriminants() {
1041        let a = Address::repeat_byte(0x77);
1042        let evm: TokenAddress = a.into();
1043        let raw = TokenAddress::Raw("abc".into());
1044
1045        assert_eq!(evm.to_evm(), Some(a));
1046        assert_eq!(evm.as_evm(), Some(&a));
1047        assert_eq!(evm.as_raw(), None);
1048        assert!(evm.is_evm());
1049        assert!(!evm.is_raw());
1050
1051        assert_eq!(raw.to_evm(), None);
1052        assert_eq!(raw.as_evm(), None);
1053        assert_eq!(raw.as_raw(), Some("abc"));
1054        assert!(!raw.is_evm());
1055        assert!(raw.is_raw());
1056    }
1057
1058    #[test]
1059    fn token_address_from_ref_address() {
1060        let a = Address::repeat_byte(0x55);
1061        let token: TokenAddress = (&a).into();
1062        assert_eq!(token.to_evm(), Some(a));
1063    }
1064
1065    #[test]
1066    fn buy_tokens_params_optional_sell_token() {
1067        let with_filter = BuyTokensParams {
1068            sell_chain_id: 1,
1069            buy_chain_id: 100,
1070            sell_token_address: Some(Address::repeat_byte(0x22)),
1071        };
1072        assert_eq!(with_filter.sell_chain_id, 1);
1073        assert_eq!(with_filter.buy_chain_id, 100);
1074        assert!(with_filter.sell_token_address.is_some());
1075
1076        let without_filter =
1077            BuyTokensParams { sell_chain_id: 1, buy_chain_id: 100, sell_token_address: None };
1078        assert!(without_filter.sell_token_address.is_none());
1079    }
1080
1081    #[test]
1082    fn get_provider_buy_tokens_carries_info_and_tokens() {
1083        let info = BridgeProviderInfo {
1084            name: "p".into(),
1085            logo_url: String::new(),
1086            dapp_id: "cow-sdk://bridging/providers/p".into(),
1087            website: String::new(),
1088            provider_type: BridgeProviderType::HookBridgeProvider,
1089        };
1090        let token = IntermediateTokenInfo {
1091            chain_id: 1,
1092            address: Address::ZERO.into(),
1093            decimals: 18,
1094            symbol: "WETH".into(),
1095            name: "Wrapped Ether".into(),
1096            logo_url: None,
1097        };
1098        let result = GetProviderBuyTokens { provider_info: info, tokens: vec![token] };
1099        assert_eq!(result.tokens.len(), 1);
1100        assert_eq!(result.tokens[0].symbol, "WETH");
1101        assert!(result.provider_info.is_hook_bridge_provider());
1102    }
1103
1104    // ── BridgeDeposit ───────────────────────────────────────────────────
1105
1106    #[test]
1107    fn bridge_deposit_roundtrips_through_serde() {
1108        let deposit = BridgeDeposit {
1109            provider_id: "cow-sdk://bridging/providers/across".into(),
1110            source_chain_id: 1,
1111            destination_chain_id: 10,
1112            bridging_id: "42".into(),
1113            deposit_tx_hash: "0xdead".into(),
1114            fill_tx_hash: Some("0xbeef".into()),
1115            amount_in: U256::from(1_000_000u64),
1116            amount_out: Some(U256::from(999_500u64)),
1117        };
1118        let json = serde_json::to_string(&deposit).unwrap();
1119        let decoded: BridgeDeposit = serde_json::from_str(&json).unwrap();
1120        assert_eq!(deposit, decoded);
1121    }
1122
1123    #[test]
1124    fn bridge_deposit_fill_tx_hash_optional() {
1125        let deposit = BridgeDeposit {
1126            provider_id: "p".into(),
1127            source_chain_id: 1,
1128            destination_chain_id: 10,
1129            bridging_id: "42".into(),
1130            deposit_tx_hash: "0xabc".into(),
1131            fill_tx_hash: None,
1132            amount_in: U256::from(500u64),
1133            amount_out: None,
1134        };
1135        assert!(deposit.fill_tx_hash.is_none());
1136        assert!(deposit.amount_out.is_none());
1137    }
1138
1139    // ── BridgeStatus ────────────────────────────────────────────────────
1140
1141    #[test]
1142    fn bridge_status_variants_are_distinct() {
1143        let statuses = [
1144            BridgeStatus::InProgress,
1145            BridgeStatus::Executed,
1146            BridgeStatus::Expired,
1147            BridgeStatus::Refund,
1148            BridgeStatus::Unknown,
1149        ];
1150        for (i, a) in statuses.iter().enumerate() {
1151            for (j, b) in statuses.iter().enumerate() {
1152                if i == j {
1153                    assert_eq!(a, b);
1154                } else {
1155                    assert_ne!(a, b);
1156                }
1157            }
1158        }
1159    }
1160
1161    // ── BridgeStatusResult ──────────────────────────────────────────────
1162
1163    #[test]
1164    fn bridge_status_result_new_sets_status_only() {
1165        let r = BridgeStatusResult::new(BridgeStatus::Executed);
1166        assert_eq!(r.status, BridgeStatus::Executed);
1167        assert!(r.fill_time_in_seconds.is_none());
1168        assert!(r.deposit_tx_hash.is_none());
1169        assert!(r.fill_tx_hash.is_none());
1170    }
1171
1172    #[test]
1173    fn bridge_status_result_new_all_statuses() {
1174        for status in [
1175            BridgeStatus::InProgress,
1176            BridgeStatus::Executed,
1177            BridgeStatus::Expired,
1178            BridgeStatus::Refund,
1179            BridgeStatus::Unknown,
1180        ] {
1181            let r = BridgeStatusResult::new(status);
1182            assert_eq!(r.status, status);
1183        }
1184    }
1185
1186    // ── QuoteBridgeResponse ─────────────────────────────────────────────
1187
1188    fn make_quote(hook: Option<CowHook>, fee: U256) -> QuoteBridgeResponse {
1189        QuoteBridgeResponse {
1190            provider: "across".into(),
1191            sell_amount: U256::from(1000u64),
1192            buy_amount: U256::from(950u64),
1193            fee_amount: fee,
1194            estimated_secs: 60,
1195            bridge_hook: hook,
1196        }
1197    }
1198
1199    #[test]
1200    fn has_bridge_hook_true_when_some() {
1201        let hook = CowHook {
1202            target: "0xdead".into(),
1203            call_data: "0x".into(),
1204            gas_limit: "100000".into(),
1205            dapp_id: None,
1206        };
1207        let q = make_quote(Some(hook), U256::ZERO);
1208        assert!(q.has_bridge_hook());
1209    }
1210
1211    #[test]
1212    fn has_bridge_hook_false_when_none() {
1213        let q = make_quote(None, U256::ZERO);
1214        assert!(!q.has_bridge_hook());
1215    }
1216
1217    #[test]
1218    fn provider_ref_returns_provider_name() {
1219        let q = make_quote(None, U256::ZERO);
1220        assert_eq!(q.provider_ref(), "across");
1221    }
1222
1223    #[test]
1224    fn net_buy_amount_subtracts_fee() {
1225        let q = make_quote(None, U256::from(50u64));
1226        assert_eq!(q.net_buy_amount(), U256::from(900u64));
1227    }
1228
1229    #[test]
1230    fn net_buy_amount_saturates_at_zero() {
1231        let q = make_quote(None, U256::from(2000u64));
1232        assert_eq!(q.net_buy_amount(), U256::ZERO);
1233    }
1234
1235    #[test]
1236    fn net_buy_amount_zero_fee() {
1237        let q = make_quote(None, U256::ZERO);
1238        assert_eq!(q.net_buy_amount(), U256::from(950u64));
1239    }
1240
1241    // ── BungeeBridge ────────────────────────────────────────────────────
1242
1243    #[test]
1244    fn bungee_bridge_as_str() {
1245        assert_eq!(BungeeBridge::Across.as_str(), "across");
1246        assert_eq!(BungeeBridge::CircleCctp.as_str(), "cctp");
1247        assert_eq!(BungeeBridge::GnosisNative.as_str(), "gnosis-native-bridge");
1248    }
1249
1250    #[test]
1251    fn bungee_bridge_display_name() {
1252        assert_eq!(BungeeBridge::Across.display_name(), "Across");
1253        assert_eq!(BungeeBridge::CircleCctp.display_name(), "Circle CCTP");
1254        assert_eq!(BungeeBridge::GnosisNative.display_name(), "Gnosis Native");
1255    }
1256
1257    #[test]
1258    fn bungee_bridge_from_display_name_valid() {
1259        assert_eq!(BungeeBridge::from_display_name("Across"), Some(BungeeBridge::Across));
1260        assert_eq!(BungeeBridge::from_display_name("Circle CCTP"), Some(BungeeBridge::CircleCctp));
1261        assert_eq!(
1262            BungeeBridge::from_display_name("Gnosis Native"),
1263            Some(BungeeBridge::GnosisNative)
1264        );
1265    }
1266
1267    #[test]
1268    fn bungee_bridge_from_display_name_invalid() {
1269        assert_eq!(BungeeBridge::from_display_name("across"), None);
1270        assert_eq!(BungeeBridge::from_display_name(""), None);
1271        assert_eq!(BungeeBridge::from_display_name("Unknown"), None);
1272    }
1273
1274    #[test]
1275    fn bungee_bridge_roundtrip_display_name() {
1276        for bridge in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
1277            let name = bridge.display_name();
1278            assert_eq!(BungeeBridge::from_display_name(name), Some(bridge));
1279        }
1280    }
1281
1282    // ── BridgeError priority ────────────────────────────────────────────
1283
1284    #[test]
1285    fn sell_amount_too_small_has_highest_priority() {
1286        assert_eq!(bridge_error_priority(&BridgeError::SellAmountTooSmall), 10);
1287    }
1288
1289    #[test]
1290    fn only_sell_order_supported_has_second_priority() {
1291        assert_eq!(bridge_error_priority(&BridgeError::OnlySellOrderSupported), 9);
1292    }
1293
1294    #[test]
1295    fn other_errors_have_base_priority() {
1296        let base_errors: Vec<BridgeError> = vec![
1297            BridgeError::NoProviders,
1298            BridgeError::NoQuote,
1299            BridgeError::SameChain,
1300            BridgeError::NoIntermediateTokens,
1301            BridgeError::ApiError("test".into()),
1302            BridgeError::InvalidApiResponse("test".into()),
1303            BridgeError::TxBuildError("test".into()),
1304            BridgeError::QuoteError("test".into()),
1305            BridgeError::NoRoutes,
1306            BridgeError::InvalidBridge("test".into()),
1307            BridgeError::QuoteDoesNotMatchDepositAddress,
1308            BridgeError::ProviderNotFound { dapp_id: "test".into() },
1309            BridgeError::Timeout,
1310        ];
1311        for e in &base_errors {
1312            assert_eq!(bridge_error_priority(e), 1, "expected priority 1 for {e}");
1313        }
1314    }
1315
1316    // ── BridgeError Display ─────────────────────────────────────────────
1317
1318    #[test]
1319    fn bridge_error_display_messages() {
1320        assert_eq!(BridgeError::NoProviders.to_string(), "no providers available");
1321        assert_eq!(BridgeError::NoQuote.to_string(), "no quote available for this route");
1322        assert_eq!(
1323            BridgeError::SameChain.to_string(),
1324            "sell and buy chains must be different for cross-chain bridging"
1325        );
1326        assert_eq!(
1327            BridgeError::OnlySellOrderSupported.to_string(),
1328            "bridging only supports SELL orders"
1329        );
1330        assert_eq!(
1331            BridgeError::NoIntermediateTokens.to_string(),
1332            "no intermediate tokens available"
1333        );
1334        assert_eq!(BridgeError::ApiError("oops".into()).to_string(), "bridge API error: oops");
1335        assert_eq!(
1336            BridgeError::InvalidApiResponse("bad".into()).to_string(),
1337            "invalid API JSON response: bad"
1338        );
1339        assert_eq!(
1340            BridgeError::TxBuildError("fail".into()).to_string(),
1341            "transaction build error: fail"
1342        );
1343        assert_eq!(BridgeError::QuoteError("nope".into()).to_string(), "quote error: nope");
1344        assert_eq!(BridgeError::NoRoutes.to_string(), "no routes available");
1345        assert_eq!(BridgeError::InvalidBridge("x".into()).to_string(), "invalid bridge: x");
1346        assert_eq!(
1347            BridgeError::QuoteDoesNotMatchDepositAddress.to_string(),
1348            "quote does not match deposit address"
1349        );
1350        assert_eq!(BridgeError::SellAmountTooSmall.to_string(), "sell amount too small");
1351        assert_eq!(
1352            BridgeError::ProviderNotFound { dapp_id: "foo".into() }.to_string(),
1353            "provider not found: foo"
1354        );
1355        assert_eq!(BridgeError::Timeout.to_string(), "provider request timed out");
1356    }
1357
1358    // ── Serde roundtrips ────────────────────────────────────────────────
1359
1360    #[test]
1361    fn bridge_provider_type_serde_roundtrip() {
1362        for v in [
1363            BridgeProviderType::HookBridgeProvider,
1364            BridgeProviderType::ReceiverAccountBridgeProvider,
1365        ] {
1366            let json = serde_json::to_string(&v).unwrap();
1367            let back: BridgeProviderType = serde_json::from_str(&json).unwrap();
1368            assert_eq!(v, back);
1369        }
1370    }
1371
1372    #[test]
1373    fn bridge_status_serde_roundtrip() {
1374        for v in [
1375            BridgeStatus::InProgress,
1376            BridgeStatus::Executed,
1377            BridgeStatus::Expired,
1378            BridgeStatus::Refund,
1379            BridgeStatus::Unknown,
1380        ] {
1381            let json = serde_json::to_string(&v).unwrap();
1382            let back: BridgeStatus = serde_json::from_str(&json).unwrap();
1383            assert_eq!(v, back);
1384        }
1385    }
1386
1387    #[test]
1388    fn bungee_bridge_serde_roundtrip() {
1389        for v in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
1390            let json = serde_json::to_string(&v).unwrap();
1391            let back: BungeeBridge = serde_json::from_str(&json).unwrap();
1392            assert_eq!(v, back);
1393        }
1394    }
1395
1396    #[test]
1397    fn across_deposit_status_serde_roundtrip() {
1398        for v in [
1399            AcrossDepositStatus::Filled,
1400            AcrossDepositStatus::SlowFillRequested,
1401            AcrossDepositStatus::Pending,
1402            AcrossDepositStatus::Expired,
1403            AcrossDepositStatus::Refunded,
1404        ] {
1405            let json = serde_json::to_string(&v).unwrap();
1406            let back: AcrossDepositStatus = serde_json::from_str(&json).unwrap();
1407            assert_eq!(v, back);
1408        }
1409    }
1410
1411    #[test]
1412    fn across_deposit_status_camel_case_serialization() {
1413        assert_eq!(serde_json::to_string(&AcrossDepositStatus::Filled).unwrap(), "\"filled\"");
1414        assert_eq!(
1415            serde_json::to_string(&AcrossDepositStatus::SlowFillRequested).unwrap(),
1416            "\"slowFillRequested\""
1417        );
1418        assert_eq!(serde_json::to_string(&AcrossDepositStatus::Pending).unwrap(), "\"pending\"");
1419    }
1420
1421    #[test]
1422    fn bungee_event_status_screaming_snake_case() {
1423        assert_eq!(serde_json::to_string(&BungeeEventStatus::Completed).unwrap(), "\"COMPLETED\"");
1424        assert_eq!(serde_json::to_string(&BungeeEventStatus::Pending).unwrap(), "\"PENDING\"");
1425    }
1426
1427    #[test]
1428    fn bungee_bridge_name_lowercase_serialization() {
1429        assert_eq!(serde_json::to_string(&BungeeBridgeName::Across).unwrap(), "\"across\"");
1430        assert_eq!(serde_json::to_string(&BungeeBridgeName::Cctp).unwrap(), "\"cctp\"");
1431    }
1432}