use crate::governance::bip32::{derive_master_key, ExtendedPrivateKey, ExtendedPublicKey};
use crate::governance::error::{GovernanceError, GovernanceResult};
use std::fmt;
pub const BIP44_PURPOSE: u32 = 44;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoinType {
Bitcoin = 0,
BitcoinTestnet = 1,
Litecoin = 2,
Dogecoin = 3,
Ethereum = 60,
}
impl CoinType {
pub fn value(&self) -> u32 {
*self as u32
}
pub fn from_value(value: u32) -> Result<Self, GovernanceError> {
match value {
0 => Ok(CoinType::Bitcoin),
1 => Ok(CoinType::BitcoinTestnet),
2 => Ok(CoinType::Litecoin),
3 => Ok(CoinType::Dogecoin),
60 => Ok(CoinType::Ethereum),
_ => Err(GovernanceError::InvalidInput(format!(
"Unsupported coin type: {value}"
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeChain {
External = 0,
Internal = 1,
}
impl ChangeChain {
pub fn value(&self) -> u32 {
*self as u32
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bip44Path {
pub purpose: u32,
pub coin_type: CoinType,
pub account: u32,
pub change: ChangeChain,
pub address_index: u32,
}
impl Bip44Path {
pub fn new(coin_type: CoinType, account: u32, change: ChangeChain, address_index: u32) -> Self {
Bip44Path {
purpose: BIP44_PURPOSE,
coin_type,
account,
change,
address_index,
}
}
pub fn bitcoin_mainnet(account: u32, change: ChangeChain, address_index: u32) -> Self {
Self::new(CoinType::Bitcoin, account, change, address_index)
}
pub fn bitcoin_testnet(account: u32, change: ChangeChain, address_index: u32) -> Self {
Self::new(CoinType::BitcoinTestnet, account, change, address_index)
}
pub fn from_string(path_str: &str) -> GovernanceResult<Self> {
let path_str = path_str.strip_prefix("m/").unwrap_or(path_str);
let parts: Vec<&str> = path_str.split('/').collect();
if parts.len() != 5 {
return Err(GovernanceError::InvalidInput(
"BIP44 path must have 5 components: purpose'/coin_type'/account'/change/address_index".to_string()
));
}
let purpose_str = parts[0].trim_end_matches('\'');
let purpose: u32 = purpose_str
.parse()
.map_err(|_| GovernanceError::InvalidInput("Invalid purpose".to_string()))?;
if purpose != BIP44_PURPOSE {
return Err(GovernanceError::InvalidInput(format!(
"Purpose must be {BIP44_PURPOSE} for BIP44"
)));
}
let coin_type_str = parts[1].trim_end_matches('\'');
let coin_type_val: u32 = coin_type_str
.parse()
.map_err(|_| GovernanceError::InvalidInput("Invalid coin type".to_string()))?;
let coin_type = CoinType::from_value(coin_type_val)?;
let account_str = parts[2].trim_end_matches('\'');
let account: u32 = account_str
.parse()
.map_err(|_| GovernanceError::InvalidInput("Invalid account".to_string()))?;
let change_val: u32 = parts[3]
.parse()
.map_err(|_| GovernanceError::InvalidInput("Invalid change".to_string()))?;
let change = match change_val {
0 => ChangeChain::External,
1 => ChangeChain::Internal,
_ => {
return Err(GovernanceError::InvalidInput(
"Change must be 0 (external) or 1 (internal)".to_string(),
))
}
};
let address_index: u32 = parts[4]
.parse()
.map_err(|_| GovernanceError::InvalidInput("Invalid address index".to_string()))?;
Ok(Bip44Path {
purpose,
coin_type,
account,
change,
address_index,
})
}
pub fn derive(
&self,
master_private: &ExtendedPrivateKey,
) -> GovernanceResult<(ExtendedPrivateKey, ExtendedPublicKey)> {
let indices = vec![
0x80000000 | self.purpose, 0x80000000 | self.coin_type.value(), 0x80000000 | self.account, self.change.value(), self.address_index, ];
let mut current = master_private.clone();
let mut current_pub = master_private.to_extended_public();
for &index in &indices {
let (new_priv, new_pub) = current.derive_child(index)?;
current = new_priv;
current_pub = new_pub;
}
Ok((current, current_pub))
}
pub fn to_indices(&self) -> Vec<u32> {
vec![
0x80000000 | self.purpose, 0x80000000 | self.coin_type.value(), 0x80000000 | self.account, self.change.value(), self.address_index, ]
}
}
impl fmt::Display for Bip44Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"m/{}/{}'/{}'/{}/{}",
self.purpose,
self.coin_type.value(),
self.account,
self.change.value(),
self.address_index
)
}
}
pub struct Bip44Wallet {
master_private: ExtendedPrivateKey,
coin_type: CoinType,
}
impl Bip44Wallet {
pub fn from_seed(seed: &[u8], coin_type: CoinType) -> GovernanceResult<Self> {
let (master_private, _) = derive_master_key(seed)?;
Ok(Bip44Wallet {
master_private,
coin_type,
})
}
pub fn from_master_key(master_private: ExtendedPrivateKey, coin_type: CoinType) -> Self {
Bip44Wallet {
master_private,
coin_type,
}
}
pub fn derive_address(
&self,
account: u32,
change: ChangeChain,
address_index: u32,
) -> GovernanceResult<(ExtendedPrivateKey, ExtendedPublicKey)> {
let path = Bip44Path::new(self.coin_type, account, change, address_index);
path.derive(&self.master_private)
}
pub fn receiving_address(
&self,
account: u32,
address_index: u32,
) -> GovernanceResult<(ExtendedPrivateKey, ExtendedPublicKey)> {
self.derive_address(account, ChangeChain::External, address_index)
}
pub fn change_address(
&self,
account: u32,
address_index: u32,
) -> GovernanceResult<(ExtendedPrivateKey, ExtendedPublicKey)> {
self.derive_address(account, ChangeChain::Internal, address_index)
}
pub fn account_xpub(&self, account: u32) -> GovernanceResult<ExtendedPublicKey> {
let path_indices = vec![
0x80000000 | BIP44_PURPOSE,
0x80000000 | self.coin_type.value(),
0x80000000 | account,
];
let mut current = self.master_private.clone();
for &index in &path_indices {
let (new_priv, _) = current.derive_child(index)?;
current = new_priv;
}
Ok(current.to_extended_public())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bip44_path_string() {
let path = Bip44Path::bitcoin_mainnet(0, ChangeChain::External, 0);
assert_eq!(path.to_string(), "m/44/0'/0'/0/0");
let parsed = Bip44Path::from_string("m/44'/0'/0'/0/0").unwrap();
assert_eq!(parsed.purpose, 44);
assert_eq!(parsed.coin_type, CoinType::Bitcoin);
assert_eq!(parsed.account, 0);
assert_eq!(parsed.change, ChangeChain::External);
assert_eq!(parsed.address_index, 0);
}
#[test]
fn test_bip44_path_derivation() {
let seed = b"test seed for BIP44 derivation";
let (master_priv, _) = derive_master_key(seed).unwrap();
let path = Bip44Path::bitcoin_mainnet(0, ChangeChain::External, 0);
let (derived_priv, derived_pub) = path.derive(&master_priv).unwrap();
assert_eq!(derived_priv.depth, 5); assert_eq!(derived_pub.depth, 5);
}
#[test]
fn test_bip44_wallet() {
let seed = b"test seed for BIP44 wallet";
let wallet = Bip44Wallet::from_seed(seed, CoinType::Bitcoin).unwrap();
let (receiving_priv, receiving_pub) = wallet.receiving_address(0, 0).unwrap();
let (change_priv, change_pub) = wallet.change_address(0, 0).unwrap();
assert_ne!(
receiving_priv.private_key_bytes(),
change_priv.private_key_bytes()
);
assert_ne!(
receiving_pub.public_key_bytes(),
change_pub.public_key_bytes()
);
}
#[test]
fn test_coin_types() {
assert_eq!(CoinType::Bitcoin.value(), 0);
assert_eq!(CoinType::BitcoinTestnet.value(), 1);
let coin = CoinType::from_value(0).unwrap();
assert_eq!(coin, CoinType::Bitcoin);
}
}