How To Build A Simple Payment System On Ethereum, Binance Smart Chain, And Polygon

How To Build A Simple Payment System On Ethereum, Binance Smart Chain, And Polygon

Introduction

Money makes the world go round; the payment system is the vehicle that carries it; since the beginning, different payment methods have made it easy for people to perform trade or exchange services.

Blockchain technology has provided a solution to the current system, the aspect of blockchain technology that solves this is Decentralized Finance.

This article will not be going in-depth about decentralized finance; it would be focusing on building a simple payment system where users can send and receive Ethereum, BNB, and MATIC.

We would be building the frontend and backend for the payment system, the frontend would be developed using react js, and the backend would be developed using Solidity.

Link to Deployed Applications

Ethereum (Rinkeby) Web App - payment-eth.netlify.app

Binance Smart Chain (Testnet) Web App - payment-bnb.netlify.app

Polygon (Mumbai Testnet) Web App - payment-polygon.netlify.app

GitHub Repositories

Prerequisites

A basic knowledge in Solidity
A basic knowledge in React JS
Make sure Node/NPM is installed.

You can check out my introduction to solidity articles.

A Friendly Beginner's Guide to Solidity (Part One)
A Friendly Beginner's Guide to Solidity (Part Two)

Project Setup and Installation

We would be using hardhat to build our smart contract. Hardhat is an Ethereum development environment and framework that simplifies our solidity development. You can write, deploy, test, and debug your smart contract.

To set up a Hardhat project, Open up a terminal and execute these commands.

mkdir payment-be
cd payment-be
npm init -y
npm install --save-dev hardhat

Create a sample project

npx hardhat
Select Create a basic sample project.
Press enter for the already specified Hardhat Project root
Press enter for the question on if you want to add a .gitignore
Press enter for Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?

The hardhat project is now ready.

If you are not using a mac, please do this extra step and install these libraries.

npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

The following files and folders should be created for you in your root directory:

hardhat.config.js - The Hardhat setup. scripts - This folder contains a script named sample-script.js; it can be used to deploy your smart contract. test - This is the folder that contains the testing script contracts - This is the folder that contains an example Solidity smart contract.

Write Payment Contract Code

We need to create a new file (Payment.sol) in the contracts directory. Here, add the following code:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract Payment {
    // send payment to an address
    function sendPayment(address receiver) public payable {
        require(msg.value > 0, "Insufficient Ether provided");
        (bool success, ) = payable(receiver).call{value: msg.value}("");
        require(success, "Failed to send money");
    }
    // send payment to a the contract
    function sendToContract() public payable {
        require(msg.value > 0, "Insufficient Ether provided");
        (bool success, ) = payable(address(this)).call{value: msg.value}("");
        require(success, "Failed to send money");
    }

    receive() external payable {}
    //get the contract balance
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }
}

The name of the contract above is Payment; it has three functions; the first function is the sendPayment function, the second one is the sendToContract function, and the last one is the getBalance function.

Send Payment Function

The sendToPayment function is used to send payment to another address. The function accepts one parameter called receiver; it has a datatype of address, which means that the function expects an address. The function is a public function, which means anyone can access the function.

The function is also a payable function; only payable functions can receive ether into the contract. Using payable gives us access to msg.value, if we remove the payable, we would not be able to access the msg.value. The msg.value contains the value of ether we are trying to send.

The second line of the sendPayment function uses the require method to check if the ether we are trying to send is greater than zero; if it is less than or equal to zero, we get an error message: "Insufficient Ether provided."

The third line of the sendPayment function is where we use the call method to send the ether to the receiver; the receiver is wrapped with payable so that it can be able to receive the ether. This line returns a bool value depending on the status of the operation; if it is successful, it returns true; if it failed, it returns false. The bool value is used on the require statement on the last line to send an error message if the transaction failed.

Send To Contract

The sendToContract function sends payment to the contract; any amount sent through this function will be stored in the contract. This function has no parameters; it is a public and payable function. The function is very similar to the sendToPayment function; the only difference is that it accepts no parameters; on line 3 of the sendToContract function, address(this) is the way we access the contract address of a contract.

The second line uses the require method to check if the ether we are trying to send is greater than zero and sends an error if the ether is not greater than zero. The third line sends the ether to the contract using the call method and returns true or false depending on the transaction's success.

Get Balance

The getBalance function is a public and view function; it has no parameters and returns the contract balance.

Deploying our code on the blockchain

We will be deploying our code on Rinkeby Testnet (Ethereum), Binance Smart Chain (Testnet), and Polygon (Mumbai Testnet).

Do we need to change any code above for it to work on Binance Smart Chain and Polygon? The answer is NO; we do not need to change anything. Binance Smart Chain and Polygon are layer two solutions.

What is Layer 1?

Layer 1 is the base blockchain. Ethereum and Bitcoin are both layer one blockchains because they are the underlying foundation that various layer two networks build on top of.

What is Layer 2?

A layer 2 is a separate blockchain that extends a Layer 1 like Ethereum. It regularly communicates with Layer 1 (by submitting bundles of transactions) to ensure it has similar security and decentralization guarantees. All this requires no changes to the layer one protocol.

Deploy Code on Ethereum(Rinkeby)

We need to install some packages.

npm install dotenv @nomiclabs/hardhat-etherscan

We need the dotenv so we don't enter confidential details directly into our code, and we use @nomiclabs/hardhat-etherscan for our contract verification.

We need to add .env to our root directory Add the following details.

ALCHEMY_API_KEY_URL=*********************************************
PRIVATE_KEY=********************************************************
ETHERSCAN_API_KEY=************************************************

Check the document below on how to get an alchemy API KEY URL for Rinkeby https://docs.alchemy.com/alchemy/introduction/getting-started

Check the document below on how to get a private key from metamask https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key

To get an etherscan API key, go to etherscan.io, create an account, log in to the account, hover on your username, click on API keys and create a new API key.

We need to update our hardhat.config.js

require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

const { ALCHEMY_API_KEY_URL, PRIVATE_KEY, ETHERSCAN_API_KEY } =
  process.env;

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    rinkeby: {
      url: ALCHEMY_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
};

We need to create deploy.js in our scripts directory and add the following code.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');
  // We get the contract to deploy

  //deploying the payment contract
  const Payment = await hre.ethers.getContractFactory("Payment");
  const payment = await Payment.deploy();

  await payment.deployed();

  console.log("Payment deployed to:", payment.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy on command line.

npx hardhat run scripts/deploy.js --network rinkeby
Payment deployed to: 0xE68C05dD79bF80f958AD4D453142b9Ad099b825B

Verify contract on command line.

The sample command

npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1"

Lets verify the contract we deployed above.

npx hardhat verify --network rinkeby 0xE68C05dD79bF80f958AD4D453142b9Ad099b825B

Nothing to compile
Successfully submitted source code for contract
contracts/Payment.sol:Payment at 0xE68C05dD79bF80f958AD4D453142b9Ad099b825B
for verification on the block explorer. Waiting for verification result...

Successfully verified contract Payment on Etherscan.
https://rinkeby.etherscan.io/address/0xE68C05dD79bF80f958AD4D453142b9Ad099b825B#code

Deploy Code on Binance Smart Chain (Testnet)

We need to update our .env file. We need to add the API_KEY_URL for BSC and the BSC_API_KEY

Add the following details.

ALCHEMY_API_KEY_URL=*********************************************
PRIVATE_KEY=********************************************************
ETHERSCAN_API_KEY=************************************************

BSC_API_KEY_URL=https://data-seed-prebsc-1-s1.binance.org:8545
BSC_API_KEY=****************************************************

Check the document below on how to get a private key from metamask https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key

To get a bscscan API key, go to bscscan.com, create an account, log in to the account, hover on your username, click on API keys and create a new API key.

We need to update our hardhat.config.js

require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

const { ALCHEMY_API_KEY_URL, BSC_API_KEY_URL, PRIVATE_KEY, BSC_API_KEY } =
  process.env;

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    rinkeby: {
      url: ALCHEMY_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
     bsc: {
      url: BSC_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: BSC_API_KEY,
  },
};

We need to create deploy.js in our scripts directory and add the following code.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');
  // We get the contract to deploy

  //deploying the payment contract
  const Payment = await hre.ethers.getContractFactory("Payment");
  const payment = await Payment.deploy();

  await payment.deployed();

  console.log("Payment deployed to:", payment.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy on command line.

npx hardhat run scripts/deploy.js --network bsc
Payment deployed to: 0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3

Verify contract on command line.

The sample command

npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1"

Lets verify the contract we deployed above.

npx hardhat verify --network bsc 0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3

Nothing to compile
Successfully submitted source code for contract
contracts/Payment.sol:Payment at 0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3
for verification on the block explorer. Waiting for verification result...

Successfully verified contract Payment on Etherscan.
https://testnet.bscscan.com/address/0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3#code

Deploy Code on Polygon (Mumbai Testnet)

We need to update our .env file. We need to add the API_KEY_URL for POLYGON and the POLYGON_API_KEY

Add the following details.

ALCHEMY_API_KEY_URL=*********************************************
PRIVATE_KEY=********************************************************
ETHERSCAN_API_KEY=************************************************

BSC_API_KEY_URL=https://data-seed-prebsc-1-s1.binance.org:8545
BSC_API_KEY=****************************************************

POLYGON_API_KEY_URL=https://speedy-nodes-nyc.moralis.io/40a88f8745bc01d3bb660792/polygon/mumbai
POLYGON_API_KEY=****************************************************

Check the document below on how to get a private key from metamask https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key

To get a polygon API key, go to bscscan.com, create an account, log in to the account, hover on your username, click on API keys and create a new API key.

We need to update our hardhat.config.js

require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

const { ALCHEMY_API_KEY_URL, BSC_API_KEY_URL, POLYGON_API_KEY_URL, PRIVATE_KEY, POLYGON_API_KEY } =
  process.env;

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    rinkeby: {
      url: ALCHEMY_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
     bsc: {
      url: BSC_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
    polygon: {
      url: POLYGON_API_KEY_URL,
      accounts: [PRIVATE_KEY],
    },
  },
  etherscan: {
    apiKey: POLYGON_API_KEY,
  },
};

We need to create deploy.js in our scripts directory and add the following code.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');
  // We get the contract to deploy

  //deploying the payment contract
  const Payment = await hre.ethers.getContractFactory("Payment");
  const payment = await Payment.deploy();

  await payment.deployed();

  console.log("Payment deployed to:", payment.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy on command line.

npx hardhat run scripts/deploy.js --network polygon
Payment deployed to: 0xdC16A298b7562DECb67A026a3a9D4c254913e65A

Verify contract on command line.

The sample command

npx hardhat verify --network mainnet DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1"

Lets verify the contract we deployed above.

npx hardhat verify --network polygon 0xdC16A298b7562DECb67A026a3a9D4c254913e65A

Nothing to compile
Successfully submitted source code for contract
contracts/Payment.sol:Payment at 0xdC16A298b7562DECb67A026a3a9D4c254913e65A
for verification on the block explorer. Waiting for verification result...

Successfully verified contract Payment on Etherscan.
https://mumbai.polygonscan.com/address/0xdC16A298b7562DECb67A026a3a9D4c254913e65A#code

Contracts Deployed and Verified

RINKEBY - 0xE68C05dD79bF80f958AD4D453142b9Ad099b825B
BSC - 0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3
POLYGON - 0xdC16A298b7562DECb67A026a3a9D4c254913e65A

Building Frontend React Client

To get started with the project setup and installation, we will clone this project on GitHub.

git clone https://github.com/Josh4324/Payment-FE.git

We will cd into the Payment-FE directory, install and start the application.

cd Payment-FE && npm install && npm start

We should have something close to the picture below on desktop view.

cur.png

We can observe in the cloned repo that the react app has been setup, to make the demo simple, we are using just one component (Home.js), which is located in the pages directory.

We have also created a payment.json file inside the utils folder, it contains our abi, which we will use to communicate with the smart contract.

We will be starting with the imports statement

import React, { useEffect, useState, useRef } from "react";
import { BigNumber, ethers, providers } from "ethers";
import abi from "../utils/payment.json";
import Web3Modal from "web3modal";
import WalletConnectProvider from "@walletconnect/web3-provider";
import axios from "axios";
import Web3 from "web3";

We are familiar withe useEffect, useState and useRef, we will be using the ethers.js library to interact with our contract, the abi is imported from the payment.json file, we will need the Web3Modal to have access to multiple wallets, instead of just metamask, walletConnect is one the wallet we will be able to access inside the web3modal.

Change Contract Address

// This is our deployed and verified rinkeby contract
 const contractAddress = "0xE68C05dD79bF80f958AD4D453142b9Ad099b825B";

Connect and Disconnect Wallet

We already have a connect wallet function but it is empty at the moment, lets fix that. We will need to write our getProviderOrSigner function first, since we will need it inside our connect wallet.

we added some variables, added information for the provider options needed inside the web3modal, inside the getProviderOrSigner, we check if the ethereum wallet exist, configure the web3modal, get the current wallet address, check the chain id to make sure the user is on the right network, if not we prompt the user to change the network, then we prompt the user in order to get the signer.

On connect wallet, we are using the provider to get the balance of the wallet address

Disconnect Wallet

Currently we should be able to able to connect and disconnect our wallet, we also added a useEffect that will be triggered anytime the wallet is disconnected.

Get Contract Balance and Get Price

Currently, we should be able to see our address balance, contract balance and their dollar equivalent.

Send Ether to another address and Send Ether to Contract

We should be able to successfully send ether to another address and send ether to the contract, the sendEthToAnotherAddr function can perform the action of the second function.

Sending BNB

In order to update the Frontend to send bnb instead of ether, first thing we need to do is change the contract address

// This is our deployed and verified BSC contract
 const contractAddress = "0x0f9ae0b2A73F0812010B9f96EE0767fC5d84e5d3";

Update our getProviderOrSigner function.

Update getPrice function

 const getPrice = async () => {
    const data = await axios.get(
      "https://api.coingecko.com/api/v3/simple/price?ids=binancecoin&vs_currencies=usd"
    );

    setPrice(data.data.binancecoin.usd);
  };

Change every mention of ether to BNB.

You should be able to successfully send bnb on the web application.

Sending MATIC

In order to update the Frontend to send bnb instead of ether, first thing we need to do is change the contract address

// This is our deployed and verified POLYGON contract
 const contractAddress = "0xdC16A298b7562DECb67A026a3a9D4c254913e65A";

Update our getProviderOrSigner function.

Update getPrice function

  const getPrice = async () => {
    const data = await axios.get(
      "https://api.coingecko.com/api/v3/simple/price?ids=matic-network&vs_currencies=usd"
    );

    setPrice(data.data["matic-network"].usd);
  };

Change every mention of ether or bnb on the web app to MATIC.

You should be able to successfully send matic on the web application.

Conclusion

We were able to build the backend and frontend for a payment system, where users can send ether, bnb and matic.

The complete Front End code (Ethereum Rinkeby) can be found here on the complete branch.

The complete Front End code (BSC Testnet ) can be found here on the bnb branch.

The complete Front End code (Polygon Mumbai Testnet ) can be found here on the polygon branch.

I hope you found this tutorial useful.

Thank you for reading.

References

WIKIPEDIA

ETHEREUM.ORG

SOLIDITY BY EXAMPLE