node.js – How to convert HD wallet master key to extended key and child keys in javascript?

I’m trying to learn how HD wallets work internally by writing javascript code that can derive child keys from parent keys. So far, the only thing I’ve been able to do is convert a BIP39-generated seed to a master key and chain code. I can do that successfully.

Now I would like to convert the master key to an extended key, the long string that starts with xprv. I’d also like to know how to derive child keys from these, and obtain the private key for signing with any of the accounts.

I’ve been using this confusing BIP32 document as a guide so far, but I’m getting nowhere with it. As far as I can tell, before you derive any keys, you have to serialize the ones you already have.

The serialization process is described like this:

Extended public and private keys are serialized as follows:

  • 4 byte: version bytes (mainnet: 0x0488B21E public, 0x0488ADE4 private;
    testnet: 0x043587CF public, 0x04358394 private)
  • 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 derived keys, ….
  • 4 bytes: the fingerprint of the parent’s key (0x00000000 if master key)
  • 4 bytes: child number. This is ser32(i) for i in xi = xpar/i, with xi the key
    being serialized. (0x00000000 if master key)
  • 32 bytes: the chain code
  • 33 bytes: the public key or private key data (serP(K) for public keys,
    0x00 || ser256(k) for private keys)

This 78 byte structure can be
encoded like other Bitcoin data in Base58, by first adding 32 checksum
bits (derived from the double SHA-256 checksum), and then converting
to the Base58 representation. This results in a Base58-encoded string
of up to 112 characters. Because of the choice of the version bytes,
the Base58 representation will start with “xprv” or “xpub” on mainnet,
“tprv” or “tpub” on testnet.

Here is the javascript code I wrote in attempting to follow these instructions.

const crypto = require ('crypto');
const base58 = require ('base58');

const master_key_hex = '5a02ee5cb622f15a4e7ab3dfe7faa9861738da2ae7bb936c685af4dd7b1fbdee';
const chain_code_hex = 'abfbc73691eaad7ca4eb5e25bc51c5804af221cc27a23460634498497f92c92c';

// the 78 bytes of data
var data = '0488ade4';
data += '00';
data += '00000000';
data += '00000000';
data += chain_code_hex;
data += '00' + master_key_hex;

// add the checksum
const hash_1 = crypto.createHash ('sha256');
var sha_256_1 = hash_1.update (data, 'hex').digest ('hex');
const hash_2 = crypto.createHash ('sha256');
var sha_256_2 = hash_2.update (sha_256_1, 'hex').digest ('hex');

var data_with_checksum = data + sha_256_2;

//data = Buffer.from (data_with_checksum, 'hex');
var result = base58.encode (data_with_checksum);
console.log (result);

I tried it with a string and with a Buffer and it gave me the same error message both times. The error message is:

Error: Value passed is not a non-negative safe integer.

I have a feeling I am not even close to doing this right. Does anyone know how it should be coded to derive child keys from a master key?