本文介绍了如何使用QuickNode的EVM Blockbook JSON-RPC插件来生成详细的以太坊交易报告,包括ETH转账、ERC-20、ERC-721和ERC-1155代币转账。文章提供了构建一个React UI的逐步指南,该UI利用Blockbook JSON-RPC插件从后端检索以太坊交易数据, 并将其导出为CSV格式,便于分析和报告。
合规的报告解决方案对于当今的数字金融专业人士至关重要。本教程将指导你使用 QuickNode 的 EVM Blockbook JSON-RPC 插件 来制作以太坊交易的详细报告,包括 ERC-20、ERC-721 和 ERC-1155 代币转账。本指南专为开发人员和金融分析师设计,提供了一个全面的工具包,用于提取、分析和展示符合监管标准的交易数据。
多链支持
QuickNode 通过单独的 Blockbook 插件支持多个基于 EVM 的链:
学习如何使用 QuickNode 的 EVM Blockbook JSON-RPC 插件 为以太币转账、代币转账(例如,ERC-20、ERC-721、ERC-1155)和内部交易等交易生成详细的交易报告。
使用 React 构建一个 UI,该 UI 在后端使用 EVM Blockbook JSON-RPC 插件 来检索基于给定地址的以太坊交易
快速入门选项
如果你希望立即开始使用该应用程序,而无需从头开始构建,我们提供了一个现成的解决方案。只需访问我们的 GitHub 存储库 来克隆示例应用程序。你只需要提供你自己的端点 URL。请按照存储库中的 README 获取有关快速设置和运行应用程序的分步说明。
依赖 | 版本 |
---|---|
node.js | >18.16 |
typescript | 最新 |
ts-node | 最新 |
EVM Blockbook JSON-RPC 插件 允许你通过 JSON-RPC 访问地址的余额、交易和地址余额历史记录。此插件利用 Blockbook REST API,该 API 旨在提供对区块链数据的高效查询,包括对智能合约、原生 ETH 转账、内部交易和代币转账的详细分析。
在撰写本文时,EVM Blockbook 插件提供了 8 个 RPC 方法。我们将在此指南中使用其中一种方法:
bb_getAddress
:返回地址的余额和交易。返回的交易按区块高度排序,最新的区块在前。在开始之前,请注意 EVM Blockbook JSON-RPC 是一项付费插件。请在此处 查看详细信息,并根据你的需求比较套餐。
使用 EVM Blockbook JSON-RPC 设置你的以太坊端点非常容易。如果你尚未注册,可以在此处 创建一个帐户。
登录后,导航到 端点 页面,然后单击 创建端点。选择 以太坊主网,然后单击下一步。然后,系统将提示你配置插件。激活 EVM Blockbook JSON-RPC。之后,只需单击 创建端点。
如果你已经有一个没有插件的以太坊端点,请转到以太坊端点内的 插件 页面,选择 EVM Blockbook JSON-RPC,然后激活它。
端点准备好后,复制 HTTP Provider 链接并妥善保管,因为你将在下一节中需要它。
在开始之前,请确保你的机器上安装了 Node.js。Node.js 将成为运行应用程序的骨干,而 npm(Node.js 中包含的默认软件包管理器)将有效地处理所有依赖项。你可以在 他们的官方页面 上找到安装说明。
此外,如果你尚未安装 TypeScript,请将其设置为全局安装,以便在所有项目中可用,方法是运行以下命令:
npm install -g typescript ts-node
在开始编码之前,让我们看一下我们将要构建的内容。最后,我们的应用程序将类似于下图所示。
为你的项目创建一个目录,并在其中初始化一个新的 Vite 项目:
npm create vite@latest ethereum-transaction-reports -- --template react-ts
cd ethereum-transaction-reports
此命令将创建一个名为 ethereum-transaction-reports 的新目录,其中包含 React 和 TypeScript 的 Vite 项目模板,然后将你当前的目录更改为新的项目文件夹。
继续安装必要的软件包:
npm install axios luxon dotenv fs-extra @quicknode/sdk
npm i --save-dev @types/fs-extra @types/luxon tailwindcss postcss autoprefixer
📘 软件包
fs-extra
和 luxon
提供 TypeScript 类型定义。现在,通过运行以下命令在项目中设置 Tailwind CSS:
npx tailwindcss init -p
修改 tailwind.config.js
文件以在配置文件中添加路径:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
删除 ./src/index.css
文件中的所有代码,并在其中添加 @tailwind 指令。
@tailwind base;
@tailwind components;
@tailwind utilities;
执行这些命令后,所需的软件包已安装,并且 Tailwind 配置文件已完成。
在深入研究以太坊交易报告工具的编码之前,务必了解其操作流程。此概述确保我们的开发由对每个组件如何为实现我们的目标做出贡献的清晰理解来指导。以下是操作流程的细分:
导入依赖项:每个组件和辅助文件都首先导入必要的库和模块。例如,App.tsx
可能会导入 React、ReportForm.tsx
和 ResultTable.tsx
以组装用户界面,而 blockbookMethods.ts
和 calculateVariables.ts
中的辅助函数会管理数据检索和处理。
用户输入:用户与 ReportForm.tsx
组件交互,输入以太坊地址并选择交易报告的日期范围。然后,此数据会提交到 App.tsx
中的主应用程序逻辑。
提取交易数据:在表单提交后,App.tsx 调用 blockbookMethods.ts
中的函数 bb_getAddress
,使用提供的以太坊地址从 Blockbook 收集其交易历史记录,包括 ERC-20、ERC-721 和 ERC-1155 代币转账的详细信息。
处理数据:在数据检索后,calculateVariables.ts
中的 calculateVariables
函数会处理此数据。此函数处理不同的方面,如代币转账、智能合约交互和标准以太坊交易,以提供结构化数据集。
显示结果:数据处理完成后,数据会传递给 ResultTable.tsx
,后者以用户友好的表格式在前端呈现数据,允许用户查看和分析其以太坊交易历史记录。
生成报告:如果用户想要以 CSV 格式复制或导出数据,convertToCsv
函数会将处理后的数据组织成 CSV 格式,使其适合分析和报告。
现在,让我们开始编码。
在你的项目的目录(即 ethereum-transaction-reports)中创建必要的文件:
mkdir src/helpers
mkdir src/components
echo > .env
echo > src/interfaces.ts
echo > src/helpers/blockbookMethods.ts
echo > src/helpers/calculateVariables.ts
echo > src/helpers/convertToCsv.ts
echo > src/components/CopyIcon.tsx
echo > src/components/ReportForm.tsx
echo > src/components/ResultTable.tsx
📘 文件
.env:存储环境变量,例如你的 QuickNode 端点 URL。此设置确保敏感信息(如 API 密钥)得到安全管理并易于配置。
src/interfaces.ts:定义 TypeScript 接口,以确保整个应用程序的类型安全性和一致性。此文件包括各种函数中使用和返回的数据结构的类型定义,从而增强了代码的可靠性和可维护性。
src/helpers/blockbookMethods.ts:包含与以太坊 Blockbook API 接口的函数,用于获取指定以太坊地址的交易和智能合约交互数据。
src/helpers/calculateVariables.ts:处理 blockbookMethods.ts 检索的以太坊区块链数据,包括交易、代 币转账以及与合约交互的计算。
src/helpers/convertToCsv.ts:将处理后的区块链数据转换为 CSV 格式,使其适合分析和分发。
src/components/CopyIcon.tsx:一个 React 组件,提供用于将文本复制到剪贴板的用户界面元素。
src/components/ReportForm.tsx:一个 React 组件,呈现一个表单,供用户输入参数,例如以太坊地址和日期范围。此表单用于根据用户输入生成特定的交易报告。
src/components/ResultTable.tsx:一个 React 组件,以表格格式显示交易报告数据,使用户可以轻松读取和分析信息。
将你的 QuickNode 端点和其他敏感信息(如果有)存储在 .env
文件中。
打开 .env
文件并按如下方式对其进行修改。不要忘记将 YOUR_QUICKNODE_ETHEREUM_ENDPOINT_URL 占位符替换为你的 QuickNode 以太坊 HTTP 提供程序 URL。
.env
VITE_QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ETHEREUM_ENDPOINT_URL"
interfaces.ts
文件定义 TypeScript 接口,以确保整个应用程序的类型安全性和一致性。此文件包括各种函数中使用和返回的数据结构的类型定义,从而增强了代码的可靠性和可维护性。
使用你的代码编辑器打开 src/interfaces.ts
文件并按如下方式修改该文件:
src/interfaces.ts
import { DateTime } from "luxon";
export interface CalculateVariablesOptions {
startDate?: DateTime;
endDate?: DateTime;
userTimezone?: string;
}
export interface Config {
startDate?: {
year: number;
month: number;
day: number;
};
endDate?: {
year: number;
month: number;
day: number;
};
userTimezone?: string;
}
export interface Result {
page: number;
totalPages: number;
itemsOnPage: number;
address: string;
balance: string;
unconfirmedBalance: string;
unconfirmedTxs: number;
txs: number;
nonTokenTxs: number;
internalTxs: number;
transactions: Transaction[];
nonce: string;
}
export interface Transaction {
txid: string;
version: number;
vin: Vin[];
vout: Vout[];
ethereumSpecific?: EthereumSpecific;
tokenTransfers?: TokenTransfer[];
blockHash: string;
blockHeight: number;
confirmations: number;
blockTime: number;
size: number;
vsize: number;
value: string;
valueIn: string;
fees: string;
hex?: string;
}
export interface EthereumSpecific {
internalTransfers?: InternalTransfer[];
parsedData?: ParsedData;
}
export interface InternalTransfer {
from: string;
to: string;
value: string;
}
export interface ParsedData {
methodId: string;
name: string;
}
export interface TokenTransfer {
type: string;
from: string;
to: string;
contract: string;
name: string;
symbol: string;
decimals: number;
value: string;
multiTokenValues?: MultiTokenValues[];
}
export interface MultiTokenValues {
id: string;
value: string;
}
export interface ExtractedTransaction {
txid: string;
blockHeight: number;
direction: "Incoming" | "Outgoing";
txType: string;
assetType: string;
senderAddress: string;
receiverAddress: string;
value: string;
fee: string;
day: string;
timestamp: string;
userTimezone: string;
status: string;
methodNameOrId: string;
contract?: string;
tokenId?: string;
}
export interface ExtendedResult extends Result {
extractedTransaction: ExtractedTransaction[];
startDate: DateTime;
endDate: DateTime;
}
export interface Vin {
txid: string;
vout?: number;
sequence: number;
n: number;
addresses: string[];
isAddress: boolean;
value: string;
hex: string;
isOwn?: boolean;
}
export interface Vout {
value: string;
n: number;
hex: string;
addresses: string[];
isAddress: boolean;
spent?: boolean;
isOwn?: boolean;
}
blockbookMethods.ts
文件包含旨在通过 Blockbook API 与 QuickNode 端点交互的函数。这些函数有助于获取特定以太坊地址的详细交易数据并获取代币转账详细信息。
使用你的代码编辑器打开 src/helpers/blockbookMethods.ts
文件并按如下方式修改该文件:
src/helpers/blockbookMethods.ts
// 导入必要的类型和库
import { Result } from "../interfaces";
import axios from "axios";
// 从环境变量中检索 QuickNode 端点 URL
const QUICKNODE_ENDPOINT = import.meta.env.VITE_QUICKNODE_ENDPOINT as string;
// 获取指定以太坊地址的详细交易数据
export async function bb_getAddress(address: string): Promise<Result> {
try {
// 准备 bb_getAddress 方法的请求 payload
const postData = {
method: "bb_getAddress",
params: [
address,
{ page: "1", size: "1000", fromHeight: "0", details: "txs" }, // 查询参数
],
id: 1,
jsonrpc: "2.0",
};
// 向 QuickNode 端点发出 POST 请求
const response = await axios.post(QUICKNODE_ENDPOINT, postData, {
headers: { "Content-Type": "application/json" },
maxBodyLength: Infinity,
});
// 检查是否成功响应并返回数据
if (response.status === 200 && response.data) {
return response.data.result;
} else {
throw new Error("Failed to fetch transactions");
}
} catch (error) {
console.error(error);
throw error;
}
}
calculateVariables.ts
文件处理 blockbookMethods.ts
检索的以太坊区块链数据。此文件处理交易、代币转账以及与合约交互的计算。
使用你的代码编辑器打开 src/helpers/calculateVariables.ts
文件并按如下方式修改该文件:
src/helpers/calculateVariables.ts
// 导入必要的类型和库
import { DateTime } from "luxon";
import { viem } from "@quicknode/sdk";
import {
Result,
ExtractedTransaction,
ExtendedResult,
CalculateVariablesOptions,
} from "../interfaces";
export async function calculateVariables(
result: Result,
options: CalculateVariablesOptions = {}
): Promise<ExtendedResult> {
const userTimezone = options.userTimezone || DateTime.local().zoneName;
const startDate = options.startDate || DateTime.now().setZone(userTimezone);
const endDate = options.endDate || DateTime.now().setZone(userTimezone);
const startOfPeriod = startDate.startOf("day");
const endOfPeriod = endDate.endOf("day");
const extractedData: ExtractedTransaction[] = [];
for (const transaction of result.transactions) {
const blockTime = DateTime.fromMillis(transaction.blockTime * 1000, {
zone: userTimezone,
});
const day = blockTime.toFormat("yyyy-MM-dd");
const timestamp: string = blockTime.toString() || "";
const status = transaction.confirmations > 0 ? "Confirmed" : "Pending";
let methodNameOrId = "";
if (transaction.ethereumSpecific?.parsedData) {
const { name, methodId } = transaction.ethereumSpecific.parsedData;
if (name && methodId) {
methodNameOrId = `${name} (${methodId})`;
} else {
methodNameOrId = name || methodId || "Unknown";
}
}
if (blockTime < startOfPeriod || blockTime > endOfPeriod) continue;
// 处理普通的 ETH 交易
for (const vin of transaction.vin) {
if (vin.addresses && vin.addresses.includes(result.address)) {
for (const vout of transaction.vout) {
if (vout.value === "0") continue;
extractedData.push({
txid: transaction.txid,
blockHeight: transaction.blockHeight,
direction: "Outgoing",
txType: "Normal",
assetType: "ETH",
senderAddress: result.address,
receiverAddress: vout.addresses.join(", "),
value: viem.formatEther(BigInt(vout.value)),
fee: viem.formatEther(BigInt(transaction.fees)),
day,
timestamp,
userTimezone,
status,
methodNameOrId,
});
}
}
}
for (const vout of transaction.vout) {
if (vout.addresses && vout.addresses.includes(result.address)) {
extractedData.push({
txid: transaction.txid,
blockHeight: transaction.blockHeight,
direction: "Incoming",
txType: "Normal",
assetType: "ETH",
senderAddress: transaction.vin.map((vin) => vin.addresses).join(", "),
receiverAddress: result.address,
value: viem.formatEther(BigInt(vout.value)),
fee: viem.formatEther(BigInt(transaction.fees)),
day,
timestamp,
userTimezone,
status,
methodNameOrId,
});
}
}
// 处理 ETH 内部转账
if (transaction.ethereumSpecific?.internalTransfers) {
for (const transfer of transaction.ethereumSpecific.internalTransfers) {
if (
transfer.from === result.address ||
transfer.to === result.address
) {
const direction =
transfer.from === result.address ? "Outgoing" : "Incoming";
extractedData.push({
txid: transaction.txid,
blockHeight: transaction.blockHeight,
direction: direction as "Outgoing" | "Incoming",
txType: "Internal",
assetType: "ETH",
senderAddress: transfer.from,
receiverAddress: transfer.to,
value: viem.formatEther(BigInt(transfer.value)),
fee: viem.formatEther(BigInt(transaction.fees)),
day,
timestamp,
userTimezone,
status,
methodNameOrId,
});
}
}
}
// 处理代币转账
if (transaction.tokenTransfers) {
for (const tokenTransfer of transaction.tokenTransfers) {
if (
tokenTransfer.from === result.address ||
tokenTransfer.to === result.address
) {
const direction =
tokenTransfer.from === result.address ? "Outgoing" : "Incoming";
const assetType =
tokenTransfer.name && tokenTransfer.symbol
? `${tokenTransfer.name} (${tokenTransfer.symbol})`
: "N/A";
let value = "";
let tokenId = "";
switch (tokenTransfer.type) {
case "ERC1155":
if (tokenTransfer.multiTokenValues) {
const tokens = tokenTransfer.multiTokenValues;
tokens.forEach((token, index) => {
tokenId += token.id + (index < tokens.length - 1 ? ", " : "");
value +=
token.value + (index < tokens.length - 1 ? ", " : "");
});
} else {
// 处理没有 multiTokenValues 的情况
tokenId = "N/A";
value = "N/A";
}
break;
case "ERC721":
value = "1";
tokenId = tokenTransfer.value;
break;
case "ERC20":
// 使用其十进制值标准化处理 ERC20 代币
value = viem.formatUnits(
BigInt(tokenTransfer.value),
tokenTransfer.decimals
);
tokenId = "N/A";
break;
default:
continue;
}
extractedData.push({
txid: transaction.txid,
blockHeight: transaction.blockHeight,
direction: direction as "Outgoing" | "Incoming",
txType: tokenTransfer.type,
assetType: assetType,
senderAddress: tokenTransfer.from,
receiverAddress: tokenTransfer.to,
value: value,
fee: viem.formatEther(BigInt(transaction.fees)),
day,
timestamp,
userTimezone,
status,
methodNameOrId,
contract: tokenTransfer.contract,
tokenId: tokenId,
});
}
}
}
}
const extendedResult: ExtendedResult = {
...result,
extractedTransaction: extractedData,
startDate: startOfPeriod,
endDate: endOfPeriod,
};
return extendedResult;
}
convertToCsv.ts
文件将处理后的区块链数据转换为 CSV 格式,使其适合分析和分发。
使用你的代码编辑器打开 src/helpers/convertToCsv.ts
文件并按如下方式修改该文件:
src/helpers/convertToCsv.ts
import { ExtractedTransaction } from "../interfaces.ts";
const convertToCSV = (data: ExtractedTransaction[]) => {
const csvRows = [];
// 标题
csvRows.push(
[
"日",
"时间",
"区块",
"交易 ID",
"交易状态",
"交易类型",
"资产",
"发送者地址",
"方向",
"接收者地址",
"金额",
"代币 ID",
"费用 [ETH]",
"方法名称/ID",
].join(",")
);
// 行
data.forEach((tx) => {
const row = []; // 为此行创建一个空数组
row.push(tx.day);
row.push(
new Date(tx.timestamp).toLocaleTimeString("en-US", {
timeZone: tx.userTimezone,
timeZoneName: "short",
})
);
row.push(tx.blockHeight);
row.push(tx.txid);
row.push(tx.status);
row.push(tx.txType);
row.push(tx.assetType);
row.push(tx.senderAddress);
row.push(tx.direction);
row.push(tx.receiverAddress);
row.push(tx.value);
row.push(tx.tokenId ? tx.tokenId : "N/A");
row.push(tx.fee);
row.push(
tx.methodNameOrId.startsWith("0x")
? `"${tx.methodNameOrId}"`
: tx.methodNameOrId
);
csvRows.push(row.join(",")); // 连接每行的列并推送
});
return csvRows.join("\n"); // 连接所有行
};
export const copyAsCSV = (data: ExtractedTransaction[]) => {
const csvData = convertToCSV(data);
navigator.clipboard.writeText(csvData).then(
() => console.log("CSV copied to clipboard"),
(err) => console.error("Failed to copy CSV: ", err)
);
};
export const exportAsCSV = (
data: ExtractedTransaction[],
filename = "ethereum-report.csv"
) => {
const csvData = convertToCSV(data);
const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
// 创建一个链接来下载 blob
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.style.visibility = "hidden";
// 追加到文档并触发下载
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
};
CopyIcon.tsx
文件是一个 React 组件,提供用于将文本复制到剪贴板的用户界面元素。
使用你的代码编辑器打开 src/components/CopyIcon.tsx
文件并按如下方式修改该文件:
src/components/CopyIcon.tsx
import React from "react";
const CopyIcon: React.FC = () => (
<svg
fill="#000000"
height="16"
width="16"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 64"
enableBackground="new 0 0 64 64"
>
<g id="Text-files">
<path
d="M53.9791489,9.1429005H50.010849c-0.0826988,0-0.1562004,0.0283995-0.2331009,0.0469999V5.0228
C49.7777481,2.253,47.4731483,0,44.6398468,0h-34.422596C7.3839517,0,5.0793519,2.253,5.0793519,5.0228v46.8432999
c0,2.7697983,2.3045998,5.0228004,5.1378999,5.0228004h6.0367002v2.2678986C16.253952,61.8274002,18.4702511,64,21.1954517,64
h32.783699c2.7252007,0,4.9414978-2.1725998,4.9414978-4.8432007V13.9861002
C58.9206467,11.3155003,56.7043495,9.1429005,53.9791489,9.1429005z M7.1110516,51.8661003V5.0228
c0-1.6487999,1.3938999-2.9909999,3.1062002-2.9909999h34.422596c1.7123032,0,3.1062012,1.3422,3.1062012,2.9909999v46.8432999
c0,1.6487999-1.393898,2.9911003-3.1062012,2.9911003h-34.422596C8.5049515,54.8572006,7.1110516,53.5149002,7.1110516,51.8661003z
M56.8888474,59.1567993c0,1.550602-1.3055,2.8115005-2.9096985,2.8115005h-32.783699
c-1.6042004,0-2.9097996-1.2608986-2.9097996-2.8115005v-2.2678986h26.3541946
c2.8333015,0,5.1379013-2.2530022,5.1379013-5.0228004V11.1275997c0.0769005,0.0186005,0.1504021,0```markdown
const ReportForm: React.FC<ReportFormProps> = ({ onSubmit, isLoading }) => {
const [address, setAddress] = useState("");
const [isValidAddress, setIsValidAddress] = useState(false);
const [startDate, setStartDate] = useState(
() => new Date().toISOString().split("T")[0]
); // default to today's date
const [endDate, setEndDate] = useState(
() => new Date().toISOString().split("T")[0]
); // default to today's date
const [timezone, setTimezone] = useState("UTC");
/* eslint-disable @typescript-eslint/no-explicit-any */
const handleAddressChange = (e: any) => {
const inputAddress = e.target.value;
setAddress(inputAddress);
setIsValidAddress(viem.isAddress(inputAddress));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(address, startDate, endDate, timezone);
};
/* eslint-disable @typescript-eslint/no-explicit-any */
const timezones = (Intl as any).supportedValuesOf("timeZone") as string[];
if (!timezones.includes("UTC")) {
timezones.unshift("UTC");
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="address" className="block">
Ethereum Address
</label>
<input
type="text"
id="address"
name="address"
value={address}
onChange={handleAddressChange}
className="border p-2 w-full"
required
/>
{!isValidAddress && address && (
<div className="text-red-500">
This is not a valid Ethereum address.
</div>
)}
</div>
<div className="flex space-x-3 ">
<div>
<label htmlFor="startDate" className="block">
Start Date
</label>
<input
type="date"
id="startDate"
name="startDate"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="border p-2"
required
/>
</div>
<div>
<label htmlFor="endDate" className="block">
End Date
</label>
<input
type="date"
id="endDate"
name="endDate"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="border p-2"
required
/>
</div>
<div>
<label htmlFor="timezone" className="block">
Timezone
</label>
<select
id="timezone"
name="timezone"
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
className="border p-2"
>
{timezones.map((timezones) => (
<option key={timezones} value={timezones}>
{timezones}
</option>
))}
</select>
</div>
</div>
<button
type="submit"
disabled={!isValidAddress}
className={`${
isValidAddress ? "bg-blue-500" : "bg-gray-500 cursor-not-allowed"
} text-white px-4 py-2 rounded`}
>
{isLoading ? "Loading..." : "Generate"}
</button>
</form>
);
};
export default ReportForm;
ResultTable.tsx
文件是一个 React 组件,它以表格格式显示交易报告数据,使用户可以轻松读取和分析信息。
使用你的代码编辑器打开 src/components/ResultTable.tsx
文件,并按如下方式修改该文件:
src/components/ResultTable.tsx
import React from "react";
import { ExtendedResult } from "../interfaces.ts";
import CopyIcon from "./CopyIcon.tsx";
import { exportAsCSV, copyAsCSV } from "../helpers/convertToCsv.ts";
interface ResultsTableProps {
data: ExtendedResult;
}
function shortenAddress(address: string) {
if (address.length < 10) {
return address;
}
return `${address.slice(0, 5)}...${address.slice(-4)}`;
}
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(
() => {
console.log("Copied to clipboard!");
},
(err) => {
console.error("Could not copy text: ", err);
}
);
}
const ResultsTable: React.FC<ResultsTableProps> = ({ data }) => {
return (
<div className="overflow-x-auto mt-6">
<div>
<h3>Address: {data.address}</h3>
<p>Current Balance: {parseFloat(data.balance) / 1e18} ETH</p>
<p>Nonce: {data.nonce}</p>
<p>Total Transactions: {data.txs}</p>
<p>Non-Token Transactions: {data.nonTokenTxs}</p>
<p>Internal Transactions: {data.internalTxs}</p>
</div>
<div className="my-4 flex space-x-4">
<button
onClick={() => exportAsCSV(data.extractedTransaction)}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Export as CSV
</button>
<button
onClick={() => copyAsCSV(data.extractedTransaction)}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Copy as CSV
</button>
</div>
<table className="min-w-full table-fixed text-xs">
<thead className="bg-blue-100">
<tr>
<th className="p-2 text-center">Day</th>
<th className="p-2 text-center">Time</th>
<th className="p-2 text-center">Block</th>
<th className="p-2 text-center">Transaction ID</th>
<th className="p-2 text-center">Transaction Status</th>
<th className="p-2 text-center">Transaction Type</th>
<th className="p-2 text-center">Asset</th>
<th className="p-2 text-center">Sender Address</th>
<th className="p-2 text-center">Direction</th>
<th className="p-2 text-center">Receiver Address</th>
<th className="p-2 text-center">Amount</th>
<th className="p-2 text-center">Token ID</th>
<th className="p-2 text-center">Fees</th>
<th className="p-2 text-center">Method Name/ID</th>
</tr>
</thead>
<tbody>
{data.extractedTransaction.map((tx, index) => (
<tr key={index} className="border-t">
<td className="p-2 text-center">{tx.day}</td>
<td className="p-2 text-center">
{new Date(tx.timestamp).toLocaleTimeString("en-US", {
timeZone: tx.userTimezone,
timeZoneName: "short",
})}
</td>
<td className="p-2 text-center">{tx.blockHeight}</td>
<td
className="p-2 flex items-top justify-center space-x-2 cursor-pointer"
onClick={() => copyToClipboard(tx.txid)}
>
<a
href={`https://etherscan.io/tx/${tx.txid}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
{shortenAddress(tx.txid)}
</a>
<CopyIcon />
</td>
<td className="p-2 text-center">{tx.status}</td>
<td className="p-2 text-center">{tx.txType}</td>
<td className="p-2 text-center">
{(tx.txType === "ERC20" ||
tx.txType === "ERC721" ||
tx.txType === "ERC1155") &&
tx.contract ? (
<a
href={`https://etherscan.io/token/${tx.contract}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800"
>
{tx.assetType}
</a>
) : (
tx.assetType
)}
</td>
<td
className="p-2 flex items-center justify-center space-x-2 cursor-pointer"
onClick={() => copyToClipboard(tx.senderAddress)}
>
<span>{shortenAddress(tx.senderAddress)}</span>
<CopyIcon />
</td>
<td className="p-2 text-center">{tx.direction}</td>
<td
className="p-2 flex items-center justify-center space-x-2 cursor-pointer"
onClick={() => copyToClipboard(tx.receiverAddress)}
>
<span>{shortenAddress(tx.receiverAddress)}</span>
<CopyIcon />
</td>
<td
className="p-2 text-center"
style={{ wordBreak: "break-word" }}
>
{tx.value}
</td>
<td
className="p-2 text-center"
style={{ wordBreak: "break-word" }}
>
{tx.tokenId ? tx.tokenId : "N/A"}
</td>
<td
className="p-2 text-center"
style={{ wordBreak: "break-word" }}
>
{tx.fee + " ETH"}
</td>
<td className="p-2 text-center">{tx.methodNameOrId}</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default ResultsTable;
App.tsx
文件充当你的 React 应用程序的主要组件。它导入并使用 ReportForm.tsx
和 ResultTable.tsx
组件来创建有凝聚力的用户界面。它还管理状态并处理用户输入提交。
使用你的代码编辑器打开 src/App.tsx
文件,并按如下方式修改该文件:
src/App.tsx
// src/App.tsx
import React, { useState } from "react";
import "./index.css";
import ReportForm from "./components/ReportForm.tsx";
import ResultTable from "./components/ResultTable.tsx";
import { bb_getAddress } from "./helpers/blockbookMethods.ts";
import { calculateVariables } from "./helpers/calculateVariables.ts";
import { ExtendedResult, CalculateVariablesOptions } from "./interfaces.ts";
import { DateTime } from "luxon";
const App = () => {
const [reportData, setReportData] = useState<ExtendedResult | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const handleFormSubmit = (
address: string,
startDate: string,
endDate: string,
timezone: string
) => {
setLoading(true); // Start loading
const configStartDate = DateTime.fromISO(startDate, {
zone: timezone,
});
const configEndDate = DateTime.fromISO(endDate, {
zone: timezone,
});
const options: CalculateVariablesOptions = {
startDate: configStartDate,
endDate: configEndDate,
userTimezone: timezone,
};
bb_getAddress(address)
.then((data) => {
return calculateVariables(data, options);
})
.then((extendedData) => {
setLoading(false);
setReportData(extendedData);
})
.catch((error) => {
setLoading(false);
console.error(error);
});
};
return (
<div className="min-h-screen flex flex-col bg-blue-50">
<header className="bg-blue-200 text-xl text-center p-4">
Ethereum Transaction Report Generator
</header>
<main className="flex-grow container mx-auto p-4">
<ReportForm onSubmit={handleFormSubmit} isLoading={loading} />
{reportData && <ResultTable data={reportData} />}
</main>
<footer className="bg-blue-200 text-center p-4">
Created with ❤️ and{" "}
<a href="https://www.quicknode.com" className="text-blue-500">
QuickNode
</a>
</footer>
</div>
);
};
export default App;
最后,启动你的开发服务器以查看你的应用程序的运行效果。在你的终端运行以下命令:
npm run dev
打开你的浏览器并转到 http://localhost:5173 以查看你的应用程序正在运行。
要使用以太坊交易报告生成器,请按照以下步骤操作:
通过执行这些步骤,你可以高效地生成、查看和导出任何以太坊地址的详细交易报告。
使用 QuickNode 的 EVM Blockbook JSON-RPC 插件,我们的以太坊交易报告生成器简化了为以太坊地址创建详细交易报告的过程。 本指南涵盖了设置和使用该工具的基本步骤,但你还可以做很多事情。 无论是用于审计、法规遵从还是市场分析,此应用程序都可以直接高效地提取和分析区块链数据。
要了解有关 QuickNode 如何帮助你提取各种用例的详细区块链数据的更多信息,请随时 联系我们; 我们很乐意与你交谈!
订阅我们的 新闻通讯,以获取有关 Web3 和区块链的更多文章和指南。 如果你有任何疑问或需要进一步的帮助,请随时加入我们的 Discord 服务器或使用下面的表格提供反馈。 请在 Twitter (@QuickNode) 和我们的 Telegram 公告频道 上关注我们,以随时了解最新信息。
如果你有任何反馈或对新主题的请求,请告诉我们。 我们很乐意听取你的意见。
>- 原文链接: [quicknode.com/guides/mar...](https://www.quicknode.com/guides/marketplace/marketplace-add-ons/how-to-generate-ethereum-transaction-reports-with-blockbook)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!