Skip to main content

cow_bridging/
provider.rs

1//! [`BridgeProvider`] trait for bridge integrations.
2//!
3//! Providers that implement this trait expose the full surface needed by the
4//! [`BridgingSdk`](crate::sdk::BridgingSdk) to orchestrate cross-chain
5//! trades: discovery (`info`, `get_networks`, `get_buy_tokens`,
6//! `get_intermediate_tokens`), quoting (`get_quote`), routing (`supports_route`),
7//! and post-settlement observability (`get_bridging_params`, `get_status`,
8//! `get_explorer_url`).
9//!
10//! The trait mirrors the `BridgeProvider<Q>` interface from the `TypeScript`
11//! SDK. Two specialisations layered on top — `HookBridgeProvider` and
12//! `ReceiverAccountBridgeProvider` — will be added in follow-up PRs to
13//! cover providers that submit a signed hook vs. providers that redirect
14//! funds to a deposit address.
15
16use std::pin::Pin;
17
18use alloy_primitives::{Address, B256};
19use alloy_signer_local::PrivateKeySigner;
20use cow_chains::SupportedChainId;
21use cow_errors::CowError;
22use cow_orderbook::types::Order;
23
24use super::types::{
25    BridgeProviderInfo, BridgeStatusResult, BridgingDepositParams, BuyTokensParams,
26    GetProviderBuyTokens, IntermediateTokenInfo, QuoteBridgeRequest, QuoteBridgeResponse,
27};
28
29/// Deposit parameters and bridge status returned by
30/// [`BridgeProvider::get_bridging_params`].
31///
32/// Mirrors the `{ params, status }` tuple returned by the `TypeScript`
33/// helper of the same name.
34#[derive(Debug, Clone)]
35pub struct BridgingParamsResult {
36    /// Decoded deposit parameters.
37    pub params: BridgingDepositParams,
38    /// Bridge status at the time of decoding.
39    pub status: BridgeStatusResult,
40}
41
42/// Network / chain metadata returned by [`BridgeProvider::get_networks`].
43///
44/// Kept minimal on purpose — providers return the chain ID plus optional
45/// display metadata; consumers can resolve richer [`cow_chains::ChainInfo`]
46/// from the ID if they need it.
47#[derive(Debug, Clone)]
48pub struct BridgeNetworkInfo {
49    /// Chain ID.
50    pub chain_id: u64,
51    /// Display name (e.g. `"Ethereum"`).
52    pub name: String,
53    /// Logo URL, if available.
54    pub logo_url: Option<String>,
55}
56
57// ── Future type aliases ───────────────────────────────────────────────────────
58//
59// Each public method that performs I/O returns a pinned, boxed future. On
60// native targets the future is `Send` so it can be spawned across tasks;
61// on WASM it is not, matching the browser `fetch` API.
62
63macro_rules! provider_future {
64    ($name:ident, $output:ty) => {
65        #[cfg(not(target_arch = "wasm32"))]
66        #[doc = concat!("Future returned by `BridgeProvider::", stringify!($name), "`.")]
67        pub type $name<'a> =
68            Pin<Box<dyn std::future::Future<Output = Result<$output, CowError>> + Send + 'a>>;
69
70        #[cfg(target_arch = "wasm32")]
71        #[doc = concat!("Future returned by `BridgeProvider::", stringify!($name), "`.")]
72        pub type $name<'a> =
73            Pin<Box<dyn std::future::Future<Output = Result<$output, CowError>> + 'a>>;
74    };
75}
76
77provider_future!(QuoteFuture, QuoteBridgeResponse);
78provider_future!(NetworksFuture, Vec<BridgeNetworkInfo>);
79provider_future!(BuyTokensFuture, GetProviderBuyTokens);
80provider_future!(IntermediateTokensFuture, Vec<IntermediateTokenInfo>);
81provider_future!(BridgingParamsFuture, Option<BridgingParamsResult>);
82provider_future!(BridgeStatusFuture, BridgeStatusResult);
83provider_future!(UnsignedCallFuture, cow_chains::EvmCall);
84provider_future!(SignedHookFuture, crate::types::BridgeHook);
85provider_future!(ReceiverOverrideFuture, String);
86provider_future!(GasEstimationFuture, u64);
87
88// ── Thread-safety marker ──────────────────────────────────────────────────────
89//
90// On native targets the trait is `Send + Sync` so providers can be shared
91// between tasks. On WASM the bounds are dropped because the browser
92// `fetch` API is single-threaded. Encoding this via a blanket auto-trait
93// lets us keep a single trait definition below (avoids duplicating ~60
94// lines of method signatures under two `cfg` branches).
95
96/// Auto-implemented marker adding `Send + Sync` on native and nothing on
97/// WASM. Used as a bound on [`BridgeProvider`].
98#[cfg(not(target_arch = "wasm32"))]
99pub trait MaybeSendSync: Send + Sync {}
100#[cfg(not(target_arch = "wasm32"))]
101impl<T: ?Sized + Send + Sync> MaybeSendSync for T {}
102
103/// Auto-implemented marker adding `Send + Sync` on native and nothing on
104/// WASM. Used as a bound on [`BridgeProvider`].
105#[cfg(target_arch = "wasm32")]
106pub trait MaybeSendSync {}
107#[cfg(target_arch = "wasm32")]
108impl<T: ?Sized> MaybeSendSync for T {}
109
110// ── Trait definition ──────────────────────────────────────────────────────────
111
112/// Trait implemented by cross-chain bridge providers (Across, Bungee,
113/// NEAR Intents, …).
114///
115/// Mirrors the `BridgeProvider<Q>` interface from the `TypeScript` SDK.
116/// Concrete providers typically implement one of the two specialisations
117/// `HookBridgeProvider` or `ReceiverAccountBridgeProvider` on top of
118/// this base trait (both coming in follow-up PRs).
119///
120/// # Native vs. WASM
121///
122/// On native targets the trait requires `Send + Sync` so providers can
123/// be shared between tasks. On `wasm32` targets those bounds are dropped
124/// via [`MaybeSendSync`] because the browser `fetch` API is
125/// single-threaded.
126pub trait BridgeProvider: MaybeSendSync {
127    /// Metadata about this provider (name, logo, dApp ID, …).
128    ///
129    /// Mirrors the `info` field on the `TypeScript` interface.
130    fn info(&self) -> &BridgeProviderInfo;
131
132    /// A short identifier for this provider (e.g. `"bungee"`).
133    ///
134    /// Default implementation delegates to [`info().name`](BridgeProviderInfo::name).
135    fn name(&self) -> &str {
136        &self.info().name
137    }
138
139    /// Returns `true` if this provider supports the given route.
140    ///
141    /// # Arguments
142    ///
143    /// * `sell_chain` — chain ID of the source (sell) chain.
144    /// * `buy_chain` — chain ID of the destination (buy) chain.
145    fn supports_route(&self, sell_chain: u64, buy_chain: u64) -> bool;
146
147    /// List the networks (source or destination) supported by this provider.
148    ///
149    /// Mirrors `getNetworks()` from the `TypeScript` SDK.
150    fn get_networks<'a>(&'a self) -> NetworksFuture<'a>;
151
152    /// List the tokens this provider can deliver on a destination chain.
153    ///
154    /// Mirrors `getBuyTokens(params)` from the `TypeScript` SDK.
155    fn get_buy_tokens<'a>(&'a self, params: BuyTokensParams) -> BuyTokensFuture<'a>;
156
157    /// List candidate intermediate tokens for a bridging request.
158    ///
159    /// Used by the orchestration layer to pick the best hop token before
160    /// asking `TradingSdk` for a swap quote. Mirrors
161    /// `getIntermediateTokens(request)` from the `TypeScript` SDK.
162    fn get_intermediate_tokens<'a>(
163        &'a self,
164        request: &'a QuoteBridgeRequest,
165    ) -> IntermediateTokensFuture<'a>;
166
167    /// Fetch a bridge quote for `req`.
168    ///
169    /// Returns a pinned, boxed future that resolves to a
170    /// [`QuoteBridgeResponse`] on success, or a [`CowError`] if the provider
171    /// is unreachable or the route is unsupported.
172    fn get_quote<'a>(&'a self, req: &'a QuoteBridgeRequest) -> QuoteFuture<'a>;
173
174    /// Reconstruct bridging deposit parameters from a settlement transaction.
175    ///
176    /// Given a chain, a fully-fetched [`Order`] (including executed amounts,
177    /// equivalent to `EnrichedOrder` on the `TypeScript` side), and the
178    /// settlement transaction hash, the provider scans the logs and
179    /// rebuilds a [`BridgingDepositParams`] alongside the current bridge
180    /// status.
181    ///
182    /// Returns `Ok(None)` if the transaction does not contain a deposit
183    /// attributable to this provider.
184    ///
185    /// The `settlement_override` argument mirrors the upstream
186    /// `settlementContractOverride` parameter (cow-sdk#807) — callers can
187    /// inject a custom settlement contract address for chains where the
188    /// default is not deployed.
189    fn get_bridging_params<'a>(
190        &'a self,
191        chain_id: u64,
192        order: &'a Order,
193        tx_hash: B256,
194        settlement_override: Option<Address>,
195    ) -> BridgingParamsFuture<'a>;
196
197    /// Return the provider's explorer URL for a given bridging ID.
198    ///
199    /// Mirrors `getExplorerUrl(bridgingId)` from the `TypeScript` SDK.
200    fn get_explorer_url(&self, bridging_id: &str) -> String;
201
202    /// Fetch the current bridge status for a given `bridging_id`.
203    ///
204    /// Mirrors `getStatus(bridgingId, originChainId)` from the `TypeScript`
205    /// SDK.
206    fn get_status<'a>(
207        &'a self,
208        bridging_id: &'a str,
209        origin_chain_id: u64,
210    ) -> BridgeStatusFuture<'a>;
211
212    /// Downcast this provider to a [`HookBridgeProvider`] trait object.
213    ///
214    /// Returns `Some(self)` if the concrete type also implements the
215    /// [`HookBridgeProvider`] sub-trait, `None` otherwise. Default
216    /// implementation returns `None`; hook-based providers (Across,
217    /// Bungee, …) override it.
218    ///
219    /// The orchestrator in [`crate::sdk::get_quote_with_bridge`] uses
220    /// this to dispatch between the hook and receiver-account branches
221    /// from a `&dyn BridgeProvider` without paying the cost (and losing
222    /// the trait bounds) of an `Any` downcast.
223    ///
224    /// Mirrors the role of the `isHookBridgeProvider` type-guard in
225    /// the `TypeScript` SDK, with the added benefit of returning the
226    /// upcast trait object directly.
227    fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
228        None
229    }
230
231    /// Downcast this provider to a [`ReceiverAccountBridgeProvider`] trait object.
232    ///
233    /// Returns `Some(self)` if the concrete type also implements the
234    /// [`ReceiverAccountBridgeProvider`] sub-trait, `None` otherwise.
235    /// Default implementation returns `None`; receiver-account
236    /// providers (NEAR Intents, …) override it.
237    ///
238    /// Mirrors the role of the `isReceiverAccountBridgeProvider`
239    /// type-guard in the `TypeScript` SDK.
240    fn as_receiver_account_bridge_provider(&self) -> Option<&dyn ReceiverAccountBridgeProvider> {
241        None
242    }
243}
244
245// ── Sub-traits ────────────────────────────────────────────────────────────────
246
247/// A [`BridgeProvider`] that triggers the bridge through a signed `CoW` Shed
248/// post-hook (e.g. Across, Bungee).
249///
250/// Mirrors the `HookBridgeProvider<Q>` specialisation of the `TypeScript`
251/// SDK. Implementors build an EVM call that the settlement solver
252/// executes as a post-interaction, then sign it under the user's
253/// `CoW` Shed proxy so the bridge contract can pull the intermediate funds.
254///
255/// # Required methods
256///
257/// | Method | Purpose |
258/// |---|---|
259/// | [`get_unsigned_bridge_call`](Self::get_unsigned_bridge_call) | Build the raw EVM call targeting the bridge contract |
260/// | [`get_gas_limit_estimation_for_hook`](Self::get_gas_limit_estimation_for_hook) | Estimate gas without knowing the final amount |
261/// | [`get_signed_hook`](Self::get_signed_hook) | Wrap the call in a `CoW` Shed EIP-712 signed hook |
262pub trait HookBridgeProvider: BridgeProvider {
263    /// Build the unsigned EVM call that initiates the bridge.
264    ///
265    /// The call is later wrapped into a `CoW` Shed post-hook and signed
266    /// with [`get_signed_hook`](Self::get_signed_hook). Mirrors
267    /// `getUnsignedBridgeCall(request, quote)` from the `TypeScript` SDK.
268    fn get_unsigned_bridge_call<'a>(
269        &'a self,
270        request: &'a QuoteBridgeRequest,
271        quote: &'a QuoteBridgeResponse,
272    ) -> UnsignedCallFuture<'a>;
273
274    /// Estimate the gas limit for the bridge post-hook before the final
275    /// amount is known.
276    ///
277    /// Used upstream of the quote to avoid a chicken-and-egg problem
278    /// between amount and gas cost. Mirrors
279    /// `getGasLimitEstimationForHook(request, extraGas, extraGasProxyCreation)`.
280    ///
281    /// The default implementation delegates to the free-standing
282    /// [`get_gas_limit_estimation_for_hook`](crate::utils::get_gas_limit_estimation_for_hook)
283    /// helper; providers can override for route-specific logic (e.g.
284    /// Bungee's +350k buffer for mainnet → gnosis).
285    fn get_gas_limit_estimation_for_hook<'a>(
286        &'a self,
287        proxy_deployed: bool,
288        extra_gas: Option<u64>,
289        extra_gas_proxy_creation: Option<u64>,
290    ) -> GasEstimationFuture<'a> {
291        let gas = crate::utils::get_gas_limit_estimation_for_hook(
292            proxy_deployed,
293            extra_gas,
294            extra_gas_proxy_creation,
295        );
296        Box::pin(async move { Ok(gas) })
297    }
298
299    /// Produce a signed bridge hook ready to attach to the order's app data.
300    ///
301    /// Mirrors `getSignedHook(chainId, unsignedCall, bridgeHookNonce, deadline,
302    /// hookGasLimit, signer)` from the `TypeScript` SDK. Typically delegates
303    /// to the `cow-shed` `sign_hook` helper once PR #5 lands.
304    #[allow(clippy::too_many_arguments, reason = "1:1 mirror of the TS signature")]
305    fn get_signed_hook<'a>(
306        &'a self,
307        chain_id: SupportedChainId,
308        unsigned_call: &'a cow_chains::EvmCall,
309        bridge_hook_nonce: &'a str,
310        deadline: u64,
311        hook_gas_limit: u64,
312        signer: &'a PrivateKeySigner,
313    ) -> SignedHookFuture<'a>;
314}
315
316/// A [`BridgeProvider`] that relies on a deposit address (e.g. NEAR Intents).
317///
318/// Mirrors the `ReceiverAccountBridgeProvider<Q>` specialisation of the
319/// `TypeScript` SDK. Instead of injecting a post-hook, the provider
320/// declares a deposit address that the user swaps into; the bridge
321/// detects the deposit off-chain and relays it to the destination chain.
322pub trait ReceiverAccountBridgeProvider: BridgeProvider {
323    /// Return the deposit address that the `CoW` swap should pay into to
324    /// trigger this bridge.
325    ///
326    /// Mirrors `getBridgeReceiverOverride(quoteRequest, quoteResult)` from
327    /// the `TypeScript` SDK.
328    fn get_bridge_receiver_override<'a>(
329        &'a self,
330        quote_request: &'a QuoteBridgeRequest,
331        quote_result: &'a QuoteBridgeResponse,
332    ) -> ReceiverOverrideFuture<'a>;
333}
334
335// ── Type guards ───────────────────────────────────────────────────────────────
336
337/// Returns `true` if the provider's [`BridgeProviderInfo::provider_type`] is
338/// [`BridgeProviderType::HookBridgeProvider`](crate::types::BridgeProviderType::HookBridgeProvider).
339///
340/// Mirrors `isHookBridgeProvider` from the `TypeScript` SDK. Useful for
341/// dispatching over a collection of `&dyn BridgeProvider` trait objects
342/// without downcasting.
343#[must_use]
344pub fn is_hook_bridge_provider<P: BridgeProvider + ?Sized>(provider: &P) -> bool {
345    provider.info().is_hook_bridge_provider()
346}
347
348/// Returns `true` if the provider's [`BridgeProviderInfo::provider_type`] is
349/// [`BridgeProviderType::ReceiverAccountBridgeProvider`](crate::types::BridgeProviderType::ReceiverAccountBridgeProvider).
350///
351/// Mirrors `isReceiverAccountBridgeProvider` from the `TypeScript` SDK.
352#[must_use]
353pub fn is_receiver_account_bridge_provider<P: BridgeProvider + ?Sized>(provider: &P) -> bool {
354    provider.info().is_receiver_account_bridge_provider()
355}
356
357#[cfg(all(test, not(target_arch = "wasm32")))]
358#[allow(clippy::tests_outside_test_module, reason = "inner module + cfg guard for WASM test skip")]
359mod tests {
360    use alloy_primitives::U256;
361
362    use crate::types::{BridgeProviderType, BridgeStatus};
363
364    use super::*;
365
366    // ── BridgeNetworkInfo ───────────────────────────────────────────────
367
368    #[test]
369    fn bridge_network_info_holds_chain_metadata() {
370        let info = BridgeNetworkInfo {
371            chain_id: 1,
372            name: "Ethereum".into(),
373            logo_url: Some("https://example.com/eth.png".into()),
374        };
375        assert_eq!(info.chain_id, 1);
376        assert_eq!(info.name, "Ethereum");
377        assert!(info.logo_url.is_some());
378    }
379
380    #[test]
381    fn bridge_network_info_logo_optional() {
382        let info = BridgeNetworkInfo { chain_id: 100, name: "Gnosis".into(), logo_url: None };
383        assert!(info.logo_url.is_none());
384    }
385
386    // ── BridgingParamsResult ────────────────────────────────────────────
387
388    #[test]
389    fn bridging_params_result_bundles_params_and_status() {
390        let params = BridgingDepositParams {
391            input_token_address: Address::ZERO,
392            output_token_address: Address::ZERO,
393            input_amount: U256::from(1000u64),
394            output_amount: None,
395            owner: Address::ZERO,
396            quote_timestamp: None,
397            fill_deadline: None,
398            recipient: Address::ZERO,
399            source_chain_id: 1,
400            destination_chain_id: 10,
401            bridging_id: "abc".into(),
402        };
403        let status = BridgeStatusResult {
404            status: BridgeStatus::InProgress,
405            fill_time_in_seconds: None,
406            deposit_tx_hash: None,
407            fill_tx_hash: None,
408        };
409        let bundle = BridgingParamsResult { params, status };
410        assert_eq!(bundle.params.bridging_id, "abc");
411        assert_eq!(bundle.status.status, BridgeStatus::InProgress);
412    }
413
414    // ── Trait default impl coverage ─────────────────────────────────────
415
416    struct FakeProvider {
417        info: BridgeProviderInfo,
418    }
419
420    impl BridgeProvider for FakeProvider {
421        fn info(&self) -> &BridgeProviderInfo {
422            &self.info
423        }
424        fn supports_route(&self, _sell: u64, _buy: u64) -> bool {
425            true
426        }
427        fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
428            Box::pin(async { Ok(Vec::new()) })
429        }
430        fn get_buy_tokens<'a>(&'a self, _params: BuyTokensParams) -> BuyTokensFuture<'a> {
431            let info = self.info.clone();
432            Box::pin(
433                async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
434            )
435        }
436        fn get_intermediate_tokens<'a>(
437            &'a self,
438            _request: &'a QuoteBridgeRequest,
439        ) -> IntermediateTokensFuture<'a> {
440            Box::pin(async { Ok(Vec::new()) })
441        }
442        fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
443            Box::pin(async {
444                Ok(QuoteBridgeResponse {
445                    provider: "fake".into(),
446                    sell_amount: U256::ZERO,
447                    buy_amount: U256::ZERO,
448                    fee_amount: U256::ZERO,
449                    estimated_secs: 0,
450                    bridge_hook: None,
451                })
452            })
453        }
454        fn get_bridging_params<'a>(
455            &'a self,
456            _chain_id: u64,
457            _order: &'a cow_orderbook::types::Order,
458            _tx_hash: B256,
459            _settlement_override: Option<Address>,
460        ) -> BridgingParamsFuture<'a> {
461            Box::pin(async { Ok(None) })
462        }
463        fn get_explorer_url(&self, bridging_id: &str) -> String {
464            format!("https://example.com/{bridging_id}")
465        }
466        fn get_status<'a>(
467            &'a self,
468            _bridging_id: &'a str,
469            _origin_chain_id: u64,
470        ) -> BridgeStatusFuture<'a> {
471            Box::pin(async {
472                Ok(BridgeStatusResult {
473                    status: BridgeStatus::Unknown,
474                    fill_time_in_seconds: None,
475                    deposit_tx_hash: None,
476                    fill_tx_hash: None,
477                })
478            })
479        }
480    }
481
482    fn fake_info() -> BridgeProviderInfo {
483        BridgeProviderInfo {
484            name: "fake-provider".into(),
485            logo_url: "https://example.com/logo.svg".into(),
486            dapp_id: "cow-sdk://bridging/providers/fake".into(),
487            website: "https://example.com".into(),
488            provider_type: BridgeProviderType::HookBridgeProvider,
489        }
490    }
491
492    #[test]
493    fn default_name_delegates_to_info() {
494        let provider = FakeProvider { info: fake_info() };
495        assert_eq!(provider.name(), "fake-provider");
496        assert_eq!(provider.name(), provider.info().name.as_str());
497    }
498
499    #[test]
500    fn default_explorer_url_composes_path() {
501        let provider = FakeProvider { info: fake_info() };
502        assert_eq!(provider.get_explorer_url("deposit-42"), "https://example.com/deposit-42");
503    }
504
505    #[tokio::test]
506    async fn trait_object_dispatch_works_with_dyn() {
507        let provider: Box<dyn BridgeProvider> = Box::new(FakeProvider { info: fake_info() });
508        assert!(provider.supports_route(1, 10));
509        assert_eq!(provider.info().dapp_id, "cow-sdk://bridging/providers/fake");
510        let networks = provider.get_networks().await.unwrap();
511        assert!(networks.is_empty());
512        let tokens = provider
513            .get_buy_tokens(BuyTokensParams {
514                sell_chain_id: 1,
515                buy_chain_id: 100,
516                sell_token_address: None,
517            })
518            .await
519            .unwrap();
520        assert!(tokens.tokens.is_empty());
521        assert_eq!(tokens.provider_info.name, "fake-provider");
522    }
523
524    fn sample_request() -> QuoteBridgeRequest {
525        QuoteBridgeRequest {
526            sell_chain_id: 1,
527            buy_chain_id: 10,
528            sell_token: Address::ZERO,
529            sell_token_decimals: 18,
530            buy_token: Address::ZERO.into(),
531            buy_token_decimals: 18,
532            sell_amount: U256::from(1u64),
533            account: Address::ZERO,
534            owner: None,
535            receiver: None,
536            bridge_recipient: None,
537            slippage_bps: 50,
538            bridge_slippage_bps: None,
539            kind: cow_types::OrderKind::Sell,
540        }
541    }
542
543    #[tokio::test]
544    async fn fake_provider_get_intermediate_tokens_is_callable() {
545        let provider = FakeProvider { info: fake_info() };
546        let tokens = provider.get_intermediate_tokens(&sample_request()).await.unwrap();
547        assert!(tokens.is_empty());
548    }
549
550    #[tokio::test]
551    async fn fake_provider_get_quote_returns_default_fake_response() {
552        let provider = FakeProvider { info: fake_info() };
553        let response = provider.get_quote(&sample_request()).await.unwrap();
554        assert_eq!(response.provider, "fake");
555        assert_eq!(response.sell_amount, U256::ZERO);
556        assert_eq!(response.buy_amount, U256::ZERO);
557    }
558
559    #[tokio::test]
560    async fn fake_provider_get_bridging_params_returns_none() {
561        let provider = FakeProvider { info: fake_info() };
562        let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
563        let result = provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap();
564        assert!(result.is_none());
565    }
566
567    #[tokio::test]
568    async fn fake_provider_get_status_returns_unknown() {
569        let provider = FakeProvider { info: fake_info() };
570        let result = provider.get_status("deposit", 1).await.unwrap();
571        assert_eq!(result.status, BridgeStatus::Unknown);
572        assert!(result.fill_tx_hash.is_none());
573        assert!(result.deposit_tx_hash.is_none());
574    }
575
576    // ── Type guards ─────────────────────────────────────────────────────
577
578    #[test]
579    fn is_hook_bridge_provider_matches_info_type() {
580        let hook_info = fake_info();
581        let hook_provider = FakeProvider { info: hook_info };
582        assert!(is_hook_bridge_provider(&hook_provider));
583        assert!(!is_receiver_account_bridge_provider(&hook_provider));
584    }
585
586    #[test]
587    fn is_receiver_account_bridge_provider_matches_info_type() {
588        let receiver_info = BridgeProviderInfo {
589            name: "rcv".into(),
590            logo_url: String::new(),
591            dapp_id: "cow-sdk://bridging/providers/rcv".into(),
592            website: String::new(),
593            provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
594        };
595        let provider = FakeProvider { info: receiver_info };
596        assert!(is_receiver_account_bridge_provider(&provider));
597        assert!(!is_hook_bridge_provider(&provider));
598    }
599
600    #[test]
601    fn type_guards_work_through_trait_object() {
602        let hook_provider: Box<dyn BridgeProvider> = Box::new(FakeProvider { info: fake_info() });
603        assert!(is_hook_bridge_provider(&*hook_provider));
604        assert!(!is_receiver_account_bridge_provider(&*hook_provider));
605    }
606
607    // ── HookBridgeProvider default impl ─────────────────────────────────
608
609    struct FakeHookProvider {
610        info: BridgeProviderInfo,
611    }
612
613    impl BridgeProvider for FakeHookProvider {
614        fn info(&self) -> &BridgeProviderInfo {
615            &self.info
616        }
617        fn supports_route(&self, _s: u64, _b: u64) -> bool {
618            true
619        }
620        fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
621            Box::pin(async { Ok(Vec::new()) })
622        }
623        fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
624            let info = self.info.clone();
625            Box::pin(
626                async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
627            )
628        }
629        fn get_intermediate_tokens<'a>(
630            &'a self,
631            _req: &'a QuoteBridgeRequest,
632        ) -> IntermediateTokensFuture<'a> {
633            Box::pin(async { Ok(Vec::new()) })
634        }
635        fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
636            Box::pin(async {
637                Ok(QuoteBridgeResponse {
638                    provider: "hook".into(),
639                    sell_amount: U256::ZERO,
640                    buy_amount: U256::ZERO,
641                    fee_amount: U256::ZERO,
642                    estimated_secs: 0,
643                    bridge_hook: None,
644                })
645            })
646        }
647        fn get_bridging_params<'a>(
648            &'a self,
649            _c: u64,
650            _o: &'a Order,
651            _t: B256,
652            _s: Option<Address>,
653        ) -> BridgingParamsFuture<'a> {
654            Box::pin(async { Ok(None) })
655        }
656        fn get_explorer_url(&self, _id: &str) -> String {
657            String::new()
658        }
659        fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
660            Box::pin(async {
661                Ok(BridgeStatusResult {
662                    status: BridgeStatus::Unknown,
663                    fill_time_in_seconds: None,
664                    deposit_tx_hash: None,
665                    fill_tx_hash: None,
666                })
667            })
668        }
669    }
670
671    impl HookBridgeProvider for FakeHookProvider {
672        fn get_unsigned_bridge_call<'a>(
673            &'a self,
674            _req: &'a QuoteBridgeRequest,
675            _quote: &'a QuoteBridgeResponse,
676        ) -> UnsignedCallFuture<'a> {
677            Box::pin(async {
678                Ok(cow_chains::EvmCall { to: Address::ZERO, data: vec![], value: U256::ZERO })
679            })
680        }
681        fn get_signed_hook<'a>(
682            &'a self,
683            _chain: SupportedChainId,
684            _call: &'a cow_chains::EvmCall,
685            _nonce: &'a str,
686            _deadline: u64,
687            _gas: u64,
688            _signer: &'a PrivateKeySigner,
689        ) -> SignedHookFuture<'a> {
690            Box::pin(async {
691                Ok(crate::types::BridgeHook {
692                    post_hook: cow_types::CowHook {
693                        target: String::new(),
694                        call_data: String::new(),
695                        gas_limit: String::new(),
696                        dapp_id: None,
697                    },
698                    recipient: String::new(),
699                })
700            })
701        }
702    }
703
704    #[tokio::test]
705    async fn hook_provider_default_gas_estimation_deployed() {
706        let provider = FakeHookProvider { info: fake_info() };
707        let gas = provider.get_gas_limit_estimation_for_hook(true, None, None).await.unwrap();
708        // Matches the free-standing helper with proxy_deployed = true.
709        assert_eq!(gas, crate::utils::get_gas_limit_estimation_for_hook(true, None, None));
710    }
711
712    #[tokio::test]
713    async fn hook_provider_default_gas_estimation_needs_proxy_creation() {
714        let provider = FakeHookProvider { info: fake_info() };
715        let gas =
716            provider.get_gas_limit_estimation_for_hook(false, None, Some(10_000)).await.unwrap();
717        assert_eq!(gas, crate::utils::get_gas_limit_estimation_for_hook(false, None, Some(10_000)));
718    }
719
720    #[tokio::test]
721    async fn hook_provider_required_methods_callable_through_trait() {
722        let provider = FakeHookProvider { info: fake_info() };
723        let req = sample_request();
724        let quote = provider.get_quote(&req).await.unwrap();
725        let call = provider.get_unsigned_bridge_call(&req, &quote).await.unwrap();
726        assert_eq!(call.to, Address::ZERO);
727        assert!(call.data.is_empty());
728
729        let signer: PrivateKeySigner =
730            "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap();
731        let hook = provider
732            .get_signed_hook(SupportedChainId::Mainnet, &call, "0", 0, 0, &signer)
733            .await
734            .unwrap();
735        assert!(hook.recipient.is_empty());
736    }
737
738    #[tokio::test]
739    async fn fake_hook_provider_bridge_provider_surface_is_callable() {
740        let provider = FakeHookProvider { info: fake_info() };
741        assert!(provider.supports_route(1, 10));
742        assert_eq!(provider.info().dapp_id, "cow-sdk://bridging/providers/fake");
743        assert!(provider.get_networks().await.unwrap().is_empty());
744        let tokens = provider
745            .get_buy_tokens(BuyTokensParams {
746                sell_chain_id: 1,
747                buy_chain_id: 10,
748                sell_token_address: None,
749            })
750            .await
751            .unwrap();
752        assert!(tokens.tokens.is_empty());
753        assert!(provider.get_intermediate_tokens(&sample_request()).await.unwrap().is_empty());
754        let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
755        assert!(provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap().is_none());
756        assert!(provider.get_explorer_url("x").is_empty());
757        assert_eq!(provider.get_status("x", 1).await.unwrap().status, BridgeStatus::Unknown);
758    }
759
760    // ── ReceiverAccountBridgeProvider ───────────────────────────────────
761
762    struct FakeReceiverProvider {
763        info: BridgeProviderInfo,
764    }
765
766    impl BridgeProvider for FakeReceiverProvider {
767        fn info(&self) -> &BridgeProviderInfo {
768            &self.info
769        }
770        fn supports_route(&self, _s: u64, _b: u64) -> bool {
771            true
772        }
773        fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
774            Box::pin(async { Ok(Vec::new()) })
775        }
776        fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
777            let info = self.info.clone();
778            Box::pin(
779                async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
780            )
781        }
782        fn get_intermediate_tokens<'a>(
783            &'a self,
784            _req: &'a QuoteBridgeRequest,
785        ) -> IntermediateTokensFuture<'a> {
786            Box::pin(async { Ok(Vec::new()) })
787        }
788        fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
789            Box::pin(async {
790                Ok(QuoteBridgeResponse {
791                    provider: "rcv".into(),
792                    sell_amount: U256::ZERO,
793                    buy_amount: U256::ZERO,
794                    fee_amount: U256::ZERO,
795                    estimated_secs: 0,
796                    bridge_hook: None,
797                })
798            })
799        }
800        fn get_bridging_params<'a>(
801            &'a self,
802            _c: u64,
803            _o: &'a Order,
804            _t: B256,
805            _s: Option<Address>,
806        ) -> BridgingParamsFuture<'a> {
807            Box::pin(async { Ok(None) })
808        }
809        fn get_explorer_url(&self, _id: &str) -> String {
810            String::new()
811        }
812        fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
813            Box::pin(async {
814                Ok(BridgeStatusResult {
815                    status: BridgeStatus::Unknown,
816                    fill_time_in_seconds: None,
817                    deposit_tx_hash: None,
818                    fill_tx_hash: None,
819                })
820            })
821        }
822    }
823
824    impl ReceiverAccountBridgeProvider for FakeReceiverProvider {
825        fn get_bridge_receiver_override<'a>(
826            &'a self,
827            _req: &'a QuoteBridgeRequest,
828            _result: &'a QuoteBridgeResponse,
829        ) -> ReceiverOverrideFuture<'a> {
830            Box::pin(async { Ok("near-deposit-address".to_owned()) })
831        }
832    }
833
834    fn fake_receiver_info() -> BridgeProviderInfo {
835        BridgeProviderInfo {
836            name: "rcv".into(),
837            logo_url: String::new(),
838            dapp_id: "cow-sdk://bridging/providers/rcv".into(),
839            website: String::new(),
840            provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
841        }
842    }
843
844    #[tokio::test]
845    async fn receiver_provider_returns_deposit_address() {
846        let provider = FakeReceiverProvider { info: fake_receiver_info() };
847        let req = sample_request();
848        let quote = provider.get_quote(&req).await.unwrap();
849        let addr = provider.get_bridge_receiver_override(&req, &quote).await.unwrap();
850        assert_eq!(addr, "near-deposit-address");
851    }
852
853    #[tokio::test]
854    async fn fake_receiver_provider_bridge_provider_surface_is_callable() {
855        let provider = FakeReceiverProvider { info: fake_receiver_info() };
856        assert!(provider.supports_route(1, 1_000_000_000));
857        assert!(provider.info().is_receiver_account_bridge_provider());
858        assert!(provider.get_networks().await.unwrap().is_empty());
859        let tokens = provider
860            .get_buy_tokens(BuyTokensParams {
861                sell_chain_id: 1,
862                buy_chain_id: 1_000_000_000,
863                sell_token_address: None,
864            })
865            .await
866            .unwrap();
867        assert!(tokens.tokens.is_empty());
868        assert!(provider.get_intermediate_tokens(&sample_request()).await.unwrap().is_empty());
869        let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "bb".repeat(56)));
870        assert!(provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap().is_none());
871        assert!(provider.get_explorer_url("dep").is_empty());
872        assert_eq!(provider.get_status("dep", 1).await.unwrap().status, BridgeStatus::Unknown);
873    }
874}