Netanel Basal

Learn development with great articles

Creating a Basic Ethereum Wallet App with Ethers.js and SolidJS

--

In this article, I’ll guide you through the process of building a basic Ethereum wallet application similar to Metamask using the ether.js library and SolidJS framework. This application will allow users to set up a wallet using a mnemonic phrase. Users will also be able to view their wallet balance and perform Ethereum transfers.

Understanding Mnemonic Phrases

A mnemonic phrase, commonly known as a seed phrase or recovery phrase, is a crucial component in the world of cryptocurrency wallets. It consists of a series of words that are randomly generated to ensure uniqueness and security. This mnemonic phrase is used to derive the cryptographic keys that control an Ethereum wallet. It acts as a human-readable backup, allowing users to restore their wallet and access their funds in case their original wallet is lost or inaccessible.

Setting Up the Project

To get started, you’ll need to create a new SolidJS TypeScript project using Vite. Open your terminal and run the following command:

npm create vite@latest my-wallet -- --template solid-ts

This command creates a new SolidJS project named my-wallet using the TypeScript template.

Installing Dependencies

In order to interact with the Ethereum blockchain and handle cryptographic operations such as encrypt and decrypt the password, you’ll need to install the required packages: ethers and crypto-js. Run the following command:

npm i ethers crypto-js

Building the UI

In the initial phase, we’ll prompt the user to input a password, which we’ll utilize to encrypt the wallet’s private key. This encrypted key will then be stored within the browser’s local storage. In the event that the user already possesses a key stored in the local storage, we’ll proceed to decrypt the private key and construct the wallet using it:

import { createSignal } from 'solid-js';
import { Wallet, HDNodeWallet } from 'ethers';

function App() {
const [step, setStep] = createSignal(1);
const [password, setPassword] = createSignal('');
const [phrase, setPhrase] = createSignal('');
const [wallet, setWallet] = createSignal<Wallet | HDNodeWallet | null>(null);

return (
<div>
<h1>Ethereum Wallet App</h1>

<Switch>
<Match when={step() === 1}>
<input
type="password"
placeholder="Enter password"
value={password()}
onChange={(e) => setPassword(e.target.value)}
/>

<button onClick={() => (key ? loadWallet() : createWallet())}>
{key ? 'Load Wallet' : 'Create Wallet'}
</button>
</Match>
</Switch>
</div>
);
}

First, let’s see the createWallet function:

import { HDNodeWallet, Wallet, formatEther } from 'ethers';
import CryptoJS from 'crypto-js';

function App() {
// ...signals

const provider = new JsonRpcProvider(
'https://sepolia.infura.io/v3/API_KEY'
);
const key = localStorage.getItem('encryptedPrivateKey');

const createWallet = () => {
const mnemonic = Wallet.createRandom().mnemonic;
setPhrase(mnemonic.phrase);

const wallet = HDNodeWallet.fromMnemonic(mnemonic!);

wallet.connect(provider);
setWallet(wallet);

encryptAndStorePrivateKey();

setStep(2);
};

const encryptAndStorePrivateKey = () => {
const encryptedPrivateKey = CryptoJS.AES.encrypt(
wallet()!.privateKey,
password()
).toString();

localStorage.setItem('encryptedPrivateKey', encryptedPrivateKey);
};
}

The createWallet function generates a random mnemonic, creates an HD wallet, connects it to a provider, encrypts and stores the wallet's private key, and progresses the user to the next step in the wallet setup process.

Please note that while this code demonstrates the process of encrypting and storing a private key, it’s important to handle encryption and security properly in a production environment. This might involve more sophisticated encryption methods, password management practices, and considerations for secure key storage.

When crafting a wallet, an essential step involves establishing a connection to a provider that facilitates communication with the blockchain. In my scenario, I’ve opted to utilize Infura on the Sepolia testnet. However, the choice of provider and blockchain is flexible and entirely up to you. It’s imperative to exercise caution and avoid exposing your provider API key, a practice I’m demonstrating here for tutorial purposes.

Now, let’s progress to step 2, where we unveil the phrase to the user. We’ll revisit the loadWallet() function in due course.

import { createSignal } from 'solid-js';
import { Wallet, HDNodeWallet } from 'ethers';

function App() {
// ...signals

return (
<div>
<h1>Ethereum Wallet App</h1>

<Switch>
<Match when={step() === 1}>...</Match>

<Match when={step() === 2}>
<p>Save the following prhase in a secure location</p>
<div>{phrase()}</div>
<button onClick={() => setStep(3}>Done</button>
</Match>
</Switch>
</div>
);
}

The second step is straightforward. We present the user with the phrase generated in the initial step and kindly request them to save it.

Moving on to the last step, it entails displaying the wallet address, wallet balance, and offering the transfer functionality:

import { createSignal } from 'solid-js';
import { Wallet, HDNodeWallet } from 'ethers';

function App() {
// ... prev signals

const [balance, setBalance] = createSignal('0');
const [recipientAddress, setRecipientAddress] = createSignal('');
const [amount, setAmount] = createSignal('0');
const [etherscanLink, setEtherscanLink] = createSignal('');

return (
<div>
<h1>Ethereum Wallet App</h1>

<Switch>
<Match when={step() === 1}>...</Match>

<Match when={step() === 2}>...</Match>

<Match when={step() === 3}>
<p>Wallet Address: {wallet()?.address}</p>
<p>Balance: {balance()}</p>

<p>Transfer to</p>

<div>
<input
placeholder="Recipient Address"
value={recipientAddress()}
onChange={(e) => setRecipientAddress(e.target.value)}
/>

<input
placeholder="Amount"
value={amount()}
onChange={(e) => setAmount(e.target.value)}
/>
</div>

{etherscanLink() && (
<a href={etherscanLink()} target="_blank">
View on Etherscan
</a>
)}
<button onClick={transfer}>Transfer</button>
</Match>
</Switch>
</div>
);
}

Let’s examine the transfer() function:

import { parseEther } from 'ethers';

const transfer = async () => {
try {
const transaction = await wallet().sendTransaction({
to: recipientAddress(),
value: parseEther(amount()),
});

setEtherscanLink(`https://sepolia.etherscan.io/tx/${transaction.hash}`);
} catch (error) {
console.error('Transaction error:', error);
}
};

The transfer function facilitates an Ethereum transfer using the currently connected wallet. It sends a transaction to the specified recipient address with the provided amount of Ether, and in case of an error, it logs the error message. Additionally, it generates a link to view the transaction on Etherscan and updates the etherscanLink state.

Now, let’s delve into the loadWallet() function, which comes into play when a wallet has been previously created:

import CryptoJS from 'crypto-js';
import { Wallet } from 'ethers';

const loadWallet = async () => {
const bytes = CryptoJS.AES.decrypt(key!, password());
const privateKey = bytes.toString(CryptoJS.enc.Utf8);

const wallet = new Wallet(privateKey, provider);
setWallet(wallet);

const balance = await wallet.provider.getBalance(wallet.address);
setBalance(formatEther(balance!));
setStep(3);
};

Within the loadWallet function, the encrypted private key is decrypted using the provided password. The decrypted private key is then utilized to create a new wallet instance, in conjunction with the designated provider. Subsequently, the function retrieves the wallet's balance from the blockchain's provider and ensures proper formatting. Finally, the user's is directed to step 3 where they can see the address, balance and make a transfer.

What’s Next

To take it to the next level, consider wrapping this functionality into a Chrome extension. Additionally, you can implement a wallet recovery feature where users can provide a recovery phrase to create a wallet from it:

function recoverWalletFromPhrase(phrase) {
try {
const wallet = Wallet.fromMnemonic(phrase);
return wallet;
} catch (error) {
console.error("Error recovering wallet:", error);
throw error;
}
}

Follow me on Medium or Twitter to read more about Angular and JS!

--

--

Netanel Basal
Netanel Basal

Written by Netanel Basal

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.

No responses yet