qualia_client_core/wallet/
derivation.rs1use bip32::{DerivationPath as Bip32Path, XPrv, XPub};
2use bs58;
3use ripemd::Ripemd160;
4use sha2::Sha256;
5use sha3::Keccak256;
6use std::str::FromStr;
7
8#[derive(Debug, Clone)]
9pub struct DerivationPath(pub String);
10
11#[derive(Debug, Clone)]
12pub struct AddressPayload {
13 pub network: String,
14 pub address: String,
15 pub path: String,
16 pub pubkey_hash: String,
17}
18
19pub struct HdWallet {
20 master_key: XPrv,
21}
22
23impl HdWallet {
24 pub fn from_seed(seed_bytes: &[u8]) -> Result<Self, String> {
25 let master_key = XPrv::new(seed_bytes).map_err(|e| e.to_string())?;
26 Ok(Self { master_key })
27 }
28
29 pub fn derive_address(&self, network: &str, path: &str) -> Result<AddressPayload, String> {
30 let derivation_path = Bip32Path::from_str(path).map_err(|e| e.to_string())?;
31 let mut child_xprv = self.master_key.clone();
32 for child in derivation_path.iter() {
33 child_xprv = child_xprv.derive_child(child).map_err(|e| e.to_string())?;
34 }
35 let public_key = child_xprv.public_key();
36
37 let address = match network {
38 "BTC" => Self::derive_btc_address(&public_key),
39 "XEC" => Self::derive_xec_address(&public_key),
40 "ETH" => Self::derive_eth_address(&public_key),
41 "NYM" => Self::derive_nym_address(&public_key),
42 _ => return Err(format!("Unsupported network: {}", network)),
43 };
44
45 let pubkey_bytes = public_key.to_bytes();
46 let sha256_hash = <Sha256 as sha2::Digest>::digest(&pubkey_bytes);
47 let ripemd160_hash = <Ripemd160 as ripemd::Digest>::digest(&sha256_hash);
48 let pubkey_hash = hex::encode(&ripemd160_hash);
49
50 Ok(AddressPayload {
51 network: network.to_string(),
52 address,
53 path: path.to_string(),
54 pubkey_hash,
55 })
56 }
57
58 fn derive_btc_address(pubkey: &XPub) -> String {
59 let pubkey_bytes = pubkey.to_bytes();
60 let sha256_hash = <Sha256 as sha2::Digest>::digest(&pubkey_bytes);
61 let ripemd160_hash = <Ripemd160 as ripemd::Digest>::digest(&sha256_hash);
62
63 let mut payload = vec![0x00]; payload.extend_from_slice(&ripemd160_hash);
65
66 let checksum_hash1 = <Sha256 as sha2::Digest>::digest(&payload);
67 let checksum_hash2 = <Sha256 as sha2::Digest>::digest(&checksum_hash1);
68
69 payload.extend_from_slice(&checksum_hash2[0..4]);
70
71 bs58::encode(payload).into_string()
72 }
73
74 fn derive_xec_address(pubkey: &XPub) -> String {
75 let btc_addr = Self::derive_btc_address(pubkey);
77 format!("ecash:{}", btc_addr)
78 }
79
80 fn derive_eth_address(pubkey: &XPub) -> String {
81 let pk = pubkey.public_key();
82 let uncompressed = pk.to_encoded_point(false);
83 let pub_bytes = &uncompressed.as_bytes()[1..];
84
85 let keccak_hash = <Keccak256 as sha3::Digest>::digest(pub_bytes);
86 let addr_bytes = &keccak_hash[12..];
87
88 format!("0x{}", hex::encode(addr_bytes))
89 }
90
91 fn derive_nym_address(pubkey: &XPub) -> String {
92 let pubkey_bytes = pubkey.to_bytes();
93 let sha256_hash = <Sha256 as sha2::Digest>::digest(&pubkey_bytes);
94 let ripemd160_hash = <Ripemd160 as ripemd::Digest>::digest(&sha256_hash);
95
96 format!("n1{}", hex::encode(&ripemd160_hash[0..16]))
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_address_derivation() {
106 let seed = [0u8; 64];
108 let wallet = HdWallet::from_seed(&seed).unwrap();
109
110 let btc = wallet.derive_address("BTC", "m/44'/0'/0'/0/0").unwrap();
111 assert_eq!(btc.network, "BTC");
112 assert!(!btc.address.is_empty());
113
114 let eth = wallet.derive_address("ETH", "m/44'/60'/0'/0/0").unwrap();
115 assert_eq!(eth.network, "ETH");
116 assert!(eth.address.starts_with("0x"));
117
118 let xec = wallet.derive_address("XEC", "m/44'/899'/0'/0/0").unwrap();
119 assert_eq!(xec.network, "XEC");
120 assert!(xec.address.starts_with("ecash:"));
121
122 let nym = wallet.derive_address("NYM", "m/44'/118'/0'/0/0").unwrap();
123 assert_eq!(nym.network, "NYM");
124 assert!(nym.address.starts_with("n1"));
125 }
126}