1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Crypto classification heuristics — single source of truth.
//!
//! Used by both `Symbol::is_crypto()` and `Ticker::FromStr` to determine
//! whether a trading pair is a cryptocurrency pair.
/// Check if quote currency is a crypto quote (stablecoins, BTC, ETH, etc.).
pub fn is_crypto_quote(quote: &str) -> bool {
matches!(
quote.to_uppercase().as_str(),
"USDT" | "USDC" | "BTC" | "ETH" | "BNB" | "BUSD" | "DAI" | "TUSD"
)
}
/// Check if base symbol is a known cryptocurrency.
///
/// This is the canonical source of truth for crypto base classification.
/// Downstream consumers (qbot-core adapters, MCP tools) MUST call this
/// function instead of maintaining local lists (#3446 split-brain fix).
pub fn is_crypto_base(base: &str) -> bool {
matches!(
base.to_uppercase().as_str(),
// ── Layer 1: BTC + top-10 by market cap ─────────────────────────
"BTC"
| "ETH"
| "SOL"
| "ADA"
| "DOT"
| "AVAX"
| "MATIC"
| "LINK"
| "UNI"
| "ATOM"
| "XRP"
| "LTC"
| "BCH"
| "DOGE"
| "SHIB"
// ── Layer 2: established alts ────────────────────────────────
| "TRX"
| "ETC"
| "XLM"
| "XMR"
| "ALGO"
| "VET"
| "ICP"
| "FIL"
| "HBAR"
| "NEAR"
| "FTM"
| "APT"
// ── Layer 3: DeFi / governance ───────────────────────────────
| "AAVE"
| "MKR"
| "SUSHI"
| "COMP"
| "YFI"
| "SNX"
| "CRV"
| "BAL"
| "ZRX"
| "1INCH"
| "LDO"
// ── Layer 4: L2 / rollup tokens ──────────────────────────────
| "ARB"
| "OP"
// ── Layer 5: metaverse / gaming ──────────────────────────────
| "SAND"
| "MANA"
| "AXS"
// ── Layer 6: 2023-2024 wave ──────────────────────────────────
| "PEPE"
| "WIF"
| "BONK"
| "JUP"
| "RNDR"
| "INJ"
| "TIA"
| "SEI"
| "SUI"
| "BLUR"
| "PYTH"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn original_bases_still_recognized() {
for sym in [
"BTC", "ETH", "SOL", "ADA", "DOT", "AVAX", "MATIC", "LINK", "UNI", "ATOM", "XRP",
"LTC", "BCH", "DOGE", "SHIB", "TRX", "ETC", "XLM", "XMR", "ALGO", "VET", "ICP", "FIL",
"AAVE", "MKR", "SUSHI", "COMP", "YFI", "SNX", "CRV", "BAL", "ZRX", "1INCH",
] {
assert!(
is_crypto_base(sym),
"{sym} should be recognized as crypto base"
);
}
}
/// Regression #3446: these were in ephemeral KNOWN_CRYPTO but missing from
/// is_crypto_base(), causing classification divergence.
#[test]
fn newly_added_bases_recognized() {
for sym in [
"NEAR", "APT", "ARB", "OP", "FTM", "HBAR", "SAND", "MANA", "AXS", "LDO", "PEPE", "WIF",
"BONK", "JUP", "RNDR", "INJ", "TIA", "SEI", "SUI", "BLUR", "PYTH",
] {
assert!(
is_crypto_base(sym),
"{sym} should be recognized as crypto base after #3446"
);
}
}
#[test]
fn case_insensitive() {
assert!(is_crypto_base("btc"));
assert!(is_crypto_base("near"));
assert!(is_crypto_base("Arb"));
}
#[test]
fn non_crypto_rejected() {
assert!(!is_crypto_base("AAPL"));
assert!(!is_crypto_base("USD"));
assert!(!is_crypto_base("XYZ"));
}
#[test]
fn crypto_quotes_recognized() {
for q in ["USDT", "USDC", "BTC", "ETH", "BNB", "BUSD", "DAI", "TUSD"] {
assert!(
is_crypto_quote(q),
"{q} should be recognized as crypto quote"
);
}
}
#[test]
fn fiat_not_crypto_quote() {
assert!(!is_crypto_quote("USD"));
assert!(!is_crypto_quote("EUR"));
}
}