use alloy_primitives::{Address, U256};
use cow_chains::SupportedChainId;
use cow_types::OrderKind;
use foldhash::HashMap;
use crate::{
types::{
AcrossChainConfig, AcrossDepositStatus, AcrossSuggestedFeesResponse, BridgeAmounts,
BridgeCosts, BridgeError, BridgeFees, BridgeLimits, BridgeQuoteAmountsAndCosts,
BridgeQuoteResult, BridgeStatus, BridgingFee,
},
utils::{apply_bps, apply_pct_fee, pct_to_bps},
};
#[must_use]
pub fn across_spoke_pool_addresses() -> HashMap<u64, Address> {
let mut m = HashMap::default();
let insert = |m: &mut HashMap<u64, Address>, chain_id: u64, addr: &str| {
if let Ok(a) = addr.parse::<Address>() {
m.insert(chain_id, a);
}
};
insert(
&mut m,
SupportedChainId::Mainnet.as_u64(),
"0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
);
insert(
&mut m,
SupportedChainId::ArbitrumOne.as_u64(),
"0xe35e9842fceaca96570b734083f4a58e8f7c5f2a",
);
insert(&mut m, SupportedChainId::Base.as_u64(), "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64");
insert(
&mut m,
SupportedChainId::Sepolia.as_u64(),
"0x5ef6C01E11889d86803e0B23e3cB3F9E9d97B662",
);
insert(
&mut m,
SupportedChainId::Polygon.as_u64(),
"0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096",
);
insert(
&mut m,
SupportedChainId::BnbChain.as_u64(),
"0x4e8E101924eDE233C13e2D8622DC8aED2872d505",
);
insert(&mut m, SupportedChainId::Linea.as_u64(), "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75");
insert(&mut m, SupportedChainId::Plasma.as_u64(), "0x50039fAEfebef707cFD94D6d462fE6D10B39207a");
insert(&mut m, SupportedChainId::Ink.as_u64(), "0xeF684C38F94F48775959ECf2012D7E864ffb9dd4");
insert(&mut m, 10, "0x6f26Bf09B1C792e3228e5467807a900A503c0281");
m
}
#[must_use]
pub fn across_math_contract_addresses() -> HashMap<u64, Address> {
let mut m = HashMap::default();
let insert = |m: &mut HashMap<u64, Address>, chain_id: u64, addr: &str| {
if let Ok(a) = addr.parse::<Address>() {
m.insert(chain_id, a);
}
};
insert(
&mut m,
SupportedChainId::Mainnet.as_u64(),
"0xf2ae6728b6f146556977Af0A68bFbf5bADA22863",
);
insert(
&mut m,
SupportedChainId::ArbitrumOne.as_u64(),
"0x5771A4b4029832e79a75De7B485E5fBbec28848f",
);
insert(&mut m, SupportedChainId::Base.as_u64(), "0xd4e943dc6ddc885f6229ce33c2e3dfe402a12c81");
m
}
#[must_use]
pub fn across_token_mapping() -> HashMap<u64, AcrossChainConfig> {
let mut configs = HashMap::default();
let make_config = |chain_id: u64, tokens: &[(&str, &str)]| -> AcrossChainConfig {
let token_map: HashMap<String, Address> = tokens
.iter()
.filter_map(|(sym, addr)| addr.parse::<Address>().ok().map(|a| ((*sym).to_owned(), a)))
.collect();
AcrossChainConfig { chain_id, tokens: token_map }
};
configs.insert(
SupportedChainId::Mainnet.as_u64(),
make_config(
SupportedChainId::Mainnet.as_u64(),
&[
("usdc", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
("weth", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
("wbtc", "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"),
("dai", "0x6B175474E89094C44Da98b954EedeAC495271d0F"),
("usdt", "0xdAC17F958D2ee523a2206206994597C13D831ec7"),
],
),
);
configs.insert(
SupportedChainId::Polygon.as_u64(),
make_config(
SupportedChainId::Polygon.as_u64(),
&[
("usdc", "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"),
("weth", "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"),
("wbtc", "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6"),
("dai", "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063"),
("usdt", "0xc2132D05D31c914a87C6611C10748AEb04B58e8F"),
],
),
);
configs.insert(
SupportedChainId::ArbitrumOne.as_u64(),
make_config(
SupportedChainId::ArbitrumOne.as_u64(),
&[
("usdc", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"),
("weth", "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
("wbtc", "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f"),
("dai", "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"),
("usdt", "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
],
),
);
configs.insert(
SupportedChainId::Base.as_u64(),
make_config(
SupportedChainId::Base.as_u64(),
&[
("usdc", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
("weth", "0x4200000000000000000000000000000000000006"),
("dai", "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"),
],
),
);
configs.insert(
10,
make_config(
10,
&[
("usdc", "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85"),
("weth", "0x4200000000000000000000000000000000000006"),
("wbtc", "0x68f180fcCe6836688e9084f035309E29Bf0A2095"),
("dai", "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1"),
("usdt", "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58"),
],
),
);
configs
}
#[must_use]
pub fn get_chain_configs(
source_chain_id: u64,
target_chain_id: u64,
) -> Option<(AcrossChainConfig, AcrossChainConfig)> {
let mapping = across_token_mapping();
let source = mapping.get(&source_chain_id)?.clone();
let target = mapping.get(&target_chain_id)?.clone();
Some((source, target))
}
#[must_use]
pub fn get_token_symbol(
token_address: Address,
chain_config: &AcrossChainConfig,
) -> Option<String> {
chain_config.tokens.iter().find(|(_, addr)| **addr == token_address).map(|(sym, _)| sym.clone())
}
#[must_use]
pub fn get_token_address(token_symbol: &str, chain_config: &AcrossChainConfig) -> Option<Address> {
chain_config.tokens.get(token_symbol).copied()
}
#[must_use]
pub fn get_token_by_address_and_chain_id(
token_address: Address,
chain_id: u64,
) -> Option<(String, Address)> {
let mapping = across_token_mapping();
let config = mapping.get(&chain_id)?;
config
.tokens
.iter()
.find(|(_, addr)| **addr == token_address)
.map(|(sym, addr)| (sym.clone(), *addr))
}
pub fn to_bridge_quote_result(
request: &super::types::QuoteBridgeRequest,
slippage_bps: u32,
suggested_fees: &AcrossSuggestedFeesResponse,
) -> Result<BridgeQuoteResult, BridgeError> {
let amounts_and_costs = to_amounts_and_costs(request, slippage_bps, suggested_fees)?;
let bridge_fee =
suggested_fees.relayer_capital_fee.total.parse::<u128>().map_or(U256::ZERO, U256::from);
let destination_gas_fee =
suggested_fees.relayer_gas_fee.total.parse::<u128>().map_or(U256::ZERO, U256::from);
let min_deposit =
suggested_fees.limits.min_deposit.parse::<u128>().map_or(U256::ZERO, U256::from);
let max_deposit =
suggested_fees.limits.max_deposit.parse::<u128>().map_or(U256::ZERO, U256::from);
let quote_timestamp = suggested_fees.timestamp.parse::<u64>().map_or(0, |v| v);
let expected_fill_time = suggested_fees.estimated_fill_time_sec.parse::<u64>().ok();
Ok(BridgeQuoteResult {
id: None,
signature: None,
attestation_signature: None,
quote_body: serde_json::to_string(suggested_fees).ok(),
is_sell: request.kind == OrderKind::Sell,
amounts_and_costs,
expected_fill_time_seconds: expected_fill_time,
quote_timestamp,
fees: BridgeFees { bridge_fee, destination_gas_fee },
limits: BridgeLimits { min_deposit, max_deposit },
})
}
fn to_amounts_and_costs(
request: &super::types::QuoteBridgeRequest,
slippage_bps: u32,
suggested_fees: &AcrossSuggestedFeesResponse,
) -> Result<BridgeQuoteAmountsAndCosts, BridgeError> {
let sell_amount_before_fee = request.sell_amount;
let buy_decimals = U256::from(10u64).pow(U256::from(request.buy_token_decimals));
let sell_decimals = U256::from(10u64).pow(U256::from(request.sell_token_decimals));
let buy_amount_before_fee = (sell_amount_before_fee * buy_decimals) / sell_decimals;
let total_relay_fee_pct: u128 =
suggested_fees.total_relay_fee.pct.parse().map_err(|_parse_err| {
BridgeError::QuoteError("invalid totalRelayFee.pct".to_owned())
})?;
let buy_amount_after_fee = apply_pct_fee(buy_amount_before_fee, total_relay_fee_pct)?;
let fee_sell_token =
sell_amount_before_fee - apply_pct_fee(sell_amount_before_fee, total_relay_fee_pct)?;
let fee_buy_token = buy_amount_before_fee - buy_amount_after_fee;
let buy_amount_after_slippage = apply_bps(buy_amount_after_fee, slippage_bps);
let fee_bps = pct_to_bps(total_relay_fee_pct)?;
Ok(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,
amount_in_sell_currency: fee_sell_token,
amount_in_buy_currency: fee_buy_token,
},
},
slippage_bps,
})
}
#[must_use]
pub const fn map_across_status_to_bridge_status(status: AcrossDepositStatus) -> BridgeStatus {
match status {
AcrossDepositStatus::Filled | AcrossDepositStatus::SlowFillRequested => {
BridgeStatus::Executed
}
AcrossDepositStatus::Pending => BridgeStatus::InProgress,
AcrossDepositStatus::Expired => BridgeStatus::Expired,
AcrossDepositStatus::Refunded => BridgeStatus::Refund,
}
}
#[must_use]
pub fn is_valid_across_status_response(response: &serde_json::Value) -> bool {
response.get("status").and_then(|s| s.as_str()).is_some()
}
pub const ACROSS_FUNDS_DEPOSITED_TOPIC: &str = "event FundsDeposited(bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 indexed destinationChainId, uint256 indexed depositId, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes32 indexed depositor, bytes32 recipient, bytes32 exclusiveRelayer, bytes message)";
pub const ACROSS_DEPOSIT_EVENT_INTERFACE: &str = ACROSS_FUNDS_DEPOSITED_TOPIC;
pub const COW_TRADE_EVENT_SIGNATURE: &str = "event Trade(address indexed owner, address sellToken, address buyToken, uint256 sellAmount, uint256 buyAmount, uint256 feeAmount, bytes orderUid)";
pub const COW_TRADE_EVENT_INTERFACE: &str = COW_TRADE_EVENT_SIGNATURE;
use alloy_primitives::B256;
#[derive(Debug, Clone)]
pub struct EvmLogEntry {
pub address: Address,
pub topics: Vec<B256>,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct CowTradeEvent {
pub owner: Address,
pub sell_token: Address,
pub buy_token: Address,
pub sell_amount: U256,
pub buy_amount: U256,
pub fee_amount: U256,
pub order_uid: String,
}
use crate::types::AcrossDepositEvent;
use alloy_primitives::{hex, keccak256};
#[must_use]
fn across_funds_deposited_topic0() -> B256 {
keccak256(
"FundsDeposited(bytes32,bytes32,uint256,uint256,uint256,uint256,uint32,uint32,uint32,bytes32,bytes32,bytes32,bytes)",
)
}
#[must_use]
fn cow_trade_event_topic0() -> B256 {
keccak256("Trade(address,address,address,uint256,uint256,uint256,bytes)")
}
#[must_use]
fn bytes32_to_address(word: &[u8]) -> Address {
if word.len() < 32 {
return Address::ZERO;
}
Address::from_slice(&word[12..32])
}
const fn bytes32_to_u256(word: &[u8]) -> U256 {
if word.len() < 32 {
return U256::ZERO;
}
U256::from_be_slice(word)
}
#[must_use]
fn bytes32_to_u32(word: &[u8]) -> u32 {
if word.len() < 32 {
return 0;
}
u32::from_be_bytes([word[28], word[29], word[30], word[31]])
}
#[must_use]
pub fn get_across_deposit_events(chain_id: u64, logs: &[EvmLogEntry]) -> Vec<AcrossDepositEvent> {
let spoke_pool_addresses = across_spoke_pool_addresses();
let Some(spoke_pool_address) = spoke_pool_addresses.get(&chain_id) else {
return vec![];
};
let topic0 = across_funds_deposited_topic0();
logs.iter()
.filter(|log| {
log.address == *spoke_pool_address && log.topics.first().is_some_and(|t| *t == topic0)
})
.filter_map(parse_across_deposit_event)
.collect()
}
fn parse_across_deposit_event(log: &EvmLogEntry) -> Option<AcrossDepositEvent> {
if log.topics.len() < 4 {
return None;
}
let destination_chain_id = bytes32_to_u256(log.topics[1].as_slice()).to::<u64>();
let deposit_id = bytes32_to_u256(log.topics[2].as_slice());
let depositor = bytes32_to_address(log.topics[3].as_slice());
let data = &log.data;
if data.len() < 9 * 32 {
return None;
}
let input_token = bytes32_to_address(&data[0..32]);
let output_token = bytes32_to_address(&data[32..64]);
let input_amount = bytes32_to_u256(&data[64..96]);
let output_amount = bytes32_to_u256(&data[96..128]);
let quote_timestamp = bytes32_to_u32(&data[128..160]);
let fill_deadline = bytes32_to_u32(&data[160..192]);
let exclusivity_deadline = bytes32_to_u32(&data[192..224]);
let recipient = bytes32_to_address(&data[224..256]);
let exclusive_relayer = bytes32_to_address(&data[256..288]);
Some(AcrossDepositEvent {
input_token,
output_token,
input_amount,
output_amount,
destination_chain_id,
deposit_id,
quote_timestamp,
fill_deadline,
exclusivity_deadline,
depositor,
recipient,
exclusive_relayer,
})
}
#[must_use]
pub fn get_cow_trade_events(
chain_id: u64,
logs: &[EvmLogEntry],
settlement_override: Option<Address>,
) -> Vec<CowTradeEvent> {
let chain = cow_chains::SupportedChainId::try_from_u64(chain_id);
let default_settlement = chain.map(cow_chains::settlement_contract);
let topic0 = cow_trade_event_topic0();
logs.iter()
.filter(|log| {
let addr_match = default_settlement.is_some_and(|a| a == log.address) ||
settlement_override.is_some_and(|a| a == log.address);
addr_match && log.topics.first().is_some_and(|t| *t == topic0)
})
.filter_map(parse_cow_trade_event)
.collect()
}
fn parse_cow_trade_event(log: &EvmLogEntry) -> Option<CowTradeEvent> {
if log.topics.len() < 2 {
return None;
}
let owner = bytes32_to_address(log.topics[1].as_slice());
let data = &log.data;
if data.len() < 7 * 32 {
return None;
}
let sell_token = bytes32_to_address(&data[0..32]);
let buy_token = bytes32_to_address(&data[32..64]);
let sell_amount = bytes32_to_u256(&data[64..96]);
let buy_amount = bytes32_to_u256(&data[96..128]);
let fee_amount = bytes32_to_u256(&data[128..160]);
let offset = bytes32_to_u256(&data[160..192]).to::<usize>();
let uid_start = offset + 32; if data.len() < offset + 32 {
return None;
}
let uid_len = bytes32_to_u256(&data[offset..offset + 32]).to::<usize>();
if data.len() < uid_start + uid_len {
return None;
}
let order_uid = format!("0x{}", hex::encode(&data[uid_start..uid_start + uid_len]));
Some(CowTradeEvent {
owner,
sell_token,
buy_token,
sell_amount,
buy_amount,
fee_amount,
order_uid,
})
}
use crate::types::BridgingDepositParams;
#[must_use]
pub fn get_deposit_params(
chain_id: u64,
order_id: &str,
logs: &[EvmLogEntry],
settlement_override: Option<Address>,
) -> Option<BridgingDepositParams> {
let deposit_events = get_across_deposit_events(chain_id, logs);
if deposit_events.is_empty() {
return None;
}
let cow_trade_events = get_cow_trade_events(chain_id, logs, settlement_override);
let order_trade_index = cow_trade_events.iter().position(|e| e.order_uid == order_id)?;
let deposit_event = deposit_events.get(order_trade_index)?;
Some(BridgingDepositParams {
input_token_address: deposit_event.input_token,
output_token_address: deposit_event.output_token,
input_amount: deposit_event.input_amount,
output_amount: Some(deposit_event.output_amount),
owner: deposit_event.depositor,
quote_timestamp: Some(u64::from(deposit_event.quote_timestamp)),
fill_deadline: Some(u64::from(deposit_event.fill_deadline)),
recipient: deposit_event.recipient,
source_chain_id: chain_id,
destination_chain_id: deposit_event.destination_chain_id,
bridging_id: deposit_event.deposit_id.to_string(),
})
}
use crate::types::QuoteBridgeRequest;
#[derive(Debug, Clone)]
pub struct AcrossDepositCallParams {
pub request: QuoteBridgeRequest,
pub suggested_fees: AcrossSuggestedFeesResponse,
pub cow_shed_account: Address,
}
pub fn create_across_deposit_call(
params: &AcrossDepositCallParams,
) -> Result<cow_chains::EvmCall, BridgeError> {
let spoke_pools = across_spoke_pool_addresses();
let spoke_pool = spoke_pools.get(¶ms.request.sell_chain_id).ok_or_else(|| {
BridgeError::TxBuildError(format!(
"spoke pool not found for chain {}",
params.request.sell_chain_id
))
})?;
let receiver = params
.request
.receiver
.as_deref()
.and_then(|r| r.parse::<Address>().ok())
.map_or(params.request.account, |a| a);
let suggested = ¶ms.suggested_fees;
let fill_deadline: u32 = suggested.fill_deadline.parse().map_or(0, |v| v);
let exclusivity_deadline: u32 = suggested.exclusivity_deadline.parse().map_or(0, |v| v);
let quote_timestamp: u32 = suggested.timestamp.parse().map_or(0, |v| v);
let exclusive_relayer: Address =
suggested.exclusive_relayer.parse().map_or(Address::ZERO, |a| a);
let selector = &keccak256(
"depositV3(address,address,address,address,uint256,uint256,uint256,address,uint32,uint32,uint32,bytes)",
)[..4];
let mut calldata = Vec::with_capacity(4 + 12 * 32 + 64);
calldata.extend_from_slice(selector);
calldata.extend_from_slice(&left_pad_address(params.cow_shed_account));
calldata.extend_from_slice(&left_pad_address(receiver));
calldata.extend_from_slice(&left_pad_address(params.request.sell_token));
calldata.extend_from_slice(&left_pad_address(params.request.buy_token));
calldata.extend_from_slice(&pad_u256(params.request.sell_amount));
let total_fee_pct: u128 = suggested.total_relay_fee.pct.parse().map_or(0, |v| v);
let output_amount = crate::utils::apply_pct_fee(params.request.sell_amount, total_fee_pct)
.map_or(params.request.sell_amount, |v| v);
calldata.extend_from_slice(&pad_u256(output_amount));
calldata.extend_from_slice(&pad_u256(U256::from(params.request.buy_chain_id)));
calldata.extend_from_slice(&left_pad_address(exclusive_relayer));
calldata.extend_from_slice(&pad_u256(U256::from(quote_timestamp)));
calldata.extend_from_slice(&pad_u256(U256::from(fill_deadline)));
calldata.extend_from_slice(&pad_u256(U256::from(exclusivity_deadline)));
calldata.extend_from_slice(&pad_u256(U256::from(12u64 * 32))); calldata.extend_from_slice(&pad_u256(U256::ZERO));
Ok(cow_chains::EvmCall { to: *spoke_pool, data: calldata, value: U256::ZERO })
}
fn left_pad_address(addr: Address) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[12..32].copy_from_slice(addr.as_slice());
buf
}
const fn pad_u256(val: U256) -> [u8; 32] {
val.to_be_bytes::<32>()
}