Skip to main content

qualia_client_core/wallet/
derivation.rs

1use 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]; // Mainnet pubkey hash version byte
64        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        // We will output a base58 legacy address format with an ecash prefix
76        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        // A known seed for deterministic testing
107        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}