paint-brush
Build a Receipt Generator with Just the Rootstock API and RPC Methodby@ileolami

Build a Receipt Generator with Just the Rootstock API and RPC Method

by IleolamiDecember 1st, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Receipts are important for validating transactions and providing proof of purchase. This article will show you how to create a simple yet effective receipt generator using the Rootstock API and a single RPC (Remote Procedure Call) method.
featured image - Build a Receipt Generator with Just the Rootstock API and RPC Method
Ileolami HackerNoon profile picture

In today’s world, receipts are crucial for validating transactions and keeping proof of purchases. Whether it’s a big bank or a small roadside shop, receipts help businesses and individuals stay organized and track their spending.


But here’s the thing: most dApps don’t provide receipts and rely on explorers to verify transaction details. What if you didn’t have to rely on that? Imagine how much more convenient it would be for your users to generate receipts directly, without needing to check their wallets.


If you’re building a payment-based dApp on Rootstock, this article will show you how to create a simple yet effective receipt generator using the Rootstock API and just one RPC (Remote Procedure Call) method. This approach makes the process easier and ensures your transaction records are accurate and easy to access.


Let's learn the steps and tools needed to create a smooth receipt generation experience.


Prerequisites

  1. You must have node installed on your device
  2. knowledge in Javascript
  3. Installed Js framework of your choice
  4. Code Editor e.g., VScode

I will be using React Typescript and TailwindCSS for styling

Tool and Technologies

  1. Rootstock API Key
  2. Web3js: to interact with RPC
  3. QRCode React: to generate a QR code for users to scan and get their receipt
  4. Jspdf: to generate the receipt into PDFs

Install, Import the Packages and Create the functional component

  1. Install all the dependencies using this command :

    npm i web3js jspdf qrcode.react
    
  2. Create a new file or use the App.jsx

  3. Import the packages into the file with the following:

    import { useState } from "react";
    import Web3 from "web3";
    import { jsPDF } from "jspdf";
    import { QRCodeSVG } from "qrcode.react";
    
  4. Initialize the functional component

    const TransactionReceipt = () => {
    /......./
    }
    export default TransactionReceipt;
    


State Management and Web3 Intilaiztion

The code snippet here will manage the state and functionality for fetching and displaying transaction details using useState hook, Web3js, Rootstock RPC and API.

  const [transactionId, setTransactionId] = useState("");
  interface TransactionDetails {
    transactionHash: string;
    from: string;
    to: string;
    cumulativeGasUsed: number;
    blockNumber: number;
    contractAddress?: string;
  }

  const [transactionDetails, setTransactionDetails] =
    useState<TransactionDetails | null>(null);
  const [error, setError] = useState("");

  const web3 = new Web3(
    `https://rpc.testnet.rootstock.io/${import.meta.env.VITE_API_KEY}`
  );
  1. State Management:

    • const [transactionId, setTransactionId] = useState("");: This line initializes a state variable transactionId . This state variable will be responsible for the inputted transaction hash which other variables and functions will leverage to generate the receipt.
    • const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);: This line initializes a state variable transactionDetails with a null value and provides a function setTransactionDetails to update its value. The state can hold either a TransactionDetails object or null.
    • const [error, setError] = useState("");: This line initializes a state variable error with an empty string and provides a function setError to update its value.
  2. TypeScript Interface:

    • interface TransactionDetails: This defines a TypeScript interface for the structure of the transaction details object. It includes properties like transactionHashfromtocumulativeGasUsedblockNumber, and an optional contractAddress.
  3. Web3 Initialization:

    • const web3 = new Web3(https://rpc.testnet.rootstock.io/${import.meta.env.VITE_API_KEY});: This line initializes Web3js, connecting to the RPC endpoint to the Rootstock testnet. The endpoint URL includes an API key that is retrieved from environment variables using import.meta.env.VITE_API_KEY.


Function to Fetch the Transaction Details

The code here will fetch the transaction details using an asynchronous function from Rootstock with the web3.js method.

const fetchTransactionDetails = async () => {
    try {
      setError("");
      setTransactionDetails(null);

      const receipt = await web3.eth.getTransactionReceipt(transactionId);

      if (!receipt) {
        throw new Error("Transaction not found!");
      }

      setTransactionDetails(receipt);
    } catch (err) {
      if (err instanceof Error) {
        setError(err.message);
      } else {
        setError("An unknown error occurred");
      }
    }
  };
  1. Error Handling Setup:
    • try { ... } catch (err) { ... }: This structure is used to handle any errors that might occur during the execution of the function.
  2. Reset State:
    • setError("");: This clears any previous error messages by setting the error state to an empty string.
    • setTransactionDetails(null);: This clears any previous transaction details by setting the transactionDetailsstate to null.
  3. Fetch Transaction Receipt:
    • const receipt = await web3.eth.getTransactionReceipt(transactionId);: This line uses the web3js method to fetch the transaction receipt for the inputted transactionId.
  4. Check for Receipt:
    • if (!receipt) { throw new Error("Transaction not found!"); }: If the receipt is not found (i.e., receipt is null or undefined), an error is thrown with the message "Transaction not found!".
  5. Set Transaction Details:
    • setTransactionDetails(receipt): If the receipt is found, it updates the transactionDetails state with the fetched receipt.
  6. Error Handling:
    • catch (err) { ... }: This block catches any errors that occur during the execution of the try block.
    • if (err instanceof Error) { setError(err.message); } else { setError("An unknown error occurred"); }: If the caught error is an instance of the Error class, it sets the error state to the error's message. Otherwise, it sets the error state to a generic error message "An unknown error occurred".


Functions to Generate Receipt PDF

Here the Jspdf package will be used to to generate the PDF containing the transaction details.

const generatePDF = () => {
    if (!transactionDetails) return;

    const {
      transactionHash,
      from,
      to,
      cumulativeGasUsed,
      blockNumber,
      contractAddress,
    } = transactionDetails;
    const pdf = new jsPDF();

    pdf.setFontSize(16);
    pdf.text("Transaction Receipt", 10, 10);

    pdf.setFontSize(12);
    pdf.text(`Transaction Hash: ${transactionHash}`, 10, 20);
    pdf.text(`From: ${from}`, 10, 30);
    pdf.text(`Contract Address: ${contractAddress}`, 10, 40);
    pdf.text(`To: ${to}`, 10, 40);
    pdf.text(`Cumulative Gas Used: ${cumulativeGasUsed}`, 10, 50);
    pdf.text(`Block Number: ${blockNumber}`, 10, 60);

    pdf.save("Transaction_Receipt.pdf");
  };
  1. Check for Transaction Details:
    • if (!transactionDetails) return;: This checks if transactionDetails is null or undefined. If it is, the function returns early and does nothing.


  2. Destructure Transaction Details:
    • const { transactionHash, from, to, cumulativeGasUsed, blockNumber, contractAddress } = transactionDetails;: This destructures the transactionDetails object to extract individual properties for easier access.


  3. Create PDF Document:
    • const pdf = new jsPDF(): This creates a new instance of the jsPDF class, which represents a PDF document.


  4. Set Font Size and Add Title:
    • pdf.setFontSize(16): This sets the font size of the heading to 16.

    • pdf.text("Transaction Receipt", 10, 10);: This adds the title "Transaction Receipt" at coordinates (10, 10) in the PDF document.


  5. Add Transaction Details to PDF:
    • pdf.setFontSize(12);: This sets the font size to 12 for the rest of the text.

    • pdf.text(Transaction Hash: ${transactionHash}, 10, 20);: This adds the transaction hash at coordinates (10, 20).

    • pdf.text(From: ${from}, 10, 30);: This adds the sender address at coordinates (10, 30).

    • pdf.text(Contract Address: ${contractAddress}, 10, 40);: This adds the contract address at coordinates (10, 40). Note: This line should be corrected to avoid overlapping text.

    • pdf.text(To: ${to}, 10, 50);: This adds the recipient address at coordinates (10, 50).

    • pdf.text(Cumulative Gas Used: ${cumulativeGasUsed}, 10, 60);: This adds the cumulative gas used at coordinates (10, 60).

    • pdf.text(Block Number: ${blockNumber}, 10, 70);: This adds the block number at coordinates (10, 70).


  6. Save PDF Document:
    • pdf.save("Transaction_Receipt.pdf");: This will saves the PDF document with the filename "Transaction_Receipt.pdf" .

The User Interface

Here you will be rendering those functional components as a UI to the users.

This code has already included the styling using Tailwindcss

  return (
    <div className="p-8 font-sans bg-gray-100 min-h-screen">
      <div className="max-w-3xl m-auto bg-white p-6 rounded-lg shadow-lg">
        <h1 className="text-3xl font-bold mb-6 text-center text-blue-600">
          Transaction Receipt Generator
        </h1>

        <div className="mb-6">
          <div className="flex">
            <input
              type="text"
              id="transactionId"
              value={transactionId}
              onChange={(e) => setTransactionId(e.target.value)}
              placeholder="Enter transaction hash"
              className="border p-2 w-full rounded-l-lg"
            />
            <button
              onClick={fetchTransactionDetails}
              className="p-2 bg-blue-500 text-white rounded-r-lg"
            >
              Fetch Details
            </button>
          </div>
        </div>

        {error && (
          <p className="text-red-500 mt-4 text-center">Error: {error}</p>
        )}

        {transactionDetails && (
          <div className="mt-6 flex flex-row gap-8">
            <div className="w-2/3">
              <h2 className="text-2xl font-semibold mb-4 text-center">
                Transaction Details
              </h2>
              <div className="bg-gray-50 p-4 rounded-lg shadow-inner w-[460px]">
                <p>
                  <strong>Transaction Hash:</strong>{" "}
                  {`${transactionDetails.transactionHash.slice(
                    0,
                    6
                  )}...${transactionDetails.transactionHash.slice(-6)}`}
                </p>
                <p>
                  <strong>From:</strong> {transactionDetails.from}
                </p>
                <p>
                  <strong>Contract Address:</strong>{" "}
                  {transactionDetails.contractAddress}
                </p>
                <p>
                  <strong>To:</strong> {transactionDetails.to}
                </p>
                <p>
                  <strong>Cumulative Gas Used:</strong>{" "}
                  {transactionDetails.cumulativeGasUsed.toString()}
                </p>
                <p>
                  <strong>Block Number:</strong>{" "}
                  {transactionDetails.blockNumber.toString()}
                </p>
              </div>

              <button
                onClick={generatePDF}
                className="mt-6 w-full p-3 bg-green-500 text-white rounded-lg"
              >
                Download PDF Receipt
              </button>
            </div>
            <div className="w-1/2 text-center">
              <h3 className="text-xl font-semibold mb-4">QR Code</h3>
              <QRCodeSVG
                value={`Transaction Hash: ${
                  transactionDetails.transactionHash
                }, 
                  From: ${transactionDetails.from}, 
                  To: ${transactionDetails.to},
                  Contract Address: ${transactionDetails.contractAddress},
                  Cumulative Gas Used: ${transactionDetails.cumulativeGasUsed.toString()}, 
                  Block Number: ${transactionDetails.blockNumber.toString()}`}
                size={200}
                className="mx-auto"
              />
            </div>
          </div>
        )}
      </div>
    </div>

For the QR code generator, qrcode.react library was used, and the transaction details were encrypted into it the QR code SVG.

Final codebase and Output

If you follow the step, your codebase should look like this:

import { useState } from "react";
import Web3 from "web3";
import { jsPDF } from "jspdf";
import { QRCodeSVG } from "qrcode.react";

const TransactionReceipt = () => {
  const [transactionId, setTransactionId] = useState("");
  interface TransactionDetails {
    transactionHash: string;
    from: string;
    to: string;
    cumulativeGasUsed: number;
    blockNumber: number;
    contractAddress?: string;
  }

  const [transactionDetails, setTransactionDetails] =
    useState<TransactionDetails | null>(null);
  const [error, setError] = useState("");

  const web3 = new Web3(
    `https://rpc.testnet.rootstock.io/${import.meta.env.VITE_API_KEY}`
  );

  const fetchTransactionDetails = async () => {
    try {
      setError("");
      setTransactionDetails(null);

      const receipt = await web3.eth.getTransactionReceipt(transactionId);

      if (!receipt) {
        throw new Error("Transaction not found!");
      }

      setTransactionDetails(receipt);
    } catch (err) {
      if (err instanceof Error) {
        setError(err.message);
      } else {
        setError("An unknown error occurred");
      }
    }
  };

  const generatePDF = () => {
    if (!transactionDetails) return;

    const {
      transactionHash,
      from,
      to,
      cumulativeGasUsed,
      blockNumber,
      contractAddress,
    } = transactionDetails;
    const pdf = new jsPDF();

    pdf.setFontSize(16);
    pdf.text("Transaction Receipt", 10, 10);

    pdf.setFontSize(12);
    pdf.text(`Transaction Hash: ${transactionHash}`, 10, 20);
    pdf.text(`From: ${from}`, 10, 30);
    pdf.text(`Contract Address: ${contractAddress}`, 10, 40);
    pdf.text(`To: ${to}`, 10, 40);
    pdf.text(`Cumulative Gas Used: ${cumulativeGasUsed}`, 10, 50);
    pdf.text(`Block Number: ${blockNumber}`, 10, 60);

    pdf.save("Transaction_Receipt.pdf");
  };

  return (
    <div className="p-8 font-sans bg-gray-100 min-h-screen">
      <div className="max-w-3xl m-auto bg-white p-6 rounded-lg shadow-lg">
        <h1 className="text-3xl font-bold mb-6 text-center text-blue-600">
          Transaction Receipt Generator
        </h1>

        <div className="mb-6">
          <div className="flex">
            <input
              type="text"
              id="transactionId"
              value={transactionId}
              onChange={(e) => setTransactionId(e.target.value)}
              placeholder="Enter transaction hash"
              className="border p-2 w-full rounded-l-lg"
            />
            <button
              onClick={fetchTransactionDetails}
              className="p-2 bg-blue-500 text-white rounded-r-lg"
            >
              Fetch Details
            </button>
          </div>
        </div>

        {error && (
          <p className="text-red-500 mt-4 text-center">Error: {error}</p>
        )}

        {transactionDetails && (
          <div className="mt-6 flex flex-row gap-8">
            <div className="w-2/3">
              <h2 className="text-2xl font-semibold mb-4 text-center">
                Transaction Details
              </h2>
              <div className="bg-gray-50 p-4 rounded-lg shadow-inner w-[460px]">
                <p>
                  <strong>Transaction Hash:</strong>{" "}
                  {`${transactionDetails.transactionHash.slice(
                    0,
                    6
                  )}...${transactionDetails.transactionHash.slice(-6)}`}
                </p>
                <p>
                  <strong>From:</strong> {transactionDetails.from}
                </p>
                <p>
                  <strong>Contract Address:</strong>{" "}
                  {transactionDetails.contractAddress}
                </p>
                <p>
                  <strong>To:</strong> {transactionDetails.to}
                </p>
                <p>
                  <strong>Cumulative Gas Used:</strong>{" "}
                  {transactionDetails.cumulativeGasUsed.toString()}
                </p>
                <p>
                  <strong>Block Number:</strong>{" "}
                  {transactionDetails.blockNumber.toString()}
                </p>
              </div>

              <button
                onClick={generatePDF}
                className="mt-6 w-full p-3 bg-green-500 text-white rounded-lg"
              >
                Download PDF Receipt
              </button>
            </div>
            <div className="w-1/2 text-center">
              <h3 className="text-xl font-semibold mb-4">QR Code</h3>
              <QRCodeSVG
                value={`Transaction Hash: ${
                  transactionDetails.transactionHash
                }, 
                  From: ${transactionDetails.from}, 
                  To: ${transactionDetails.to},
                  Contract Address: ${transactionDetails.contractAddress},
                  Cumulative Gas Used: ${transactionDetails.cumulativeGasUsed.toString()}, 
                  Block Number: ${transactionDetails.blockNumber.toString()}`}
                size={200}
                className="mx-auto"
              />
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default TransactionReceipt;


Then, import the TransactionReceipt and render it in your App.tsx file

Demo

Conclusion

In this article, you have been able to build a receipt generator into a PDF or QR code using the Rootstock API Key and RPC method. So in your next dApp project, I hope to see this feature in it.