Building Your Own Encrypted Wallet: A Developer’s Guide to Playing in the Mud.

Idogwu Chinonso
11 min readFeb 5, 2024

--

Wallets are an important tool in blockchain architecture. This essential tool or application serves as the connection point between users and the blockchain itself. For non-developers, who account for a larger percentage of bitcoin users, most of their interaction with the blockchain occurs through a wallet, as their transactions must be signed using the private keys of an account, which is controlled by a wallet.Wallets are also important for holding and managing users’ assets (bitcoins) and as such, there’s a great need for proper security when it comes to building wallets. As of writing this article, there are lots of wallet architectures and mechanisms put in place to ensure the security of user’s assets held in wallets, but this article aims to guide new developers or Bitcoin enthusiasts through the general idea of wallet creation and also the importance of security, encryption, access, and re-accessibility in wallet design and architecture.

This article assumes that you already know the different Bitcoin networks (testnet, regtest, mainnet) and also the different wallet types (p2pk, p2pkh, etc.). For the scope of this article, we will be focusing on building a multipurpose Bitcoin wallet using a specific set of libraries.

  • BitcoinJs library: The BitcoinJS library is a JavaScript library that provides tools and utilities for working with Bitcoin and Bitcoin-like cryptocurrencies in Node.js and web applications. It allows developers to perform various tasks related to Bitcoin, such as generating Bitcoin addresses, creating and signing transactions, and working with Bitcoin scripts.
  • ecpair library: The ecpair library is a part of the BitcoinJS ecosystem and is specifically designed for working with elliptic curve cryptography (ECC) in JavaScript applications. It provides functionality for creating and managing elliptic curve key pairs, which are essential for cryptographic operations such as digital signatures and public-key encryption.
  • tiny-secp256k1 library: The tiny-secp256k1 library is a compact JavaScript library specifically designed for working with the secp256k1 elliptic curve, which is widely used in Bitcoin and other cryptocurrencies. This library provides efficient and optimized implementations of cryptographic operations related to the secp256k1 curve, including key pair generation, digital signature generation and verification, and public key recovery.
  • CryptoJs library: The CryptoJS library is a popular JavaScript library that provides cryptographic functionalities for web applications. It offers a wide range of cryptographic algorithms and utilities, allowing developers to perform various cryptographic operations such as encryption, decryption, hashing, and HMAC (Keyed-Hash Message Authentication Code) generation.
  • fs library: this is also called the file system library. It offers us the ability to create and read the contents of a file from our typescript code. we’ll be using this to store the details of each wallet we create.

Our project will be written in TypeScript, so it is expected that you have Node and TypeScript installed before attempting to code along. Our project will be structured like this:

We start by creating a new folder called “Bitcoin_Cli_Wallet” then we cd into that folder and run the command “npm init”. This will initialize a new node module with a package.json file in the directory, after which we create a new folder called “wallets” in our root directory. Then we run the command “npm i -D typescript ts-node” This will install typescript and a typescript execution environment to let us run typescript code in Node.js. Next, we install the libraries we would use by running the following codes: “npm install bitcoinjs-lib”, “npm install ecpair”, “npm install tiny-secp256k1”, “npm install crypto”, “npm install dotenv”. These, except dotenv library, have already been mentioned. The dotenv library helps us keep private data or information that is needed in our code in a separate file so that it is not exposed to the rest of our code.

Importing dependencies

Up Next, we create an index.ts file and then populate the index file with the logic for our wallet. We will start by importing all the dependencies we’ll need for the project.

import * as bitcoin from 'bitcoinjs-lib'
import {ECPairFactory, ECPairInterface} from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import * as fs from 'fs'
import * as crypto from 'crypto';
import * as walletTypes from 'bitcoinjs-lib/src/payments/index'
import * as dotenv from 'dotenv';

Setting global and .env variables

The above code basically brings the contents of the different libraries we would be using into the scope of our project.

let totalWallets: number;
let allWalets: any;
let allAddresses: any;

interface History {
totalWallets: number;
allWalets: any;
allAddresses: any;
}

interface walletData {
keyPair: ECPairInterface;
walletObject: walletTypes.Payment;
walletType: string;
}

const ECPair = ECPairFactory(ecc);
const TESTNET = bitcoin.networks.testnet;
const REGTEST = bitcoin.networks.regtest;
const BITCOIN = bitcoin.networks.bitcoin;

dotenv.config();
const encryptionKey = process.env.ENCRYPTION_KEY;
const algorithm = process.env.ALGORITHM;

Next, we define global variables to track the total number of wallets we’ve created, an array of all the wallet names, and an array of all the addresses we’ve created. we also define 2 interfaces. The first is to define an object structure for storing our wallet history. This is important because we would be storing the history of our wallet activities using that structure. Finally, we have an interface to define the structure of each wallet’s data, and we would be storing each wallet we create on our machine using that structure. Next, we define the different network types that users can create a wallet for; basically, users would be able to make use of this cli wallet to create a wallet for any of the above-listed networks and finally, we read the encryption key and algorithm variables from our dotenv file, so yeah, we need to create a “.env” file in our root directory and paste the following into the file:

ENCRYPTION_KEY=MySuperSecretKey
ALGORITHM='aes-256-cbc'

This represents the key for encrypting and decrypting our wallet data, as well as the encryption algorithm we intend to use.

Create wallet function

function createWallet(walletname:string, network:string, walletType:string) {
let walletHistory = fetchWalletHistory();
totalWallets = walletHistory.totalWallets;
allWalets = walletHistory.allWalets;
allAddresses = walletHistory.allAddresses;
console.log("creating wallet.....................");

let keyPair: ECPairInterface = decodeNetwork(network);
let walletData: walletTypes.Payment = decodeWalletType(walletType, keyPair);
let address: string | undefined = walletData.address;

totalWallets += 1;
allWalets.push(walletname);
console.log("Initializing address:", address);
allAddresses.push(address);
console.log("all addresses", allAddresses);

const newWallet: walletData = {
keyPair: keyPair,
walletObject: walletData,
walletType: walletType
}

const newHistory: History = {
totalWallets: totalWallets,
allWalets: allWalets,
allAddresses: allAddresses
}

writeWalletDataToFile(walletname, newWallet);
writeWalletHistoryToFile(newHistory);
console.log("wallet created. your wallet address is: " + address);
}

The above code is a function called `createWallet` that generates a new wallet. It begins by fetching the wallet history, encompassing the total wallet count, names, and addresses, using the `fetchWalletHistory` function. Subsequently, it initializes variables to store the retrieved wallet history data, then decodes the network and wallet type parameters that were passed in by the user to acquire the requisite key pair and wallet data for the new wallet. The function then increments the total wallet count and appends the new wallet name and corresponding address to the respective arrays. It proceeds to construct an object representing the new wallet, incorporating its key pair, wallet object, and type. Additionally, it generates a new history object containing the updated wallet count, names, and addresses. The function writes the new wallet’s data to a file using `writeWalletDataToFile` and updates the wallet history file with the new data via `writeWalletHistoryToFile` function. Finally, it logs a message confirming the wallet’s creation and displays its address. The above function called some helper functions “decodeNetwork(network)” and “decodeWalletType(walletType, keyPair)” so we’ll be discussing them next.

Helper functions

function decodeNetwork(network: string): ECPairInterface {
let keyPair: ECPairInterface;

switch (network) {
case 'testnet':
keyPair = ECPair.makeRandom({ network: TESTNET });
break;
case 'bitcoin':
keyPair = ECPair.makeRandom({ network: BITCOIN });
break;
case 'regtest':
keyPair = ECPair.makeRandom({ network: REGTEST });
break;
default:
throw new Error ('Invalid wallet type');
}

return keyPair;
}


function decodeWalletType(userInput: string, keyPair: ECPairInterface):walletTypes.Payment {
let walletData: walletTypes.Payment;
switch (userInput) {
case 'embed':
walletData = bitcoin.payments.embed({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2ms':
walletData = bitcoin.payments.p2ms({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2pk':
walletData = bitcoin.payments.p2pk({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2pkh':
walletData = bitcoin.payments.p2pkh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2sh':
walletData = bitcoin.payments.p2sh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2wpkh':
walletData = bitcoin.payments.p2wpkh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2wsh':
walletData = bitcoin.payments.p2wsh({pubkey: keyPair.publicKey, network: TESTNET,});
break;
case 'p2tr':
walletData = bitcoin.payments.p2tr({pubkey: keyPair.publicKey, network: TESTNET,});
break;
default: throw new Error('Unknown wallet type: ' + userInput);
}

return walletData;
}

The above code block consists of two functions aimed at decoding the network and wallet type parameters, respectively, within our Bitcoin wallet creation process.

  1. decodeNetwork(network: string): ECPairInterface: This function takes a string parameter network representing the desired network type (‘testnet’, ‘bitcoin’, ‘regtest’). It then uses a switch statement to determine the appropriate network configuration based on the provided input. For each case, it generates a random key pair associated with the specified network using ECPair.makeRandom(). If an invalid network type is provided, it throws an error. Finally, it returns the generated key pair.
  2. decodeWalletType(userInput: string, keyPair: ECPairInterface): This function decodes the user input to determine the desired wallet type (‘p2ms’, ‘p2pk’, ‘p2pkh’, etc). It also accepts the key pair generated from the previous function as input. Similar to decodeNetwork, it utilizes a switch statement to construct the appropriate wallet data based on the provided input and network configuration. It utilizes the bitcoinJs library to generate and return the generated wallet data.

Storing and retrieving encrypted data from wallets directory.

function writeWalletDataToFile(walletname: string, walletData: walletData ): void {
let filename: string = `${walletname}.json`;
let filePath: string = `./wallets/${filename}`;
const encryptedData = encryptData(JSON.stringify(walletData));
fs.writeFileSync(filePath, encryptedData);
console.log("Encrypted walletData stored sucessfully.......");
}


function readWalletDataFromFile(walletname: string): walletData {
let filePath: string = `./wallets/${walletname}`;
console.log("accessing wallet data.............")
try {
let data = fs.readFileSync(filePath, 'utf8');
data = decryptData(data);
console.log("wallet data accessed sucessfully......");
return JSON.parse(data);
} catch (e) {
console.log("error accessing wallet data......");
throw new Error (e);
}
}

function writeWalletHistoryToFile(walletHistory: History): void {
let filename: string = 'history.json';
let filePath: string = `./wallets/${filename}`;
const encryptedData = encryptData(JSON.stringify(walletHistory));
fs.writeFileSync(filePath, encryptedData);
}

The above code block contains function relating to encrypting wallet data and storing them to individual files in the wallets folder in our project root directory;

  • writeWalletDataToFile(walletname: string, walletData: walletData ): void: This function takes the name of the wallet and its corresponding data object as parameters. It constructs the filename and file path based on the wallet name, and then encrypts the wallet data using the encryptData helper function which will be explained later. The encrypted data is then written to the specified file path using fs.writeFileSync. This function ensures that wallet data is securely stored in a file after encryption to aid retrieval even after program shutdown.
  • readWalletDataFromFile(walletname: string): walletData:” This function reads wallet data from a file based on the provided wallet name. It constructs the file path using the wallet name and attempts to read the data from the file. Upon successful reading, it decrypts the data using the decryptData function (which will be explained later), which corresponds to the encryption algorithm used during writing. The decrypted data is then parsed into a JSON object and returned. In case of an error during file reading or decryption, it throws an error.
  • writeWalletHistoryToFile(walletHistory: History): void:” This function is responsible for writing wallet history data to a file. It takes the wallet history object as input, constructs the file path, encrypts the history data, and writes it to the file using fs.writeFileSync. Similar to writeWalletDataToFile, this function ensures that wallet history data is securely stored after encryption and can be re-accessed later at a user-defined time.
function fetchWalletHistory(): History {
console.log("fetching wallet history..............");
const historyFilePath = './wallets/history.json';

try {
if (!fs.existsSync(historyFilePath)) {
// Create a new empty file if it doesn't exist
console.log("New wallet history file created.");
fs.writeFileSync(historyFilePath, '{}');
const newHistory: History = {
totalWallets: 0,
allWalets: [],
allAddresses: []
}
writeWalletHistoryToFile(newHistory);
return newHistory; // Return an empty object
}

let data = fs.readFileSync(historyFilePath, 'utf8');
data = decryptData(data);
console.log("wallet history accessed sucessfully......");
return JSON.parse(data);
} catch (e) {
console.log("error accessing wallet history......");
throw e;
}
}

The above function ensures the retrieval of wallet history data from a file, creates a new file if necessary, and handles errors that may occur during the process. It plays a crucial role in providing access to wallet history information for various operations within the application.

Firstly, it checks if the history file exists by using the fs.existsSync method to determine if the file exists at the specified path (./wallets/history.json). If the history file does not exist, the function creates a new empty file at the specified path using fs.writeFileSync. It initializes an empty object representing the wallet history and writes it to the newly created file, ensuring that there is always a history file available for storing wallet history data.

If the history file exists, the function reads its contents using fs.readFileSync and specifies the encoding as UTF-8. It then decrypts the data using the decryptData function, which corresponds to the encryption algorithm used when writing the history file. After decrypting the data, the function parses the JSON-formatted data into a JavaScript object representing the wallet history. It returns this object to the caller, providing access to the wallet history data for further processing.

Encrypting and decrypting wallet data/ history

// Function to encrypt data
function encryptData(data: string): string {
console.log("encrypting data..........");
const cipher = crypto.createCipher(algorithm, encryptionKey);
let encryptedData = cipher.update(data, 'utf8', 'hex');
encryptedData += cipher.final('hex');
console.log("encryption succesful..........");
return encryptedData;
}

// Function to decrypt data
function decryptData(encryptedData: string): string {
console.log("decrypting data..........");
const decipher = crypto.createDecipher(algorithm, encryptionKey);
let decryptedData = decipher.update(encryptedData, 'hex', 'utf8');
decryptedData += decipher.final('utf8');
console.log("decrypting successful..........");
return decryptedData;
}

function listAllAddresses(): [string | undefined] {
console.log("Accessing all addresses...");
let history = fetchWalletHistory();
let allAddresses = history.allAddresses;
console.log("All available addresses are: " + JSON.stringify(allAddresses));
return allAddresses;
}

This last block of code contains the main logic to encrypt and decrypt data being stored or retrieved from our local machine. It also implements a function that reads the wallet history data from our local machine and then logs all the addresses that have been created using our program.

encryptData(data: string): string: ” This function takes a string of data as input and encrypts it using the specified encryption algorithm (algorithm) and encryption key (encryption key). It uses the crypto.createCipher method to create a Cipher object, updates the data with the ‘utf8’ encoding, and generates the encrypted data in hexadecimal format (‘hex’). Finally, it returns the encrypted data.

decryptData(encryptedData: string): string:” This function takes the encrypted data as input and decrypts it using the same encryption algorithm and encryption key. It uses the ‘crypto.createDecipher’ method to create a decipher object, updates the encrypted data in hexadecimal format (‘hex’) and generates the decrypted data with the ‘utf8’ encoding. Finally, it returns the decrypted data.

“listAllAddresses(): [string | undefined]:” This function retrieves all wallet addresses from the wallet history. It first fetches the wallet history using the fetchWalletHistory function and then extracts the array of all wallet addresses. It logs the retrieved addresses to the console and returns them as an array of strings or undefined values.

Creating wallets using the current code Base

With a combination of all the code blocks explained above, we can now successfully create a new wallet, encrypt the wallet details, and store the encrypted data on our local machine for retrieval and decryption when the need arises. We can test out the functionality of our current code base by adding the following lines of code just below the last line of code in the code base.

// Below line of codes are simply for test purposes.
console.log("running tests...");
createWallet("myWallet", "regtest", "p2pkh");
console.log("================================================================");
createWallet("BobWallet", "regtest", "p2pkh");
console.log("================================================================");
createWallet("ALiceWallet", "regtest", "p2pkh");
console.log("================================================================");
listAllAddresses();

To test our current code base, we simply save the current workflow we have and then run the command “tsc index.ts”. This command generates an index.js file corresponding to the transcribed JavaScript representation of our typescript codebase. Next, we execute the js file by running the command “node index.js” This command runs the function to create multiple wallets and then also returns the list of all addresses contained in our cli wallet.

Conclusion

The current code base implements a majority of the functions we’ll need in a minimalist wallet except for the initiate transaction function; we’ll be covering that in Part 2 of this article, where we create a function to spend a user’s UTXO. Pending that, a complete implementation of our current stage can be found here on Github:

https://github.com/Nonnyjoe/Bitcoin_cli_wallet.

--

--

Idogwu Chinonso

I'm a blockchain developer who creates decentralized applications that empower users and drive innovation through continuous learning and collaboration