How to build our own crypto wallet app: walletBT

Home » Technology » Blockchain » How to build our own crypto wallet app: walletBT

In this article, I will show you how to create your crypto wallet mobile application using React Native. I will create the crypto wallet for the Ethereum blockchain. Let’s call the app name the walletBT. All the source code for the crypto wallet walletBT is available.

Tools and libraries used for building the crypto wallet

The list of the tools and libraries used for building the walletBT are as follows

React Native: The cross-platform framework for building the mobile app.
Ethers: The library for interacting with the Ethereum blockchain. We will be using version 5 since version 6 is having some issues at the time of writing this post.
Expo Secure Store: The package used to store the app data on the mobile securely.
Infura: It will provide you with the API to interact with the Ethereum blockchain.
Etherscan: It will provide you with the API to get the transaction details.
Redux-Toolkit: State management in the React Native app.

Functionality of the walletBT wallet application

The walletBT crypto wallet will have the following functionalities

Create Wallet: You will be able to create 10 accounts.
View Account Information: You will be able to view the address, balance, and private key.
Send Transaction: You will be able to send the ether(s) to any valid account inside the Ethereum network(Sepolia Test Network).
View Transaction: You will be able to view the transaction history.
Recover Wallet: You will be able to recover your wallet with the mnemonic phrase that you used to create an account.

The consideration taken for walletBT is that it only works with the Ethereum Sepolia test network. If you want to make it work for the Ethereum Mainnet then you can change the code accordingly. I will reflect in the code where you can make the changes. Also, the wallet will have 10 unique accounts created for you when you create an account in the walletBT. The walletBT is fully controlled by the user. Every detail of the account will be stored in the user’s phone storage but encrypted except the passphrase.

The reason behind having 10 accounts is to simplify the process of creating an account and the recovery of the account. You can have your own logic if you don’t prefer this approach.

walletBT crypto wallet mobile application breakdown

The application is created using the React Native Expo Managed Workflow. So create the app called the walletBT. You can use the command “npx create-expo-app walletBT“. At the root level, we will create a folder called “ethereum“, which will have some of the code related to Ethereum. Inside this folder create a file called “utils.js”. Here we will define a function to create the 10 accounts called createAccounts() and export it. You can install the ethers with the command “npm install ethers@5“.

How to create an account using ethers library

import { ethers } from "ethers";

function createAccounts(mnemonic) {
  let accounts = [];
  const hdNode = ethers.utils.HDNode.fromMnemonic(mnemonic);
  for (let index = 0; index < 10; index++) {
    // The BIP-44 standard for creating HD wallet path
    // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

    let path = `m/44'/60'/0'/0/${index}`;
    let myAccount = hdNode.derivePath(path);
    accounts.push({
      account: myAccount.address,
      key: myAccount.privateKey,
    });
  }
  return accounts;
}

export { createAccounts };

The createAccounts() function takes a mnemonic phrase which we will use later on to recover the accounts as well. In this mnemonic phrase, the user has to save it securely. The app does not save it. But the app will generate it for you. Another approach could be for the user to select the words for the mnemonic phrase by themselves as well. If you want to implement this approach you can refer to the documentation for BIP-39. The createAccounts() function returns the array of 10 accounts which contains the address and key pair.

Store for the walletBT to access the account information

Let’s create a central storage for the accounts so that we can access the account from any components in the React Native app. We will make use of the redux store with the redux toolkit. Therefore, create a folder called “store” in the app root folder. Inside it create a file called “walletBTSlice.js” and “store.js”. Basically. it will have a state for currentAccountAddress, currentAccountKey, and accounts. Also, it will have the function to set the value of currentAccountAddress, currentAccountKey, and accounts state.

walletBTSlice.js

import { createSlice } from "@reduxjs/toolkit";

const walletBTSlice = createSlice({
  name: "walletBT",
  initialState: {
    currentAccountAddress: null,
    currentAccountKey: null,
    accounts: null,
  },

  reducers: {
    setCurrentAccountAddress: (state, action) => {
      state.currentAccountAddress = action.payload;
    },

    setCurrentAccountKey: (state, action) => {
      state.currentAccountKey = action.payload;
    },

    setAccounts: (state, action) => {
      state.accounts = action.payload;
    },
  },
});

export const { setCurrentAccountAddress, setCurrentAccountKey, setAccounts } =
  walletBTSlice.actions;
export default walletBTSlice.reducer;

store.js

import { configureStore } from "@reduxjs/toolkit";
import walletBTSlice from "./walletBTSlice";

export const store = configureStore({
  reducer: {
    walletBT: walletBTSlice,
  },
});

Also, the account details are stored on your phone locally using the expo-secure-store. The structure of the wallet is an array of accounts and key objects. Everything is stored on your phone. The walletBT does not store anything remotely related to your wallet. You don’t have to log in time and again, walletBT fetches the account information from the local storage directly. With this, you should be able to create the CreateAccount screen.

Home Screen of walletBT

The home screen will have a top section that displays the account address in short notation from which you can select any other accounts. The truncate-eth-address package is used to represent the address in short notation. The middle section will have the account details like address, balance, and private key. Then at the bottom, it will have the transaction section which shows the latest 10 transactions.

Modify utils.js file

Since we are retrieving the account balance and transaction history, you can now add the two more methods in the utils.js file as follows.

import { ethers } from "ethers";

function createAccounts(mnemonic) {
  let accounts = [];
  const hdNode = ethers.utils.HDNode.fromMnemonic(mnemonic);
  for (let index = 0; index < 10; index++) {
    // The BIP-44 standard for creating HD wallet path
    // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

    let path = `m/44'/60'/0'/0/${index}`;
    let myAccount = hdNode.derivePath(path);
    accounts.push({
      account: myAccount.address,
      key: myAccount.privateKey,
    });
  }
  return accounts;
}

function getTransactionsList(
  currentAccountAddress,
  ETHERSCAN_API_KEY,
  transactionCount
) {
  const result = fetch(
    `https://api-sepolia.etherscan.io/api?module=account&action=txlist&address=${currentAccountAddress}&startblock=0&endblock=99999999&page=1&offset=${transactionCount}&sort=desc&apikey=${ETHERSCAN_API_KEY}`
  )
    .then((response) => response.json())
    .then((data) => {
      const tnx = data.result;
      return tnx;
    })
    .catch((error) => alert(error));
  return result;
}
async function getBalance(currentAccountAddress, provider_api) {
  try {
    const provider = new ethers.providers.JsonRpcProvider(provider_api);
    let balance = await provider.getBalance(currentAccountAddress);
    return ethers.utils.formatEther(balance);
  } catch (error) {
    console.log("Could not fetch balance");
  }
}
export { createAccounts, getTransactionsList, getBalance };

The two new methods are getTransactionsList() and getBalance(). The getTransactionsList() takes 3 parameters, namely the account address, etherescan API key, and the transaction count. You can get more information about how to construct the URL for fetching the transaction history from the Etherescan API endpoints doc. All three parameters are required to construct the API endpoint URL. If the request is successful it returns an array of transaction lists else throws an error. The details about the response of the endpoints can be found in the same doc mentioned above. A sample is given below for your quick reference.

{
   "status":"1",
   "message":"OK-Missing/Invalid API Key, rate limit of 1/5sec applied",
   "result":[
      {
         "blockNumber":"1037571",
         "timeStamp":"1651759857",
         "hash":"0x710e53707c79dd438a8bc3db2a45a123af6dba0d4a653a134035306d11f415fd",
         "nonce":"5",
         "blockHash":"0x64201f8940cee65186f64adb68a99fe3da0450bc9490617aeedd08f80cb8e29b",
         "transactionIndex":"0",
         "from":"0x10f5d45854e038071485ac9e402308cf80d2d2fe",
         "to":"0x382b4ca2c4a7cd28c1c400c69d81ec2b2637f7dd",
         "value":"50000000000000000000000",
         "gas":"21000",
         "gasPrice":"1500000007",
         "isError":"0",
         "txreceipt_status":"1",
         "input":"0x",
         "contractAddress":"",
         "cumulativeGasUsed":"21000",
         "gasUsed":"21000",
         "confirmations":"48476"
      },
      {
         "blockNumber":"1061942",
         "timeStamp":"1652077697",
         "hash":"0x7734cae034c8a7198a5988c5ae927adf30cf77d2cbb25ed996278e402c0e0032",
         "nonce":"0",
         "blockHash":"0xedc7119adb34edf5480fe52f090dedbef647948ffc91806afe7d8b0182781b40",
         "transactionIndex":"0",
         "from":"0x382b4ca2c4a7cd28c1c400c69d81ec2b2637f7dd",
         "to":"0x93e973436cd7757f21b1c947599f67082624a721",
         "value":"1000000000000000",
         "gas":"21000",
         "gasPrice":"2000000007",
         "isError":"0",
         "txreceipt_status":"1",
         "input":"0x",
         "contractAddress":"",
         "cumulativeGasUsed":"21000",
         "gasUsed":"21000",
         "confirmations":"24105"
      }
   ]
}

If you want to implement it for the Mainnet, then replace the URL with the Mainnet network API endpoints. The getBalance() method takes 2 parameters, namely the account address and the provider API(INFURA) to interact with the Ethereum blockchain. If the request is successful it returns the balance in ethers else throws an error. Now with these two methods, you should be able to create the Home Screen.

How to send ether(s) – Send Screen

The send screen will have a top section that will show the selected account. From it, you can also change the account. The bottom section will have the place to enter the recipient address, amount, and then finally send button. To send the ether(s), you need to add one more function inside the utils.js file.

Modify utils.js file

import { ethers } from "ethers";

function createAccounts(mnemonic) {
  let accounts = [];
  const hdNode = ethers.utils.HDNode.fromMnemonic(mnemonic);
  for (let index = 0; index < 10; index++) {
    // The BIP-44 standard for creating HD wallet path
    // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki

    let path = `m/44'/60'/0'/0/${index}`;
    let myAccount = hdNode.derivePath(path);
    accounts.push({
      account: myAccount.address,
      key: myAccount.privateKey,
    });
  }react
  return accounts;
}

function getTransactionsList(
  currentAccountAddress,
  ETHERSCAN_API_KEY,
  transactionCount
) {
  const result = fetch(
    `https://api-sepolia.etherscan.io/api?module=account&action=txlist&address=${currentAccountAddress}&startblock=0&endblock=99999999&page=1&offset=${transactionCount}&sort=desc&apikey=${ETHERSCAN_API_KEY}`
  )
    .then((response) => response.json())
    .then((data) => {
      const tnx = data.result;
      return tnx;
    })
    .catch((error) => alert(error));
  return result;
}
async function getBalance(currentAccountAddress, provider_api) {
  try {
    const provider = new ethers.providers.JsonRpcProvider(provider_api);
    let balance = await provider.getBalance(currentAccountAddress);
    return ethers.utils.formatEther(balance);
  } catch (error) {
    console.log("Could not fetch balance");
  }
}

async function sendTransaction(
  amount,
  provider_api,
  currentAccountKey,
  recipientAddress
) {
  try {
    const provider = new ethers.providers.JsonRpcProvider(provider_api);
    const wallet = new ethers.Wallet(currentAccountKey, provider);
    let gasPrice = await provider.getGasPrice();
    gasPrice = ethers.utils.formatUnits(gasPrice, "wei");
    const txn = await wallet
      .sendTransaction({
        to: recipientAddress,
        gasPrice: gasPrice,
        gasLimit: 21000,
        value: ethers.utils.parseEther(amount),
      })
      .catch((e) => {
        // console.log(e);
        alert(e);
      });
    const response = await txn.wait();
    return response;
  } catch {
    console.log("Could not process the transaction");
  }
}
export { createAccounts, getTransactionsList, getBalance, sendTransaction };

The method to send the ether(s) is called sendTransaction(). It takes 4 parameters, namely the amount in ether(s), the provider API(INFURA), the private key of the account, and the recipient address. If the transaction is successful it returns the response of the transaction or else it will throw an error. Replace the provider API with the Mainnet, if you want to implement it for the Mainnet network.

The Transaction Screen can be easily created. You just need to increase the transaction parameter value in the getTransactionsList() method inside the utils.js file when you call it.

walletBT wallet Recovery

Now if you recall, in our walletBT we are creating 10 different accounts. Since the logic is implemented to create 10 accounts only, now you can apply the same method to recover the wallet as well. Just to differentiate the wallet creation and recovery, you can use one of the states to handle it. In this case, we have used the recoveryMode state to indicate that the process is in a recovery state inside the CreateAccount Screen.

Conclusion

Since you have come to the end of the article, I hope this gave you some ideas on how to create a wallet on the Ethereum network using the React Native and Ethers libraries. Basically how to send the ether(s) in the Ethereum network, view transactions, and recover the accounts. I have not focused on the UI part of walletBT in this article. If you want to contribute to the walletBT, you can send me the pull request on the GitHub repo at the walletBT.