use std::fmt;
use alloy_primitives::Address;
use super::chain::SupportedChainId;
pub const NATIVE_CURRENCY_ADDRESS: Address = Address::new([0xee; 20]);
pub const EVM_NATIVE_CURRENCY_ADDRESS: Address = NATIVE_CURRENCY_ADDRESS;
pub const SOL_NATIVE_CURRENCY_ADDRESS: &str = "11111111111111111111111111111111";
pub const BTC_CURRENCY_ADDRESS: &str = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa";
#[derive(Debug, Clone, Copy)]
pub struct TokenInfo {
pub address: Address,
pub decimals: u8,
pub symbol: &'static str,
pub name: Option<&'static str>,
pub logo_url: Option<&'static str>,
}
impl TokenInfo {
#[must_use]
pub const fn new(address: Address, decimals: u8, symbol: &'static str) -> Self {
Self { address, decimals, symbol, name: None, logo_url: None }
}
#[must_use]
pub const fn with_name(mut self, name: &'static str) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub const fn with_logo_url(mut self, url: &'static str) -> Self {
self.logo_url = Some(url);
self
}
#[must_use]
pub const fn has_name(&self) -> bool {
self.name.is_some()
}
#[must_use]
pub const fn has_logo_url(&self) -> bool {
self.logo_url.is_some()
}
#[must_use]
pub fn is_native_currency(&self) -> bool {
self.address == NATIVE_CURRENCY_ADDRESS
}
#[must_use]
pub fn decimals_multiplier(&self) -> u128 {
10u128.pow(u32::from(self.decimals))
}
#[must_use]
pub const fn decimals_u64(&self) -> u64 {
self.decimals as u64
}
#[must_use]
pub const fn is_zero_decimals(&self) -> bool {
self.decimals == 0
}
}
impl fmt::Display for TokenInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({:#x})", self.symbol, self.address)
}
}
impl From<TokenInfo> for Address {
fn from(t: TokenInfo) -> Self {
t.address
}
}
impl From<TokenInfo> for u8 {
fn from(t: TokenInfo) -> Self {
t.decimals
}
}
#[must_use]
pub const fn get_wrapped_token_for_chain(chain: SupportedChainId) -> Option<TokenInfo> {
Some(wrapped_native_currency(chain))
}
#[must_use]
pub const fn wrapped_native_currency(chain: SupportedChainId) -> TokenInfo {
match chain {
SupportedChainId::Mainnet => TokenInfo {
address: address("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"),
decimals: 18,
symbol: "WETH",
name: Some("Wrapped Ether"),
logo_url: None,
},
SupportedChainId::GnosisChain => TokenInfo {
address: address("e91d153e0b41518a2ce8dd3d7944fa863463a97d"),
decimals: 18,
symbol: "WXDAI",
name: Some("Wrapped XDAI"),
logo_url: None,
},
SupportedChainId::ArbitrumOne => TokenInfo {
address: address("82af49447d8a07e3bd95bd0d56f35241523fbab1"),
decimals: 18,
symbol: "WETH",
name: Some("Wrapped Ether"),
logo_url: None,
},
SupportedChainId::Base | SupportedChainId::Ink => TokenInfo {
address: address("4200000000000000000000000000000000000006"),
decimals: 18,
symbol: "WETH",
name: Some("Wrapped Ether"),
logo_url: None,
},
SupportedChainId::Sepolia => TokenInfo {
address: address("fff9976782d46cc05630d1f6ebab18b2324d6b14"),
decimals: 18,
symbol: "WETH",
name: Some("Wrapped Ether"),
logo_url: None,
},
SupportedChainId::Polygon => TokenInfo {
address: address("0d500b1d8e8ef31e21c99d1db9a6444d3adf1270"),
decimals: 18,
symbol: "WPOL",
name: Some("Wrapped POL"),
logo_url: None,
},
SupportedChainId::Avalanche => TokenInfo {
address: address("b31f66aa3c1e785363f0875a1b74e27b85fd66c7"),
decimals: 18,
symbol: "WAVAX",
name: Some("Wrapped AVAX"),
logo_url: None,
},
SupportedChainId::BnbChain => TokenInfo {
address: address("bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c"),
decimals: 18,
symbol: "WBNB",
name: Some("Wrapped BNB"),
logo_url: None,
},
SupportedChainId::Linea => TokenInfo {
address: address("e5d7c2a44ffddf6b295a15c148167daaaf5cf34e"),
decimals: 18,
symbol: "WETH",
name: Some("Wrapped Ether"),
logo_url: None,
},
SupportedChainId::Lens => TokenInfo {
address: address("6bdc36e20d267ff0dd6097799f82e78907105e2f"),
decimals: 18,
symbol: "WGHO",
name: Some("Wrapped GHO"),
logo_url: None,
},
SupportedChainId::Plasma => TokenInfo {
address: address("6100e367285b01f48d07953803a2d8dca5d19873"),
decimals: 18,
symbol: "WXPL",
name: Some("Wrapped XPL"),
logo_url: None,
},
}
}
const fn address(hex: &str) -> Address {
let bytes = hex.as_bytes();
let mut out = [0u8; 20];
let mut i = 0;
while i < 20 {
out[i] = nibble(bytes[i * 2]) << 4 | nibble(bytes[i * 2 + 1]);
i += 1;
}
Address::new(out)
}
const fn nibble(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_currency_address_is_all_ee() {
assert_eq!(NATIVE_CURRENCY_ADDRESS, Address::new([0xee; 20]));
}
#[test]
fn evm_native_currency_address_equals_native() {
assert_eq!(EVM_NATIVE_CURRENCY_ADDRESS, NATIVE_CURRENCY_ADDRESS);
}
#[test]
fn sol_native_currency_address_is_all_ones() {
assert_eq!(SOL_NATIVE_CURRENCY_ADDRESS, "11111111111111111111111111111111");
}
#[test]
fn btc_currency_address_is_genesis() {
assert_eq!(BTC_CURRENCY_ADDRESS, "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
}
#[test]
fn new_sets_fields_and_defaults() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert_eq!(token.address, Address::ZERO);
assert_eq!(token.decimals, 18);
assert_eq!(token.symbol, "WETH");
assert!(token.name.is_none());
assert!(token.logo_url.is_none());
}
#[test]
fn with_name_sets_name() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH").with_name("Wrapped Ether");
assert_eq!(token.name, Some("Wrapped Ether"));
}
#[test]
fn with_logo_url_sets_url() {
let token =
TokenInfo::new(Address::ZERO, 18, "WETH").with_logo_url("https://example.com/weth.png");
assert_eq!(token.logo_url, Some("https://example.com/weth.png"));
}
#[test]
fn chaining_with_name_and_logo() {
let token = TokenInfo::new(Address::ZERO, 6, "USDC")
.with_name("USD Coin")
.with_logo_url("https://example.com/usdc.png");
assert_eq!(token.name, Some("USD Coin"));
assert_eq!(token.logo_url, Some("https://example.com/usdc.png"));
}
#[test]
fn has_name_false_when_none() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert!(!token.has_name());
}
#[test]
fn has_name_true_when_set() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH").with_name("Wrapped Ether");
assert!(token.has_name());
}
#[test]
fn has_logo_url_false_when_none() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert!(!token.has_logo_url());
}
#[test]
fn has_logo_url_true_when_set() {
let token =
TokenInfo::new(Address::ZERO, 18, "WETH").with_logo_url("https://example.com/w.png");
assert!(token.has_logo_url());
}
#[test]
fn is_native_currency_true_for_sentinel() {
let token = TokenInfo::new(NATIVE_CURRENCY_ADDRESS, 18, "ETH");
assert!(token.is_native_currency());
}
#[test]
fn is_native_currency_false_for_regular() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert!(!token.is_native_currency());
}
#[test]
fn decimals_multiplier_18() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert_eq!(token.decimals_multiplier(), 1_000_000_000_000_000_000u128);
}
#[test]
fn decimals_multiplier_6() {
let token = TokenInfo::new(Address::ZERO, 6, "USDC");
assert_eq!(token.decimals_multiplier(), 1_000_000u128);
}
#[test]
fn decimals_multiplier_0() {
let token = TokenInfo::new(Address::ZERO, 0, "NFT");
assert_eq!(token.decimals_multiplier(), 1u128);
}
#[test]
fn decimals_multiplier_8() {
let token = TokenInfo::new(Address::ZERO, 8, "WBTC");
assert_eq!(token.decimals_multiplier(), 100_000_000u128);
}
#[test]
fn decimals_u64_returns_widened_value() {
let token = TokenInfo::new(Address::ZERO, 6, "USDC");
assert_eq!(token.decimals_u64(), 6u64);
}
#[test]
fn decimals_u64_zero() {
let token = TokenInfo::new(Address::ZERO, 0, "NFT");
assert_eq!(token.decimals_u64(), 0u64);
}
#[test]
fn is_zero_decimals_true() {
let token = TokenInfo::new(Address::ZERO, 0, "NFT");
assert!(token.is_zero_decimals());
}
#[test]
fn is_zero_decimals_false() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
assert!(!token.is_zero_decimals());
}
#[test]
fn display_format() {
let token = TokenInfo::new(Address::ZERO, 18, "WETH");
let s = format!("{token}");
assert!(s.contains("WETH"));
assert!(s.contains("0x"));
}
#[test]
fn from_token_info_to_address() {
let addr = Address::new([0x11; 20]);
let token = TokenInfo::new(addr, 18, "TKN");
let converted: Address = token.into();
assert_eq!(converted, addr);
}
#[test]
fn from_token_info_to_u8() {
let token = TokenInfo::new(Address::ZERO, 6, "USDC");
let decimals: u8 = token.into();
assert_eq!(decimals, 6);
}
#[test]
fn get_wrapped_token_for_chain_mainnet() {
let maybe_token = get_wrapped_token_for_chain(SupportedChainId::Mainnet);
assert!(maybe_token.is_some());
let token = maybe_token.unwrap();
assert_eq!(token.symbol, "WETH");
assert_eq!(token.decimals, 18);
assert!(token.has_name());
}
#[test]
fn get_wrapped_token_for_chain_gnosis() {
let token = get_wrapped_token_for_chain(SupportedChainId::GnosisChain).unwrap();
assert_eq!(token.symbol, "WXDAI");
assert_eq!(token.name, Some("Wrapped XDAI"));
}
#[test]
fn wrapped_native_currency_mainnet() {
let weth = wrapped_native_currency(SupportedChainId::Mainnet);
assert_eq!(weth.symbol, "WETH");
assert_eq!(weth.decimals, 18);
assert_eq!(weth.name, Some("Wrapped Ether"));
assert!(!weth.is_native_currency());
}
#[test]
fn wrapped_native_currency_gnosis() {
let wxdai = wrapped_native_currency(SupportedChainId::GnosisChain);
assert_eq!(wxdai.symbol, "WXDAI");
assert_eq!(wxdai.decimals, 18);
}
#[test]
fn wrapped_native_currency_arbitrum() {
let weth = wrapped_native_currency(SupportedChainId::ArbitrumOne);
assert_eq!(weth.symbol, "WETH");
}
#[test]
fn wrapped_native_currency_base() {
let weth = wrapped_native_currency(SupportedChainId::Base);
assert_eq!(weth.symbol, "WETH");
}
#[test]
fn wrapped_native_currency_ink_same_as_base() {
let base = wrapped_native_currency(SupportedChainId::Base);
let ink = wrapped_native_currency(SupportedChainId::Ink);
assert_eq!(base.address, ink.address);
assert_eq!(base.symbol, ink.symbol);
}
#[test]
fn wrapped_native_currency_sepolia() {
let weth = wrapped_native_currency(SupportedChainId::Sepolia);
assert_eq!(weth.symbol, "WETH");
assert_eq!(weth.name, Some("Wrapped Ether"));
}
#[test]
fn wrapped_native_currency_polygon() {
let wpol = wrapped_native_currency(SupportedChainId::Polygon);
assert_eq!(wpol.symbol, "WPOL");
assert_eq!(wpol.name, Some("Wrapped POL"));
}
#[test]
fn wrapped_native_currency_avalanche() {
let wavax = wrapped_native_currency(SupportedChainId::Avalanche);
assert_eq!(wavax.symbol, "WAVAX");
assert_eq!(wavax.name, Some("Wrapped AVAX"));
}
#[test]
fn wrapped_native_currency_bnb() {
let wbnb = wrapped_native_currency(SupportedChainId::BnbChain);
assert_eq!(wbnb.symbol, "WBNB");
assert_eq!(wbnb.name, Some("Wrapped BNB"));
}
#[test]
fn wrapped_native_currency_linea() {
let weth = wrapped_native_currency(SupportedChainId::Linea);
assert_eq!(weth.symbol, "WETH");
}
#[test]
fn wrapped_native_currency_lens() {
let wgho = wrapped_native_currency(SupportedChainId::Lens);
assert_eq!(wgho.symbol, "WGHO");
assert_eq!(wgho.name, Some("Wrapped GHO"));
}
#[test]
fn wrapped_native_currency_plasma() {
let wxpl = wrapped_native_currency(SupportedChainId::Plasma);
assert_eq!(wxpl.symbol, "WXPL");
assert_eq!(wxpl.name, Some("Wrapped XPL"));
}
#[test]
fn all_wrapped_tokens_have_18_decimals() {
let chains = [
SupportedChainId::Mainnet,
SupportedChainId::GnosisChain,
SupportedChainId::ArbitrumOne,
SupportedChainId::Base,
SupportedChainId::Sepolia,
SupportedChainId::Polygon,
SupportedChainId::Avalanche,
SupportedChainId::BnbChain,
SupportedChainId::Linea,
SupportedChainId::Lens,
SupportedChainId::Plasma,
SupportedChainId::Ink,
];
for chain in chains {
let token = wrapped_native_currency(chain);
assert_eq!(token.decimals, 18, "Expected 18 decimals for {chain:?}");
}
}
#[test]
fn all_wrapped_tokens_have_name() {
let chains = [
SupportedChainId::Mainnet,
SupportedChainId::GnosisChain,
SupportedChainId::ArbitrumOne,
SupportedChainId::Base,
SupportedChainId::Sepolia,
SupportedChainId::Polygon,
SupportedChainId::Avalanche,
SupportedChainId::BnbChain,
SupportedChainId::Linea,
SupportedChainId::Lens,
SupportedChainId::Plasma,
SupportedChainId::Ink,
];
for chain in chains {
let token = wrapped_native_currency(chain);
assert!(token.has_name(), "Expected name for {chain:?}");
}
}
#[test]
fn all_wrapped_tokens_are_not_native_currency() {
let chains = [
SupportedChainId::Mainnet,
SupportedChainId::GnosisChain,
SupportedChainId::ArbitrumOne,
SupportedChainId::Base,
SupportedChainId::Sepolia,
SupportedChainId::Polygon,
SupportedChainId::Avalanche,
SupportedChainId::BnbChain,
SupportedChainId::Linea,
SupportedChainId::Lens,
SupportedChainId::Plasma,
SupportedChainId::Ink,
];
for chain in chains {
let token = wrapped_native_currency(chain);
assert!(
!token.is_native_currency(),
"Wrapped token should not be native for {chain:?}"
);
}
}
#[test]
fn all_wrapped_tokens_have_nonzero_address() {
let chains = [
SupportedChainId::Mainnet,
SupportedChainId::GnosisChain,
SupportedChainId::ArbitrumOne,
SupportedChainId::Base,
SupportedChainId::Sepolia,
SupportedChainId::Polygon,
SupportedChainId::Avalanche,
SupportedChainId::BnbChain,
SupportedChainId::Linea,
SupportedChainId::Lens,
SupportedChainId::Plasma,
SupportedChainId::Ink,
];
for chain in chains {
let token = wrapped_native_currency(chain);
assert!(!token.address.is_zero(), "Expected non-zero address for {chain:?}");
}
}
}