跳到内容

如何在活动票务 Dapp 中集成 MetaMask Gas API

在本博客中,我们探讨了如何将 MetaMask Gas API 集成到使用 Next.js 和 ShadcnUI 构建的现有票务 dApp 中。

6 分钟阅读
如何在活动票务 Dapp 中集成 MetaMask Gas API

在购买基于区块链的活动门票之前,了解 gas 价格对于成本透明度和有效的财务规划至关重要。在区块链交易中,"gas"是指在区块链上成功执行交易或合约所需支付的费用。

该费用会根据网络需求波动。提前了解 gas 价格,购票者可以对购买总成本做出更明智的决策。这一点尤为重要,因为 gas 费用可能会显著增加门票的总体成本,尤其是在网络拥堵严重的时期。

在本教程中,我们将在现有的活动售票 dapp 中集成 MetaMask Gas API,使用户在购买活动门票时能够查看 gas 价格。

前提条件与依赖项

前端框架将使用 Next.js,UI 将使用 ShadcnUI。让我们安装以下 ShadcnUI 组件:

npx shadcn-ui@latest add hover-card
npm i swr
复制

上述代码将添加 hover-card 组件(用于显示 gas 价格)并安装 SWR React hooks 库。Stale-While-Revalidate(SWR)库有助于从网络获取数据。其工作原理是:先使用已缓存的旧数据,再请求新数据,最后用新数据替换旧数据。这使网站更快速、更及时。

接下来,我们将安装 react-circular-progressbar 组件,用于显示环形进度指示器。该组件适用于在 Web 应用中展示任务进度或加载状态,支持自定义配置,并使用 SVG 实现高质量图形渲染。

npm i react-circular-progressbar

集成 Gas API

在 app/web 目录下,创建以下文件夹:app/api/gas。在其中创建 route.ts 文件,并添加以下代码:

import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const chainId = searchParams.get("chainId");

  const Auth = Buffer.from(
    process.env.INFURA_API_KEY + ":" + process.env.INFURA_API_SECRET
  ).toString("base64");

  const gasPricesResp = await fetch(
    `https://gas.api.infura.io/networks/${chainId}/suggestedGasFees`,
    {
      headers: {
        Authorization: `Basic ${Auth}`,
      },
    }
  );

  const baseFeePercentileRes = await fetch(
    `https://gas.api.infura.io/networks/${chainId}/baseFeePercentile`,
    {
      headers: {
        Authorization: `Basic ${Auth}`,
      },
    }
  );

  const gasPricesData = await gasPricesResp.json();
  const baseFeePercentileData = await baseFeePercentileRes.json();

  return NextResponse.json({
    estimatedBaseFee: gasPricesData.estimatedBaseFee,
    baseFeeTrend: gasPricesData.baseFeeTrend,
    baseFeePercentile: baseFeePercentileData.baseFeePercentile,
  });
}
复制

以下是具体实现说明:

  1. 提取查询参数:- const { searchParams } = new URL(request.url);:从请求 URL 中提取查询参数。

  • const chainId = searchParams.get("chainId");:从 URL 中获取 chainId 参数,用于指定以太坊网络。

  1. 配置授权:- 将环境变量 INFURA_API_KEY 和 INFURA_API_SECRET 合并写入 Buffer,再编码为 Base64 字符串。该字符串用于向 Infura API 发起请求时的 HTTP 基本认证。

  2. 获取 Gas 价格和基础费用百分位数据:- 向 Infura API 发起两个异步 fetch 请求:- gasPricesResp 获取指定以太坊网络(chainId)的建议 gas 费用。

  • baseFeePercentileRes 获取同一网络的基础费用百分位。

  • Authorization 请求头设置为 Base64 编码的凭证。

  1. 解析并返回数据:- 将两个 fetch 请求的响应转换为 JSON。

  • NextResponse.json({...}) 创建并返回包含以下数据的 JSON 响应:- estimatedBaseFee:交易的预估基础费用。

  • baseFeeTrend:基础费用趋势(如上涨、下降)。

  • baseFeePercentile:基础费用的百分位分布。

创建用户界面

我们将创建一个名为 GasFeeCard.tsx 的组件,并添加以下代码:


import useSWR, { mutate } from "swr";
import {
  CircularProgressbarWithChildren,
  buildStyles,
} from "react-circular-progressbar";
import "react-circular-progressbar/dist/styles.css";
import {
  HoverCard,
  HoverCardContent,
  HoverCardTrigger,
} from "@/components/ui/hover-card";
import { useEffect, useState } from "react";
import { ArrowUp, ArrowDown, InfoIcon } from "lucide-react";
import { useSDK } from "@metamask/sdk-react";

const REFETCH_INTERVAL = 10000;
const STEP_INTERVAL = 1000;
const NETWORK_LATENCY = 2000; // rough estimate of network latency to consider when refetching

// format the gas price nicely for small values
const formatGweiValues = (value: string) => {
  const gwei = parseFloat(value);

  if (gwei < 0.01) {
    return gwei.toExponential(2);
  } else {
    return gwei.toFixed(2);
  }
};

export function GasFeeCard() {
  const { chainId } = useSDK();

  const [countdown, setCountdown] = useState(REFETCH_INTERVAL);
  const [currentChainId, setCurrentChainId] = useState(chainId);

  useEffect(() => {
    if (chainId !== currentChainId) {
      setCurrentChainId(chainId);
      mutate("gas-data");
      if (countdown < NETWORK_LATENCY) {
        setCountdown(REFETCH_INTERVAL + NETWORK_LATENCY);
      }
      setCountdown(REFETCH_INTERVAL);
    }
  }, [countdown, chainId, currentChainId, mutate]);

  useEffect(() => {
    const interval = setInterval(() => {
      setCountdown((oldCountdown) => {
        if (oldCountdown <= 0) {
          mutate("gas-data");
          return REFETCH_INTERVAL;
        } else {
          return oldCountdown - STEP_INTERVAL;
        }
      });
    }, STEP_INTERVAL);

    return () => clearInterval(interval);
  }, [chainId]);

  const fetcher = () =>
    fetch(`/api/gas?&chainId=${parseInt(chainId || "0xe704")}`).then((res) =>
      res.json()
    );

  const {
    data: gasData,
    error: gasDataError,
    isLoading: gasDataLoading,
  } = useSWR("gas-data", fetcher);

  if (gasDataLoading) {
    return <p>loading</p>;
  }

  if (gasDataError) {
    return <p className="text-red-500">error</p>;
  }

  const trendingUp = gasData?.baseFeeTrend === "up";
  const trendingDown = gasData?.baseFeeTrend === "down";

  return (
    <HoverCard>
      <HoverCardTrigger asChild>
        <div className="cursor-pointer">
          <div className="flex space-x-1">
            <div className="w-6 h-6">
              <CircularProgressbarWithChildren
                strokeWidth={12}
                styles={buildStyles({
                  pathColor: trendingUp ? "red" : "green",
                })}
                value={
                  ((REFETCH_INTERVAL - countdown) / REFETCH_INTERVAL) * 100
                }
              >
                <>
                  {trendingUp && (
                    <ArrowUp
                      className="animate-bounceUp"
                      color="red"
                      size={14}
                    />
                  )}
                  {trendingDown && (
                    <ArrowDown
                      className="animate-bounceDown"
                      color="green "
                      size={14}
                    />
                  )}
                </>
              </CircularProgressbarWithChildren>
            </div>
            <p>
              ~{formatGweiValues(gasData?.estimatedBaseFee)}
              Gwei
            </p>
            <InfoIcon size={14} />
          </div>
        </div>
      </HoverCardTrigger>
      <HoverCardContent className="p-0 w-80">
        <div className="p-2 space-y-4">
          <p>
            This is an <strong>estimated base gas fee.</strong>
          </p>
          <p>
            50% of the historical base fees are less then or equal to{" "}
            <strong>
              ~{formatGweiValues(gasData?.baseFeePercentile)} Gwei
            </strong>
          </p>
        </div>
      </HoverCardContent>
    </HoverCard>
  );
}
复制

以下是具体实现说明:

  1. 常量:- REFETCH_INTERVAL、STEP_INTERVAL、NETWORK_LATENCY:用于管理数据获取间隔、倒计时更新步长以及预估网络延迟的常量。

  2. 工具函数:- formatGweiValues:将 gas 价格格式化为可读形式的函数,对较小的值使用科学计数法,对较大的值使用定点表示法。

  3. GasFeeCard 组件:- 使用 useSDK 获取当前以太坊 chainId。

  • 初始化状态变量 countdown 和 currentChainId。

  • 第一个 useEffect 钩子在 chain ID 变化时更新当前 chain ID 并触发数据重新获取(mutate),同时重置倒计时。

  • 第二个 useEffect 设置一个定时器,每次递减倒计时,当倒计时归零时触发数据重新获取,并在组件卸载时清除定时器。

  • fetcher:从服务端 API 端点获取 gas 数据的函数,将当前 chain ID 作为查询参数传入。

  • useSWR:使用 fetcher 函数获取 gas 数据的 hook,提供数据、加载和错误状态。

  1. 渲染逻辑:- 组件根据条件渲染加载和错误状态。

  • trendingUp 和 trendingDown 是根据获取的 gas 数据中 baseFeeTrend 确定的布尔值。

  • 主 UI 由一个 HoverCard 组成,其中包含显示倒计时进度的 CircularProgressbarWithChildren 和表示 gas 价格趋势的箭头图标,并以 Gwei 为单位显示预估基础费用。

  • HoverCardContent 展示预估基础费用和历史基础费用百分位的详细信息。

  1. 样式:- 代码使用 Tailwind CSS 类进行样式设置(如 text-red-500、cursor-pointer)。

  • CircularProgressbarWithChildren 根据 gas 价格趋势动态改变颜色(上涨为红色,下降为绿色)。

我们还使用了 lucide-react 中的 ArrowUp、ArrowDown 和 InfoIcon 图标。现在,只需将此组件导入 GetTicket.tsx 组件,使其显示在门票总价下方即可。

继续使用 MetaMask Gas API 进行开发

了解 gas 价格有助于买家选择最具性价比的购买时机,通过在 gas 价格较低时进行交易来节省费用。对于活动售票平台而言,提供这些信息有助于提升透明度和用户满意度,让用户对自己的消费更有掌控感,同时避免产生意外费用。

我们已将 Gas API 集成到现有的活动售票 dapp 中。本教程的代码可在此处获取。

同时推荐参考以下资源:

Gas API 文档:https://docs.infura.io/infura-expansion-apis/gas-api

Gas API 模板:https://github.com/meowyx/GasAPI-Template

祝开发愉快 🚀

AI 翻译。可能包含错误。请务必核实信息。

评价此翻译

本文作者: