Skip to main content

hw_chain/
chain.rs

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}