Skip to main content

scope/chains/
registry.rs

1//! # Chain registry (L2)
2//!
3//! The one table of chains: the canonical id, the aliases users type, the
4//! EVM chain id, the explorer, the native token, the DexScreener id and the
5//! wrapped native token. Chain metadata, the DEX lookups, the contract
6//! source and Sourcify checks, the EVM client and the CLI's lists of
7//! supported chains all read it.
8
9/// How scope talks to a chain.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ChainKind {
12    /// An EVM chain behind the Etherscan v2 API (or a JSON-RPC node).
13    Evm,
14    Solana,
15    Tron,
16    /// Known to DexScreener only: prices, no chain client.
17    DexOnly,
18}
19
20/// One chain.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Chain {
23    /// Canonical id (`ethereum`).
24    pub id: &'static str,
25    /// Other names users type (`eth`), lower case.
26    pub aliases: &'static [&'static str],
27    pub kind: ChainKind,
28    /// The EVM chain id, for Etherscan v2 and Sourcify (`"1"`).
29    pub evm_chain_id: Option<&'static str>,
30    /// The config key of the chain's explorer API key.
31    pub api_key_name: Option<&'static str>,
32    pub native_symbol: &'static str,
33    pub native_decimals: u8,
34    /// Explorer base for token pages.
35    pub explorer_token_base: Option<&'static str>,
36    /// Explorer base for address pages.
37    pub explorer_address_base: Option<&'static str>,
38    /// The chain's id at DexScreener.
39    pub dexscreener_id: Option<&'static str>,
40    /// The wrapped native token, which prices the native token on a DEX.
41    pub wrapped_native: Option<&'static str>,
42}
43
44const fn evm(
45    id: &'static str,
46    aliases: &'static [&'static str],
47    chain_id: &'static str,
48    key: &'static str,
49    symbol: &'static str,
50    explorer: (&'static str, &'static str),
51    wrapped: &'static str,
52) -> Chain {
53    Chain {
54        id,
55        aliases,
56        kind: ChainKind::Evm,
57        evm_chain_id: Some(chain_id),
58        api_key_name: Some(key),
59        native_symbol: symbol,
60        native_decimals: 18,
61        explorer_token_base: Some(explorer.0),
62        explorer_address_base: Some(explorer.1),
63        dexscreener_id: Some(id),
64        wrapped_native: Some(wrapped),
65    }
66}
67
68/// Every chain scope knows, in the order lists show them.
69pub const CHAINS: &[Chain] = &[
70    evm(
71        "ethereum",
72        &["eth"],
73        "1",
74        "etherscan",
75        "ETH",
76        ("https://etherscan.io/token", "https://etherscan.io/address"),
77        "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
78    ),
79    evm(
80        "polygon",
81        &["matic"],
82        "137",
83        "polygonscan",
84        "MATIC",
85        (
86            "https://polygonscan.com/token",
87            "https://polygonscan.com/address",
88        ),
89        "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
90    ),
91    evm(
92        "arbitrum",
93        &["arb"],
94        "42161",
95        "arbiscan",
96        "ETH",
97        ("https://arbiscan.io/token", "https://arbiscan.io/address"),
98        "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
99    ),
100    evm(
101        "optimism",
102        &["op"],
103        "10",
104        "optimism",
105        "ETH",
106        (
107            "https://optimistic.etherscan.io/token",
108            "https://optimistic.etherscan.io/address",
109        ),
110        "0x4200000000000000000000000000000000000006",
111    ),
112    evm(
113        "base",
114        &[],
115        "8453",
116        "basescan",
117        "ETH",
118        ("https://basescan.org/token", "https://basescan.org/address"),
119        "0x4200000000000000000000000000000000000006",
120    ),
121    evm(
122        "bsc",
123        &["bnb"],
124        "56",
125        "bscscan",
126        "BNB",
127        ("https://bscscan.com/token", "https://bscscan.com/address"),
128        "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c",
129    ),
130    Chain {
131        id: "aegis",
132        aliases: &[],
133        kind: ChainKind::Evm,
134        // A JSON-RPC node (config `aegis_rpc`), not Etherscan.
135        evm_chain_id: None,
136        api_key_name: None,
137        native_symbol: "WRAITH",
138        native_decimals: 18,
139        explorer_token_base: None,
140        explorer_address_base: None,
141        dexscreener_id: None,
142        wrapped_native: None,
143    },
144    Chain {
145        id: "solana",
146        aliases: &["sol"],
147        kind: ChainKind::Solana,
148        evm_chain_id: None,
149        api_key_name: None,
150        native_symbol: "SOL",
151        native_decimals: 9,
152        explorer_token_base: Some("https://solscan.io/token"),
153        explorer_address_base: Some("https://solscan.io/account"),
154        dexscreener_id: Some("solana"),
155        wrapped_native: Some("So11111111111111111111111111111111111111112"),
156    },
157    Chain {
158        id: "tron",
159        aliases: &["trx"],
160        kind: ChainKind::Tron,
161        evm_chain_id: None,
162        api_key_name: None,
163        native_symbol: "TRX",
164        native_decimals: 6,
165        explorer_token_base: Some("https://tronscan.org/#/token20"),
166        explorer_address_base: Some("https://tronscan.org/#/address"),
167        dexscreener_id: Some("tron"),
168        // The wrapped TRX token differs by DEX: no one address.
169        wrapped_native: None,
170    },
171    Chain {
172        id: "avalanche",
173        aliases: &["avax"],
174        kind: ChainKind::DexOnly,
175        evm_chain_id: None,
176        api_key_name: None,
177        native_symbol: "AVAX",
178        native_decimals: 18,
179        explorer_token_base: None,
180        explorer_address_base: None,
181        dexscreener_id: Some("avalanche"),
182        wrapped_native: None,
183    },
184];
185
186/// The chain named `name` (its id or an alias, any case).
187pub fn chain(name: &str) -> Option<&'static Chain> {
188    let name = name.trim().to_lowercase();
189    CHAINS
190        .iter()
191        .find(|c| c.id == name || c.aliases.contains(&name.as_str()))
192}
193
194/// The ids of the chains with a chain client (address, tx, contract):
195/// everything but the DEX-only chains.
196pub fn client_chain_ids() -> Vec<&'static str> {
197    CHAINS
198        .iter()
199        .filter(|c| c.kind != ChainKind::DexOnly)
200        .map(|c| c.id)
201        .collect()
202}
203
204/// [`client_chain_ids`] joined for help and error text.
205pub fn client_chain_list() -> String {
206    client_chain_ids().join(", ")
207}
208
209/// The EVM chains with an Etherscan-compatible API, joined for help text.
210pub fn explorer_api_chain_list() -> String {
211    CHAINS
212        .iter()
213        .filter(|c| c.evm_chain_id.is_some())
214        .map(|c| c.id)
215        .collect::<Vec<_>>()
216        .join(", ")
217}
218
219/// The `--chain` help of commands that take any chain with a client.
220pub fn chain_help() -> String {
221    format!("Target blockchain network: {}", client_chain_list())
222}
223
224/// True when `name` has a chain client and a block explorer (token info and
225/// holders): not a JSON-RPC-only chain, not a DEX-only one.
226pub fn has_explorer(name: &str) -> bool {
227    chain(name).is_some_and(|c| c.kind != ChainKind::DexOnly && c.explorer_token_base.is_some())
228}
229
230/// True when `name` is an EVM chain with a client.
231pub fn is_evm(name: &str) -> bool {
232    chain(name).is_some_and(|c| c.kind == ChainKind::Evm)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_ids_and_aliases_resolve_in_any_case() {
241        assert_eq!(chain("ETH").unwrap().id, "ethereum");
242        assert_eq!(chain("matic").unwrap().id, "polygon");
243        assert_eq!(chain(" Sol ").unwrap().id, "solana");
244        assert!(chain("nosuch").is_none());
245    }
246
247    #[test]
248    fn test_every_alias_names_one_chain_only() {
249        let mut names: Vec<&str> = CHAINS
250            .iter()
251            .flat_map(|c| std::iter::once(c.id).chain(c.aliases.iter().copied()))
252            .collect();
253        let n = names.len();
254        names.sort_unstable();
255        names.dedup();
256        assert_eq!(names.len(), n, "a name is listed twice");
257    }
258
259    #[test]
260    fn test_the_client_list_has_aegis_and_not_the_dex_only_chains() {
261        let ids = client_chain_ids();
262        assert!(ids.contains(&"aegis"));
263        assert!(!ids.contains(&"avalanche"));
264        assert!(is_evm("arb") && !is_evm("solana"));
265    }
266}