1use std::{
36 collections::HashMap,
37 sync::{LazyLock, OnceLock},
38};
39
40use parking_lot::Mutex;
41use ustr::Ustr;
42
43use crate::{enums::CurrencyType, types::Currency};
44
45macro_rules! currency_constants {
50 ($(
51 $accessor:ident => $code:literal, $precision:literal, $iso4217:literal, $name:literal,
52 $currency_type:ident;
53 )+) => {
54 impl Currency {
55 $(
56 #[doc = concat!("Returns the ", $name, " (`", $code, "`) currency.")]
57 #[allow(non_snake_case)]
58 #[must_use]
59 pub fn $accessor() -> Self {
60 static LOCK: OnceLock<Currency> = OnceLock::new();
61 *LOCK.get_or_init(|| Self {
62 code: Ustr::from($code),
63 precision: $precision,
64 iso4217: $iso4217,
65 name: Ustr::from($name),
66 currency_type: CurrencyType::$currency_type,
67 })
68 }
69 )+
70 }
71
72 pub static CURRENCY_MAP: LazyLock<Mutex<HashMap<String, Currency>>> = LazyLock::new(|| {
74 let mut map = HashMap::new();
75 $(map.insert(Currency::$accessor().code.to_string(), Currency::$accessor());)+
76 Mutex::new(map)
77 });
78
79 #[cfg(test)]
80 fn all_currency_constants() -> Vec<Currency> {
81 vec![$(Currency::$accessor()),+]
82 }
83 };
84}
85
86currency_constants! {
87 AUD => "AUD", 2, 36, "Australian dollar", Fiat;
89 BRL => "BRL", 2, 986, "Brazilian real", Fiat;
90 CAD => "CAD", 2, 124, "Canadian dollar", Fiat;
91 CHF => "CHF", 2, 756, "Swiss franc", Fiat;
92 CNY => "CNY", 2, 156, "Chinese yuan", Fiat;
93 CNH => "CNH", 2, 0, "Chinese yuan (offshore)", Fiat;
94 CZK => "CZK", 2, 203, "Czech koruna", Fiat;
95 DKK => "DKK", 2, 208, "Danish krone", Fiat;
96 EUR => "EUR", 2, 978, "Euro", Fiat;
97 GBP => "GBP", 2, 826, "British Pound", Fiat;
98 HKD => "HKD", 2, 344, "Hong Kong dollar", Fiat;
99 HUF => "HUF", 2, 348, "Hungarian forint", Fiat;
100 ILS => "ILS", 2, 376, "Israeli new shekel", Fiat;
101 INR => "INR", 2, 356, "Indian rupee", Fiat;
102 JPY => "JPY", 0, 392, "Japanese yen", Fiat;
103 KRW => "KRW", 0, 410, "South Korean won", Fiat;
104 MXN => "MXN", 2, 484, "Mexican peso", Fiat;
105 NOK => "NOK", 2, 578, "Norwegian krone", Fiat;
106 NZD => "NZD", 2, 554, "New Zealand dollar", Fiat;
107 PLN => "PLN", 2, 985, "Polish złoty", Fiat;
108 RUB => "RUB", 2, 643, "Russian ruble", Fiat;
109 SAR => "SAR", 2, 682, "Saudi riyal", Fiat;
110 SEK => "SEK", 2, 752, "Swedish krona", Fiat;
111 SGD => "SGD", 2, 702, "Singapore dollar", Fiat;
112 THB => "THB", 2, 764, "Thai baht", Fiat;
113 TRY => "TRY", 2, 949, "Turkish lira", Fiat;
114 TWD => "TWD", 2, 901, "New Taiwan dollar", Fiat;
115 USD => "USD", 2, 840, "United States dollar", Fiat;
116 ZAR => "ZAR", 2, 710, "South African rand", Fiat;
117
118 XAG => "XAG", 2, 961, "Silver (one troy ounce)", CommodityBacked;
120 XAU => "XAU", 2, 959, "Gold (one troy ounce)", CommodityBacked;
121 XPT => "XPT", 2, 962, "Platinum (one troy ounce)", CommodityBacked;
122
123 ONEINCH => "1INCH", 8, 0, "1inch Network", Crypto;
125 AAVE => "AAVE", 8, 0, "Aave", Crypto;
126 ACA => "ACA", 8, 0, "Acala Token", Crypto;
127 ADA => "ADA", 6, 0, "Cardano", Crypto;
128 APT => "APT", 8, 0, "Aptos", Crypto;
129 ARB => "ARB", 8, 0, "Arbitrum", Crypto;
130 AVAX => "AVAX", 8, 0, "Avalanche", Crypto;
131 BCH => "BCH", 8, 0, "Bitcoin Cash", Crypto;
132 BIO => "BIO", 8, 0, "BioPassport", Crypto;
133 BTC => "BTC", 8, 0, "Bitcoin", Crypto;
134 BTTC => "BTTC", 8, 0, "BitTorrent", Crypto;
135 BNB => "BNB", 8, 0, "Binance Coin", Crypto;
136 BRZ => "BRZ", 8, 0, "Brazilian Digital Token", Crypto;
137 BSV => "BSV", 8, 0, "Bitcoin SV", Crypto;
138 BUSD => "BUSD", 8, 0, "Binance USD", Crypto;
139 CAKE => "CAKE", 8, 0, "PancakeSwap", Crypto;
140 CRV => "CRV", 8, 0, "Curve DAO Token", Crypto;
141 DASH => "DASH", 8, 0, "Dash", Crypto;
142 DOT => "DOT", 8, 0, "Polkadot", Crypto;
143 DOGE => "DOGE", 8, 0, "Dogecoin", Crypto;
144 ENA => "ENA", 8, 0, "Ethena", Crypto;
145 EOS => "EOS", 8, 0, "EOS", Crypto;
146 ETH => "ETH", 8, 0, "Ethereum", Crypto;
147 ETHW => "ETHW", 8, 0, "EthereumPoW", Crypto;
148 FDUSD => "FDUSD", 8, 0, "First Digital USD", Crypto;
149 GWEI => "GWEI", 8, 0, "Gwei", Crypto;
150 HYPE => "HYPE", 8, 0, "Hyperliquid", Crypto;
151 JOE => "JOE", 8, 0, "JOE", Crypto;
152 LINK => "LINK", 8, 0, "Chainlink", Crypto;
153 LTC => "LTC", 8, 0, "Litecoin", Crypto;
154 LUNA => "LUNA", 8, 0, "Terra", Crypto;
155 MAMUSD => "MAMUSD", 8, 0, "MAMUSD", Crypto;
156 NBT => "NBT", 8, 0, "NanoByte Token", Crypto;
157 POL => "POL", 8, 0, "Polygon", Crypto;
158 PROVE => "PROVE", 8, 0, "Prove AI", Crypto;
159 RLUSD => "RLUSD", 8, 0, "Ripple USD", Crypto;
160 SOL => "SOL", 8, 0, "Solana", Crypto;
161 SHIB => "SHIB", 8, 0, "Shiba Inu", Crypto;
162 SUI => "SUI", 8, 0, "Sui", Crypto;
163 TON => "TON", 8, 0, "Toncoin", Crypto;
164 TRX => "TRX", 8, 0, "TRON", Crypto;
165 TRYB => "TRYB", 8, 0, "BiLira", Crypto;
166 TUSD => "TUSD", 8, 0, "TrueUSD", Crypto;
167 UNI => "UNI", 8, 0, "Uniswap", Crypto;
168 VTC => "VTC", 8, 0, "Vertcoin", Crypto;
169 WBTC => "WBTC", 8, 0, "Wrapped Bitcoin", Crypto;
170 WSB => "WSB", 8, 0, "WallStreetBets DApp", Crypto;
171 XBT => "XBT", 8, 0, "Bitcoin", Crypto;
172 XEC => "XEC", 8, 0, "eCash", Crypto;
173 XLM => "XLM", 8, 0, "Stellar Lumen", Crypto;
174 XMR => "XMR", 8, 0, "Monero", Crypto;
175 USDT => "USDT", 8, 0, "Tether", Crypto;
176 XRP => "XRP", 6, 0, "XRP", Crypto;
177 XTZ => "XTZ", 6, 0, "Tezos", Crypto;
178 USDC => "USDC", 8, 0, "USD Coin", Crypto;
179 USDC_POS => "USDC.e", 6, 0, "USD Coin (PoS)", Crypto;
180 USDG => "USDG", 8, 0, "Global Dollar", Crypto;
181 USDP => "USDP", 8, 0, "Pax Dollar", Crypto;
182 pUSD => "pUSD", 6, 0, "Polymarket USD", Crypto;
183 ZEC => "ZEC", 8, 0, "Zcash", Crypto;
184}
185
186#[cfg(test)]
187mod tests {
188 use std::collections::HashSet;
189
190 use rstest::rstest;
191
192 use super::*;
193 use crate::types::fixed::FIXED_PRECISION;
194
195 #[rstest]
196 fn test_every_constant_is_registered_and_round_trips_by_code() {
197 for currency in all_currency_constants() {
198 let code = currency.code.as_str();
199
200 let try_from = Currency::try_from_str(code).expect("constant must be registered");
203 let from_str = Currency::from(code);
204
205 for (label, found) in [("try_from_str", try_from), ("from", from_str)] {
206 assert_eq!(found.code, currency.code, "{code} via {label}");
207 assert_eq!(found.precision, currency.precision, "{code} via {label}");
208 assert_eq!(found.iso4217, currency.iso4217, "{code} via {label}");
209 assert_eq!(found.name, currency.name, "{code} via {label}");
210 assert_eq!(
211 found.currency_type, currency.currency_type,
212 "{code} via {label}",
213 );
214 }
215 }
216 }
217
218 const EXPECTED_CURRENCIES: &[&str] = &[
225 "1INCH|8|0|1inch Network|Crypto",
226 "AAVE|8|0|Aave|Crypto",
227 "ACA|8|0|Acala Token|Crypto",
228 "ADA|6|0|Cardano|Crypto",
229 "APT|8|0|Aptos|Crypto",
230 "ARB|8|0|Arbitrum|Crypto",
231 "AUD|2|36|Australian dollar|Fiat",
232 "AVAX|8|0|Avalanche|Crypto",
233 "BCH|8|0|Bitcoin Cash|Crypto",
234 "BIO|8|0|BioPassport|Crypto",
235 "BNB|8|0|Binance Coin|Crypto",
236 "BRL|2|986|Brazilian real|Fiat",
237 "BRZ|8|0|Brazilian Digital Token|Crypto",
238 "BSV|8|0|Bitcoin SV|Crypto",
239 "BTC|8|0|Bitcoin|Crypto",
240 "BTTC|8|0|BitTorrent|Crypto",
241 "BUSD|8|0|Binance USD|Crypto",
242 "CAD|2|124|Canadian dollar|Fiat",
243 "CAKE|8|0|PancakeSwap|Crypto",
244 "CHF|2|756|Swiss franc|Fiat",
245 "CNH|2|0|Chinese yuan (offshore)|Fiat",
246 "CNY|2|156|Chinese yuan|Fiat",
247 "CRV|8|0|Curve DAO Token|Crypto",
248 "CZK|2|203|Czech koruna|Fiat",
249 "DASH|8|0|Dash|Crypto",
250 "DKK|2|208|Danish krone|Fiat",
251 "DOGE|8|0|Dogecoin|Crypto",
252 "DOT|8|0|Polkadot|Crypto",
253 "ENA|8|0|Ethena|Crypto",
254 "EOS|8|0|EOS|Crypto",
255 "ETHW|8|0|EthereumPoW|Crypto",
256 "ETH|8|0|Ethereum|Crypto",
257 "EUR|2|978|Euro|Fiat",
258 "FDUSD|8|0|First Digital USD|Crypto",
259 "GBP|2|826|British Pound|Fiat",
260 "GWEI|8|0|Gwei|Crypto",
261 "HKD|2|344|Hong Kong dollar|Fiat",
262 "HUF|2|348|Hungarian forint|Fiat",
263 "HYPE|8|0|Hyperliquid|Crypto",
264 "ILS|2|376|Israeli new shekel|Fiat",
265 "INR|2|356|Indian rupee|Fiat",
266 "JOE|8|0|JOE|Crypto",
267 "JPY|0|392|Japanese yen|Fiat",
268 "KRW|0|410|South Korean won|Fiat",
269 "LINK|8|0|Chainlink|Crypto",
270 "LTC|8|0|Litecoin|Crypto",
271 "LUNA|8|0|Terra|Crypto",
272 "MAMUSD|8|0|MAMUSD|Crypto",
273 "MXN|2|484|Mexican peso|Fiat",
274 "NBT|8|0|NanoByte Token|Crypto",
275 "NOK|2|578|Norwegian krone|Fiat",
276 "NZD|2|554|New Zealand dollar|Fiat",
277 "PLN|2|985|Polish złoty|Fiat",
278 "POL|8|0|Polygon|Crypto",
279 "PROVE|8|0|Prove AI|Crypto",
280 "RLUSD|8|0|Ripple USD|Crypto",
281 "RUB|2|643|Russian ruble|Fiat",
282 "SAR|2|682|Saudi riyal|Fiat",
283 "SEK|2|752|Swedish krona|Fiat",
284 "SGD|2|702|Singapore dollar|Fiat",
285 "SHIB|8|0|Shiba Inu|Crypto",
286 "SOL|8|0|Solana|Crypto",
287 "SUI|8|0|Sui|Crypto",
288 "THB|2|764|Thai baht|Fiat",
289 "TON|8|0|Toncoin|Crypto",
290 "TRX|8|0|TRON|Crypto",
291 "TRYB|8|0|BiLira|Crypto",
292 "TRY|2|949|Turkish lira|Fiat",
293 "TUSD|8|0|TrueUSD|Crypto",
294 "TWD|2|901|New Taiwan dollar|Fiat",
295 "UNI|8|0|Uniswap|Crypto",
296 "USDC.e|6|0|USD Coin (PoS)|Crypto",
297 "USDC|8|0|USD Coin|Crypto",
298 "USDG|8|0|Global Dollar|Crypto",
299 "USDP|8|0|Pax Dollar|Crypto",
300 "USDT|8|0|Tether|Crypto",
301 "USD|2|840|United States dollar|Fiat",
302 "VTC|8|0|Vertcoin|Crypto",
303 "WBTC|8|0|Wrapped Bitcoin|Crypto",
304 "WSB|8|0|WallStreetBets DApp|Crypto",
305 "XAG|2|961|Silver (one troy ounce)|CommodityBacked",
306 "XAU|2|959|Gold (one troy ounce)|CommodityBacked",
307 "XBT|8|0|Bitcoin|Crypto",
308 "XEC|8|0|eCash|Crypto",
309 "XLM|8|0|Stellar Lumen|Crypto",
310 "XMR|8|0|Monero|Crypto",
311 "XPT|2|962|Platinum (one troy ounce)|CommodityBacked",
312 "XRP|6|0|XRP|Crypto",
313 "XTZ|6|0|Tezos|Crypto",
314 "ZAR|2|710|South African rand|Fiat",
315 "ZEC|8|0|Zcash|Crypto",
316 "pUSD|6|0|Polymarket USD|Crypto",
317 ];
318
319 #[rstest]
320 fn test_registered_currency_metadata_matches_the_pinned_list() {
321 let mut actual: Vec<String> = all_currency_constants()
322 .iter()
323 .map(|c| {
324 format!(
325 "{}|{}|{}|{}|{:?}",
326 c.code, c.precision, c.iso4217, c.name, c.currency_type
327 )
328 })
329 .collect();
330 actual.sort();
331
332 assert_eq!(
333 actual.len(),
334 EXPECTED_CURRENCIES.len(),
335 "currency count changed; update `EXPECTED_CURRENCIES`",
336 );
337
338 for (got, expected) in actual.iter().zip(EXPECTED_CURRENCIES) {
339 assert_eq!(got, expected);
340 }
341 }
342
343 #[rstest]
344 fn test_currency_codes_are_unique() {
345 let constants = all_currency_constants();
347 let codes: HashSet<Ustr> = constants.iter().map(|c| c.code).collect();
348
349 assert_eq!(
350 codes.len(),
351 constants.len(),
352 "currency codes must be unique"
353 );
354 }
355
356 #[rstest]
357 fn test_registered_currencies_satisfy_value_invariants() {
358 for currency in all_currency_constants() {
359 let code = currency.code;
360
361 assert!(!currency.name.is_empty(), "{code} must carry a name");
362 assert!(
363 currency.precision <= FIXED_PRECISION,
364 "{code} precision {} exceeds `FIXED_PRECISION` {FIXED_PRECISION}",
365 currency.precision,
366 );
367
368 if currency.currency_type == CurrencyType::Crypto {
369 assert_eq!(
370 currency.iso4217, 0,
371 "{code} is crypto and must not carry an ISO 4217 code",
372 );
373 }
374 }
375 }
376
377 #[rstest]
378 fn test_unknown_codes_are_not_registered() {
379 assert_eq!(Currency::try_from_str("NOT_A_CURRENCY"), None);
380 assert_eq!(Currency::try_from_str(""), None);
381 assert_eq!(
382 Currency::try_from_str("btc"),
383 None,
384 "lookup is case sensitive"
385 );
386 }
387}