use std::{
collections::HashMap,
sync::{LazyLock, OnceLock},
};
use parking_lot::Mutex;
use ustr::Ustr;
use crate::{enums::CurrencyType, types::Currency};
macro_rules! currency_constants {
($(
$accessor:ident => $code:literal, $precision:literal, $iso4217:literal, $name:literal,
$currency_type:ident;
)+) => {
impl Currency {
$(
#[doc = concat!("Returns the ", $name, " (`", $code, "`) currency.")]
#[allow(non_snake_case)]
#[must_use]
pub fn $accessor() -> Self {
static LOCK: OnceLock<Currency> = OnceLock::new();
*LOCK.get_or_init(|| Self {
code: Ustr::from($code),
precision: $precision,
iso4217: $iso4217,
name: Ustr::from($name),
currency_type: CurrencyType::$currency_type,
})
}
)+
}
pub static CURRENCY_MAP: LazyLock<Mutex<HashMap<String, Currency>>> = LazyLock::new(|| {
let mut map = HashMap::new();
$(map.insert(Currency::$accessor().code.to_string(), Currency::$accessor());)+
Mutex::new(map)
});
#[cfg(test)]
fn all_currency_constants() -> Vec<Currency> {
vec![$(Currency::$accessor()),+]
}
};
}
currency_constants! {
AUD => "AUD", 2, 36, "Australian dollar", Fiat;
BRL => "BRL", 2, 986, "Brazilian real", Fiat;
CAD => "CAD", 2, 124, "Canadian dollar", Fiat;
CHF => "CHF", 2, 756, "Swiss franc", Fiat;
CNY => "CNY", 2, 156, "Chinese yuan", Fiat;
CNH => "CNH", 2, 0, "Chinese yuan (offshore)", Fiat;
CZK => "CZK", 2, 203, "Czech koruna", Fiat;
DKK => "DKK", 2, 208, "Danish krone", Fiat;
EUR => "EUR", 2, 978, "Euro", Fiat;
GBP => "GBP", 2, 826, "British Pound", Fiat;
HKD => "HKD", 2, 344, "Hong Kong dollar", Fiat;
HUF => "HUF", 2, 348, "Hungarian forint", Fiat;
ILS => "ILS", 2, 376, "Israeli new shekel", Fiat;
INR => "INR", 2, 356, "Indian rupee", Fiat;
JPY => "JPY", 0, 392, "Japanese yen", Fiat;
KRW => "KRW", 0, 410, "South Korean won", Fiat;
MXN => "MXN", 2, 484, "Mexican peso", Fiat;
NOK => "NOK", 2, 578, "Norwegian krone", Fiat;
NZD => "NZD", 2, 554, "New Zealand dollar", Fiat;
PLN => "PLN", 2, 985, "Polish złoty", Fiat;
RUB => "RUB", 2, 643, "Russian ruble", Fiat;
SAR => "SAR", 2, 682, "Saudi riyal", Fiat;
SEK => "SEK", 2, 752, "Swedish krona", Fiat;
SGD => "SGD", 2, 702, "Singapore dollar", Fiat;
THB => "THB", 2, 764, "Thai baht", Fiat;
TRY => "TRY", 2, 949, "Turkish lira", Fiat;
TWD => "TWD", 2, 901, "New Taiwan dollar", Fiat;
USD => "USD", 2, 840, "United States dollar", Fiat;
ZAR => "ZAR", 2, 710, "South African rand", Fiat;
XAG => "XAG", 2, 961, "Silver (one troy ounce)", CommodityBacked;
XAU => "XAU", 2, 959, "Gold (one troy ounce)", CommodityBacked;
XPT => "XPT", 2, 962, "Platinum (one troy ounce)", CommodityBacked;
ONEINCH => "1INCH", 8, 0, "1inch Network", Crypto;
AAVE => "AAVE", 8, 0, "Aave", Crypto;
ACA => "ACA", 8, 0, "Acala Token", Crypto;
ADA => "ADA", 6, 0, "Cardano", Crypto;
APT => "APT", 8, 0, "Aptos", Crypto;
ARB => "ARB", 8, 0, "Arbitrum", Crypto;
AVAX => "AVAX", 8, 0, "Avalanche", Crypto;
BCH => "BCH", 8, 0, "Bitcoin Cash", Crypto;
BIO => "BIO", 8, 0, "BioPassport", Crypto;
BTC => "BTC", 8, 0, "Bitcoin", Crypto;
BTTC => "BTTC", 8, 0, "BitTorrent", Crypto;
BNB => "BNB", 8, 0, "Binance Coin", Crypto;
BRZ => "BRZ", 8, 0, "Brazilian Digital Token", Crypto;
BSV => "BSV", 8, 0, "Bitcoin SV", Crypto;
BUSD => "BUSD", 8, 0, "Binance USD", Crypto;
CAKE => "CAKE", 8, 0, "PancakeSwap", Crypto;
CRV => "CRV", 8, 0, "Curve DAO Token", Crypto;
DASH => "DASH", 8, 0, "Dash", Crypto;
DOT => "DOT", 8, 0, "Polkadot", Crypto;
DOGE => "DOGE", 8, 0, "Dogecoin", Crypto;
ENA => "ENA", 8, 0, "Ethena", Crypto;
EOS => "EOS", 8, 0, "EOS", Crypto;
ETH => "ETH", 8, 0, "Ethereum", Crypto;
ETHW => "ETHW", 8, 0, "EthereumPoW", Crypto;
FDUSD => "FDUSD", 8, 0, "First Digital USD", Crypto;
GWEI => "GWEI", 8, 0, "Gwei", Crypto;
HYPE => "HYPE", 8, 0, "Hyperliquid", Crypto;
JOE => "JOE", 8, 0, "JOE", Crypto;
LINK => "LINK", 8, 0, "Chainlink", Crypto;
LTC => "LTC", 8, 0, "Litecoin", Crypto;
LUNA => "LUNA", 8, 0, "Terra", Crypto;
MAMUSD => "MAMUSD", 8, 0, "MAMUSD", Crypto;
NBT => "NBT", 8, 0, "NanoByte Token", Crypto;
POL => "POL", 8, 0, "Polygon", Crypto;
PROVE => "PROVE", 8, 0, "Prove AI", Crypto;
RLUSD => "RLUSD", 8, 0, "Ripple USD", Crypto;
SOL => "SOL", 8, 0, "Solana", Crypto;
SHIB => "SHIB", 8, 0, "Shiba Inu", Crypto;
SUI => "SUI", 8, 0, "Sui", Crypto;
TON => "TON", 8, 0, "Toncoin", Crypto;
TRX => "TRX", 8, 0, "TRON", Crypto;
TRYB => "TRYB", 8, 0, "BiLira", Crypto;
TUSD => "TUSD", 8, 0, "TrueUSD", Crypto;
UNI => "UNI", 8, 0, "Uniswap", Crypto;
VTC => "VTC", 8, 0, "Vertcoin", Crypto;
WBTC => "WBTC", 8, 0, "Wrapped Bitcoin", Crypto;
WSB => "WSB", 8, 0, "WallStreetBets DApp", Crypto;
XBT => "XBT", 8, 0, "Bitcoin", Crypto;
XEC => "XEC", 8, 0, "eCash", Crypto;
XLM => "XLM", 8, 0, "Stellar Lumen", Crypto;
XMR => "XMR", 8, 0, "Monero", Crypto;
USDT => "USDT", 8, 0, "Tether", Crypto;
XRP => "XRP", 6, 0, "XRP", Crypto;
XTZ => "XTZ", 6, 0, "Tezos", Crypto;
USDC => "USDC", 8, 0, "USD Coin", Crypto;
USDC_POS => "USDC.e", 6, 0, "USD Coin (PoS)", Crypto;
USDG => "USDG", 8, 0, "Global Dollar", Crypto;
USDP => "USDP", 8, 0, "Pax Dollar", Crypto;
pUSD => "pUSD", 6, 0, "Polymarket USD", Crypto;
ZEC => "ZEC", 8, 0, "Zcash", Crypto;
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use rstest::rstest;
use super::*;
use crate::types::fixed::FIXED_PRECISION;
#[rstest]
fn test_every_constant_is_registered_and_round_trips_by_code() {
for currency in all_currency_constants() {
let code = currency.code.as_str();
let try_from = Currency::try_from_str(code).expect("constant must be registered");
let from_str = Currency::from(code);
for (label, found) in [("try_from_str", try_from), ("from", from_str)] {
assert_eq!(found.code, currency.code, "{code} via {label}");
assert_eq!(found.precision, currency.precision, "{code} via {label}");
assert_eq!(found.iso4217, currency.iso4217, "{code} via {label}");
assert_eq!(found.name, currency.name, "{code} via {label}");
assert_eq!(
found.currency_type, currency.currency_type,
"{code} via {label}",
);
}
}
}
const EXPECTED_CURRENCIES: &[&str] = &[
"1INCH|8|0|1inch Network|Crypto",
"AAVE|8|0|Aave|Crypto",
"ACA|8|0|Acala Token|Crypto",
"ADA|6|0|Cardano|Crypto",
"APT|8|0|Aptos|Crypto",
"ARB|8|0|Arbitrum|Crypto",
"AUD|2|36|Australian dollar|Fiat",
"AVAX|8|0|Avalanche|Crypto",
"BCH|8|0|Bitcoin Cash|Crypto",
"BIO|8|0|BioPassport|Crypto",
"BNB|8|0|Binance Coin|Crypto",
"BRL|2|986|Brazilian real|Fiat",
"BRZ|8|0|Brazilian Digital Token|Crypto",
"BSV|8|0|Bitcoin SV|Crypto",
"BTC|8|0|Bitcoin|Crypto",
"BTTC|8|0|BitTorrent|Crypto",
"BUSD|8|0|Binance USD|Crypto",
"CAD|2|124|Canadian dollar|Fiat",
"CAKE|8|0|PancakeSwap|Crypto",
"CHF|2|756|Swiss franc|Fiat",
"CNH|2|0|Chinese yuan (offshore)|Fiat",
"CNY|2|156|Chinese yuan|Fiat",
"CRV|8|0|Curve DAO Token|Crypto",
"CZK|2|203|Czech koruna|Fiat",
"DASH|8|0|Dash|Crypto",
"DKK|2|208|Danish krone|Fiat",
"DOGE|8|0|Dogecoin|Crypto",
"DOT|8|0|Polkadot|Crypto",
"ENA|8|0|Ethena|Crypto",
"EOS|8|0|EOS|Crypto",
"ETHW|8|0|EthereumPoW|Crypto",
"ETH|8|0|Ethereum|Crypto",
"EUR|2|978|Euro|Fiat",
"FDUSD|8|0|First Digital USD|Crypto",
"GBP|2|826|British Pound|Fiat",
"GWEI|8|0|Gwei|Crypto",
"HKD|2|344|Hong Kong dollar|Fiat",
"HUF|2|348|Hungarian forint|Fiat",
"HYPE|8|0|Hyperliquid|Crypto",
"ILS|2|376|Israeli new shekel|Fiat",
"INR|2|356|Indian rupee|Fiat",
"JOE|8|0|JOE|Crypto",
"JPY|0|392|Japanese yen|Fiat",
"KRW|0|410|South Korean won|Fiat",
"LINK|8|0|Chainlink|Crypto",
"LTC|8|0|Litecoin|Crypto",
"LUNA|8|0|Terra|Crypto",
"MAMUSD|8|0|MAMUSD|Crypto",
"MXN|2|484|Mexican peso|Fiat",
"NBT|8|0|NanoByte Token|Crypto",
"NOK|2|578|Norwegian krone|Fiat",
"NZD|2|554|New Zealand dollar|Fiat",
"PLN|2|985|Polish złoty|Fiat",
"POL|8|0|Polygon|Crypto",
"PROVE|8|0|Prove AI|Crypto",
"RLUSD|8|0|Ripple USD|Crypto",
"RUB|2|643|Russian ruble|Fiat",
"SAR|2|682|Saudi riyal|Fiat",
"SEK|2|752|Swedish krona|Fiat",
"SGD|2|702|Singapore dollar|Fiat",
"SHIB|8|0|Shiba Inu|Crypto",
"SOL|8|0|Solana|Crypto",
"SUI|8|0|Sui|Crypto",
"THB|2|764|Thai baht|Fiat",
"TON|8|0|Toncoin|Crypto",
"TRX|8|0|TRON|Crypto",
"TRYB|8|0|BiLira|Crypto",
"TRY|2|949|Turkish lira|Fiat",
"TUSD|8|0|TrueUSD|Crypto",
"TWD|2|901|New Taiwan dollar|Fiat",
"UNI|8|0|Uniswap|Crypto",
"USDC.e|6|0|USD Coin (PoS)|Crypto",
"USDC|8|0|USD Coin|Crypto",
"USDG|8|0|Global Dollar|Crypto",
"USDP|8|0|Pax Dollar|Crypto",
"USDT|8|0|Tether|Crypto",
"USD|2|840|United States dollar|Fiat",
"VTC|8|0|Vertcoin|Crypto",
"WBTC|8|0|Wrapped Bitcoin|Crypto",
"WSB|8|0|WallStreetBets DApp|Crypto",
"XAG|2|961|Silver (one troy ounce)|CommodityBacked",
"XAU|2|959|Gold (one troy ounce)|CommodityBacked",
"XBT|8|0|Bitcoin|Crypto",
"XEC|8|0|eCash|Crypto",
"XLM|8|0|Stellar Lumen|Crypto",
"XMR|8|0|Monero|Crypto",
"XPT|2|962|Platinum (one troy ounce)|CommodityBacked",
"XRP|6|0|XRP|Crypto",
"XTZ|6|0|Tezos|Crypto",
"ZAR|2|710|South African rand|Fiat",
"ZEC|8|0|Zcash|Crypto",
"pUSD|6|0|Polymarket USD|Crypto",
];
#[rstest]
fn test_registered_currency_metadata_matches_the_pinned_list() {
let mut actual: Vec<String> = all_currency_constants()
.iter()
.map(|c| {
format!(
"{}|{}|{}|{}|{:?}",
c.code, c.precision, c.iso4217, c.name, c.currency_type
)
})
.collect();
actual.sort();
assert_eq!(
actual.len(),
EXPECTED_CURRENCIES.len(),
"currency count changed; update `EXPECTED_CURRENCIES`",
);
for (got, expected) in actual.iter().zip(EXPECTED_CURRENCIES) {
assert_eq!(got, expected);
}
}
#[rstest]
fn test_currency_codes_are_unique() {
let constants = all_currency_constants();
let codes: HashSet<Ustr> = constants.iter().map(|c| c.code).collect();
assert_eq!(
codes.len(),
constants.len(),
"currency codes must be unique"
);
}
#[rstest]
fn test_registered_currencies_satisfy_value_invariants() {
for currency in all_currency_constants() {
let code = currency.code;
assert!(!currency.name.is_empty(), "{code} must carry a name");
assert!(
currency.precision <= FIXED_PRECISION,
"{code} precision {} exceeds `FIXED_PRECISION` {FIXED_PRECISION}",
currency.precision,
);
if currency.currency_type == CurrencyType::Crypto {
assert_eq!(
currency.iso4217, 0,
"{code} is crypto and must not carry an ISO 4217 code",
);
}
}
}
#[rstest]
fn test_unknown_codes_are_not_registered() {
assert_eq!(Currency::try_from_str("NOT_A_CURRENCY"), None);
assert_eq!(Currency::try_from_str(""), None);
assert_eq!(
Currency::try_from_str("btc"),
None,
"lookup is case sensitive"
);
}
}