1use alloy_primitives::U256;
2use alloy_signer_local::{
3 MnemonicBuilder, PrivateKeySigner,
4 coins_bip39::{
5 ChineseSimplified, ChineseTraditional, Czech, English, French, Italian, Japanese, Korean,
6 Portuguese, Spanish, Wordlist,
7 },
8};
9
10pub const BIP32_HARDEN: u32 = 0x8000_0000;
12
13pub fn validate_bip32_path(path: &str) -> Result<(), String> {
22 for c in path.split('/') {
23 let num = c.strip_suffix('\'').or_else(|| c.strip_suffix('h')).unwrap_or(c);
24 if let Ok(v) = num.parse::<u32>()
25 && v >= BIP32_HARDEN
26 {
27 return Err(format!(
28 "BIP32 component {c} overflows harden bit (index must be < {BIP32_HARDEN})"
29 ));
30 }
31 }
32 Ok(())
33}
34
35pub fn derive_key_path(path: &str, index: u32) -> String {
37 let mut out = path.to_string();
38 if !out.ends_with('/') {
39 out.push('/');
40 }
41 out.push_str(&index.to_string());
42 out
43}
44
45pub fn derive_key_path_checked(path: &str, index: u32) -> Result<String, String> {
47 if index >= BIP32_HARDEN {
48 return Err(format!(
49 "BIP32 index {index} already sets harden bit (must be < {BIP32_HARDEN})"
50 ));
51 }
52 validate_bip32_path(path)?;
53 Ok(derive_key_path(path, index))
54}
55
56pub fn derive_private_key<W: Wordlist>(
58 mnemonic: &str,
59 path: &str,
60 index: u32,
61) -> Result<U256, String> {
62 let wallet = MnemonicBuilder::<W>::default()
63 .phrase(mnemonic)
64 .derivation_path(derive_key_path_checked(path, index)?)
65 .map_err(|e| e.to_string())?
66 .build()
67 .map_err(|e| e.to_string())?;
68 Ok(U256::from_be_bytes(wallet.credential().to_bytes().into()))
69}
70
71pub fn derive_private_key_with_language(
76 mnemonic: &str,
77 path: &str,
78 index: u32,
79 language: &str,
80) -> Result<U256, String> {
81 match language {
82 "chinese_simplified" => derive_private_key::<ChineseSimplified>(mnemonic, path, index),
83 "chinese_traditional" => derive_private_key::<ChineseTraditional>(mnemonic, path, index),
84 "czech" => derive_private_key::<Czech>(mnemonic, path, index),
85 "english" => derive_private_key::<English>(mnemonic, path, index),
86 "french" => derive_private_key::<French>(mnemonic, path, index),
87 "italian" => derive_private_key::<Italian>(mnemonic, path, index),
88 "japanese" => derive_private_key::<Japanese>(mnemonic, path, index),
89 "korean" => derive_private_key::<Korean>(mnemonic, path, index),
90 "portuguese" => derive_private_key::<Portuguese>(mnemonic, path, index),
91 "spanish" => derive_private_key::<Spanish>(mnemonic, path, index),
92 _ => Err(format!("unsupported mnemonic language: {language:?}")),
93 }
94}
95
96pub fn private_key_from_u256(private_key: U256) -> Result<PrivateKeySigner, String> {
100 if private_key.is_zero() {
101 return Err("private key cannot be zero".to_string());
102 }
103 PrivateKeySigner::from_slice(&private_key.to_be_bytes::<32>()).map_err(|e| e.to_string())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 const MNEMONIC: &str = "test test test test test test test test test test test junk";
112
113 #[test]
114 fn validate_rejects_overflow_and_leaves_malformed_to_builder() {
115 assert!(validate_bip32_path("m/44'/60'/0'/0/2147483648").is_err());
116 assert!(validate_bip32_path("m/2147483648'").is_err());
117 assert!(validate_bip32_path("m/2147483648h").is_err());
118 assert!(validate_bip32_path("m/02147483648").is_err());
119 assert!(validate_bip32_path(&format!("m/{}", u32::MAX)).is_err());
120 assert!(validate_bip32_path("m/44'/60'/0'/0/0").is_ok());
121 assert!(validate_bip32_path("m/44'/60'/0'/0/0'").is_ok());
122 assert!(validate_bip32_path(&format!("m/{}", BIP32_HARDEN - 1)).is_ok());
123 assert!(validate_bip32_path("m/+2147483648").is_err());
124 assert!(validate_bip32_path("m/not-a-number").is_ok());
125 }
126
127 #[test]
128 fn derive_key_path_stays_string() {
129 assert_eq!(derive_key_path("m/44'/60'/0'/0", 0), "m/44'/60'/0'/0/0");
130 assert_eq!(derive_key_path("m/44'/60'/0'/0/", 1), "m/44'/60'/0'/0/1");
131 }
132
133 #[test]
134 fn derive_key_path_checked_rejects_overflowing_index() {
135 assert!(derive_key_path_checked("m/44'/60'/0'/0", BIP32_HARDEN).is_err());
136 assert_eq!(derive_key_path_checked("m/44'/60'/0'/0", 0).unwrap(), "m/44'/60'/0'/0/0");
137 }
138
139 #[test]
140 fn derive_private_key_rejects_overflowing_path_and_index() {
141 let err =
142 derive_private_key::<English>(MNEMONIC, "m/44'/60'/0'/0", BIP32_HARDEN).unwrap_err();
143 assert!(err.contains("harden bit"), "{err}");
144
145 let err =
146 derive_private_key::<English>(MNEMONIC, "m/44'/60'/0'/0/2147483648'", 0).unwrap_err();
147 assert!(err.contains("harden bit") || err.contains("overflow"), "{err}");
148
149 let key = derive_private_key::<English>(MNEMONIC, "m/44'/60'/0'/0", 0).unwrap();
150 assert!(!key.is_zero());
151 }
152}