1use std::str::FromStr;
2
3pub const DEFAULT_ETHEREUM_BIP32_PATH: &str = "m/44'/60'/0'/0/0";
4
5pub const DEFAULT_BITCOIN_BIP32_PATH: &str = "m/84'/0'/0'/0/0";
6
7pub const DEFAULT_SOLANA_BIP32_PATH: &str = "m/44'/501'/0'/0'";
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub struct ChainConfig {
11 pub code: &'static str,
12 pub slip44: u32,
13 pub default_path: &'static str,
14}
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum Chain {
18 Ethereum,
19 Bitcoin,
20 Solana,
21}
22
23impl Chain {
24 pub const ALL: [Self; 3] = [Self::Ethereum, Self::Bitcoin, Self::Solana];
25
26 pub const fn config(self) -> ChainConfig {
27 match self {
28 Self::Ethereum => ChainConfig {
29 code: "eth",
30 slip44: 60,
31 default_path: DEFAULT_ETHEREUM_BIP32_PATH,
32 },
33 Self::Bitcoin => ChainConfig {
34 code: "btc",
35 slip44: 0,
36 default_path: DEFAULT_BITCOIN_BIP32_PATH,
37 },
38 Self::Solana => ChainConfig {
39 code: "sol",
40 slip44: 501,
41 default_path: DEFAULT_SOLANA_BIP32_PATH,
42 },
43 }
44 }
45
46 pub fn default_path(self) -> &'static str {
47 self.config().default_path
48 }
49
50 pub fn as_str(self) -> &'static str {
51 self.config().code
52 }
53
54 pub fn from_slip44(slip44: u32) -> Option<Self> {
55 Self::ALL
56 .into_iter()
57 .find(|chain| chain.config().slip44 == slip44)
58 }
59}
60
61impl FromStr for Chain {
62 type Err = String;
63
64 fn from_str(value: &str) -> Result<Self, Self::Err> {
65 match value.to_ascii_lowercase().as_str() {
66 "eth" | "ethereum" => Ok(Self::Ethereum),
67 "btc" | "bitcoin" => Ok(Self::Bitcoin),
68 "sol" | "solana" => Ok(Self::Solana),
69 _ => Err(format!(
70 "unsupported chain '{value}'; expected eth, btc, or sol"
71 )),
72 }
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn parse_accepts_codes_and_names_case_insensitively() {
82 let cases = [
83 ("eth", Chain::Ethereum),
84 ("Ethereum", Chain::Ethereum),
85 ("BTC", Chain::Bitcoin),
86 ("bitcoin", Chain::Bitcoin),
87 ("SOL", Chain::Solana),
88 ("solana", Chain::Solana),
89 ];
90 for (input, expected) in cases {
91 assert_eq!(input.parse::<Chain>().unwrap(), expected, "{input}");
92 }
93 assert!("UNKNOWN".parse::<Chain>().is_err());
94 }
95
96 #[test]
97 fn code_and_slip44_round_trip() {
98 for chain in Chain::ALL {
99 assert_eq!(chain.as_str().parse::<Chain>().unwrap(), chain);
100 assert_eq!(Chain::from_slip44(chain.config().slip44), Some(chain));
101 }
102 assert_eq!(Chain::from_slip44(999), None);
103 }
104}