hw-chain 0.1.2

Shared chain models/configuration for wallet and transport layers
Documentation
use std::str::FromStr;

pub const DEFAULT_ETHEREUM_BIP32_PATH: &str = "m/44'/60'/0'/0/0";

pub const DEFAULT_BITCOIN_BIP32_PATH: &str = "m/84'/0'/0'/0/0";

pub const DEFAULT_SOLANA_BIP32_PATH: &str = "m/44'/501'/0'/0'";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ChainConfig {
    pub code: &'static str,
    pub slip44: u32,
    pub default_path: &'static str,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Chain {
    Ethereum,
    Bitcoin,
    Solana,
}

impl Chain {
    pub const ALL: [Self; 3] = [Self::Ethereum, Self::Bitcoin, Self::Solana];

    pub const fn config(self) -> ChainConfig {
        match self {
            Self::Ethereum => ChainConfig {
                code: "eth",
                slip44: 60,
                default_path: DEFAULT_ETHEREUM_BIP32_PATH,
            },
            Self::Bitcoin => ChainConfig {
                code: "btc",
                slip44: 0,
                default_path: DEFAULT_BITCOIN_BIP32_PATH,
            },
            Self::Solana => ChainConfig {
                code: "sol",
                slip44: 501,
                default_path: DEFAULT_SOLANA_BIP32_PATH,
            },
        }
    }

    pub fn default_path(self) -> &'static str {
        self.config().default_path
    }

    pub fn as_str(self) -> &'static str {
        self.config().code
    }

    pub fn from_slip44(slip44: u32) -> Option<Self> {
        Self::ALL
            .into_iter()
            .find(|chain| chain.config().slip44 == slip44)
    }
}

impl FromStr for Chain {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.to_ascii_lowercase().as_str() {
            "eth" | "ethereum" => Ok(Self::Ethereum),
            "btc" | "bitcoin" => Ok(Self::Bitcoin),
            "sol" | "solana" => Ok(Self::Solana),
            _ => Err(format!(
                "unsupported chain '{value}'; expected eth, btc, or sol"
            )),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_accepts_codes_and_names_case_insensitively() {
        let cases = [
            ("eth", Chain::Ethereum),
            ("Ethereum", Chain::Ethereum),
            ("BTC", Chain::Bitcoin),
            ("bitcoin", Chain::Bitcoin),
            ("SOL", Chain::Solana),
            ("solana", Chain::Solana),
        ];
        for (input, expected) in cases {
            assert_eq!(input.parse::<Chain>().unwrap(), expected, "{input}");
        }
        assert!("UNKNOWN".parse::<Chain>().is_err());
    }

    #[test]
    fn code_and_slip44_round_trip() {
        for chain in Chain::ALL {
            assert_eq!(chain.as_str().parse::<Chain>().unwrap(), chain);
            assert_eq!(Chain::from_slip44(chain.config().slip44), Some(chain));
        }
        assert_eq!(Chain::from_slip44(999), None);
    }
}