Building a Transaction Receipt Generator for Rootstock Blockchain

Pranav KondePranav Konde
14 min read

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:

  1. 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.

  2. Smart Contracts: Rootstock supports smart contracts, enabling developers to create complex applications that can automate processes and facilitate transactions without intermediaries.

  3. Decentralized Finance (DeFi): Rootstock incorporates DeFi functionalities, making it a versatile platform for developers and users alike.

  4. 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

  1. Install Node.js: Ensure you have Node.js installed on your machine. You can download it from nodejs.org

  2. 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
    
  3. 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 as rsk_qr_generator

  4. Navigate to Your Project Directory:

     cd my-app (your directory name)
    
  5. 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:

  1. User input for transaction hash

  2. Fetching transaction details from the Rootstock blockchain

  3. Displaying the information in a user-friendly format

  4. Generating downloadable PDFs

  5. 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:

  1. Create a new file called TransactionReceipt.tsx

  2. Copy the complete component code into this file

  3. 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!

0
Subscribe to my newsletter

Read articles from Pranav Konde directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pranav Konde
Pranav Konde