コンテンツへスキップ

イベントチケット販売DappにMetaMask Gas APIを実装する方法

このブログでは、Next.jsとShadcnUIで構築された既存のチケット発行dAppにMetaMask Gas APIを実装する方法を解説しました。

6分で読める
イベントチケット販売DappにMetaMask Gas APIを実装する方法

ブロックチェーンベースのイベントチケットを購入する前にガス価格を把握することは、コストの透明性と効果的な資金計画の両面において非常に重要です。ブロックチェーントランザクションの文脈において、「ガス」とはブロックチェーン上でトランザクションを正常に実行したり、コントラクトを実行したりするために必要な手数料を指します。

この手数料はネットワークの需要に応じて変動します。ガス価格を事前に把握することで、チケット購入者は購入総コストについてより十分な情報に基づいた判断を下すことができます。ガス手数料はチケットの総コストに大きく影響する可能性があるため、特にネットワークが混雑している時期には、この点が特に重要となります。

このチュートリアルでは、既存のイベントチケット販売dappにMetaMask Gas APIを実装し、ユーザーがイベントのチケットを購入する際にガス価格を確認できるようにします。

前提条件と依存関係

フロントエンドフレームワークにはNext.jsを使用し、UIにはShadcnUIを使用します。以下のShadcnUIコンポーネントをインストールしましょう:

npx shadcn-ui@latest add hover-card
npm i swr
コピー

上記のコードにより、ガス価格の表示に使用するhover-cardコンポーネントが追加され、SWR Reactフックライブラリがインストールされます。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. ガス価格とベース手数料パーセンタイルデータの取得:- Infura APIに対して2つの非同期fetchリクエストを実行します:- gasPricesRespは、指定されたEthereumネットワーク(chainId)の推奨ガス手数料を取得します。

  • 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:ガス価格の値を読みやすい形式にフォーマットする関数。小さい値には指数表記を、大きい値には固定小数点表記を使用します。

  3. GasFeeCardコンポーネント:- useSDKを使用して現在のEthereumのchainIdを取得します。

  • 状態変数countdownとcurrentChainIdを初期化します。

  • 最初のuseEffectフックは、チェーンIDが変更された際に現在のチェーンIDを更新し、データの再取得(mutate)をトリガーします。また、カウントダウンタイマーをリセットします。

  • 2番目のuseEffectは、カウントダウンをデクリメントし、カウントダウンがゼロに達した際にデータの再取得をトリガーするインターバルを設定します。アンマウント時にインターバルをクリーンアップします。

  • fetcher:現在のチェーンIDをクエリパラメータとして渡し、サーバーサイドのAPIエンドポイントからガスデータを取得する関数。

  • useSWR:fetcher関数を使用してガスデータを取得し、データ、ローディング、エラーの状態を提供するフック。

  1. レンダリングロジック:- コンポーネントはローディング状態とエラー状態を条件付きでレンダリングします。

  • trendingUpとtrendingDownは、取得したガスデータのbaseFeeTrendによって決定されるブール値です。

  • メインUIは、カウントダウンの進捗を示すCircularProgressbarWithChildrenとガス価格のトレンドを示す矢印アイコンを含むHoverCardで構成されています。推定ベース手数料をGwei単位で表示します。

  • HoverCardContentには、推定ベース手数料と過去のベース手数料パーセンタイルに関する詳細情報が表示されます。

  1. スタイリング:- コードはスタイリングにTailwind CSSクラスを使用しています(例:text-red-500、cursor-pointer)。

  • CircularProgressbarWithChildrenは、ガス価格のトレンドに基づいて色が変わるようにスタイリングされています(上昇時は赤、下降時は緑)。

また、lucide-reactからArrowUp、ArrowDown、InfoIconアイコンを使用しました。あとはこのコンポーネントをGetTicket.tsxコンポーネントにインポートするだけで、チケット総額の下に表示されるようになります。

MetaMask Gas APIを使ったさらなる開発

ガス価格を把握することで、購入者はより費用対効果の高いタイミングを選んで購入できるようになり、ガス価格が低い時期にトランザクションを実行することで節約できる可能性があります。イベントチケット販売プラットフォームにとって、この情報を提供することは透明性と顧客満足度を高めます。ユーザーは自分の支出をより細かくコントロールでき、予期せぬ手数料を回避できるからです。

既存のイベントチケット販売dappにGas APIを実装しました。このチュートリアルのコードはこちらでご確認いただけます。

また、以下のリソースもあわせてご参照ください:

Gas API ドキュメント:https://docs.infura.io/infura-expansion-apis/gas-api

Gas API テンプレート:https://github.com/meowyx/GasAPI-Template

Happy Building 🚀

AIによる翻訳です。エラーが含まれている可能性があります。常に情報を確認してください。

翻訳を評価する

この記事の執筆者: