跳至內容

如何在活動票務 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 元件,用於顯示圓形進度指示器,適合呈現任務進度或網頁應用程式中的載入狀態。此元件支援自訂設定,並以 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 參數,用於指定 Ethereum 網路。

  1. 設定授權:- 環境變數 INFURA_API_KEY 與 INFURA_API_SECRET 合併為 Buffer,再編碼為 Base64 字串。此字串用於向 Infura API 發出請求時的基本 HTTP 驗證。

  2. 擷取 Gas 價格與基礎費用百分位數資料:- 向 Infura API 發出兩個非同步 fetch 請求:- gasPricesResp 擷取指定 Ethereum 網路(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 取得目前的 Ethereum chainId。

  • 初始化狀態變數 countdown 與 currentChainId。

  • 第一個 useEffect hook 在 chain ID 變更時更新目前的 chain ID 並觸發資料重新擷取(mutate),同時重設倒數計時器。

  • 第二個 useEffect 設定一個間隔,遞減倒數計時,並在倒數歸零時觸發資料重新擷取。元件卸載時清除該間隔。

  • fetcher:從伺服器端 API 端點擷取 gas 資料的函式,以目前的 chain ID 作為查詢參數傳入。

  • useSWR:此 hook 使用 fetcher 函式擷取 gas 資料,並提供資料、載入中及錯誤狀態。

  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 翻譯。可能包含錯誤。請務必核實資訊。

評價此翻譯

本文作者: