1use cow_errors::CowError;
4
5use crate::swap_quoter::SwapQuoter;
6
7pub const BUNGEE_API_PATH: &str = "/api/v1/bungee";
11
12pub const BUNGEE_MANUAL_API_PATH: &str = "/api/v1/bungee-manual";
14
15pub const BUNGEE_BASE_URL: &str = "https://public-backend.bungee.exchange";
17
18pub const BUNGEE_API_URL: &str = "https://public-backend.bungee.exchange/api/v1/bungee";
20
21pub const BUNGEE_MANUAL_API_URL: &str =
23 "https://public-backend.bungee.exchange/api/v1/bungee-manual";
24
25pub const BUNGEE_EVENTS_API_URL: &str = "https://microservices.socket.tech/loki";
27
28pub const ACROSS_API_URL: &str = "https://app.across.to/api";
30
31pub const DEFAULT_BRIDGE_SLIPPAGE_BPS: u32 = 50;
33
34pub const DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION: u64 = 240_000;
36
37pub const DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION: u64 = 350_000;
39
40pub const DEFAULT_EXTRA_GAS_PROXY_CREATION: u64 = 400_000;
42
43pub const HOOK_DAPP_BRIDGE_PROVIDER_PREFIX: &str = "cow-sdk://bridging/providers";
45
46pub const BUNGEE_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/bungee";
48
49pub const ACROSS_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/across";
51
52pub const NEAR_INTENTS_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/near-intents";
54
55pub const BUNGEE_API_FALLBACK_TIMEOUT: u64 = 300_000;
57
58use super::{
59 bungee::BungeeProvider,
60 provider::BridgeProvider,
61 types::{BridgeError, QuoteBridgeRequest, QuoteBridgeResponse},
62};
63
64#[derive(Default)]
78pub struct BridgingSdk {
79 providers: Vec<Box<dyn BridgeProvider>>,
80}
81
82impl std::fmt::Debug for BridgingSdk {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("BridgingSdk").field("provider_count", &self.providers.len()).finish()
85 }
86}
87
88impl BridgingSdk {
89 #[must_use]
104 pub fn new() -> Self {
105 Self { providers: vec![] }
106 }
107
108 #[must_use]
121 pub fn with_bungee(mut self, api_key: impl Into<String>) -> Self {
122 self.providers.push(Box::new(BungeeProvider::new(api_key)));
123 self
124 }
125
126 pub fn add_provider(&mut self, provider: impl BridgeProvider + 'static) {
133 self.providers.push(Box::new(provider));
134 }
135
136 #[must_use]
143 pub fn provider_count(&self) -> usize {
144 self.providers.len()
145 }
146
147 pub async fn get_best_quote(
156 &self,
157 req: &QuoteBridgeRequest,
158 ) -> Result<QuoteBridgeResponse, BridgeError> {
159 let eligible: Vec<&dyn BridgeProvider> = self
160 .providers
161 .iter()
162 .filter(|p| p.supports_route(req.sell_chain_id, req.buy_chain_id))
163 .map(|p| p.as_ref())
164 .collect();
165
166 if eligible.is_empty() {
167 return Err(BridgeError::NoProviders);
168 }
169
170 let futures: Vec<_> = eligible.iter().map(|p| p.get_quote(req)).collect();
171 let results = futures::future::join_all(futures).await;
172
173 let best = results
174 .into_iter()
175 .filter_map(|r| r.ok())
176 .max_by_key(QuoteBridgeResponse::net_buy_amount);
177
178 best.ok_or(BridgeError::NoQuote)
179 }
180
181 pub async fn get_all_quotes(
191 &self,
192 req: &QuoteBridgeRequest,
193 ) -> Vec<Result<QuoteBridgeResponse, CowError>> {
194 let eligible: Vec<&dyn BridgeProvider> = self
195 .providers
196 .iter()
197 .filter(|p| p.supports_route(req.sell_chain_id, req.buy_chain_id))
198 .map(|p| p.as_ref())
199 .collect();
200
201 let futures: Vec<_> = eligible.iter().map(|p| p.get_quote(req)).collect();
202 futures::future::join_all(futures).await
203 }
204}
205
206use super::types::BridgeQuoteResults;
209
210#[derive(Debug, Clone)]
216pub struct BridgeQuoteAndPost {
217 pub swap: QuoteBridgeResponse,
219 pub bridge: BridgeQuoteResults,
221}
222
223#[derive(Debug, Clone)]
228pub struct QuoteAndPost {
229 pub quote: QuoteBridgeResponse,
231}
232
233#[derive(Debug, Clone)]
237pub enum CrossChainQuoteAndPost {
238 SameChain(Box<QuoteAndPost>),
240 CrossChain(Box<BridgeQuoteAndPost>),
242}
243
244#[must_use]
251pub const fn is_bridge_quote_and_post(result: &CrossChainQuoteAndPost) -> bool {
252 matches!(result, CrossChainQuoteAndPost::CrossChain(_))
253}
254
255#[must_use]
259pub const fn is_quote_and_post(result: &CrossChainQuoteAndPost) -> bool {
260 matches!(result, CrossChainQuoteAndPost::SameChain(_))
261}
262
263pub fn assert_is_bridge_quote_and_post(
270 result: &CrossChainQuoteAndPost,
271) -> Result<&BridgeQuoteAndPost, BridgeError> {
272 match result {
273 CrossChainQuoteAndPost::CrossChain(bqp) => Ok(bqp.as_ref()),
274 CrossChainQuoteAndPost::SameChain(_) => {
275 Err(BridgeError::QuoteError("expected BridgeQuoteAndPost, got QuoteAndPost".to_owned()))
276 }
277 }
278}
279
280pub fn assert_is_quote_and_post(
287 result: &CrossChainQuoteAndPost,
288) -> Result<&QuoteAndPost, BridgeError> {
289 match result {
290 CrossChainQuoteAndPost::SameChain(qp) => Ok(qp.as_ref()),
291 CrossChainQuoteAndPost::CrossChain(_) => {
292 Err(BridgeError::QuoteError("expected QuoteAndPost, got BridgeQuoteAndPost".to_owned()))
293 }
294 }
295}
296
297use crate::{
300 across::{EvmLogEntry, get_deposit_params},
301 types::{BridgeHook, BridgeQuoteResult, BridgeStatus, BridgeStatusResult, CrossChainOrder},
302};
303use alloy_primitives::Address;
304
305#[derive(Debug)]
307pub struct GetCrossChainOrderParams<'a> {
308 pub chain_id: u64,
310 pub order_id: String,
312 pub full_app_data: Option<String>,
314 pub trade_tx_hash: String,
316 pub logs: &'a [EvmLogEntry],
318 pub settlement_override: Option<Address>,
320}
321
322pub fn get_cross_chain_order(
336 params: &GetCrossChainOrderParams<'_>,
337) -> Result<CrossChainOrder, BridgeError> {
338 let bridging_params = get_deposit_params(
339 params.chain_id,
340 ¶ms.order_id,
341 params.logs,
342 params.settlement_override,
343 )
344 .ok_or_else(|| {
345 BridgeError::QuoteError(format!(
346 "bridging params cannot be derived from transaction: {}",
347 params.trade_tx_hash
348 ))
349 })?;
350
351 Ok(CrossChainOrder {
352 chain_id: params.chain_id,
353 status_result: BridgeStatusResult::new(BridgeStatus::Unknown),
354 bridging_params,
355 trade_tx_hash: params.trade_tx_hash.clone(),
356 explorer_url: None,
357 })
358}
359
360#[derive(Debug)]
366pub struct GetBridgeSignedHookContext<'a> {
367 pub signer: &'a alloy_signer_local::PrivateKeySigner,
369 pub hook_gas_limit: u64,
372 pub chain_id: cow_chains::SupportedChainId,
375 pub deadline: u64,
378}
379
380#[derive(Debug, Clone)]
386pub struct GetBridgeSignedHookOutput {
387 pub hook: BridgeHook,
390 pub unsigned_bridge_call: cow_chains::EvmCall,
392 pub bridging_quote: QuoteBridgeResponse,
394}
395
396pub async fn get_bridge_signed_hook<P: crate::provider::HookBridgeProvider + ?Sized>(
416 hook_provider: &P,
417 bridge_request: &QuoteBridgeRequest,
418 context: GetBridgeSignedHookContext<'_>,
419) -> Result<GetBridgeSignedHookOutput, BridgeError> {
420 let bridging_quote = hook_provider
422 .get_quote(bridge_request)
423 .await
424 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
425
426 let unsigned_bridge_call = hook_provider
428 .get_unsigned_bridge_call(bridge_request, &bridging_quote)
429 .await
430 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
431
432 let nonce_hex = derive_hook_nonce(&unsigned_bridge_call.data, context.deadline);
434
435 let hook = hook_provider
437 .get_signed_hook(
438 context.chain_id,
439 &unsigned_bridge_call,
440 &nonce_hex,
441 context.deadline,
442 context.hook_gas_limit,
443 context.signer,
444 )
445 .await
446 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
447
448 Ok(GetBridgeSignedHookOutput { hook, unsigned_bridge_call, bridging_quote })
449}
450
451fn derive_hook_nonce(data: &[u8], deadline: u64) -> String {
464 let deadline_be: [u8; 32] = alloy_primitives::U256::from(deadline).to_be_bytes();
465 let mut packed = Vec::with_capacity(data.len() + 32);
466 packed.extend_from_slice(data);
467 packed.extend_from_slice(&deadline_be);
468 let hash = alloy_primitives::keccak256(&packed);
469 format!("{hash:#x}")
470}
471
472#[derive(Clone)]
474pub struct GetQuoteWithBridgeParams {
475 pub swap_and_bridge_request: QuoteBridgeRequest,
477 pub slippage_bps: u32,
479 pub advanced_settings_metadata: Option<serde_json::Value>,
485 pub quote_signer: Option<std::sync::Arc<alloy_signer_local::PrivateKeySigner>>,
495 pub hook_deadline: Option<u64>,
500}
501
502impl std::fmt::Debug for GetQuoteWithBridgeParams {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 f.debug_struct("GetQuoteWithBridgeParams")
505 .field("swap_and_bridge_request", &self.swap_and_bridge_request)
506 .field("slippage_bps", &self.slippage_bps)
507 .field("advanced_settings_metadata", &self.advanced_settings_metadata)
508 .field("quote_signer", &self.quote_signer.is_some())
509 .field("hook_deadline", &self.hook_deadline)
510 .finish()
511 }
512}
513
514pub async fn get_quote_with_bridge(
536 params: &GetQuoteWithBridgeParams,
537 provider: &dyn BridgeProvider,
538 quoter: &dyn SwapQuoter,
539) -> Result<BridgeQuoteAndPost, BridgeError> {
540 if params.swap_and_bridge_request.kind != cow_types::OrderKind::Sell {
541 return Err(BridgeError::OnlySellOrderSupported);
542 }
543
544 if let Some(hook_provider) = provider.as_hook_bridge_provider() {
545 return get_quote_with_hook_bridge(hook_provider, params, quoter).await;
546 }
547
548 if let Some(receiver_provider) = provider.as_receiver_account_bridge_provider() {
549 return get_quote_with_receiver_account_bridge(receiver_provider, params, quoter).await;
550 }
551
552 Err(BridgeError::TxBuildError(format!(
553 "provider {name} implements neither HookBridgeProvider nor ReceiverAccountBridgeProvider",
554 name = provider.info().name,
555 )))
556}
557
558pub async fn get_quote_without_bridge(
569 request: &QuoteBridgeRequest,
570 quoter: &dyn SwapQuoter,
571) -> Result<QuoteAndPost, BridgeError> {
572 let buy_token_evm = request.buy_token.to_evm().ok_or_else(|| {
573 BridgeError::TxBuildError(
574 "same-chain swaps require an EVM buy_token; got TokenAddress::Raw".into(),
575 )
576 })?;
577 let params = crate::swap_quoter::SwapQuoteParams {
578 owner: request.account,
579 chain_id: request.sell_chain_id,
580 sell_token: request.sell_token,
581 sell_token_decimals: request.sell_token_decimals,
582 buy_token: buy_token_evm,
583 buy_token_decimals: request.buy_token_decimals,
584 amount: request.sell_amount,
585 kind: request.kind,
586 slippage_bps: request.slippage_bps,
587 app_data_json: None,
588 };
589 let outcome =
590 quoter.quote_swap(params).await.map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
591
592 Ok(QuoteAndPost {
593 quote: QuoteBridgeResponse {
594 provider: "same-chain".to_owned(),
595 sell_amount: outcome.sell_amount,
596 buy_amount: outcome.buy_amount_after_slippage,
597 fee_amount: outcome.fee_amount,
598 estimated_secs: 0,
599 bridge_hook: None,
600 },
601 })
602}
603
604pub async fn get_swap_quote(
614 request: &QuoteBridgeRequest,
615 quoter: &dyn SwapQuoter,
616) -> Result<QuoteBridgeResponse, BridgeError> {
617 let buy_token_evm = request.buy_token.to_evm().ok_or_else(|| {
618 BridgeError::TxBuildError("intermediate swap quote requires an EVM buy_token".into())
619 })?;
620 let params = crate::swap_quoter::SwapQuoteParams {
621 owner: request.account,
622 chain_id: request.sell_chain_id,
623 sell_token: request.sell_token,
624 sell_token_decimals: request.sell_token_decimals,
625 buy_token: buy_token_evm,
626 buy_token_decimals: request.buy_token_decimals,
627 amount: request.sell_amount,
628 kind: request.kind,
629 slippage_bps: request.slippage_bps,
630 app_data_json: None,
631 };
632 let outcome =
633 quoter.quote_swap(params).await.map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
634
635 Ok(QuoteBridgeResponse {
636 provider: "swap".to_owned(),
637 sell_amount: outcome.sell_amount,
638 buy_amount: outcome.buy_amount_after_slippage,
639 fee_amount: outcome.fee_amount,
640 estimated_secs: 0,
641 bridge_hook: None,
642 })
643}
644
645pub async fn get_intermediate_swap_result(
672 request: &QuoteBridgeRequest,
673 provider: &dyn crate::provider::BridgeProvider,
674 quoter: &dyn SwapQuoter,
675 advanced_settings_metadata: Option<&serde_json::Value>,
676) -> Result<QuoteBridgeResponse, BridgeError> {
677 use crate::utils::determine_intermediate_token;
678
679 let candidates = provider
681 .get_intermediate_tokens(request)
682 .await
683 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
684
685 if candidates.is_empty() {
686 return Err(BridgeError::NoIntermediateTokens);
687 }
688
689 let candidate_addrs: Vec<alloy_primitives::Address> =
692 candidates.iter().filter_map(|t| t.address.to_evm()).collect();
693 let intermediate = determine_intermediate_token(
694 request.sell_chain_id,
695 request.sell_token,
696 &candidate_addrs,
697 &foldhash::HashSet::default(),
698 false,
699 )?;
700 let intermediate_info = candidates
701 .iter()
702 .find(|t| t.address == intermediate)
703 .cloned()
704 .ok_or_else(intermediate_not_in_candidates_err)?;
705 let intermediate_evm =
706 intermediate_info.address.to_evm().ok_or_else(intermediate_must_be_evm_err)?;
707
708 let app_data_json = build_intermediate_app_data_json(advanced_settings_metadata, provider);
710
711 let params = crate::swap_quoter::SwapQuoteParams {
713 owner: request.account,
714 chain_id: request.sell_chain_id,
715 sell_token: request.sell_token,
716 sell_token_decimals: request.sell_token_decimals,
717 buy_token: intermediate_evm,
718 buy_token_decimals: intermediate_info.decimals,
719 amount: request.sell_amount,
720 kind: request.kind,
721 slippage_bps: request.slippage_bps,
722 app_data_json: Some(app_data_json),
723 };
724 let outcome =
725 quoter.quote_swap(params).await.map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
726
727 Ok(QuoteBridgeResponse {
729 provider: provider.info().name.clone(),
730 sell_amount: outcome.sell_amount,
731 buy_amount: outcome.buy_amount_after_slippage,
732 fee_amount: outcome.fee_amount,
733 estimated_secs: 0,
734 bridge_hook: None,
735 })
736}
737
738fn intermediate_not_in_candidates_err() -> BridgeError {
761 BridgeError::TxBuildError("intermediate token not in candidates".into())
762}
763
764fn intermediate_must_be_evm_err() -> BridgeError {
765 BridgeError::TxBuildError("intermediate token must be EVM on the source chain".into())
766}
767
768fn build_intermediate_app_data_json(
769 caller_metadata: Option<&serde_json::Value>,
770 provider: &dyn crate::provider::BridgeProvider,
771) -> String {
772 let mut metadata = caller_metadata.and_then(|v| v.as_object().cloned()).unwrap_or_default();
773
774 metadata.insert(
777 "bridging".to_owned(),
778 serde_json::json!({ "providerId": provider.info().dapp_id }),
779 );
780 if !metadata.contains_key("hooks") {
784 metadata.insert("hooks".to_owned(), serde_json::json!({ "post": [] }));
785 }
786
787 let doc = serde_json::json!({
788 "version": "1.4.0",
789 "appCode": "CoW Bridging",
790 "metadata": metadata,
791 });
792 serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_owned())
793}
794
795#[cfg(feature = "native")]
814pub async fn create_bridge_request_timeout(timeout_ms: u64, prefix: &str) -> BridgeError {
815 tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)).await;
816 BridgeError::ApiError(format!("{prefix} timeout after {timeout_ms}ms"))
817}
818
819#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum QuoteStrategy {
827 Single,
829 Multi,
831 Best,
833}
834
835impl QuoteStrategy {
836 #[must_use]
851 pub const fn name(self) -> &'static str {
852 match self {
853 Self::Single => "SingleQuoteStrategy",
854 Self::Multi => "MultiQuoteStrategy",
855 Self::Best => "BestQuoteStrategy",
856 }
857 }
858}
859
860#[must_use]
869pub const fn create_strategies() -> [QuoteStrategy; 3] {
870 [QuoteStrategy::Single, QuoteStrategy::Multi, QuoteStrategy::Best]
871}
872
873use super::types::MultiQuoteResult;
876
877pub const DEFAULT_TOTAL_TIMEOUT_MS: u64 = 40_000;
879
880pub const DEFAULT_PROVIDER_TIMEOUT_MS: u64 = 20_000;
882
883#[cfg(feature = "native")]
896pub async fn execute_provider_quotes(
897 sdk: &BridgingSdk,
898 request: &QuoteBridgeRequest,
899 timeout_ms: u64,
900) -> Vec<MultiQuoteResult> {
901 use futures::future::join_all;
902
903 let futs: Vec<_> = sdk
904 .providers
905 .iter()
906 .map(|p| {
907 let name = p.name().to_owned();
908 async move {
909 let result = p.get_quote(request).await;
910 match result {
911 Ok(quote) => MultiQuoteResult {
912 provider_dapp_id: name,
913 quote: Some(crate::types::BridgeQuoteAmountsAndCosts {
914 before_fee: crate::types::BridgeAmounts {
915 sell_amount: quote.sell_amount,
916 buy_amount: quote.buy_amount,
917 },
918 after_fee: crate::types::BridgeAmounts {
919 sell_amount: quote.sell_amount,
920 buy_amount: quote.buy_amount.saturating_sub(quote.fee_amount),
921 },
922 after_slippage: crate::types::BridgeAmounts {
923 sell_amount: quote.sell_amount,
924 buy_amount: quote.buy_amount.saturating_sub(quote.fee_amount),
925 },
926 costs: crate::types::BridgeCosts {
927 bridging_fee: crate::types::BridgingFee {
928 fee_bps: 0,
929 amount_in_sell_currency: quote.fee_amount,
930 amount_in_buy_currency: quote.fee_amount,
931 },
932 },
933 slippage_bps: request.slippage_bps,
934 }),
935 error: None,
936 },
937 Err(e) => MultiQuoteResult {
938 provider_dapp_id: name,
939 quote: None,
940 error: Some(e.to_string()),
941 },
942 }
943 }
944 })
945 .collect();
946
947 let fetched_results =
949 tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), join_all(futs)).await;
950
951 match fetched_results {
952 Ok(results) => results,
953 Err(_timeout) => {
954 sdk.providers
956 .iter()
957 .map(|p| MultiQuoteResult {
958 provider_dapp_id: p.name().to_owned(),
959 quote: None,
960 error: Some(format!("Multi-quote timeout after {timeout_ms}ms")),
961 })
962 .collect()
963 }
964 }
965}
966
967#[cfg(feature = "native")]
979pub async fn fetch_multi_quote(
980 sdk: &BridgingSdk,
981 request: &QuoteBridgeRequest,
982 timeout_ms: Option<u64>,
983) -> Vec<MultiQuoteResult> {
984 let timeout = timeout_ms.map_or(DEFAULT_TOTAL_TIMEOUT_MS, |v| v);
985 let mut results = execute_provider_quotes(sdk, request, timeout).await;
986
987 let dapp_ids: Vec<String> = sdk.providers.iter().map(|p| p.name().to_owned()).collect();
989 crate::utils::fill_timeout_results(&mut results, &dapp_ids);
990
991 results.sort_by(|a, b| {
993 let a_amount =
994 a.quote.as_ref().map_or(alloy_primitives::U256::ZERO, |q| q.after_slippage.buy_amount);
995 let b_amount =
996 b.quote.as_ref().map_or(alloy_primitives::U256::ZERO, |q| q.after_slippage.buy_amount);
997 b_amount.cmp(&a_amount)
998 });
999
1000 results
1001}
1002
1003#[must_use]
1018pub fn get_cache_key(request: &QuoteBridgeRequest) -> String {
1019 let buy_token = match &request.buy_token {
1020 crate::types::TokenAddress::Evm(addr) => format!("{addr:#x}"),
1021 crate::types::TokenAddress::Raw(s) => format!("raw:{s}"),
1022 };
1023 format!(
1024 "{}-{}-{:#x}-{}",
1025 request.sell_chain_id, request.buy_chain_id, request.sell_token, buy_token,
1026 )
1027}
1028
1029pub fn safe_call_best_quote_callback<F: FnOnce(&MultiQuoteResult)>(
1043 callback: Option<F>,
1044 result: &MultiQuoteResult,
1045) {
1046 if let Some(cb) = callback {
1047 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1048 cb(result);
1049 }));
1050 if let Err(e) = outcome {
1051 tracing::warn!("Error in best-quote callback: {:?}", e);
1052 }
1053 }
1054}
1055
1056pub fn safe_call_progressive_callback<F: FnOnce(&MultiQuoteResult)>(
1068 callback: Option<F>,
1069 result: &MultiQuoteResult,
1070) {
1071 if let Some(cb) = callback {
1072 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1073 cb(result);
1074 }));
1075 if let Err(e) = outcome {
1076 tracing::warn!("Error in progressive-quote callback: {:?}", e);
1077 }
1078 }
1079}
1080
1081pub async fn get_quote_with_hook_bridge(
1108 hook_provider: &dyn crate::provider::HookBridgeProvider,
1109 params: &GetQuoteWithBridgeParams,
1110 quoter: &dyn SwapQuoter,
1111) -> Result<BridgeQuoteAndPost, BridgeError> {
1112 let hook_gas_limit = hook_provider
1115 .get_gas_limit_estimation_for_hook(
1116 true,
1117 Some(DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION),
1118 Some(DEFAULT_EXTRA_GAS_PROXY_CREATION),
1119 )
1120 .await
1121 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
1122
1123 let swap = get_intermediate_swap_result(
1125 ¶ms.swap_and_bridge_request,
1126 hook_provider,
1127 quoter,
1128 params.advanced_settings_metadata.as_ref(),
1129 )
1130 .await?;
1131
1132 let (unsigned_bridge_call, bridge_response, pre_authorized_bridging_hook) =
1135 if let Some(signer) = ¶ms.quote_signer {
1136 let chain_id = cow_chains::SupportedChainId::try_from(
1137 params.swap_and_bridge_request.sell_chain_id,
1138 )
1139 .map_err(|e| {
1140 BridgeError::TxBuildError(format!(
1141 "unsupported sell_chain_id {} for hook signing: {e}",
1142 params.swap_and_bridge_request.sell_chain_id,
1143 ))
1144 })?;
1145 let deadline = params.hook_deadline.unwrap_or_else(|| u64::from(u32::MAX));
1146 let ctx = GetBridgeSignedHookContext {
1147 signer: signer.as_ref(),
1148 hook_gas_limit,
1149 chain_id,
1150 deadline,
1151 };
1152 let out =
1153 get_bridge_signed_hook(hook_provider, ¶ms.swap_and_bridge_request, ctx).await?;
1154 (out.unsigned_bridge_call, out.bridging_quote, out.hook)
1155 } else {
1156 let bridge_response = hook_provider
1157 .get_quote(¶ms.swap_and_bridge_request)
1158 .await
1159 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
1160 let unsigned_call = hook_provider
1161 .get_unsigned_bridge_call(¶ms.swap_and_bridge_request, &bridge_response)
1162 .await
1163 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
1164 let mock_post_hook = crate::utils::hook_mock_for_cost_estimation(hook_gas_limit);
1165 let hook = BridgeHook {
1166 post_hook: mock_post_hook,
1167 recipient: format!("{:#x}", params.swap_and_bridge_request.account),
1168 };
1169 (unsigned_call, bridge_response, hook)
1170 };
1171
1172 let quote = minimal_bridge_quote_result(¶ms.swap_and_bridge_request, &bridge_response);
1174
1175 Ok(BridgeQuoteAndPost {
1176 swap,
1177 bridge: crate::types::BridgeQuoteResults {
1178 provider_info: hook_provider.info().clone(),
1179 quote,
1180 bridge_call_details: Some(crate::types::BridgeCallDetails {
1181 unsigned_bridge_call,
1182 pre_authorized_bridging_hook,
1183 }),
1184 bridge_receiver_override: None,
1185 },
1186 })
1187}
1188
1189pub async fn get_quote_with_receiver_account_bridge(
1208 receiver_provider: &dyn crate::provider::ReceiverAccountBridgeProvider,
1209 params: &GetQuoteWithBridgeParams,
1210 quoter: &dyn SwapQuoter,
1211) -> Result<BridgeQuoteAndPost, BridgeError> {
1212 let swap = get_intermediate_swap_result(
1214 ¶ms.swap_and_bridge_request,
1215 receiver_provider,
1216 quoter,
1217 params.advanced_settings_metadata.as_ref(),
1218 )
1219 .await?;
1220
1221 let bridge_response = receiver_provider
1223 .get_quote(¶ms.swap_and_bridge_request)
1224 .await
1225 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
1226
1227 let receiver_override = receiver_provider
1229 .get_bridge_receiver_override(¶ms.swap_and_bridge_request, &bridge_response)
1230 .await
1231 .map_err(|e| BridgeError::TxBuildError(e.to_string()))?;
1232
1233 let quote = minimal_bridge_quote_result(¶ms.swap_and_bridge_request, &bridge_response);
1234
1235 Ok(BridgeQuoteAndPost {
1236 swap,
1237 bridge: crate::types::BridgeQuoteResults {
1238 provider_info: receiver_provider.info().clone(),
1239 quote,
1240 bridge_call_details: None,
1241 bridge_receiver_override: Some(receiver_override),
1242 },
1243 })
1244}
1245
1246fn minimal_bridge_quote_result(
1260 request: &QuoteBridgeRequest,
1261 response: &QuoteBridgeResponse,
1262) -> crate::types::BridgeQuoteResult {
1263 use crate::types::{
1264 BridgeAmounts, BridgeCosts, BridgeFees, BridgeLimits, BridgeQuoteAmountsAndCosts,
1265 BridgingFee,
1266 };
1267
1268 let fee = response.fee_amount;
1269 let before_fee_buy = response.buy_amount.saturating_add(fee);
1270
1271 BridgeQuoteResult {
1272 id: None,
1273 signature: None,
1274 attestation_signature: None,
1275 quote_body: None,
1276 is_sell: request.kind == cow_types::OrderKind::Sell,
1277 amounts_and_costs: BridgeQuoteAmountsAndCosts {
1278 before_fee: BridgeAmounts {
1279 sell_amount: response.sell_amount,
1280 buy_amount: before_fee_buy,
1281 },
1282 after_fee: BridgeAmounts {
1283 sell_amount: response.sell_amount,
1284 buy_amount: response.buy_amount,
1285 },
1286 after_slippage: BridgeAmounts {
1287 sell_amount: response.sell_amount,
1288 buy_amount: response.buy_amount,
1289 },
1290 costs: BridgeCosts {
1291 bridging_fee: BridgingFee {
1292 fee_bps: 0,
1293 amount_in_sell_currency: fee,
1294 amount_in_buy_currency: fee,
1295 },
1296 },
1297 slippage_bps: request.slippage_bps,
1298 },
1299 expected_fill_time_seconds: Some(response.estimated_secs),
1300 quote_timestamp: 0,
1301 fees: BridgeFees { bridge_fee: fee, destination_gas_fee: alloy_primitives::U256::ZERO },
1302 limits: BridgeLimits {
1303 min_deposit: alloy_primitives::U256::ZERO,
1304 max_deposit: alloy_primitives::U256::MAX,
1305 },
1306 }
1307}
1308
1309#[cfg(test)]
1312pub mod test_helpers {
1313 use alloy_primitives::Address;
1320 use alloy_signer_local::PrivateKeySigner;
1321
1322 pub const TEST_PRIVATE_KEY: &str =
1326 "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
1327
1328 #[must_use]
1332 pub fn get_pk() -> &'static str {
1333 TEST_PRIVATE_KEY
1334 }
1335
1336 #[must_use]
1340 pub fn get_mock_signer() -> PrivateKeySigner {
1341 TEST_PRIVATE_KEY.parse::<PrivateKeySigner>().expect("valid test key")
1342 }
1343
1344 #[must_use]
1346 pub fn get_wallet() -> PrivateKeySigner {
1347 get_mock_signer()
1348 }
1349
1350 #[must_use]
1355 pub fn get_rpc_provider() -> &'static str {
1356 "https://eth.llamarpc.com"
1357 }
1358
1359 pub fn expect_to_equal<T: serde::Serialize>(actual: &T, expected: &T) {
1368 let actual_json = serde_json::to_string_pretty(actual).expect("failed to serialise actual");
1369 let expected_json =
1370 serde_json::to_string_pretty(expected).expect("failed to serialise expected");
1371 assert_eq!(actual_json, expected_json);
1372 }
1373
1374 #[must_use]
1376 pub fn test_address() -> Address {
1377 get_mock_signer().address()
1378 }
1379
1380 #[cfg(test)]
1381 mod tests {
1382 use super::*;
1383
1384 #[test]
1385 fn mock_signer_has_expected_address() {
1386 let signer = get_mock_signer();
1387 let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse().unwrap();
1389 assert_eq!(signer.address(), expected);
1390 }
1391
1392 #[test]
1393 fn expect_to_equal_passes_for_equal_values() {
1394 expect_to_equal(&42u64, &42u64);
1395 }
1396
1397 #[test]
1398 #[should_panic]
1399 fn expect_to_equal_panics_for_different_values() {
1400 expect_to_equal(&42u64, &43u64);
1401 }
1402
1403 #[test]
1404 fn get_pk_returns_key() {
1405 assert_eq!(get_pk().len(), 64);
1406 }
1407
1408 #[test]
1409 fn get_rpc_provider_returns_url() {
1410 assert!(get_rpc_provider().starts_with("https://"));
1411 }
1412 }
1413}
1414
1415#[cfg(test)]
1416#[allow(clippy::tests_outside_test_module, reason = "inner module pattern")]
1417mod intermediate_swap_tests {
1418 use alloy_primitives::{B256, U256};
1419 use cow_types::OrderKind;
1420
1421 use super::*;
1422 use crate::{
1423 provider::{
1424 BridgeNetworkInfo, BridgeStatusFuture, BridgingParamsFuture, BuyTokensFuture,
1425 IntermediateTokensFuture, NetworksFuture, QuoteFuture,
1426 },
1427 swap_quoter::{QuoteSwapFuture, SwapQuoteOutcome, SwapQuoteParams},
1428 types::{
1429 BridgeProviderInfo, BridgeProviderType, BuyTokensParams, GetProviderBuyTokens,
1430 IntermediateTokenInfo,
1431 },
1432 };
1433
1434 fn dummy_info(name: &str) -> BridgeProviderInfo {
1435 BridgeProviderInfo {
1436 name: name.to_owned(),
1437 logo_url: String::new(),
1438 dapp_id: format!("cow-sdk://bridging/providers/{name}"),
1439 website: String::new(),
1440 provider_type: BridgeProviderType::HookBridgeProvider,
1441 }
1442 }
1443
1444 struct FixedProvider {
1445 info: BridgeProviderInfo,
1446 tokens: Vec<IntermediateTokenInfo>,
1447 }
1448
1449 impl BridgeProvider for FixedProvider {
1450 fn info(&self) -> &BridgeProviderInfo {
1451 &self.info
1452 }
1453 fn supports_route(&self, _s: u64, _b: u64) -> bool {
1454 true
1455 }
1456 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
1457 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
1458 }
1459 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
1460 let info = self.info.clone();
1461 Box::pin(
1462 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
1463 )
1464 }
1465 fn get_intermediate_tokens<'a>(
1466 &'a self,
1467 _req: &'a QuoteBridgeRequest,
1468 ) -> IntermediateTokensFuture<'a> {
1469 let tokens = self.tokens.clone();
1470 Box::pin(async move { Ok(tokens) })
1471 }
1472 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
1473 Box::pin(async {
1474 Ok(QuoteBridgeResponse {
1475 provider: "fixed".into(),
1476 sell_amount: U256::ZERO,
1477 buy_amount: U256::ZERO,
1478 fee_amount: U256::ZERO,
1479 estimated_secs: 0,
1480 bridge_hook: None,
1481 })
1482 })
1483 }
1484 fn get_bridging_params<'a>(
1485 &'a self,
1486 _c: u64,
1487 _o: &'a cow_orderbook::types::Order,
1488 _t: B256,
1489 _s: Option<Address>,
1490 ) -> BridgingParamsFuture<'a> {
1491 Box::pin(async { Ok(None) })
1492 }
1493 fn get_explorer_url(&self, _id: &str) -> String {
1494 String::new()
1495 }
1496 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
1497 Box::pin(async {
1498 Ok(BridgeStatusResult {
1499 status: BridgeStatus::Unknown,
1500 fill_time_in_seconds: None,
1501 deposit_tx_hash: None,
1502 fill_tx_hash: None,
1503 })
1504 })
1505 }
1506 }
1507
1508 struct CapturingQuoter {
1509 captured: std::sync::OnceLock<SwapQuoteParams>,
1510 outcome: SwapQuoteOutcome,
1511 }
1512
1513 impl SwapQuoter for CapturingQuoter {
1514 fn quote_swap<'a>(&'a self, params: SwapQuoteParams) -> QuoteSwapFuture<'a> {
1515 self.captured.set(params).ok();
1516 let outcome = self.outcome.clone();
1517 Box::pin(async move { Ok(outcome) })
1518 }
1519 }
1520
1521 fn usdc_token() -> IntermediateTokenInfo {
1522 IntermediateTokenInfo {
1523 chain_id: 1,
1524 address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
1525 .parse::<Address>()
1526 .unwrap()
1527 .into(),
1528 decimals: 6,
1529 symbol: "USDC".into(),
1530 name: "USD Coin".into(),
1531 logo_url: None,
1532 }
1533 }
1534
1535 fn sample_request() -> QuoteBridgeRequest {
1536 QuoteBridgeRequest {
1537 sell_chain_id: 1,
1538 buy_chain_id: 42_161,
1539 sell_token: Address::repeat_byte(0x11),
1540 sell_token_decimals: 18,
1541 buy_token: Address::repeat_byte(0x22).into(),
1542 buy_token_decimals: 6,
1543 sell_amount: U256::from(1_000_000u64),
1544 account: Address::repeat_byte(0x33),
1545 owner: None,
1546 receiver: None,
1547 bridge_recipient: None,
1548 slippage_bps: 50,
1549 bridge_slippage_bps: None,
1550 kind: OrderKind::Sell,
1551 }
1552 }
1553
1554 fn default_outcome() -> SwapQuoteOutcome {
1555 SwapQuoteOutcome {
1556 sell_amount: U256::from(1_000_000u64),
1557 buy_amount_after_slippage: U256::from(999_500u64),
1558 fee_amount: U256::from(500u64),
1559 valid_to: 9_999_999,
1560 app_data_hex: "0xabc".into(),
1561 full_app_data: "{\"version\":\"1.4.0\"}".into(),
1562 }
1563 }
1564
1565 #[tokio::test]
1566 async fn errors_when_provider_has_no_candidates() {
1567 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![] };
1568 let quoter =
1569 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1570 let err = get_intermediate_swap_result(&sample_request(), &provider, "er, None)
1571 .await
1572 .unwrap_err();
1573 assert!(matches!(err, BridgeError::NoIntermediateTokens));
1574 }
1575
1576 #[tokio::test]
1577 async fn picks_first_candidate_and_returns_wrapped_outcome() {
1578 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![usdc_token()] };
1579 let quoter =
1580 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1581 let resp = get_intermediate_swap_result(&sample_request(), &provider, "er, None)
1582 .await
1583 .unwrap();
1584 assert_eq!(resp.provider, "p");
1585 assert_eq!(resp.buy_amount, U256::from(999_500u64));
1586 assert_eq!(resp.fee_amount, U256::from(500u64));
1587 }
1588
1589 #[tokio::test]
1590 async fn threads_intermediate_token_to_quoter() {
1591 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![usdc_token()] };
1592 let quoter =
1593 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1594 get_intermediate_swap_result(&sample_request(), &provider, "er, None).await.unwrap();
1595 let captured = quoter.captured.get().cloned().expect("quoter called");
1596 assert_eq!(captured.buy_token, usdc_token().address);
1597 assert_eq!(captured.buy_token_decimals, 6);
1598 assert_eq!(captured.chain_id, 1);
1599 }
1600
1601 #[tokio::test]
1604 async fn fix_852_preserves_caller_metadata() {
1605 let provider = FixedProvider { info: dummy_info("cow-prov"), tokens: vec![usdc_token()] };
1606 let quoter =
1607 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1608 let caller_meta = serde_json::json!({
1609 "partnerFee": { "bps": 25, "recipient": "0xpartner" },
1610 "utm": { "utmSource": "cow-widget" },
1611 "orderClass": { "orderClass": "market" }
1612 });
1613
1614 get_intermediate_swap_result(&sample_request(), &provider, "er, Some(&caller_meta))
1615 .await
1616 .unwrap();
1617
1618 let captured = quoter.captured.get().cloned().expect("quoter called");
1619 let app_data_json = captured.app_data_json.expect("app_data threaded through");
1620 let parsed: serde_json::Value = serde_json::from_str(&app_data_json).unwrap();
1621 let metadata = parsed.get("metadata").expect("metadata key present");
1622
1623 assert_eq!(
1625 metadata.get("partnerFee").and_then(|v| v.get("bps")).and_then(|v| v.as_u64()),
1626 Some(25)
1627 );
1628 assert_eq!(
1629 metadata.get("utm").and_then(|v| v.get("utmSource")).and_then(|v| v.as_str()),
1630 Some("cow-widget")
1631 );
1632 assert_eq!(
1633 metadata.get("orderClass").and_then(|v| v.get("orderClass")).and_then(|v| v.as_str()),
1634 Some("market")
1635 );
1636
1637 assert_eq!(
1639 metadata.get("bridging").and_then(|v| v.get("providerId")).and_then(|v| v.as_str()),
1640 Some("cow-sdk://bridging/providers/cow-prov")
1641 );
1642
1643 assert!(metadata.get("hooks").is_some());
1645 }
1646
1647 #[tokio::test]
1648 async fn bridging_entry_overwrites_caller_attempt_to_inject_its_own() {
1649 let provider = FixedProvider { info: dummy_info("cow-prov"), tokens: vec![usdc_token()] };
1650 let quoter =
1651 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1652 let caller_meta = serde_json::json!({
1655 "bridging": { "providerId": "caller-spoofed" },
1656 });
1657
1658 get_intermediate_swap_result(&sample_request(), &provider, "er, Some(&caller_meta))
1659 .await
1660 .unwrap();
1661 let captured = quoter.captured.get().cloned().unwrap();
1662 let parsed: serde_json::Value =
1663 serde_json::from_str(&captured.app_data_json.unwrap()).unwrap();
1664 assert_eq!(
1665 parsed.pointer("/metadata/bridging/providerId").and_then(|v| v.as_str()),
1666 Some("cow-sdk://bridging/providers/cow-prov")
1667 );
1668 }
1669
1670 #[tokio::test]
1673 async fn propagates_quoter_error_as_tx_build_error() {
1674 struct FailingQuoter;
1675 impl SwapQuoter for FailingQuoter {
1676 fn quote_swap<'a>(&'a self, _p: SwapQuoteParams) -> QuoteSwapFuture<'a> {
1677 Box::pin(async {
1678 Err(cow_errors::CowError::Api { status: 500, body: "orderbook down".into() })
1679 })
1680 }
1681 }
1682 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![usdc_token()] };
1683 let err = get_intermediate_swap_result(&sample_request(), &provider, &FailingQuoter, None)
1684 .await
1685 .unwrap_err();
1686 assert!(
1687 matches!(&err, BridgeError::TxBuildError(s) if s.contains("500") && s.contains("orderbook down")),
1688 "got {err:?}"
1689 );
1690 }
1691
1692 #[tokio::test]
1693 async fn errors_when_all_candidates_are_the_sell_token() {
1694 let req = sample_request();
1699 let same = |chain| IntermediateTokenInfo {
1700 chain_id: chain,
1701 address: req.sell_token.into(),
1702 decimals: 18,
1703 symbol: "SELL".into(),
1704 name: "Sell Token".into(),
1705 logo_url: None,
1706 };
1707 struct Never;
1708 #[cfg_attr(coverage_nightly, coverage(off))]
1711 impl SwapQuoter for Never {
1712 fn quote_swap<'a>(&'a self, _p: SwapQuoteParams) -> QuoteSwapFuture<'a> {
1713 Box::pin(async { panic!("quoter should not be called") })
1714 }
1715 }
1716 let provider = FixedProvider {
1717 info: dummy_info("p"),
1718 tokens: vec![same(req.sell_chain_id), same(req.sell_chain_id)],
1719 };
1720 let err = get_intermediate_swap_result(&req, &provider, &Never, None).await.unwrap_err();
1721 assert!(matches!(err, BridgeError::NoIntermediateTokens));
1722 }
1723
1724 #[tokio::test]
1725 async fn provider_info_name_is_threaded_into_response() {
1726 let provider = FixedProvider { info: dummy_info("zany"), tokens: vec![usdc_token()] };
1727 let quoter =
1728 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1729 let resp = get_intermediate_swap_result(&sample_request(), &provider, "er, None)
1730 .await
1731 .unwrap();
1732 assert_eq!(resp.provider, "zany");
1733 }
1734
1735 #[tokio::test]
1736 async fn non_object_caller_metadata_is_ignored_gracefully() {
1737 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![usdc_token()] };
1738 let quoter =
1739 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1740 let bogus = serde_json::json!("not-an-object");
1743 get_intermediate_swap_result(&sample_request(), &provider, "er, Some(&bogus))
1744 .await
1745 .unwrap();
1746 let captured = quoter.captured.get().cloned().unwrap();
1747 let parsed: serde_json::Value =
1748 serde_json::from_str(&captured.app_data_json.unwrap()).unwrap();
1749 assert!(parsed.pointer("/metadata/bridging/providerId").is_some());
1750 }
1751
1752 #[tokio::test]
1753 async fn caller_hooks_entry_is_preserved_when_present() {
1754 let provider = FixedProvider { info: dummy_info("p"), tokens: vec![usdc_token()] };
1757 let quoter =
1758 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1759 let caller_meta = serde_json::json!({
1760 "hooks": { "pre": [{ "target": "0xabc", "callData": "0x", "gasLimit": "100000" }], "post": [] },
1761 });
1762 get_intermediate_swap_result(&sample_request(), &provider, "er, Some(&caller_meta))
1763 .await
1764 .unwrap();
1765 let captured = quoter.captured.get().cloned().unwrap();
1766 let parsed: serde_json::Value =
1767 serde_json::from_str(&captured.app_data_json.unwrap()).unwrap();
1768 let pre = parsed
1769 .pointer("/metadata/hooks/pre")
1770 .and_then(|v| v.as_array())
1771 .expect("pre array present");
1772 assert_eq!(pre.len(), 1);
1773 }
1774
1775 #[tokio::test]
1776 async fn fixed_provider_surface_is_callable_for_coverage() {
1777 use alloy_primitives::{Address, B256};
1781 let p = FixedProvider { info: dummy_info("surface"), tokens: vec![usdc_token()] };
1782 assert!(p.supports_route(1, 10));
1783 assert!(p.get_networks().await.unwrap().is_empty());
1784 let toks = p
1785 .get_buy_tokens(BuyTokensParams {
1786 sell_chain_id: 1,
1787 buy_chain_id: 10,
1788 sell_token_address: None,
1789 })
1790 .await
1791 .unwrap();
1792 assert!(toks.tokens.is_empty());
1793 assert_eq!(p.get_quote(&sample_request()).await.unwrap().provider, "fixed");
1794 let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
1795 assert!(p.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap().is_none());
1796 assert!(p.get_explorer_url("x").is_empty());
1797 assert_eq!(p.get_status("x", 1).await.unwrap().status, BridgeStatus::Unknown);
1798 let _ = Address::ZERO;
1801 }
1802
1803 #[tokio::test]
1804 async fn no_caller_metadata_still_produces_bridging_entry() {
1805 let provider = FixedProvider { info: dummy_info("cow-prov"), tokens: vec![usdc_token()] };
1806 let quoter =
1807 CapturingQuoter { captured: std::sync::OnceLock::new(), outcome: default_outcome() };
1808 get_intermediate_swap_result(&sample_request(), &provider, "er, None).await.unwrap();
1809 let captured = quoter.captured.get().cloned().unwrap();
1810 let parsed: serde_json::Value =
1811 serde_json::from_str(&captured.app_data_json.unwrap()).unwrap();
1812 assert!(parsed.pointer("/metadata/bridging/providerId").is_some());
1813 assert!(parsed.pointer("/metadata/hooks").is_some());
1814 }
1815}
1816
1817#[cfg(test)]
1820#[allow(clippy::tests_outside_test_module, reason = "inner module pattern")]
1821mod orchestration_tests {
1822 use alloy_primitives::{B256, U256};
1823 use cow_chains::EvmCall;
1824 use cow_types::OrderKind;
1825
1826 use super::*;
1827 use crate::{
1828 provider::{
1829 BridgeNetworkInfo, BridgeStatusFuture, BridgingParamsFuture, BuyTokensFuture,
1830 GasEstimationFuture, HookBridgeProvider, IntermediateTokensFuture, NetworksFuture,
1831 QuoteFuture, ReceiverAccountBridgeProvider, ReceiverOverrideFuture, SignedHookFuture,
1832 UnsignedCallFuture,
1833 },
1834 swap_quoter::{QuoteSwapFuture, SwapQuoteOutcome, SwapQuoteParams},
1835 types::{
1836 BridgeProviderInfo, BridgeProviderType, BuyTokensParams, GetProviderBuyTokens,
1837 IntermediateTokenInfo,
1838 },
1839 };
1840
1841 fn hook_info() -> BridgeProviderInfo {
1842 BridgeProviderInfo {
1843 name: "mock-hook".into(),
1844 logo_url: String::new(),
1845 dapp_id: "cow-sdk://bridging/providers/mock-hook".into(),
1846 website: String::new(),
1847 provider_type: BridgeProviderType::HookBridgeProvider,
1848 }
1849 }
1850
1851 fn receiver_info() -> BridgeProviderInfo {
1852 BridgeProviderInfo {
1853 name: "mock-receiver".into(),
1854 logo_url: String::new(),
1855 dapp_id: "cow-sdk://bridging/providers/mock-receiver".into(),
1856 website: String::new(),
1857 provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
1858 }
1859 }
1860
1861 fn usdc() -> IntermediateTokenInfo {
1862 IntermediateTokenInfo {
1863 chain_id: 1,
1864 address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
1865 .parse::<Address>()
1866 .unwrap()
1867 .into(),
1868 decimals: 6,
1869 symbol: "USDC".into(),
1870 name: "USD Coin".into(),
1871 logo_url: None,
1872 }
1873 }
1874
1875 fn sample_request(kind: OrderKind) -> QuoteBridgeRequest {
1876 QuoteBridgeRequest {
1877 sell_chain_id: 1,
1878 buy_chain_id: 42_161,
1879 sell_token: Address::repeat_byte(0x11),
1880 sell_token_decimals: 18,
1881 buy_token: Address::repeat_byte(0x22).into(),
1882 buy_token_decimals: 6,
1883 sell_amount: U256::from(1_000_000u64),
1884 account: Address::repeat_byte(0x33),
1885 owner: None,
1886 receiver: None,
1887 bridge_recipient: None,
1888 slippage_bps: 50,
1889 bridge_slippage_bps: None,
1890 kind,
1891 }
1892 }
1893
1894 fn sample_outcome() -> SwapQuoteOutcome {
1895 SwapQuoteOutcome {
1896 sell_amount: U256::from(1_000_000u64),
1897 buy_amount_after_slippage: U256::from(999_500u64),
1898 fee_amount: U256::from(500u64),
1899 valid_to: 9_999_999,
1900 app_data_hex: "0xabc".into(),
1901 full_app_data: "{}".into(),
1902 }
1903 }
1904
1905 fn sample_bridge_response(provider_name: &str) -> QuoteBridgeResponse {
1906 QuoteBridgeResponse {
1907 provider: provider_name.to_owned(),
1908 sell_amount: U256::from(999_500u64),
1909 buy_amount: U256::from(998_000u64),
1910 fee_amount: U256::from(1_500u64),
1911 estimated_secs: 42,
1912 bridge_hook: None,
1913 }
1914 }
1915
1916 struct MockHookProvider {
1920 info: BridgeProviderInfo,
1921 tokens: Vec<IntermediateTokenInfo>,
1922 bridge_response: QuoteBridgeResponse,
1923 unsigned_call: EvmCall,
1924 gas_limit: u64,
1925 }
1926
1927 impl BridgeProvider for MockHookProvider {
1928 fn info(&self) -> &BridgeProviderInfo {
1929 &self.info
1930 }
1931 fn supports_route(&self, _s: u64, _b: u64) -> bool {
1932 true
1933 }
1934 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
1935 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
1936 }
1937 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
1938 let info = self.info.clone();
1939 Box::pin(
1940 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
1941 )
1942 }
1943 fn get_intermediate_tokens<'a>(
1944 &'a self,
1945 _req: &'a QuoteBridgeRequest,
1946 ) -> IntermediateTokensFuture<'a> {
1947 let tokens = self.tokens.clone();
1948 Box::pin(async move { Ok(tokens) })
1949 }
1950 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
1951 let response = self.bridge_response.clone();
1952 Box::pin(async move { Ok(response) })
1953 }
1954 fn get_bridging_params<'a>(
1955 &'a self,
1956 _c: u64,
1957 _o: &'a cow_orderbook::types::Order,
1958 _t: B256,
1959 _s: Option<Address>,
1960 ) -> BridgingParamsFuture<'a> {
1961 Box::pin(async { Ok(None) })
1962 }
1963 fn get_explorer_url(&self, _id: &str) -> String {
1964 String::new()
1965 }
1966 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
1967 Box::pin(async {
1968 Ok(BridgeStatusResult {
1969 status: BridgeStatus::Unknown,
1970 fill_time_in_seconds: None,
1971 deposit_tx_hash: None,
1972 fill_tx_hash: None,
1973 })
1974 })
1975 }
1976 fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
1977 Some(self)
1978 }
1979 }
1980
1981 impl HookBridgeProvider for MockHookProvider {
1982 fn get_unsigned_bridge_call<'a>(
1983 &'a self,
1984 _req: &'a QuoteBridgeRequest,
1985 _quote: &'a QuoteBridgeResponse,
1986 ) -> UnsignedCallFuture<'a> {
1987 let call = self.unsigned_call.clone();
1988 Box::pin(async move { Ok(call) })
1989 }
1990 fn get_gas_limit_estimation_for_hook<'a>(
1991 &'a self,
1992 _proxy_deployed: bool,
1993 _extra_gas: Option<u64>,
1994 _extra_gas_proxy_creation: Option<u64>,
1995 ) -> GasEstimationFuture<'a> {
1996 let gas = self.gas_limit;
1997 Box::pin(async move { Ok(gas) })
1998 }
1999 fn get_signed_hook<'a>(
2000 &'a self,
2001 _chain_id: cow_chains::SupportedChainId,
2002 _unsigned_call: &'a EvmCall,
2003 _nonce: &'a str,
2004 _deadline: u64,
2005 _gas: u64,
2006 _signer: &'a alloy_signer_local::PrivateKeySigner,
2007 ) -> SignedHookFuture<'a> {
2008 Box::pin(async {
2009 Err(cow_errors::CowError::Signing("not needed in PR #7 tests".into()))
2010 })
2011 }
2012 }
2013
2014 struct MockReceiverProvider {
2016 info: BridgeProviderInfo,
2017 tokens: Vec<IntermediateTokenInfo>,
2018 bridge_response: QuoteBridgeResponse,
2019 deposit_address: String,
2020 }
2021
2022 impl BridgeProvider for MockReceiverProvider {
2023 fn info(&self) -> &BridgeProviderInfo {
2024 &self.info
2025 }
2026 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2027 true
2028 }
2029 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2030 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2031 }
2032 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2033 let info = self.info.clone();
2034 Box::pin(
2035 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2036 )
2037 }
2038 fn get_intermediate_tokens<'a>(
2039 &'a self,
2040 _req: &'a QuoteBridgeRequest,
2041 ) -> IntermediateTokensFuture<'a> {
2042 let tokens = self.tokens.clone();
2043 Box::pin(async move { Ok(tokens) })
2044 }
2045 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2046 let response = self.bridge_response.clone();
2047 Box::pin(async move { Ok(response) })
2048 }
2049 fn get_bridging_params<'a>(
2050 &'a self,
2051 _c: u64,
2052 _o: &'a cow_orderbook::types::Order,
2053 _t: B256,
2054 _s: Option<Address>,
2055 ) -> BridgingParamsFuture<'a> {
2056 Box::pin(async { Ok(None) })
2057 }
2058 fn get_explorer_url(&self, _id: &str) -> String {
2059 String::new()
2060 }
2061 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2062 Box::pin(async {
2063 Ok(BridgeStatusResult {
2064 status: BridgeStatus::Unknown,
2065 fill_time_in_seconds: None,
2066 deposit_tx_hash: None,
2067 fill_tx_hash: None,
2068 })
2069 })
2070 }
2071 fn as_receiver_account_bridge_provider(
2072 &self,
2073 ) -> Option<&dyn ReceiverAccountBridgeProvider> {
2074 Some(self)
2075 }
2076 }
2077
2078 impl ReceiverAccountBridgeProvider for MockReceiverProvider {
2079 fn get_bridge_receiver_override<'a>(
2080 &'a self,
2081 _quote_request: &'a QuoteBridgeRequest,
2082 _quote_result: &'a QuoteBridgeResponse,
2083 ) -> ReceiverOverrideFuture<'a> {
2084 let addr = self.deposit_address.clone();
2085 Box::pin(async move { Ok(addr) })
2086 }
2087 }
2088
2089 struct MockUnknownProvider {
2091 info: BridgeProviderInfo,
2092 }
2093
2094 impl BridgeProvider for MockUnknownProvider {
2095 fn info(&self) -> &BridgeProviderInfo {
2096 &self.info
2097 }
2098 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2099 true
2100 }
2101 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2102 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2103 }
2104 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2105 let info = self.info.clone();
2106 Box::pin(
2107 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2108 )
2109 }
2110 fn get_intermediate_tokens<'a>(
2111 &'a self,
2112 _req: &'a QuoteBridgeRequest,
2113 ) -> IntermediateTokensFuture<'a> {
2114 Box::pin(async { Ok(Vec::<IntermediateTokenInfo>::new()) })
2115 }
2116 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2117 Box::pin(async { Ok(sample_bridge_response("unknown")) })
2118 }
2119 fn get_bridging_params<'a>(
2120 &'a self,
2121 _c: u64,
2122 _o: &'a cow_orderbook::types::Order,
2123 _t: B256,
2124 _s: Option<Address>,
2125 ) -> BridgingParamsFuture<'a> {
2126 Box::pin(async { Ok(None) })
2127 }
2128 fn get_explorer_url(&self, _id: &str) -> String {
2129 String::new()
2130 }
2131 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2132 Box::pin(async {
2133 Ok(BridgeStatusResult {
2134 status: BridgeStatus::Unknown,
2135 fill_time_in_seconds: None,
2136 deposit_tx_hash: None,
2137 fill_tx_hash: None,
2138 })
2139 })
2140 }
2141 }
2142
2143 struct FixedQuoter {
2144 outcome: SwapQuoteOutcome,
2145 captured: std::sync::OnceLock<SwapQuoteParams>,
2146 }
2147
2148 impl SwapQuoter for FixedQuoter {
2149 fn quote_swap<'a>(&'a self, params: SwapQuoteParams) -> QuoteSwapFuture<'a> {
2150 self.captured.set(params).ok();
2151 let outcome = self.outcome.clone();
2152 Box::pin(async move { Ok(outcome) })
2153 }
2154 }
2155
2156 fn build_unsigned_call() -> EvmCall {
2157 EvmCall { to: Address::repeat_byte(0xAC), data: vec![0xde, 0xad], value: U256::ZERO }
2158 }
2159
2160 fn hook_params_with_metadata(metadata: Option<serde_json::Value>) -> GetQuoteWithBridgeParams {
2161 GetQuoteWithBridgeParams {
2162 swap_and_bridge_request: sample_request(OrderKind::Sell),
2163 slippage_bps: 50,
2164 advanced_settings_metadata: metadata,
2165 quote_signer: None,
2166 hook_deadline: None,
2167 }
2168 }
2169
2170 #[tokio::test]
2173 async fn get_quote_with_bridge_rejects_buy_orders() {
2174 let provider = MockHookProvider {
2175 info: hook_info(),
2176 tokens: vec![usdc()],
2177 bridge_response: sample_bridge_response("mock-hook"),
2178 unsigned_call: build_unsigned_call(),
2179 gas_limit: 500_000,
2180 };
2181 let quoter =
2182 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2183 let params = GetQuoteWithBridgeParams {
2184 swap_and_bridge_request: sample_request(OrderKind::Buy),
2185 slippage_bps: 50,
2186 advanced_settings_metadata: None,
2187 quote_signer: None,
2188 hook_deadline: None,
2189 };
2190 let err = get_quote_with_bridge(¶ms, &provider, "er).await.unwrap_err();
2191 assert!(matches!(err, BridgeError::OnlySellOrderSupported));
2192 }
2193
2194 #[tokio::test]
2195 async fn get_quote_with_bridge_dispatches_to_hook_branch() {
2196 let provider = MockHookProvider {
2197 info: hook_info(),
2198 tokens: vec![usdc()],
2199 bridge_response: sample_bridge_response("mock-hook"),
2200 unsigned_call: build_unsigned_call(),
2201 gas_limit: 500_000,
2202 };
2203 let quoter =
2204 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2205 let result = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2206 .await
2207 .unwrap();
2208 assert!(result.bridge.bridge_call_details.is_some());
2210 assert!(result.bridge.bridge_receiver_override.is_none());
2211 assert_eq!(result.bridge.provider_info.name, "mock-hook");
2212 }
2213
2214 #[tokio::test]
2215 async fn get_quote_with_bridge_dispatches_to_receiver_branch() {
2216 let provider = MockReceiverProvider {
2217 info: receiver_info(),
2218 tokens: vec![usdc()],
2219 bridge_response: sample_bridge_response("mock-receiver"),
2220 deposit_address: "0xDEA00DEA00DEA00DEA00DEA00DEA00DEA00DEA000".into(),
2221 };
2222 let quoter =
2223 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2224 let result = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2225 .await
2226 .unwrap();
2227 assert!(result.bridge.bridge_call_details.is_none());
2229 assert_eq!(
2230 result.bridge.bridge_receiver_override.as_deref(),
2231 Some("0xDEA00DEA00DEA00DEA00DEA00DEA00DEA00DEA000"),
2232 );
2233 }
2234
2235 #[tokio::test]
2236 async fn get_quote_with_bridge_errors_on_unknown_provider_kind() {
2237 let provider = MockUnknownProvider { info: hook_info() };
2238 let quoter =
2239 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2240 let err = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2241 .await
2242 .unwrap_err();
2243 if let BridgeError::TxBuildError(msg) = err {
2244 assert!(msg.contains("implements neither"));
2245 } else {
2246 panic!("expected TxBuildError, got {err:?}");
2247 }
2248 }
2249
2250 #[tokio::test]
2253 async fn hook_branch_preserves_caller_metadata_on_intermediate_swap() {
2254 let provider = MockHookProvider {
2255 info: hook_info(),
2256 tokens: vec![usdc()],
2257 bridge_response: sample_bridge_response("mock-hook"),
2258 unsigned_call: build_unsigned_call(),
2259 gas_limit: 500_000,
2260 };
2261 let quoter =
2262 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2263 let caller_meta = serde_json::json!({
2264 "partnerFee": { "bps": 25, "recipient": "0xpartner" },
2265 });
2266 let params = hook_params_with_metadata(Some(caller_meta));
2267
2268 get_quote_with_bridge(¶ms, &provider, "er).await.unwrap();
2269
2270 let captured = quoter.captured.get().cloned().expect("quoter called");
2271 let app_data: serde_json::Value =
2272 serde_json::from_str(&captured.app_data_json.unwrap()).unwrap();
2273 assert_eq!(app_data.pointer("/metadata/partnerFee/bps").and_then(|v| v.as_u64()), Some(25),);
2274 assert_eq!(
2275 app_data.pointer("/metadata/bridging/providerId").and_then(|v| v.as_str()),
2276 Some("cow-sdk://bridging/providers/mock-hook"),
2277 );
2278 }
2279
2280 #[tokio::test]
2283 async fn get_quote_without_bridge_calls_quoter_with_full_request() {
2284 let outcome = sample_outcome();
2285 let quoter = FixedQuoter { outcome: outcome.clone(), captured: std::sync::OnceLock::new() };
2286 let result =
2287 get_quote_without_bridge(&sample_request(OrderKind::Sell), "er).await.unwrap();
2288 assert_eq!(result.quote.sell_amount, outcome.sell_amount);
2289 assert_eq!(result.quote.buy_amount, outcome.buy_amount_after_slippage);
2290 assert_eq!(result.quote.fee_amount, outcome.fee_amount);
2291 assert_eq!(result.quote.provider, "same-chain");
2292
2293 let captured = quoter.captured.get().cloned().unwrap();
2294 assert_eq!(captured.buy_token, sample_request(OrderKind::Sell).buy_token);
2297 }
2298
2299 #[tokio::test]
2300 async fn get_swap_quote_returns_provider_agnostic_response() {
2301 let outcome = sample_outcome();
2302 let quoter = FixedQuoter { outcome: outcome.clone(), captured: std::sync::OnceLock::new() };
2303 let resp = get_swap_quote(&sample_request(OrderKind::Sell), "er).await.unwrap();
2304 assert_eq!(resp.provider, "swap");
2305 assert_eq!(resp.buy_amount, outcome.buy_amount_after_slippage);
2306 }
2307
2308 #[tokio::test]
2309 async fn get_quote_without_bridge_propagates_quoter_error() {
2310 struct Failing;
2311 impl SwapQuoter for Failing {
2312 fn quote_swap<'a>(&'a self, _p: SwapQuoteParams) -> QuoteSwapFuture<'a> {
2313 Box::pin(async {
2314 Err(cow_errors::CowError::Api { status: 502, body: "upstream".into() })
2315 })
2316 }
2317 }
2318 let err =
2319 get_quote_without_bridge(&sample_request(OrderKind::Sell), &Failing).await.unwrap_err();
2320 assert!(matches!(err, BridgeError::TxBuildError(_)));
2321 }
2322
2323 #[tokio::test]
2324 async fn get_swap_quote_propagates_quoter_error() {
2325 struct Failing;
2326 impl SwapQuoter for Failing {
2327 fn quote_swap<'a>(&'a self, _p: SwapQuoteParams) -> QuoteSwapFuture<'a> {
2328 Box::pin(async {
2329 Err(cow_errors::CowError::Api { status: 500, body: "boom".into() })
2330 })
2331 }
2332 }
2333 let err = get_swap_quote(&sample_request(OrderKind::Sell), &Failing).await.unwrap_err();
2334 assert!(matches!(err, BridgeError::TxBuildError(_)));
2335 }
2336
2337 #[tokio::test]
2338 async fn get_quote_without_bridge_rejects_raw_buy_token() {
2339 let quoter =
2342 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2343 let mut req = sample_request(OrderKind::Sell);
2344 req.buy_token = crate::types::TokenAddress::Raw("bc1qanything".into());
2345 let err = get_quote_without_bridge(&req, "er).await.unwrap_err();
2346 let msg = err.to_string();
2347 assert!(matches!(err, BridgeError::TxBuildError(_)));
2348 assert!(msg.contains("EVM buy_token"), "unexpected err: {err}");
2349 }
2350
2351 #[tokio::test]
2352 async fn get_swap_quote_rejects_raw_buy_token() {
2353 let quoter =
2354 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2355 let mut req = sample_request(OrderKind::Sell);
2356 req.buy_token = crate::types::TokenAddress::Raw("SPL_MINT".into());
2357 let err = get_swap_quote(&req, "er).await.unwrap_err();
2358 assert!(matches!(err, BridgeError::TxBuildError(_)));
2359 }
2360
2361 #[test]
2362 fn get_cache_key_serialises_evm_and_raw_buy_tokens_differently() {
2363 let mut evm_req = sample_request(OrderKind::Sell);
2364 evm_req.buy_token = Address::repeat_byte(0x22).into();
2365 let evm_key = get_cache_key(&evm_req);
2366 assert!(evm_key.contains("0x22"));
2367 assert!(!evm_key.contains("raw:"));
2368
2369 let mut raw_req = sample_request(OrderKind::Sell);
2370 raw_req.buy_token = crate::types::TokenAddress::Raw("sol_mint_pubkey".into());
2371 let raw_key = get_cache_key(&raw_req);
2372 assert!(raw_key.contains("raw:sol_mint_pubkey"));
2373 assert_ne!(evm_key, raw_key);
2374 }
2375
2376 #[tokio::test]
2379 async fn hook_branch_propagates_gas_estimation_error() {
2380 struct FailingGasProvider {
2382 info: BridgeProviderInfo,
2383 tokens: Vec<IntermediateTokenInfo>,
2384 }
2385 impl BridgeProvider for FailingGasProvider {
2386 fn info(&self) -> &BridgeProviderInfo {
2387 &self.info
2388 }
2389 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2390 true
2391 }
2392 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2393 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2394 }
2395 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2396 let info = self.info.clone();
2397 Box::pin(
2398 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2399 )
2400 }
2401 fn get_intermediate_tokens<'a>(
2402 &'a self,
2403 _req: &'a QuoteBridgeRequest,
2404 ) -> IntermediateTokensFuture<'a> {
2405 let tokens = self.tokens.clone();
2406 Box::pin(async move { Ok(tokens) })
2407 }
2408 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2409 Box::pin(async { Ok(sample_bridge_response("hook-failing-gas")) })
2410 }
2411 fn get_bridging_params<'a>(
2412 &'a self,
2413 _c: u64,
2414 _o: &'a cow_orderbook::types::Order,
2415 _t: B256,
2416 _s: Option<Address>,
2417 ) -> BridgingParamsFuture<'a> {
2418 Box::pin(async { Ok(None) })
2419 }
2420 fn get_explorer_url(&self, _id: &str) -> String {
2421 String::new()
2422 }
2423 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2424 Box::pin(async {
2425 Ok(BridgeStatusResult {
2426 status: BridgeStatus::Unknown,
2427 fill_time_in_seconds: None,
2428 deposit_tx_hash: None,
2429 fill_tx_hash: None,
2430 })
2431 })
2432 }
2433 fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
2434 Some(self)
2435 }
2436 }
2437 impl HookBridgeProvider for FailingGasProvider {
2438 fn get_unsigned_bridge_call<'a>(
2439 &'a self,
2440 _req: &'a QuoteBridgeRequest,
2441 _quote: &'a QuoteBridgeResponse,
2442 ) -> UnsignedCallFuture<'a> {
2443 Box::pin(async {
2444 Err(cow_errors::CowError::Api { status: 0, body: "not called".into() })
2445 })
2446 }
2447 fn get_gas_limit_estimation_for_hook<'a>(
2448 &'a self,
2449 _proxy_deployed: bool,
2450 _extra_gas: Option<u64>,
2451 _extra_gas_proxy_creation: Option<u64>,
2452 ) -> GasEstimationFuture<'a> {
2453 Box::pin(async {
2454 Err(cow_errors::CowError::Api { status: 500, body: "gas oracle down".into() })
2455 })
2456 }
2457 fn get_signed_hook<'a>(
2458 &'a self,
2459 _chain_id: cow_chains::SupportedChainId,
2460 _unsigned_call: &'a EvmCall,
2461 _nonce: &'a str,
2462 _deadline: u64,
2463 _gas: u64,
2464 _signer: &'a alloy_signer_local::PrivateKeySigner,
2465 ) -> SignedHookFuture<'a> {
2466 Box::pin(async { Err(cow_errors::CowError::Signing("n/a".into())) })
2467 }
2468 }
2469
2470 let provider = FailingGasProvider { info: hook_info(), tokens: vec![usdc()] };
2471 exercise_bridge_surface(&provider).await;
2472 exercise_hook_bridge_surface(&provider).await;
2473 let quoter =
2474 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2475 let err = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2476 .await
2477 .unwrap_err();
2478 if let BridgeError::TxBuildError(msg) = err {
2479 assert!(msg.contains("gas oracle down"), "unexpected msg: {msg}");
2480 } else {
2481 panic!("expected TxBuildError, got {err:?}");
2482 }
2483 }
2484
2485 #[tokio::test]
2486 async fn hook_branch_propagates_unsigned_call_error() {
2487 struct FailingUnsignedCall {
2489 info: BridgeProviderInfo,
2490 tokens: Vec<IntermediateTokenInfo>,
2491 }
2492 impl BridgeProvider for FailingUnsignedCall {
2493 fn info(&self) -> &BridgeProviderInfo {
2494 &self.info
2495 }
2496 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2497 true
2498 }
2499 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2500 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2501 }
2502 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2503 let info = self.info.clone();
2504 Box::pin(
2505 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2506 )
2507 }
2508 fn get_intermediate_tokens<'a>(
2509 &'a self,
2510 _req: &'a QuoteBridgeRequest,
2511 ) -> IntermediateTokensFuture<'a> {
2512 let tokens = self.tokens.clone();
2513 Box::pin(async move { Ok(tokens) })
2514 }
2515 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2516 Box::pin(async { Ok(sample_bridge_response("hook-bad-calldata")) })
2517 }
2518 fn get_bridging_params<'a>(
2519 &'a self,
2520 _c: u64,
2521 _o: &'a cow_orderbook::types::Order,
2522 _t: B256,
2523 _s: Option<Address>,
2524 ) -> BridgingParamsFuture<'a> {
2525 Box::pin(async { Ok(None) })
2526 }
2527 fn get_explorer_url(&self, _id: &str) -> String {
2528 String::new()
2529 }
2530 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2531 Box::pin(async {
2532 Ok(BridgeStatusResult {
2533 status: BridgeStatus::Unknown,
2534 fill_time_in_seconds: None,
2535 deposit_tx_hash: None,
2536 fill_tx_hash: None,
2537 })
2538 })
2539 }
2540 fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
2541 Some(self)
2542 }
2543 }
2544 impl HookBridgeProvider for FailingUnsignedCall {
2545 fn get_unsigned_bridge_call<'a>(
2546 &'a self,
2547 _req: &'a QuoteBridgeRequest,
2548 _quote: &'a QuoteBridgeResponse,
2549 ) -> UnsignedCallFuture<'a> {
2550 Box::pin(async {
2551 Err(cow_errors::CowError::Api { status: 0, body: "bad calldata".into() })
2552 })
2553 }
2554 fn get_gas_limit_estimation_for_hook<'a>(
2555 &'a self,
2556 _proxy_deployed: bool,
2557 _extra_gas: Option<u64>,
2558 _extra_gas_proxy_creation: Option<u64>,
2559 ) -> GasEstimationFuture<'a> {
2560 Box::pin(async move { Ok(500_000u64) })
2561 }
2562 fn get_signed_hook<'a>(
2563 &'a self,
2564 _chain_id: cow_chains::SupportedChainId,
2565 _unsigned_call: &'a EvmCall,
2566 _nonce: &'a str,
2567 _deadline: u64,
2568 _gas: u64,
2569 _signer: &'a alloy_signer_local::PrivateKeySigner,
2570 ) -> SignedHookFuture<'a> {
2571 Box::pin(async { Err(cow_errors::CowError::Signing("n/a".into())) })
2572 }
2573 }
2574
2575 let provider = FailingUnsignedCall { info: hook_info(), tokens: vec![usdc()] };
2576 exercise_bridge_surface(&provider).await;
2577 exercise_hook_bridge_surface(&provider).await;
2578 let quoter =
2579 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2580 let err = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2581 .await
2582 .unwrap_err();
2583 if let BridgeError::TxBuildError(msg) = err {
2584 assert!(msg.contains("bad calldata"), "unexpected msg: {msg}");
2585 } else {
2586 panic!("expected TxBuildError, got {err:?}");
2587 }
2588 }
2589
2590 #[tokio::test]
2593 async fn receiver_branch_propagates_override_error() {
2594 struct FailingReceiverOverride {
2596 info: BridgeProviderInfo,
2597 tokens: Vec<IntermediateTokenInfo>,
2598 }
2599 impl BridgeProvider for FailingReceiverOverride {
2600 fn info(&self) -> &BridgeProviderInfo {
2601 &self.info
2602 }
2603 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2604 true
2605 }
2606 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2607 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2608 }
2609 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2610 let info = self.info.clone();
2611 Box::pin(
2612 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2613 )
2614 }
2615 fn get_intermediate_tokens<'a>(
2616 &'a self,
2617 _req: &'a QuoteBridgeRequest,
2618 ) -> IntermediateTokensFuture<'a> {
2619 let tokens = self.tokens.clone();
2620 Box::pin(async move { Ok(tokens) })
2621 }
2622 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2623 Box::pin(async { Ok(sample_bridge_response("receiver-failing-override")) })
2624 }
2625 fn get_bridging_params<'a>(
2626 &'a self,
2627 _c: u64,
2628 _o: &'a cow_orderbook::types::Order,
2629 _t: B256,
2630 _s: Option<Address>,
2631 ) -> BridgingParamsFuture<'a> {
2632 Box::pin(async { Ok(None) })
2633 }
2634 fn get_explorer_url(&self, _id: &str) -> String {
2635 String::new()
2636 }
2637 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2638 Box::pin(async {
2639 Ok(BridgeStatusResult {
2640 status: BridgeStatus::Unknown,
2641 fill_time_in_seconds: None,
2642 deposit_tx_hash: None,
2643 fill_tx_hash: None,
2644 })
2645 })
2646 }
2647 fn as_receiver_account_bridge_provider(
2648 &self,
2649 ) -> Option<&dyn ReceiverAccountBridgeProvider> {
2650 Some(self)
2651 }
2652 }
2653 impl ReceiverAccountBridgeProvider for FailingReceiverOverride {
2654 fn get_bridge_receiver_override<'a>(
2655 &'a self,
2656 _quote_request: &'a QuoteBridgeRequest,
2657 _quote_result: &'a QuoteBridgeResponse,
2658 ) -> ReceiverOverrideFuture<'a> {
2659 Box::pin(async {
2660 Err(cow_errors::CowError::Api {
2661 status: 0,
2662 body: "no deposit addr available".into(),
2663 })
2664 })
2665 }
2666 }
2667
2668 let provider = FailingReceiverOverride { info: receiver_info(), tokens: vec![usdc()] };
2669 exercise_bridge_surface(&provider).await;
2670 let quoter =
2671 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2672 let err = get_quote_with_bridge(&hook_params_with_metadata(None), &provider, "er)
2673 .await
2674 .unwrap_err();
2675 if let BridgeError::TxBuildError(msg) = err {
2676 assert!(msg.contains("no deposit addr available"), "unexpected msg: {msg}");
2677 } else {
2678 panic!("expected TxBuildError, got {err:?}");
2679 }
2680 }
2681
2682 #[tokio::test]
2685 async fn hook_branch_bridge_call_details_carry_unsigned_call_bytes() {
2686 let provider = MockHookProvider {
2687 info: hook_info(),
2688 tokens: vec![usdc()],
2689 bridge_response: sample_bridge_response("mock-hook"),
2690 unsigned_call: build_unsigned_call(),
2691 gas_limit: 500_000,
2692 };
2693 let quoter =
2694 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2695 let result =
2696 get_quote_with_hook_bridge(&provider, &hook_params_with_metadata(None), "er)
2697 .await
2698 .unwrap();
2699 let details =
2700 result.bridge.bridge_call_details.expect("hook branch populates call_details");
2701 assert_eq!(details.unsigned_bridge_call.data, vec![0xde, 0xad]);
2702 assert_eq!(details.unsigned_bridge_call.to, Address::repeat_byte(0xAC),);
2703 assert_eq!(details.pre_authorized_bridging_hook.post_hook.gas_limit, "500000",);
2706 }
2707
2708 #[tokio::test]
2711 async fn receiver_branch_sets_override_and_clears_call_details() {
2712 let provider = MockReceiverProvider {
2713 info: receiver_info(),
2714 tokens: vec![usdc()],
2715 bridge_response: sample_bridge_response("mock-receiver"),
2716 deposit_address: "TOPsolanaDepositAddrXXXXXXXXXXXXXXXXXXXXXXX".into(),
2717 };
2718 let quoter =
2719 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
2720 let result = get_quote_with_receiver_account_bridge(
2721 &provider,
2722 &hook_params_with_metadata(None),
2723 "er,
2724 )
2725 .await
2726 .unwrap();
2727 assert!(result.bridge.bridge_call_details.is_none());
2728 assert_eq!(
2729 result.bridge.bridge_receiver_override.as_deref(),
2730 Some("TOPsolanaDepositAddrXXXXXXXXXXXXXXXXXXXXXXX"),
2731 );
2732 }
2733
2734 #[test]
2737 fn minimal_bridge_quote_result_wraps_response_amounts() {
2738 let req = sample_request(OrderKind::Sell);
2739 let resp = sample_bridge_response("arb");
2740 let quote = minimal_bridge_quote_result(&req, &resp);
2741 assert!(quote.is_sell);
2742 assert_eq!(quote.amounts_and_costs.after_fee.buy_amount, resp.buy_amount);
2743 assert_eq!(
2745 quote.amounts_and_costs.before_fee.buy_amount,
2746 resp.buy_amount.saturating_add(resp.fee_amount),
2747 );
2748 assert_eq!(quote.fees.bridge_fee, resp.fee_amount);
2749 assert_eq!(quote.expected_fill_time_seconds, Some(resp.estimated_secs));
2750 }
2751
2752 #[test]
2753 fn minimal_bridge_quote_result_flags_buy_orders_as_non_sell() {
2754 let req = sample_request(OrderKind::Buy);
2755 let resp = sample_bridge_response("arb");
2756 let quote = minimal_bridge_quote_result(&req, &resp);
2757 assert!(!quote.is_sell);
2758 }
2759
2760 struct SigningCaptureProvider {
2765 info: BridgeProviderInfo,
2766 tokens: Vec<IntermediateTokenInfo>,
2767 bridge_response: QuoteBridgeResponse,
2768 unsigned_call: EvmCall,
2769 captured_nonce: std::sync::OnceLock<String>,
2770 captured_deadline: std::sync::OnceLock<u64>,
2771 captured_gas: std::sync::OnceLock<u64>,
2772 }
2773
2774 impl BridgeProvider for SigningCaptureProvider {
2775 fn info(&self) -> &BridgeProviderInfo {
2776 &self.info
2777 }
2778 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2779 true
2780 }
2781 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2782 Box::pin(async { Ok(Vec::<BridgeNetworkInfo>::new()) })
2783 }
2784 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2785 let info = self.info.clone();
2786 Box::pin(
2787 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2788 )
2789 }
2790 fn get_intermediate_tokens<'a>(
2791 &'a self,
2792 _req: &'a QuoteBridgeRequest,
2793 ) -> IntermediateTokensFuture<'a> {
2794 let tokens = self.tokens.clone();
2795 Box::pin(async move { Ok(tokens) })
2796 }
2797 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2798 let resp = self.bridge_response.clone();
2799 Box::pin(async move { Ok(resp) })
2800 }
2801 fn get_bridging_params<'a>(
2802 &'a self,
2803 _c: u64,
2804 _o: &'a cow_orderbook::types::Order,
2805 _t: B256,
2806 _s: Option<Address>,
2807 ) -> BridgingParamsFuture<'a> {
2808 Box::pin(async { Ok(None) })
2809 }
2810 fn get_explorer_url(&self, _id: &str) -> String {
2811 String::new()
2812 }
2813 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2814 Box::pin(async {
2815 Ok(BridgeStatusResult {
2816 status: BridgeStatus::Unknown,
2817 fill_time_in_seconds: None,
2818 deposit_tx_hash: None,
2819 fill_tx_hash: None,
2820 })
2821 })
2822 }
2823 fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
2824 Some(self)
2825 }
2826 }
2827
2828 impl HookBridgeProvider for SigningCaptureProvider {
2829 fn get_unsigned_bridge_call<'a>(
2830 &'a self,
2831 _req: &'a QuoteBridgeRequest,
2832 _quote: &'a QuoteBridgeResponse,
2833 ) -> UnsignedCallFuture<'a> {
2834 let call = self.unsigned_call.clone();
2835 Box::pin(async move { Ok(call) })
2836 }
2837 fn get_gas_limit_estimation_for_hook<'a>(
2838 &'a self,
2839 _proxy_deployed: bool,
2840 _extra_gas: Option<u64>,
2841 _extra_gas_proxy_creation: Option<u64>,
2842 ) -> GasEstimationFuture<'a> {
2843 Box::pin(async move { Ok(500_000u64) })
2844 }
2845 fn get_signed_hook<'a>(
2846 &'a self,
2847 _chain_id: cow_chains::SupportedChainId,
2848 _unsigned_call: &'a EvmCall,
2849 nonce: &'a str,
2850 deadline: u64,
2851 hook_gas_limit: u64,
2852 _signer: &'a alloy_signer_local::PrivateKeySigner,
2853 ) -> SignedHookFuture<'a> {
2854 self.captured_nonce.set(nonce.to_owned()).ok();
2855 self.captured_deadline.set(deadline).ok();
2856 self.captured_gas.set(hook_gas_limit).ok();
2857 Box::pin(async {
2858 Ok(BridgeHook {
2859 post_hook: crate::utils::hook_mock_for_cost_estimation(500_000),
2860 recipient: "0x0000000000000000000000000000000000000001".into(),
2861 })
2862 })
2863 }
2864 }
2865
2866 fn make_signer() -> alloy_signer_local::PrivateKeySigner {
2867 use std::str::FromStr;
2868 alloy_signer_local::PrivateKeySigner::from_str(
2869 "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
2870 )
2871 .unwrap()
2872 }
2873
2874 #[tokio::test]
2875 async fn get_bridge_signed_hook_threads_context_into_provider() {
2876 let provider = SigningCaptureProvider {
2877 info: hook_info(),
2878 tokens: vec![usdc()],
2879 bridge_response: sample_bridge_response("sig-capture"),
2880 unsigned_call: build_unsigned_call(),
2881 captured_nonce: std::sync::OnceLock::new(),
2882 captured_deadline: std::sync::OnceLock::new(),
2883 captured_gas: std::sync::OnceLock::new(),
2884 };
2885 let signer = make_signer();
2886 let ctx = GetBridgeSignedHookContext {
2887 signer: &signer,
2888 hook_gas_limit: 123_456,
2889 chain_id: cow_chains::SupportedChainId::Mainnet,
2890 deadline: 9_999_999,
2891 };
2892 let out =
2893 get_bridge_signed_hook(&provider, &sample_request(OrderKind::Sell), ctx).await.unwrap();
2894 assert_eq!(*provider.captured_gas.get().unwrap(), 123_456);
2896 assert_eq!(*provider.captured_deadline.get().unwrap(), 9_999_999);
2897 let expected = derive_hook_nonce(&out.unsigned_bridge_call.data, 9_999_999);
2899 assert_eq!(provider.captured_nonce.get().unwrap(), &expected);
2900 assert_eq!(out.bridging_quote.provider, "sig-capture");
2901 }
2902
2903 #[test]
2904 fn derive_hook_nonce_is_deterministic() {
2905 let data = vec![0xde, 0xad, 0xbe, 0xef];
2906 let a = derive_hook_nonce(&data, 42);
2907 let b = derive_hook_nonce(&data, 42);
2908 assert_eq!(a, b);
2909 assert!(a.starts_with("0x"));
2910 assert_eq!(a.len(), 2 + 64); }
2912
2913 #[test]
2914 fn derive_hook_nonce_changes_with_deadline() {
2915 let data = vec![0x01, 0x02];
2916 let a = derive_hook_nonce(&data, 42);
2917 let b = derive_hook_nonce(&data, 43);
2918 assert_ne!(a, b);
2919 }
2920
2921 #[test]
2922 fn derive_hook_nonce_changes_with_data() {
2923 let a = derive_hook_nonce(&[0x01], 42);
2924 let b = derive_hook_nonce(&[0x02], 42);
2925 assert_ne!(a, b);
2926 }
2927
2928 #[tokio::test]
2929 async fn get_bridge_signed_hook_propagates_quote_error() {
2930 struct QuoteFailing {
2932 info: BridgeProviderInfo,
2933 }
2934 impl BridgeProvider for QuoteFailing {
2935 fn info(&self) -> &BridgeProviderInfo {
2936 &self.info
2937 }
2938 fn supports_route(&self, _s: u64, _b: u64) -> bool {
2939 true
2940 }
2941 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
2942 Box::pin(async { Ok(Vec::new()) })
2943 }
2944 fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
2945 let info = self.info.clone();
2946 Box::pin(
2947 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
2948 )
2949 }
2950 fn get_intermediate_tokens<'a>(
2951 &'a self,
2952 _req: &'a QuoteBridgeRequest,
2953 ) -> IntermediateTokensFuture<'a> {
2954 Box::pin(async { Ok(Vec::new()) })
2955 }
2956 fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
2957 Box::pin(async {
2958 Err(cow_errors::CowError::Api { status: 500, body: "nope".into() })
2959 })
2960 }
2961 fn get_bridging_params<'a>(
2962 &'a self,
2963 _c: u64,
2964 _o: &'a cow_orderbook::types::Order,
2965 _t: B256,
2966 _s: Option<Address>,
2967 ) -> BridgingParamsFuture<'a> {
2968 Box::pin(async { Ok(None) })
2969 }
2970 fn get_explorer_url(&self, _id: &str) -> String {
2971 String::new()
2972 }
2973 fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
2974 Box::pin(async {
2975 Ok(BridgeStatusResult {
2976 status: BridgeStatus::Unknown,
2977 fill_time_in_seconds: None,
2978 deposit_tx_hash: None,
2979 fill_tx_hash: None,
2980 })
2981 })
2982 }
2983 fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
2984 Some(self)
2985 }
2986 }
2987 impl HookBridgeProvider for QuoteFailing {
2988 fn get_unsigned_bridge_call<'a>(
2989 &'a self,
2990 _req: &'a QuoteBridgeRequest,
2991 _quote: &'a QuoteBridgeResponse,
2992 ) -> UnsignedCallFuture<'a> {
2993 Box::pin(async { Err(cow_errors::CowError::Signing("n/a".into())) })
2994 }
2995 fn get_gas_limit_estimation_for_hook<'a>(
2996 &'a self,
2997 _proxy_deployed: bool,
2998 _extra_gas: Option<u64>,
2999 _extra_gas_proxy_creation: Option<u64>,
3000 ) -> GasEstimationFuture<'a> {
3001 Box::pin(async move { Ok(500_000u64) })
3002 }
3003 fn get_signed_hook<'a>(
3004 &'a self,
3005 _chain_id: cow_chains::SupportedChainId,
3006 _unsigned_call: &'a EvmCall,
3007 _nonce: &'a str,
3008 _deadline: u64,
3009 _gas: u64,
3010 _signer: &'a alloy_signer_local::PrivateKeySigner,
3011 ) -> SignedHookFuture<'a> {
3012 Box::pin(async { Err(cow_errors::CowError::Signing("n/a".into())) })
3013 }
3014 }
3015
3016 let provider = QuoteFailing { info: hook_info() };
3017 assert!(provider.as_hook_bridge_provider().is_some());
3020 assert!(provider.as_receiver_account_bridge_provider().is_none());
3021 exercise_bridge_surface(&provider).await;
3022 exercise_hook_bridge_surface(&provider).await;
3023 let signer = make_signer();
3024 let err = get_bridge_signed_hook(
3025 &provider,
3026 &sample_request(OrderKind::Sell),
3027 GetBridgeSignedHookContext {
3028 signer: &signer,
3029 hook_gas_limit: 1_000,
3030 chain_id: cow_chains::SupportedChainId::Mainnet,
3031 deadline: 1_234,
3032 },
3033 )
3034 .await
3035 .unwrap_err();
3036 if let BridgeError::TxBuildError(msg) = err {
3037 assert!(msg.contains("nope"), "unexpected: {msg}");
3038 } else {
3039 panic!("expected TxBuildError, got {err:?}");
3040 }
3041 }
3042
3043 #[tokio::test]
3046 async fn hook_branch_produces_real_hook_when_signer_provided() {
3047 let provider = SigningCaptureProvider {
3048 info: hook_info(),
3049 tokens: vec![usdc()],
3050 bridge_response: sample_bridge_response("with-signer"),
3051 unsigned_call: build_unsigned_call(),
3052 captured_nonce: std::sync::OnceLock::new(),
3053 captured_deadline: std::sync::OnceLock::new(),
3054 captured_gas: std::sync::OnceLock::new(),
3055 };
3056 let quoter =
3057 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
3058 let signer = std::sync::Arc::new(make_signer());
3059 let params = GetQuoteWithBridgeParams {
3060 swap_and_bridge_request: sample_request(OrderKind::Sell),
3061 slippage_bps: 50,
3062 advanced_settings_metadata: None,
3063 quote_signer: Some(std::sync::Arc::clone(&signer)),
3064 hook_deadline: Some(5_000_000),
3065 };
3066
3067 get_quote_with_hook_bridge(&provider, ¶ms, "er).await.unwrap();
3068
3069 assert_eq!(*provider.captured_deadline.get().unwrap(), 5_000_000);
3072 }
3073
3074 #[tokio::test]
3075 async fn hook_branch_defaults_deadline_to_u32_max_when_unset() {
3076 let provider = SigningCaptureProvider {
3077 info: hook_info(),
3078 tokens: vec![usdc()],
3079 bridge_response: sample_bridge_response("default-deadline"),
3080 unsigned_call: build_unsigned_call(),
3081 captured_nonce: std::sync::OnceLock::new(),
3082 captured_deadline: std::sync::OnceLock::new(),
3083 captured_gas: std::sync::OnceLock::new(),
3084 };
3085 let quoter =
3086 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
3087 let signer = std::sync::Arc::new(make_signer());
3088 let params = GetQuoteWithBridgeParams {
3089 swap_and_bridge_request: sample_request(OrderKind::Sell),
3090 slippage_bps: 50,
3091 advanced_settings_metadata: None,
3092 quote_signer: Some(std::sync::Arc::clone(&signer)),
3093 hook_deadline: None,
3094 };
3095
3096 get_quote_with_hook_bridge(&provider, ¶ms, "er).await.unwrap();
3097 assert_eq!(*provider.captured_deadline.get().unwrap(), u64::from(u32::MAX));
3098 }
3099
3100 async fn exercise_bridge_surface(provider: &dyn BridgeProvider) {
3108 assert_eq!(provider.name(), provider.info().name);
3109 _ = provider.supports_route(1, 2);
3110 _ = provider.get_networks().await;
3111 _ = provider
3112 .get_buy_tokens(BuyTokensParams {
3113 sell_chain_id: 1,
3114 buy_chain_id: 10,
3115 sell_token_address: None,
3116 })
3117 .await;
3118 _ = provider.get_intermediate_tokens(&sample_request(OrderKind::Sell)).await;
3119 _ = provider.get_quote(&sample_request(OrderKind::Sell)).await;
3120 let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
3121 _ = provider.get_bridging_params(1, &order, B256::ZERO, None).await;
3122 _ = provider.get_explorer_url("bridging-id");
3123 _ = provider.get_status("bridging-id", 1).await;
3124 }
3125
3126 async fn exercise_hook_bridge_surface(provider: &dyn HookBridgeProvider) {
3127 let req = sample_request(OrderKind::Sell);
3128 let quote = sample_bridge_response("surface");
3129 _ = provider.get_unsigned_bridge_call(&req, "e).await;
3130 _ = provider.get_gas_limit_estimation_for_hook(true, Some(0), Some(0)).await;
3131 let signer = make_signer();
3132 let call = build_unsigned_call();
3133 _ = provider
3134 .get_signed_hook(cow_chains::SupportedChainId::Mainnet, &call, "nonce", 0, 0, &signer)
3135 .await;
3136 }
3137
3138 #[tokio::test]
3139 async fn mock_hook_provider_surface_is_callable() {
3140 let provider = MockHookProvider {
3141 info: hook_info(),
3142 tokens: vec![usdc()],
3143 bridge_response: sample_bridge_response("mock-hook"),
3144 unsigned_call: build_unsigned_call(),
3145 gas_limit: 500_000,
3146 };
3147 exercise_bridge_surface(&provider).await;
3148 assert!(provider.as_hook_bridge_provider().is_some());
3149 assert!(provider.as_receiver_account_bridge_provider().is_none());
3150 let signer = make_signer();
3152 let call = build_unsigned_call();
3153 let err = HookBridgeProvider::get_signed_hook(
3154 &provider,
3155 cow_chains::SupportedChainId::Mainnet,
3156 &call,
3157 "nonce",
3158 0,
3159 0,
3160 &signer,
3161 )
3162 .await
3163 .unwrap_err();
3164 assert!(matches!(err, cow_errors::CowError::Signing(_)));
3165 }
3166
3167 #[tokio::test]
3168 async fn mock_receiver_provider_surface_is_callable() {
3169 let provider = MockReceiverProvider {
3170 info: receiver_info(),
3171 tokens: vec![usdc()],
3172 bridge_response: sample_bridge_response("mock-receiver"),
3173 deposit_address: "0xDEA00DEA00DEA00DEA00DEA00DEA00DEA00DEA000".into(),
3174 };
3175 exercise_bridge_surface(&provider).await;
3176 assert!(provider.as_receiver_account_bridge_provider().is_some());
3177 assert!(provider.as_hook_bridge_provider().is_none());
3178 }
3179
3180 #[tokio::test]
3181 async fn mock_unknown_provider_surface_is_callable() {
3182 let provider = MockUnknownProvider { info: hook_info() };
3183 exercise_bridge_surface(&provider).await;
3184 assert!(provider.as_hook_bridge_provider().is_none());
3185 assert!(provider.as_receiver_account_bridge_provider().is_none());
3186 }
3187
3188 #[tokio::test]
3189 async fn signing_capture_provider_surface_is_callable() {
3190 let provider = SigningCaptureProvider {
3191 info: hook_info(),
3192 tokens: vec![usdc()],
3193 bridge_response: sample_bridge_response("sig-surface"),
3194 unsigned_call: build_unsigned_call(),
3195 captured_nonce: std::sync::OnceLock::new(),
3196 captured_deadline: std::sync::OnceLock::new(),
3197 captured_gas: std::sync::OnceLock::new(),
3198 };
3199 exercise_bridge_surface(&provider).await;
3200 assert!(provider.as_hook_bridge_provider().is_some());
3201 }
3202
3203 #[test]
3206 fn get_quote_with_bridge_params_debug_is_concise() {
3207 let params = hook_params_with_metadata(Some(serde_json::json!({"k": "v"})));
3208 let dbg = format!("{params:?}");
3209 assert!(dbg.starts_with("GetQuoteWithBridgeParams"));
3210 assert!(dbg.contains("slippage_bps"));
3211 let signer = std::sync::Arc::new(make_signer());
3213 let mut with_signer = hook_params_with_metadata(None);
3214 with_signer.quote_signer = Some(signer);
3215 let dbg_signed = format!("{with_signer:?}");
3216 assert!(dbg_signed.contains("quote_signer: true"));
3217 }
3218
3219 #[cfg(feature = "native")]
3220 #[tokio::test]
3221 async fn create_bridge_request_timeout_reports_prefix_and_duration() {
3222 let err = create_bridge_request_timeout(1, "TestProvider").await;
3223 let msg = err.to_string();
3224 assert!(msg.contains("TestProvider"));
3225 assert!(msg.contains("1ms"));
3226 }
3227
3228 #[cfg(feature = "native")]
3229 #[tokio::test]
3230 async fn fetch_multi_quote_runs_each_provider_and_sorts() {
3231 let mut sdk = BridgingSdk::new();
3232 sdk.add_provider(MockHookProvider {
3233 info: hook_info(),
3234 tokens: vec![usdc()],
3235 bridge_response: QuoteBridgeResponse {
3236 provider: "mock-hook".into(),
3237 sell_amount: U256::from(1_000u64),
3238 buy_amount: U256::from(900u64),
3239 fee_amount: U256::from(10u64),
3240 estimated_secs: 0,
3241 bridge_hook: None,
3242 },
3243 unsigned_call: build_unsigned_call(),
3244 gas_limit: 500_000,
3245 });
3246 sdk.add_provider(MockReceiverProvider {
3247 info: receiver_info(),
3248 tokens: vec![usdc()],
3249 bridge_response: QuoteBridgeResponse {
3250 provider: "mock-receiver".into(),
3251 sell_amount: U256::from(1_000u64),
3252 buy_amount: U256::from(950u64),
3253 fee_amount: U256::ZERO,
3254 estimated_secs: 0,
3255 bridge_hook: None,
3256 },
3257 deposit_address: "0x0".into(),
3258 });
3259 let results = fetch_multi_quote(&sdk, &sample_request(OrderKind::Sell), Some(20_000)).await;
3260 assert_eq!(results.len(), 2);
3261 assert_eq!(results[0].provider_dapp_id, "mock-receiver");
3263 assert_eq!(results[1].provider_dapp_id, "mock-hook");
3264 }
3265
3266 #[cfg(feature = "native")]
3267 #[tokio::test]
3268 async fn fetch_multi_quote_captures_provider_errors() {
3269 struct AlwaysFails {
3270 info: BridgeProviderInfo,
3271 }
3272 impl BridgeProvider for AlwaysFails {
3273 fn info(&self) -> &BridgeProviderInfo {
3274 &self.info
3275 }
3276 fn supports_route(&self, _: u64, _: u64) -> bool {
3277 true
3278 }
3279 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
3280 Box::pin(async { Ok(Vec::new()) })
3281 }
3282 fn get_buy_tokens<'a>(&'a self, _: BuyTokensParams) -> BuyTokensFuture<'a> {
3283 let info = self.info.clone();
3284 Box::pin(
3285 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
3286 )
3287 }
3288 fn get_intermediate_tokens<'a>(
3289 &'a self,
3290 _: &'a QuoteBridgeRequest,
3291 ) -> IntermediateTokensFuture<'a> {
3292 Box::pin(async { Ok(Vec::new()) })
3293 }
3294 fn get_quote<'a>(&'a self, _: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
3295 Box::pin(async {
3296 Err(cow_errors::CowError::Api { status: 500, body: "nope".into() })
3297 })
3298 }
3299 fn get_bridging_params<'a>(
3300 &'a self,
3301 _: u64,
3302 _: &'a cow_orderbook::types::Order,
3303 _: B256,
3304 _: Option<Address>,
3305 ) -> BridgingParamsFuture<'a> {
3306 Box::pin(async { Ok(None) })
3307 }
3308 fn get_explorer_url(&self, _: &str) -> String {
3309 String::new()
3310 }
3311 fn get_status<'a>(&'a self, _: &'a str, _: u64) -> BridgeStatusFuture<'a> {
3312 Box::pin(async { Ok(BridgeStatusResult::new(BridgeStatus::Unknown)) })
3313 }
3314 }
3315
3316 let provider_for_surface = AlwaysFails { info: hook_info() };
3317 exercise_bridge_surface(&provider_for_surface).await;
3318 let mut sdk = BridgingSdk::new();
3319 sdk.add_provider(AlwaysFails { info: hook_info() });
3320 let results = fetch_multi_quote(&sdk, &sample_request(OrderKind::Sell), None).await;
3321 assert_eq!(results.len(), 1);
3322 assert!(results[0].quote.is_none());
3323 assert!(results[0].error.as_deref().is_some_and(|e| e.contains("500")));
3324 }
3325
3326 #[cfg(feature = "native")]
3327 #[tokio::test]
3328 async fn execute_provider_quotes_reports_timeout_for_all_providers() {
3329 struct Slow {
3331 info: BridgeProviderInfo,
3332 }
3333 impl BridgeProvider for Slow {
3334 fn info(&self) -> &BridgeProviderInfo {
3335 &self.info
3336 }
3337 fn supports_route(&self, _: u64, _: u64) -> bool {
3338 true
3339 }
3340 fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
3341 Box::pin(async { Ok(Vec::new()) })
3342 }
3343 fn get_buy_tokens<'a>(&'a self, _: BuyTokensParams) -> BuyTokensFuture<'a> {
3344 let info = self.info.clone();
3345 Box::pin(
3346 async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
3347 )
3348 }
3349 fn get_intermediate_tokens<'a>(
3350 &'a self,
3351 _: &'a QuoteBridgeRequest,
3352 ) -> IntermediateTokensFuture<'a> {
3353 Box::pin(async { Ok(Vec::new()) })
3354 }
3355 #[cfg_attr(coverage_nightly, coverage(off))]
3359 fn get_quote<'a>(&'a self, _: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
3360 Box::pin(async {
3361 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
3363 Ok(QuoteBridgeResponse {
3364 provider: "slow".into(),
3365 sell_amount: U256::ZERO,
3366 buy_amount: U256::ZERO,
3367 fee_amount: U256::ZERO,
3368 estimated_secs: 0,
3369 bridge_hook: None,
3370 })
3371 })
3372 }
3373 fn get_bridging_params<'a>(
3374 &'a self,
3375 _: u64,
3376 _: &'a cow_orderbook::types::Order,
3377 _: B256,
3378 _: Option<Address>,
3379 ) -> BridgingParamsFuture<'a> {
3380 Box::pin(async { Ok(None) })
3381 }
3382 fn get_explorer_url(&self, _: &str) -> String {
3383 String::new()
3384 }
3385 fn get_status<'a>(&'a self, _: &'a str, _: u64) -> BridgeStatusFuture<'a> {
3386 Box::pin(async { Ok(BridgeStatusResult::new(BridgeStatus::Unknown)) })
3387 }
3388 }
3389
3390 let slow_for_surface = Slow { info: hook_info() };
3392 _ = slow_for_surface.info();
3393 _ = slow_for_surface.supports_route(1, 2);
3394 _ = slow_for_surface.get_networks().await;
3395 _ = slow_for_surface
3396 .get_buy_tokens(BuyTokensParams {
3397 sell_chain_id: 1,
3398 buy_chain_id: 10,
3399 sell_token_address: None,
3400 })
3401 .await;
3402 _ = slow_for_surface.get_intermediate_tokens(&sample_request(OrderKind::Sell)).await;
3403 let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
3404 _ = slow_for_surface.get_bridging_params(1, &order, B256::ZERO, None).await;
3405 _ = slow_for_surface.get_explorer_url("id");
3406 _ = slow_for_surface.get_status("id", 1).await;
3407
3408 let mut sdk = BridgingSdk::new();
3409 sdk.add_provider(Slow { info: hook_info() });
3410 let results = execute_provider_quotes(&sdk, &sample_request(OrderKind::Sell), 5).await;
3411 assert_eq!(results.len(), 1);
3412 let err = results[0].error.as_deref().unwrap_or_default();
3413 assert!(err.contains("Multi-quote timeout"), "unexpected err: {err}");
3414 }
3415
3416 #[test]
3417 fn safe_call_best_quote_callback_swallows_panic() {
3418 let result = MultiQuoteResult { provider_dapp_id: "cb".into(), quote: None, error: None };
3419 safe_call_best_quote_callback::<fn(&MultiQuoteResult)>(None, &result);
3421 safe_call_best_quote_callback(Some(|_r: &MultiQuoteResult| panic!("boom")), &result);
3422 }
3423
3424 #[test]
3425 fn safe_call_progressive_callback_swallows_panic() {
3426 let result = MultiQuoteResult { provider_dapp_id: "cb".into(), quote: None, error: None };
3427 safe_call_progressive_callback::<fn(&MultiQuoteResult)>(None, &result);
3428 safe_call_progressive_callback(Some(|_r: &MultiQuoteResult| panic!("boom")), &result);
3429 }
3430
3431 #[tokio::test]
3432 async fn get_quote_with_hook_bridge_errors_on_unsupported_chain_id() {
3433 let provider = MockHookProvider {
3435 info: hook_info(),
3436 tokens: vec![usdc()],
3437 bridge_response: sample_bridge_response("mock-hook"),
3438 unsigned_call: build_unsigned_call(),
3439 gas_limit: 500_000,
3440 };
3441 let quoter =
3442 FixedQuoter { outcome: sample_outcome(), captured: std::sync::OnceLock::new() };
3443 let mut req = sample_request(OrderKind::Sell);
3444 req.sell_chain_id = 999_999;
3445 let signer = std::sync::Arc::new(make_signer());
3446 let params = GetQuoteWithBridgeParams {
3447 swap_and_bridge_request: req,
3448 slippage_bps: 50,
3449 advanced_settings_metadata: None,
3450 quote_signer: Some(signer),
3451 hook_deadline: Some(1_234),
3452 };
3453 let err = get_quote_with_hook_bridge(&provider, ¶ms, "er).await.unwrap_err();
3454 if let BridgeError::TxBuildError(msg) = err {
3455 assert!(msg.contains("unsupported sell_chain_id"), "unexpected err: {msg}",);
3456 } else {
3457 panic!("expected TxBuildError, got {err:?}");
3458 }
3459 }
3460}
3461
3462#[cfg(test)]
3463#[allow(clippy::tests_outside_test_module, reason = "inner module pattern")]
3464mod miscellaneous_coverage_tests {
3465 use alloy_primitives::Address;
3466
3467 #[test]
3468 fn test_helpers_expose_wallet_and_address() {
3469 use super::test_helpers::{get_wallet, test_address};
3470
3471 let wallet = get_wallet();
3472 assert_eq!(alloy_signer::Signer::address(&wallet), test_address());
3474 let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse().unwrap();
3476 assert_eq!(test_address(), expected);
3477 }
3478
3479 #[test]
3480 fn intermediate_not_in_candidates_err_returns_tx_build_error() {
3481 use crate::types::BridgeError;
3482 let err = super::intermediate_not_in_candidates_err();
3483 assert!(
3484 matches!(&err, BridgeError::TxBuildError(m) if m.contains("not in candidates")),
3485 "got {err:?}"
3486 );
3487 }
3488
3489 #[test]
3490 fn intermediate_must_be_evm_err_returns_tx_build_error() {
3491 use crate::types::BridgeError;
3492 let err = super::intermediate_must_be_evm_err();
3493 assert!(
3494 matches!(&err, BridgeError::TxBuildError(m) if m.contains("must be EVM")),
3495 "got {err:?}"
3496 );
3497 }
3498}
3499
3500#[cfg(test)]
3501#[allow(clippy::tests_outside_test_module, reason = "inner module pattern")]
3502mod cross_chain_order_tests {
3503 use crate::{
3504 across::{EvmLogEntry, get_cow_trade_events},
3505 sdk::{GetCrossChainOrderParams, get_cross_chain_order},
3506 };
3507 use alloy_primitives::{Address, B256, U256, keccak256};
3508
3509 fn pad_u256(value: U256) -> [u8; 32] {
3510 value.to_be_bytes::<32>()
3511 }
3512
3513 fn left_pad_address(addr: Address) -> [u8; 32] {
3514 let mut buf = [0u8; 32];
3515 buf[12..32].copy_from_slice(addr.as_slice());
3516 buf
3517 }
3518
3519 struct DepositLogArgs {
3520 spoke_pool: Address,
3521 destination_chain: u64,
3522 deposit_id: u64,
3523 depositor: Address,
3524 recipient: Address,
3525 sell_token: Address,
3526 buy_token: Address,
3527 }
3528
3529 fn deposit_event_log(args: DepositLogArgs) -> EvmLogEntry {
3530 let DepositLogArgs {
3531 spoke_pool,
3532 destination_chain,
3533 deposit_id,
3534 depositor,
3535 recipient,
3536 sell_token,
3537 buy_token,
3538 } = args;
3539 let topic0 = keccak256(
3542 "FundsDeposited(bytes32,bytes32,uint256,uint256,uint256,uint256,uint32,uint32,uint32,bytes32,bytes32,bytes32,bytes)",
3543 );
3544 let topics = vec![
3545 topic0,
3546 B256::from(pad_u256(U256::from(destination_chain))),
3547 B256::from(pad_u256(U256::from(deposit_id))),
3548 B256::from(left_pad_address(depositor)),
3549 ];
3550 let mut data = Vec::with_capacity(9 * 32);
3551 data.extend_from_slice(&left_pad_address(sell_token)); data.extend_from_slice(&left_pad_address(buy_token)); data.extend_from_slice(&pad_u256(U256::from(1_000_000u64))); data.extend_from_slice(&pad_u256(U256::from(999_500u64))); data.extend_from_slice(&pad_u256(U256::from(1_700_000_000u64))); data.extend_from_slice(&pad_u256(U256::from(1_700_999_999u64))); data.extend_from_slice(&pad_u256(U256::ZERO)); data.extend_from_slice(&left_pad_address(recipient)); data.extend_from_slice(&left_pad_address(Address::ZERO)); EvmLogEntry { address: spoke_pool, topics, data }
3562 }
3563
3564 fn cow_trade_log(
3565 settlement: Address,
3566 owner: Address,
3567 sell_token: Address,
3568 buy_token: Address,
3569 order_uid_bytes: &[u8],
3570 ) -> EvmLogEntry {
3571 let topic0 = keccak256("Trade(address,address,address,uint256,uint256,uint256,bytes)");
3572 let topics = vec![topic0, B256::from(left_pad_address(owner))];
3573 let uid_len = order_uid_bytes.len();
3575 let uid_padded_len = uid_len.div_ceil(32) * 32;
3576 let mut data = Vec::with_capacity(7 * 32 + uid_padded_len);
3577 data.extend_from_slice(&left_pad_address(sell_token)); data.extend_from_slice(&left_pad_address(buy_token)); data.extend_from_slice(&pad_u256(U256::from(1_000_000u64))); data.extend_from_slice(&pad_u256(U256::from(999_500u64))); data.extend_from_slice(&pad_u256(U256::ZERO)); data.extend_from_slice(&pad_u256(U256::from(6 * 32u64)));
3584 data.extend_from_slice(&pad_u256(U256::from(uid_len as u64)));
3586 data.extend_from_slice(order_uid_bytes);
3587 if !uid_len.is_multiple_of(32) {
3589 data.extend(vec![0u8; 32 - uid_len % 32]);
3590 }
3591 EvmLogEntry { address: settlement, topics, data }
3592 }
3593
3594 #[test]
3595 fn get_cross_chain_order_builds_order_when_logs_match() {
3596 use cow_chains::SupportedChainId;
3600 let chain_id = SupportedChainId::Mainnet.as_u64();
3601 let spoke_pool = crate::across::across_spoke_pool_addresses()[&chain_id];
3602 let settlement = cow_chains::settlement_contract(SupportedChainId::Mainnet);
3603
3604 let depositor = Address::repeat_byte(0x11);
3605 let recipient = Address::repeat_byte(0x22);
3606 let sell_token = Address::repeat_byte(0x33);
3607 let buy_token = Address::repeat_byte(0x44);
3608 let order_uid_bytes: Vec<u8> = (0..56u8).collect(); let order_uid_hex = format!("0x{}", alloy_primitives::hex::encode(&order_uid_bytes));
3610
3611 let logs = vec![
3612 cow_trade_log(settlement, depositor, sell_token, buy_token, &order_uid_bytes),
3613 deposit_event_log(DepositLogArgs {
3614 spoke_pool,
3615 destination_chain: 42_161,
3616 deposit_id: 7,
3617 depositor,
3618 recipient,
3619 sell_token,
3620 buy_token,
3621 }),
3622 ];
3623 let trades = get_cow_trade_events(chain_id, &logs, None);
3625 assert_eq!(trades.len(), 1, "expected one trade event, got {trades:?}");
3626 assert_eq!(trades[0].order_uid, order_uid_hex);
3627
3628 let params = GetCrossChainOrderParams {
3629 chain_id,
3630 order_id: order_uid_hex,
3631 full_app_data: None,
3632 trade_tx_hash: "0xdeadbeef".to_owned(),
3633 logs: &logs,
3634 settlement_override: None,
3635 };
3636 let order = get_cross_chain_order(¶ms).expect("logs cover both event types");
3637 assert_eq!(order.chain_id, chain_id);
3638 assert_eq!(order.trade_tx_hash, "0xdeadbeef");
3639 assert!(order.explorer_url.is_none());
3640 assert_eq!(order.bridging_params.source_chain_id, chain_id);
3641 assert_eq!(order.bridging_params.destination_chain_id, 42_161);
3642 assert_eq!(order.bridging_params.bridging_id, "7");
3643 assert_eq!(order.bridging_params.recipient, recipient);
3644 }
3645}