1use foldhash::HashMap;
4
5use alloy_primitives::{Address, U256};
6use serde::{Deserialize, Serialize};
7
8use cow_types::CowHook;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum BridgeProviderType {
15 HookBridgeProvider,
17 ReceiverAccountBridgeProvider,
19}
20
21impl BridgeProviderType {
22 #[must_use]
26 pub const fn is_hook_bridge_provider(self) -> bool {
27 matches!(self, Self::HookBridgeProvider)
28 }
29
30 #[must_use]
35 pub const fn is_receiver_account_bridge_provider(self) -> bool {
36 matches!(self, Self::ReceiverAccountBridgeProvider)
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct BridgeProviderInfo {
43 pub name: String,
45 pub logo_url: String,
47 pub dapp_id: String,
49 pub website: String,
51 pub provider_type: BridgeProviderType,
53}
54
55impl BridgeProviderInfo {
56 #[must_use]
66 pub const fn is_hook_bridge_provider(&self) -> bool {
67 self.provider_type.is_hook_bridge_provider()
68 }
69
70 #[must_use]
80 pub const fn is_receiver_account_bridge_provider(&self) -> bool {
81 self.provider_type.is_receiver_account_bridge_provider()
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
89pub enum BridgeStatus {
90 InProgress,
92 Executed,
94 Expired,
96 Refund,
98 Unknown,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct BridgeStatusResult {
105 pub status: BridgeStatus,
107 pub fill_time_in_seconds: Option<u64>,
109 pub deposit_tx_hash: Option<String>,
111 pub fill_tx_hash: Option<String>,
113}
114
115impl BridgeStatusResult {
116 #[must_use]
129 pub const fn new(status: BridgeStatus) -> Self {
130 Self { status, fill_time_in_seconds: None, deposit_tx_hash: None, fill_tx_hash: None }
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[serde(tag = "kind", content = "value")]
149pub enum TokenAddress {
150 Evm(Address),
152 Raw(String),
156}
157
158impl TokenAddress {
159 #[must_use]
161 pub const fn as_evm(&self) -> Option<&Address> {
162 match self {
163 Self::Evm(a) => Some(a),
164 Self::Raw(_) => None,
165 }
166 }
167
168 #[must_use]
172 pub const fn to_evm(&self) -> Option<Address> {
173 match self {
174 Self::Evm(a) => Some(*a),
175 Self::Raw(_) => None,
176 }
177 }
178
179 #[must_use]
181 pub const fn as_raw(&self) -> Option<&str> {
182 match self {
183 Self::Evm(_) => None,
184 Self::Raw(s) => Some(s.as_str()),
185 }
186 }
187
188 #[must_use]
190 pub const fn is_evm(&self) -> bool {
191 matches!(self, Self::Evm(_))
192 }
193
194 #[must_use]
196 pub const fn is_raw(&self) -> bool {
197 matches!(self, Self::Raw(_))
198 }
199}
200
201impl From<Address> for TokenAddress {
202 fn from(value: Address) -> Self {
203 Self::Evm(value)
204 }
205}
206
207impl From<&Address> for TokenAddress {
208 fn from(value: &Address) -> Self {
209 Self::Evm(*value)
210 }
211}
212
213impl PartialEq<Address> for TokenAddress {
214 fn eq(&self, other: &Address) -> bool {
215 matches!(self, Self::Evm(a) if a == other)
216 }
217}
218
219impl PartialEq<TokenAddress> for Address {
220 fn eq(&self, other: &TokenAddress) -> bool {
221 other == self
222 }
223}
224
225#[derive(Debug, Clone)]
229pub struct QuoteBridgeRequest {
230 pub sell_chain_id: u64,
232 pub buy_chain_id: u64,
234 pub sell_token: Address,
236 pub sell_token_decimals: u8,
238 pub buy_token: TokenAddress,
240 pub buy_token_decimals: u8,
242 pub sell_amount: U256,
244 pub account: Address,
246 pub owner: Option<Address>,
248 pub receiver: Option<String>,
250 pub bridge_recipient: Option<String>,
252 pub slippage_bps: u32,
254 pub bridge_slippage_bps: Option<u32>,
256 pub kind: cow_types::OrderKind,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct BridgeAmounts {
263 pub sell_amount: U256,
265 pub buy_amount: U256,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct BridgeCosts {
272 pub bridging_fee: BridgingFee,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct BridgingFee {
279 pub fee_bps: u32,
281 pub amount_in_sell_currency: U256,
283 pub amount_in_buy_currency: U256,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct BridgeQuoteAmountsAndCosts {
290 pub costs: BridgeCosts,
292 pub before_fee: BridgeAmounts,
294 pub after_fee: BridgeAmounts,
296 pub after_slippage: BridgeAmounts,
298 pub slippage_bps: u32,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct BridgeLimits {
305 pub min_deposit: U256,
307 pub max_deposit: U256,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct BridgeFees {
314 pub bridge_fee: U256,
316 pub destination_gas_fee: U256,
318}
319
320#[derive(Debug, Clone)]
322pub struct QuoteBridgeResponse {
323 pub provider: String,
325 pub sell_amount: U256,
327 pub buy_amount: U256,
329 pub fee_amount: U256,
331 pub estimated_secs: u64,
333 pub bridge_hook: Option<CowHook>,
335}
336
337impl QuoteBridgeResponse {
338 #[must_use]
347 pub const fn has_bridge_hook(&self) -> bool {
348 self.bridge_hook.is_some()
349 }
350
351 #[must_use]
357 pub fn provider_ref(&self) -> &str {
358 &self.provider
359 }
360
361 #[must_use]
365 pub const fn net_buy_amount(&self) -> U256 {
366 self.buy_amount.saturating_sub(self.fee_amount)
367 }
368}
369
370#[derive(Debug, Clone)]
372pub struct BridgeQuoteResult {
373 pub id: Option<String>,
375 pub signature: Option<String>,
377 pub attestation_signature: Option<String>,
379 pub quote_body: Option<String>,
381 pub is_sell: bool,
383 pub amounts_and_costs: BridgeQuoteAmountsAndCosts,
385 pub expected_fill_time_seconds: Option<u64>,
387 pub quote_timestamp: u64,
389 pub fees: BridgeFees,
391 pub limits: BridgeLimits,
393}
394
395#[derive(Debug, Clone)]
397pub struct BridgeQuoteResults {
398 pub provider_info: BridgeProviderInfo,
400 pub quote: BridgeQuoteResult,
402 pub bridge_call_details: Option<BridgeCallDetails>,
404 pub bridge_receiver_override: Option<String>,
406}
407
408#[derive(Debug, Clone)]
410pub struct BridgeCallDetails {
411 pub unsigned_bridge_call: cow_chains::EvmCall,
413 pub pre_authorized_bridging_hook: BridgeHook,
415}
416
417#[derive(Debug, Clone)]
419pub struct BridgeHook {
420 pub post_hook: CowHook,
422 pub recipient: String,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct BridgingDepositParams {
429 pub input_token_address: Address,
431 pub output_token_address: Address,
433 pub input_amount: U256,
435 pub output_amount: Option<U256>,
437 pub owner: Address,
439 pub quote_timestamp: Option<u64>,
441 pub fill_deadline: Option<u64>,
443 pub recipient: Address,
445 pub source_chain_id: u64,
447 pub destination_chain_id: u64,
449 pub bridging_id: String,
451}
452
453#[derive(Debug, Clone)]
455pub struct CrossChainOrder {
456 pub chain_id: u64,
458 pub status_result: BridgeStatusResult,
460 pub bridging_params: BridgingDepositParams,
462 pub trade_tx_hash: String,
464 pub explorer_url: Option<String>,
466}
467
468#[derive(Debug, Clone)]
470pub struct MultiQuoteResult {
471 pub provider_dapp_id: String,
473 pub quote: Option<BridgeQuoteAmountsAndCosts>,
475 pub error: Option<String>,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489pub struct IntermediateTokenInfo {
490 pub chain_id: u64,
492 pub address: TokenAddress,
496 pub decimals: u8,
498 pub symbol: String,
500 pub name: String,
502 pub logo_url: Option<String>,
504}
505
506#[derive(Debug, Clone)]
514pub struct BuyTokensParams {
515 pub sell_chain_id: u64,
517 pub buy_chain_id: u64,
519 pub sell_token_address: Option<Address>,
522}
523
524#[derive(Debug, Clone)]
528pub struct GetProviderBuyTokens {
529 pub provider_info: BridgeProviderInfo,
532 pub tokens: Vec<IntermediateTokenInfo>,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct BridgeDeposit {
547 pub provider_id: String,
549 pub source_chain_id: u64,
551 pub destination_chain_id: u64,
553 pub bridging_id: String,
555 pub deposit_tx_hash: String,
557 pub fill_tx_hash: Option<String>,
559 pub amount_in: U256,
561 pub amount_out: Option<U256>,
564}
565
566#[derive(Debug, thiserror::Error)]
570pub enum BridgeError {
571 #[error("no providers available")]
573 NoProviders,
574 #[error("no quote available for this route")]
576 NoQuote,
577 #[error("sell and buy chains must be different for cross-chain bridging")]
579 SameChain,
580 #[error("bridging only supports SELL orders")]
582 OnlySellOrderSupported,
583 #[error("no intermediate tokens available")]
585 NoIntermediateTokens,
586 #[error("bridge API error: {0}")]
588 ApiError(String),
589 #[error("invalid API JSON response: {0}")]
591 InvalidApiResponse(String),
592 #[error("transaction build error: {0}")]
594 TxBuildError(String),
595 #[error("quote error: {0}")]
597 QuoteError(String),
598 #[error("no routes available")]
600 NoRoutes,
601 #[error("invalid bridge: {0}")]
603 InvalidBridge(String),
604 #[error("quote does not match deposit address")]
606 QuoteDoesNotMatchDepositAddress,
607 #[error("sell amount too small")]
609 SellAmountTooSmall,
610 #[error("provider not found: {dapp_id}")]
612 ProviderNotFound {
613 dapp_id: String,
615 },
616 #[error("provider request timed out")]
618 Timeout,
619 #[error(transparent)]
621 Cow(#[from] cow_errors::CowError),
622}
623
624#[must_use]
640pub const fn bridge_error_priority(error: &BridgeError) -> u32 {
641 match error {
642 BridgeError::SellAmountTooSmall => 10,
643 BridgeError::OnlySellOrderSupported => 9,
644 BridgeError::NoProviders |
645 BridgeError::NoQuote |
646 BridgeError::SameChain |
647 BridgeError::NoIntermediateTokens |
648 BridgeError::ApiError(_) |
649 BridgeError::InvalidApiResponse(_) |
650 BridgeError::TxBuildError(_) |
651 BridgeError::QuoteError(_) |
652 BridgeError::NoRoutes |
653 BridgeError::InvalidBridge(_) |
654 BridgeError::QuoteDoesNotMatchDepositAddress |
655 BridgeError::ProviderNotFound { .. } |
656 BridgeError::Timeout |
657 BridgeError::Cow(_) => 1,
658 }
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct AcrossPctFee {
668 pub pct: String,
670 pub total: String,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct AcrossSuggestedFeesLimits {
677 pub min_deposit: String,
679 pub max_deposit: String,
681 pub max_deposit_instant: String,
683 pub max_deposit_short_delay: String,
685 pub recommended_deposit_instant: String,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct AcrossSuggestedFeesResponse {
692 pub total_relay_fee: AcrossPctFee,
694 pub relayer_capital_fee: AcrossPctFee,
696 pub relayer_gas_fee: AcrossPctFee,
698 pub lp_fee: AcrossPctFee,
700 pub timestamp: String,
702 pub is_amount_too_low: bool,
704 pub quote_block: String,
706 pub spoke_pool_address: String,
708 pub exclusive_relayer: String,
710 pub exclusivity_deadline: String,
712 pub estimated_fill_time_sec: String,
714 pub fill_deadline: String,
716 pub limits: AcrossSuggestedFeesLimits,
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
722#[serde(rename_all = "camelCase")]
723pub enum AcrossDepositStatus {
724 Filled,
726 SlowFillRequested,
728 Pending,
730 Expired,
732 Refunded,
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
738pub struct AcrossDepositStatusResponse {
739 pub status: AcrossDepositStatus,
741 pub origin_chain_id: String,
743 pub deposit_id: String,
745 pub deposit_tx_hash: Option<String>,
747 pub fill_tx: Option<String>,
749 pub destination_chain_id: Option<String>,
751 pub deposit_refund_tx_hash: Option<String>,
753}
754
755#[derive(Debug, Clone)]
757pub struct AcrossDepositEvent {
758 pub input_token: Address,
760 pub output_token: Address,
762 pub input_amount: U256,
764 pub output_amount: U256,
766 pub destination_chain_id: u64,
768 pub deposit_id: U256,
770 pub quote_timestamp: u32,
772 pub fill_deadline: u32,
774 pub exclusivity_deadline: u32,
776 pub depositor: Address,
778 pub recipient: Address,
780 pub exclusive_relayer: Address,
782}
783
784#[derive(Debug, Clone)]
786pub struct AcrossChainConfig {
787 pub chain_id: u64,
789 pub tokens: HashMap<String, Address>,
791}
792
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
797pub enum BungeeBridge {
798 Across,
800 CircleCctp,
802 GnosisNative,
804}
805
806impl BungeeBridge {
807 #[must_use]
825 pub const fn as_str(&self) -> &'static str {
826 match self {
827 Self::Across => "across",
828 Self::CircleCctp => "cctp",
829 Self::GnosisNative => "gnosis-native-bridge",
830 }
831 }
832
833 #[must_use]
847 pub fn from_display_name(name: &str) -> Option<Self> {
848 match name {
849 "Across" => Some(Self::Across),
850 "Circle CCTP" => Some(Self::CircleCctp),
851 "Gnosis Native" => Some(Self::GnosisNative),
852 _ => None,
853 }
854 }
855
856 #[must_use]
865 pub const fn display_name(&self) -> &'static str {
866 match self {
867 Self::Across => "Across",
868 Self::CircleCctp => "Circle CCTP",
869 Self::GnosisNative => "Gnosis Native",
870 }
871 }
872}
873
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
876#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
877pub enum BungeeEventStatus {
878 Completed,
880 Pending,
882}
883
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
886#[serde(rename_all = "lowercase")]
887pub enum BungeeBridgeName {
888 Across,
890 Cctp,
892}
893
894#[derive(Debug, Clone, Serialize, Deserialize)]
896pub struct BungeeEvent {
897 pub identifier: String,
899 pub src_transaction_hash: Option<String>,
901 pub bridge_name: BungeeBridgeName,
903 pub from_chain_id: u64,
905 pub is_cowswap_trade: bool,
907 pub order_id: String,
909 pub src_tx_status: BungeeEventStatus,
911 pub dest_tx_status: BungeeEventStatus,
913 pub dest_transaction_hash: Option<String>,
915}
916
917#[derive(Debug, Clone, Copy)]
919pub struct BungeeTxDataBytesIndex {
920 pub bytes_start_index: usize,
922 pub bytes_length: usize,
924 pub bytes_string_start_index: usize,
926 pub bytes_string_length: usize,
928}
929
930#[derive(Debug, Clone)]
932pub struct DecodedBungeeTxData {
933 pub route_id: String,
935 pub encoded_function_data: String,
937 pub function_selector: String,
939}
940
941#[derive(Debug, Clone)]
943pub struct DecodedBungeeAmounts {
944 pub input_amount_bytes: String,
946 pub input_amount: U256,
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953
954 #[test]
957 fn hook_bridge_provider_is_hook() {
958 assert!(BridgeProviderType::HookBridgeProvider.is_hook_bridge_provider());
959 assert!(!BridgeProviderType::HookBridgeProvider.is_receiver_account_bridge_provider());
960 }
961
962 #[test]
963 fn receiver_account_bridge_provider_is_receiver() {
964 assert!(
965 BridgeProviderType::ReceiverAccountBridgeProvider.is_receiver_account_bridge_provider()
966 );
967 assert!(!BridgeProviderType::ReceiverAccountBridgeProvider.is_hook_bridge_provider());
968 }
969
970 #[test]
973 fn bridge_provider_info_delegates_hook() {
974 let info = BridgeProviderInfo {
975 name: "test".into(),
976 logo_url: String::new(),
977 dapp_id: String::new(),
978 website: String::new(),
979 provider_type: BridgeProviderType::HookBridgeProvider,
980 };
981 assert!(info.is_hook_bridge_provider());
982 assert!(!info.is_receiver_account_bridge_provider());
983 }
984
985 #[test]
986 fn bridge_provider_info_delegates_receiver() {
987 let info = BridgeProviderInfo {
988 name: "test".into(),
989 logo_url: String::new(),
990 dapp_id: String::new(),
991 website: String::new(),
992 provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
993 };
994 assert!(info.is_receiver_account_bridge_provider());
995 assert!(!info.is_hook_bridge_provider());
996 }
997
998 #[test]
1001 fn intermediate_token_info_roundtrips_through_serde() {
1002 let token = IntermediateTokenInfo {
1003 chain_id: 1,
1004 address: Address::repeat_byte(0x11).into(),
1005 decimals: 6,
1006 symbol: "USDC".into(),
1007 name: "USD Coin".into(),
1008 logo_url: Some("https://example.com/usdc.png".into()),
1009 };
1010 let json = serde_json::to_string(&token).unwrap();
1011 let decoded: IntermediateTokenInfo = serde_json::from_str(&json).unwrap();
1012 assert_eq!(token, decoded);
1013 }
1014
1015 #[test]
1016 fn token_address_evm_and_raw_roundtrip_through_serde() {
1017 let evm = TokenAddress::Evm(Address::repeat_byte(0xaa));
1018 let raw = TokenAddress::Raw("bc1qexample000000000000000000000000000000".into());
1019 let evm_json = serde_json::to_string(&evm).unwrap();
1020 let raw_json = serde_json::to_string(&raw).unwrap();
1021 assert!(evm_json.contains("\"kind\":\"Evm\""));
1022 assert!(raw_json.contains("\"kind\":\"Raw\""));
1023 let evm_decoded: TokenAddress = serde_json::from_str(&evm_json).unwrap();
1024 let raw_decoded: TokenAddress = serde_json::from_str(&raw_json).unwrap();
1025 assert_eq!(evm, evm_decoded);
1026 assert_eq!(raw, raw_decoded);
1027 }
1028
1029 #[test]
1030 fn token_address_partial_eq_with_evm_address() {
1031 let a = Address::repeat_byte(0x11);
1032 let token: TokenAddress = a.into();
1033 assert!(token == a);
1034 assert!(a == token);
1035 let raw = TokenAddress::Raw("anything".into());
1036 assert!(raw != a);
1037 }
1038
1039 #[test]
1040 fn token_address_extractors_and_discriminants() {
1041 let a = Address::repeat_byte(0x77);
1042 let evm: TokenAddress = a.into();
1043 let raw = TokenAddress::Raw("abc".into());
1044
1045 assert_eq!(evm.to_evm(), Some(a));
1046 assert_eq!(evm.as_evm(), Some(&a));
1047 assert_eq!(evm.as_raw(), None);
1048 assert!(evm.is_evm());
1049 assert!(!evm.is_raw());
1050
1051 assert_eq!(raw.to_evm(), None);
1052 assert_eq!(raw.as_evm(), None);
1053 assert_eq!(raw.as_raw(), Some("abc"));
1054 assert!(!raw.is_evm());
1055 assert!(raw.is_raw());
1056 }
1057
1058 #[test]
1059 fn token_address_from_ref_address() {
1060 let a = Address::repeat_byte(0x55);
1061 let token: TokenAddress = (&a).into();
1062 assert_eq!(token.to_evm(), Some(a));
1063 }
1064
1065 #[test]
1066 fn buy_tokens_params_optional_sell_token() {
1067 let with_filter = BuyTokensParams {
1068 sell_chain_id: 1,
1069 buy_chain_id: 100,
1070 sell_token_address: Some(Address::repeat_byte(0x22)),
1071 };
1072 assert_eq!(with_filter.sell_chain_id, 1);
1073 assert_eq!(with_filter.buy_chain_id, 100);
1074 assert!(with_filter.sell_token_address.is_some());
1075
1076 let without_filter =
1077 BuyTokensParams { sell_chain_id: 1, buy_chain_id: 100, sell_token_address: None };
1078 assert!(without_filter.sell_token_address.is_none());
1079 }
1080
1081 #[test]
1082 fn get_provider_buy_tokens_carries_info_and_tokens() {
1083 let info = BridgeProviderInfo {
1084 name: "p".into(),
1085 logo_url: String::new(),
1086 dapp_id: "cow-sdk://bridging/providers/p".into(),
1087 website: String::new(),
1088 provider_type: BridgeProviderType::HookBridgeProvider,
1089 };
1090 let token = IntermediateTokenInfo {
1091 chain_id: 1,
1092 address: Address::ZERO.into(),
1093 decimals: 18,
1094 symbol: "WETH".into(),
1095 name: "Wrapped Ether".into(),
1096 logo_url: None,
1097 };
1098 let result = GetProviderBuyTokens { provider_info: info, tokens: vec![token] };
1099 assert_eq!(result.tokens.len(), 1);
1100 assert_eq!(result.tokens[0].symbol, "WETH");
1101 assert!(result.provider_info.is_hook_bridge_provider());
1102 }
1103
1104 #[test]
1107 fn bridge_deposit_roundtrips_through_serde() {
1108 let deposit = BridgeDeposit {
1109 provider_id: "cow-sdk://bridging/providers/across".into(),
1110 source_chain_id: 1,
1111 destination_chain_id: 10,
1112 bridging_id: "42".into(),
1113 deposit_tx_hash: "0xdead".into(),
1114 fill_tx_hash: Some("0xbeef".into()),
1115 amount_in: U256::from(1_000_000u64),
1116 amount_out: Some(U256::from(999_500u64)),
1117 };
1118 let json = serde_json::to_string(&deposit).unwrap();
1119 let decoded: BridgeDeposit = serde_json::from_str(&json).unwrap();
1120 assert_eq!(deposit, decoded);
1121 }
1122
1123 #[test]
1124 fn bridge_deposit_fill_tx_hash_optional() {
1125 let deposit = BridgeDeposit {
1126 provider_id: "p".into(),
1127 source_chain_id: 1,
1128 destination_chain_id: 10,
1129 bridging_id: "42".into(),
1130 deposit_tx_hash: "0xabc".into(),
1131 fill_tx_hash: None,
1132 amount_in: U256::from(500u64),
1133 amount_out: None,
1134 };
1135 assert!(deposit.fill_tx_hash.is_none());
1136 assert!(deposit.amount_out.is_none());
1137 }
1138
1139 #[test]
1142 fn bridge_status_variants_are_distinct() {
1143 let statuses = [
1144 BridgeStatus::InProgress,
1145 BridgeStatus::Executed,
1146 BridgeStatus::Expired,
1147 BridgeStatus::Refund,
1148 BridgeStatus::Unknown,
1149 ];
1150 for (i, a) in statuses.iter().enumerate() {
1151 for (j, b) in statuses.iter().enumerate() {
1152 if i == j {
1153 assert_eq!(a, b);
1154 } else {
1155 assert_ne!(a, b);
1156 }
1157 }
1158 }
1159 }
1160
1161 #[test]
1164 fn bridge_status_result_new_sets_status_only() {
1165 let r = BridgeStatusResult::new(BridgeStatus::Executed);
1166 assert_eq!(r.status, BridgeStatus::Executed);
1167 assert!(r.fill_time_in_seconds.is_none());
1168 assert!(r.deposit_tx_hash.is_none());
1169 assert!(r.fill_tx_hash.is_none());
1170 }
1171
1172 #[test]
1173 fn bridge_status_result_new_all_statuses() {
1174 for status in [
1175 BridgeStatus::InProgress,
1176 BridgeStatus::Executed,
1177 BridgeStatus::Expired,
1178 BridgeStatus::Refund,
1179 BridgeStatus::Unknown,
1180 ] {
1181 let r = BridgeStatusResult::new(status);
1182 assert_eq!(r.status, status);
1183 }
1184 }
1185
1186 fn make_quote(hook: Option<CowHook>, fee: U256) -> QuoteBridgeResponse {
1189 QuoteBridgeResponse {
1190 provider: "across".into(),
1191 sell_amount: U256::from(1000u64),
1192 buy_amount: U256::from(950u64),
1193 fee_amount: fee,
1194 estimated_secs: 60,
1195 bridge_hook: hook,
1196 }
1197 }
1198
1199 #[test]
1200 fn has_bridge_hook_true_when_some() {
1201 let hook = CowHook {
1202 target: "0xdead".into(),
1203 call_data: "0x".into(),
1204 gas_limit: "100000".into(),
1205 dapp_id: None,
1206 };
1207 let q = make_quote(Some(hook), U256::ZERO);
1208 assert!(q.has_bridge_hook());
1209 }
1210
1211 #[test]
1212 fn has_bridge_hook_false_when_none() {
1213 let q = make_quote(None, U256::ZERO);
1214 assert!(!q.has_bridge_hook());
1215 }
1216
1217 #[test]
1218 fn provider_ref_returns_provider_name() {
1219 let q = make_quote(None, U256::ZERO);
1220 assert_eq!(q.provider_ref(), "across");
1221 }
1222
1223 #[test]
1224 fn net_buy_amount_subtracts_fee() {
1225 let q = make_quote(None, U256::from(50u64));
1226 assert_eq!(q.net_buy_amount(), U256::from(900u64));
1227 }
1228
1229 #[test]
1230 fn net_buy_amount_saturates_at_zero() {
1231 let q = make_quote(None, U256::from(2000u64));
1232 assert_eq!(q.net_buy_amount(), U256::ZERO);
1233 }
1234
1235 #[test]
1236 fn net_buy_amount_zero_fee() {
1237 let q = make_quote(None, U256::ZERO);
1238 assert_eq!(q.net_buy_amount(), U256::from(950u64));
1239 }
1240
1241 #[test]
1244 fn bungee_bridge_as_str() {
1245 assert_eq!(BungeeBridge::Across.as_str(), "across");
1246 assert_eq!(BungeeBridge::CircleCctp.as_str(), "cctp");
1247 assert_eq!(BungeeBridge::GnosisNative.as_str(), "gnosis-native-bridge");
1248 }
1249
1250 #[test]
1251 fn bungee_bridge_display_name() {
1252 assert_eq!(BungeeBridge::Across.display_name(), "Across");
1253 assert_eq!(BungeeBridge::CircleCctp.display_name(), "Circle CCTP");
1254 assert_eq!(BungeeBridge::GnosisNative.display_name(), "Gnosis Native");
1255 }
1256
1257 #[test]
1258 fn bungee_bridge_from_display_name_valid() {
1259 assert_eq!(BungeeBridge::from_display_name("Across"), Some(BungeeBridge::Across));
1260 assert_eq!(BungeeBridge::from_display_name("Circle CCTP"), Some(BungeeBridge::CircleCctp));
1261 assert_eq!(
1262 BungeeBridge::from_display_name("Gnosis Native"),
1263 Some(BungeeBridge::GnosisNative)
1264 );
1265 }
1266
1267 #[test]
1268 fn bungee_bridge_from_display_name_invalid() {
1269 assert_eq!(BungeeBridge::from_display_name("across"), None);
1270 assert_eq!(BungeeBridge::from_display_name(""), None);
1271 assert_eq!(BungeeBridge::from_display_name("Unknown"), None);
1272 }
1273
1274 #[test]
1275 fn bungee_bridge_roundtrip_display_name() {
1276 for bridge in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
1277 let name = bridge.display_name();
1278 assert_eq!(BungeeBridge::from_display_name(name), Some(bridge));
1279 }
1280 }
1281
1282 #[test]
1285 fn sell_amount_too_small_has_highest_priority() {
1286 assert_eq!(bridge_error_priority(&BridgeError::SellAmountTooSmall), 10);
1287 }
1288
1289 #[test]
1290 fn only_sell_order_supported_has_second_priority() {
1291 assert_eq!(bridge_error_priority(&BridgeError::OnlySellOrderSupported), 9);
1292 }
1293
1294 #[test]
1295 fn other_errors_have_base_priority() {
1296 let base_errors: Vec<BridgeError> = vec![
1297 BridgeError::NoProviders,
1298 BridgeError::NoQuote,
1299 BridgeError::SameChain,
1300 BridgeError::NoIntermediateTokens,
1301 BridgeError::ApiError("test".into()),
1302 BridgeError::InvalidApiResponse("test".into()),
1303 BridgeError::TxBuildError("test".into()),
1304 BridgeError::QuoteError("test".into()),
1305 BridgeError::NoRoutes,
1306 BridgeError::InvalidBridge("test".into()),
1307 BridgeError::QuoteDoesNotMatchDepositAddress,
1308 BridgeError::ProviderNotFound { dapp_id: "test".into() },
1309 BridgeError::Timeout,
1310 ];
1311 for e in &base_errors {
1312 assert_eq!(bridge_error_priority(e), 1, "expected priority 1 for {e}");
1313 }
1314 }
1315
1316 #[test]
1319 fn bridge_error_display_messages() {
1320 assert_eq!(BridgeError::NoProviders.to_string(), "no providers available");
1321 assert_eq!(BridgeError::NoQuote.to_string(), "no quote available for this route");
1322 assert_eq!(
1323 BridgeError::SameChain.to_string(),
1324 "sell and buy chains must be different for cross-chain bridging"
1325 );
1326 assert_eq!(
1327 BridgeError::OnlySellOrderSupported.to_string(),
1328 "bridging only supports SELL orders"
1329 );
1330 assert_eq!(
1331 BridgeError::NoIntermediateTokens.to_string(),
1332 "no intermediate tokens available"
1333 );
1334 assert_eq!(BridgeError::ApiError("oops".into()).to_string(), "bridge API error: oops");
1335 assert_eq!(
1336 BridgeError::InvalidApiResponse("bad".into()).to_string(),
1337 "invalid API JSON response: bad"
1338 );
1339 assert_eq!(
1340 BridgeError::TxBuildError("fail".into()).to_string(),
1341 "transaction build error: fail"
1342 );
1343 assert_eq!(BridgeError::QuoteError("nope".into()).to_string(), "quote error: nope");
1344 assert_eq!(BridgeError::NoRoutes.to_string(), "no routes available");
1345 assert_eq!(BridgeError::InvalidBridge("x".into()).to_string(), "invalid bridge: x");
1346 assert_eq!(
1347 BridgeError::QuoteDoesNotMatchDepositAddress.to_string(),
1348 "quote does not match deposit address"
1349 );
1350 assert_eq!(BridgeError::SellAmountTooSmall.to_string(), "sell amount too small");
1351 assert_eq!(
1352 BridgeError::ProviderNotFound { dapp_id: "foo".into() }.to_string(),
1353 "provider not found: foo"
1354 );
1355 assert_eq!(BridgeError::Timeout.to_string(), "provider request timed out");
1356 }
1357
1358 #[test]
1361 fn bridge_provider_type_serde_roundtrip() {
1362 for v in [
1363 BridgeProviderType::HookBridgeProvider,
1364 BridgeProviderType::ReceiverAccountBridgeProvider,
1365 ] {
1366 let json = serde_json::to_string(&v).unwrap();
1367 let back: BridgeProviderType = serde_json::from_str(&json).unwrap();
1368 assert_eq!(v, back);
1369 }
1370 }
1371
1372 #[test]
1373 fn bridge_status_serde_roundtrip() {
1374 for v in [
1375 BridgeStatus::InProgress,
1376 BridgeStatus::Executed,
1377 BridgeStatus::Expired,
1378 BridgeStatus::Refund,
1379 BridgeStatus::Unknown,
1380 ] {
1381 let json = serde_json::to_string(&v).unwrap();
1382 let back: BridgeStatus = serde_json::from_str(&json).unwrap();
1383 assert_eq!(v, back);
1384 }
1385 }
1386
1387 #[test]
1388 fn bungee_bridge_serde_roundtrip() {
1389 for v in [BungeeBridge::Across, BungeeBridge::CircleCctp, BungeeBridge::GnosisNative] {
1390 let json = serde_json::to_string(&v).unwrap();
1391 let back: BungeeBridge = serde_json::from_str(&json).unwrap();
1392 assert_eq!(v, back);
1393 }
1394 }
1395
1396 #[test]
1397 fn across_deposit_status_serde_roundtrip() {
1398 for v in [
1399 AcrossDepositStatus::Filled,
1400 AcrossDepositStatus::SlowFillRequested,
1401 AcrossDepositStatus::Pending,
1402 AcrossDepositStatus::Expired,
1403 AcrossDepositStatus::Refunded,
1404 ] {
1405 let json = serde_json::to_string(&v).unwrap();
1406 let back: AcrossDepositStatus = serde_json::from_str(&json).unwrap();
1407 assert_eq!(v, back);
1408 }
1409 }
1410
1411 #[test]
1412 fn across_deposit_status_camel_case_serialization() {
1413 assert_eq!(serde_json::to_string(&AcrossDepositStatus::Filled).unwrap(), "\"filled\"");
1414 assert_eq!(
1415 serde_json::to_string(&AcrossDepositStatus::SlowFillRequested).unwrap(),
1416 "\"slowFillRequested\""
1417 );
1418 assert_eq!(serde_json::to_string(&AcrossDepositStatus::Pending).unwrap(), "\"pending\"");
1419 }
1420
1421 #[test]
1422 fn bungee_event_status_screaming_snake_case() {
1423 assert_eq!(serde_json::to_string(&BungeeEventStatus::Completed).unwrap(), "\"COMPLETED\"");
1424 assert_eq!(serde_json::to_string(&BungeeEventStatus::Pending).unwrap(), "\"PENDING\"");
1425 }
1426
1427 #[test]
1428 fn bungee_bridge_name_lowercase_serialization() {
1429 assert_eq!(serde_json::to_string(&BungeeBridgeName::Across).unwrap(), "\"across\"");
1430 assert_eq!(serde_json::to_string(&BungeeBridgeName::Cctp).unwrap(), "\"cctp\"");
1431 }
1432}