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.

https://i.imgur.com/hJWWx0A.png

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.

https://i.imgur.com/1HhQ6EF.png

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.

Start with a Sample React Project

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

Add a Basic Form

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

Setting the Amount and Destination Address

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

Add Ethers.js

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.

https://i.imgur.com/1HhQ6EF.png

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});

	}

}

Explaining the Payment Steps

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.

Getting "Play Money"

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.

  1. Visit https://faucet.egorfine.com/. (Sometimes it can be hard to find a faucet that works reliably. So you can use a search engine for "ropsten faucet" and try different ones until you find one that works.
  2. Enter the address of the wallet you are going to be sending the ETH from, not the address that will be receiving the ETH..

Sending the first ETH transaction

  1. Make sure you switch to the Ropsten test network in Metamask
  2. Enter a desired amount and a wallet address that you want to send the ETH to. If you don't have one, make a new one in Metamask.
  3. Send the Payment

Showing Transaction Status

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:

https://i.imgur.com/OVynwW5.png

Practice confirming and rejecting the transaction to see what the success and error messages look like.

Viewing the Transaction in a Block Explorer

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}`

}

Ethereum Payment Complete

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.

Bonus: Adding Binance Coin (BNB) Payments

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.

Add Binance Smart Chain to Metamask

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

Screen Shot 2022-01-17 at 7.47.49 AM.png

Troubleshooting the Testnet

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:

  1. Wait for a bit. I tried to use the testnet on BSC and some of the steps below worked for a bit then stopped working. Then I tried again on Saturday morning and then everything worked normally. So I’m actually not sure if the fix is what I tried below or just waiting for the network issues to go away. Sharing the following tips in case others find it helpful.
  2. changing the RPC URL, here’s a list of Binance Smart Chain RPC URLs
    1. This should be seen as a temporary fix, the primary URL: https://data-seed-prebsc-1-s1.binance.org:8545/ is the most reliable one, so if the secondary ones start working, try to witch batch to the primary URL
  3. Set the chain ID in Hex (e.g. set 0x61 instead of 97)
  4. Change your browser

Getting Binance Smart Chain “Play Money”

  1. Go to the Binance Faucet and follow the same steps you used in the Rposten faucet

Add Binance Network to Ethers.js

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.

Add ability to switch Between Different Networks

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.

https://i.imgur.com/QJy9h04.png

https://i.imgur.com/t1rrPuH.png

https://i.imgur.com/oSd34ju.png

Conclusion

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:

  • You don’t need to set up a bank account
  • Your users don’t need to set up a bank account
  • Anyone in the world can access the payments on your website
  • You don’t have to get permission from anyone and no individual or group can censor you from receiving payments
  • Finally, and most importantly this was all done completely for free using open source software!

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.