本文详细介绍了如何使用QuickNode的EVM Blockbook JSON-RPC附加组件生成以太坊交易报告,涵盖ERC-20、ERC-721和ERC-1155代币转移等交易。文章提供了丰富的实施步骤,包括所需工具、设置以太坊端点、构建用户界面及数据处理等,以期满足球类合规报告的需求。
合规报告解决方案是当今数字金融专业人士至关重要的工具。本教程将指导你使用 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 构建一个用户界面,该界面在后端利用 EVM Blockbook JSON-RPC 附加组件 根据给定地址检索以太坊交易。
快速入门选项
如果你希望立即开始使用该应用程序,而不是从头构建它,我们提供了一个可供使用的解决方案。只需访问我们的 GitHub 仓库 克隆样本应用程序。你只需要提供自己的端点 URL。请按照仓库中的 README 进行逐步指导,以快速设置和运行该应用程序。
依赖项 | 版本 |
---|---|
node.js | >18.16 |
typescript | latest |
ts-node | latest |
EVM Blockbook JSON-RPC 附加组件 通过 JSON-RPC 使你能够访问地址的余额、交易和地址余额历史。该附加组件利用 Blockbook REST API,旨在提供区块链数据的高效查询,包括智能合约的详细分析、原生以太币转账、内部交易和代币转账。
截至本书写作时,EVM Blockbook 附加组件提供 8 个 RPC 方法。我们将在本指南中使用其中之一:
bb_getAddress
:返回地址的余额和交易。返回的交易按区块高度排序,最新的区块在前。在你开始之前,请注意 EVM Blockbook JSON-RPC 是一个付费附加组件。请查看详情 这里 并根据你的需求比较计划。
设置带有 EVM Blockbook JSON-RPC 的以太坊端点非常简单。如果你还没有注册,可以 在这里 创建一个账户。
登录后,导航至 Endpoints 页面并点击 创建端点。选择 以太坊主网,然后点击下一步。接下来,你将被提示配置附加组件。激活 EVM Blockbook JSON-RPC。然后,只需点击 创建端点。
如果你已经有一个没有附加组件的以太坊端点,请进入 Ethereum 端点的 附加组件 页面,选择 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
函数来自 calculateVariables.ts
,负责处理这些数据。该函数处理不同方面,如代币转账、智能合约交互和标准以太坊交易,以提供结构化数据集。
显示结果:一旦数据处理完成,数据将传递给 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
文件并修改如下,别忘了用你的 QuickNode 以太坊 HTTP 提供商 URL 替换 YOUR_QUICKNODE_ETHEREUM_ENDPOINT_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 端点交互的函数。这些函数 facilitate 提取特定以太坊地址的详细交易数据,并获取代币转账的详细信息。
用你的代码编辑器打开 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 方法准备请求负载
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("获取交易失败");
}
} 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 ? "已确认" : "待处理";
let methodNameOrId = "";
if (transaction.ethereumSpecific?.parsedData) {
const { name, methodId } = transaction.ethereumSpecific.parsedData;
if (name && methodId) {
methodNameOrId = `${name} (${methodId})`;
} else {
methodNameOrId = name || methodId || "未知";
}
}
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: "普通",
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: "普通",
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: "内部",
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("zh-CN", {
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 复制到剪贴板"),
(err) => console.error("复制 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.0469999,0.2331009,0.0469999
h3.9682999c1.6041985,0,2.9096985,1.2609005,2.9096985,2.8115005V59.1567993z"
/>
<path
d="M38.6031494,13.2063999H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0158005
c0,0.5615997,0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4542999,1.0158997-1.0158997
C39.6190491,13.6606998,39.16465,13.2063999,38.6031494,13.2063999z"
/>
<path
d="M38.6031494,21.3334007H16.253952c-0.5615005,0-1.0159006,0.4542999-1.0159006,1.0157986
c0,0.5615005,0.4544001,1.0159016,1.0159006,1.0159016h22.3491974c0.5615005,0,1.0158997-0.454401,1.0158997-1.0159016
C39.6190491,21.7877007,39.16465,21.3334007,38.6031494,21.3334007z"
/>
<path
d="M38.6031494,29.4603004H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
s0.4544001,1.0158997,1.0159006,1.0158997h22.3491974c0.5615005,0,1.0158997-0.4543991,1.0158997-1.0158997
S39.16465,29.4603004,38.6031494,29.4603004z"
/>
<path
d="M28.4444485,37.5872993H16.253952c-0.5615005,0-1.0159006,0.4543991-1.0159006,1.0158997
s0.4544001,1.0158997,1.0159006,1.0158997h12.1904964c0.5615025,0,1.0158005-0.4543991,1.0158005-1.0158997
S29.0059509,37.5872993,28.4444485,37.5872993z"
/>
</g>
</svg>
);
export default CopyIcon;
ReportForm.tsx
文件是一个 React 组件,用于呈现供用户输入参数(如以太坊地址和日期范围)的表单。此表单用于根据用户输入生成特定交易报告。
用你的代码编辑器打开 src/components/ReportForm.tsx
文件,并将文件修改为如下内容:
src/components/ReportForm.tsx
// src/components/ReportForm.tsx
import React, { useState } from "react";
import { viem } from "@quicknode/sdk";
interface ReportFormProps {
onSubmit: (
address: string,
startDate: string,
endDate: string,
timezone: string
) => void;
isLoading: boolean;
}
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]
); // 默认今天日期
const [endDate, setEndDate] = useState(
() => new Date().toISOString().split("T")[0]
); // 默认今天日期
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");
}
请将该部分内容在整个文档中进一步完成。```jsx return ( <form onSubmit={handleSubmit} className="space-y-4"> <div> <label htmlFor="address" className="block"> 以太坊地址 </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"> 这不是一个有效的以太坊地址。 </div> )} </div> <div className="flex space-x-3 "> <div> <label htmlFor="startDate" className="block"> 开始日期 </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"> 结束日期 </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"> 时区 </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 ? "加载中..." : "生成"} </button> </form> ); };
export default ReportForm;
#### 第9步:创建结果表组件
`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("已复制到剪贴板!"); }, (err) => { console.error("无法复制文本:", err); } ); }
const ResultsTable: React.FC<ResultsTableProps> = ({ data }) => { return ( <div className="overflow-x-auto mt-6"> <div> <h3>地址: {data.address}</h3> <p>当前余额: {parseFloat(data.balance) / 1e18} ETH</p> <p>Nonce: {data.nonce}</p> <p>总交易数: {data.txs}</p> <p>非代币交易数: {data.nonTokenTxs}</p> <p>内部交易数: {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"
导出为CSV </button> <button onClick={() => copyAsCSV(data.extractedTransaction)} className="bg-blue-500 text-white px-4 py-2 rounded"
复制为CSV </button> </div> <table className="min-w-full table-fixed text-xs"> <thead className="bg-blue-100"> <tr> <th className="p-2 text-center">日期</th> <th className="p-2 text-center">时间</th> <th className="p-2 text-center">区块</th> <th className="p-2 text-center">交易ID</th> <th className="p-2 text-center">交易状态</th> <th className="p-2 text-center">交易类型</th> <th className="p-2 text-center">资产</th> <th className="p-2 text-center">发送者地址</th> <th className="p-2 text-center">方向</th> <th className="p-2 text-center">接收者地址</th> <th className="p-2 text-center">金额</th> <th className="p-2 text-center">代币ID</th> <th className="p-2 text-center">费用</th> <th className="p-2 text-center">方法名称/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;
#### 第10步:组装应用程序
`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); // 开始加载
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"> 以太坊交易报告生成器 </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"> 由 ❤️ 和{" "} <a href="https://www.quicknode.com" className="text-blue-500"> QuickNode </a> 创建 </footer> </div> ); };
export default App;
#### 第11步:运行应用
最后,启动你的开发服务器以查看你的应用程序在运行。请在终端中运行以下命令:
npm run dev
打开你的浏览器并访问 [http://localhost:5173](http://localhost:5173/) 以查看你的应用程序正在运行。

### 与应用交互
要使用以太坊交易报告生成器,请按照以下步骤操作:
1. **输入以太坊地址**:
- 在提供的输入字段中输入你想要生成交易报告的以太坊地址。如果输入的地址不是一个以太坊地址,应用将显示警告并且不会激活按钮。
2. **指定日期间隔(可选)**:
- 可选设置开始日期和结束日期,以便在特定时间范围内过滤交易,并从下拉菜单中选择合适的时区,以确保正确过滤日期和时间。
3. **生成报告**:
- 单击“生成”按钮以根据输入条件获取和处理交易数据。
4. **查看结果**:
- 交易数据将在表单下方以表格形式显示。该表格包括交易的日期、时间、区块号、交易ID、交易状态、类型、资产、发送者和接收者地址、金额、代币ID、费用和方法名称/ID等详细信息。
5. **导出或复制数据**:
- 导出为 CSV:单击“导出为 CSV”按钮下载交易数据的 CSV 文件。
- 复制为 CSV:单击“复制为 CSV”按钮将交易数据复制到你的剪贴板,以 CSV 格式便于粘贴到其他应用程序中。
通过遵循这些步骤,你可以高效生成、查看和导出任何以太坊地址的详细交易报告。
## 结论
使用 QuickNode 的 [EVM Blockbook JSON-RPC 附加组件](https://marketplace.quicknode.com/add-on/evm-blockbook-json-rpc),我们的以太坊交易报告生成器简化了为以太坊地址创建详细交易报告的过程。本指南涵盖了设置和使用该工具的基本步骤,但你可以做的事情还有很多。无论是进行审计、合规,还是市场分析,该应用程序都使提取和分析区块链数据变得简单且高效。
要了解有关 QuickNode 如何帮助你提取各种用例的详细区块链数据的更多信息,请随时 [联系我们](https://www.quicknode.com/contact-us);我们很乐意与你交谈!
订阅我们的 [新闻通讯](https://go.quicknode.com/newsletter),获取更多 Web3 和区块链的文章和指南。如果你有任何疑问或需要进一步帮助,请随时加入我们的 [Discord](https://discord.gg/quicknode) 服务器或使用下面的表单提供反馈。通过关注我们的 [Twitter](https://twitter.com/QuickNode) (@QuickNode) 和我们的 [Telegram 宣传频道](https://t.me/quicknodehq) 获取最新动态。
#### 我们 ❤️ 反馈!
[让我们知道](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=How%20to%20Generate%20Ethereum%20Transaction%20Reports%20with%20Blockbook) 如果你有任何反馈或新主题请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/qui...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!