use cow_errors::CowError;
pub const BUNGEE_API_PATH: &str = "/api/v1/bungee";
pub const BUNGEE_MANUAL_API_PATH: &str = "/api/v1/bungee-manual";
pub const BUNGEE_BASE_URL: &str = "https://public-backend.bungee.exchange";
pub const BUNGEE_API_URL: &str = "https://public-backend.bungee.exchange/api/v1/bungee";
pub const BUNGEE_MANUAL_API_URL: &str =
"https://public-backend.bungee.exchange/api/v1/bungee-manual";
pub const BUNGEE_EVENTS_API_URL: &str = "https://microservices.socket.tech/loki";
pub const ACROSS_API_URL: &str = "https://app.across.to/api";
pub const DEFAULT_BRIDGE_SLIPPAGE_BPS: u32 = 50;
pub const DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION: u64 = 240_000;
pub const DEFAULT_EXTRA_GAS_FOR_HOOK_ESTIMATION: u64 = 350_000;
pub const DEFAULT_EXTRA_GAS_PROXY_CREATION: u64 = 400_000;
pub const HOOK_DAPP_BRIDGE_PROVIDER_PREFIX: &str = "cow-sdk://bridging/providers";
pub const BUNGEE_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/bungee";
pub const ACROSS_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/across";
pub const NEAR_INTENTS_HOOK_DAPP_ID: &str = "cow-sdk://bridging/providers/near-intents";
pub const BUNGEE_API_FALLBACK_TIMEOUT: u64 = 300_000;
use super::{
bungee::BungeeProvider,
provider::BridgeProvider,
types::{BridgeError, QuoteBridgeRequest, QuoteBridgeResponse},
};
#[derive(Default)]
pub struct BridgingSdk {
providers: Vec<Box<dyn BridgeProvider>>,
}
impl std::fmt::Debug for BridgingSdk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BridgingSdk").field("provider_count", &self.providers.len()).finish()
}
}
impl BridgingSdk {
#[must_use]
pub fn new() -> Self {
Self { providers: vec![] }
}
#[must_use]
pub fn with_bungee(mut self, api_key: impl Into<String>) -> Self {
self.providers.push(Box::new(BungeeProvider::new(api_key)));
self
}
pub fn add_provider(&mut self, provider: impl BridgeProvider + 'static) {
self.providers.push(Box::new(provider));
}
#[must_use]
pub fn provider_count(&self) -> usize {
self.providers.len()
}
pub async fn get_best_quote(
&self,
req: &QuoteBridgeRequest,
) -> Result<QuoteBridgeResponse, BridgeError> {
let eligible: Vec<&dyn BridgeProvider> = self
.providers
.iter()
.filter(|p| p.supports_route(req.sell_chain_id, req.buy_chain_id))
.map(|p| p.as_ref())
.collect();
if eligible.is_empty() {
return Err(BridgeError::NoProviders);
}
let futures: Vec<_> = eligible.iter().map(|p| p.get_quote(req)).collect();
let results = futures::future::join_all(futures).await;
let best = results
.into_iter()
.filter_map(|r| r.ok())
.max_by_key(QuoteBridgeResponse::net_buy_amount);
best.ok_or(BridgeError::NoQuote)
}
pub async fn get_all_quotes(
&self,
req: &QuoteBridgeRequest,
) -> Vec<Result<QuoteBridgeResponse, CowError>> {
let eligible: Vec<&dyn BridgeProvider> = self
.providers
.iter()
.filter(|p| p.supports_route(req.sell_chain_id, req.buy_chain_id))
.map(|p| p.as_ref())
.collect();
let futures: Vec<_> = eligible.iter().map(|p| p.get_quote(req)).collect();
futures::future::join_all(futures).await
}
}
use super::types::BridgeQuoteResults;
#[derive(Debug, Clone)]
pub struct BridgeQuoteAndPost {
pub swap: QuoteBridgeResponse,
pub bridge: BridgeQuoteResults,
}
#[derive(Debug, Clone)]
pub struct QuoteAndPost {
pub quote: QuoteBridgeResponse,
}
#[derive(Debug, Clone)]
pub enum CrossChainQuoteAndPost {
SameChain(Box<QuoteAndPost>),
CrossChain(Box<BridgeQuoteAndPost>),
}
#[must_use]
pub const fn is_bridge_quote_and_post(result: &CrossChainQuoteAndPost) -> bool {
matches!(result, CrossChainQuoteAndPost::CrossChain(_))
}
#[must_use]
pub const fn is_quote_and_post(result: &CrossChainQuoteAndPost) -> bool {
matches!(result, CrossChainQuoteAndPost::SameChain(_))
}
pub fn assert_is_bridge_quote_and_post(
result: &CrossChainQuoteAndPost,
) -> Result<&BridgeQuoteAndPost, BridgeError> {
match result {
CrossChainQuoteAndPost::CrossChain(bqp) => Ok(bqp.as_ref()),
CrossChainQuoteAndPost::SameChain(_) => {
Err(BridgeError::QuoteError("expected BridgeQuoteAndPost, got QuoteAndPost".to_owned()))
}
}
}
pub fn assert_is_quote_and_post(
result: &CrossChainQuoteAndPost,
) -> Result<&QuoteAndPost, BridgeError> {
match result {
CrossChainQuoteAndPost::SameChain(qp) => Ok(qp.as_ref()),
CrossChainQuoteAndPost::CrossChain(_) => {
Err(BridgeError::QuoteError("expected QuoteAndPost, got BridgeQuoteAndPost".to_owned()))
}
}
}
use crate::{
across::{EvmLogEntry, get_deposit_params},
types::{BridgeHook, BridgeQuoteResult, BridgeStatus, BridgeStatusResult, CrossChainOrder},
};
use alloy_primitives::Address;
#[derive(Debug)]
pub struct GetCrossChainOrderParams<'a> {
pub chain_id: u64,
pub order_id: String,
pub full_app_data: Option<String>,
pub trade_tx_hash: String,
pub logs: &'a [EvmLogEntry],
pub settlement_override: Option<Address>,
}
pub fn get_cross_chain_order(
params: &GetCrossChainOrderParams<'_>,
) -> Result<CrossChainOrder, BridgeError> {
let bridging_params = get_deposit_params(
params.chain_id,
¶ms.order_id,
params.logs,
params.settlement_override,
)
.ok_or_else(|| {
BridgeError::QuoteError(format!(
"bridging params cannot be derived from transaction: {}",
params.trade_tx_hash
))
})?;
Ok(CrossChainOrder {
chain_id: params.chain_id,
status_result: BridgeStatusResult::new(BridgeStatus::Unknown),
bridging_params,
trade_tx_hash: params.trade_tx_hash.clone(),
explorer_url: None,
})
}
pub async fn get_bridge_signed_hook(
_quote: &BridgeQuoteResult,
_signer: &[u8],
) -> Result<BridgeHook, BridgeError> {
Err(BridgeError::TxBuildError(
"get_bridge_signed_hook requires CowShedSdk signing infrastructure (not yet ported)"
.to_owned(),
))
}
#[derive(Debug, Clone)]
pub struct GetQuoteWithBridgeParams {
pub swap_and_bridge_request: QuoteBridgeRequest,
pub slippage_bps: u32,
}
pub async fn get_quote_with_bridge(
_params: &GetQuoteWithBridgeParams,
) -> Result<BridgeQuoteAndPost, BridgeError> {
Err(BridgeError::TxBuildError(
"get_quote_with_bridge requires TradingSdk orchestration (not yet ported)".to_owned(),
))
}
pub async fn get_quote_without_bridge(
_request: &QuoteBridgeRequest,
) -> Result<QuoteAndPost, BridgeError> {
Err(BridgeError::TxBuildError(
"get_quote_without_bridge requires TradingSdk (not yet ported)".to_owned(),
))
}
pub async fn get_swap_quote(
_request: &QuoteBridgeRequest,
) -> Result<QuoteBridgeResponse, BridgeError> {
Err(BridgeError::TxBuildError("get_swap_quote requires TradingSdk (not yet ported)".to_owned()))
}
pub async fn create_post_swap_order_from_quote(
_quote: &BridgeQuoteAndPost,
) -> Result<(), BridgeError> {
Err(BridgeError::TxBuildError(
"create_post_swap_order_from_quote requires TradingSdk + OrderBookApi (not yet ported)"
.to_owned(),
))
}
pub async fn get_intermediate_swap_result(
_request: &QuoteBridgeRequest,
) -> Result<QuoteBridgeResponse, BridgeError> {
Err(BridgeError::TxBuildError(
"get_intermediate_swap_result requires TradingSdk (not yet ported)".to_owned(),
))
}
#[cfg(feature = "native")]
pub async fn create_bridge_request_timeout(timeout_ms: u64, prefix: &str) -> BridgeError {
tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)).await;
BridgeError::ApiError(format!("{prefix} timeout after {timeout_ms}ms"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuoteStrategy {
Single,
Multi,
Best,
}
impl QuoteStrategy {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Single => "SingleQuoteStrategy",
Self::Multi => "MultiQuoteStrategy",
Self::Best => "BestQuoteStrategy",
}
}
}
#[must_use]
pub const fn create_strategies() -> [QuoteStrategy; 3] {
[QuoteStrategy::Single, QuoteStrategy::Multi, QuoteStrategy::Best]
}
use super::types::MultiQuoteResult;
pub const DEFAULT_TOTAL_TIMEOUT_MS: u64 = 40_000;
pub const DEFAULT_PROVIDER_TIMEOUT_MS: u64 = 20_000;
#[cfg(feature = "native")]
pub async fn execute_provider_quotes(
sdk: &BridgingSdk,
request: &QuoteBridgeRequest,
timeout_ms: u64,
) -> Vec<MultiQuoteResult> {
use futures::future::join_all;
let futs: Vec<_> = sdk
.providers
.iter()
.map(|p| {
let name = p.name().to_owned();
async move {
let result = p.get_quote(request).await;
match result {
Ok(quote) => MultiQuoteResult {
provider_dapp_id: name,
quote: Some(crate::types::BridgeQuoteAmountsAndCosts {
before_fee: crate::types::BridgeAmounts {
sell_amount: quote.sell_amount,
buy_amount: quote.buy_amount,
},
after_fee: crate::types::BridgeAmounts {
sell_amount: quote.sell_amount,
buy_amount: quote.buy_amount.saturating_sub(quote.fee_amount),
},
after_slippage: crate::types::BridgeAmounts {
sell_amount: quote.sell_amount,
buy_amount: quote.buy_amount.saturating_sub(quote.fee_amount),
},
costs: crate::types::BridgeCosts {
bridging_fee: crate::types::BridgingFee {
fee_bps: 0,
amount_in_sell_currency: quote.fee_amount,
amount_in_buy_currency: quote.fee_amount,
},
},
slippage_bps: request.slippage_bps,
}),
error: None,
},
Err(e) => MultiQuoteResult {
provider_dapp_id: name,
quote: None,
error: Some(e.to_string()),
},
}
}
})
.collect();
let fetched_results =
tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), join_all(futs)).await;
match fetched_results {
Ok(results) => results,
Err(_timeout) => {
sdk.providers
.iter()
.map(|p| MultiQuoteResult {
provider_dapp_id: p.name().to_owned(),
quote: None,
error: Some(format!("Multi-quote timeout after {timeout_ms}ms")),
})
.collect()
}
}
}
#[cfg(feature = "native")]
pub async fn fetch_multi_quote(
sdk: &BridgingSdk,
request: &QuoteBridgeRequest,
timeout_ms: Option<u64>,
) -> Vec<MultiQuoteResult> {
let timeout = timeout_ms.map_or(DEFAULT_TOTAL_TIMEOUT_MS, |v| v);
let mut results = execute_provider_quotes(sdk, request, timeout).await;
let dapp_ids: Vec<String> = sdk.providers.iter().map(|p| p.name().to_owned()).collect();
crate::utils::fill_timeout_results(&mut results, &dapp_ids);
results.sort_by(|a, b| {
let a_amount =
a.quote.as_ref().map_or(alloy_primitives::U256::ZERO, |q| q.after_slippage.buy_amount);
let b_amount =
b.quote.as_ref().map_or(alloy_primitives::U256::ZERO, |q| q.after_slippage.buy_amount);
b_amount.cmp(&a_amount)
});
results
}
#[must_use]
pub fn get_cache_key(request: &QuoteBridgeRequest) -> String {
format!(
"{}-{}-{:#x}-{:#x}",
request.sell_chain_id, request.buy_chain_id, request.sell_token, request.buy_token,
)
}
pub fn safe_call_best_quote_callback<F: FnOnce(&MultiQuoteResult)>(
callback: Option<F>,
result: &MultiQuoteResult,
) {
if let Some(cb) = callback {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cb(result);
}));
if let Err(e) = outcome {
tracing::warn!("Error in best-quote callback: {:?}", e);
}
}
}
pub fn safe_call_progressive_callback<F: FnOnce(&MultiQuoteResult)>(
callback: Option<F>,
result: &MultiQuoteResult,
) {
if let Some(cb) = callback {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cb(result);
}));
if let Err(e) = outcome {
tracing::warn!("Error in progressive-quote callback: {:?}", e);
}
}
}
pub async fn get_quote_with_hook_bridge(
params: &GetQuoteWithBridgeParams,
) -> Result<BridgeQuoteAndPost, BridgeError> {
get_quote_with_bridge(params).await
}
pub async fn get_quote_with_receiver_account_bridge(
params: &GetQuoteWithBridgeParams,
) -> Result<BridgeQuoteAndPost, BridgeError> {
get_quote_with_bridge(params).await
}
#[cfg(test)]
pub mod test_helpers {
use alloy_primitives::Address;
use alloy_signer_local::PrivateKeySigner;
pub const TEST_PRIVATE_KEY: &str =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
#[must_use]
pub fn get_pk() -> &'static str {
TEST_PRIVATE_KEY
}
#[must_use]
pub fn get_mock_signer() -> PrivateKeySigner {
TEST_PRIVATE_KEY.parse::<PrivateKeySigner>().expect("valid test key")
}
#[must_use]
pub fn get_wallet() -> PrivateKeySigner {
get_mock_signer()
}
#[must_use]
pub fn get_rpc_provider() -> &'static str {
"https://eth.llamarpc.com"
}
pub fn expect_to_equal<T: serde::Serialize>(actual: &T, expected: &T) {
let actual_json = serde_json::to_string_pretty(actual).expect("failed to serialise actual");
let expected_json =
serde_json::to_string_pretty(expected).expect("failed to serialise expected");
assert_eq!(actual_json, expected_json);
}
#[must_use]
pub fn test_address() -> Address {
get_mock_signer().address()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mock_signer_has_expected_address() {
let signer = get_mock_signer();
let expected: Address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".parse().unwrap();
assert_eq!(signer.address(), expected);
}
#[test]
fn expect_to_equal_passes_for_equal_values() {
expect_to_equal(&42u64, &42u64);
}
#[test]
#[should_panic]
fn expect_to_equal_panics_for_different_values() {
expect_to_equal(&42u64, &43u64);
}
#[test]
fn get_pk_returns_key() {
assert_eq!(get_pk().len(), 64);
}
#[test]
fn get_rpc_provider_returns_url() {
assert!(get_rpc_provider().starts_with("https://"));
}
}
}