Skip to main content

wallet_hd_derivation_kit/
lib.rs

1use bip39::{Language, Mnemonic};
2use ed25519_dalek::SigningKey;
3use hmac::{Hmac, Mac};
4use k256::{
5    elliptic_curve::{
6        ff::PrimeField,
7        group::Group,
8        sec1::{FromEncodedPoint, ToEncodedPoint},
9    },
10    AffinePoint, EncodedPoint, ProjectivePoint, Scalar,
11};
12use ripemd::Ripemd160;
13use serde::Serialize;
14use sha2::{Digest, Sha256, Sha512};
15use sha3::Keccak256;
16use thiserror::Error;
17use zeroize::{Zeroize, ZeroizeOnDrop};
18
19type HmacSha512 = Hmac<Sha512>;
20pub const HARDENED_OFFSET: u32 = 0x8000_0000;
21pub const API_SCHEMA_VERSION: u8 = 1;
22
23#[derive(Debug, Error)]
24pub enum HdError {
25    #[error("invalid BIP39 English mnemonic")]
26    InvalidMnemonic,
27    #[error("seed must be between 16 and 64 bytes")]
28    InvalidSeed,
29    #[error("invalid derivation path: {0}")]
30    InvalidPath(String),
31    #[error("unsupported chain: {0}")]
32    UnsupportedChain(String),
33    #[error("unsupported script type: {0}")]
34    UnsupportedScript(String),
35    #[error("unsupported extended-key format: {0}")]
36    UnsupportedFormat(String),
37    #[error("extended public keys can derive only non-hardened children")]
38    HardenedPublicDerivation,
39    #[error("invalid BIP32 key material")]
40    InvalidKey,
41    #[error("invalid extended key")]
42    InvalidExtendedKey,
43    #[error("chain is required because extended-key version bytes do not identify every coin")]
44    ChainRequired,
45    #[error("Solana SLIP10 does not define extended public keys")]
46    NoExtendedPublicKey,
47}
48
49pub type Result<T> = std::result::Result<T, HdError>;
50
51#[derive(Clone, Zeroize, ZeroizeOnDrop)]
52pub enum Source {
53    Mnemonic { words: String, passphrase: String },
54    Seed(Vec<u8>),
55}
56
57impl Source {
58    pub fn mnemonic(words: impl Into<String>, passphrase: impl Into<String>) -> Self {
59        Self::Mnemonic {
60            words: words.into(),
61            passphrase: passphrase.into(),
62        }
63    }
64
65    pub fn seed(&self) -> Result<Vec<u8>> {
66        match self {
67            Source::Seed(seed) if (16..=64).contains(&seed.len()) => Ok(seed.clone()),
68            Source::Seed(_) => Err(HdError::InvalidSeed),
69            Source::Mnemonic { words, passphrase } => {
70                let mnemonic = Mnemonic::parse_in_normalized(Language::English, words)
71                    .map_err(|_| HdError::InvalidMnemonic)?;
72                Ok(mnemonic.to_seed(passphrase).to_vec())
73            }
74        }
75    }
76}
77
78#[derive(Clone, Copy, Debug)]
79pub struct Format {
80    pub name: &'static str,
81    pub public_version: u32,
82    pub private_version: u32,
83    pub purpose: u32,
84    pub script_type: &'static str,
85}
86
87const XPUB: Format = Format {
88    name: "xpub",
89    public_version: 0x0488_b21e,
90    private_version: 0x0488_ade4,
91    purpose: 44,
92    script_type: "p2pkh",
93};
94const YPUB: Format = Format {
95    name: "ypub",
96    public_version: 0x049d_7cb2,
97    private_version: 0x049d_7878,
98    purpose: 49,
99    script_type: "p2sh-p2wpkh",
100};
101const ZPUB: Format = Format {
102    name: "zpub",
103    public_version: 0x04b2_4746,
104    private_version: 0x04b2_430c,
105    purpose: 84,
106    script_type: "p2wpkh",
107};
108const TPUB: Format = Format {
109    name: "tpub",
110    public_version: 0x0435_87cf,
111    private_version: 0x0435_8394,
112    purpose: 44,
113    script_type: "p2pkh",
114};
115const UPUB: Format = Format {
116    name: "upub",
117    public_version: 0x044a_5262,
118    private_version: 0x044a_4e28,
119    purpose: 49,
120    script_type: "p2sh-p2wpkh",
121};
122const VPUB: Format = Format {
123    name: "vpub",
124    public_version: 0x045f_1cf6,
125    private_version: 0x045f_18bc,
126    purpose: 84,
127    script_type: "p2wpkh",
128};
129const LTUB: Format = Format {
130    name: "Ltub",
131    public_version: 0x019d_a462,
132    private_version: 0x019d_9cfe,
133    purpose: 44,
134    script_type: "p2pkh",
135};
136const MTUB: Format = Format {
137    name: "Mtub",
138    public_version: 0x01b2_6ef6,
139    private_version: 0x01b2_6792,
140    purpose: 49,
141    script_type: "p2sh-p2wpkh",
142};
143
144pub fn format(name: &str) -> Result<Format> {
145    match name {
146        "xpub" => Ok(XPUB),
147        "ypub" => Ok(YPUB),
148        "zpub" => Ok(ZPUB),
149        "tpub" => Ok(TPUB),
150        "upub" => Ok(UPUB),
151        "vpub" => Ok(VPUB),
152        "Ltub" => Ok(LTUB),
153        "Mtub" => Ok(MTUB),
154        _ => Err(HdError::UnsupportedFormat(name.into())),
155    }
156}
157
158#[derive(Clone, Copy, Debug, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct Chain {
161    pub id: &'static str,
162    pub name: &'static str,
163    pub symbol: &'static str,
164    pub coin_type: u32,
165    pub curve: &'static str,
166    pub default_format: Option<&'static str>,
167    pub default_script_type: &'static str,
168    #[serde(skip)]
169    pub p2pkh: &'static [u8],
170    #[serde(skip)]
171    pub p2sh: &'static [u8],
172    pub hrp: Option<&'static str>,
173}
174
175pub const CHAINS: &[Chain] = &[
176    Chain {
177        id: "bitcoin",
178        name: "Bitcoin",
179        symbol: "BTC",
180        coin_type: 0,
181        curve: "secp256k1",
182        default_format: Some("zpub"),
183        default_script_type: "p2wpkh",
184        p2pkh: &[0x00],
185        p2sh: &[0x05],
186        hrp: Some("bc"),
187    },
188    Chain {
189        id: "bitcoin-testnet",
190        name: "Bitcoin Testnet",
191        symbol: "TBTC",
192        coin_type: 1,
193        curve: "secp256k1",
194        default_format: Some("vpub"),
195        default_script_type: "p2wpkh",
196        p2pkh: &[0x6f],
197        p2sh: &[0xc4],
198        hrp: Some("tb"),
199    },
200    Chain {
201        id: "litecoin",
202        name: "Litecoin",
203        symbol: "LTC",
204        coin_type: 2,
205        curve: "secp256k1",
206        default_format: Some("Ltub"),
207        default_script_type: "p2pkh",
208        p2pkh: &[0x30],
209        p2sh: &[0x32],
210        hrp: Some("ltc"),
211    },
212    Chain {
213        id: "dogecoin",
214        name: "Dogecoin",
215        symbol: "DOGE",
216        coin_type: 3,
217        curve: "secp256k1",
218        default_format: Some("xpub"),
219        default_script_type: "p2pkh",
220        p2pkh: &[0x1e],
221        p2sh: &[0x16],
222        hrp: None,
223    },
224    Chain {
225        id: "dash",
226        name: "Dash",
227        symbol: "DASH",
228        coin_type: 5,
229        curve: "secp256k1",
230        default_format: Some("xpub"),
231        default_script_type: "p2pkh",
232        p2pkh: &[0x4c],
233        p2sh: &[0x10],
234        hrp: None,
235    },
236    Chain {
237        id: "digibyte",
238        name: "DigiByte",
239        symbol: "DGB",
240        coin_type: 20,
241        curve: "secp256k1",
242        default_format: Some("xpub"),
243        default_script_type: "p2pkh",
244        p2pkh: &[0x1e],
245        p2sh: &[0x3f],
246        hrp: Some("dgb"),
247    },
248    Chain {
249        id: "bitcoin-cash",
250        name: "Bitcoin Cash",
251        symbol: "BCH",
252        coin_type: 145,
253        curve: "secp256k1",
254        default_format: Some("xpub"),
255        default_script_type: "cashaddr",
256        p2pkh: &[0x00],
257        p2sh: &[0x05],
258        hrp: None,
259    },
260    Chain {
261        id: "zcash-transparent",
262        name: "Zcash Transparent",
263        symbol: "ZEC",
264        coin_type: 133,
265        curve: "secp256k1",
266        default_format: Some("xpub"),
267        default_script_type: "p2pkh",
268        p2pkh: &[0x1c, 0xb8],
269        p2sh: &[0x1c, 0xbd],
270        hrp: None,
271    },
272    Chain {
273        id: "ethereum",
274        name: "Ethereum",
275        symbol: "ETH",
276        coin_type: 60,
277        curve: "secp256k1",
278        default_format: None,
279        default_script_type: "evm",
280        p2pkh: &[],
281        p2sh: &[],
282        hrp: None,
283    },
284    Chain {
285        id: "ethereum-classic",
286        name: "Ethereum Classic",
287        symbol: "ETC",
288        coin_type: 61,
289        curve: "secp256k1",
290        default_format: None,
291        default_script_type: "evm",
292        p2pkh: &[],
293        p2sh: &[],
294        hrp: None,
295    },
296    Chain {
297        id: "polygon",
298        name: "Polygon",
299        symbol: "POL",
300        coin_type: 60,
301        curve: "secp256k1",
302        default_format: None,
303        default_script_type: "evm",
304        p2pkh: &[],
305        p2sh: &[],
306        hrp: None,
307    },
308    Chain {
309        id: "bsc",
310        name: "BNB Smart Chain",
311        symbol: "BNB",
312        coin_type: 60,
313        curve: "secp256k1",
314        default_format: None,
315        default_script_type: "evm",
316        p2pkh: &[],
317        p2sh: &[],
318        hrp: None,
319    },
320    Chain {
321        id: "avalanche-c",
322        name: "Avalanche C-Chain",
323        symbol: "AVAX",
324        coin_type: 60,
325        curve: "secp256k1",
326        default_format: None,
327        default_script_type: "evm",
328        p2pkh: &[],
329        p2sh: &[],
330        hrp: None,
331    },
332    Chain {
333        id: "arbitrum",
334        name: "Arbitrum",
335        symbol: "ARB",
336        coin_type: 60,
337        curve: "secp256k1",
338        default_format: None,
339        default_script_type: "evm",
340        p2pkh: &[],
341        p2sh: &[],
342        hrp: None,
343    },
344    Chain {
345        id: "optimism",
346        name: "Optimism",
347        symbol: "OP",
348        coin_type: 60,
349        curve: "secp256k1",
350        default_format: None,
351        default_script_type: "evm",
352        p2pkh: &[],
353        p2sh: &[],
354        hrp: None,
355    },
356    Chain {
357        id: "base",
358        name: "Base",
359        symbol: "ETH",
360        coin_type: 60,
361        curve: "secp256k1",
362        default_format: None,
363        default_script_type: "evm",
364        p2pkh: &[],
365        p2sh: &[],
366        hrp: None,
367    },
368    Chain {
369        id: "tron",
370        name: "TRON",
371        symbol: "TRX",
372        coin_type: 195,
373        curve: "secp256k1",
374        default_format: None,
375        default_script_type: "tron",
376        p2pkh: &[],
377        p2sh: &[],
378        hrp: None,
379    },
380    Chain {
381        id: "solana",
382        name: "Solana",
383        symbol: "SOL",
384        coin_type: 501,
385        curve: "ed25519",
386        default_format: None,
387        default_script_type: "solana",
388        p2pkh: &[],
389        p2sh: &[],
390        hrp: None,
391    },
392];
393
394pub fn supported_chains() -> &'static [Chain] {
395    CHAINS
396}
397
398pub fn chain(id: &str) -> Result<Chain> {
399    CHAINS
400        .iter()
401        .find(|item| item.id == id)
402        .copied()
403        .ok_or_else(|| HdError::UnsupportedChain(id.into()))
404}
405
406#[derive(Clone, Zeroize, ZeroizeOnDrop)]
407pub struct ExtendedPrivateKey {
408    pub private_key: [u8; 32],
409    pub chain_code: [u8; 32],
410    pub depth: u8,
411    pub parent_fingerprint: [u8; 4],
412    pub child_number: u32,
413}
414
415#[derive(Clone, Debug)]
416pub struct ExtendedPublicKey {
417    pub public_key: [u8; 33],
418    pub chain_code: [u8; 32],
419    pub depth: u8,
420    pub parent_fingerprint: [u8; 4],
421    pub child_number: u32,
422}
423
424impl ExtendedPrivateKey {
425    pub fn master(seed: &[u8]) -> Result<Self> {
426        if !(16..=64).contains(&seed.len()) {
427            return Err(HdError::InvalidSeed);
428        }
429        let mut material = seed.to_vec();
430        loop {
431            let digest = hmac_sha512(b"Bitcoin seed", &material);
432            let private_key: [u8; 32] = digest[..32].try_into().unwrap();
433            if scalar(&private_key).is_ok() {
434                return Ok(Self {
435                    private_key,
436                    chain_code: digest[32..].try_into().unwrap(),
437                    depth: 0,
438                    parent_fingerprint: [0; 4],
439                    child_number: 0,
440                });
441            }
442            material = digest.to_vec();
443        }
444    }
445
446    pub fn public_key(&self) -> [u8; 33] {
447        let scalar = scalar(&self.private_key).expect("validated private key");
448        let point = (ProjectivePoint::GENERATOR * scalar)
449            .to_affine()
450            .to_encoded_point(true);
451        point.as_bytes().try_into().unwrap()
452    }
453
454    pub fn fingerprint(&self) -> [u8; 4] {
455        hash160(&self.public_key())[..4].try_into().unwrap()
456    }
457
458    pub fn derive(&self, index: u32) -> Result<Self> {
459        let mut data = Vec::with_capacity(37);
460        if index >= HARDENED_OFFSET {
461            data.push(0);
462            data.extend(self.private_key);
463        } else {
464            data.extend(self.public_key());
465        }
466        data.extend(index.to_be_bytes());
467        let digest = hmac_sha512(&self.chain_code, &data);
468        let tweak = scalar(&digest[..32].try_into().unwrap())?;
469        let child = tweak + scalar(&self.private_key)?;
470        if bool::from(child.is_zero()) {
471            return Err(HdError::InvalidKey);
472        }
473        Ok(Self {
474            private_key: child.to_bytes().into(),
475            chain_code: digest[32..].try_into().unwrap(),
476            depth: self.depth.checked_add(1).ok_or(HdError::InvalidKey)?,
477            parent_fingerprint: self.fingerprint(),
478            child_number: index,
479        })
480    }
481
482    pub fn derive_path(&self, path: &str) -> Result<Self> {
483        parse_path(path)?
484            .into_iter()
485            .try_fold(self.clone(), |node, index| node.derive(index))
486    }
487
488    pub fn neuter(&self) -> ExtendedPublicKey {
489        ExtendedPublicKey {
490            public_key: self.public_key(),
491            chain_code: self.chain_code,
492            depth: self.depth,
493            parent_fingerprint: self.parent_fingerprint,
494            child_number: self.child_number,
495        }
496    }
497
498    pub fn serialize_private(&self, version: u32) -> String {
499        let mut key = [0u8; 33];
500        key[1..].copy_from_slice(&self.private_key);
501        serialize_key(
502            version,
503            self.depth,
504            &self.parent_fingerprint,
505            self.child_number,
506            &self.chain_code,
507            &key,
508        )
509    }
510
511    pub fn serialize_public(&self, version: u32) -> String {
512        self.neuter().serialize_public(version)
513    }
514}
515
516impl ExtendedPublicKey {
517    pub fn fingerprint(&self) -> [u8; 4] {
518        hash160(&self.public_key)[..4].try_into().unwrap()
519    }
520
521    pub fn derive(&self, index: u32) -> Result<Self> {
522        if index >= HARDENED_OFFSET {
523            return Err(HdError::HardenedPublicDerivation);
524        }
525        let mut data = self.public_key.to_vec();
526        data.extend(index.to_be_bytes());
527        let digest = hmac_sha512(&self.chain_code, &data);
528        let tweak = scalar(&digest[..32].try_into().unwrap())?;
529        let encoded = EncodedPoint::from_bytes(self.public_key).map_err(|_| HdError::InvalidKey)?;
530        let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
531            .ok_or(HdError::InvalidKey)?;
532        let child = ProjectivePoint::GENERATOR * tweak + ProjectivePoint::from(affine);
533        if bool::from(child.is_identity()) {
534            return Err(HdError::InvalidKey);
535        }
536        let point = child.to_affine().to_encoded_point(true);
537        Ok(Self {
538            public_key: point.as_bytes().try_into().unwrap(),
539            chain_code: digest[32..].try_into().unwrap(),
540            depth: self.depth.checked_add(1).ok_or(HdError::InvalidKey)?,
541            parent_fingerprint: self.fingerprint(),
542            child_number: index,
543        })
544    }
545
546    pub fn serialize_public(&self, version: u32) -> String {
547        serialize_key(
548            version,
549            self.depth,
550            &self.parent_fingerprint,
551            self.child_number,
552            &self.chain_code,
553            &self.public_key,
554        )
555    }
556}
557
558pub enum ParsedExtendedKey {
559    Private(ExtendedPrivateKey, u32),
560    Public(ExtendedPublicKey, u32),
561}
562
563pub fn parse_extended_key(value: &str) -> Result<ParsedExtendedKey> {
564    let decoded = bs58::decode(value)
565        .with_check(None)
566        .into_vec()
567        .map_err(|_| HdError::InvalidExtendedKey)?;
568    if decoded.len() != 78 {
569        return Err(HdError::InvalidExtendedKey);
570    }
571    let version = u32::from_be_bytes(decoded[..4].try_into().unwrap());
572    let depth = decoded[4];
573    let parent_fingerprint = decoded[5..9].try_into().unwrap();
574    let child_number = u32::from_be_bytes(decoded[9..13].try_into().unwrap());
575    let chain_code = decoded[13..45].try_into().unwrap();
576    let is_private = decoded[45] == 0;
577    let registered = [XPUB, YPUB, ZPUB, TPUB, UPUB, VPUB, LTUB, MTUB]
578        .iter()
579        .any(|candidate| {
580            version
581                == if is_private {
582                    candidate.private_version
583                } else {
584                    candidate.public_version
585                }
586        });
587    if !registered || (depth == 0 && (parent_fingerprint != [0; 4] || child_number != 0)) {
588        return Err(HdError::InvalidExtendedKey);
589    }
590    if is_private {
591        let private_key = decoded[46..78].try_into().unwrap();
592        scalar(&private_key)?;
593        Ok(ParsedExtendedKey::Private(
594            ExtendedPrivateKey {
595                private_key,
596                chain_code,
597                depth,
598                parent_fingerprint,
599                child_number,
600            },
601            version,
602        ))
603    } else {
604        let public_key = decoded[45..78].try_into().unwrap();
605        let encoded =
606            EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidExtendedKey)?;
607        Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
608            .ok_or(HdError::InvalidExtendedKey)?;
609        Ok(ParsedExtendedKey::Public(
610            ExtendedPublicKey {
611                public_key,
612                chain_code,
613                depth,
614                parent_fingerprint,
615                child_number,
616            },
617            version,
618        ))
619    }
620}
621
622pub fn parse_path(path: &str) -> Result<Vec<u32>> {
623    if path == "m" || path == "M" {
624        return Ok(vec![]);
625    }
626    let mut parts = path.split('/');
627    if !matches!(parts.next(), Some("m" | "M")) {
628        return Err(HdError::InvalidPath(path.into()));
629    }
630    let values: Vec<_> = parts.collect();
631    if values.is_empty() || values.len() > 255 {
632        return Err(HdError::InvalidPath(path.into()));
633    }
634    values
635        .into_iter()
636        .map(|part| {
637            let hardened = part.ends_with(['\'', 'h', 'H']);
638            let raw = if hardened {
639                &part[..part.len() - 1]
640            } else {
641                part
642            };
643            if raw.is_empty() || (raw.len() > 1 && raw.starts_with('0')) {
644                return Err(HdError::InvalidPath(path.into()));
645            }
646            let value: u32 = raw.parse().map_err(|_| HdError::InvalidPath(path.into()))?;
647            if value >= HARDENED_OFFSET {
648                return Err(HdError::InvalidPath(path.into()));
649            }
650            Ok(if hardened {
651                value + HARDENED_OFFSET
652            } else {
653                value
654            })
655        })
656        .collect()
657}
658
659#[derive(Debug, Serialize, Clone)]
660#[serde(rename_all = "camelCase")]
661pub struct DerivedAddress {
662    pub schema_version: u8,
663    pub chain: String,
664    pub curve: String,
665    pub path: String,
666    pub account: u32,
667    pub change: u32,
668    pub index: u32,
669    pub script_type: String,
670    pub address: String,
671    pub public_key_hex: String,
672}
673
674#[derive(Debug, Serialize)]
675#[serde(rename_all = "camelCase")]
676pub struct NodeResult {
677    pub schema_version: u8,
678    pub curve: String,
679    pub path: String,
680    pub public_key_hex: String,
681    pub chain_code_hex: String,
682    pub depth: u8,
683    pub child_number: u32,
684}
685
686#[derive(Debug, Serialize)]
687#[serde(rename_all = "camelCase")]
688pub struct AccountPublicKey {
689    pub schema_version: u8,
690    pub chain: String,
691    pub curve: String,
692    pub path: String,
693    pub format: String,
694    pub extended_public_key: String,
695    pub public_key_hex: String,
696}
697
698#[derive(Serialize)]
699#[serde(rename_all = "camelCase")]
700pub struct AccountPrivateKey {
701    pub schema_version: u8,
702    pub chain: String,
703    pub curve: String,
704    pub path: String,
705    pub format: Option<String>,
706    pub extended_private_key: Option<String>,
707    pub private_key_hex: String,
708    pub public_key_hex: String,
709}
710
711#[derive(Clone, Debug, Default)]
712pub struct DeriveOptions<'a> {
713    pub chain: &'a str,
714    pub format: Option<&'a str>,
715    pub script_type: Option<&'a str>,
716    pub path: Option<&'a str>,
717    pub account: u32,
718    pub change: u32,
719    pub index: u32,
720}
721
722pub fn derive_node(source: &Source, curve: &str, path: &str) -> Result<NodeResult> {
723    let seed = source.seed()?;
724    match curve {
725        "secp256k1" => {
726            let node = ExtendedPrivateKey::master(&seed)?.derive_path(path)?;
727            Ok(NodeResult {
728                schema_version: API_SCHEMA_VERSION,
729                curve: curve.into(),
730                path: path.into(),
731                public_key_hex: hex::encode(node.public_key()),
732                chain_code_hex: hex::encode(node.chain_code),
733                depth: node.depth,
734                child_number: node.child_number,
735            })
736        }
737        "ed25519" => {
738            let node = slip10_ed25519(&seed, path)?;
739            Ok(NodeResult {
740                schema_version: API_SCHEMA_VERSION,
741                curve: curve.into(),
742                path: path.into(),
743                public_key_hex: hex::encode(node.public_key),
744                chain_code_hex: hex::encode(node.chain_code),
745                depth: node.depth,
746                child_number: node.child_number,
747            })
748        }
749        _ => Err(HdError::UnsupportedFormat(curve.into())),
750    }
751}
752
753pub fn serialize_extended_key(key: &ParsedExtendedKey, version: Option<u32>) -> String {
754    match key {
755        ParsedExtendedKey::Private(node, original) => {
756            node.serialize_private(version.unwrap_or(*original))
757        }
758        ParsedExtendedKey::Public(node, original) => {
759            node.serialize_public(version.unwrap_or(*original))
760        }
761    }
762}
763
764pub fn derive_account_public_key(
765    source: &Source,
766    options: DeriveOptions<'_>,
767) -> Result<AccountPublicKey> {
768    let chain = chain(if options.chain.is_empty() {
769        "bitcoin"
770    } else {
771        options.chain
772    })?;
773    if chain.curve == "ed25519" {
774        return Err(HdError::NoExtendedPublicKey);
775    }
776    let fmt = resolve_format(chain, options.format, options.script_type)?;
777    let path = options
778        .path
779        .map(str::to_owned)
780        .unwrap_or_else(|| account_path(chain, options.account, options.script_type, fmt));
781    let node = ExtendedPrivateKey::master(&source.seed()?)?.derive_path(&path)?;
782    Ok(AccountPublicKey {
783        schema_version: API_SCHEMA_VERSION,
784        chain: chain.id.into(),
785        curve: chain.curve.into(),
786        path,
787        format: fmt.name.into(),
788        extended_public_key: node.serialize_public(fmt.public_version),
789        public_key_hex: hex::encode(node.public_key()),
790    })
791}
792
793pub fn derive_account_private_key(
794    source: &Source,
795    options: DeriveOptions<'_>,
796) -> Result<AccountPrivateKey> {
797    let chain = chain(if options.chain.is_empty() {
798        "bitcoin"
799    } else {
800        options.chain
801    })?;
802    let path = options.path.map(str::to_owned).unwrap_or_else(|| {
803        account_path(
804            chain,
805            options.account,
806            options.script_type,
807            resolve_format(chain, options.format, options.script_type).unwrap_or(XPUB),
808        )
809    });
810    let seed = source.seed()?;
811    if chain.curve == "ed25519" {
812        let node = slip10_ed25519(&seed, &path)?;
813        return Ok(AccountPrivateKey {
814            schema_version: API_SCHEMA_VERSION,
815            chain: chain.id.into(),
816            curve: chain.curve.into(),
817            path,
818            format: None,
819            extended_private_key: None,
820            private_key_hex: hex::encode(node.private_key),
821            public_key_hex: hex::encode(node.public_key),
822        });
823    }
824    let fmt = resolve_format(chain, options.format, options.script_type)?;
825    let node = ExtendedPrivateKey::master(&seed)?.derive_path(&path)?;
826    Ok(AccountPrivateKey {
827        schema_version: API_SCHEMA_VERSION,
828        chain: chain.id.into(),
829        curve: chain.curve.into(),
830        path,
831        format: Some(fmt.name.into()),
832        extended_private_key: Some(node.serialize_private(fmt.private_version)),
833        private_key_hex: hex::encode(node.private_key),
834        public_key_hex: hex::encode(node.public_key()),
835    })
836}
837
838pub fn derive_address(source: &Source, options: DeriveOptions<'_>) -> Result<DerivedAddress> {
839    let chain = chain(if options.chain.is_empty() {
840        "bitcoin"
841    } else {
842        options.chain
843    })?;
844    let script_type = options.script_type.unwrap_or(chain.default_script_type);
845    let seed = source.seed()?;
846    if chain.curve == "ed25519" {
847        let path = options.path.map(str::to_owned).unwrap_or_else(|| {
848            format!(
849                "m/44'/{}'/{}'/{}'",
850                chain.coin_type, options.account, options.index
851            )
852        });
853        let node = slip10_ed25519(&seed, &path)?;
854        return Ok(address_result(
855            chain,
856            path,
857            options,
858            script_type,
859            bs58::encode(node.public_key).into_string(),
860            node.public_key.to_vec(),
861        ));
862    }
863    let fmt = resolve_format(chain, options.format, Some(script_type)).unwrap_or(XPUB);
864    let path = options.path.map(str::to_owned).unwrap_or_else(|| {
865        format!(
866            "{}/{}/{}",
867            account_path(chain, options.account, Some(script_type), fmt),
868            options.change,
869            options.index
870        )
871    });
872    let node = ExtendedPrivateKey::master(&seed)?.derive_path(&path)?;
873    let public_key = node.public_key();
874    let address = public_key_to_address(&public_key, chain, script_type)?;
875    Ok(address_result(
876        chain,
877        path,
878        options,
879        script_type,
880        address,
881        public_key.to_vec(),
882    ))
883}
884
885pub fn derive_addresses(
886    source: &Source,
887    mut options: DeriveOptions<'_>,
888    start: u32,
889    count: u32,
890) -> Result<Vec<DerivedAddress>> {
891    if count == 0 || count > 10_000 {
892        return Err(HdError::InvalidPath(
893            "count must be between 1 and 10000".into(),
894        ));
895    }
896    (start..start.checked_add(count).ok_or(HdError::InvalidKey)?)
897        .map(|index| {
898            options.index = index;
899            derive_address(source, options.clone())
900        })
901        .collect()
902}
903
904pub fn derive_address_from_extended_public_key(
905    value: &str,
906    chain_id: Option<&str>,
907    change: u32,
908    index: u32,
909    script_type: Option<&str>,
910) -> Result<DerivedAddress> {
911    let chain = chain(chain_id.ok_or(HdError::ChainRequired)?)?;
912    let (node, version) = match parse_extended_key(value)? {
913        ParsedExtendedKey::Public(node, version) => (node, version),
914        _ => return Err(HdError::InvalidExtendedKey),
915    };
916    let node = node.derive(change)?.derive(index)?;
917    let script = script_type.unwrap_or_else(|| match version {
918        0x049d_7cb2 | 0x044a_5262 | 0x01b2_6ef6 => "p2sh-p2wpkh",
919        0x04b2_4746 | 0x045f_1cf6 => "p2wpkh",
920        _ => chain.default_script_type,
921    });
922    let address = public_key_to_address(&node.public_key, chain, script)?;
923    Ok(address_result(
924        chain,
925        format!("{change}/{index}"),
926        DeriveOptions {
927            chain: chain.id,
928            change,
929            index,
930            ..Default::default()
931        },
932        script,
933        address,
934        node.public_key.to_vec(),
935    ))
936}
937
938fn resolve_format(chain: Chain, requested: Option<&str>, script: Option<&str>) -> Result<Format> {
939    let name = requested
940        .or_else(|| {
941            if script == Some("p2tr") {
942                Some("xpub")
943            } else {
944                chain.default_format
945            }
946        })
947        .unwrap_or("xpub");
948    let fmt = format(name)?;
949    let allowed = match chain.id {
950        "bitcoin" => ["xpub", "ypub", "zpub"].as_slice(),
951        "bitcoin-testnet" => ["tpub", "upub", "vpub"].as_slice(),
952        "litecoin" => ["Ltub", "Mtub"].as_slice(),
953        _ => ["xpub"].as_slice(),
954    };
955    if !allowed.contains(&fmt.name) {
956        return Err(HdError::UnsupportedFormat(name.into()));
957    }
958    Ok(fmt)
959}
960
961fn account_path(chain: Chain, account: u32, script: Option<&str>, fmt: Format) -> String {
962    let purpose = if script == Some("p2tr") {
963        86
964    } else {
965        fmt.purpose
966    };
967    format!("m/{purpose}'/{}'/{account}'", chain.coin_type)
968}
969
970fn address_result(
971    chain: Chain,
972    path: String,
973    options: DeriveOptions<'_>,
974    script: &str,
975    address: String,
976    public_key: Vec<u8>,
977) -> DerivedAddress {
978    DerivedAddress {
979        schema_version: API_SCHEMA_VERSION,
980        chain: chain.id.into(),
981        curve: chain.curve.into(),
982        path,
983        account: options.account,
984        change: options.change,
985        index: options.index,
986        script_type: script.into(),
987        address,
988        public_key_hex: hex::encode(public_key),
989    }
990}
991
992fn public_key_to_address(public_key: &[u8], chain: Chain, script: &str) -> Result<String> {
993    match script {
994        "evm" => Ok(evm_address(public_key)?),
995        "tron" => Ok(tron_address(public_key)?),
996        "cashaddr" => Ok(cash_address("bitcoincash", &hash160(public_key))),
997        "p2pkh" => Ok(base58check(&[chain.p2pkh, &hash160(public_key)].concat())),
998        "p2sh-p2wpkh" => {
999            let redeem = [&[0x00, 0x14][..], &hash160(public_key)].concat();
1000            Ok(base58check(&[chain.p2sh, &hash160(&redeem)].concat()))
1001        }
1002        "p2wpkh" => Ok(segwit_address(
1003            chain
1004                .hrp
1005                .ok_or_else(|| HdError::UnsupportedScript(script.into()))?,
1006            0,
1007            &hash160(public_key),
1008        )),
1009        "p2tr" => Ok(segwit_address(
1010            chain
1011                .hrp
1012                .ok_or_else(|| HdError::UnsupportedScript(script.into()))?,
1013            1,
1014            &taproot_output_key(public_key)?,
1015        )),
1016        _ => Err(HdError::UnsupportedScript(script.into())),
1017    }
1018}
1019
1020fn evm_address(public_key: &[u8]) -> Result<String> {
1021    let encoded = EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidKey)?;
1022    let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
1023        .ok_or(HdError::InvalidKey)?;
1024    let uncompressed = affine.to_encoded_point(false);
1025    let digest = Keccak256::digest(&uncompressed.as_bytes()[1..]);
1026    Ok(eip55(&digest[12..]))
1027}
1028
1029fn tron_address(public_key: &[u8]) -> Result<String> {
1030    let encoded = EncodedPoint::from_bytes(public_key).map_err(|_| HdError::InvalidKey)?;
1031    let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&encoded))
1032        .ok_or(HdError::InvalidKey)?;
1033    let uncompressed = affine.to_encoded_point(false);
1034    let digest = Keccak256::digest(&uncompressed.as_bytes()[1..]);
1035    Ok(base58check(&[&[0x41], &digest[12..]].concat()))
1036}
1037
1038fn eip55(bytes: &[u8]) -> String {
1039    let lower = hex::encode(bytes);
1040    let hash = hex::encode(Keccak256::digest(lower.as_bytes()));
1041    let mut output = String::from("0x");
1042    for (index, character) in lower.chars().enumerate() {
1043        if character.is_ascii_alphabetic()
1044            && u8::from_str_radix(&hash[index..=index], 16).unwrap() >= 8
1045        {
1046            output.push(character.to_ascii_uppercase());
1047        } else {
1048            output.push(character);
1049        }
1050    }
1051    output
1052}
1053
1054fn taproot_output_key(public_key: &[u8]) -> Result<Vec<u8>> {
1055    let x_only = &public_key[1..];
1056    let even =
1057        EncodedPoint::from_bytes([&[0x02], x_only].concat()).map_err(|_| HdError::InvalidKey)?;
1058    let affine = Option::<AffinePoint>::from(AffinePoint::from_encoded_point(&even))
1059        .ok_or(HdError::InvalidKey)?;
1060    let tweak_bytes: [u8; 32] = tagged_hash(b"TapTweak", x_only).into();
1061    let tweak = scalar(&tweak_bytes)?;
1062    let output = ProjectivePoint::from(affine) + ProjectivePoint::GENERATOR * tweak;
1063    Ok(output.to_affine().to_encoded_point(true).as_bytes()[1..].to_vec())
1064}
1065
1066struct Slip10Node {
1067    private_key: [u8; 32],
1068    public_key: [u8; 32],
1069    chain_code: [u8; 32],
1070    depth: u8,
1071    child_number: u32,
1072}
1073
1074fn slip10_ed25519(seed: &[u8], path: &str) -> Result<Slip10Node> {
1075    if !(16..=64).contains(&seed.len()) {
1076        return Err(HdError::InvalidSeed);
1077    }
1078    let mut digest = hmac_sha512(b"ed25519 seed", seed);
1079    let mut private_key: [u8; 32] = digest[..32].try_into().unwrap();
1080    let mut chain_code: [u8; 32] = digest[32..].try_into().unwrap();
1081    let mut depth = 0u8;
1082    let mut child_number = 0u32;
1083    for index in parse_path(path)? {
1084        if index < HARDENED_OFFSET {
1085            return Err(HdError::HardenedPublicDerivation);
1086        }
1087        let data = [&[0], &private_key[..], &index.to_be_bytes()].concat();
1088        digest = hmac_sha512(&chain_code, &data);
1089        private_key = digest[..32].try_into().unwrap();
1090        chain_code = digest[32..].try_into().unwrap();
1091        depth = depth
1092            .checked_add(1)
1093            .ok_or_else(|| HdError::InvalidPath(path.into()))?;
1094        child_number = index;
1095    }
1096    let public_key = SigningKey::from_bytes(&private_key)
1097        .verifying_key()
1098        .to_bytes();
1099    Ok(Slip10Node {
1100        private_key,
1101        public_key,
1102        chain_code,
1103        depth,
1104        child_number,
1105    })
1106}
1107
1108fn scalar(bytes: &[u8; 32]) -> Result<Scalar> {
1109    let value =
1110        Option::<Scalar>::from(Scalar::from_repr((*bytes).into())).ok_or(HdError::InvalidKey)?;
1111    if bool::from(value.is_zero()) {
1112        Err(HdError::InvalidKey)
1113    } else {
1114        Ok(value)
1115    }
1116}
1117
1118fn hmac_sha512(key: &[u8], data: &[u8]) -> [u8; 64] {
1119    let mut mac = HmacSha512::new_from_slice(key).expect("HMAC accepts arbitrary key sizes");
1120    mac.update(data);
1121    mac.finalize().into_bytes().into()
1122}
1123
1124fn hash160(bytes: &[u8]) -> Vec<u8> {
1125    Ripemd160::digest(Sha256::digest(bytes)).to_vec()
1126}
1127fn base58check(payload: &[u8]) -> String {
1128    bs58::encode(payload).with_check().into_string()
1129}
1130fn serialize_key(
1131    version: u32,
1132    depth: u8,
1133    parent: &[u8; 4],
1134    child: u32,
1135    chain_code: &[u8; 32],
1136    key: &[u8; 33],
1137) -> String {
1138    let payload = [
1139        &version.to_be_bytes()[..],
1140        &[depth],
1141        parent,
1142        &child.to_be_bytes(),
1143        chain_code,
1144        key,
1145    ]
1146    .concat();
1147    base58check(&payload)
1148}
1149
1150const BECH32_ALPHABET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1151fn segwit_address(hrp: &str, version: u8, program: &[u8]) -> String {
1152    let mut words = vec![version];
1153    words.extend(convert_bits(program, 8, 5, true).unwrap());
1154    bech32_encode(hrp, &words, if version == 0 { 1 } else { 0x2bc8_30a3 })
1155}
1156fn bech32_encode(hrp: &str, words: &[u8], constant: u32) -> String {
1157    let mut values: Vec<u8> = hrp.bytes().map(|byte| byte >> 5).collect();
1158    values.push(0);
1159    values.extend(hrp.bytes().map(|byte| byte & 31));
1160    values.extend(words);
1161    values.extend([0; 6]);
1162    let polymod = bech32_polymod(&values) ^ constant;
1163    let checksum: Vec<u8> = (0..6)
1164        .map(|index| ((polymod >> (5 * (5 - index))) & 31) as u8)
1165        .collect();
1166    let data: String = words
1167        .iter()
1168        .chain(checksum.iter())
1169        .map(|value| BECH32_ALPHABET[*value as usize] as char)
1170        .collect();
1171    format!("{hrp}1{data}")
1172}
1173fn bech32_polymod(values: &[u8]) -> u32 {
1174    let generators = [
1175        0x3b6a_57b2,
1176        0x2650_8e6d,
1177        0x1ea1_19fa,
1178        0x3d42_33dd,
1179        0x2a14_62b3,
1180    ];
1181    let mut checksum = 1u32;
1182    for value in values {
1183        let top = checksum >> 25;
1184        checksum = ((checksum & 0x1ff_ffff) << 5) ^ *value as u32;
1185        for (index, generator) in generators.iter().enumerate() {
1186            if (top >> index) & 1 == 1 {
1187                checksum ^= generator;
1188            }
1189        }
1190    }
1191    checksum
1192}
1193fn convert_bits(data: &[u8], from: u32, to: u32, pad: bool) -> Result<Vec<u8>> {
1194    let mut acc = 0u32;
1195    let mut bits = 0u32;
1196    let mut output = vec![];
1197    let mask = (1u32 << to) - 1;
1198    for value in data {
1199        if (*value as u32) >> from != 0 {
1200            return Err(HdError::InvalidKey);
1201        }
1202        acc = (acc << from) | *value as u32;
1203        bits += from;
1204        while bits >= to {
1205            bits -= to;
1206            output.push(((acc >> bits) & mask) as u8);
1207        }
1208    }
1209    if pad && bits > 0 {
1210        output.push(((acc << (to - bits)) & mask) as u8);
1211    } else if !pad && (bits >= from || ((acc << (to - bits)) & mask) != 0) {
1212        return Err(HdError::InvalidKey);
1213    }
1214    Ok(output)
1215}
1216
1217fn cash_address(prefix: &str, hash: &[u8]) -> String {
1218    let mut bytes = vec![0];
1219    bytes.extend(hash);
1220    let payload = convert_bits(&bytes, 8, 5, true).unwrap();
1221    let mut values: Vec<u8> = prefix.bytes().map(|byte| byte & 31).collect();
1222    values.push(0);
1223    values.extend(&payload);
1224    values.extend([0; 8]);
1225    let polymod = cashaddr_polymod(&values) ^ 1;
1226    let checksum: Vec<u8> = (0..8)
1227        .map(|index| ((polymod >> (5 * (7 - index))) & 31) as u8)
1228        .collect();
1229    let data: String = payload
1230        .iter()
1231        .chain(checksum.iter())
1232        .map(|value| BECH32_ALPHABET[*value as usize] as char)
1233        .collect();
1234    format!("{prefix}:{data}")
1235}
1236fn cashaddr_polymod(values: &[u8]) -> u64 {
1237    let generators = [
1238        0x98f2_bc8e61,
1239        0x79b7_6d99e2,
1240        0xf33e_5fb3c4,
1241        0xae2e_abe2a8,
1242        0x1e4f_43e470,
1243    ];
1244    let mut checksum = 1u64;
1245    for value in values {
1246        let top = checksum >> 35;
1247        checksum = ((checksum & 0x07_ff_ff_ff_ff) << 5) ^ *value as u64;
1248        for (index, generator) in generators.iter().enumerate() {
1249            if (top >> index) & 1 == 1 {
1250                checksum ^= generator;
1251            }
1252        }
1253    }
1254    checksum
1255}
1256fn tagged_hash(tag: &[u8], message: &[u8]) -> [u8; 32] {
1257    let tag_hash = Sha256::digest(tag);
1258    Sha256::digest([&tag_hash[..], &tag_hash[..], message].concat()).into()
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264    use proptest::prelude::*;
1265    const WORDS: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
1266    fn source() -> Source {
1267        Source::mnemonic(WORDS, "")
1268    }
1269
1270    #[test]
1271    fn slip132_and_bip86_vectors() {
1272        let zpub = derive_account_public_key(
1273            &source(),
1274            DeriveOptions {
1275                chain: "bitcoin",
1276                format: Some("zpub"),
1277                ..Default::default()
1278            },
1279        )
1280        .unwrap();
1281        assert_eq!(zpub.extended_public_key, "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs");
1282        let address = derive_address(
1283            &source(),
1284            DeriveOptions {
1285                chain: "bitcoin",
1286                ..Default::default()
1287            },
1288        )
1289        .unwrap();
1290        assert_eq!(
1291            address.address,
1292            "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1293        );
1294        let taproot = derive_address(
1295            &source(),
1296            DeriveOptions {
1297                chain: "bitcoin",
1298                script_type: Some("p2tr"),
1299                ..Default::default()
1300            },
1301        )
1302        .unwrap();
1303        assert_eq!(
1304            taproot.address,
1305            "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
1306        );
1307    }
1308
1309    #[test]
1310    fn multi_chain_vectors() {
1311        for (id, expected) in [
1312            ("litecoin", "LUWPbpM43E2p7ZSh8cyTBEkvpHmr3cB8Ez"),
1313            ("dogecoin", "DBus3bamQjgJULBJtYXpEzDWQRwF5iwxgC"),
1314            ("ethereum", "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"),
1315            ("tron", "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"),
1316            ("solana", "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk"),
1317        ] {
1318            assert_eq!(
1319                derive_address(
1320                    &source(),
1321                    DeriveOptions {
1322                        chain: id,
1323                        ..Default::default()
1324                    }
1325                )
1326                .unwrap()
1327                .address,
1328                expected
1329            );
1330        }
1331    }
1332
1333    #[test]
1334    fn public_and_private_children_match() {
1335        let root = ExtendedPrivateKey::master(&source().seed().unwrap())
1336            .unwrap()
1337            .derive_path("m/84'/0'/0'")
1338            .unwrap();
1339        assert_eq!(
1340            root.derive(0).unwrap().public_key(),
1341            root.neuter().derive(0).unwrap().public_key
1342        );
1343        assert!(root.neuter().derive(HARDENED_OFFSET).is_err());
1344    }
1345
1346    #[test]
1347    fn all_official_bip32_vectors_and_invalid_keys() {
1348        let vectors: serde_json::Value =
1349            serde_json::from_str(include_str!("../../test-vectors/bip32-official.json")).unwrap();
1350        for vector in vectors["vectors"].as_array().unwrap() {
1351            let seed = hex::decode(vector["seedHex"].as_str().unwrap()).unwrap();
1352            let root = ExtendedPrivateKey::master(&seed).unwrap();
1353            for expected in vector["nodes"].as_array().unwrap() {
1354                let path = expected["path"].as_str().unwrap();
1355                let node = root.derive_path(path).unwrap();
1356                assert_eq!(
1357                    node.serialize_public(XPUB.public_version),
1358                    expected["extendedPublicKey"].as_str().unwrap(),
1359                    "{path}"
1360                );
1361                assert_eq!(
1362                    node.serialize_private(XPUB.private_version),
1363                    expected["extendedPrivateKey"].as_str().unwrap(),
1364                    "{path}"
1365                );
1366            }
1367        }
1368        for invalid in vectors["invalidExtendedKeys"].as_array().unwrap() {
1369            assert!(
1370                parse_extended_key(invalid["value"].as_str().unwrap()).is_err(),
1371                "accepted {}",
1372                invalid["reason"].as_str().unwrap()
1373            );
1374        }
1375    }
1376
1377    #[test]
1378    fn official_slip10_ed25519_vector() {
1379        let vectors: serde_json::Value = serde_json::from_str(include_str!(
1380            "../../test-vectors/slip10-ed25519-official.json"
1381        ))
1382        .unwrap();
1383        let seed = hex::decode(vectors["seedHex"].as_str().unwrap()).unwrap();
1384        for expected in vectors["nodes"].as_array().unwrap() {
1385            let path = expected["path"].as_str().unwrap();
1386            let node = slip10_ed25519(&seed, path).unwrap();
1387            assert_eq!(hex::encode(node.chain_code), expected["chainCodeHex"]);
1388            assert_eq!(hex::encode(node.private_key), expected["privateKeyHex"]);
1389            assert_eq!(hex::encode(node.public_key), expected["publicKeyHex"]);
1390        }
1391    }
1392
1393    #[test]
1394    fn public_api_branches_and_failure_boundaries() {
1395        assert_eq!(supported_chains().len(), 18);
1396        assert!(chain("unknown").is_err());
1397        assert!(Source::Seed(vec![0; 15]).seed().is_err());
1398        assert!(Source::mnemonic("abandon abandon", "").seed().is_err());
1399        assert!(ExtendedPrivateKey::master(&[0; 15]).is_err());
1400        for name in [
1401            "xpub", "ypub", "zpub", "tpub", "upub", "vpub", "Ltub", "Mtub",
1402        ] {
1403            assert_eq!(format(name).unwrap().name, name);
1404        }
1405        assert!(format("unknown").is_err());
1406
1407        let secp = derive_node(&source(), "secp256k1", "m/0h/1H/2'").unwrap();
1408        assert_eq!(secp.depth, 3);
1409        let ed = derive_node(&source(), "ed25519", "m/0'").unwrap();
1410        assert_eq!(ed.depth, 1);
1411        assert!(derive_node(&source(), "p256", "m").is_err());
1412        assert!(derive_node(&source(), "ed25519", "m/0").is_err());
1413
1414        let account = derive_account_public_key(&source(), DeriveOptions::default()).unwrap();
1415        let parsed_public = parse_extended_key(&account.extended_public_key).unwrap();
1416        assert_eq!(
1417            serialize_extended_key(&parsed_public, None),
1418            account.extended_public_key
1419        );
1420        assert!(derive_account_public_key(
1421            &source(),
1422            DeriveOptions {
1423                chain: "solana",
1424                ..Default::default()
1425            }
1426        )
1427        .is_err());
1428        assert!(derive_account_public_key(
1429            &source(),
1430            DeriveOptions {
1431                chain: "bitcoin",
1432                format: Some("tpub"),
1433                ..Default::default()
1434            }
1435        )
1436        .is_err());
1437
1438        let secret = derive_account_private_key(
1439            &source(),
1440            DeriveOptions {
1441                chain: "bitcoin",
1442                format: Some("xpub"),
1443                path: Some("m"),
1444                ..Default::default()
1445            },
1446        )
1447        .unwrap();
1448        let parsed_private =
1449            parse_extended_key(secret.extended_private_key.as_deref().unwrap()).unwrap();
1450        assert_eq!(
1451            serialize_extended_key(&parsed_private, Some(XPUB.private_version)),
1452            secret.extended_private_key.unwrap()
1453        );
1454        let solana_secret = derive_account_private_key(
1455            &source(),
1456            DeriveOptions {
1457                chain: "solana",
1458                ..Default::default()
1459            },
1460        )
1461        .unwrap();
1462        assert!(solana_secret.extended_private_key.is_none());
1463
1464        let vectors: serde_json::Value =
1465            serde_json::from_str(include_str!("../../test-vectors/public-vectors.json")).unwrap();
1466        for expected in vectors["addresses"].as_array().unwrap() {
1467            let chain = expected["chain"].as_str().unwrap();
1468            let path = expected["path"].as_str().unwrap();
1469            let script_type = expected["scriptType"].as_str().unwrap();
1470            assert_eq!(
1471                derive_address(
1472                    &source(),
1473                    DeriveOptions {
1474                        chain,
1475                        path: Some(path),
1476                        script_type: Some(script_type),
1477                        ..Default::default()
1478                    }
1479                )
1480                .unwrap()
1481                .address,
1482                expected["address"]
1483            );
1484        }
1485        assert_eq!(
1486            derive_addresses(&source(), DeriveOptions::default(), 3, 4)
1487                .unwrap()
1488                .len(),
1489            4
1490        );
1491        assert!(derive_addresses(&source(), DeriveOptions::default(), 0, 0).is_err());
1492        assert!(derive_addresses(&source(), DeriveOptions::default(), 0, 10_001).is_err());
1493        assert!(derive_addresses(&source(), DeriveOptions::default(), u32::MAX, 2).is_err());
1494
1495        let watched = derive_address_from_extended_public_key(
1496            &account.extended_public_key,
1497            Some("bitcoin"),
1498            0,
1499            0,
1500            None,
1501        )
1502        .unwrap();
1503        assert_eq!(
1504            watched.address,
1505            "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
1506        );
1507        assert!(derive_address_from_extended_public_key(
1508            &account.extended_public_key,
1509            None,
1510            0,
1511            0,
1512            None
1513        )
1514        .is_err());
1515        let serialized_private = match &parsed_private {
1516            ParsedExtendedKey::Private(node, _) => node.serialize_private(XPUB.private_version),
1517            _ => unreachable!(),
1518        };
1519        assert!(derive_address_from_extended_public_key(
1520            &serialized_private,
1521            Some("bitcoin"),
1522            0,
1523            0,
1524            None
1525        )
1526        .is_err());
1527        assert!(derive_address_from_extended_public_key(
1528            &account.extended_public_key,
1529            Some("bitcoin"),
1530            HARDENED_OFFSET,
1531            0,
1532            None
1533        )
1534        .is_err());
1535
1536        for path in ["relative/0", "m/", "m/00", "m/abc", "m/2147483648"] {
1537            assert!(parse_path(path).is_err(), "accepted {path}");
1538        }
1539        assert_eq!(parse_path("M/0h/1H/2'").unwrap().len(), 3);
1540        assert!(parse_extended_key("not-a-key").is_err());
1541        assert!(public_key_to_address(&[4; 33], chain("ethereum").unwrap(), "evm").is_err());
1542        assert!(public_key_to_address(&[4; 33], chain("tron").unwrap(), "tron").is_err());
1543        assert!(public_key_to_address(
1544            &ExtendedPrivateKey::master(&[0; 16]).unwrap().public_key(),
1545            chain("dogecoin").unwrap(),
1546            "p2wpkh"
1547        )
1548        .is_err());
1549        assert!(public_key_to_address(
1550            &ExtendedPrivateKey::master(&[0; 16]).unwrap().public_key(),
1551            chain("bitcoin").unwrap(),
1552            "unknown"
1553        )
1554        .is_err());
1555    }
1556
1557    proptest! {
1558        #[test]
1559        fn arbitrary_derivation_paths_never_panic(value in ".{0,512}") {
1560            let _ = parse_path(&value);
1561        }
1562
1563        #[test]
1564        fn random_non_hardened_private_and_public_children_agree(index in 0u32..HARDENED_OFFSET) {
1565            let root = ExtendedPrivateKey::master(&[7; 32]).unwrap().derive_path("m/84'/0'/0'").unwrap();
1566            prop_assert_eq!(root.derive(index).unwrap().public_key(), root.neuter().derive(index).unwrap().public_key);
1567        }
1568    }
1569}