Building a Transaction Receipt Generator for Rootstock Blockchain


In the world of blockchain, things are changing fast, but sometimes user experience isn’t keeping up. A common issue is that when you make a transaction in a decentralized application (dApp), you often don’t get a simple receipt like you would from a regular bank or store. Instead, you have to jump through hoops, like visiting websites such as Etherscan, to check if your transaction went through. This can be confusing and frustrating, especially for new users.
In this tutorial, we’ll create a straightforward transaction receipt generator for the Rootstock blockchain. The goal is to provide users with an easy way to see their transaction details without having to leave the dApp.
By the end of this guide, you'll have created a professional receipt generator that can:
Fetch transaction details directly from the Rootstock blockchain
Display transaction information in a clean, user-friendly interface
Generate downloadable PDF receipts for record-keeping
Create scannable QR codes containing comprehensive transaction data
Let's bring the familiar comfort of transaction receipts to the innovative world of blockchain! Let’s build this together!
What is Rootstock?
Rootstock (RSK) is a smart contract platform that is built on top of the Bitcoin blockchain. It aims to extend Bitcoin's functionality by enabling the execution of smart contracts, similar to what Ethereum offers. Rootstock achieves this by utilizing a two-way peg mechanism that allows Bitcoin to be converted into RSK tokens (RBTC) and vice versa. This integration allows developers to create decentralized applications (dApps) that benefit from Bitcoin's security while leveraging the flexibility of smart contracts.
Key Features of Rootstock:
EVM Compatibility: Rootstock is compatible with the Ethereum Virtual Machine (EVM), allowing developers to easily port their existing Ethereum dApps to the Rootstock platform without significant modifications. This compatibility fosters a broader ecosystem of decentralized applications and services.
Smart Contracts: Rootstock supports smart contracts, enabling developers to create complex applications that can automate processes and facilitate transactions without intermediaries.
Decentralized Finance (DeFi): Rootstock incorporates DeFi functionalities, making it a versatile platform for developers and users alike.
Scalability: By utilizing sidechains, Rootstock addresses some of the limitations of the Ethereum network, such as high gas fees and slow transaction times, providing a robust environment for building and deploying decentralized applications.
Why Transaction Receipts Matter in Blockchain Applications
Before diving into the technical implementation, let's understand why transaction receipts are so crucial in blockchain applications.
Building Trust Through Transparency
Blockchain technology is often lauded for its transparency, yet many applications fail to capitalize on this advantage by providing clear, understandable transaction records to users. A well-designed receipt bridges the gap between complex blockchain data and user-friendly information presentation.
Addressing User Anxiety
Many users, especially those new to blockchain, experience anxiety when conducting transactions. Did my transaction go through? Where did my funds go? How can I prove this transaction happened? A receipt system answers these questions immediately, reducing user stress and support inquiries.
Professional Appearance
Applications that provide proper documentation of transactions appear more professional and trustworthy. This is particularly important for financial applications where users are moving valuable assets.
Practical Record-Keeping
For business users or individuals who need to maintain financial records, downloadable receipts simplify accounting and tax reporting related to blockchain transactions.
Reduced Dependency on Third-Party Services
By implementing receipt generation directly in your application, you reduce dependency on external blockchain explorers, creating a more seamless user experience and maintaining users within your application ecosystem.
Prerequisites
Before beginning, ensure you have:
Node.js installed on your development machine
Basic knowledge of JavaScript/TypeScript
A code editor (like Visual Studio Code)
Familiarity with React and TypeScript
A Rootstock API key for accessing the RPC endpoint
(If you don’t have one, no worries, follow this Blog to get the API key)
Setting Up the Project
For our transaction receipt generator, we'll be using:
React with TypeScript for a robust frontend framework
TailwindCSS for clean, responsive styling
Web3.js for seamless blockchain interaction
jsPDF for generating downloadable PDF receipts
QRCode React for creating scannable transaction QR codes
Start by creating a new React project with TypeScript or adapting this component to your existing project.
Creating a New React Project with TypeScript
Install Node.js: Ensure you have Node.js installed on your machine. You can download it from nodejs.org
Install Create React App: Open your terminal and run the following command to install Create React App globally (if you haven't already):
npm install -g create-react-app
Create a New React Project: Use Create React App to set up a new project with TypeScript by running:
npx create-react-app my-app --template typescript
Replace
my-app
With your desired project name. Here we will be naming it asrsk_qr_generator
Navigate to Your Project Directory:
cd my-app (your directory name)
Start the Development Server: Run the following command to start your React application
npm start
Your new React app should now be running at
http://localhost:3000
Then install the required packages:
npm i web3 jspdf qrcode.react
Designing the Receipt Generator Component
Our transaction receipt generator will be a self-contained React component that handles:
User input for transaction hash
Fetching transaction details from the Rootstock blockchain
Displaying the information in a user-friendly format
Generating downloadable PDFs
Creating QR codes with embedded transaction data
The component structure follows a logical flow that guides users from input to output, with clear error handling and visual feedback throughout the process.
Building the Receipt Generator Component
Let's create our TransactionReceipt.tsx
component:
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 [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const web3 = new Web3(
`https://rootstock-testnet.g.alchemy.com/v2/{API-KEY}`
);
const fetchTransactionDetails = async () => {
if (!transactionId.trim()) {
setError("Please enter a transaction hash");
return;
}
try {
setLoading(true);
setError("");
setTransactionDetails(null);
const receipt = await web3.eth.getTransactionReceipt(transactionId);
if (!receipt) {
throw new Error("Transaction not found! Please check the hash and try again.");
}
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 while fetching transaction details");
}
} finally {
setLoading(false);
}
};
const generatePDF = () => {
if (!transactionDetails) return;
const {
transactionHash,
from,
to,
cumulativeGasUsed,
blockNumber,
contractAddress,
} = transactionDetails;
const pdf = new jsPDF();
const pageWidth = pdf.internal.pageSize.getWidth();
const currentDate = new Date().toLocaleDateString();
pdf.setFontSize(16);
pdf.text("Rootstock Transaction Receipt", pageWidth / 2, 15, { align: "center" });
pdf.setFontSize(10);
pdf.text(`Generated on: ${currentDate}`, pageWidth / 2, 22, { align: "center" });
pdf.setLineWidth(0.5);
pdf.line(15, 25, pageWidth - 15, 25);
pdf.setFontSize(12);
const formatLongString = (label: string, value: string, yPos: number) => {
pdf.text(`${label}:`, 15, yPos);
if (value.length > 60) {
pdf.setFont("Courier");
pdf.text(value.slice(0, 60), 15, yPos + 5);
pdf.text(value.slice(60), 15, yPos + 10);
pdf.setFont("Helvetica");
return 15;
} else {
pdf.text(value, 45, yPos);
return 0;
}
};
let yPosition = 35;
yPosition += formatLongString("Transaction Hash", transactionHash, yPosition);
yPosition += 10;
yPosition += formatLongString("From Address", from, yPosition);
yPosition += 10;
if (to) {
yPosition += formatLongString("To Address", to, yPosition);
yPosition += 10;
}
if (contractAddress) {
yPosition += formatLongString("Contract Address", contractAddress, yPosition);
yPosition += 10;
}
pdf.text(`Gas Used: ${cumulativeGasUsed}`, 15, yPosition);
yPosition += 7;
pdf.text(`Block Number: ${blockNumber}`, 15, yPosition);
yPosition += 15;
pdf.text("A QR code containing this transaction information is available", 15, yPosition);
pdf.text("in the online receipt generator.", 15, yPosition + 5);
pdf.setFontSize(10);
pdf.text("This receipt was generated using the Rootstock Blockchain Receipt Generator", pageWidth / 2, 280, { align: "center" });
pdf.save("Rootstock_Transaction_Receipt.pdf");
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
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">
Rootstock Transaction Receipt Generator
</h1>
<p className="text-gray-600 mb-6 text-center">
Generate professional receipts for any transaction on the Rootstock blockchain
</p>
<div className="mb-6">
<div className="flex flex-col md:flex-row gap-2">
<input
type="text"
id="transactionId"
value={transactionId}
onChange={(e) => setTransactionId(e.target.value)}
placeholder="Enter transaction hash"
className="border p-3 w-full rounded-lg md:rounded-r-none"
/>
<button
onClick={fetchTransactionDetails}
disabled={loading}
className="p-3 bg-blue-500 text-white rounded-lg md:rounded-l-none hover:bg-blue-600 transition-colors disabled:bg-blue-300"
>
{loading ? "Fetching..." : "Fetch Details"}
</button>
</div>
<p className="text-xs text-gray-500 mt-2">
Enter a valid Rootstock transaction hash to generate a receipt
</p>
</div>
{error && (
<div className="p-4 bg-red-50 border-l-4 border-red-500 text-red-700 mb-6">
<p className="font-bold">Error</p>
<p>{error}</p>
</div>
)}
{transactionDetails && (
<div className="mt-6 flex flex-col md:flex-row gap-8">
<div className="md:w-2/3">
<h2 className="text-2xl font-semibold mb-4 text-center">
Transaction Details
</h2>
<div className="bg-gradient-to-br from-gray-50 to-gray-100 p-6 rounded-xl shadow-sm border border-gray-200">
<div className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<div className="flex justify-between items-center">
<div>
<strong className="text-blue-600">Transaction Hash:</strong>
<div className="font-mono text-sm mt-1">
{transactionDetails.transactionHash}
</div>
</div>
<button
onClick={() => copyToClipboard(transactionDetails.transactionHash)}
className="text-gray-500 hover:text-blue-600 ml-2"
>
📋
</button>
</div>
</div>
<div className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<div className="flex justify-between items-center">
<div>
<strong className="text-blue-600">From:</strong>
<div className="font-mono text-sm mt-1">
{transactionDetails.from}
</div>
</div>
<button
onClick={() => copyToClipboard(transactionDetails.from)}
className="text-gray-500 hover:text-blue-600 ml-2"
>
📋
</button>
</div>
</div>
{transactionDetails.to && (
<div className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<div className="flex justify-between items-center">
<div>
<strong className="text-blue-600">To:</strong>
<div className="font-mono text-sm mt-1">
{transactionDetails.to}
</div>
</div>
<button
onClick={() => copyToClipboard(transactionDetails.to)}
className="text-gray-500 hover:text-blue-600 ml-2"
>
📋
</button>
</div>
</div>
)}
{transactionDetails.contractAddress && (
<div className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<div className="flex justify-between items-center">
<div>
<strong className="text-blue-600">Contract Address:</strong>
<div className="font-mono text-sm mt-1">
{transactionDetails.contractAddress}
</div>
</div>
<button
onClick={() => copyToClipboard(transactionDetails.contractAddress!)}
className="text-gray-500 hover:text-blue-600 ml-2"
>
📋
</button>
</div>
</div>
)}
<p className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<strong className="text-blue-600">Cumulative Gas Used:</strong>{" "}
<span className="font-mono">{transactionDetails.cumulativeGasUsed.toLocaleString()}</span>
</p>
<p className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
<strong className="text-blue-600">Block Number:</strong>{" "}
<span className="font-mono">{transactionDetails.blockNumber.toLocaleString()}</span>
</p>
<div className="text-sm text-gray-500 mt-4 p-3">
<p>Transaction verified on Rootstock blockchain</p>
</div>
</div>
<button
onClick={generatePDF}
className="mt-6 w-full p-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex justify-center items-center gap-2"
>
<span>📄</span> Download PDF Receipt
</button>
</div>
<div className="md:w-1/3 text-center mt-6 md:mt-0">
<h3 className="text-xl font-semibold mb-4">QR Code</h3>
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<QRCodeSVG
value={`Transaction Hash: ${transactionDetails.transactionHash},
From: ${transactionDetails.from},
${transactionDetails.to ? `To: ${transactionDetails.to},` : ''}
${transactionDetails.contractAddress ? `Contract Address: ${transactionDetails.contractAddress},` : ''}
Cumulative Gas Used: ${transactionDetails.cumulativeGasUsed.toString()},
Block Number: ${transactionDetails.blockNumber.toString()}`}
size={200}
className="mx-auto"
/>
<p className="text-xs text-gray-500 mt-4">
Scan to view transaction details
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default TransactionReceipt;
Understanding the Code Structure
Let's break down the key components of our transaction receipt generator:
State Management
Our component uses four state variables:
const [transactionId, setTransactionId] = useState("");
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
These handle:
The transaction hash input from users
The retrieved transaction details
Loading state for UI feedback
Any error messages that might occur
Web3 Initialization
We initialize Web3.js with the Rootstock testnet RPC endpoint:
const web3 = new Web3(
`https://rootstock-testnet.g.alchemy.com/v2/{API-KEY}`
);
In a production environment, you'd want to store this API key securely in an environment variable.
Fetching Transaction Details
The core functionality of our application is handled by the fetchTransactionDetails
function. It makes a single RPC call to retrieve all necessary transaction information:
const receipt = await web3.eth.getTransactionReceipt(transactionId);
This simple call provides us with:
Transaction hash
Sender address (from)
Recipient address (to)
Contract address (if applicable)
Gas usage
Block number
The beauty of this approach is its simplicity - we need just one API method to build a full-featured receipt generator.
PDF Generation
The generatePDF
function creates a professional, downloadable receipt using jsPDF:
const pdf = new jsPDF();
// Configure PDF content with transaction details
pdf.save("Rootstock_Transaction_Receipt.pdf");
Our implementation includes:
Proper formatting for long blockchain addresses
Clear section headers
Date and time of receipt generation
Structured layout with appropriate spacing
QR Code Creation
We use the QRCodeSVG component to generate a scannable QR code containing all transaction details:
<QRCodeSVG
value={`Transaction Hash: ${transactionDetails.transactionHash}, ...`}
size={200}
className="mx-auto"
/>
This allows users to quickly share transaction information by having others scan the code.
Enhanced User Experience Features
Our component includes several UX improvements:
Copy to Clipboard Functionality
Each transaction detail has a copy button, allowing users to quickly copy addresses and other information:
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
// Could add toast notification here
};
Loading States
We've added loading indicators to provide feedback during API calls:
<button
onClick={fetchTransactionDetails}
disabled={loading}
className="p-3 bg-blue-500 text-white rounded-lg md:rounded-l-none hover:bg-blue-600 transition-colors disabled:bg-blue-300"
>
{loading ? "Fetching..." : "Fetch Details"}
</button>
Responsive Design
The layout adapts to different screen sizes using Tailwind's responsive classes:
<div className="mt-6 flex flex-col md:flex-row gap-8">
Visual Feedback
Hover effects on transaction details enhance the interactive feel:
<div className="mb-4 hover:bg-white p-3 rounded-lg transition-all">
Integrating the Component Into Your Project
To use this component in your React application:
Create a new file called
TransactionReceipt.tsx
Copy the complete component code into this file
Import and use it in your App component:
import TransactionReceipt from './components/TransactionReceipt';
function App() {
return (
<div className="App">
<TransactionReceipt />
</div>
);
}
export default App;
To run the App, paste the below command
npm start
Woaahhhh, your App ran successfully, without any errors and issues, Congratulations 🤩🤩
If facing any issues, you can refer to my project on GitHub: Transaction Receipt Generator
Which will navigate you to Port 3000 by default, where you need to enter the transaction hash for the contract you deployed on the Rootstock network, and your output will look like
Once you click on the Download PDF Receipt, it will download a PDF of receipt, which will look like
And when you scan the QR code present on port 3000, the output will look like
Security Considerations for Production Use
When implementing this component in a production environment, consider these security best practices:
API Key Protection
Never expose your API keys in client-side code. Use environment variables and potentially a backend proxy for API calls:
// Instead of this
const web3 = new Web3(`https://rpc.endpoint.io/YOUR_API_KEY`);
// Do this
const web3 = new Web3(`${process.env.REACT_APP_ROOTSTOCK_RPC_ENDPOINT}`);
Input Validation
Add more robust validation for transaction hash input:
const validateTransactionHash = (hash: string) => {
// Remove 0x prefix if present
const cleanHash = hash.startsWith('0x') ? hash.substring(2) : hash;
// Check if it's a valid hex string of the right length
const validHex = /^[0-9a-fA-F]{64}$/.test(cleanHash);
if (!validHex) {
setError("Invalid transaction hash format");
return false;
}
return true;
};
Rate Limiting
Implement rate limiting to prevent abuse of your API endpoint:
const [lastFetchTime, setLastFetchTime] = useState(0);
const fetchWithRateLimit = async () => {
const now = Date.now();
if (now - lastFetchTime < 2000) { // 2 second cooldown
setError("Please wait before making another request");
return;
}
setLastFetchTime(now);
await fetchTransactionDetails();
};
Error Handling
Add more comprehensive error handling for network issues and API responses:
try {
// API call
} catch (err) {
if (err.message.includes('Invalid JSON RPC response')) {
setError("Network error: Cannot connect to Rootstock node");
} else if (err.message.includes('Rate limit')) {
setError("API rate limit exceeded. Please try again in a moment");
} else {
setError(`Error: ${err.message}`);
}
}
Enhancing the Component Further
There are several ways you could extend this component to add even more value:
Transaction History
Store recent transaction hashes in local storage for quick access:
// Save transaction to history
const saveToHistory = (hash: string) => {
const history = JSON.parse(localStorage.getItem('txHistory') || '[]');
if (!history.includes(hash)) {
const updatedHistory = [hash, ...history].slice(0, 10); // Keep last 10
localStorage.setItem('txHistory', JSON.stringify(updatedHistory));
}
};
// Add this to fetchTransactionDetails on success
if (receipt) {
saveToHistory(transactionId);
}
Network Selection
Add support for multiple Rootstock networks (mainnet, testnet):
const NETWORKS = {
mainnet: "https://rpc.rootstock.io/",
testnet: "https://rootstock-testnet.g.alchemy.com/v2/YOUR_API_KEY"
};
const [selectedNetwork, setSelectedNetwork] = useState("testnet");
// Then use the selected network when initializing Web3
const web3 = new Web3(NETWORKS[selectedNetwork]);
Additional Transaction Data
Expand the receipt with more details like transaction status, timestamp, and gas price:
const fetchFullTransactionDetails = async () => {
const receipt = await web3.eth.getTransactionReceipt(transactionId);
const tx = await web3.eth.getTransaction(transactionId);
const block = await web3.eth.getBlock(receipt.blockNumber);
setTransactionDetails({
...receipt,
...tx,
timestamp: block.timestamp,
// Add other details
});
};
Email Functionality
Add the ability to email the receipt directly from the application:
const emailReceipt = () => {
const pdfBlob = pdf.output('blob');
const formData = new FormData();
formData.append('pdf', pdfBlob, 'transaction_receipt.pdf');
formData.append('email', userEmail);
// Send to your backend email service
fetch('/api/email-receipt', {
method: 'POST',
body: formData
});
};
Conclusion
With just a single RPC method and a well-designed React component, we've created a full-featured transaction receipt generator for the Rootstock blockchain. This enhancement significantly improves the user experience of blockchain applications by providing immediate, tangible proof of transactions.
The beauty of this approach lies in its simplicity. By using the getTransactionReceipt
method, we access all the necessary transaction data without complex API integrations or multiple endpoints. This demonstrates how powerful blockchain tooling can be when leveraged correctly, even with minimal API usage.
As blockchain applications continue their march toward mainstream adoption, user-friendly features like transaction receipts will play a crucial role in making the technology accessible to everyone. By implementing this component in your dApp, you're giving users more control and visibility into their blockchain interactions, building trust and confidence in your application.
Remember that small UX improvements like this can make a significant difference in user retention and overall satisfaction. The blockchain space is rapidly evolving, and the applications that will succeed are those that combine innovative technology with thoughtful, user-centered design principles.
What will you build next to enhance your blockchain application's user experience?
If facing any errors, join Rootstock Discord and ask under the respective channel.
Until then, dive deeper into Rootstock by exploring its official documentation. Keep experimenting, and happy coding!
Subscribe to my newsletter
Read articles from Pranav Konde directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
