use foldhash::HashMap;
use alloy_primitives::{Address, U256};
use crate::CowError;
use super::{
provider::{BridgeProvider, QuoteFuture},
types::{
BridgeAmounts, BridgeCosts, BridgeError, BridgeFees, BridgeLimits,
BridgeQuoteAmountsAndCosts, BridgeQuoteResult, BridgeStatus, BridgeStatusResult,
BridgingFee, BungeeBridge, BungeeBridgeName, BungeeEvent, BungeeEventStatus,
BungeeTxDataBytesIndex, DecodedBungeeAmounts, DecodedBungeeTxData, QuoteBridgeRequest,
QuoteBridgeResponse,
},
utils::{apply_bps, calculate_fee_bps},
};
const BUNGEE_API_BASE: &str = "https://api.socket.tech/v2/quote";
pub const BUNGEE_APPROVE_AND_BRIDGE_V1_ADDRESS: &str = "0xD06a673fe1fa27B1b9E5BA0be980AB15Dbce85cc";
pub const BUNGEE_COWSWAP_LIB_ADDRESS: &str = "0x75b6ba5fcab20848ca00f132d253638fea82e598";
pub const SOCKET_VERIFIER_ADDRESS: &str = "0xa27A3f5A96DF7D8Be26EE2790999860C00eb688D";
#[must_use]
pub fn bungee_approve_and_bridge_v1_addresses() -> HashMap<u64, Address> {
use crate::config::SupportedChainId;
let Ok(addr) = BUNGEE_APPROVE_AND_BRIDGE_V1_ADDRESS.parse::<Address>() else {
return HashMap::default();
};
let mut m = HashMap::default();
for chain in &[
SupportedChainId::Mainnet,
SupportedChainId::GnosisChain,
SupportedChainId::ArbitrumOne,
SupportedChainId::Base,
SupportedChainId::Avalanche,
SupportedChainId::Polygon,
] {
m.insert(chain.as_u64(), addr);
}
m.insert(10, addr);
m
}
#[must_use]
pub fn bungee_tx_data_bytes_index(
bridge: BungeeBridge,
function_selector: &str,
) -> Option<BungeeTxDataBytesIndex> {
let selector = function_selector.to_lowercase();
match bridge {
BungeeBridge::Across => match selector.as_str() {
"0xcc54d224" | "0xa3b8bfba" => Some(BungeeTxDataBytesIndex {
bytes_start_index: 8,
bytes_length: 32,
bytes_string_start_index: 2 + 8 * 2,
bytes_string_length: 32 * 2,
}),
_ => None,
},
BungeeBridge::CircleCctp => match selector.as_str() {
"0xb7dfe9d0" => Some(BungeeTxDataBytesIndex {
bytes_start_index: 8,
bytes_length: 32,
bytes_string_start_index: 2 + 8 * 2,
bytes_string_length: 32 * 2,
}),
_ => None,
},
BungeeBridge::GnosisNative => match selector.as_str() {
"0x3bf5c228" => Some(BungeeTxDataBytesIndex {
bytes_start_index: 136,
bytes_length: 32,
bytes_string_start_index: 2 + 8 * 2,
bytes_string_length: 32 * 2,
}),
"0xfcb23eb0" => Some(BungeeTxDataBytesIndex {
bytes_start_index: 104,
bytes_length: 32,
bytes_string_start_index: 2 + 8 * 2,
bytes_string_length: 32 * 2,
}),
_ => None,
},
}
}
pub fn decode_bungee_bridge_tx_data(tx_data: &str) -> Result<DecodedBungeeTxData, BridgeError> {
if tx_data.len() < 10 {
return Err(BridgeError::TxBuildError("txData too short".to_owned()));
}
if !tx_data.starts_with("0x") {
return Err(BridgeError::TxBuildError("txData must start with 0x".to_owned()));
}
let without_prefix = &tx_data[2..];
if without_prefix.len() < 8 {
return Err(BridgeError::TxBuildError("insufficient data for routeId".to_owned()));
}
let route_id = format!("0x{}", &without_prefix[..8]);
let encoded_function_data = format!("0x{}", &without_prefix[8..]);
if encoded_function_data.len() < 10 {
return Err(BridgeError::TxBuildError("insufficient data for function selector".to_owned()));
}
let function_selector = encoded_function_data[..10].to_owned();
Ok(DecodedBungeeTxData { route_id, encoded_function_data, function_selector })
}
pub fn decode_amounts_bungee_tx_data(
tx_data: &str,
bridge: BungeeBridge,
) -> Result<DecodedBungeeAmounts, BridgeError> {
if tx_data.is_empty() || !tx_data.starts_with("0x") {
return Err(BridgeError::TxBuildError("invalid txData format".to_owned()));
}
let decoded = decode_bungee_bridge_tx_data(tx_data)?;
let indices =
bungee_tx_data_bytes_index(bridge, &decoded.function_selector).ok_or_else(|| {
BridgeError::TxBuildError(format!(
"unsupported bridge type {:?} with selector {}",
bridge, decoded.function_selector
))
})?;
let start = indices.bytes_string_start_index;
let len = indices.bytes_string_length;
if tx_data.len() < start + len {
return Err(BridgeError::TxBuildError("txData too short for amount field".to_owned()));
}
let input_amount_hex = format!("0x{}", &tx_data[start..start + len]);
let input_amount = U256::from_str_radix(&input_amount_hex[2..], 16)
.map_err(|e| BridgeError::TxBuildError(format!("cannot parse amount: {e}")))?;
Ok(DecodedBungeeAmounts { input_amount_bytes: input_amount_hex, input_amount })
}
#[must_use]
pub fn get_bungee_bridge_from_display_name(display_name: &str) -> Option<BungeeBridge> {
BungeeBridge::from_display_name(display_name)
}
#[must_use]
pub const fn get_display_name_from_bungee_bridge(bridge: BungeeBridge) -> &'static str {
bridge.display_name()
}
#[allow(clippy::too_many_arguments, reason = "mirrors the multi-field Bungee API response")]
pub fn bungee_to_bridge_quote_result(
request: &QuoteBridgeRequest,
slippage_bps: u32,
buy_amount: U256,
route_fee_amount: U256,
quote_timestamp: u64,
estimated_time: u64,
quote_id: Option<String>,
quote_body: Option<String>,
) -> Result<BridgeQuoteResult, BridgeError> {
let sell_amount_before_fee = request.sell_amount;
let buy_amount_before_fee = buy_amount;
let buy_amount_after_fee = buy_amount_before_fee;
let fee_buy_token = if sell_amount_before_fee.is_zero() {
U256::ZERO
} else {
(route_fee_amount * buy_amount_before_fee) / sell_amount_before_fee
};
let buy_amount_after_slippage = apply_bps(buy_amount_after_fee, slippage_bps);
let bridge_fee_bps = calculate_fee_bps(route_fee_amount, request.sell_amount).map_or(0, |v| v);
let amounts_and_costs = BridgeQuoteAmountsAndCosts {
before_fee: BridgeAmounts {
sell_amount: sell_amount_before_fee,
buy_amount: buy_amount_before_fee,
},
after_fee: BridgeAmounts {
sell_amount: sell_amount_before_fee,
buy_amount: buy_amount_after_fee,
},
after_slippage: BridgeAmounts {
sell_amount: sell_amount_before_fee,
buy_amount: buy_amount_after_slippage,
},
costs: BridgeCosts {
bridging_fee: BridgingFee {
fee_bps: bridge_fee_bps,
amount_in_sell_currency: route_fee_amount,
amount_in_buy_currency: fee_buy_token,
},
},
slippage_bps,
};
Ok(BridgeQuoteResult {
id: quote_id,
signature: None,
attestation_signature: None,
quote_body,
is_sell: request.kind == crate::OrderKind::Sell,
amounts_and_costs,
expected_fill_time_seconds: Some(estimated_time),
quote_timestamp,
fees: BridgeFees { bridge_fee: route_fee_amount, destination_gas_fee: U256::ZERO },
limits: BridgeLimits { min_deposit: U256::ZERO, max_deposit: U256::ZERO },
})
}
pub async fn get_bridging_status_from_events<F, Fut>(
events: Option<&[BungeeEvent]>,
get_across_status: F,
) -> Result<BridgeStatusResult, BridgeError>
where
F: Fn(&str) -> Fut,
Fut: std::future::Future<Output = Result<String, BridgeError>>,
{
let active_events = match events {
Some(e) if !e.is_empty() => e,
_ => return Ok(BridgeStatusResult::new(BridgeStatus::Unknown)),
};
let event = &active_events[0];
if event.src_tx_status == BungeeEventStatus::Pending {
return Ok(BridgeStatusResult::new(BridgeStatus::InProgress));
}
if event.src_tx_status == BungeeEventStatus::Completed &&
event.dest_tx_status == BungeeEventStatus::Pending
{
if event.bridge_name == BungeeBridgeName::Across &&
let Ok(across_status) = get_across_status(&event.order_id).await
{
match across_status.as_str() {
"expired" => {
return Ok(BridgeStatusResult {
status: BridgeStatus::Expired,
deposit_tx_hash: event.src_transaction_hash.clone(),
..BridgeStatusResult::new(BridgeStatus::Expired)
});
}
"refunded" => {
return Ok(BridgeStatusResult {
status: BridgeStatus::Refund,
deposit_tx_hash: event.src_transaction_hash.clone(),
..BridgeStatusResult::new(BridgeStatus::Refund)
});
}
_ => {}
}
}
return Ok(BridgeStatusResult {
status: BridgeStatus::InProgress,
deposit_tx_hash: event.src_transaction_hash.clone(),
..BridgeStatusResult::new(BridgeStatus::InProgress)
});
}
if event.src_tx_status == BungeeEventStatus::Completed &&
event.dest_tx_status == BungeeEventStatus::Completed
{
return Ok(BridgeStatusResult {
status: BridgeStatus::Executed,
deposit_tx_hash: event.src_transaction_hash.clone(),
fill_tx_hash: event.dest_transaction_hash.clone(),
..BridgeStatusResult::new(BridgeStatus::Executed)
});
}
Err(BridgeError::QuoteError("unknown Bungee event status combination".to_owned()))
}
#[must_use]
pub fn is_valid_quote_response(response: &serde_json::Value) -> bool {
let Some(success) = response.get("success").and_then(|v| v.as_bool()) else {
return false;
};
if !success {
return false;
}
let Some(result) = response.get("result") else {
return false;
};
let Some(routes) = result.get("manualRoutes").and_then(|r| r.as_array()) else {
return false;
};
routes.iter().all(|route| {
route.get("quoteId").is_some() &&
route.get("output").is_some() &&
route.get("estimatedTime").is_some() &&
route
.get("routeDetails")
.and_then(|rd| rd.get("routeFee"))
.and_then(|rf| rf.get("amount"))
.is_some()
})
}
#[must_use]
pub fn is_valid_bungee_events_response(response: &serde_json::Value) -> bool {
let Some(success) = response.get("success").and_then(|v| v.as_bool()) else {
return false;
};
if !success {
return false;
}
let Some(result) = response.get("result").and_then(|r| r.as_array()) else {
return false;
};
result.iter().all(|event| {
event.get("identifier").is_some() &&
event.get("bridgeName").is_some() &&
event.get("fromChainId").is_some() &&
event.get("orderId").is_some() &&
event.get("srcTxStatus").is_some() &&
event.get("destTxStatus").is_some()
})
}
#[derive(Debug, Clone)]
pub struct BungeeApiUrlOptions {
pub api_base_url: String,
pub manual_api_base_url: String,
pub events_api_base_url: String,
pub across_api_base_url: String,
}
impl Default for BungeeApiUrlOptions {
fn default() -> Self {
Self {
api_base_url: super::sdk::BUNGEE_API_URL.to_owned(),
manual_api_base_url: super::sdk::BUNGEE_MANUAL_API_URL.to_owned(),
events_api_base_url: super::sdk::BUNGEE_EVENTS_API_URL.to_owned(),
across_api_base_url: super::sdk::ACROSS_API_URL.to_owned(),
}
}
}
#[must_use]
pub fn resolve_api_endpoint_from_options(
key: &str,
options: &BungeeApiUrlOptions,
use_fallback: bool,
custom_url: Option<&str>,
) -> String {
let defaults = BungeeApiUrlOptions::default();
let default_val = match key {
"manual_api_base_url" => &defaults.manual_api_base_url,
"events_api_base_url" => &defaults.events_api_base_url,
"across_api_base_url" => &defaults.across_api_base_url,
_ => &defaults.api_base_url,
};
if use_fallback {
return default_val.clone();
}
if let Some(url) = custom_url {
return url.to_owned();
}
let opt_val = match key {
"manual_api_base_url" => &options.manual_api_base_url,
"events_api_base_url" => &options.events_api_base_url,
"across_api_base_url" => &options.across_api_base_url,
_ => &options.api_base_url,
};
if opt_val.is_empty() { default_val.clone() } else { opt_val.clone() }
}
use alloy_primitives::{hex, keccak256};
#[derive(Debug, Clone)]
pub struct BungeeDepositCallParams {
pub request: QuoteBridgeRequest,
pub build_tx_data: String,
pub input_amount: U256,
pub bridge: BungeeBridge,
}
pub fn create_bungee_deposit_call(
params: &BungeeDepositCallParams,
) -> Result<crate::config::EvmCall, BridgeError> {
let decoded_tx = decode_bungee_bridge_tx_data(¶ms.build_tx_data)?;
let function_selector = decoded_tx.function_selector.to_lowercase();
let function_params = bungee_tx_data_bytes_index(params.bridge, &function_selector)
.ok_or_else(|| {
BridgeError::TxBuildError(format!("no params for function [{function_selector}]"))
})?;
let input_amount_start_index = function_params.bytes_start_index;
let mut modify_params = Vec::with_capacity(3 * 32);
modify_params.extend_from_slice(&U256::from(input_amount_start_index).to_be_bytes::<32>());
modify_params.extend_from_slice(&U256::ZERO.to_be_bytes::<32>()); modify_params.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
let raw_data =
params.build_tx_data.strip_prefix("0x").map_or(params.build_tx_data.as_str(), |s| s);
let modify_hex = hex::encode(&modify_params);
let full_data_hex = format!("{raw_data}{modify_hex}");
let full_data_bytes = hex::decode(&full_data_hex)
.map_err(|e| BridgeError::TxBuildError(format!("hex decode error: {e}")))?;
let selector = &keccak256("approveAndBridge(address,uint256,uint256,bytes)")[..4];
let mut calldata = Vec::with_capacity(4 + 5 * 32 + full_data_bytes.len() + 32);
calldata.extend_from_slice(selector);
let mut addr_buf = [0u8; 32];
addr_buf[12..32].copy_from_slice(params.request.sell_token.as_slice());
calldata.extend_from_slice(&addr_buf);
calldata.extend_from_slice(¶ms.input_amount.to_be_bytes::<32>());
calldata.extend_from_slice(&U256::ZERO.to_be_bytes::<32>());
calldata.extend_from_slice(&U256::from(4u64 * 32).to_be_bytes::<32>());
calldata.extend_from_slice(&U256::from(full_data_bytes.len()).to_be_bytes::<32>());
calldata.extend_from_slice(&full_data_bytes);
let padding = (32 - (full_data_bytes.len() % 32)) % 32;
calldata.extend(std::iter::repeat_n(0u8, padding));
let addresses = bungee_approve_and_bridge_v1_addresses();
let to = addresses.get(¶ms.request.sell_chain_id).ok_or_else(|| {
BridgeError::TxBuildError("BungeeApproveAndBridgeV1 not found".to_owned())
})?;
let native = crate::config::NATIVE_CURRENCY_ADDRESS;
let value = if params.request.sell_token == native { params.input_amount } else { U256::ZERO };
Ok(crate::config::EvmCall { to: *to, data: calldata, value })
}
#[derive(Debug)]
pub struct BungeeProvider {
client: reqwest::Client,
api_key: String,
}
impl BungeeProvider {
#[must_use]
pub fn new(api_key: impl Into<String>) -> Self {
Self { client: reqwest::Client::new(), api_key: api_key.into() }
}
}
impl BridgeProvider for BungeeProvider {
fn name(&self) -> &str {
"bungee"
}
fn supports_route(&self, _sell_chain: u64, _buy_chain: u64) -> bool {
true
}
fn get_quote<'a>(&'a self, req: &'a QuoteBridgeRequest) -> QuoteFuture<'a> {
Box::pin(self.get_quote_inner(req))
}
}
impl BungeeProvider {
async fn get_quote_inner(
&self,
req: &QuoteBridgeRequest,
) -> Result<QuoteBridgeResponse, CowError> {
let slippage_pct = req.slippage_bps as f64 / 100.0;
let slippage_str = format!("{slippage_pct:.1}");
let url = reqwest::Url::parse_with_params(
BUNGEE_API_BASE,
&[
("fromChainId", req.sell_chain_id.to_string()),
("toChainId", req.buy_chain_id.to_string()),
("fromTokenAddress", format!("{:#x}", req.sell_token)),
("toTokenAddress", format!("{:#x}", req.buy_token)),
("fromAmount", req.sell_amount.to_string()),
("userAddress", format!("{:#x}", req.account)),
("slippageTolerance", slippage_str),
("isContractCall", "false".to_owned()),
],
)
.map_err(|e| CowError::Parse { field: "bungee_url", reason: e.to_string() })?;
let resp = self.client.get(url).header("API-KEY", &self.api_key).send().await?;
let status = resp.status().as_u16();
if !resp.status().is_success() {
let body = resp.text().await.map_or(String::new(), |b| b);
return Err(CowError::Api { status, body });
}
let json: serde_json::Value = resp.json().await?;
let route = json
.get("result")
.and_then(|r| r.get("routes"))
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.ok_or_else(|| CowError::Parse {
field: "bungee_routes",
reason: "no routes in response".to_owned(),
})?;
let output_amount_str =
route.get("outputAmount").and_then(|v| v.as_str()).map_or("0", |s| s);
let buy_amount = output_amount_str
.parse::<U256>()
.map_err(|e| CowError::Parse { field: "outputAmount", reason: e.to_string() })?;
let estimated_secs =
route.get("estimatedTimeInSeconds").and_then(|v| v.as_u64()).map_or(0, |v| v);
Ok(QuoteBridgeResponse {
provider: "bungee".to_owned(),
sell_amount: req.sell_amount,
buy_amount,
fee_amount: U256::ZERO,
estimated_secs,
bridge_hook: None,
})
}
}