diff --git a/modules/ecash-lib-wasm/src/ecc.rs b/modules/ecash-lib-wasm/src/ecc.rs
index 3c9a39545..cffb85f05 100644
--- a/modules/ecash-lib-wasm/src/ecc.rs
+++ b/modules/ecash-lib-wasm/src/ecc.rs
@@ -1,101 +1,164 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 //! Module for [`Ecc`] for signing secp256k1 signatures.
 
-use ecash_secp256k1::{All, Message, PublicKey, Secp256k1, SecretKey};
+use ecash_secp256k1::{All, Message, PublicKey, Scalar, Secp256k1, SecretKey};
 use thiserror::Error;
 use wasm_bindgen::prelude::*;
 
 use crate::ecc::EccError::*;
 
 /// ECC signatures with libsecp256k1.
 #[derive(Debug)]
 #[wasm_bindgen]
 pub struct Ecc {
     curve: Secp256k1<All>,
 }
 
 /// Errors indicating something went wrong with [`Ecc`].
 #[derive(Error, Debug, PartialEq, Eq)]
 pub enum EccError {
     /// Invalid secret key size
     #[error("Invalid secret key size, expected 32 bytes but got {0}")]
     InvalidSeckeySize(usize),
 
     /// Invalid secret key for signing
     #[error("Secret key not valid for secp256k1")]
     InvalidSeckey,
 
+    /// Invalid public key size
+    #[error("Invalid public key size, expected 33 bytes but got {0}")]
+    InvalidPubkeySize(usize),
+
+    /// Invalid pubkey key for signing
+    #[error("Pubkey key not valid for secp256k1")]
+    InvalidPubkey,
+
+    /// Invalid scalar size
+    #[error("Invalid scalar size, expected 32 bytes but got {0}")]
+    InvalidScalarSize(usize),
+
+    /// Invalid scalar in range
+    #[error("Scalar not valid for secp256k1")]
+    InvalidScalar,
+
     /// Invalid message size
     #[error("Invalid message size, expected 32 bytes but got {0}")]
     InvalidMessageSize(usize),
 }
 
 impl From<EccError> for String {
     fn from(ecc: EccError) -> Self {
         ecc.to_string()
     }
 }
 
 fn parse_secret_key(seckey: &[u8]) -> Result<SecretKey, String> {
     Ok(SecretKey::from_byte_array(
         &seckey
             .try_into()
             .map_err(|_| InvalidSeckeySize(seckey.len()))?,
     )
     .map_err(|_| InvalidSeckey)?)
 }
 
+fn parse_public_key(pubkey: &[u8]) -> Result<PublicKey, String> {
+    Ok(PublicKey::from_byte_array_compressed(
+        pubkey
+            .try_into()
+            .map_err(|_| InvalidPubkeySize(pubkey.len()))?,
+    )
+    .map_err(|_| InvalidPubkey)?)
+}
+
+fn parse_scalar(scalar: &[u8]) -> Result<Scalar, String> {
+    Ok(Scalar::from_be_bytes(
+        scalar
+            .try_into()
+            .map_err(|_| InvalidScalarSize(scalar.len()))?,
+    )
+    .map_err(|_| InvalidScalar)?)
+}
+
 fn parse_msg(msg: &[u8]) -> Result<Message, String> {
     Ok(Message::from_digest(
         msg.try_into().map_err(|_| InvalidMessageSize(msg.len()))?,
     ))
 }
 
 #[wasm_bindgen]
 impl Ecc {
     /// Create a new Ecc instance.
     #[allow(clippy::new_without_default)]
     #[wasm_bindgen(constructor)]
     pub fn new() -> Self {
         Ecc {
             curve: Secp256k1::default(),
         }
     }
 
     /// Derive a public key from secret key.
     #[wasm_bindgen(js_name = derivePubkey)]
     pub fn derive_pubkey(&self, seckey: &[u8]) -> Result<Vec<u8>, String> {
         let seckey = parse_secret_key(seckey)?;
         let pubkey = PublicKey::from_secret_key(&self.curve, &seckey);
         Ok(pubkey.serialize().to_vec())
     }
 
     /// Sign an ECDSA signature.
     #[wasm_bindgen(js_name = ecdsaSign)]
     pub fn ecdsa_sign(
         &self,
         seckey: &[u8],
         msg: &[u8],
     ) -> Result<Vec<u8>, String> {
         let seckey = parse_secret_key(seckey)?;
         let msg = parse_msg(msg)?;
         let sig = self.curve.sign_ecdsa(&msg, &seckey);
         Ok(sig.serialize_der().to_vec())
     }
 
     /// Sign a Schnorr signature.
     #[wasm_bindgen(js_name = schnorrSign)]
     pub fn schnorr_sign(
         &self,
         seckey: &[u8],
         msg: &[u8],
     ) -> Result<Vec<u8>, String> {
         let seckey = parse_secret_key(seckey)?;
         let msg = parse_msg(msg)?;
         let sig = self.curve.sign_schnorrabc_no_aux_rand(&msg, &seckey);
         Ok(sig.as_ref().to_vec())
     }
+
+    /// Return whether the given secret key is valid, i.e. whether is of correct
+    /// length (32 bytes) and is on the curve.
+    #[wasm_bindgen(js_name = isValidSeckey)]
+    pub fn is_valid_seckey(&self, seckey: &[u8]) -> bool {
+        parse_secret_key(seckey).is_ok()
+    }
+
+    /// Add a scalar to a secret key.
+    #[wasm_bindgen(js_name = seckeyAdd)]
+    pub fn seckey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
+        let a = parse_secret_key(a)?;
+        let b = parse_scalar(b)?;
+        Ok(a.add_tweak(&b)
+            .map_err(|_| InvalidSeckey)?
+            .secret_bytes()
+            .to_vec())
+    }
+
+    /// Add a scalar to a public key (adding G*b).
+    #[wasm_bindgen(js_name = pubkeyAdd)]
+    pub fn pubkey_add(&self, a: &[u8], b: &[u8]) -> Result<Vec<u8>, String> {
+        let a = parse_public_key(a)?;
+        let b = parse_scalar(b)?;
+        Ok(a.add_exp_tweak(&self.curve, &b)
+            .map_err(|_| InvalidPubkey)?
+            .serialize()
+            .to_vec())
+    }
 }
diff --git a/modules/ecash-lib/src/ecc.ts b/modules/ecash-lib/src/ecc.ts
index d533f3461..0ed94edea 100644
--- a/modules/ecash-lib/src/ecc.ts
+++ b/modules/ecash-lib/src/ecc.ts
@@ -1,37 +1,61 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 /** Interface to abstract over Elliptic Curve Cryptography */
 export interface Ecc {
     /** Derive a public key from secret key. */
     derivePubkey(seckey: Uint8Array): Uint8Array;
 
     /** Sign an ECDSA signature. msg needs to be a 32-byte hash */
     ecdsaSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
 
     /** Sign a Schnorr signature. msg needs to be a 32-byte hash */
     schnorrSign(seckey: Uint8Array, msg: Uint8Array): Uint8Array;
+
+    /**
+     * Return whether the given secret key is valid, i.e. whether is of correct
+     * length (32 bytes) and is on the curve.
+     */
+    isValidSeckey(seckey: Uint8Array): boolean;
+
+    /** Add a scalar to a secret key */
+    seckeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
+
+    /** Add a scalar to a public key (adding G*b) */
+    pubkeyAdd(a: Uint8Array, b: Uint8Array): Uint8Array;
 }
 
 /** Ecc implementation using WebAssembly */
 export let Ecc: { new (): Ecc };
 
 /** Dummy Ecc impl that always returns 0, useful for measuring tx size */
 export class EccDummy implements Ecc {
     derivePubkey(_seckey: Uint8Array): Uint8Array {
         return new Uint8Array(33);
     }
 
     ecdsaSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
         return new Uint8Array(73);
     }
 
     schnorrSign(_seckey: Uint8Array, _msg: Uint8Array): Uint8Array {
         return new Uint8Array(64);
     }
+
+    isValidSeckey(_seckey: Uint8Array): boolean {
+        return false;
+    }
+
+    seckeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
+        return new Uint8Array(32);
+    }
+
+    pubkeyAdd(_a: Uint8Array, _b: Uint8Array): Uint8Array {
+        return new Uint8Array(32);
+    }
 }
 
 export function __setEcc(ecc: { new (): Ecc }) {
     Ecc = ecc;
 }
diff --git a/modules/ecash-lib/src/hdwallet.test.ts b/modules/ecash-lib/src/hdwallet.test.ts
new file mode 100644
index 000000000..f436f6b13
--- /dev/null
+++ b/modules/ecash-lib/src/hdwallet.test.ts
@@ -0,0 +1,236 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { expect } from 'chai';
+
+import { fromHex, toHex } from './io/hex.js';
+import { initWasm } from './initNodeJs.js';
+import { HdNode } from './hdwallet.js';
+import { Ecc } from './ecc.js';
+
+// Tests are based on https://github.com/bitcoinjs/bip32/blob/master/test/fixtures/index.json
+
+describe('hdwallet', async () => {
+    await initWasm();
+    const ecc = new Ecc();
+
+    it('hdwallet 000102030405060708090a0b0c0d0e0f', () => {
+        const seed = fromHex('000102030405060708090a0b0c0d0e0f');
+        const master = HdNode.fromSeed(ecc, seed);
+        expect(toHex(master.seckey()!)).to.equal(
+            'e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35',
+        );
+        expect(toHex(master.pubkey())).to.equal(
+            '0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2',
+        );
+        expect(toHex(master.pkh())).to.equal(
+            '3442193e1bb70916e914552172cd4e2dbc9df811',
+        );
+        expect(toHex(master.fingerprint())).to.equal('3442193e');
+        expect(toHex(master.chainCode())).to.equal(
+            '873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508',
+        );
+        expect(master.index()).to.equal(0);
+        expect(master.depth()).to.equal(0);
+
+        const child0 = master.deriveHardened(0);
+        expect(toHex(child0.seckey()!)).to.equal(
+            'edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea',
+        );
+        expect(toHex(child0.pubkey())).to.equal(
+            '035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56',
+        );
+        expect(toHex(child0.pkh())).to.equal(
+            '5c1bd648ed23aa5fd50ba52b2457c11e9e80a6a7',
+        );
+        expect(toHex(child0.fingerprint())).to.equal('5c1bd648');
+        expect(toHex(child0.chainCode())).to.equal(
+            '47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141',
+        );
+        expect(child0.index()).to.equal(0x80000000);
+        expect(child0.depth()).to.equal(1);
+
+        const child01 = child0.derive(1);
+        expect(toHex(child01.seckey()!)).to.equal(
+            '3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368',
+        );
+        expect(toHex(child01.pubkey())).to.equal(
+            '03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c',
+        );
+        expect(toHex(child01.pkh())).to.equal(
+            'bef5a2f9a56a94aab12459f72ad9cf8cf19c7bbe',
+        );
+        expect(toHex(child01.fingerprint())).to.equal('bef5a2f9');
+        expect(toHex(child01.chainCode())).to.equal(
+            '2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19',
+        );
+        expect(child01.index()).to.equal(1);
+        expect(child01.depth()).to.equal(2);
+
+        expect(toHex(master.derivePath("m/0'").seckey()!)).to.equal(
+            toHex(child0.seckey()!),
+        );
+        expect(toHex(master.derivePath("m/0'/1").seckey()!)).to.equal(
+            toHex(child01.seckey()!),
+        );
+
+        const child012 = master.derivePath("m/0'/1/2'");
+        expect(toHex(child012.seckey()!)).to.equal(
+            'cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca',
+        );
+        expect(toHex(child012.pubkey())).to.equal(
+            '0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2',
+        );
+        expect(toHex(child012.pkh())).to.equal(
+            'ee7ab90cde56a8c0e2bb086ac49748b8db9dce72',
+        );
+        expect(toHex(child012.fingerprint())).to.equal('ee7ab90c');
+        expect(toHex(child012.chainCode())).to.equal(
+            '04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f',
+        );
+        expect(child012.index()).to.equal(0x80000002);
+        expect(child012.depth()).to.equal(3);
+
+        const child0122 = master.derivePath("m/0'/1/2'/2");
+        expect(toHex(child0122.seckey()!)).to.equal(
+            '0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4',
+        );
+        expect(toHex(child0122.pubkey())).to.equal(
+            '02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29',
+        );
+        expect(toHex(child0122.pkh())).to.equal(
+            'd880d7d893848509a62d8fb74e32148dac68412f',
+        );
+        expect(toHex(child0122.fingerprint())).to.equal('d880d7d8');
+        expect(toHex(child0122.chainCode())).to.equal(
+            'cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd',
+        );
+        expect(child0122.index()).to.equal(2);
+        expect(child0122.depth()).to.equal(4);
+
+        const child = master.derivePath("m/0'/1/2'/2/1000000000");
+        expect(toHex(child.seckey()!)).to.equal(
+            '471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8',
+        );
+        expect(toHex(child.pubkey())).to.equal(
+            '022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011',
+        );
+        expect(toHex(child.pkh())).to.equal(
+            'd69aa102255fed74378278c7812701ea641fdf32',
+        );
+        expect(toHex(child.fingerprint())).to.equal('d69aa102');
+        expect(toHex(child.chainCode())).to.equal(
+            'c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e',
+        );
+        expect(child.index()).to.equal(1000000000);
+        expect(child.depth()).to.equal(5);
+    });
+
+    it('hdwallet fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9...', () => {
+        const seed = fromHex(
+            'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542',
+        );
+        const master = HdNode.fromSeed(ecc, seed);
+        expect(toHex(master.seckey()!)).to.equal(
+            '4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e',
+        );
+        expect(toHex(master.pubkey())).to.equal(
+            '03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7',
+        );
+        expect(toHex(master.pkh())).to.equal(
+            'bd16bee53961a47d6ad888e29545434a89bdfe95',
+        );
+        expect(toHex(master.fingerprint())).to.equal('bd16bee5');
+        expect(toHex(master.chainCode())).to.equal(
+            '60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689',
+        );
+        expect(master.index()).to.equal(0);
+        expect(master.depth()).to.equal(0);
+
+        const child0 = master.derivePath('m/0');
+        expect(toHex(child0.seckey()!)).to.equal(
+            'abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e',
+        );
+        expect(toHex(child0.pubkey())).to.equal(
+            '02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea',
+        );
+        expect(toHex(child0.pkh())).to.equal(
+            '5a61ff8eb7aaca3010db97ebda76121610b78096',
+        );
+        expect(toHex(child0.fingerprint())).to.equal('5a61ff8e');
+        expect(toHex(child0.chainCode())).to.equal(
+            'f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c',
+        );
+        expect(child0.index()).to.equal(0);
+        expect(child0.depth()).to.equal(1);
+
+        const child1 = master.derivePath("m/0/2147483647'");
+        expect(toHex(child1.seckey()!)).to.equal(
+            '877c779ad9687164e9c2f4f0f4ff0340814392330693ce95a58fe18fd52e6e93',
+        );
+        expect(toHex(child1.pubkey())).to.equal(
+            '03c01e7425647bdefa82b12d9bad5e3e6865bee0502694b94ca58b666abc0a5c3b',
+        );
+        expect(toHex(child1.pkh())).to.equal(
+            'd8ab493736da02f11ed682f88339e720fb0379d1',
+        );
+        expect(toHex(child1.fingerprint())).to.equal('d8ab4937');
+        expect(toHex(child1.chainCode())).to.equal(
+            'be17a268474a6bb9c61e1d720cf6215e2a88c5406c4aee7b38547f585c9a37d9',
+        );
+        expect(child1.index()).to.equal(0xffffffff);
+        expect(child1.depth()).to.equal(2);
+
+        const child2 = master.derivePath("m/0/2147483647'/1");
+        expect(toHex(child2.seckey()!)).to.equal(
+            '704addf544a06e5ee4bea37098463c23613da32020d604506da8c0518e1da4b7',
+        );
+        expect(toHex(child2.pubkey())).to.equal(
+            '03a7d1d856deb74c508e05031f9895dab54626251b3806e16b4bd12e781a7df5b9',
+        );
+        expect(toHex(child2.pkh())).to.equal(
+            '78412e3a2296a40de124307b6485bd19833e2e34',
+        );
+        expect(toHex(child2.fingerprint())).to.equal('78412e3a');
+        expect(toHex(child2.chainCode())).to.equal(
+            'f366f48f1ea9f2d1d3fe958c95ca84ea18e4c4ddb9366c336c927eb246fb38cb',
+        );
+        expect(child2.index()).to.equal(1);
+        expect(child2.depth()).to.equal(3);
+
+        const child3 = master.derivePath("m/0/2147483647'/1/2147483646'");
+        expect(toHex(child3.seckey()!)).to.equal(
+            'f1c7c871a54a804afe328b4c83a1c33b8e5ff48f5087273f04efa83b247d6a2d',
+        );
+        expect(toHex(child3.pubkey())).to.equal(
+            '02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0',
+        );
+        expect(toHex(child3.pkh())).to.equal(
+            '31a507b815593dfc51ffc7245ae7e5aee304246e',
+        );
+        expect(toHex(child3.fingerprint())).to.equal('31a507b8');
+        expect(toHex(child3.chainCode())).to.equal(
+            '637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29',
+        );
+        expect(child3.index()).to.equal(0xfffffffe);
+        expect(child3.depth()).to.equal(4);
+
+        const child4 = master.derivePath("m/0/2147483647'/1/2147483646'/2");
+        expect(toHex(child4.seckey()!)).to.equal(
+            'bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23',
+        );
+        expect(toHex(child4.pubkey())).to.equal(
+            '024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c',
+        );
+        expect(toHex(child4.pkh())).to.equal(
+            '26132fdbe7bf89cbc64cf8dafa3f9f88b8666220',
+        );
+        expect(toHex(child4.fingerprint())).to.equal('26132fdb');
+        expect(toHex(child4.chainCode())).to.equal(
+            '9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271',
+        );
+        expect(child4.index()).to.equal(2);
+        expect(child4.depth()).to.equal(5);
+    });
+});
diff --git a/modules/ecash-lib/src/hdwallet.ts b/modules/ecash-lib/src/hdwallet.ts
new file mode 100644
index 000000000..0f9005b29
--- /dev/null
+++ b/modules/ecash-lib/src/hdwallet.ts
@@ -0,0 +1,181 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { Ecc } from './ecc.js';
+import { hmacSha512 } from './hmac.js';
+import { shaRmd160 } from './hash.js';
+import { Bytes } from './io/bytes.js';
+import { strToBytes } from './io/str.js';
+import { WriterBytes } from './io/writerbytes.js';
+
+const HIGHEST_BIT = 0x80000000;
+
+export class HdNode {
+    private _ecc: Ecc;
+    private _seckey: Uint8Array | undefined;
+    private _pubkey: Uint8Array;
+    private _chainCode: Uint8Array;
+    private _depth: number;
+    private _index: number;
+    private _parentFingerprint: number;
+
+    public constructor(params: {
+        ecc: Ecc;
+        seckey: Uint8Array | undefined;
+        pubkey: Uint8Array;
+        chainCode: Uint8Array;
+        depth: number;
+        index: number;
+        parentFingerprint: number;
+    }) {
+        this._ecc = params.ecc;
+        this._seckey = params.seckey;
+        this._pubkey = params.pubkey;
+        this._chainCode = params.chainCode;
+        this._depth = params.depth;
+        this._index = params.index;
+        this._parentFingerprint = params.parentFingerprint;
+    }
+
+    public seckey(): Uint8Array | undefined {
+        return this._seckey;
+    }
+
+    public pubkey(): Uint8Array {
+        return this._pubkey;
+    }
+
+    public pkh(): Uint8Array {
+        return shaRmd160(this._pubkey);
+    }
+
+    public fingerprint(): Uint8Array {
+        return this.pkh().slice(0, 4);
+    }
+
+    public index(): number {
+        return this._index;
+    }
+
+    public depth(): number {
+        return this._depth;
+    }
+
+    public parentFingerprint(): number {
+        return this._parentFingerprint;
+    }
+
+    public chainCode(): Uint8Array {
+        return this._chainCode;
+    }
+
+    public derive(index: number): HdNode {
+        const isHardened = index >= HIGHEST_BIT;
+        const data = new WriterBytes(1 + 32 + 4);
+        if (isHardened) {
+            if (this._seckey === undefined) {
+                throw new Error('Missing private key for hardened child key');
+            }
+            data.putU8(0);
+            data.putBytes(this._seckey);
+        } else {
+            data.putBytes(this._pubkey);
+        }
+        data.putU32(index, 'BE');
+        const hashed = hmacSha512(this._chainCode, data.data);
+        const hashedLeft = hashed.slice(0, 32);
+        const hashedRight = hashed.slice(32);
+
+        // In case the secret key doesn't lie on the curve, we proceed with the
+        // next index. This is astronomically unlikely but part of the specification.
+        if (!this._ecc.isValidSeckey(hashedLeft)) {
+            return this.derive(index + 1);
+        }
+
+        let seckey: Uint8Array | undefined;
+        let pubkey: Uint8Array;
+        if (this._seckey !== undefined) {
+            try {
+                seckey = this._ecc.seckeyAdd(this._seckey, hashedLeft);
+            } catch (ex) {
+                console.log('Skipping index', index, ':', ex);
+                return this.derive(index + 1);
+            }
+            pubkey = this._ecc.derivePubkey(seckey);
+        } else {
+            try {
+                pubkey = this._ecc.pubkeyAdd(this._pubkey, hashedLeft);
+            } catch (ex) {
+                console.log('Skipping index', index, ':', ex);
+                return this.derive(index + 1);
+            }
+            seckey = undefined;
+        }
+        return new HdNode({
+            ecc: this._ecc,
+            seckey: seckey,
+            pubkey: pubkey,
+            chainCode: hashedRight,
+            depth: this._depth + 1,
+            index,
+            parentFingerprint: new Bytes(this.fingerprint()).readU32('BE'),
+        });
+    }
+
+    public deriveHardened(index: number): HdNode {
+        if (index < 0 || index >= HIGHEST_BIT) {
+            throw new TypeError(
+                `index must be between 0 and ${HIGHEST_BIT}, got ${index}`,
+            );
+        }
+        return this.derive(index + HIGHEST_BIT);
+    }
+
+    public derivePath(path: string): HdNode {
+        let splitPath = path.split('/');
+        if (splitPath[0] === 'm') {
+            if (this._parentFingerprint) {
+                throw new TypeError('Expected master, got child');
+            }
+            splitPath = splitPath.slice(1);
+        }
+
+        let hd: HdNode = this;
+        for (const step of splitPath) {
+            if (step.slice(-1) === `'`) {
+                hd = hd.deriveHardened(parseInt(step.slice(0, -1), 10));
+            } else {
+                hd = hd.derive(parseInt(step, 10));
+            }
+        }
+
+        return hd;
+    }
+
+    public static fromPrivateKey(
+        ecc: Ecc,
+        seckey: Uint8Array,
+        chainCode: Uint8Array,
+    ): HdNode {
+        return new HdNode({
+            ecc,
+            seckey: seckey,
+            pubkey: ecc.derivePubkey(seckey),
+            chainCode,
+            depth: 0,
+            index: 0,
+            parentFingerprint: 0,
+        });
+    }
+
+    public static fromSeed(ecc: Ecc, seed: Uint8Array): HdNode {
+        if (seed.length < 16 || seed.length > 64) {
+            throw new TypeError('Seed must be between 16 and 64 bytes long');
+        }
+        const hashed = hmacSha512(strToBytes('Bitcoin seed'), seed);
+        const hashedLeft = hashed.slice(0, 32);
+        const hashedRight = hashed.slice(32);
+        return HdNode.fromPrivateKey(ecc, hashedLeft, hashedRight);
+    }
+}
diff --git a/modules/ecash-lib/src/io/bytes.ts b/modules/ecash-lib/src/io/bytes.ts
index 3b40fd3b1..c395e9cfa 100644
--- a/modules/ecash-lib/src/io/bytes.ts
+++ b/modules/ecash-lib/src/io/bytes.ts
@@ -1,70 +1,80 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+export type Endian = 'LE' | 'BE';
+
+export function endianToBool(endian?: Endian): boolean {
+    if (!endian) {
+        // By default, little endian
+        return true;
+    }
+    return endian === 'LE';
+}
+
 /** Reads ints/bytes from a Uint8Array. All integers are little-endian. */
 export class Bytes {
     public data: Uint8Array;
     public view: DataView;
     public idx: number;
 
     /** Create a new Bytes that reads from the given data */
     public constructor(data: Uint8Array) {
         this.data = data;
         this.view = new DataView(
             this.data.buffer,
             this.data.byteOffset,
             this.data.byteLength,
         );
         this.idx = 0;
     }
 
     /** Read a single byte */
     public readU8(): number {
         this.ensureSize(1);
         const result = this.data[this.idx];
         this.idx++;
         return result;
     }
 
     /** Read 2-byte little-endian integer (uint16_t) */
-    public readU16(): number {
+    public readU16(endian?: Endian): number {
         this.ensureSize(2);
-        const result = this.view.getUint16(this.idx, true);
+        const result = this.view.getUint16(this.idx, endianToBool(endian));
         this.idx += 2;
         return result;
     }
 
     /** Read 4-byte little-endian integer (uint32_t) */
-    public readU32(): number {
+    public readU32(endian?: Endian): number {
         this.ensureSize(4);
-        const result = this.view.getUint32(this.idx, true);
+        const result = this.view.getUint32(this.idx, endianToBool(endian));
         this.idx += 4;
         return result;
     }
 
     /** Read 8-byte little-endian integer (uint64_t) */
-    public readU64(): bigint {
+    public readU64(endian?: Endian): bigint {
         this.ensureSize(8);
-        const result = this.view.getBigUint64(this.idx, true);
+        const result = this.view.getBigUint64(this.idx, endianToBool(endian));
         this.idx += 8;
         return result;
     }
 
     /** Read the given number of bytes as array */
     public readBytes(numBytes: number): Uint8Array {
         this.ensureSize(numBytes);
         const result = this.data.slice(this.idx, this.idx + numBytes);
         this.idx += numBytes;
         return result;
     }
 
     private ensureSize(extraBytes: number) {
         if (this.data.length < this.idx + extraBytes) {
             throw (
                 `Not enough bytes: Tried reading ${extraBytes} byte(s), but ` +
                 `there are only ${this.data.length - this.idx} byte(s) left`
             );
         }
     }
 }
diff --git a/modules/ecash-lib/src/io/writer.ts b/modules/ecash-lib/src/io/writer.ts
index 78b229c7c..af247ad59 100644
--- a/modules/ecash-lib/src/io/writer.ts
+++ b/modules/ecash-lib/src/io/writer.ts
@@ -1,19 +1,20 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+import { Endian } from './bytes.js';
 import { Int } from './int.js';
 
 /** Writer interface to abstract over writing Bitcoin objects */
 export interface Writer {
     /** Write a single byte */
     putU8(value: Int): void;
     /** Write a 2-byte little-endian integer (uint16_t) */
-    putU16(value: Int): void;
+    putU16(value: Int, endian?: Endian): void;
     /** Write a 4-byte little-endian integer (uint32_t) */
-    putU32(value: Int): void;
+    putU32(value: Int, endian?: Endian): void;
     /** Write an 8-byte little-endian integer (uint64_t) */
-    putU64(value: Int): void;
+    putU64(value: Int, endian?: Endian): void;
     /** Write the given bytes */
     putBytes(bytes: Uint8Array): void;
 }
diff --git a/modules/ecash-lib/src/io/writerbytes.test.ts b/modules/ecash-lib/src/io/writerbytes.test.ts
index 8af05eb76..5574d905d 100644
--- a/modules/ecash-lib/src/io/writerbytes.test.ts
+++ b/modules/ecash-lib/src/io/writerbytes.test.ts
@@ -1,64 +1,75 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
 import { expect } from 'chai';
 
 import { fromHex } from './hex.js';
 import { WriterBytes } from './writerbytes.js';
 
 const wrote = (size: number, fn: (writer: WriterBytes) => void) => {
     const writer = new WriterBytes(size);
     fn(writer);
     return writer.data;
 };
 
 describe('WriterBytes', () => {
     it('WriterBytes.put* one', () => {
         expect(wrote(1, writer => writer.putU8(0xde))).to.deep.equal(
             fromHex('de'),
         );
         expect(wrote(2, writer => writer.putU16(0xdead))).to.deep.equal(
             fromHex('adde'),
         );
         expect(wrote(4, writer => writer.putU32(0xdeadbeef))).to.deep.equal(
             fromHex('efbeadde'),
         );
         expect(
             wrote(8, writer => writer.putU64(0xdeadbeef0badcafen)),
         ).to.deep.equal(fromHex('fecaad0befbeadde'));
         expect(
             wrote(3, writer => writer.putBytes(fromHex('abcdef'))),
         ).to.deep.equal(fromHex('abcdef'));
     });
     it('WriterBytes.put* multiple', () => {
         const writer = new WriterBytes(17);
         writer.putU8(0x44);
         writer.putU16(0x0201);
         writer.putU32(0x06050403);
         writer.putU64(0x0e0d0c0b0a090807n);
         writer.putBytes(fromHex('0f10'));
         expect(writer.data).to.deep.equal(
             fromHex('440102030405060708090a0b0c0d0e0f10'),
         );
     });
+    it('WriterBytes.put* multiple big-endian', () => {
+        const writer = new WriterBytes(17);
+        writer.putU8(0x44);
+        writer.putU16(0x0201, 'BE');
+        writer.putU32(0x06050403, 'BE');
+        writer.putU64(0x0e0d0c0b0a090807n, 'BE');
+        writer.putBytes(fromHex('0f10'));
+        expect(writer.data).to.deep.equal(
+            fromHex('440201060504030e0d0c0b0a0908070f10'),
+        );
+    });
     it('WriterBytes.put* failure', () => {
         expect(() => wrote(0, writer => writer.putU8(0))).to.throw(
             'Not enough bytes: Tried writing 1 byte(s), but only 0 byte(s) have been pre-allocated',
         );
         expect(() => wrote(1, writer => writer.putU16(0))).to.throw(
             'Not enough bytes: Tried writing 2 byte(s), but only 1 byte(s) have been pre-allocated',
         );
         expect(() => wrote(3, writer => writer.putU32(0))).to.throw(
             'Not enough bytes: Tried writing 4 byte(s), but only 3 byte(s) have been pre-allocated',
         );
         expect(() => wrote(7, writer => writer.putU64(0n))).to.throw(
             'Not enough bytes: Tried writing 8 byte(s), but only 7 byte(s) have been pre-allocated',
         );
         expect(() =>
             wrote(2, writer => writer.putBytes(new Uint8Array(3))),
         ).to.throw(
             'Not enough bytes: Tried writing 3 byte(s), but only 2 byte(s) have been pre-allocated',
         );
     });
 });
diff --git a/modules/ecash-lib/src/io/writerbytes.ts b/modules/ecash-lib/src/io/writerbytes.ts
index 95e94bfb8..fb231be66 100644
--- a/modules/ecash-lib/src/io/writerbytes.ts
+++ b/modules/ecash-lib/src/io/writerbytes.ts
@@ -1,86 +1,87 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+import { Endian, endianToBool } from './bytes.js';
 import { Int } from './int.js';
 import { Writer } from './writer.js';
 
 /**
  * Implementation of `Writer` which writes to an array of pre-allocated size.
  * It's intended to be used in unison with `WriterLength`, which first finds
  * out the length of the serialized object and then the actual data is written
  * using this class.
  **/
 export class WriterBytes implements Writer {
     public data: Uint8Array;
     public view: DataView;
     public idx: number;
 
     /** Create a new WriterBytes with the given pre-allocated size */
     public constructor(length: number) {
         this.data = new Uint8Array(length);
         this.view = new DataView(
             this.data.buffer,
             this.data.byteOffset,
             this.data.byteLength,
         );
         this.idx = 0;
     }
 
     /** Write a single byte */
     public putU8(value: Int): void {
         if (value < 0 || value > 0xff) {
             throw new Error(`Cannot fit ${value} into a u8`);
         }
         this.ensureSize(1);
         this.data[this.idx] = Number(value);
         this.idx++;
     }
 
     /** Write a 2-byte little-endian integer (uint16_t) */
-    public putU16(value: Int): void {
+    public putU16(value: Int, endian?: Endian): void {
         if (value < 0 || value > 0xffff) {
             throw new Error(`Cannot fit ${value} into a u16`);
         }
         this.ensureSize(2);
-        this.view.setUint16(this.idx, Number(value), true);
+        this.view.setUint16(this.idx, Number(value), endianToBool(endian));
         this.idx += 2;
     }
 
     /** Write a 4-byte little-endian integer (uint32_t) */
-    public putU32(value: Int): void {
+    public putU32(value: Int, endian?: Endian): void {
         if (value < 0 || value > 0xffffffff) {
             throw new Error(`Cannot fit ${value} into a u32`);
         }
         this.ensureSize(4);
-        this.view.setUint32(this.idx, Number(value), true);
+        this.view.setUint32(this.idx, Number(value), endianToBool(endian));
         this.idx += 4;
     }
 
     /** Write an 8-byte little-endian integer (uint64_t) */
-    public putU64(value: Int): void {
+    public putU64(value: Int, endian?: Endian): void {
         if (value < 0 || value > 0xffffffffffffffffn) {
             throw new Error(`Cannot fit ${value} into a u64`);
         }
         this.ensureSize(8);
-        this.view.setBigUint64(this.idx, BigInt(value), true);
+        this.view.setBigUint64(this.idx, BigInt(value), endianToBool(endian));
         this.idx += 8;
     }
 
     /** Write the given bytes */
     public putBytes(bytes: Uint8Array): void {
         this.ensureSize(bytes.length);
         this.data.set(bytes, this.idx);
         this.idx += bytes.length;
     }
 
     private ensureSize(extraBytes: number) {
         if (this.data.length < this.idx + extraBytes) {
             throw new Error(
                 `Not enough bytes: Tried writing ${extraBytes} byte(s), but ` +
                     `only ${this.data.length - this.idx} byte(s) have been ` +
                     `pre-allocated`,
             );
         }
     }
 }
diff --git a/modules/ecash-lib/src/io/writerlength.ts b/modules/ecash-lib/src/io/writerlength.ts
index 164be05f6..b328ab38e 100644
--- a/modules/ecash-lib/src/io/writerlength.ts
+++ b/modules/ecash-lib/src/io/writerlength.ts
@@ -1,43 +1,44 @@
 // Copyright (c) 2024 The Bitcoin developers
 // Distributed under the MIT software license, see the accompanying
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 
+import { Endian } from './bytes.js';
 import { Int } from './int.js';
 import { Writer } from './writer.js';
 
 /**
  * Writer implementation which only measures the length of the serialized
  * output but doesn't actually store any byte data.
  **/
 export class WriterLength implements Writer {
     public length: number;
 
     public constructor() {
         this.length = 0;
     }
 
     /** Write a single byte */
     public putU8(_value: Int): void {
         this.length++;
     }
 
     /** Write a 2-byte little-endian integer (uint16_t) */
-    public putU16(_value: Int): void {
+    public putU16(_value: Int, _endian?: Endian): void {
         this.length += 2;
     }
 
     /** Write a 4-byte little-endian integer (uint32_t) */
-    public putU32(_value: Int): void {
+    public putU32(_value: Int, _endian?: Endian): void {
         this.length += 4;
     }
 
     /** Write an 8-byte little-endian integer (uint64_t) */
-    public putU64(_value: Int): void {
+    public putU64(_value: Int, _endian?: Endian): void {
         this.length += 8;
     }
 
     /** Write the given bytes */
     public putBytes(bytes: Uint8Array): void {
         this.length += bytes.length;
     }
 }