Lumaktaw patungo sa pangunahing content

React.js & Ethers.js: Demo of using the InfuraProvider vs the Metamask wallet Web3Provider

Hello friends! 👋 Today we’re going to take a look at how to connect to Ethereum using the ethers.js InfuraProvider and the Web3Provider which injects a web3 wallet, in this case MetaMask.

We’ll use a simple React app and try to showcase the key differences when we try to:

  • Call a simple read method that gets the latest block number
  • Send a simple transaction. We’ll notice here the key difference when signing the transaction - while with the Web3Provider one could simply load the private key from Metamask, the InfuraProvider would need a wallet created locally with a stored private key. Therefore this method is only advised to be used within a backend service and not in a client side app.

CAUTION! For demonstration purposes only, this tutorial showcases creating a .env file to store our Infura API KEY and account PRIVATE KEY locally. However, when working with React the environment variables are embedded into the build, meaning anyone can view them by inspecting your app’s files. In the real world it is advised to use a backend service in order to protect the keys.

For those who only need the code, it can be found at the bottom of the article and you can simply copy & paste it in your App.js and create a .env file.

The final app and its’ functionalities after completing the tutorial will look like this:

App
Overview

Prerequisites

Node.js and NPM

First of all, let’s make sure that Node and NPM are installed on our machine. To do so, you can run the following command in a terminal:

node -v && npm -v

If node and npm are installed, the command will return their respective versions. If not, feel free to navigate to their documentation page for installing instructions.

Create-React-App

In order to install our app, let’s navigate to our workspace and run the following command:

npx create-react-app my-app

The installation process may take a few minutes. After that, you should see a folder showing up in your workspace with the name you gave to your app.

Environmental variables

In the root folder of our project, let’s go ahead and create the .env file. On Windows, simply create the file normally. On Mac & Linux this can be done by running the following command, and then the open command to start editing the file:

touch.env;
open.env;

Next up, we’ll need to populate our .env with two variables, our test wallet’s private key and our Infura API KEY. Reminder that in the real world it is not advisable to keep these in the frontend of your app, but rather use a backend service to protect the private key.

For React to be able to read these variables, their names will need to start with REACTAPP, like so:

REACT_APP_API_KEY = "229f0...";
REACT_APP_PRIVATE_KEY = "563d0...";

Ethers.js

Finally, let’s install ethers and we can get to the fun part - coding! To install ethers simply run the command below in a terminal:

npm install --save ethers

Building the app

After using create-react-app to generate a brand new React project, let’s navigate to the App.js file and start coding. For this tutorial, I’m going to be using functional components and hooks, rather than the old-school class components.

Let’s begin by importing the needed dependencies and creating the providers for communicating with the blockchain. For this tutorial, we’ll be using the Goerli testnet.

import React, {useState} from 'react';
import './App.css';

function App() {
const ethers = require('ethers')
const API_KEY = process.env.REACT_APP_API_KEY;
const PRIVATE_KEY = process.env.REACT_APP_PRIVATE_KEY;
const provider_Metamask = new ethers.providers.Web3Provider(window.ethereum);
const infuraProvider = new ethers.providers.InfuraProvider(
'goerli',
API_KEY
);

useState is the hook we’re going to be using for keeping the state of our app, with the aid of the variables initialized below:

const [blockNumber, setBlockNumber] = useState(null);
const [txSent, setTxSent] = useState(null);
const [txSentInfura, setTxSentInfura] = useState(null);

Let’s now start creating the frontend of the app, by editing the return statement of our App( ) function. The code below creates the two forms used for sending the transactions and the two different buttons for each provider used for fetching the latest block number.

return (
<div className="App">
<header className="App-header">
<h3> Press one of the buttons to find out the latest block number: </h3>
<div>
<button onClick={handleButton1}>InfuraProvider</button>
<button onClick={handleButton2}>Web3Provider</button>
<p>{blockNumber}</p>
</div>
<h3> Fill out the form to send a transaction via Web3Provider: </h3>
<div>
<form onSubmit={handleSubmitWeb3}>
<input type="text" name="address" placeholder="Recipient Address" />
<input type="text" name="amount" placeholder="Amount (ETH)" />
<input type="submit" value="Send w/ Web3Provider" />
</form>
<p>{txSent}</p>
</div>
<h3> Fill out the form to send a transaction via InfuraProvider: </h3>
<div>
<form onSubmit={handleSubmitInfura}>
<input type="text" name="address" placeholder="Recipient Address" />
<input type="text" name="amount" placeholder="Amount (ETH)" />
<input type="submit" value="Send w/ InfuraProvider" />
</form>
<p>{txSentInfura}</p>
</div>
</header>
</div>
);

Retrieving the latest block number

For handling the two buttons in charge of getting the latest block, we’ll simply perform a request to the correspondent provider (Infura or a wallet) and then set that retrieved value into our state, which will then be shown on the screen using the

blockNumber

line from the above return statement.

const handleButton1 = async () => {
const latest_block = await infuraProvider.getBlockNumber("latest");
setBlockNumber(latest_block);
};

const handleButton2 = async () => {
const latest_block = await provider_Metamask.getBlockNumber("latest");
setBlockNumber(latest_block);
};

Sending a transaction

For sending a transaction we’ll need to know at least the to_address and the amount sent during the transaction. After our forms created in the return statement have gathered this information, after pressing the submit button of either of the forms, we’ll need a function to take care of sending this data to our function in charge of sending the transaction.

const handleSubmitWeb3 = async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const address = data.get("address");
const amount = data.get("amount");
sendTransaction(address, amount);
};

const handleSubmitInfura = async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const address = data.get("address");
const amount = data.get("amount");
const signer = new ethers.Wallet(PRIVATE_KEY, infuraProvider);
sendTransaction(address, amount, signer);
};

Finally, for sending the transaction let’s create the sendTransaction( ) function, which should be placed before the above two handleSubmit methods.

const sendTransaction = async (address, amount, signer = null) => {
if (signer == null) {
// Web3 Provider
if (!window.ethereum) console.error("No wallet found!");
else {
await window.ethereum.send("eth_requestAccounts");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const tx = await signer.sendTransaction({
to: address,
value: ethers.utils.parseEther(amount),
});
console.log("tx", tx);
setTxSent("Transaction initiated! Tx hash: " + tx.hash);
}
} // InfuraProvider
else {
const tx = await signer.sendTransaction({
to: address,
value: ethers.utils.parseEther(amount),
});
console.log("tx", tx);
setTxSentInfura("Transaction initiated! Tx hash: " + tx.hash);
}
};

The sendTransaction( ) function is split into two parts, one for each provider we’ve used in this tutorial. To determine which provider to use, the function check if it receives any signer when being called.

If the function receives a signer, it means that the provider used should be the Infura Provider, since for the Infura Provider the signer is generated in the handleSubmitInfura function, from the Private key entered in our .env file.

For the Web3 Provider the signer can be generated directly from window.ethereum, as seen in the first part of the sendTransaction( ) function.

Metamask

Using the Web3 Provider will prompt the Metamask extension to open and ask for approval (for the first time) and then ask the user to confirm the transaction.

If using the Infura Provider, the transaction will be sent directly by the App, without using any wallet. This is possible due to the App having access to the Private Key of our account, which was entered in the .env file.

Finally, the transaction hash is displayed on screen, and we can use a network scanner to check out the transaction on the blockchain.

Transaction

Thanks for sticking to the end and congratulations on learning how to use Infura with React! 🎉

Complete code overview

import React, { useState } from "react";
import "./App.css";

function App() {
const ethers = require("ethers");
const API_KEY = process.env.REACT_APP_API_KEY;
const PRIVATE_KEY = process.env.REACT_APP_PRIVATE_KEY;
const provider_Metamask = new ethers.providers.Web3Provider(window.ethereum);
const infuraProvider = new ethers.providers.InfuraProvider("goerli", API_KEY);
const [blockNumber, setBlockNumber] = useState(null);
const [txSent, setTxSent] = useState(null);
const [txSentInfura, setTxSentInfura] = useState(null);

const sendTransaction = async (address, amount, signer = null) => {
console.log(address, amount);
if (signer == null) {
if (!window.ethereum) console.error("No wallet found!");
else {
await window.ethereum.send("eth_requestAccounts");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const tx = await signer.sendTransaction({
to: address,
value: ethers.utils.parseEther(amount),
});
console.log("tx", tx);
setTxSent("Transaction initiated! Tx hash: " + tx.hash);
}
} else {
const tx = await signer.sendTransaction({
to: address,
value: ethers.utils.parseEther(amount),
});
console.log("tx", tx);
setTxSentInfura("Transaction initiated! Tx hash: " + tx.hash);
}
};

const handleButton1 = async () => {
const latest_block = await infuraProvider.getBlockNumber("latest");
setBlockNumber(latest_block);
};

const handleButton2 = async () => {
const latest_block = await provider_Metamask.getBlockNumber("latest");
setBlockNumber(latest_block);
};

const handleSubmitWeb3 = async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const address = data.get("address");
const amount = data.get("amount");
sendTransaction(address, amount);
};

const handleSubmitInfura = async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const address = data.get("address");
const amount = data.get("amount");
const signer = new ethers.Wallet(PRIVATE_KEY, infuraProvider);
sendTransaction(address, amount, signer);
};

return (
<div className="App">
<header className="App-header">
<h3> Press one of the buttons to find out the latest block number: </h3>
<div>
<button onClick={handleButton1}>InfuraProvider</button>
<button onClick={handleButton2}>Web3Provider</button>
<p>{blockNumber}</p>
</div>
<h3> Fill out the form to send a transaction via Web3Provider: </h3>
<div>
<form onSubmit={handleSubmitWeb3}>
<input type="text" name="address" placeholder="Recipient Address" />
<input type="text" name="amount" placeholder="Amount (ETH)" />
<input type="submit" value="Send w/ Web3Provider" />
</form>
<p>{txSent}</p>
</div>
<h3> Fill out the form to send a transaction via InfuraProvider: </h3>
<div>
<form onSubmit={handleSubmitInfura}>
<input type="text" name="address" placeholder="Recipient Address" />
<input type="text" name="amount" placeholder="Amount (ETH)" />
<input type="submit" value="Send w/ InfuraProvider" />
</form>
<p>{txSentInfura}</p>
</div>
</header>
</div>
);
}

export default App;