How To Build a Crypto Wallet

When you are starting off on your Web3 journey, one of the first steps you’ll take will be installing a crypto wallet. Wallets are an essential piece of software, enabling you to store and interact with your cryptocurrencies. In this technical tutorial, we will explore different types of crypto wallets and learn how to build our own.

Managing funds carries risk, and the purpose of this article is just to explain the technical concepts behind crypto wallet software. It’s highly recommended that you use an already established wallet with an open-sourced and audited codebase, such as MetaMask, Ledger, or Argent.

Getting Started

For the purpose of this demo we will create a simple Node.js app and use the ethereum-cryptography and ethers.js libraries. Start by creating a new empty folder named my-crypto-wallet and navigate to it. Then create a new project and install necessary dependencies by typing:

yard init -y
yarn add ethereum-cryptography ethers

To follow along, check out the full working example located in the Chainlink Smart Contract Examples repository under the My Crypto Wallet folder.

git clone https://github.com/smartcontractkit/smart-contract-examples.git
cd smart-contract-examples/my-crypto-wallet

It’s All Cryptography

Blockchain is solving the problem of trust. With cryptocurrencies, you alone are in control of your funds rather than a bank or another third party. Crypto wallets are just a tool for managing your funds, and you can easily switch from one wallet service to another. This is all possible as a result of cryptography: We don’t need to trust anyone, we can easily cryptographically verify the truth and protect our own funds.

For the purpose of this demo, we will focus on Ethereum Virtual Machine (EVM) accounts, because the number of developers familiar with the EVM is high. An account is just a cryptographic pair of a private key and a public key. Imagine that your account is your credit card for example. The public key would be your credit card number, while the private key would be your PIN. You can easily share your public key with anyone, but the private key must remain secret only to yourself. If your private key is compromised, you are at risk of losing all of your funds permanently. And like any debit card, as long you have funds on your account, you can spend money.

The very first step in generating a new wallet is writing down a “seed phrase” or mnemonic. This generates the rest of the account (pairs private/public key) and is the only way to restore your crypto wallet. If you deleted a mobile wallet or lost your phone, your hardware wallet broke, or you used a web browser extension wallet and your computer died, your seed phrase would allow you to set up a new wallet on a phone, browser, or hardware device and regain complete access to your crypto.

Create a new JavaScript file inside your project and name it 01_newAccount.js. Let’s create a function to generate a mnemonic. Bitcoin Improvement Proposal 39, or BIP-39 for short, is a specification for generating mnemonic or seed phrases. It starts with a random number, known as entropy, which needs to be a multiple of 32 bits (strength % 32 == 0) and between 128- and 256-bits long. A 128 bits-long entropy will produce a mnemonic consisting of 12 words, while a 256 bits-long entropy will produce a mnemonic of 24 words. The larger the entropy, the more mnemonic words generated, and the greater the security of your wallets.

const { generateMnemonic, mnemonicToEntropy } 
= require("ethereum-cryptography/bip39");
const { wordlist } = require("ethereum-cryptography/bip39/wordlists/english");

function _generateMnemonic() {
  const strength = 256; // 256 bits, 24 words; default is 128 bits, 12 words
  const mnemonic = generateMnemonic(wordlist, strength);
  const entropy = mnemonicToEntropy(mnemonic, wordlist);
  return { mnemonic, entropy };
}

If you have the private key of one account you can move it to another wallet application and use it from there, while you can use your seed phrase to restore all of the wallet’s accounts just by generating them from the mnemonic one by one.

BIP-32 is a specification for creating Hierarchical Deterministic (HD) wallets, where a single key can be used to generate an entire tree of key pairs. This single key serves as the root of the tree and it will always be generated by the exact same combination of words, also known as mnemonic or seed phrase. The root key actually generates all the other private keys for accounts and they can all be restored by this single root key. Since it’s easier for humans to remember a set of 12 or 24 words rather than a complex stream of random numbers and characters, we tend to say that a seed phrase restores a wallet, when in reality it’s used for generating a root key which can then derive a tree of private keys.

const { HDKey } = require("ethereum-cryptography/hdkey");

function _getHdRootKey(_mnemonic) {
  return HDKey.fromMasterSeed(_mnemonic);
}

An account’s private key is a 256 bit-long stream of zeros and ones. If you toss a coin 256 times and write one/zero for head/tails, there’s a large possibility that you will generate a private key that no one else is using currently. However, it’s not recommended to generate random private keys when you can use seed phrases for creating wallet accounts.

function _generatePrivateKey(_hdRootKey, _accountIndex) {
  return _hdRootKey.deriveChild(_accountIndex).privateKey;
}

Ethereum and Bitcoin use the secp256k1 elliptic curve for cryptographic computations. Each account’s public key is derived from a corresponding private key using the Elliptic Curve Digital Signature Algorithm or ECDSA for short. By applying the ECDSA to the private key, we get a 64-byte integer, which is two 32-byte integers that represent X and Y of a point on the secp256k1 elliptic curve, concatenated together. The math behind this algorithm allows software to easily calculate the public key of a given private key, while the reverse process is impossible. One can’t compute the private key of a given public key using ECDSA on the secp256k1 elliptic curve.

const { getPublicKey } = require("ethereum-cryptography/secp256k1");

function _getPublicKey(_privateKey) {
  return getPublicKey(_privateKey);
}

Once we have the public key, we can calculate the account address. Ethereum uses the same addresses across all networks including rollups, test networks, and mainnet. Users specify the network that they want to use inside their wallet software. 

To calculate an address from the public key, we need to apply the Keccak-256 hashing alghorithm to the public key and take the last (least significant) 20 bytes of the result. 

const { keccak256 } = require("ethereum-cryptography/keccak");

function _getEthAddress(_publicKey) {
  return keccak256(_publicKey).slice(-20);
}

To generate a new wallet mnemonic and the first account out of it, we need to call all of the previously defined functions.

const { bytesToHex } = require("ethereum-cryptography/utils");

async function main() {
  const { mnemonic, entropy } = _generateMnemonic();
  console.log(`WARNING! Never disclose your Seed Phrase:\n ${mnemonic}`);

  const hdRootKey = _getHdRootKey(entropy);
  const accountOneIndex = 0;
  const accountOnePrivateKey = _generatePrivateKey(hdRootKey, accountOneIndex);
  const accountOnePublicKey = _getPublicKey(accountOnePrivateKey);
  const accountOneAddress = _getEthAddress(accountOnePublicKey);
  console.log(`Account One Wallet Address: 0x${bytesToHex(accountOneAddress)}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

To run this program, enter this command into your terminal:

node 01_newAccount.js

The previous program creates a new wallet. If you want to restore it using your mnemonic, create new file, name it 02_restoreWallet.js, and paste the following code:

const { mnemonicToEntropy } = require("ethereum-cryptography/bip39");
const { wordlist } = require("ethereum-cryptography/bip39/wordlists/english");
const { HDKey } = require("ethereum-cryptography/hdkey");
const { getPublicKey } = require("ethereum-cryptography/secp256k1");
const { keccak256 } = require("ethereum-cryptography/keccak");
const { bytesToHex } = require("ethereum-cryptography/utils");

async function main(_mnemonic) {
  const entropy = mnemonicToEntropy(_mnemonic, wordlist);
  const hdRootKey = HDKey.fromMasterSeed(entropy);
  const privateKey = hdRootKey.deriveChild(0).privateKey;
  const publicKey = getPublicKey(privateKey);
  const address = keccak256(publicKey).slice(-20);

  console.log(`Account One Wallet Address: 0x${bytesToHex(address)}`);
}

main(process.argv[2])
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

To run it, type node 02_restoreWallet.js and your seed phrase.

node 02_restoreWallet.js "here is where you should put your twelve-word mnemonic"

The Difference Between Hot and Cold Wallets

When you want to send some of your digital assets (coins, tokens, NFTs, etc.), you digitally sign the data using ECDSA with your private key and encrypt it before it’s sent to the receiver. The receiver can verify the signature using your public key. 

Hot or software wallets like MetaMask and Coinbase Wallet typically store account private keys on their servers and in the browser’s local storage. Cold or hardware wallets like Ledger or Trezor are physical devices that hold your private keys and keep them offline. To sign a transaction, you must connect your hardware wallet, bring it online, and physically click it to confirm. As soon as you’re done with the transaction, you disconnect your wallet and your keys are back offline. An important note is that neither hot or cold wallets actually store your assets. They hold your private keys. Your assets are always on the blockchain.

For the purpose of this demo, we will store our keys in a file system on your machine. Add this function to your 01_newAccount.js and 02_restoreWallet.js files:

const { writeFileSync } = require("fs");

function _store(_privateKey, _publicKey, _address) {
  const accountOne = {
    privateKey: _privateKey,
    publicKey: _publicKey,
    address: _address,
  };

  const accountOneData = JSON.stringify(accountOne);
  writeFileSync("account 2.json", accountOneData);
}

Make a Transaction

Finally, let’s create functionality for sending native coins. To do this, we first need to read our account’s private key from our file system. Then we need to create an ethers.js wallet object to pass the private key and provider as arguments. We will hardcode the network to be a Goerli testnet, but you can obviously expand that and create your provider objects by passing URLs for either a local blockchain client instance or a node-as-a-service provider like Infura or Alchemy. Next, we need to pass the receiver’s address and a gETH amount for sending. And finally, we will create a transaction object and broadcast it to the network.

Create a new file called 03_send.js and paste the following code.

const { getDefaultProvider, Wallet, utils } = require("ethers");
const { readFileSync } = require("fs");

async function main(_receiverAddress, _ethAmount) {
  const network = "goerli";
  const provider = getDefaultProvider(network);
  const accountRawData = readFileSync("account 1.json", "utf8");
  const accountData = JSON.parse(accountRawData);
  const privateKey = Object.values(accountData.privateKey);
  const signer = new Wallet(privateKey, provider);
  const transaction = await signer.sendTransaction({
    to: _receiverAddress,
    value: utils.parseEther(_ethAmount),
  });

  console.log(transaction);
}

main(process.argv[2], process.argv[3])
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

To run it, type:

node 03_send.js “receiverAddress” “amount”

Summary

Your wallet lets you read your balance, send transactions, and connect to decentralized applications. Many wallets also let you manage several accounts from one application. That’s because wallets don’t have custody of your funds, you do. They’re just a tool for managing what’s really yours.

In this article, you’ve learned how to develop a basic crypto wallet using Node.js, ethereum-cryptography, and ethers.js. Although this was an interesting engineering tutorial, it is highly recommended that you use already tested and proven wallet solutions instead of creating your own from scratch.

To learn more, head to the Chainlink Smart Contract Examples repository and start experimenting with this and the other example projects.

Learn more about Chainlink by visiting chain.link or reading the documentation at docs.chain.link. To discuss an integration, reach out to an expert.

Need Integration Support?
Talk to an expert
Faucets
Get testnet tokens
Read the Docs
Technical documentation