如何使用Blockbook生成以太坊交易报告

本文介绍了如何使用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 插件概述

EVM Blockbook JSON-RPC 插件 允许你通过 JSON-RPC 访问地址的余额、交易和地址余额历史记录。此插件利用 Blockbook REST API,该 API 旨在提供对区块链数据的高效查询,包括对智能合约、原生 ETH 转账、内部交易和代币转账的详细分析。

优于标准 EVM 方法的优势

  • 详细数据:与标准 EVM 方法相比,Blockbook JSON-RPC 插件返回更详细的数据,包括内部交易和代币转账。
  • 高效查询:它简化了对交易和余额执行高效查询的操作,降低了与区块链交互的复杂性。
  • 货币转换:允许检索特定货币(例如,美元、欧元)和具体时间戳的日期对应的货币汇率,从而更轻松地执行财务分析和报告。

在撰写本文时,EVM Blockbook 插件提供了 8 个 RPC 方法。我们将在此指南中使用其中一种方法:

  • bb_getAddress:返回地址的余额和交易。返回的交易按区块高度排序,最新的区块在前。

设置以太坊端点

在开始之前,请注意 EVM Blockbook JSON-RPC 是一项付费插件。请在此处 查看详细信息,并根据你的需求比较套餐。

使用 EVM Blockbook JSON-RPC 设置你的以太坊端点非常容易。如果你尚未注册,可以在此处 创建一个帐户。

登录后,导航到 端点 页面,然后单击 创建端点。选择 以太坊主网,然后单击下一步。然后,系统将提示你配置插件。激活 EVM Blockbook JSON-RPC。之后,只需单击 创建端点

如果你已经有一个没有插件的以太坊端点,请转到以太坊端点内的 插件 页面,选择 EVM Blockbook JSON-RPC,然后激活它。

Quicknode 端点页面

端点准备好后,复制 HTTP Provider 链接并妥善保管,因为你将在下一节中需要它。

使用 EVM Blockbook JSON-RPC 构建以太坊交易报告生成器

在开始之前,请确保你的机器上安装了 Node.js。Node.js 将成为运行应用程序的骨干,而 npm(Node.js 中包含的默认软件包管理器)将有效地处理所有依赖项。你可以在 他们的官方页面 上找到安装说明。

此外,如果你尚未安装 TypeScript,请将其设置为全局安装,以便在所有项目中可用,方法是运行以下命令:

npm install -g typescript ts-node

设置项目

在开始编码之前,让我们看一下我们将要构建的内容。最后,我们的应用程序将类似于下图所示。

应用程序概述

步骤 1:初始化一个新的 Vite 项目

为你的项目创建一个目录,并在其中初始化一个新的 Vite 项目:

npm create vite@latest ethereum-transaction-reports -- --template react-ts
cd ethereum-transaction-reports

此命令将创建一个名为 ethereum-transaction-reports 的新目录,其中包含 React 和 TypeScript 的 Vite 项目模板,然后将你当前的目录更改为新的项目文件夹。

步骤 2:安装必要的软件包

继续安装必要的软件包:

npm install axios luxon dotenv fs-extra @quicknode/sdk
npm i --save-dev @types/fs-extra @types/luxon tailwindcss postcss autoprefixer

📘 软件包

  • axios:一个基于 Promise 的 HTTP 客户端,用于向外部服务发出请求,非常适合从 API 获取数据。
  • luxon:一个强大的现代 JavaScript 库,用于处理日期和时间。Luxon 用于处理和操作日期和时间,这对于有效地处理交易时间戳至关重要。
  • dotenv:一个零依赖模块,可将环境变量从 .env 文件加载到 process.env 中,确保敏感信息得到安全保护。
  • fs-extra:一个用于与文件系统交互的模块,例如写入文件和读取文件。
  • @quicknode/sdk:QuickNode SDK 提供了对 QuickNode 基础设施的便捷访问,从而更容易与以太坊区块链进行交互。此 SDK 增强了应用程序连接、查询和与以太坊交互的能力,从而简化了复杂的区块链操作。
  • @types/fs-extra@types/luxon:这些是 DefinitelyTyped 软件包,可为 fs-extraluxon 提供 TypeScript 类型定义。
  • tailwindcss:一个实用类优先的 CSS 框架,提供一组预定义的类,以帮助构建自定义设计,而无需编写自定义 CSS。Tailwind CSS 提倡基于设计系统的方法并加快样式设置过程。
  • postcss:一个用于使用 JavaScript 插件转换 CSS 的工具。PostCSS 允许你使用插件来自动执行例行的 CSS 任务,例如优化样式或与 CSS 预处理器和框架集成。
  • autoprefixer:一个 PostCSS 插件,可解析你的 CSS 并将供应商前缀添加到 CSS 规则。它通过自动应用必要的前缀来帮助确保与不同浏览器的兼容性。

步骤 3:设置 Tailwind CSS

现在,通过运行以下命令在项目中设置 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.tsxResultTable.tsx 以组装用户界面,而 blockbookMethods.tscalculateVariables.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 格式,使其适合分析和报告。

现在,让我们开始编码。

步骤 1:创建必要的文件

在你的项目的目录(即 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 组件,以表格格式显示交易报告数据,使用户可以轻松读取和分析信息。

步骤 2:配置环境变量

将你的 QuickNode 端点和其他敏感信息(如果有)存储在 .env 文件中。

打开 .env 文件并按如下方式对其进行修改。不要忘记将 YOUR_QUICKNODE_ETHEREUM_ENDPOINT_URL 占位符替换为你的 QuickNode 以太坊 HTTP 提供程序 URL。

.env

VITE_QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ETHEREUM_ENDPOINT_URL"

步骤 3:创建接口

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;
}

步骤 4:使用 Blockbook 获取数据

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;
  }
}

步骤 5:处理数据

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;
}

步骤 6:将数据转换为 CSV

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);
};

步骤 7:创建复制图标组件

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;

第 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("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;

第 10 步:组装应用程序

App.tsx 文件充当你的 React 应用程序的主要组件。它导入并使用 ReportForm.tsxResultTable.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;

第 11 步:运行应用程序

最后,启动你的开发服务器以查看你的应用程序的运行效果。在你的终端运行以下命令:

npm run dev

打开你的浏览器并转到 http://localhost:5173 以查看你的应用程序正在运行。

App Overview

与应用程序交互

要使用以太坊交易报告生成器,请按照以下步骤操作:

  1. 输入以太坊地址
  • 在提供的输入字段中输入要为其生成交易报告的以太坊地址。 如果输入的地址不是以太坊地址,应用程序将显示警告并且不会激活该按钮。
  1. 指定日期间隔(可选)
  • (可选)设置开始日期和结束日期,以过滤特定时间范围内的交易,并从下拉菜单中选择适当的时区,以确保日期和时间被正确过滤。
  1. 生成报告
  • 单击“生成”按钮以根据输入条件获取和处理交易数据。
  1. 查看结果
  • 交易数据将以表格格式显示在表格下方。 该表包括详细信息,例如日期、时间、区块号、交易 ID、交易状态、类型、资产、发送者和接收者地址、金额、Token ID、费用和方法名称/ID。
  1. 导出或复制数据
  • 导出为 CSV:单击“导出为 CSV”按钮以将交易数据下载为 CSV 文件。
  • 复制为 CSV:单击“复制为 CSV”按钮以将交易数据以 CSV 格式复制到剪贴板,以便轻松粘贴到其他应用程序中。

通过执行这些步骤,你可以高效地生成、查看和导出任何以太坊地址的详细交易报告。

结论

使用 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。