paint-brush
Build Your Own Payment Solution that Accepts Cryptoby@induction
123 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. The centralized payment systems have many downsides security challenges, slower payment processing, lack of transparency, excessive human interventions, etc. 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. 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, incorporating a frontend built with React and MetaMask integration for transaction confirmations. We'll also access Rootstock's testnet via dRPC.org.


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 will talk about which files to place in which folders stepwise so there is no need to worry first.

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:

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


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


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


2_deploy_payment.js

const Payment = artifacts.require("Payment");

module.exports = function (deployer) {
    deployer.deploy(Payment);
};


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, // Rootstock Testnet ID
            gas: 6000000,
            gasPrice: 60000000,
            confirmations: 2,
            timeoutBlocks: 500,
            skipDryRun: true,
        }
    },
    compilers: {
        solc: {
            version: '0.8.0',
        }
    }
};


Step 4: React Frontend Development

Please include all the codes in the directory exhibited in the 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);

                // Estimate gas
                const gasEstimate = await contract.methods.buyItem().estimateGas({ from: account, value: price });
                console.log('Gas Estimate:', gasEstimate);

                // Get gas price
                const gasPrice = await web3.eth.getGasPrice();
                console.log('Gas Price:', gasPrice);

                // Send transaction
                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;


4.3 Create CSS styles App.css specific to the App 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'); // Price for the item in RBTC
    const [transactionHash, setTransactionHash] = useState('');

    // Connect to Metamask and get the user's account
    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');
        }
    };

    // Purchase function to interact with the smart contract
    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') // Send price in RBTC
            });

            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;


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, it is recommended you to adjust the lower prices say 0.00001 tRBTC.


Step 5: Run the Project

5.1 Compile and Deploy Contracts

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.


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



Well, 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!