We're going to add a payment form to a React website that allows you to send Ethereum and Binance Coin (BNB) to any address using Metamask. This tutorial uses Metamask since it’s the most common crypto wallet (and for SEO click-bait goodness) but it should work with any compatible wallet.
You can see what the finished product looks like here:
Note that the final demo has additional features which aren’t part of this tutorial but the code is open-sourced so you can add it yourself.
At a high level, we’re going to connect our React website to Ethers.js which will allow us to connect to our crypto wallet, Metamask in this case, and connect directly to the Ethereum (and later Binance Smart Chain) blockchain.
This video was inspired by watching Arthur Chmaro's video, How to Send ETH using React.js which is another good video I’d recommend checking out.
For this tutorial we'll use an existing project, the open source atila client web app:
git clone https://github.com/atilatech/client-web-app
You can also create a new React project using npx create-react-app sample-project
First we're going to create a basic form that takes two inputs: a destination address and an amount.
Create the file: touch src/components/CryptoPaymentsForm.tsx
Paste the following into the file to create a simple form:
// CryptoPaymentsForm.tsx
import React from 'react'
const CryptoPaymentsForm = () => {
return (
<div>
<input type="number" placeholder="Amount" />
<input placeholder="Destination address" />
<button className="col-12 btn btn-primary">
Send Payment
</button>
</div>
)
}
export default CryptoPaymentsForm
Instead of copy-pasting, you can also try typing the code yourself, some people find it helps them with muscle memory.
Protip: You can use an IDE extension like ES7 React/Redux/GraphQL/React-Native snippets to simplify the creation of boiler plate comments.
For me, all I had to type was rfce
then it creates the boilerplate for the component.
Import that component in our practice page.
{/* src/scenes/Practice.tsx */}
import React from 'react'
{/* Add the line below */}
import CryptoPaymentsForm from '../../components/CryptoPaymentsForm'
const Practice = () => {
return (
<div className="my-3 p-5">
<h1>
Practice
</h1>
<CryptoPaymentsForm /> {/* Add this line */}
</div>
)
}
export default Practice
Add some Bootstrap classes to make our form look nicer.
import React from 'react'
const CryptoPaymentsForm = () => {
return (
<div className="p-5 card shadow text-center">
<input type="number" placeholder="Amount" className="col-12 form-control mb-3" />
<input placeholder="Destination address" className="col-12 form-control mb-3" />
<button className="col-12 btn btn-primary">
Send Payment
</button>
</div>
)
}
export default CryptoPaymentsForm
Now we need to be able to set the amount and address and be able to reference them later.
import React, { useState } from 'react'
const CryptoPaymentsForm = () => {
const [amount, setAmount] = useState(0); // new line
const [destinationAddress, setDestinationAddress] = useState(""); // new line
const startPayment = async (event: any) => { // new line
console.log({amount, destinationAddress});
}
return (
<div className="m-5 p-5 card shadow text-center">
{/* added onChange and onClick attributes */}
<input type="number" placeholder="Amount" value={amount} className="col-12 form-control mb-3" onChange={event => {setAmount(Number.parseFloat(event.target.value))}} />
<input placeholder="Destination address" value={destinationAddress} className="col-12 form-control mb-3" onChange={event => {setDestinationAddress(event.target.value)}} />
<button className="col-12 btn btn-primary" onClick={startPayment}>
Send Payment
</button>
</div>
)
}
export default CryptoPaymentsForm
Okay, now it's time for the really fun part! We're going to use ethers.js to connect Metamask to our website.
Ethers is a Javascript package that allows you to connect to the Ethereum blockchain (and other blockchains) using a crypto wallet like Metamask.
Huge shoutout to the author, Richard Moore, for creating such a useful and easy to use library.
Install ethers.js: yarn add ethers
or npm install --save ethers
Send the transaction to the blockchain by implementing our startPayment()
function:
import { Radio } from 'antd'
import React, { useState } from 'react'
declare global {
interface Window {
ethereum:any;
}
}
const startPayment = async (event: any) => { // new line
console.log({amount, destinationAddress});
event.preventDefault();
try {
if (!window.ethereum) {
throw new Error("No crypto wallet found. Please install it.");
}
await window.ethereum.send("eth_requestAccounts");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
ethers.utils.getAddress(destinationAddress);
const transactionResponse = await signer.sendTransaction({
to: destinationAddress,
value: ethers.utils.parseEther(amount.toString())
});
console.log({transactionResponse});
} catch (error: any) {
console.log({error});
}
}
Let's walk through what each snippet does.
This snippet allows us to use the window.ethereum
without our Typescript compiler complaining. See this Stack Overflow answer for more context.
declare global {
interface Window {
ethereum:any;
}
}
First, we need to see if Metamask (or a similar wallet browser extension) is installed. When you install Metamask, it automatically injects data into the Ethereum global object. If Metamask isn't installed then window.ethereum
will be empty and an error will be returned to the user. More information on the Ethereum Metamask provider API .
One piece of advice I give people who want to really learn a topic is to "read the original documents". If you want to learn more about how this API conencts to Ethereum, you might also be interested in EIP-1193 which goes into more detail on the specificaitons for the Javascript provider API.
if (!window.ethereum) {
throw new Error("No crypto wallet found. Please install it.");
}
This line will bring up a Metamask popup that asks the user if they want to allow our website to connect to their wallet. They need to grant us permission before we can send transactions with their wallets.
await window.ethereum.send("eth_requestAccounts");
The next few lines define the parts of the transaction that need to exist for us to send a transaction. For more information, see the Etherscan API reference, it's very well documented.
The final part actually sends the transaction. Ether values can go up to 18 decimal places ( 1 wei is 10^-18 Ether)! This is so many digits that you can run into underflow issues, which basically means that there are too many digits to be displayed and I know this from personal experience. So often you'll have to convert your numbers to a fixed number of decimal places or parse them into a hex representation.
ethers.utils.parseEther(amount.toString())
As a fun side experiment. console.log(ethers.utils.parseEther(amount.toString()))
and you should see a number in hex format. You can convert that to an actual number using: More information here.
To get some "fake money" that we can test with, we'll use the Ropsten test network and the Egor Fine Faucet to get some "free eth" sent to us.
We're using the Ropsten test network because it's the test network that most resembles the main Ethereum blockchain.
Next, let's display some information to the user if the transaction went through or if an error occurred.
const [amount, setAmount] = useState(0);
const [destinationAddress, setDestinationAddress] = useState("");
const [error, setError] = useState(""); //newline
const [transaction, setTransaction] = useState<ethers.providers.TransactionResponse | null >(null); // new line
Inside startPayment()
we'll set the error or transaction response
// startPayment()
// ...
event.preventDefault();
setError("");// clear previous error when a new transaction starts
// ...
console.log({transactionResponse});
setTransaction(transactionResponse); // new line
// ...
catch (error: any) {
console.log({error});
setError(error.message); // new line
}
Render the transaction response:
<button className="col-12 btn btn-primary" onClick={startPayment}>
Send Payment
</button>
{transaction &&
<div className="alert alert-success mt-3" role="alert">
{JSON.stringify(transaction)}
</div>
}
{error &&
<div className="alert alert-danger" role="alert">
{JSON.stringify(error)}
</div>
}
Now, if you send another transaction you should see the following:
Practice confirming and rejecting the transaction to see what the success and error messages look like.
Let's add the ability to actually see the transaction on the blockchain using a block explorer like Etherscan. We’ll be using ropsten.etherscan.com to match our testnet block explorer.
We also want to change the block explorer we're using based on the network where our transaction was sent. For example, if the transaction was on mainnet we’d want to use the etherscan.io block explorer.
export interface TransactionResponsePayment extends ethers.providers.TransactionResponse {
network?: ethers.providers.Network,
}
const CryptoPaymentsForm = () => {
// ...
const [error, setError] = useState("");
const [transaction, setTransaction] = useState<TransactionResponsePayment | null >(null);
let transactionUrl = "";
if (transaction?.hash) {
transactionUrl = `https://${transaction.network?.name === "homestead" ? "": transaction.network?.name+"."}etherscan.io/tx/${transaction.hash}`
}
// ...
const signer = provider.getSigner();
const network = await provider.getNetwork();
const transactionResponse = await signer.sendTransaction({
to: destinationAddress,
value: ethers.utils.parseEther(amount.toString())
}) as TransactionResponsePayment;
transactionResponse.network = network;
if (transaction?.hash) {
transactionUrl = `https://${transaction.network?.name === "homestead" ? "": transaction.network?.name+"."}etherscan.io/tx/${transaction.hash}`
}
That's it! We've now added Ethereum payment to our website in just 5 lines of code.
Excluding the optional stuff, this is all we needed:
await window.ethereum.send("eth_requestAccounts");
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
ethers.utils.getAddress(destinationAddress);
const transactionResponse = await signer.sendTransaction({
to: destinationAddress,
value: ethers.utils.parseEther(amount.toString())
});
This is truly remarkable! In just 5 lines of code, without having to open a bank account and using completely open-source software we're now able to accept money from anyone with an internet connection.
As someone who's integrated credit card payments into my website before the difference is so vast that things like this are what makes me so optimistic about the future and why I believe so much in the power of cryptocurrencies and blockchain in improving the fate of humanity.
While I personally think that Ethereum is one of the best blockchains in the world, the high gas fees can make it hard for using. Recently I’ve been recommending Binance Smart Chain (BSC). It’s not as decentralized as Ethereum it has low gas fees, it's used by the world’s largest crypto exchange, and it’s EVM compatible.
Binance’s compatibility with Ethereum is one of the reasons I like using it so much. This means we can build a dApp both Ethereum and Binance networks without switching and all your ETH addresses map to the same address and accounts on BSC. To learn more about why I like Ethereum and BSC, read Binance Smart Chain is Ethereum’s best scaling solution.
Update: You can also use the Add a Blockchain feature to add Binance Smart Chain and Binance Smart Chain testnet in one click.
In your Metamask extension go to Settings > Network > Add a network then add the following details.
Mainnet
Network Name: Binance Smart Chain
New RPC URL: https://bsc-dataseed.binance.org/
ChainID: 56
Symbol: BNB
Block Explorer URL: https://bscscan.com
Testnet
Network Name: Smart Chain - Testnet
New RPC URL: https://data-seed-prebsc-1-s1.binance.org:8545/
ChainID: 97
Symbol: BNB
Block Explorer URL: https://testnet.bscscan.com
You might run into issues with the Binance Smart Chain test net where it might not detect your RPC URL or the Chain ID.
Some things you can try include:
As I mentioned earlier, the cool thing about Ethers.js and BSC is that you don’t actually have to make any code changes to support the new BSC network. This even surprised me because I tried using all sorts of tricks to support it until I realized that it actually works without making any code changes. All you have to do is swap the networks in Metamask.
However, since it’s so easy to send money between different networks or send it to the wrong network by accident, we’ll add a small helper function that verifies that we’re going to the right network.
// import Network
import { Network } from '@ethersproject/providers';
export const CHAIN_IDS = {
BINANCE: {
NAME: "Binance",
CURRENCY_CODE: "BNB",
MAIN_NET: {
ID: 56
},
TEST_NET: {
NAME: "testnet",
ID: 97,
}
},
ETHEREUM: {
NAME: "Ethereum",
CURRENCY_CODE: "ETH",
MAIN_NET: {
ID: 1
},
ROPSTEN: {
NAME: "ropsten",
ID: 3
}
}
}
// add proptypes
interface CryptoPaymentFormPropTypes {
isTestNet?: boolean;
currency?: string;
}
CryptoPaymentsForm.defaultProps = {
isTestNet: true,
currency: "ETH",
};
function CryptoPaymentForm(props: CryptoPaymentFormPropTypes) {
const { currency, isTestNetwork } = props;
// ...
/**
* Check that the selected network in the browser matches the network in the user's settings.
* TODO: Make this code simpler.
* @param network
* @returns
*/
const checkCorrectNetwork = (network: Network) => {
let expectedChainId;
if (currency === CHAIN_IDS.ETHEREUM.CURRENCY_CODE) {
if (isTestNet) {
expectedChainId = CHAIN_IDS.ETHEREUM.ROPSTEN.ID;
} else {
expectedChainId = CHAIN_IDS.ETHEREUM.MAIN_NET.ID;
}
} else if (currency === CHAIN_IDS.BINANCE.CURRENCY_CODE) {
if (isTestNet) {
expectedChainId = CHAIN_IDS.BINANCE.TEST_NET.ID;
} else {
expectedChainId = CHAIN_IDS.BINANCE.MAIN_NET.ID;
}
}
if (network.chainId !== expectedChainId) {
const actualNetworkName = [CHAIN_IDS.BINANCE.TEST_NET.ID, CHAIN_IDS.ETHEREUM.ROPSTEN.ID].includes(network.chainId) ? "testnet" : "mainnet";
const actualCurrency = [CHAIN_IDS.BINANCE.MAIN_NET.ID, CHAIN_IDS.BINANCE.TEST_NET.ID].includes(network.chainId)? CHAIN_IDS.BINANCE.CURRENCY_CODE : CHAIN_IDS.ETHEREUM.CURRENCY_CODE;
return {isCorrectNetwork: false, message: `Change your crypto wallet network. Expected "${isTestNet ? "testnet" : "mainnet"}" network (${networkName}) for currency: ${currency}.
Instead received "${actualNetworkName}" network (${network.name}) for currency: ${actualCurrency}.`}
}
return { isCorrectNetwork: true, message: "" }
}
// inside const startPayment =
const { isCorrectNetwork, message } = checkCorrectNetwork(network);
if (!isCorrectNetwork) {
throw new Error(message)
}
/// inside render()
<div className="input-group mb-3">
<div className="input-group-prepend">
<span className="input-group-text">{`${currency} ${networkName}`}</span>
</div>
<input placeholder="Destination address" className="col-12 form-control" value={destinationAddress} onChange={event => {setDestinationAddress(event.target.value)}} />
</div>
First, we defined the Chain IDs for the networks that our payment supports. These Chain IDs can be found at chainlist.org.
export const CHAIN_IDS = ...
Create 2 new props currency
and isTestNetwork
and whenever a transaction is about to be sent, we check that the network in our wallet matches the expected wallet in our payment form.
Next, in our payment form we’ll add a Toggle that allows us to switch between the two different cryptocurrencies.
Next we’re going to use some React Hooks to toggle between the different networks. I’m going to use the Ant Design library but you can pick any library you prefer.
// Practice.tsx
// update your imports
import CryptoPaymentForm, { CHAIN_IDS, CRYPTO_IN_USD, MAXIMUM_DECIMAL_PLACES } from './CryptoPaymentForm';
import 'bootstrap/dist/css/bootstrap.css';
import 'antd/dist/antd.css';
import { Select } from 'antd';
const currencyOptions = [
{
name: CHAIN_IDS.ETHEREUM.NAME,
value: CHAIN_IDS.ETHEREUM.CURRENCY_CODE,
},
{
name: CHAIN_IDS.BINANCE.NAME,
value: CHAIN_IDS.BINANCE.CURRENCY_CODE,
}
]
const networkOptions = [
{
value: "testnet",
},
{
value: "mainnet",
}
]
export Practice {
// ...
const [currency, setCurrency] = useState(currencyOptions[0].value);
const [network, setNetwork] = useState(networkOptions[0].value)
const handleAmountChange = (value: any) => {
setPaymentAmount(value);
}
const handleCurrencyChange = (event: any) => {
setCurrency(event.target.value);
}
const selectCurrency = (
<Radio.Group value={currency} onChange={handleCurrencyChange} optionType="button" buttonStyle="solid" className="mb-3">
{currencyOptions.map(currencyOption => (<Radio.Button key={currencyOption.value} value={currencyOption.value}>{currencyOption.name}</Radio.Button>))}
</Radio.Group>
)
const selectNetwork = (
<Radio.Group value={network} onChange={event => setNetwork(event.target.value)} optionType="button" buttonStyle="solid" className="mb-3">
{networkOptions.map(currencyOption => (<Radio.Button key={currencyOption.value} value={currencyOption.value}>{currencyOption.value}</Radio.Button>))}
</Radio.Group>
)
return (
<>
{selectCurrency}<br/>
{selectNetwork}<br/>
<CryptoPaymentForm currency={currency} isTestNet={network === "testnet"} isEditableDestinationAddress={true} />
</>)
};
Now, you can send transactions on either Ethereum or Binance Smart Chain using their respective testnet or mainnet. Also, if your users select the wrong network, they’ll receive an error message alerting them of the error and stopping the transaction before it goes through.
That’s it! Just like that we’ve created a fully functional payments platform that you can add to a website to receive real money, using completely open source software and without having to get permission from any third party.
If you found this tutorial helpful, consider giving me a tip in ETH or BNB (tomiwa1a.eth on Ethereum (on BSC)), it helps me make more tutorials like this. You can also send me a tip using the form on this article, I actually made the payment form for the tip, using the same steps I’ve just shown you.
In just a few steps we’ve built a fully functional form that can send and receive real money on any website.
The best part about this is:
If you’ve ever integrated credit card payments into your website before (I have!), you can tell how big of an improvement this is compared to traditional payment methods.
It’s because of these reasons that I’m confident cryptocurrencies will be one of the most positive forces for the future of humanity and I plan to spend the rest of my life, spreading this positive impact with others. WAGMI. We all gonna make it.
In this tutorial, we’ll be learning how to create a multi-chain NFT marketplace like arthouse where you can create an NFT on multiple blockchains such as Ethereum, Polygon and Binance Smart using Solidity, React and Ethers.js. We’ll be wri...
A simple tutorial that explains how to set up your Metamask wallet to Atila!
We’re going to make a crypto wallet that allows you to create accounts, recover accounts, send and receive crypto and view your transactions. Very similar to the popular crypto wallet Metamask but with a few extra features.