Learn Ethereum in 2024. #16. Wallets in ethers.js
In this hands-on chapter, we’ll delve into wallets and accounts using the ethers.js JavaScript library. While not all methods demonstrated in this article are essential for programming dApps, they are crucial for developing wallet applications or crypto payment gateways. Nonetheless, the primary focus of this article is to implement, in code, the concepts discussed in the preceding articles.
Ethers.js provides classes and methods enabling the creation of HD Wallets, following BIP39, BIP32, and BIP44 standards. To recap, BIP39 outlines the process of generating a recovery phrase and a seed from entropy, which is a random number. Let’s begin our exploration of BIP39.
BIP39
Our initial step is to generate a random number to serve as the entropy. This can be accomplished by utilizing the native node.js crypto module, as demonstrated below.
import crypto from "node:crypto";
const entropy = crypto.randomBytes(16); // A 16-byte random number
console.log(entropy) // <Buffer 13 2d c2 89 3c 31 58 a3 e4 c6 7f 61 af 41 9e 9c>
Entropy alone is sufficient to generate the seed, yet the essence of BIP39 lies in alleviating the need to memorize a random number within the vast range of 0 to 340282366920938463463374607431768211455. Hence, the concept is to convert this entropy into a set of 12 words, commonly referred to as a recovery phrase or mnemonic phrase. We extensively discussed how to generate this phrase from entropy in the previous article.
Ethers.js provides a class called Mnemonic, which can be used to generate both the recovery phrase and the seed. It is possible to create an instance of the Mnemonic class with the entropy as the constructor parameter. Here’s the definition of this class:
Mnemonic.fromEntropy(entropy: BytesLike, password?: null | string,
wordlist?: null | Wordlist) => Mnemonic
It accepts entropy as arguments and, optionally, a password and a wordlist. The password corresponds to what we previously referred to as a passphrase. While the passphrase doesn’t affect the 12 words, which solely rely on entropy, it does influence the generated seed.
Let’s instantiate two objects using the same entropy but with different passphrases.
const entropy = crypto.randomBytes(16);
const mnemonic1 = Mnemonic.fromEntropy(entropy);
const mnemonic2 = Mnemonic.fromEntropy(entropy, "secret");
console.log(mnemonic1.phrase); // meat enlist ...conduct
console.log(mnemonic2.phrase); // meat enlist ...conduct
console.log(mnemonic1.computeSeed()); // 0xa3a80c...74
console.log(mnemonic2.computeSeed()); // 0xb0a8ca...42
That’s precisely the aim of BIP39: to establish the 12 (or more) words and derive the seed. To generate accounts from the seed, we need to utilize BIP32 and BIP44 standards.
BIP32 and BIP44
As per BIP32, starting from the seed, we can generate an unlimited number of accounts, each associated with a specific path. This path follows a syntax such as “m/0/1'”
, where each “/” denotes a branch in a tree structure with “m”
as the root. This concept forms the foundation of hierarchically deterministic wallets.
Ethers.js provides a class named HDNodeWallet, which allows for the instantiation of objects representing hierarchically deterministic wallets.
There are various methods to instantiate this class. To maintain continuity with the step-by-step process we’ve followed thus far, we’ll instantiate a new HD Wallet from the seed generated via BIP39.
const entropy = crypto.randomBytes(16); // entropy
const mnemonic = Mnemonic.fromEntropy(entropy); // mnemonic
const wallet = HDNodeWallet.fromSeed(mnemonic.computeSeed()); // wallet from seed
console.log(wallet.derivePath("m/0/0")); // account from path m/0/0
console.log(wallet.derivePath("m/0/1")); // account from path m/0/1
Based on the root, represented here by the wallet
variable, it’s possible to create an account (a pair of keys) for each path. In the code above, we utilize “m/0/0”
and “m/0/1”
as examples to generate accounts.
BIP44 defines a standard path for various networks to adhere to, and Ethereum wallets frequently adopt this standard. Typically, the initial account created by a deterministic wallet is located at “m/44'/60'/0'/0/0”
, although this is not obligatory. Let’s generate the private key and address for this path from a seed.
const entropy = crypto.randomBytes(16);
const mnemonic = Mnemonic.fromEntropy(entropy);
const wallet = HDNodeWallet.fromSeed(mnemonic.computeSeed());
const account = wallet.derivePath("m/44'/60'/0'/0/0");
console.log(account.privateKey); // 0x0d5250...e9
console.log(account.address); // 0x7e42...3e
What we’ve demonstrated above not only serves a pedagogical purpose but is also crucial when developing a wallet application or a cryptocurrency payment gateway. However, for other scenarios, Ethers.js provides direct methods for creating wallets and accounts, eliminating the need for this step-by-step processes.
Wallet class in Ethers.js
A rapid method to generate an account in Ethers.js is by utilizing the HDNodeWallet.createRandom
method. Let’s examine the code snippet below.
const wallet = HDNodeWallet.createRandom();
console.log(wallet);
The image below displays the wallet created, showcasing all associated information, including the entropy, the recovery phrase, and the current path. It’s worth noting that the default path is “m/44'/60'/0'/0/0”
, which is commonly employed by Ethereum-compatible wallets.
It’s also common to generate accounts using an existing private key. Let’s assume we already possess an Ethereum account and wants to use that account for signing transactions using code. For this objective, Ethers.js offers the Wallet class.
const account = new Wallet("0x0d52502e3aeb17d9078e580e1da17bb2dba6f3019e7598de18cb2e517e3962e9");
Once the account is created, it becomes possible to, for instance, sign messages using the signMessage
and signMessageSync
methods.
const account = new Wallet("0x0d52502e3aeb17d9078e580e1da17bb2dba6f3019e7598de18cb2e517e3962e9");
const signature = account.signMessageSync("Hello World");
console.log(signature); // 0x532b62...81be1c
Typically, accounts are utilized to sign transactions destined for Ethereum nodes. However, we still need to gain a deeper understanding of the transaction structure within the network and the process of sending signed transactions to network nodes. We’ll explore these topics more thoroughly in future articles.
Enjoyed the content? Connect with me on LinkedIn: https://www.linkedin.com/in/jpmorais/