Skip to main content

chains_sdk/
hd_key.rs

1//! **BIP-32** Hierarchical Deterministic (HD) key derivation for secp256k1.
2//!
3//! Implements master key generation from seed, hardened & normal child derivation,
4//! and BIP-44 path parsing (`m/44'/60'/0'/0/0`).
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::hd_key::{ExtendedPrivateKey, DerivationPath};
9//!
10//! fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!     let seed = [0xab_u8; 64];
12//!     let master = ExtendedPrivateKey::from_seed(&seed)?;
13//!     let path = DerivationPath::parse("m/44'/60'/0'/0/0")?;
14//!     let child = master.derive_path(&path)?;
15//!     Ok(())
16//! }
17//! ```
18
19use crate::crypto;
20use crate::error::SignerError;
21use hmac::{Hmac, Mac};
22use k256::elliptic_curve::sec1::ToEncodedPoint;
23use sha2::Sha512;
24use zeroize::Zeroizing;
25
26type HmacSha512 = Hmac<Sha512>;
27
28/// The BIP-32 master key derivation salt.
29const BIP32_SEED_KEY: &[u8] = b"Bitcoin seed";
30
31/// A BIP-32 extended private key (key + chain code).
32pub struct ExtendedPrivateKey {
33    /// The 32-byte private key scalar.
34    key: Zeroizing<[u8; 32]>,
35    /// The 32-byte chain code used for child derivation.
36    /// Chain code is security-sensitive: knowing it + public key enables
37    /// deriving all non-hardened child keys.
38    chain_code: Zeroizing<[u8; 32]>,
39    /// Derivation depth (0 = master).
40    depth: u8,
41    /// Parent key fingerprint (first 4 bytes of HASH160(parent_pubkey)).
42    parent_fingerprint: [u8; 4],
43    /// Child index used in derivation (includes hardened bit if applicable).
44    child_index: u32,
45}
46
47impl Drop for ExtendedPrivateKey {
48    fn drop(&mut self) {
49        // key and chain_code are both Zeroizing — automatically scrubbed on drop.
50    }
51}
52
53impl ExtendedPrivateKey {
54    /// Derive the master key from a BIP-39 seed (typically 16–64 bytes).
55    ///
56    /// Computes `HMAC-SHA512("Bitcoin seed", seed)`.
57    /// Left 32 bytes = private key, right 32 bytes = chain code.
58    pub fn from_seed(seed: &[u8]) -> Result<Self, SignerError> {
59        if seed.len() < 16 || seed.len() > 64 {
60            return Err(SignerError::InvalidPrivateKey(format!(
61                "BIP-32 seed must be 16–64 bytes, got {}",
62                seed.len()
63            )));
64        }
65
66        let mut mac = HmacSha512::new_from_slice(BIP32_SEED_KEY)
67            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
68        mac.update(seed);
69        let mut result = mac.finalize().into_bytes();
70
71        let mut key = Zeroizing::new([0u8; 32]);
72        key.copy_from_slice(&result[..32]);
73        let mut chain_code = Zeroizing::new([0u8; 32]);
74        chain_code.copy_from_slice(&result[32..]);
75
76        // Zeroize the full HMAC output immediately
77        use zeroize::Zeroize;
78        for b in result.iter_mut() {
79            b.zeroize();
80        }
81
82        // Validate the key is a valid secp256k1 scalar
83        k256::SecretKey::from_bytes((&*key).into())
84            .map_err(|_| SignerError::InvalidPrivateKey("master key is zero or >= n".into()))?;
85
86        Ok(Self {
87            key,
88            chain_code,
89            depth: 0,
90            parent_fingerprint: [0u8; 4],
91            child_index: 0,
92        })
93    }
94
95    /// Derive a child key at the given index.
96    ///
97    /// If `hardened` is true, uses hardened derivation (index + 0x80000000).
98    pub fn derive_child(&self, index: u32, hardened: bool) -> Result<Self, SignerError> {
99        use zeroize::Zeroize;
100
101        let mut mac = HmacSha512::new_from_slice(&*self.chain_code)
102            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
103
104        let effective_index = if hardened {
105            index
106                .checked_add(0x8000_0000)
107                .ok_or_else(|| SignerError::InvalidPrivateKey("index overflow".into()))?
108        } else {
109            index
110        };
111
112        if hardened {
113            // Hardened: HMAC-SHA512(chain_code, 0x00 || key || index)
114            mac.update(&[0x00]);
115            mac.update(&*self.key);
116        } else {
117            // Normal: HMAC-SHA512(chain_code, public_key || index)
118            let sk = k256::SecretKey::from_bytes((&*self.key).into())
119                .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
120            let pk = sk.public_key().to_encoded_point(true);
121            mac.update(pk.as_bytes());
122        }
123
124        mac.update(&effective_index.to_be_bytes());
125        let mut result = mac.finalize().into_bytes();
126
127        let mut il = [0u8; 32];
128        il.copy_from_slice(&result[..32]);
129        let mut child_chain = Zeroizing::new([0u8; 32]);
130        child_chain.copy_from_slice(&result[32..]);
131
132        // Zeroize the full HMAC output immediately — il and child_chain are copies
133        for b in result.iter_mut() {
134            b.zeroize();
135        }
136
137        // child_key = (il + parent_key) mod n
138        let derive_result = (|| -> Result<Zeroizing<[u8; 32]>, SignerError> {
139            let parent_scalar = k256::NonZeroScalar::try_from(&*self.key as &[u8])
140                .map_err(|_| SignerError::InvalidPrivateKey("parent key invalid".into()))?;
141            let il_scalar = k256::NonZeroScalar::try_from(&il as &[u8])
142                .map_err(|_| SignerError::InvalidPrivateKey("derived key is zero".into()))?;
143
144            // Add scalars: parent + il mod n
145            let child_scalar = parent_scalar.as_ref() + il_scalar.as_ref();
146
147            // Validate the child scalar is non-zero (CtOption -> Option)
148            let child_nz: Option<k256::NonZeroScalar> =
149                k256::NonZeroScalar::new(child_scalar).into();
150            let child_secret = k256::SecretKey::from(
151                child_nz
152                    .ok_or_else(|| SignerError::InvalidPrivateKey("child key is zero".into()))?,
153            );
154
155            let mut child_key = Zeroizing::new([0u8; 32]);
156            child_key.copy_from_slice(&child_secret.to_bytes());
157            Ok(child_key)
158        })();
159
160        // Always zeroize il regardless of success/failure
161        il.zeroize();
162
163        let child_key = derive_result?;
164
165        // Compute parent fingerprint: HASH160(parent_pubkey)[..4]
166        let parent_fp = {
167            let sk = k256::SecretKey::from_bytes((&*self.key).into())
168                .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
169            let pk_bytes = sk.public_key().to_encoded_point(true);
170            let h160 = crypto::hash160(pk_bytes.as_bytes());
171            let mut fp = [0u8; 4];
172            fp.copy_from_slice(&h160[..4]);
173            fp
174        };
175
176        Ok(Self {
177            key: child_key,
178            chain_code: child_chain,
179            depth: self.depth.saturating_add(1),
180            parent_fingerprint: parent_fp,
181            child_index: effective_index,
182        })
183    }
184
185    /// Derive a child key following a full derivation path.
186    pub fn derive_path(&self, path: &DerivationPath) -> Result<Self, SignerError> {
187        let mut current = Self {
188            key: self.key.clone(),
189            chain_code: self.chain_code.clone(),
190            depth: self.depth,
191            parent_fingerprint: self.parent_fingerprint,
192            child_index: self.child_index,
193        };
194        for step in &path.steps {
195            current = current.derive_child(step.index, step.hardened)?;
196        }
197        Ok(current)
198    }
199
200    /// Get the raw 32-byte private key.
201    #[must_use]
202    pub fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
203        Zeroizing::new(self.key.to_vec())
204    }
205
206    /// Get the compressed public key (33 bytes).
207    pub fn public_key_bytes(&self) -> Result<Vec<u8>, SignerError> {
208        let sk = k256::SecretKey::from_bytes((&*self.key).into())
209            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
210        Ok(sk.public_key().to_encoded_point(true).as_bytes().to_vec())
211    }
212
213    /// Get the current derivation depth (0 = master).
214    #[must_use]
215    pub fn depth(&self) -> u8 {
216        self.depth
217    }
218
219    /// Get the chain code (useful for extended public key export).
220    #[must_use]
221    pub fn chain_code(&self) -> &[u8; 32] {
222        &self.chain_code
223    }
224
225    /// Get the parent key fingerprint (4 bytes).
226    pub fn parent_fingerprint(&self) -> &[u8; 4] {
227        &self.parent_fingerprint
228    }
229
230    /// Get the child index used in this key's derivation.
231    pub fn child_index(&self) -> u32 {
232        self.child_index
233    }
234
235    /// Serialize as an **xprv** Base58Check string (BIP-32).
236    ///
237    /// Format: `4 bytes version || 1 byte depth || 4 bytes fingerprint || 4 bytes child index || 32 bytes chain code || 1 byte 0x00 || 32 bytes key`
238    ///
239    /// # Security
240    /// The returned String contains the private key — handle with care.
241    #[must_use]
242    pub fn to_xprv(&self) -> Zeroizing<String> {
243        let mut data = Zeroizing::new(Vec::with_capacity(82));
244        data.extend_from_slice(&[0x04, 0x88, 0xAD, 0xE4]); // xprv version
245        data.push(self.depth);
246        data.extend_from_slice(&self.parent_fingerprint);
247        data.extend_from_slice(&self.child_index.to_be_bytes());
248        data.extend_from_slice(&*self.chain_code);
249        data.push(0x00); // private key prefix
250        data.extend_from_slice(&*self.key);
251        // Base58Check: double-SHA256 checksum
252        let checksum = crypto::double_sha256(&data);
253        data.extend_from_slice(&checksum[..4]);
254        Zeroizing::new(bs58::encode(&*data).into_string())
255    }
256
257    /// Serialize the public key as an **xpub** Base58Check string (BIP-32).
258    pub fn to_xpub(&self) -> Result<String, SignerError> {
259        let pubkey = self.public_key_bytes()?;
260        let mut data = Vec::with_capacity(82);
261        data.extend_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); // xpub version
262        data.push(self.depth);
263        data.extend_from_slice(&self.parent_fingerprint);
264        data.extend_from_slice(&self.child_index.to_be_bytes());
265        data.extend_from_slice(&*self.chain_code);
266        data.extend_from_slice(&pubkey);
267        let checksum = crypto::double_sha256(&data);
268        data.extend_from_slice(&checksum[..4]);
269        Ok(bs58::encode(data).into_string())
270    }
271
272    /// Deserialize an **xprv** Base58Check string back into an extended private key.
273    pub fn from_xprv(xprv: &str) -> Result<Self, SignerError> {
274        let data = Zeroizing::new(
275            bs58::decode(xprv)
276                .into_vec()
277                .map_err(|e| SignerError::InvalidPrivateKey(format!("invalid base58: {e}")))?,
278        );
279        if data.len() != 82 {
280            return Err(SignerError::InvalidPrivateKey(format!(
281                "xprv must be 82 bytes, got {}",
282                data.len()
283            )));
284        }
285        // Verify checksum (constant-time)
286        let checksum = crypto::double_sha256(&data[..78]);
287        use subtle::ConstantTimeEq;
288        if data[78..82].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
289            return Err(SignerError::InvalidPrivateKey(
290                "invalid xprv checksum".into(),
291            ));
292        }
293        // Verify version
294        if data[..4] != [0x04, 0x88, 0xAD, 0xE4] {
295            return Err(SignerError::InvalidPrivateKey(
296                "not an xprv (wrong version)".into(),
297            ));
298        }
299        let depth = data[4];
300        let mut parent_fingerprint = [0u8; 4];
301        parent_fingerprint.copy_from_slice(&data[5..9]);
302        let child_index = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
303        let mut chain_code = Zeroizing::new([0u8; 32]);
304        chain_code.copy_from_slice(&data[13..45]);
305        // data[45] should be 0x00 (private key prefix)
306        if data[45] != 0x00 {
307            return Err(SignerError::InvalidPrivateKey(
308                "invalid private key prefix".into(),
309            ));
310        }
311        let mut key = Zeroizing::new([0u8; 32]);
312        key.copy_from_slice(&data[46..78]);
313        // Validate the key
314        k256::SecretKey::from_bytes((&*key).into())
315            .map_err(|_| SignerError::InvalidPrivateKey("invalid xprv key".into()))?;
316        Ok(Self {
317            key,
318            chain_code,
319            depth,
320            parent_fingerprint,
321            child_index,
322        })
323    }
324
325    /// Convert to an `ExtendedPublicKey` for watch-only derivation.
326    pub fn to_extended_public_key(&self) -> Result<ExtendedPublicKey, SignerError> {
327        let pubkey_bytes = self.public_key_bytes()?;
328        let mut key = [0u8; 33];
329        key.copy_from_slice(&pubkey_bytes);
330        Ok(ExtendedPublicKey {
331            key,
332            chain_code: *self.chain_code,
333            depth: self.depth,
334            parent_fingerprint: self.parent_fingerprint,
335            child_index: self.child_index,
336        })
337    }
338}
339
340// ─── Extended Public Key (BIP-32 Watch-Only) ────────────────────────
341
342/// A BIP-32 extended public key for watch-only wallets.
343///
344/// Supports **normal** (non-hardened) child derivation only.
345/// Hardened derivation requires the private key.
346#[derive(Clone, Debug)]
347pub struct ExtendedPublicKey {
348    /// Compressed SEC1 public key (33 bytes).
349    key: [u8; 33],
350    /// Chain code (32 bytes).
351    chain_code: [u8; 32],
352    /// Derivation depth.
353    depth: u8,
354    /// Parent key fingerprint.
355    parent_fingerprint: [u8; 4],
356    /// Child index.
357    child_index: u32,
358}
359
360impl ExtendedPublicKey {
361    /// Get the compressed public key (33 bytes).
362    #[must_use]
363    pub fn public_key_bytes(&self) -> &[u8; 33] {
364        &self.key
365    }
366
367    /// Get the derivation depth.
368    #[must_use]
369    pub fn depth(&self) -> u8 {
370        self.depth
371    }
372
373    /// Get the chain code.
374    #[must_use]
375    pub fn chain_code(&self) -> &[u8; 32] {
376        &self.chain_code
377    }
378
379    /// Get the parent fingerprint.
380    #[must_use]
381    pub fn parent_fingerprint(&self) -> &[u8; 4] {
382        &self.parent_fingerprint
383    }
384
385    /// Get the child index.
386    #[must_use]
387    pub fn child_index(&self) -> u32 {
388        self.child_index
389    }
390
391    /// Derive a **normal** (non-hardened) child public key.
392    ///
393    /// Only normal derivation (index < 2^31) is supported.
394    /// Hardened derivation requires the private key.
395    pub fn derive_child_normal(&self, index: u32) -> Result<Self, SignerError> {
396        if index >= 0x8000_0000 {
397            return Err(SignerError::InvalidPrivateKey(
398                "hardened derivation requires private key".into(),
399            ));
400        }
401
402        let mut mac = HmacSha512::new_from_slice(&self.chain_code)
403            .map_err(|_| SignerError::InvalidPrivateKey("HMAC init failed".into()))?;
404
405        // For normal child: HMAC-SHA512(chain_code, pubkey || index)
406        mac.update(&self.key);
407        mac.update(&index.to_be_bytes());
408
409        let result = mac.finalize().into_bytes();
410
411        let mut il = [0u8; 32];
412        il.copy_from_slice(&result[..32]);
413        let mut child_chain = [0u8; 32];
414        child_chain.copy_from_slice(&result[32..]);
415
416        // Parse IL as scalar and add to parent point
417        use k256::elliptic_curve::group::GroupEncoding;
418        use k256::elliptic_curve::ops::Reduce;
419        use k256::{ProjectivePoint, Scalar, U256};
420
421        let il_scalar = <Scalar as Reduce<U256>>::reduce(U256::from_be_slice(&il));
422        let parent_point = k256::AffinePoint::from_bytes((&self.key).into());
423        let parent_proj: ProjectivePoint = Option::from(parent_point.map(ProjectivePoint::from))
424            .ok_or_else(|| SignerError::InvalidPublicKey("invalid parent public key".into()))?;
425
426        let child_point = parent_proj + ProjectivePoint::GENERATOR * il_scalar;
427
428        // Serialize child public key
429        use k256::elliptic_curve::sec1::ToEncodedPoint;
430        let child_affine = child_point.to_affine();
431        let encoded = child_affine.to_encoded_point(true);
432        let child_key_bytes = encoded.as_bytes();
433        if child_key_bytes.len() != 33 {
434            return Err(SignerError::InvalidPublicKey(
435                "child key serialization failed".into(),
436            ));
437        }
438        let mut child_key = [0u8; 33];
439        child_key.copy_from_slice(child_key_bytes);
440
441        // Parent fingerprint = first 4 bytes of HASH160(parent_pubkey)
442        let fingerprint = crypto::hash160(&self.key);
443        let mut parent_fp = [0u8; 4];
444        parent_fp.copy_from_slice(&fingerprint[..4]);
445
446        // Zeroize IL
447        use zeroize::Zeroize;
448        il.zeroize();
449
450        Ok(Self {
451            key: child_key,
452            chain_code: child_chain,
453            depth: self.depth.checked_add(1).ok_or_else(|| {
454                SignerError::InvalidPrivateKey("derivation depth overflow".into())
455            })?,
456            parent_fingerprint: parent_fp,
457            child_index: index,
458        })
459    }
460
461    /// Serialize as an **xpub** Base58Check string.
462    #[must_use]
463    pub fn to_xpub(&self) -> String {
464        let mut data = Vec::with_capacity(82);
465        data.extend_from_slice(&[0x04, 0x88, 0xB2, 0x1E]); // xpub version
466        data.push(self.depth);
467        data.extend_from_slice(&self.parent_fingerprint);
468        data.extend_from_slice(&self.child_index.to_be_bytes());
469        data.extend_from_slice(&self.chain_code);
470        data.extend_from_slice(&self.key);
471        let checksum = crypto::double_sha256(&data);
472        data.extend_from_slice(&checksum[..4]);
473        bs58::encode(data).into_string()
474    }
475
476    /// Deserialize an **xpub** Base58Check string.
477    pub fn from_xpub(xpub: &str) -> Result<Self, SignerError> {
478        let data = bs58::decode(xpub)
479            .into_vec()
480            .map_err(|e| SignerError::InvalidPublicKey(format!("invalid base58: {e}")))?;
481        if data.len() != 82 {
482            return Err(SignerError::InvalidPublicKey(format!(
483                "xpub must be 82 bytes, got {}",
484                data.len()
485            )));
486        }
487        let checksum = crypto::double_sha256(&data[..78]);
488        use subtle::ConstantTimeEq;
489        if data[78..82].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
490            return Err(SignerError::InvalidPublicKey(
491                "invalid xpub checksum".into(),
492            ));
493        }
494        if data[..4] != [0x04, 0x88, 0xB2, 0x1E] {
495            return Err(SignerError::InvalidPublicKey(
496                "not an xpub (wrong version)".into(),
497            ));
498        }
499        let depth = data[4];
500        let mut parent_fingerprint = [0u8; 4];
501        parent_fingerprint.copy_from_slice(&data[5..9]);
502        let child_index = u32::from_be_bytes([data[9], data[10], data[11], data[12]]);
503        let mut chain_code = [0u8; 32];
504        chain_code.copy_from_slice(&data[13..45]);
505        let mut key = [0u8; 33];
506        key.copy_from_slice(&data[45..78]);
507        // Validate the public key is on the curve
508        let _pt = k256::AffinePoint::from_bytes((&key).into());
509        use k256::elliptic_curve::group::GroupEncoding;
510        if bool::from(k256::AffinePoint::from_bytes((&key).into()).is_none()) {
511            return Err(SignerError::InvalidPublicKey(
512                "invalid xpub key point".into(),
513            ));
514        }
515        Ok(Self {
516            key,
517            chain_code,
518            depth,
519            parent_fingerprint,
520            child_index,
521        })
522    }
523
524    /// Derive a **P2WPKH** (SegWit) address from this public key.
525    ///
526    /// Uses Bech32 encoding: `bc1q...` for mainnet.
527    ///
528    /// # Arguments
529    /// - `hrp` — Human-readable part: `"bc"` for mainnet, `"tb"` for testnet
530    #[cfg(feature = "bitcoin")]
531    pub fn p2wpkh_address(&self, hrp: &str) -> Result<String, SignerError> {
532        let pubkey_hash = crypto::hash160(&self.key);
533        crate::bitcoin::bech32_encode(hrp, 0, &pubkey_hash)
534    }
535
536    /// Derive a **P2TR** (Taproot) address from this public key.
537    ///
538    /// Extracts the x-only public key (drops the prefix byte) and encodes
539    /// as a Bech32m `bc1p...` address.
540    ///
541    /// # Arguments
542    /// - `hrp` — Human-readable part: `"bc"` for mainnet, `"tb"` for testnet
543    #[cfg(feature = "bitcoin")]
544    pub fn p2tr_address(&self, hrp: &str) -> Result<String, SignerError> {
545        // x-only = drop the 0x02/0x03 prefix from compressed key
546        if self.key.len() != 33 {
547            return Err(SignerError::InvalidPublicKey(
548                "expected 33-byte compressed key".into(),
549            ));
550        }
551        let x_only = &self.key[1..33];
552        crate::bitcoin::bech32_encode(hrp, 1, x_only)
553    }
554}
555
556/// A single step in a BIP-32 derivation path.
557#[derive(Debug, Clone)]
558pub struct DerivationStep {
559    /// Child index (0-based).
560    pub index: u32,
561    /// Whether this is a hardened derivation step.
562    pub hardened: bool,
563}
564
565/// A BIP-32/BIP-44 derivation path (e.g., `m/44'/60'/0'/0/0`).
566#[derive(Debug, Clone)]
567pub struct DerivationPath {
568    /// The steps in this path.
569    pub steps: Vec<DerivationStep>,
570}
571
572impl DerivationPath {
573    /// Parse a BIP-32 path string like `"m/44'/60'/0'/0/0"`.
574    ///
575    /// Supported formats: `44'` or `44h` for hardened.
576    pub fn parse(path: &str) -> Result<Self, SignerError> {
577        let path = path.trim();
578        let segments: Vec<&str> = path.split('/').collect();
579
580        if segments.is_empty() {
581            return Err(SignerError::InvalidPrivateKey(
582                "empty derivation path".into(),
583            ));
584        }
585
586        // First segment must be "m" or "M"
587        if segments[0] != "m" && segments[0] != "M" {
588            return Err(SignerError::InvalidPrivateKey(
589                "derivation path must start with 'm/'".into(),
590            ));
591        }
592
593        let mut steps = Vec::new();
594        for seg in &segments[1..] {
595            if seg.is_empty() {
596                continue;
597            }
598            let (hardened, num_str) =
599                if seg.ends_with('\'') || seg.ends_with('h') || seg.ends_with('H') {
600                    (true, &seg[..seg.len() - 1])
601                } else {
602                    (false, *seg)
603                };
604
605            let index: u32 = num_str.parse().map_err(|_| {
606                SignerError::InvalidPrivateKey(format!("invalid path segment: {seg}"))
607            })?;
608
609            if index >= 0x8000_0000 {
610                return Err(SignerError::InvalidPrivateKey(format!(
611                    "index {index} too large (must be < 2^31)"
612                )));
613            }
614
615            steps.push(DerivationStep { index, hardened });
616        }
617
618        Ok(Self { steps })
619    }
620
621    /// BIP-44 Ethereum path: `m/44'/60'/0'/0/{index}`
622    pub fn ethereum(index: u32) -> Self {
623        Self {
624            steps: vec![
625                DerivationStep {
626                    index: 44,
627                    hardened: true,
628                },
629                DerivationStep {
630                    index: 60,
631                    hardened: true,
632                },
633                DerivationStep {
634                    index: 0,
635                    hardened: true,
636                },
637                DerivationStep {
638                    index: 0,
639                    hardened: false,
640                },
641                DerivationStep {
642                    index,
643                    hardened: false,
644                },
645            ],
646        }
647    }
648
649    /// BIP-44 Bitcoin path: `m/44'/0'/0'/0/{index}`
650    pub fn bitcoin(index: u32) -> Self {
651        Self {
652            steps: vec![
653                DerivationStep {
654                    index: 44,
655                    hardened: true,
656                },
657                DerivationStep {
658                    index: 0,
659                    hardened: true,
660                },
661                DerivationStep {
662                    index: 0,
663                    hardened: true,
664                },
665                DerivationStep {
666                    index: 0,
667                    hardened: false,
668                },
669                DerivationStep {
670                    index,
671                    hardened: false,
672                },
673            ],
674        }
675    }
676
677    /// BIP-84 Bitcoin Segwit path: `m/84'/0'/0'/0/{index}`
678    pub fn bitcoin_segwit(index: u32) -> Self {
679        Self {
680            steps: vec![
681                DerivationStep {
682                    index: 84,
683                    hardened: true,
684                },
685                DerivationStep {
686                    index: 0,
687                    hardened: true,
688                },
689                DerivationStep {
690                    index: 0,
691                    hardened: true,
692                },
693                DerivationStep {
694                    index: 0,
695                    hardened: false,
696                },
697                DerivationStep {
698                    index,
699                    hardened: false,
700                },
701            ],
702        }
703    }
704
705    /// BIP-86 Bitcoin Taproot path: `m/86'/0'/0'/0/{index}`
706    pub fn bitcoin_taproot(index: u32) -> Self {
707        Self {
708            steps: vec![
709                DerivationStep {
710                    index: 86,
711                    hardened: true,
712                },
713                DerivationStep {
714                    index: 0,
715                    hardened: true,
716                },
717                DerivationStep {
718                    index: 0,
719                    hardened: true,
720                },
721                DerivationStep {
722                    index: 0,
723                    hardened: false,
724                },
725                DerivationStep {
726                    index,
727                    hardened: false,
728                },
729            ],
730        }
731    }
732
733    /// BIP-44 Solana path: `m/44'/501'/{index}'/0'`
734    pub fn solana(index: u32) -> Self {
735        Self {
736            steps: vec![
737                DerivationStep {
738                    index: 44,
739                    hardened: true,
740                },
741                DerivationStep {
742                    index: 501,
743                    hardened: true,
744                },
745                DerivationStep {
746                    index,
747                    hardened: true,
748                },
749                DerivationStep {
750                    index: 0,
751                    hardened: true,
752                },
753            ],
754        }
755    }
756
757    /// BIP-44 XRP path: `m/44'/144'/0'/0/{index}`
758    pub fn xrp(index: u32) -> Self {
759        Self {
760            steps: vec![
761                DerivationStep {
762                    index: 44,
763                    hardened: true,
764                },
765                DerivationStep {
766                    index: 144,
767                    hardened: true,
768                },
769                DerivationStep {
770                    index: 0,
771                    hardened: true,
772                },
773                DerivationStep {
774                    index: 0,
775                    hardened: false,
776                },
777                DerivationStep {
778                    index,
779                    hardened: false,
780                },
781            ],
782        }
783    }
784
785    /// BIP-44 NEO path: `m/44'/888'/0'/0/{index}`
786    pub fn neo(index: u32) -> Self {
787        Self {
788            steps: vec![
789                DerivationStep {
790                    index: 44,
791                    hardened: true,
792                },
793                DerivationStep {
794                    index: 888,
795                    hardened: true,
796                },
797                DerivationStep {
798                    index: 0,
799                    hardened: true,
800                },
801                DerivationStep {
802                    index: 0,
803                    hardened: false,
804                },
805                DerivationStep {
806                    index,
807                    hardened: false,
808                },
809            ],
810        }
811    }
812}
813
814#[cfg(test)]
815#[allow(clippy::unwrap_used, clippy::expect_used)]
816mod tests {
817    use super::*;
818
819    // BIP-32 Test Vector 1 (from BIP-32 spec)
820    // Seed: 000102030405060708090a0b0c0d0e0f
821    #[test]
822    fn test_bip32_vector1_master() {
823        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
824        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
825        let pk = master.public_key_bytes().unwrap();
826
827        assert_eq!(
828            hex::encode(&*master.private_key_bytes()),
829            "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"
830        );
831        assert_eq!(
832            hex::encode(&pk),
833            "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"
834        );
835        assert_eq!(
836            hex::encode(master.chain_code()),
837            "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"
838        );
839    }
840
841    #[test]
842    fn test_bip32_vector1_child_0h() {
843        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
844        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
845        let child = master.derive_child(0, true).unwrap();
846
847        assert_eq!(
848            hex::encode(&*child.private_key_bytes()),
849            "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea"
850        );
851        assert_eq!(child.depth(), 1);
852    }
853
854    #[test]
855    fn test_bip32_vector1_path_m44h_60h_0h_0_0() {
856        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
857        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
858        let path = DerivationPath::parse("m/44'/60'/0'/0/0").unwrap();
859        let child = master.derive_path(&path).unwrap();
860        assert_eq!(child.depth(), 5);
861        assert_eq!(child.private_key_bytes().len(), 32);
862        // Ensure deterministic — derive twice
863        let child2 = master.derive_path(&path).unwrap();
864        assert_eq!(&*child.private_key_bytes(), &*child2.private_key_bytes());
865    }
866
867    #[test]
868    fn test_bip32_vector2_seed() {
869        // BIP-32 Test Vector 2
870        let seed = hex::decode("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542").unwrap();
871        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
872        assert_eq!(
873            hex::encode(&*master.private_key_bytes()),
874            "4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e"
875        );
876        assert_eq!(
877            hex::encode(master.chain_code()),
878            "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689"
879        );
880    }
881
882    #[test]
883    fn test_derivation_path_parse() {
884        let path = DerivationPath::parse("m/44'/60'/0'/0/0").unwrap();
885        assert_eq!(path.steps.len(), 5);
886        assert!(path.steps[0].hardened);
887        assert_eq!(path.steps[0].index, 44);
888        assert!(path.steps[1].hardened);
889        assert_eq!(path.steps[1].index, 60);
890        assert!(!path.steps[3].hardened);
891        assert_eq!(path.steps[4].index, 0);
892    }
893
894    #[test]
895    fn test_derivation_path_shortcuts() {
896        let eth = DerivationPath::ethereum(0);
897        assert_eq!(eth.steps.len(), 5);
898        assert_eq!(eth.steps[1].index, 60);
899
900        let btc = DerivationPath::bitcoin(0);
901        assert_eq!(btc.steps[1].index, 0);
902
903        let sol = DerivationPath::solana(0);
904        assert_eq!(sol.steps[1].index, 501);
905        assert_eq!(sol.steps.len(), 4); // Solana uses all-hardened
906    }
907
908    #[test]
909    fn test_invalid_path_rejected() {
910        assert!(DerivationPath::parse("").is_err());
911        assert!(DerivationPath::parse("x/44'/60'").is_err());
912    }
913
914    #[test]
915    fn test_seed_length_validation() {
916        assert!(ExtendedPrivateKey::from_seed(&[0u8; 15]).is_err());
917        assert!(ExtendedPrivateKey::from_seed(&[0u8; 65]).is_err());
918        assert!(ExtendedPrivateKey::from_seed(&[0u8; 16]).is_ok());
919        assert!(ExtendedPrivateKey::from_seed(&[0u8; 64]).is_ok());
920    }
921
922    #[test]
923    fn test_normal_vs_hardened_different_keys() {
924        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
925        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
926        let normal = master.derive_child(0, false).unwrap();
927        let hardened = master.derive_child(0, true).unwrap();
928        assert_ne!(&*normal.private_key_bytes(), &*hardened.private_key_bytes());
929    }
930
931    #[test]
932    fn test_multi_account_derivation() {
933        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
934        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
935
936        let eth0 = master.derive_path(&DerivationPath::ethereum(0)).unwrap();
937        let eth1 = master.derive_path(&DerivationPath::ethereum(1)).unwrap();
938        let btc0 = master.derive_path(&DerivationPath::bitcoin(0)).unwrap();
939
940        // All different keys
941        assert_ne!(&*eth0.private_key_bytes(), &*eth1.private_key_bytes());
942        assert_ne!(&*eth0.private_key_bytes(), &*btc0.private_key_bytes());
943    }
944
945    // ─── BIP-32 Vector 1: xprv/xpub Serialization ───────────────
946
947    #[test]
948    fn test_bip32_vector1_master_xprv() {
949        // BIP-32 Test Vector 1 — Master key xprv
950        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
951        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
952        assert_eq!(
953            &*master.to_xprv(),
954            "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"
955        );
956    }
957
958    #[test]
959    fn test_bip32_vector1_master_xpub() {
960        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
961        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
962        assert_eq!(
963            master.to_xpub().unwrap(),
964            "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
965        );
966    }
967
968    #[test]
969    fn test_bip32_vector1_chain_m_0h() {
970        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
971        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
972        let child = master.derive_child(0, true).unwrap();
973        assert_eq!(
974            &*child.to_xprv(),
975            "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7"
976        );
977        assert_eq!(
978            child.to_xpub().unwrap(),
979            "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"
980        );
981    }
982
983    #[test]
984    fn test_bip32_xprv_roundtrip() {
985        let seed = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
986        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
987        let xprv_str = master.to_xprv();
988        let restored = ExtendedPrivateKey::from_xprv(&xprv_str).unwrap();
989        assert_eq!(&*master.private_key_bytes(), &*restored.private_key_bytes());
990        assert_eq!(master.chain_code(), restored.chain_code());
991        assert_eq!(master.depth(), restored.depth());
992    }
993
994    #[test]
995    fn test_bip32_from_xprv_invalid_checksum() {
996        // Valid xprv but flip last character to break checksum
997        let xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHiX";
998        // This should fail because the checksum is invalid (we appended 'X')
999        // But the base58 decoding may also fail since length changes
1000        assert!(ExtendedPrivateKey::from_xprv(xprv).is_err());
1001    }
1002
1003    // ─── BIP-32 Vector 2: Full chain ────────────────────────────
1004
1005    #[test]
1006    fn test_bip32_vector2_master_xprv() {
1007        let seed = hex::decode(
1008            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1009        ).unwrap();
1010        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1011        assert_eq!(
1012            &*master.to_xprv(),
1013            "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U"
1014        );
1015    }
1016
1017    #[test]
1018    fn test_bip32_vector2_chain_m_0() {
1019        let seed = hex::decode(
1020            "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"
1021        ).unwrap();
1022        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1023        let child = master.derive_child(0, false).unwrap();
1024        assert_eq!(
1025            &*child.to_xprv(),
1026            "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt"
1027        );
1028    }
1029
1030    // ─── Derivation Path Edge Cases ─────────────────────────────
1031
1032    #[test]
1033    fn test_derivation_path_hardened_h_notation() {
1034        let path = DerivationPath::parse("m/44h/60h/0h/0/0").unwrap();
1035        assert_eq!(path.steps.len(), 5);
1036        assert!(path.steps[0].hardened);
1037        assert_eq!(path.steps[0].index, 44);
1038    }
1039
1040    #[test]
1041    fn test_derivation_path_all_chain_presets() {
1042        let btc_segwit = DerivationPath::bitcoin_segwit(0);
1043        assert_eq!(btc_segwit.steps[0].index, 84); // BIP-84
1044        assert!(btc_segwit.steps[0].hardened);
1045
1046        let btc_taproot = DerivationPath::bitcoin_taproot(0);
1047        assert_eq!(btc_taproot.steps[0].index, 86); // BIP-86
1048        assert!(btc_taproot.steps[0].hardened);
1049
1050        let xrp = DerivationPath::xrp(0);
1051        assert_eq!(xrp.steps[1].index, 144); // XRP coin type
1052
1053        let neo = DerivationPath::neo(0);
1054        assert_eq!(neo.steps[1].index, 888); // NEO coin type
1055    }
1056
1057    // ─── ExtendedPublicKey Tests ────────────────────────────────────
1058
1059    #[test]
1060    fn test_extended_public_key_from_private() {
1061        let seed = [0xABu8; 64];
1062        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1063        let pubkey = master.to_extended_public_key().unwrap();
1064        assert_eq!(pubkey.depth(), 0);
1065        assert_eq!(pubkey.public_key_bytes().len(), 33); // compressed
1066    }
1067
1068    #[test]
1069    fn test_xpub_starts_with_xpub() {
1070        let seed = [0xABu8; 64];
1071        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1072        let xpub = master.to_xpub().unwrap();
1073        assert!(xpub.starts_with("xpub"));
1074    }
1075
1076    #[test]
1077    fn test_xpub_roundtrip() {
1078        let seed = [0xABu8; 64];
1079        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1080        let pubkey = master.to_extended_public_key().unwrap();
1081        let xpub_str = pubkey.to_xpub();
1082        let restored = ExtendedPublicKey::from_xpub(&xpub_str).unwrap();
1083        assert_eq!(pubkey.public_key_bytes(), restored.public_key_bytes());
1084        assert_eq!(pubkey.depth(), restored.depth());
1085        assert_eq!(pubkey.chain_code(), restored.chain_code());
1086    }
1087
1088    #[test]
1089    fn test_xpub_deterministic() {
1090        let seed = [0xABu8; 64];
1091        let m1 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1092        let m2 = ExtendedPrivateKey::from_seed(&seed).unwrap();
1093        assert_eq!(
1094            m1.to_extended_public_key().unwrap().to_xpub(),
1095            m2.to_extended_public_key().unwrap().to_xpub(),
1096        );
1097    }
1098
1099    #[test]
1100    fn test_extended_public_key_normal_derivation() {
1101        let seed = [0xABu8; 64];
1102        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1103        let pubkey = master.to_extended_public_key().unwrap();
1104
1105        // Derive normal child 0
1106        let child = pubkey.derive_child_normal(0).unwrap();
1107        assert_eq!(child.depth(), 1);
1108        assert_eq!(child.public_key_bytes().len(), 33);
1109    }
1110
1111    #[test]
1112    fn test_extended_public_key_derivation_consistency() {
1113        // Deriving pub child from pub key must match pub key derived from priv child
1114        let seed = [0x42u8; 64];
1115        let master_priv = ExtendedPrivateKey::from_seed(&seed).unwrap();
1116        let master_pub = master_priv.to_extended_public_key().unwrap();
1117
1118        // Private path: master_priv → child_priv(0) → to_pubkey
1119        let child_priv = master_priv.derive_child(0, false).unwrap();
1120        let child_pub_from_priv = child_priv.to_extended_public_key().unwrap();
1121
1122        // Public path: master_pub → child_pub(0)
1123        let child_pub_from_pub = master_pub.derive_child_normal(0).unwrap();
1124
1125        // Both paths should produce the same public key
1126        assert_eq!(
1127            child_pub_from_priv.public_key_bytes(),
1128            child_pub_from_pub.public_key_bytes(),
1129        );
1130    }
1131
1132    #[test]
1133    fn test_extended_public_key_hardened_rejected() {
1134        // ExtendedPublicKey should not support hardened derivation
1135        let seed = [0xABu8; 64];
1136        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1137        let pubkey = master.to_extended_public_key().unwrap();
1138
1139        // There's no hardened derive method on ExtendedPublicKey,
1140        // so we just verify normal derivation works for multiple indices
1141        for i in 0..5 {
1142            let child = pubkey.derive_child_normal(i).unwrap();
1143            assert_eq!(child.depth(), 1);
1144        }
1145    }
1146
1147    #[test]
1148    fn test_extended_public_key_different_indices() {
1149        let seed = [0xABu8; 64];
1150        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1151        let pubkey = master.to_extended_public_key().unwrap();
1152
1153        let c0 = pubkey.derive_child_normal(0).unwrap();
1154        let c1 = pubkey.derive_child_normal(1).unwrap();
1155        assert_ne!(c0.public_key_bytes(), c1.public_key_bytes());
1156    }
1157
1158    #[test]
1159    fn test_extended_public_key_chain_derivation() {
1160        // Multi-level normal derivation via public path
1161        let seed = [0xABu8; 64];
1162        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1163        let pubkey = master.to_extended_public_key().unwrap();
1164
1165        let child1 = pubkey.derive_child_normal(0).unwrap();
1166        let child2 = child1.derive_child_normal(1).unwrap();
1167        assert_eq!(child2.depth(), 2);
1168        assert_eq!(child2.public_key_bytes().len(), 33);
1169    }
1170
1171    #[test]
1172    fn test_xpub_invalid_prefix_rejected() {
1173        let seed = [0xABu8; 64];
1174        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1175        let xpub = master.to_extended_public_key().unwrap().to_xpub();
1176
1177        // Corrupt the first character
1178        let mut bad = String::from("ypub");
1179        bad.push_str(&xpub[4..]);
1180        assert!(ExtendedPublicKey::from_xpub(&bad).is_err());
1181    }
1182
1183    #[cfg(feature = "bitcoin")]
1184    #[test]
1185    fn test_extended_public_key_p2wpkh_address() {
1186        let seed = [0xABu8; 64];
1187        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1188        let pubkey = master.to_extended_public_key().unwrap();
1189        let addr = pubkey.p2wpkh_address("bc").unwrap();
1190        assert!(
1191            addr.starts_with("bc1q"),
1192            "P2WPKH should start with bc1q: {addr}"
1193        );
1194    }
1195
1196    #[cfg(feature = "bitcoin")]
1197    #[test]
1198    fn test_extended_public_key_p2tr_address() {
1199        let seed = [0xABu8; 64];
1200        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1201        let pubkey = master.to_extended_public_key().unwrap();
1202        let addr = pubkey.p2tr_address("bc").unwrap();
1203        assert!(
1204            addr.starts_with("bc1p"),
1205            "P2TR should start with bc1p: {addr}"
1206        );
1207    }
1208
1209    #[cfg(feature = "bitcoin")]
1210    #[test]
1211    fn test_extended_public_key_derived_addresses_differ() {
1212        let seed = [0xABu8; 64];
1213        let master = ExtendedPrivateKey::from_seed(&seed).unwrap();
1214        let pubkey = master.to_extended_public_key().unwrap();
1215        let c0 = pubkey.derive_child_normal(0).unwrap();
1216        let c1 = pubkey.derive_child_normal(1).unwrap();
1217        assert_ne!(
1218            c0.p2wpkh_address("bc").unwrap(),
1219            c1.p2wpkh_address("bc").unwrap(),
1220        );
1221    }
1222
1223    #[cfg(feature = "bitcoin")]
1224    #[test]
1225    fn test_parse_unsigned_tx_roundtrip() {
1226        use crate::bitcoin::transaction::*;
1227        let mut tx = Transaction::new(2);
1228        tx.inputs.push(TxIn {
1229            previous_output: OutPoint {
1230                txid: [0xAA; 32],
1231                vout: 0,
1232            },
1233            script_sig: vec![],
1234            sequence: 0xFFFFFFFF,
1235        });
1236        tx.outputs.push(TxOut {
1237            value: 50_000,
1238            script_pubkey: vec![
1239                0x00, 0x14, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1240                0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
1241            ],
1242        });
1243        let raw = tx.serialize_legacy();
1244        let parsed = parse_unsigned_tx(&raw).unwrap();
1245        assert_eq!(parsed.version, 2);
1246        assert_eq!(parsed.inputs.len(), 1);
1247        assert_eq!(parsed.outputs.len(), 1);
1248        assert_eq!(parsed.outputs[0].value, 50_000);
1249        assert_eq!(parsed.locktime, 0);
1250    }
1251}