Blockchain has transformed many aspects of our real life. Centralized payment systems have many downsides such as security challenges, slower payment processing, lack of transparency, and excessive human intervention. It doesn’t mean that blockchain-based systems are 100% immune to these challenges but they solve most of the notorious downsides of centralized payment systems. This tutorial will guide you through building a decentralized payment solution on the Rootstock Testnet. We utilize blockchain’s potential to back payment systems. By the end of this guide, you will have a fully functioning dApp with MetaMask integration and Rootstock Testnet deployment.
It will also show you a real-world use case for blockchain, incorporating a frontend built with React and MetaMask integration for transaction confirmations. We'll also access Rootstock's testnet via dRPC.org.The entire development process is straightforward as this tutorial guides you from the beginning.
The goal is to create a simple web app where users can:
👉Step 1: Project Setup
1.1 Directory Structure
First, please create a project directory as shown in the following figure. We’ll walk you through each step as we’ll explain where to place files, so no need to worry about the folder structure upfront.
We will structure the project as follows:
1.2 Install Prerequisites
Before starting a development process, install the major prerequisites first. We will also talk about the necessary package installation later (whenever required). So, ensure you have the following installed first:
To start the process, run the following commands in the terminal:
npm install -g truffle
npx create-react-app client
cd client
npm install web3 @truffle/hdwallet-provider
1.3 Create .env
file to store your sensitive details as following structure:
mnemonic="your metamask wallet's mnemonic phrases"
RSKTestURL="https://lb.drpc.org/ogrpc?network=rootstock-testnet&dkey=APIKEY"
Please note that you can get your wallet’s secret phrase from your MetaMask wallet and dRPC endpoint from dRPC.org. Just create a free account on the site get an endpoint for the RSK testnet for free and update your .env
file.
👉Step 2: Write Smart Contracts
2.1 Payment Contract (contracts/Payment.sol
)
Create a smart contract to handle payments.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Payment {
address public owner;
constructor() {
owner = msg.sender;
}
function buyItem() public payable {
require(msg.value > 0, "Send some tRBTC to purchase the item");
payable(owner).transfer(msg.value);
}
}
This buyItem
function allows a user to send tRBTC to the contract when purchasing an item. The require
statement ensures that the buyer sends a non-zero amount, and the contract then transfers that amount to the contract owner.
Create another solidity-based smart contract Migrations.sol
to place in the same directory:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() {
owner = msg.sender;
}
modifier restricted() {
require(msg.sender == owner, "This function is restricted to the contract's owner");
_;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}
Before deploying the contract, you can test it on Remix—Ethereum IDE. Open Remix, paste your smart contract code and simulate purchases using test tRBTC. If you face errors such as ‘Gas limit exceeded,’ check the gas estimation in your Truffle config or Remix settings.
2.2 Migrations
Here we need to have two migration scripts that need to be placed in the migrations
folder of your project’s root directory.
1_initial_migration.js
const Migrations = artifacts.require("Migrations");
module.exports = function (deployer) {
deployer.deploy(Migrations);
};
Here, the code first loads the Migrations
contract using artifacts.require()
and then uses the deployer
object to deploy it to the blockchain.
2_deploy_payment.js
const Payment = artifacts.require("Payment");
module.exports = function (deployer) {
deployer.deploy(Payment);
};
This script is responsible for deploying the Payment
smart contract. It loads the Payment
contract using artifacts.require()
and deploys it to the blockchain using the deployer object.
👉Step 3: Configuring Truffle for Rootstock
3.1 Create Truffle Configuration Code
In your truffle-config.js
, add the Rootstock testnet configuration as follows:
const HDWalletProvider = require('@truffle/hdwallet-provider');
require('dotenv').config();
const mnemonic = process.env.mnemonic;
const rskRpcUrl = process.env.RSKTestURL;
module.exports = {
networks: {
rsk_testnet: {
provider: () => new HDWalletProvider({
mnemonic: {
phrase: mnemonic,
},
providerOrUrl: rskRpcUrl,
}),
network_id: 31,
gas: 6000000,
gasPrice: 60000000,
confirmations: 2,
timeoutBlocks: 500,
skipDryRun: true,
}
},
compilers: {
solc: {
version: '0.8.0',
}
}
};
Here, “RSKTestURL” is used to import dRPC endpoint from the .env file, make sure you have followed the .env file structure correctly. Avoid having unnecessary spaces.
👉Step 4: React Frontend Development
Please include all the codes in the directory exhibited in Figure: 1
4.1 Create the React App
Navigate to the client directory:
npx create-react-app client
cd client
npm install web3
4.2 MetaMask Integration
Please, care to install the following packages demanded by App.js
as:
npm install react react-dom web3 react-bootstrap bootstrap dotenv
In your App.js
, connect MetaMask, and allow users to make purchases.
import React, { useEffect, useState } from 'react';
import Web3 from 'web3';
import { Button, Card, Container, Row, Col, Alert } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import Payment from './contracts/Payment.json';
const RSKTestURL = process.env.RSKTestURL;
const App = () => {
const [web3, setWeb3] = useState(null);
const [account, setAccount] = useState(null);
const [contract, setContract] = useState(null);
const [error, setError] = useState(null);
const items = [
{ id: 1, name: 'Camera', price: '0.000001 tRBTC', image: 'camera-431119_1920.jpg' },
{ id: 2, name: 'Laptop', price: '0.000001 tRBTC', image: 'laptop-1205256_1920.jpg' },
{ id: 3, name: 'Pendrive', price: '0.000001 tRBTC', image: 'pendrive-183146_1280.jpg' },
{ id: 4, name: 'Drone', price: '0.000001 tRBTC', image: 'technology-7061138_1920.jpg' },
{ id: 5, name: 'Sunglasses', price: '0.000001 tRBTC', image: 'wood-sunglasses-2500488_1920.jpg' },
{ id: 6, name: 'Headset', price: '0.000001 tRBTC', image: 'headphones-814055_1920.jpg' }
];
useEffect(() => {
const initWeb3 = async () => {
if (window.ethereum) {
try {
const web3 = new Web3(window.ethereum);
setWeb3(web3);
await window.ethereum.request({ method: 'eth_requestAccounts' });
const accounts = await web3.eth.getAccounts();
setAccount(accounts[0]);
const networkId = await web3.eth.net.getId();
const networkData = Payment.networks[networkId];
if (networkData) {
const contract = new web3.eth.Contract(Payment.abi, networkData.address);
setContract(contract);
} else {
setError('Smart contract not deployed to detected network.');
}
} catch (err) {
setError(err.message);
}
} else {
setError('Please install MetaMask!');
}
};
initWeb3();
}, []);
const handlePurchase = async (itemId) => {
try {
if (contract) {
const price = web3.utils.toWei('0.000001', 'ether');
console.log('Price:', price);
console.log('Account:', account);
const gasEstimate = await contract.methods.buyItem().estimateGas({ from: account, value: price });
console.log('Gas Estimate:', gasEstimate);
const gasPrice = await web3.eth.getGasPrice();
console.log('Gas Price:', gasPrice);
await contract.methods.buyItem().send({
from: account,
value: price,
gas: gasEstimate,
gasPrice: gasPrice
});
alert('Purchase successful!');
} else {
setError('Contract is not loaded.');
}
} catch (err) {
console.error('Transaction Error:', err);
setError('Purchase failed: ' + err.message);
}
};
return (
<Container>
<header className="App-header">
<h1>Purchase Items</h1>
</header>
{error && <Alert variant="danger">{error}</Alert>}
<Row>
{items.map(item => (
<Col md={4} key={item.id} className="mb-4">
<Card className="card">
<Card.Img variant="top" src={`/images/${item.image}`} alt={item.name} />
<Card.Body>
<Card.Title className="card-title">{item.name}</Card.Title>
<Card.Text className="card-content">Price: {item.price}</Card.Text>
<Button
variant="primary"
onClick={() => handlePurchase(item.id)}
>
Purchase
</Button>
</Card.Body>
</Card>
</Col>
))}
</Row>
<footer className="footer">
<p>© 2024 Your Company</p>
</footer>
</Container>
);
};
export default App;
The code can handle several errors which could assist you in tracking the logs in the browser’s console for effective debugging.
4.3 Create CSS styles App.css
specific to the dApp component that controls its visual appearance.
/* Reset some default browser styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
color: #333;
line-height: 1.6;
}
.App {
text-align: center;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
padding: 20px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 20px;
}
.card {
background-color: white;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 20px 0;
}
.card-title {
font-size: 1.5rem;
margin-bottom: 10px;
}
.card-content {
font-size: 1rem;
}
button {
background-color: #61dafb;
border: none;
color: white;
padding: 10px 20px;
font-size: 1rem;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #4fa3b2;
}
button:focus {
outline: none;
}
.footer {
background-color: #282c34;
color: white;
padding: 20px;
text-align: center;
}
4.4 Create index.js
as an entry point for the React app which is responsible for rendering the App component
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
4.5 Create \client\src\components
directory and in components
folder, place the following JavaScript codes:
ItemCard.js
function ItemCard({ item }) {
return (
<div className="item-card">
<img src={item.image} alt={item.name} />
<h2>{item.name}</h2>
<p>{item.price} RBTC</p>
</div>
);
}
export default ItemCard;
Purchase.js
import React, { useState } from 'react';
import Web3 from 'web3';
const Purchase = ({ contractAddress, abi }) => {
const [account, setAccount] = useState('');
const [price, setPrice] = useState('0.01');
const [transactionHash, setTransactionHash] = useState('');
const connectWallet = async () => {
if (window.ethereum) {
const web3 = new Web3(window.ethereum);
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setAccount(accounts[0]);
} catch (error) {
console.error('Error connecting to Metamask:', error);
}
} else {
alert('Please install Metamask');
}
};
const purchaseItem = async () => {
if (!account) {
alert('Please connect your wallet first.');
return;
}
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(abi, contractAddress);
try {
const tx = await contract.methods.purchase().send({
from: account,
value: web3.utils.toWei(price, 'ether')
});
setTransactionHash(tx.transactionHash);
alert('Transaction successful! Hash: ' + tx.transactionHash);
} catch (error) {
console.error('Transaction failed:', error);
alert('Transaction failed');
}
};
return (
<div>
<h1>Purchase an Item</h1>
{account ? (
<p>Connected account: {account}</p>
) : (
<button onClick={connectWallet}>Connect Metamask</button>
)}
<div>
<label>
Price (in RBTC):
<input
type="text"
value={price}
onChange={(e) => setPrice(e.target.value)}
/>
</label>
</div>
<button onClick={purchaseItem}>Purchase Item</button>
{transactionHash && (
<p>Transaction Hash: <a href={`https://explorer.testnet.rsk.co/tx/${transactionHash}`} target="_blank" rel="noopener noreferrer">{transactionHash}</a></p>
)}
</div>
);
};
export default Purchase;
The Purchase
component enables users to connect their MetaMask wallet, set a price, and initiate a blockchain transaction to purchase an item in dApp. It uses Web3 to interact with a smart contract which handles wallet connection and transaction execution processes.
The connectWallet
function connects to MetaMask and retrieves the user's wallet address.The purchaseItem
function sends a purchase transaction with the specified price to the smart contract. The transaction hash is displayed upon success.
4.6 Create \client-app\public\images
directory and in the images
folder, add all images of the products you wish to list in the platform, and also make sure it matches the code mentioned in the App.js
as:
const items = [
{ id: 1, name: 'Camera', price: '0.000001 tRBTC', image: 'camera-431119_1920.jpg' },
{ id: 2, name: 'Laptop', price: '0.000001 tRBTC', image: 'laptop-1205256_1920.jpg' },
{ id: 3, name: 'Pendrive', price: '0.000001 tRBTC', image: 'pendrive-183146_1280.jpg' },
{ id: 4, name: 'Drone', price: '0.000001 tRBTC', image: 'technology-7061138_1920.jpg' },
{ id: 5, name: 'Sunglasses', price: '0.000001 tRBTC', image: 'wood-sunglasses-2500488_1920.jpg' },
{ id: 6, name: 'Headset', price: '0.000001 tRBTC', image: 'headphones-814055_1920.jpg' }
Note: For testing purposes, you should adjust the lower prices, say 0.00001 tRBTC.
👉Step 5: Run the Project
5.1 Compile and Deploy Contracts
Once you've tested the contract locally and are satisfied with the results, you'll want to deploy it to the Rootstock testnet. First, make sure you have a certain amount of tRBTC in your wallet before running the following codes in the terminal. You can get free tRBTC from here. You can also monitor transactions using the Rootstock Block Explorer.
Run the following command in the terminal:
truffle compile
truffle migrate --network rsk_testnet
If everything goes right, you can see the following output in the terminal:
Note: After truffle compile, please note Payment.json
in build\contracts
directory. Copy the Payment.json
file and paste it to
client\src\contracts
5.2 Run the React App
Start the React app:
cd client
npm start
Visit http://localhost:3000 to see your front end in action.
The react app should call the MetaMask to switch the network. If not, please manually add network on your MetaMask wallet as guided in this HackerNoon article.
Once you click on the “Purchase” button, the app calls the MetaMask to confirm the transaction. Check for the confirmation.
Once the transaction is confirmed, you might see the confirmation message as shown in the following figure:
Congratulations! You’ve successfully built a decentralized payment system on Rootstock Testnet. As a next step, you can extend this project by allowing users to select multiple items, track their purchase history, or integrate with other blockchain networks like Ethereum.
These are all about testing our decentralized payment solution system in Rootstock’s testnet. It will be incomplete if there is no clue about how to run your platform in Rootstock’s mainnet by using Rootstock’s mainnet endpoint from dRPC.org. Once you can test your platform in the testnet, it is not complicated for the mainnet’s deployment. Make sure to fund your wallet with RBTC. Add Rootstock’s mainnet endpoint to your .env
file and make adjustments to our truffle-config.js
by preferring the project’s documentation, you’re done.
This tutorial shows that building your own crypto payment platform isn't just for tech wizards anymore (I attempted my best to write everything in simple language to break down complex concepts in a simple way). Whether you're a seasoned coder or someone who still thinks "blockchain" is a fancy way to play with Legos, you now have the tools to create a crypto payment solution with ease. But, at the same time, it is recommended to check the project’s documentation and guides mentioned in this tutorial. This is a real-world example of how blockchain technology can empower anyone to take control of their finances—no magic wand or Silicon Valley garage required 😊. So, roll up your sleeves, follow the steps, and before you know it, you'll be accepting crypto like a pro!