use std::pin::Pin;
use alloy_primitives::{Address, B256};
use alloy_signer_local::PrivateKeySigner;
use cow_chains::SupportedChainId;
use cow_errors::CowError;
use cow_orderbook::types::Order;
use super::types::{
BridgeProviderInfo, BridgeStatusResult, BridgingDepositParams, BuyTokensParams,
GetProviderBuyTokens, IntermediateTokenInfo, QuoteBridgeRequest, QuoteBridgeResponse,
};
#[derive(Debug, Clone)]
pub struct BridgingParamsResult {
pub params: BridgingDepositParams,
pub status: BridgeStatusResult,
}
#[derive(Debug, Clone)]
pub struct BridgeNetworkInfo {
pub chain_id: u64,
pub name: String,
pub logo_url: Option<String>,
}
macro_rules! provider_future {
($name:ident, $output:ty) => {
#[cfg(not(target_arch = "wasm32"))]
#[doc = concat!("Future returned by `BridgeProvider::", stringify!($name), "`.")]
pub type $name<'a> =
Pin<Box<dyn std::future::Future<Output = Result<$output, CowError>> + Send + 'a>>;
#[cfg(target_arch = "wasm32")]
#[doc = concat!("Future returned by `BridgeProvider::", stringify!($name), "`.")]
pub type $name<'a> =
Pin<Box<dyn std::future::Future<Output = Result<$output, CowError>> + 'a>>;
};
}
provider_future!(QuoteFuture, QuoteBridgeResponse);
provider_future!(NetworksFuture, Vec<BridgeNetworkInfo>);
provider_future!(BuyTokensFuture, GetProviderBuyTokens);
provider_future!(IntermediateTokensFuture, Vec<IntermediateTokenInfo>);
provider_future!(BridgingParamsFuture, Option<BridgingParamsResult>);
provider_future!(BridgeStatusFuture, BridgeStatusResult);
provider_future!(UnsignedCallFuture, cow_chains::EvmCall);
provider_future!(SignedHookFuture, crate::types::BridgeHook);
provider_future!(ReceiverOverrideFuture, String);
provider_future!(GasEstimationFuture, u64);
#[cfg(not(target_arch = "wasm32"))]
pub trait MaybeSendSync: Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: ?Sized + Send + Sync> MaybeSendSync for T {}
#[cfg(target_arch = "wasm32")]
pub trait MaybeSendSync {}
#[cfg(target_arch = "wasm32")]
impl<T: ?Sized> MaybeSendSync for T {}
pub trait BridgeProvider: MaybeSendSync {
fn info(&self) -> &BridgeProviderInfo;
fn name(&self) -> &str {
&self.info().name
}
fn supports_route(&self, sell_chain: u64, buy_chain: u64) -> bool;
fn get_networks<'a>(&'a self) -> NetworksFuture<'a>;
fn get_buy_tokens<'a>(&'a self, params: BuyTokensParams) -> BuyTokensFuture<'a>;
fn get_intermediate_tokens<'a>(
&'a self,
request: &'a QuoteBridgeRequest,
) -> IntermediateTokensFuture<'a>;
fn get_quote<'a>(&'a self, req: &'a QuoteBridgeRequest) -> QuoteFuture<'a>;
fn get_bridging_params<'a>(
&'a self,
chain_id: u64,
order: &'a Order,
tx_hash: B256,
settlement_override: Option<Address>,
) -> BridgingParamsFuture<'a>;
fn get_explorer_url(&self, bridging_id: &str) -> String;
fn get_status<'a>(
&'a self,
bridging_id: &'a str,
origin_chain_id: u64,
) -> BridgeStatusFuture<'a>;
fn as_hook_bridge_provider(&self) -> Option<&dyn HookBridgeProvider> {
None
}
fn as_receiver_account_bridge_provider(&self) -> Option<&dyn ReceiverAccountBridgeProvider> {
None
}
}
pub trait HookBridgeProvider: BridgeProvider {
fn get_unsigned_bridge_call<'a>(
&'a self,
request: &'a QuoteBridgeRequest,
quote: &'a QuoteBridgeResponse,
) -> UnsignedCallFuture<'a>;
fn get_gas_limit_estimation_for_hook<'a>(
&'a self,
proxy_deployed: bool,
extra_gas: Option<u64>,
extra_gas_proxy_creation: Option<u64>,
) -> GasEstimationFuture<'a> {
let gas = crate::utils::get_gas_limit_estimation_for_hook(
proxy_deployed,
extra_gas,
extra_gas_proxy_creation,
);
Box::pin(async move { Ok(gas) })
}
#[allow(clippy::too_many_arguments, reason = "1:1 mirror of the TS signature")]
fn get_signed_hook<'a>(
&'a self,
chain_id: SupportedChainId,
unsigned_call: &'a cow_chains::EvmCall,
bridge_hook_nonce: &'a str,
deadline: u64,
hook_gas_limit: u64,
signer: &'a PrivateKeySigner,
) -> SignedHookFuture<'a>;
}
pub trait ReceiverAccountBridgeProvider: BridgeProvider {
fn get_bridge_receiver_override<'a>(
&'a self,
quote_request: &'a QuoteBridgeRequest,
quote_result: &'a QuoteBridgeResponse,
) -> ReceiverOverrideFuture<'a>;
}
#[must_use]
pub fn is_hook_bridge_provider<P: BridgeProvider + ?Sized>(provider: &P) -> bool {
provider.info().is_hook_bridge_provider()
}
#[must_use]
pub fn is_receiver_account_bridge_provider<P: BridgeProvider + ?Sized>(provider: &P) -> bool {
provider.info().is_receiver_account_bridge_provider()
}
#[cfg(all(test, not(target_arch = "wasm32")))]
#[allow(clippy::tests_outside_test_module, reason = "inner module + cfg guard for WASM test skip")]
mod tests {
use alloy_primitives::U256;
use crate::types::{BridgeProviderType, BridgeStatus};
use super::*;
#[test]
fn bridge_network_info_holds_chain_metadata() {
let info = BridgeNetworkInfo {
chain_id: 1,
name: "Ethereum".into(),
logo_url: Some("https://example.com/eth.png".into()),
};
assert_eq!(info.chain_id, 1);
assert_eq!(info.name, "Ethereum");
assert!(info.logo_url.is_some());
}
#[test]
fn bridge_network_info_logo_optional() {
let info = BridgeNetworkInfo { chain_id: 100, name: "Gnosis".into(), logo_url: None };
assert!(info.logo_url.is_none());
}
#[test]
fn bridging_params_result_bundles_params_and_status() {
let params = BridgingDepositParams {
input_token_address: Address::ZERO,
output_token_address: Address::ZERO,
input_amount: U256::from(1000u64),
output_amount: None,
owner: Address::ZERO,
quote_timestamp: None,
fill_deadline: None,
recipient: Address::ZERO,
source_chain_id: 1,
destination_chain_id: 10,
bridging_id: "abc".into(),
};
let status = BridgeStatusResult {
status: BridgeStatus::InProgress,
fill_time_in_seconds: None,
deposit_tx_hash: None,
fill_tx_hash: None,
};
let bundle = BridgingParamsResult { params, status };
assert_eq!(bundle.params.bridging_id, "abc");
assert_eq!(bundle.status.status, BridgeStatus::InProgress);
}
struct FakeProvider {
info: BridgeProviderInfo,
}
impl BridgeProvider for FakeProvider {
fn info(&self) -> &BridgeProviderInfo {
&self.info
}
fn supports_route(&self, _sell: u64, _buy: u64) -> bool {
true
}
fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_buy_tokens<'a>(&'a self, _params: BuyTokensParams) -> BuyTokensFuture<'a> {
let info = self.info.clone();
Box::pin(
async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
)
}
fn get_intermediate_tokens<'a>(
&'a self,
_request: &'a QuoteBridgeRequest,
) -> IntermediateTokensFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
Box::pin(async {
Ok(QuoteBridgeResponse {
provider: "fake".into(),
sell_amount: U256::ZERO,
buy_amount: U256::ZERO,
fee_amount: U256::ZERO,
estimated_secs: 0,
bridge_hook: None,
})
})
}
fn get_bridging_params<'a>(
&'a self,
_chain_id: u64,
_order: &'a cow_orderbook::types::Order,
_tx_hash: B256,
_settlement_override: Option<Address>,
) -> BridgingParamsFuture<'a> {
Box::pin(async { Ok(None) })
}
fn get_explorer_url(&self, bridging_id: &str) -> String {
format!("https://example.com/{bridging_id}")
}
fn get_status<'a>(
&'a self,
_bridging_id: &'a str,
_origin_chain_id: u64,
) -> BridgeStatusFuture<'a> {
Box::pin(async {
Ok(BridgeStatusResult {
status: BridgeStatus::Unknown,
fill_time_in_seconds: None,
deposit_tx_hash: None,
fill_tx_hash: None,
})
})
}
}
fn fake_info() -> BridgeProviderInfo {
BridgeProviderInfo {
name: "fake-provider".into(),
logo_url: "https://example.com/logo.svg".into(),
dapp_id: "cow-sdk://bridging/providers/fake".into(),
website: "https://example.com".into(),
provider_type: BridgeProviderType::HookBridgeProvider,
}
}
#[test]
fn default_name_delegates_to_info() {
let provider = FakeProvider { info: fake_info() };
assert_eq!(provider.name(), "fake-provider");
assert_eq!(provider.name(), provider.info().name.as_str());
}
#[test]
fn default_explorer_url_composes_path() {
let provider = FakeProvider { info: fake_info() };
assert_eq!(provider.get_explorer_url("deposit-42"), "https://example.com/deposit-42");
}
#[tokio::test]
async fn trait_object_dispatch_works_with_dyn() {
let provider: Box<dyn BridgeProvider> = Box::new(FakeProvider { info: fake_info() });
assert!(provider.supports_route(1, 10));
assert_eq!(provider.info().dapp_id, "cow-sdk://bridging/providers/fake");
let networks = provider.get_networks().await.unwrap();
assert!(networks.is_empty());
let tokens = provider
.get_buy_tokens(BuyTokensParams {
sell_chain_id: 1,
buy_chain_id: 100,
sell_token_address: None,
})
.await
.unwrap();
assert!(tokens.tokens.is_empty());
assert_eq!(tokens.provider_info.name, "fake-provider");
}
fn sample_request() -> QuoteBridgeRequest {
QuoteBridgeRequest {
sell_chain_id: 1,
buy_chain_id: 10,
sell_token: Address::ZERO,
sell_token_decimals: 18,
buy_token: Address::ZERO,
buy_token_decimals: 18,
sell_amount: U256::from(1u64),
account: Address::ZERO,
owner: None,
receiver: None,
bridge_recipient: None,
slippage_bps: 50,
bridge_slippage_bps: None,
kind: cow_types::OrderKind::Sell,
}
}
#[tokio::test]
async fn fake_provider_get_intermediate_tokens_is_callable() {
let provider = FakeProvider { info: fake_info() };
let tokens = provider.get_intermediate_tokens(&sample_request()).await.unwrap();
assert!(tokens.is_empty());
}
#[tokio::test]
async fn fake_provider_get_quote_returns_default_fake_response() {
let provider = FakeProvider { info: fake_info() };
let response = provider.get_quote(&sample_request()).await.unwrap();
assert_eq!(response.provider, "fake");
assert_eq!(response.sell_amount, U256::ZERO);
assert_eq!(response.buy_amount, U256::ZERO);
}
#[tokio::test]
async fn fake_provider_get_bridging_params_returns_none() {
let provider = FakeProvider { info: fake_info() };
let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
let result = provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn fake_provider_get_status_returns_unknown() {
let provider = FakeProvider { info: fake_info() };
let result = provider.get_status("deposit", 1).await.unwrap();
assert_eq!(result.status, BridgeStatus::Unknown);
assert!(result.fill_tx_hash.is_none());
assert!(result.deposit_tx_hash.is_none());
}
#[test]
fn is_hook_bridge_provider_matches_info_type() {
let hook_info = fake_info();
let hook_provider = FakeProvider { info: hook_info };
assert!(is_hook_bridge_provider(&hook_provider));
assert!(!is_receiver_account_bridge_provider(&hook_provider));
}
#[test]
fn is_receiver_account_bridge_provider_matches_info_type() {
let receiver_info = BridgeProviderInfo {
name: "rcv".into(),
logo_url: String::new(),
dapp_id: "cow-sdk://bridging/providers/rcv".into(),
website: String::new(),
provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
};
let provider = FakeProvider { info: receiver_info };
assert!(is_receiver_account_bridge_provider(&provider));
assert!(!is_hook_bridge_provider(&provider));
}
#[test]
fn type_guards_work_through_trait_object() {
let hook_provider: Box<dyn BridgeProvider> = Box::new(FakeProvider { info: fake_info() });
assert!(is_hook_bridge_provider(&*hook_provider));
assert!(!is_receiver_account_bridge_provider(&*hook_provider));
}
struct FakeHookProvider {
info: BridgeProviderInfo,
}
impl BridgeProvider for FakeHookProvider {
fn info(&self) -> &BridgeProviderInfo {
&self.info
}
fn supports_route(&self, _s: u64, _b: u64) -> bool {
true
}
fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
let info = self.info.clone();
Box::pin(
async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
)
}
fn get_intermediate_tokens<'a>(
&'a self,
_req: &'a QuoteBridgeRequest,
) -> IntermediateTokensFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
Box::pin(async {
Ok(QuoteBridgeResponse {
provider: "hook".into(),
sell_amount: U256::ZERO,
buy_amount: U256::ZERO,
fee_amount: U256::ZERO,
estimated_secs: 0,
bridge_hook: None,
})
})
}
fn get_bridging_params<'a>(
&'a self,
_c: u64,
_o: &'a Order,
_t: B256,
_s: Option<Address>,
) -> BridgingParamsFuture<'a> {
Box::pin(async { Ok(None) })
}
fn get_explorer_url(&self, _id: &str) -> String {
String::new()
}
fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
Box::pin(async {
Ok(BridgeStatusResult {
status: BridgeStatus::Unknown,
fill_time_in_seconds: None,
deposit_tx_hash: None,
fill_tx_hash: None,
})
})
}
}
impl HookBridgeProvider for FakeHookProvider {
fn get_unsigned_bridge_call<'a>(
&'a self,
_req: &'a QuoteBridgeRequest,
_quote: &'a QuoteBridgeResponse,
) -> UnsignedCallFuture<'a> {
Box::pin(async {
Ok(cow_chains::EvmCall { to: Address::ZERO, data: vec![], value: U256::ZERO })
})
}
fn get_signed_hook<'a>(
&'a self,
_chain: SupportedChainId,
_call: &'a cow_chains::EvmCall,
_nonce: &'a str,
_deadline: u64,
_gas: u64,
_signer: &'a PrivateKeySigner,
) -> SignedHookFuture<'a> {
Box::pin(async {
Ok(crate::types::BridgeHook {
post_hook: cow_types::CowHook {
target: String::new(),
call_data: String::new(),
gas_limit: String::new(),
dapp_id: None,
},
recipient: String::new(),
})
})
}
}
#[tokio::test]
async fn hook_provider_default_gas_estimation_deployed() {
let provider = FakeHookProvider { info: fake_info() };
let gas = provider.get_gas_limit_estimation_for_hook(true, None, None).await.unwrap();
assert_eq!(gas, crate::utils::get_gas_limit_estimation_for_hook(true, None, None));
}
#[tokio::test]
async fn hook_provider_default_gas_estimation_needs_proxy_creation() {
let provider = FakeHookProvider { info: fake_info() };
let gas =
provider.get_gas_limit_estimation_for_hook(false, None, Some(10_000)).await.unwrap();
assert_eq!(gas, crate::utils::get_gas_limit_estimation_for_hook(false, None, Some(10_000)));
}
#[tokio::test]
async fn hook_provider_required_methods_callable_through_trait() {
let provider = FakeHookProvider { info: fake_info() };
let req = sample_request();
let quote = provider.get_quote(&req).await.unwrap();
let call = provider.get_unsigned_bridge_call(&req, "e).await.unwrap();
assert_eq!(call.to, Address::ZERO);
assert!(call.data.is_empty());
let signer: PrivateKeySigner =
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse().unwrap();
let hook = provider
.get_signed_hook(SupportedChainId::Mainnet, &call, "0", 0, 0, &signer)
.await
.unwrap();
assert!(hook.recipient.is_empty());
}
#[tokio::test]
async fn fake_hook_provider_bridge_provider_surface_is_callable() {
let provider = FakeHookProvider { info: fake_info() };
assert!(provider.supports_route(1, 10));
assert_eq!(provider.info().dapp_id, "cow-sdk://bridging/providers/fake");
assert!(provider.get_networks().await.unwrap().is_empty());
let tokens = provider
.get_buy_tokens(BuyTokensParams {
sell_chain_id: 1,
buy_chain_id: 10,
sell_token_address: None,
})
.await
.unwrap();
assert!(tokens.tokens.is_empty());
assert!(provider.get_intermediate_tokens(&sample_request()).await.unwrap().is_empty());
let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "aa".repeat(56)));
assert!(provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap().is_none());
assert!(provider.get_explorer_url("x").is_empty());
assert_eq!(provider.get_status("x", 1).await.unwrap().status, BridgeStatus::Unknown);
}
struct FakeReceiverProvider {
info: BridgeProviderInfo,
}
impl BridgeProvider for FakeReceiverProvider {
fn info(&self) -> &BridgeProviderInfo {
&self.info
}
fn supports_route(&self, _s: u64, _b: u64) -> bool {
true
}
fn get_networks<'a>(&'a self) -> NetworksFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_buy_tokens<'a>(&'a self, _p: BuyTokensParams) -> BuyTokensFuture<'a> {
let info = self.info.clone();
Box::pin(
async move { Ok(GetProviderBuyTokens { provider_info: info, tokens: vec![] }) },
)
}
fn get_intermediate_tokens<'a>(
&'a self,
_req: &'a QuoteBridgeRequest,
) -> IntermediateTokensFuture<'a> {
Box::pin(async { Ok(Vec::new()) })
}
fn get_quote<'a>(&'a self, _req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
Box::pin(async {
Ok(QuoteBridgeResponse {
provider: "rcv".into(),
sell_amount: U256::ZERO,
buy_amount: U256::ZERO,
fee_amount: U256::ZERO,
estimated_secs: 0,
bridge_hook: None,
})
})
}
fn get_bridging_params<'a>(
&'a self,
_c: u64,
_o: &'a Order,
_t: B256,
_s: Option<Address>,
) -> BridgingParamsFuture<'a> {
Box::pin(async { Ok(None) })
}
fn get_explorer_url(&self, _id: &str) -> String {
String::new()
}
fn get_status<'a>(&'a self, _id: &'a str, _c: u64) -> BridgeStatusFuture<'a> {
Box::pin(async {
Ok(BridgeStatusResult {
status: BridgeStatus::Unknown,
fill_time_in_seconds: None,
deposit_tx_hash: None,
fill_tx_hash: None,
})
})
}
}
impl ReceiverAccountBridgeProvider for FakeReceiverProvider {
fn get_bridge_receiver_override<'a>(
&'a self,
_req: &'a QuoteBridgeRequest,
_result: &'a QuoteBridgeResponse,
) -> ReceiverOverrideFuture<'a> {
Box::pin(async { Ok("near-deposit-address".to_owned()) })
}
}
fn fake_receiver_info() -> BridgeProviderInfo {
BridgeProviderInfo {
name: "rcv".into(),
logo_url: String::new(),
dapp_id: "cow-sdk://bridging/providers/rcv".into(),
website: String::new(),
provider_type: BridgeProviderType::ReceiverAccountBridgeProvider,
}
}
#[tokio::test]
async fn receiver_provider_returns_deposit_address() {
let provider = FakeReceiverProvider { info: fake_receiver_info() };
let req = sample_request();
let quote = provider.get_quote(&req).await.unwrap();
let addr = provider.get_bridge_receiver_override(&req, "e).await.unwrap();
assert_eq!(addr, "near-deposit-address");
}
#[tokio::test]
async fn fake_receiver_provider_bridge_provider_surface_is_callable() {
let provider = FakeReceiverProvider { info: fake_receiver_info() };
assert!(provider.supports_route(1, 1_000_000_000));
assert!(provider.info().is_receiver_account_bridge_provider());
assert!(provider.get_networks().await.unwrap().is_empty());
let tokens = provider
.get_buy_tokens(BuyTokensParams {
sell_chain_id: 1,
buy_chain_id: 1_000_000_000,
sell_token_address: None,
})
.await
.unwrap();
assert!(tokens.tokens.is_empty());
assert!(provider.get_intermediate_tokens(&sample_request()).await.unwrap().is_empty());
let order = cow_orderbook::api::mock_get_order(&format!("0x{}", "bb".repeat(56)));
assert!(provider.get_bridging_params(1, &order, B256::ZERO, None).await.unwrap().is_none());
assert!(provider.get_explorer_url("dep").is_empty());
assert_eq!(provider.get_status("dep", 1).await.unwrap().status, BridgeStatus::Unknown);
}
}