在購買基於區塊鏈的活動票券之前,了解 gas 價格對於費用透明度和有效的財務規劃至關重要。在區塊鏈交易的情境中,「gas」是指在區塊鏈上成功執行交易或合約所需支付的費用。
此費用會根據網路需求而波動。事先了解 gas 價格,購票者便能對購買總成本做出更明智的決策。這一點尤為重要,因為 gas 費用可能大幅增加票券的整體成本,特別是在網路壅塞嚴重的時期。
在本教學中,我們將在現有的活動售票 dapp 中實作 MetaMask Gas API,讓使用者在購買活動票券時能夠查看 gas 價格。
前置條件與相依套件
有效的 Web3 API 金鑰與 API 金鑰密碼。
已安裝 Node.js 以及 npm 或 yarn。
熟悉 Next.js 與 React。
前端框架我們將使用 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,
});
}
以下說明我們在此所做的事:
擷取查詢參數:- const { searchParams } = new URL(request.url);:從請求 URL 中擷取搜尋參數。
const chainId = searchParams.get("chainId");:從 URL 取得 chainId 參數,用於指定 Ethereum 網路。
設定授權:- 環境變數 INFURA_API_KEY 與 INFURA_API_SECRET 合併為 Buffer,再編碼為 Base64 字串。此字串用於向 Infura API 發出請求時的基本 HTTP 驗證。
擷取 Gas 價格與基礎費用百分位數資料:- 向 Infura API 發出兩個非同步 fetch 請求:- gasPricesResp 擷取指定 Ethereum 網路(chainId)的建議 gas 費用。
baseFeePercentileRes 擷取同一網路的基礎費用百分位數。
Authorization 標頭設定為 Base64 編碼的憑證。
解析並回傳資料:- 兩個 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;
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>
);
}
以下說明我們在此所做的事:
常數:- REFETCH_INTERVAL、STEP_INTERVAL、NETWORK_LATENCY:用於管理資料擷取間隔、倒數計時更新的步進間隔,以及預估網路延遲的常數。
工具函式:- formatGweiValues:將 gas 價格值格式化為可讀格式的函式,對小數值使用科學記號,對較大數值使用定點記號。
GasFeeCard 元件:- 使用 useSDK 取得目前的 Ethereum chainId。
初始化狀態變數 countdown 與 currentChainId。
第一個 useEffect hook 在 chain ID 變更時更新目前的 chain ID 並觸發資料重新擷取(mutate),同時重設倒數計時器。
第二個 useEffect 設定一個間隔,遞減倒數計時,並在倒數歸零時觸發資料重新擷取。元件卸載時清除該間隔。
fetcher:從伺服器端 API 端點擷取 gas 資料的函式,以目前的 chain ID 作為查詢參數傳入。
useSWR:此 hook 使用 fetcher 函式擷取 gas 資料,並提供資料、載入中及錯誤狀態。
渲染邏輯:- 元件依條件渲染載入中與錯誤狀態。
trendingUp 與 trendingDown 為布林值,由擷取的 gas 資料中的 baseFeeTrend 決定。
主要 UI 由一個 HoverCard 組成,內含 CircularProgressbarWithChildren 顯示倒數進度,以及一個箭頭圖示顯示 gas 價格趨勢,並以 Gwei 顯示預估基礎費用。
HoverCardContent 顯示預估基礎費用與歷史基礎費用百分位數的詳細資訊。
樣式:- 程式碼使用 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
祝開發順利 🚀