Build a Bitcoin wallet API Using Nodejs

Part One

A bitcoin wallet is a digital software application that allows users to securely store, send, and receive Bitcoin. It does not actually store Bitcoin, but instead, it stores the private keys necessary to access and manage the user's Bitcoin balance.

A wallet can be custodial(Does not give users access to their private keys) or Non-Custodial(Gives users full control of their bitcoins by providing the private keys and Mnemonic words for HD wallets). In this project, we will be building a non-custodial wallet.

Technologies Used

Project Setup

The Repo for this project can be found on GitHub(https://github.com/tonyguesswho/Non_Custodial_wallet)
The focus of this tutorial is to explain the concept and code found in the repo and not necessarily on Nodejs /Typescript setup.

To run the project follow these steps

In other to create a custodial HD wallet here are the steps involved

  • Generate mnemonic

  • Generate Private Keys

  • Derive Public key from Private keys

  • Generate Addresses

In the routes/wallet.ts file, we can find the 3 routes to perform the above steps

import express, { Router } from 'express';
import WalletController from "../controllers/walletControllers";
import {validategenerateKeys} from "../utils/validator/wallet";


const router: Router = express.Router();

router.get('/mnenomic', WalletController.generateMnenomic);

router.post('/privatekey', validategenerateKeys,  WalletController.generateMasterKeys);

router.post('/getaddress', WalletController.generateAddress);

export default router;

The main logic exists in the controller file

import { Request, Response } from "express";
import { generateMnemonic, mnemonicToSeed } from 'bip39';
import BIP32Factory, { BIP32Interface } from 'bip32';
import { payments, Psbt, networks } from "bitcoinjs-lib";
import { validationResult } from 'express-validator';
import {createAddressBatch, changeAddressBatch} from "../helpers/bitcoinlib";

import * as ecc from 'tiny-secp256k1';

export interface Address extends payments.Payment {
    derivationPath: string;
    masterFingerprint: Buffer;
    type?: "used" | "unused";
  }


const bip32 = BIP32Factory(ecc);

const derivationPath = "m/84'/0'/0'";


/**
   * @export
   * @class WalletController
   *  @description Performs wallet operation
   */
class WalletController {
  /**
    * @description -This method generates a mnemonic
    * @param {object} req - The request payload
    * @param {object} res - The response payload sent back from the method
    * @returns {object} - mnemonic
    */
  static async generateMnenomic(req: Request, res:Response) {
    const mnemonic = generateMnemonic(256);
    try {
      return res.status(200).json({
        message: "mnemonic generated Successfully",
        data: mnemonic
      });
    } catch (error) {
      res.status(500).json({ error: 'Internal Server Error' });
    }
  }

    /**
    * @description -This method generates MasterKeys
    * @param {object} req - The request payload
    * @param {object} res - The response payload sent back from the method
    * @returns {object} - MasterKeys
    */
  static async generateMasterKeys(req: Request, res:Response) {
    try {
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            return res.status(400).json({ error: errors.array() });
        }
        const {mnemonic} = req.body;
        const seed = await mnemonicToSeed(mnemonic);
        const privateKey = bip32.fromSeed(seed, networks.testnet);
        const xprv = privateKey.toBase58();

        const xpub = privateKey.derivePath(derivationPath).neutered().toBase58();
        return res.status(200).json({
            message: "Successfully generated master keys",
            data: {
                xprv,
                xpub,
            }
          });
    } catch (error) {
      res.status(500).json({ error: 'Internal Server Error' });
    }
  }

  static async generateAddress(req: Request, res:Response) {
    try {
        const errors = validationResult(req);

        if (!errors.isEmpty()) {
            return res.status(400).json({ error: errors.array() });
        }
        // get xpub
        const {xpub} = req.body;
        const addressType: string | unknown = req.query.type;

        const node: BIP32Interface = bip32.fromBase58(xpub, networks.testnet).derivePath("0/0");

        const currentAddressBatch: Address[] = createAddressBatch(xpub, node, addressType);

        const currentChangeAddressBatch: Address[] = changeAddressBatch(xpub, node, addressType);

        const data = {
            address: currentAddressBatch,
            changeAddress: currentChangeAddressBatch,
        };
        return res.status(200).json({
            message:  'Successfully generated address',
            data
          });
    } catch (error) {
      res.status(500).json({ error: 'Internal Server Error' });
    }
  }
}

export default WalletController;

Looking at the generateMnenomic method we are able to generate a 256 bytes seed phrase using generateMnemonic(256) from the bip39 library. This will generate a 24-word phrase changing the 256 to 128 will generate a 12-word phrase.

{
    "message": "mnemonic generated Successfully",
    "data": "hammer glance comic under spread mirror disagree behave program column render drama aspect update valley ginger panel unique yellow mix hint pear captain armed"
}

With the above seed phrase, we can generate a private key and a public key as seen in the generateMasterKeys method.

What is generated is a master private key and a master public key. The master public key will be used to generate child public keys, which will subsequently be used to generate user addresses.
Note: The output below is for educational purposes, in most cases, you wouldn't want to expose these keys in this manner.

{
    "message": "Successfully generated master keys",
    "data": {
        "xprv": "tprv8ZgxMBicQKsPeYzmD5BP4DSsZWUCGBHDqHdShaTuHQ4Nfj37zmjLSAdC8NyF6WzjWYmB4RNQFf4EU3FRiG5Thcqzv2akQkzCP4G1tANQdWF",
        "xpub": "tpubDC41Xjis25JGWBWQjpSHdeXxQhdJHC4znvfwURiRyA2DCpodLG7aykJumk5FyMZ9KJCTKyHZKw16e4Hn6YxUxTdEKdedQTTms8ShGSEq72V"
    }
}

The createAddressBatch the function was used to generate 10 addresses of different derivation paths. To learn more about derivation paths check out https://learnmeabitcoin.com/technical/derivation-paths

The code can be found in the helpers/bitcoinlib.ts file

import BIP32Factory, { BIP32Interface } from 'bip32';
import { networks, payments } from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import ECPairFactory from 'ecpair';


export interface Address extends payments.Payment {
  derivationPath: string;
  masterFingerprint: Buffer;
  type?: "used" | "unused";
}



const ECPair = ECPairFactory(ecc);
const bip32 = BIP32Factory(ecc);


export const createAddressBatch = (xpub: string, root: BIP32Interface, adType: string | unknown): Address[] => {
    const addressBatch: Address[] = [];

    for (let i = 0; i < 10; i++) {
      const derivationPath = `0/${i}`;
      const currentChildPubkey = deriveChildPublicKey(xpub, derivationPath);
      const currentAddress = getAddressFromChildPubkey(currentChildPubkey, adType);

      addressBatch.push({
        ...currentAddress,
        derivationPath,
        masterFingerprint: root.fingerprint,
      });
    }

    return addressBatch;
  };

  export const deriveChildPublicKey = (
    xpub: string,
    derivationPath: string
  ): BIP32Interface => {
    const node = bip32.fromBase58(xpub, networks.testnet);
    const child = node.derivePath(derivationPath);
    return child;
  };


/// Generate P2PKH address and P2WPKH
export const getAddressFromChildPubkey = (
    child: BIP32Interface, type: string | unknown = 'p2pkh'
  ): payments.Payment => {
    let address: payments.Payment;

    if (type === 'p2wpkh') {
      address = payments.p2wpkh({
        pubkey: child.publicKey,
        network: networks.testnet,
      });

      return address;
    }
    address = payments.p2pkh({
      pubkey: child.publicKey,
      network: networks.testnet,
    });

    return address;
  };


  export const changeAddressBatch = (xpub: string, root: BIP32Interface, addressType: string | unknown): Address[] => {
    const addressBatch: Address[] = [];

    for (let i = 0; i < 10; i++) {
      const derivationPath = `1/${i}`;
      const currentChildPubkey = deriveChildPublicKey(xpub, derivationPath);
      const currentAddress = getAddressFromChildPubkey(currentChildPubkey, addressType);

      addressBatch.push({
        ...currentAddress,
        derivationPath,
        masterFingerprint: root.fingerprint,
      });
    }

    return addressBatch;
  };

From the getAddressFromChildPubkey we can see that two different types of addresses can be generated(p2wpkh and p2pkh).

And by providing the xpub key to the /getaddress endpoint we get the needed addresses.

Conclusion

So far we have been able to go from generating a seed phrase to creating bitcoin addresses.
In part two I will show how we can create and broadcast transactions using the Blockstream Testnet Api.

References
https://github.com/KayBeSee/tabconf-workshop

https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki