콘텐츠로 건너뛰기

이벤트 티켓팅 Dapp에서 MetaMask Gas API를 구현하는 방법

이 블로그에서는 Next.js와 ShadcnUI로 구축된 기존 티켓팅 dApp에 MetaMask Gas API를 구현하는 방법을 살펴보았습니다.

6분 읽기
이벤트 티켓팅 Dapp에서 MetaMask Gas API를 구현하는 방법

블록체인 기반 이벤트 티켓 구매 전 가스 가격을 이해하는 것은 비용 투명성과 효과적인 재무 계획 모두에 있어 매우 중요합니다. 블록체인 트랜잭션 맥락에서 'gas'란 블록체인에서 트랜잭션을 성공적으로 수행하거나 컨트랙트를 실행하는 데 필요한 수수료를 의미합니다.

이 수수료는 네트워크 수요에 따라 변동됩니다. 가스 가격을 미리 파악함으로써 티켓 구매자는 총 구매 비용에 대해 보다 합리적인 결정을 내릴 수 있습니다. 특히 네트워크 혼잡이 심한 시기에는 가스 수수료가 티켓 전체 비용에 상당한 영향을 미칠 수 있으므로 이 점이 더욱 중요합니다.

이 튜토리얼에서는 기존 이벤트 티켓 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");: Ethereum 네트워크를 지정하는 데 사용되는 chainId 파라미터를 URL에서 가져옵니다.

  1. 인증 설정:- 환경 변수 INFURA_API_KEY와 INFURA_API_SECRET을 Buffer로 결합한 후 Base64 문자열로 인코딩합니다. 이 문자열은 Infura API에 요청할 때 기본 HTTP 인증에 사용됩니다.

  2. 가스 가격 및 기본 수수료 백분위 데이터 가져오기:- Infura API에 두 개의 비동기 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 훅은 chain ID가 변경될 때 현재 chain ID를 업데이트하고 데이터 재가져오기(mutate)를 트리거합니다. 또한 카운트다운 타이머를 초기화합니다.

  • 두 번째 useEffect는 카운트다운을 감소시키고 카운트다운이 0에 도달하면 데이터 재가져오기를 트리거하는 인터벌을 설정합니다. 언마운트 시 인터벌을 정리합니다.

  • fetcher: 현재 chain 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

즐거운 빌딩 되세요 🚀

AI가 번역했습니다. 오류가 있을 수 있습니다. 항상 정보를 확인하시기 바랍니다.

번역 평가하기

이 글의 작성자: