use crate::config::{
SupportedChainId,
chains::get_chain_info,
contracts::{
SETTLEMENT_CONTRACT, SETTLEMENT_CONTRACT_STAGING, VAULT_RELAYER, VAULT_RELAYER_STAGING,
},
wrapped_native_currency,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AddressKey {
Evm(String),
Btc(String),
Sol(String),
}
impl AddressKey {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Evm(s) | Self::Btc(s) | Self::Sol(s) => s,
}
}
}
impl std::fmt::Display for AddressKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TokenId(String);
impl TokenId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TokenId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[must_use]
pub fn is_evm_address(address: &str) -> bool {
if address.len() != 42 {
return false;
}
let Some(hex) = address.strip_prefix("0x").or_else(|| address.strip_prefix("0X")) else {
return false;
};
hex.len() == 40 && hex.bytes().all(|b| b.is_ascii_hexdigit())
}
#[must_use]
pub fn is_btc_address(address: &str) -> bool {
let len = address.len();
if !(25..=62).contains(&len) {
return false;
}
is_btc_legacy(address) || is_btc_bech32_mainnet(address)
}
#[must_use]
pub fn is_solana_address(address: &str) -> bool {
let len = address.len();
if !(32..=44).contains(&len) {
return false;
}
address.bytes().all(is_base58_char)
}
#[must_use]
pub fn is_supported_address(address: &str) -> bool {
is_evm_address(address) || is_btc_address(address) || is_solana_address(address)
}
#[must_use]
pub fn get_evm_address_key(address: &str) -> String {
address.to_ascii_lowercase()
}
#[must_use]
pub fn get_btc_address_key(address: &str) -> String {
address.to_owned()
}
#[must_use]
pub fn get_sol_address_key(address: &str) -> String {
address.to_owned()
}
#[must_use]
pub fn get_address_key(address: &str) -> AddressKey {
if is_evm_address(address) {
AddressKey::Evm(get_evm_address_key(address))
} else if is_btc_address(address) {
AddressKey::Btc(address.to_owned())
} else {
AddressKey::Sol(address.to_owned())
}
}
#[must_use]
#[allow(clippy::shadow_reuse, reason = "destructuring Option parameters into inner values")]
pub fn are_addresses_equal(a: Option<&str>, b: Option<&str>) -> bool {
let (Some(a), Some(b)) = (a, b) else {
return false;
};
let a_is_evm = is_evm_address(a);
let b_is_evm = is_evm_address(b);
if a_is_evm && b_is_evm {
return get_evm_address_key(a) == get_evm_address_key(b);
}
a == b
}
pub trait TokenLike {
fn chain_id(&self) -> u64;
fn address(&self) -> &str;
}
#[must_use]
#[allow(clippy::shadow_reuse, reason = "destructuring Option parameters into inner values")]
pub fn are_tokens_equal<T: TokenLike>(a: Option<&T>, b: Option<&T>) -> bool {
let (Some(a), Some(b)) = (a, b) else {
return false;
};
get_token_id(a.chain_id(), a.address()) == get_token_id(b.chain_id(), b.address())
}
#[must_use]
pub fn get_token_id(chain_id: u64, address: &str) -> TokenId {
let key = get_address_key(address);
TokenId(format!("{chain_id}:{key}"))
}
#[must_use]
pub fn is_native_token(chain_id: u64, address: &str) -> bool {
let Some(chain_info) = get_chain_info(chain_id) else {
return false;
};
let native_addr = chain_info.native_currency().address;
are_addresses_equal(Some(native_addr), Some(address))
}
#[must_use]
pub fn is_wrapped_native_token(chain_id: u64, address: &str) -> bool {
let Some(supported) = SupportedChainId::try_from_u64(chain_id) else {
return false;
};
let wrapped = wrapped_native_currency(supported);
are_addresses_equal(Some(&format!("{:#x}", wrapped.address)), Some(address))
}
#[must_use]
pub fn is_cow_settlement_contract(address: &str, _chain_id: SupportedChainId) -> bool {
let key = get_address_key(address);
let prod = format!("{SETTLEMENT_CONTRACT:#x}");
let staging = format!("{SETTLEMENT_CONTRACT_STAGING:#x}");
are_addresses_equal(Some(key.as_str()), Some(&prod)) ||
are_addresses_equal(Some(key.as_str()), Some(&staging))
}
#[must_use]
pub fn is_cow_vault_relayer_contract(address: &str, _chain_id: SupportedChainId) -> bool {
let key = get_address_key(address);
let prod = format!("{VAULT_RELAYER:#x}");
let staging = format!("{VAULT_RELAYER_STAGING:#x}");
are_addresses_equal(Some(key.as_str()), Some(&prod)) ||
are_addresses_equal(Some(key.as_str()), Some(&staging))
}
const fn is_base58_char(b: u8) -> bool {
matches!(b,
b'1'..=b'9'
| b'A'..=b'H'
| b'J'..=b'N'
| b'P'..=b'Z'
| b'a'..=b'k'
| b'm'..=b'z'
)
}
fn is_btc_legacy(address: &str) -> bool {
let bytes = address.as_bytes();
if bytes.is_empty() {
return false;
}
if bytes[0] != b'1' && bytes[0] != b'3' {
return false;
}
let len = bytes.len();
if !(25..=34).contains(&len) {
return false;
}
bytes[1..].iter().all(|&b| is_base58_char(b))
}
fn is_btc_bech32_mainnet(address: &str) -> bool {
let len = address.len();
if !(42..=62).contains(&len) {
return false;
}
if let Some(rest) = address.strip_prefix("bc1") {
rest.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
} else if let Some(rest) = address.strip_prefix("BC1") {
rest.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
} else {
false
}
}
pub use is_cow_settlement_contract as is_co_w_settlement_contract;
pub use is_cow_vault_relayer_contract as is_co_w_vault_relayer_contract;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn evm_valid() {
assert!(is_evm_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
assert!(is_evm_address("0x0000000000000000000000000000000000000000"));
}
#[test]
fn evm_invalid() {
assert!(!is_evm_address(""));
assert!(!is_evm_address("0x"));
assert!(!is_evm_address("0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"));
assert!(!is_evm_address("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
assert!(!is_evm_address("0xabcd"));
assert!(!is_evm_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2a"));
}
#[test]
fn btc_legacy_valid() {
assert!(is_btc_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"));
assert!(is_btc_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"));
}
#[test]
fn btc_bech32_valid() {
assert!(is_btc_address("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"));
}
#[test]
fn btc_invalid() {
assert!(!is_btc_address(""));
assert!(!is_btc_address("not_a_btc_address"));
}
#[test]
fn sol_valid() {
assert!(is_solana_address("11111111111111111111111111111111"));
assert!(is_solana_address("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"));
}
#[test]
fn sol_invalid() {
assert!(!is_solana_address("short"));
assert!(!is_solana_address("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO"));
}
#[test]
fn evm_case_insensitive_equal() {
assert!(are_addresses_equal(
Some("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
Some("0xabcdef1234567890abcdef1234567890abcdef12"),
));
}
#[test]
fn none_never_equal() {
assert!(!are_addresses_equal(None, Some("0x1234")));
assert!(!are_addresses_equal(Some("0x1234"), None));
assert!(!are_addresses_equal(None, None));
}
#[test]
fn token_id_normalizes_evm() {
let id = get_token_id(1, "0xABCDEF1234567890abcdef1234567890ABCDEF12");
assert_eq!(id.as_str(), "1:0xabcdef1234567890abcdef1234567890abcdef12");
}
#[test]
fn native_token_detected() {
assert!(is_native_token(1, "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"));
}
#[test]
fn wrapped_native_token_detected() {
assert!(is_wrapped_native_token(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn settlement_contract_detected() {
assert!(is_cow_settlement_contract(
"0x9008D19f58AAbD9eD0D60971565AA8510560ab41",
SupportedChainId::Mainnet,
));
assert!(is_cow_settlement_contract(
"0xf553d092b50bdcbddeD1A99aF2cA29FBE5E2CB13",
SupportedChainId::Mainnet,
));
}
#[test]
fn vault_relayer_detected() {
assert!(is_cow_vault_relayer_contract(
"0xC92E8bdf79f0507f65a392b0ab4667716BFE0110",
SupportedChainId::Mainnet,
));
}
#[test]
fn evm_address_0x_prefix() {
assert!(is_evm_address("0XC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn evm_address_no_hex_prefix() {
assert!(!is_evm_address("xxC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn btc_legacy_p2sh_valid() {
assert!(is_btc_address("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"));
}
#[test]
fn btc_bech32_uppercase_valid() {
assert!(is_btc_address("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"));
}
#[test]
fn btc_address_too_short() {
assert!(!is_btc_address("1234567890123456789012345"[..24].to_string().as_str()));
}
#[test]
fn btc_address_bad_start_char() {
assert!(!is_btc_address("2A1zP1eP5QGefi2DMPTfTL5SLmv7D"));
}
#[test]
fn btc_bech32_too_short() {
assert!(!is_btc_address("bc1qw508d6qejxtdg4y5r3zarvar"));
}
#[test]
fn btc_bech32_bad_prefix() {
let addr = "xc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
assert!(!is_btc_address(addr));
}
#[test]
fn solana_address_with_invalid_base58() {
assert!(!is_solana_address("OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO"));
assert!(!is_solana_address("llllllllllllllllllllllllllllllllll"));
}
#[test]
fn is_supported_address_solana() {
assert!(is_supported_address("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"));
}
#[test]
fn get_address_key_btc() {
let key = get_address_key("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
assert!(matches!(key, AddressKey::Btc(_)));
assert_eq!(key.as_str(), "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
}
#[test]
fn get_address_key_unknown_falls_to_sol() {
let key = get_address_key("some-unknown-address-format");
assert!(matches!(key, AddressKey::Sol(_)));
}
#[test]
fn address_key_display() {
let key = get_address_key("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
assert_eq!(format!("{key}"), "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
}
#[test]
fn token_id_display() {
let id = get_token_id(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
assert_eq!(format!("{id}"), "1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
}
#[test]
fn are_addresses_equal_non_evm_exact() {
assert!(are_addresses_equal(
Some("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
Some("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
));
assert!(!are_addresses_equal(
Some("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
Some("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
));
}
#[test]
fn are_tokens_equal_none() {
struct Tok {
chain: u64,
addr: String,
}
impl TokenLike for Tok {
fn chain_id(&self) -> u64 {
self.chain
}
fn address(&self) -> &str {
&self.addr
}
}
let t = Tok { chain: 1, addr: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into() };
assert!(!are_tokens_equal::<Tok>(None, Some(&t)));
assert!(!are_tokens_equal::<Tok>(Some(&t), None));
assert!(!are_tokens_equal::<Tok>(None, None));
}
#[test]
fn are_tokens_equal_different_chain() {
struct Tok {
chain: u64,
addr: String,
}
impl TokenLike for Tok {
fn chain_id(&self) -> u64 {
self.chain
}
fn address(&self) -> &str {
&self.addr
}
}
let a = Tok { chain: 1, addr: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into() };
let b = Tok { chain: 100, addr: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into() };
assert!(!are_tokens_equal(Some(&a), Some(&b)));
}
#[test]
fn is_native_token_unknown_chain() {
assert!(!is_native_token(9999, "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"));
}
#[test]
fn is_wrapped_native_token_unknown_chain() {
assert!(!is_wrapped_native_token(9999, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn vault_relayer_staging_detected() {
assert!(is_cow_vault_relayer_contract(
"0xc7242d167563352e2bca4d71c043fbe542db8fb2",
SupportedChainId::Mainnet,
));
}
#[test]
fn vault_relayer_non_matching() {
assert!(!is_cow_vault_relayer_contract(
"0x0000000000000000000000000000000000000000",
SupportedChainId::Mainnet,
));
}
#[test]
fn settlement_contract_non_matching() {
assert!(!is_cow_settlement_contract(
"0x0000000000000000000000000000000000000000",
SupportedChainId::Mainnet,
));
}
#[test]
fn btc_legacy_with_base58_invalid_chars() {
assert!(!is_btc_address("1I1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"));
}
#[test]
fn get_btc_address_key_identity() {
let addr = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
assert_eq!(get_btc_address_key(addr), addr);
}
#[test]
fn get_sol_address_key_identity() {
let addr = "11111111111111111111111111111111";
assert_eq!(get_sol_address_key(addr), addr);
}
#[test]
fn address_key_hash_and_eq() {
use foldhash::HashSet;
let a = get_address_key("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let b = get_address_key("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
let mut set = HashSet::default();
set.insert(a);
assert!(set.contains(&b));
}
#[test]
fn is_native_token_gnosis() {
assert!(is_native_token(100, "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"));
}
#[test]
fn is_native_token_non_native() {
assert!(!is_native_token(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn is_wrapped_native_token_gnosis() {
assert!(is_wrapped_native_token(100, "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"));
}
#[test]
fn btc_legacy_too_long() {
assert!(!is_btc_legacy("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNaa"));
}
#[test]
fn btc_bech32_mixed_case_invalid() {
assert!(!is_btc_bech32_mainnet("bc1Qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"));
}
#[test]
fn btc_legacy_empty_is_false() {
assert!(!is_btc_legacy(""));
}
#[test]
fn is_supported_address_btc() {
assert!(is_supported_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"));
}
#[test]
fn is_supported_address_evm() {
assert!(is_supported_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"));
}
#[test]
fn is_supported_address_invalid() {
assert!(!is_supported_address("xyz"));
}
#[test]
fn token_id_hash_eq() {
let a = get_token_id(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2");
let b = get_token_id(1, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2");
assert_eq!(a, b);
}
#[test]
fn get_address_key_sol_fallback() {
let key = get_address_key("11111111111111111111111111111111");
assert!(matches!(key, AddressKey::Btc(_)));
}
}