En el mundo actual, los recibos son fundamentales para validar las transacciones y conservar las pruebas de las compras. Ya sea que se trate de un gran banco o de una pequeña tienda al borde de la carretera, los recibos ayudan a las empresas y a las personas a mantenerse organizadas y a realizar un seguimiento de sus gastos.
Pero la cuestión es la siguiente: la mayoría de las aplicaciones descentralizadas no proporcionan recibos y dependen de exploradores para verificar los detalles de las transacciones. ¿Qué pasaría si no tuviera que depender de eso? Imagine cuánto más conveniente sería para sus usuarios generar recibos directamente, sin necesidad de revisar sus billeteras.
Si está creando una aplicación descentralizada basada en pagos en Rootstock, este artículo le mostrará cómo crear un generador de recibos simple pero efectivo utilizando la API de Rootstock y solo un método RPC (llamada a procedimiento remoto). Este enfoque facilita el proceso y garantiza que sus registros de transacciones sean precisos y de fácil acceso.
Aprenda los pasos y las herramientas necesarias para crear una experiencia de generación de recibos fluida.
Usaré React Typescript y TailwindCSS para diseñar.
Instale todas las dependencias usando este comando:
npm i web3js jspdf qrcode.react
Crea un nuevo archivo o utiliza App.jsx
Importe los paquetes al archivo con lo siguiente:
import { useState } from "react"; import Web3 from "web3"; import { jsPDF } from "jspdf"; import { QRCodeSVG } from "qrcode.react";
Inicializar el componente funcional
const TransactionReceipt = () => { /......./ } export default TransactionReceipt;
El fragmento de código aquí administrará el estado y la funcionalidad para obtener y mostrar detalles de la transacción usando el gancho useState, Web3js, Rootstock RPC y 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}` );
Gestión estatal :
const [transactionId, setTransactionId] = useState("");
: Esta línea inicializa una variable de estado transactionId
. Esta variable de estado será responsable del hash de la transacción ingresada que otras variables y funciones aprovecharán para generar el recibo.const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
: Esta línea inicializa una variable de estado transactionDetails
con un valor null
y proporciona una función setTransactionDetails
para actualizar su valor. El estado puede contener un objeto TransactionDetails
o null
.const [error, setError] = useState("");
: Esta línea inicializa una variable de estado error
con una cadena vacía y proporciona una función setError
para actualizar su valor.Interfaz TypeScript :
interface TransactionDetails
: define una interfaz TypeScript para la estructura del objeto de detalles de transacción. Incluye propiedades como transactionHash
, from
, to
, cumulativeGasUsed
, blockNumber
y una contractAddress
opcional.Inicialización de Web3 :
const web3 = new Web3(https://rpc.testnet.rootstock.io/${import.meta.env.VITE_API_KEY});
: Esta línea inicializa Web3js y conecta el punto final de RPC a la red de prueba Rootstock. La URL del punto final incluye una clave API que se recupera de las variables de entorno mediante import.meta.env.VITE_API_KEY
.
El código aquí obtendrá los detalles de la transacción utilizando una función asincrónica de Rootstock con el método web3.js.
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"); } } };
try { ... } catch (err) { ... }
: Esta estructura se utiliza para manejar cualquier error que pueda ocurrir durante la ejecución de la función.error
en una cadena vacía.transactionDetails
en null
.const receipt = await web3.eth.getTransactionReceipt(transactionId)
;: Esta línea utiliza el método web3js para obtener el recibo de transacción para el transactionId ingresado.if (!receipt) { throw new Error("Transaction not found!"); }
: Si no se encuentra el recibo (es decir, el recibo es null
o undefined
), se genera un error con el mensaje "¡Transacción no encontrada!".setTransactionDetails(receipt)
: si se encuentra el recibo, actualiza el estado transactionDetails
con el recibo obtenido.catch (err) { ... }
: Este bloque captura cualquier error que ocurra durante la ejecución del bloque try
.if (err instanceof Error) { setError(err.message); } else { setError("An unknown error occurred"); }
: Si el error detectado es una instancia de la clase Error, establece el estado error
en el mensaje del error. De lo contrario, establece el estado error
en un mensaje de error genérico "Ocurrió un error desconocido".
Aquí se utilizará el paquete Jspdf para generar el PDF que contiene los detalles de la transacción.
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;
: Esto verifica si transactionDetails
es null
o undefined
. Si lo es, la función retorna antes y no hace nada.
const { transactionHash, from, to, cumulativeGasUsed, blockNumber, contractAddress } = transactionDetails;
: Esto desestructura el objeto transactionDetails
para extraer propiedades individuales para un acceso más fácil.
const pdf = new jsPDF()
: Esto crea una nueva instancia de la clase jsPDF, que representa un documento PDF.
pdf.setFontSize(16)
: Esto establece el tamaño de fuente del encabezado a 16.
pdf.text("Transaction Receipt", 10, 10);
: Esto agrega el título "Recibo de transacción" en las coordenadas (10, 10) en el documento PDF.
pdf.setFontSize(12);
: Esto establece el tamaño de fuente a 12 para el resto del texto.
pdf.text(Transaction Hash
: ${transactionHash} , 10, 20);
: Esto agrega el hash de la transacción en las coordenadas (10, 20).
pdf.text(From: ${from}, 10, 30);
: Esto agrega la dirección del remitente en las coordenadas (10, 30).
pdf.text(Contract Address: ${contractAddress}, 10, 40);
: Esto agrega la dirección del contrato en las coordenadas (10, 40). Nota: Esta línea debe corregirse para evitar superponer texto.
pdf.text(To: ${to}, 10, 50);
: Esto agrega la dirección del destinatario en las coordenadas (10, 50).
pdf.text(Gas acumulado utilizado: ${cumulativeGasUsed} , 10, 60);
: Esto agrega el gas acumulado utilizado en las coordenadas (10, 60).
pdf.text(Block Number: ${blockNumber}, 10, 70);
: Esto agrega el número de bloque en las coordenadas (10, 70).
Aquí representarás esos componentes funcionales como una interfaz de usuario para los usuarios.
Este código ya ha incluido el estilo usando 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>
Para el generador de código QR, se utilizó la biblioteca qrcode.react y los detalles de la transacción se cifraron en el código QR SVG.
Si sigues los pasos, tu código base debería verse así:
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;
Luego, importe el TransactionReceipt
y preséntelo en su archivo App.tsx
En este artículo, ha podido crear un generador de recibos en formato PDF o código QR utilizando la clave API de Rootstock y el método RPC. Por lo tanto, espero ver esta función en su próximo proyecto de dApp.