Skip to main content

foundry_common/
wallet.rs

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
10/// BIP-32 harden bit (`0x8000_0000`). Indices at or above this already encode hardened.
11pub const BIP32_HARDEN: u32 = 0x8000_0000;
12
13/// Reject BIP-32 path components whose parsed index overflows the harden bit.
14///
15/// `coins-bip32` parses `2147483648'` as `harden_index(0x8000_0000)` which wraps to `0`,
16/// silently selecting the wrong (unhardened) child. Raw `2147483648` is also rejected
17/// because it already equals the harden bit (`0'`).
18///
19/// This is only an overflow guard. One trailing `'` or `h` is stripped so the numeric
20/// index can be checked; malformed syntax is left to [`MnemonicBuilder`].
21pub 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
35/// Appends `index` to `path`, inserting a `/` separator when needed.
36pub 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
45/// [`derive_key_path`] plus the harden-bit overflow guard for mnemonic derive consumers.
46pub 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
56/// Derives a private key from a BIP-39 mnemonic using the given BIP-32 path and index.
57pub 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
71/// Derives a private key from a BIP-39 mnemonic, selecting the wordlist by name.
72///
73/// Recognised language names: `chinese_simplified`, `chinese_traditional`, `czech`, `english`,
74/// `french`, `italian`, `japanese`, `korean`, `portuguese`, `spanish`.
75pub 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
96/// Constructs a [`PrivateKeySigner`] from a raw private key value.
97///
98/// Returns `Err` when `private_key` is zero or its bytes are not a valid secp256k1 scalar.
99pub 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    // anvil default test mnemonic
111    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}