paint-brush
Build Your Own Payment Solution that Accepts Cryptoby@induction
254 reads

Build Your Own Payment Solution that Accepts Crypto

by Vision NPSeptember 12th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Blockchain has transformed many aspects of our real life. centralized payment systems have many downsides security challenges, slower payment processing, lack of transparency, excessive human interventions, etc. In this tutorial, I will guide you through building a decentralized payment solution on the Rootstock Testnet. It will show you a real-world use case for blockchain.
featured image - Build Your Own Payment Solution that Accepts Crypto
Vision NP HackerNoon profile picture

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.


📥Project Overview

The goal is to create a simple web app where users can:

  • View a list of items available for purchase
  • Purchase items using MetaMask
  • Verify transactions on the Rootstock Testnet


👉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:

Figure:1 Project's Directory


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:

  • Node.js (v20.17.0 or later)
  • Truffle (for smart contract development)
  • MetaMask (browser extension)


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>&copy; 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:

Figure:2 Message in terminal upon a successful truffle compilation


Figure:3 Exhibition of a successful contracts deployment in RSK testnet


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.

Figure:4 React app is calling MetaMask to confirm the transaction


Once the transaction is confirmed, you might see the confirmation message as shown in the following figure:

Figure :5 Message exhibited after the purchase completed along with confirmed transaction notification



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.

📥Conclusion

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!