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.
I will be using React Typescript and TailwindCSS for styling
npm i web3js jspdf qrcode.react
App.jsx
import { useState } from "react"; import Web3 from "web3"; import { jsPDF } from "jspdf"; import { QRCodeSVG } from "qrcode.react";
const TransactionReceipt = () => { /......./ } export default TransactionReceipt;
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}
);
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.interface TransactionDetails
: This defines a TypeScript interface for the structure of the transaction details object. It includes properties like transactionHash
, from
, to
, cumulativeGasUsed
, blockNumber
, and an optional contractAddress
.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
.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,
cumulativeGasUsed: Number(receipt.cumulativeGasUsed),
blockNumber: Number(receipt.blockNumber),
})
} catch (err) { if (err instanceof Error) { setError(err.message); } else { setError("An unknown error occurred"); } } };
try { ... } catch (err) { ... }
: This structure is used to handle any errors that might occur during the execution of the function.error
state to an empty string.transactionDetails
state to null
.const receipt = await web3.eth.getTransactionReceipt(transactionId)
;: This line uses the web3js method to fetch the transaction receipt for the inputted transactionId.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!".setTransactionDetails(receipt)
: If the receipt is found, it updates the transactionDetails
state with the fetched receipt.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".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");
};
if (!transactionDetails) return;
: This checks if transactionDetails
is null
or undefined
. If it is, the function returns early and does nothing.const { transactionHash, from, to, cumulativeGasUsed, blockNumber, contractAddress } = transactionDetails;
: This destructures the transactionDetails
object to extract individual properties for easier access.const pdf = new jsPDF()
: This creates a new instance of the jsPDF class, which represents a PDF document.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.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)., 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).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.
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,
cumulativeGasUsed: Number(receipt.cumulativeGasUsed),
blockNumber: Number(receipt.blockNumber),
})
} 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
Live Site: https://receipt-generator-six.vercel.app/
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.