How to Implement the MetaMask Gas API in an Event Ticketing Dapp

In this blog, we explored implementing the MetaMask Gas API into an existing ticketing dApp built with Next.js and ShadcnUI.

by MetaMask DeveloperDecember 5, 2023
Ticketing dapp image

Understanding the gas price before purchasing blockchain-based event tickets is crucial for both cost transparency and effective financial planning. In the context of blockchain transactions, 'gas' refers to the fee required to successfully conduct a transaction or execute a contract on the blockchain.

This fee fluctuates based on network demand. By knowing the gas price in advance, ticket buyers can make more informed decisions about the total cost of their purchase. This is especially important as the gas fee can significantly add to the overall cost of the ticket, particularly during times of high network congestion.

In this tutorial, we will implement the MetaMask Gas API in an existing event ticketing dapp, enabling users to see the gas prices while buying tickets for events.

Event ticketing dapp image 1

Prerequisites and dependencies


For the frontend framework, we will use Next.js, and for the UI, we will use ShadcnUI. Let’s install the following ShadcnUI components:

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

The above code will add the hover-card component that we will use for displaying the gas prices and install the SWR React hooks library. The Stale-While-Revalidate (SWR) library helps with fetching data from the internet. It works by first using old data it has saved, then requesting new data, and finally replacing the old data with this new information. This makes websites faster and more up-to-date.

Next, we'll install the react-circular-progressbar component to display circular progress indicators, which are useful for showing the progress of tasks or loading states in web applications. It's customizable and implemented using SVG for high-quality graphics.

npm i react-circular-progressbar

Implementing the Gas API


In the app/web directory, let's create the following folder: app/api/gas. Inside, let's make a route.ts file and add the following code:

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,
  });
}

Let’s see what we did here:

  1. Extracting Query Parameters:
    • const { searchParams } = new URL(request.url);: Extracts the search parameters from the request URL.
    • const chainId = searchParams.get("chainId");: Retrieves the chainId parameter from the URL, which is used to specify the Ethereum network.
  2. Setting Up Authorization:
    • Environment variables INFURA_API_KEY and INFURA_API_SECRET are combined into a Buffer, then encoded into a Base64 string. This string is used for basic HTTP authentication when making requests to the Infura API.
  3. Fetching Gas Prices and Base Fee Percentile Data:
    • Two asynchronous fetch requests are made to the Infura API:
      • gasPricesResp fetches suggested gas fees for the specified Ethereum network (chainId).
      • baseFeePercentileRes fetches the base fee percentile for the same network.
    • The Authorization header is set with the Base64-encoded credentials.
  4. Parsing and Returning Data:
    • The responses from both fetch requests are converted to JSON.
    • NextResponse.json({...}) creates and returns a JSON response containing the following data:
      • estimatedBaseFee: The estimated base fee for transactions.
      • baseFeeTrend: The trend of the base fee (e.g., increasing, decreasing).
      • baseFeePercentile: The percentile distribution of base fees.

Creating the user interface


We will create a component called GasFeeCard.tsx and add the following code into it:


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>
  );
}

Let’s take a look at what we did here:

  1. Constants:
    • REFETCH_INTERVAL, STEP_INTERVAL, NETWORK_LATENCY: Constants for managing the data fetching interval, the step interval for countdown updates, and estimated network latency.
  2. Utility Function:
    • formatGweiValues: A function to format gas price values into a readable format, using either exponential notation for small values or fixed-point notation for larger values.
  3. GasFeeCard Component:
    • Uses useSDK to get the current Ethereum chainId.
    • State variables countdown and currentChainId are initialized.
    • The first useEffect hook updates the current chain ID and triggers data re-fetching (mutate) when the chain ID changes. It also resets the countdown timer.
    • The second useEffect sets up an interval that decrements the countdown and triggers a data re-fetch when the countdown reaches zero. It cleans up the interval on unmount.
    • fetcher: A function that fetches gas data from a server-side API endpoint, passing the current chain ID as a query parameter.
    • useSWR: This hook is used to fetch gas data using the fetcher function and provides data, loading, and error states.
  4. Rendering Logic:
    • The component conditionally renders loading and error states.
    • trendingUp and trendingDown are booleans determined by the baseFeeTrend from the fetched gas data.
    • The main UI consists of a HoverCard with a CircularProgressbarWithChildren indicating the countdown progress and an arrow icon showing the gas price trend. It displays the estimated base fee in Gwei.
    • The HoverCardContent shows more detailed information about the estimated base fee and the historical base fee percentile.
  5. Styling:
    • The code uses Tailwind CSS classes for styling (e.g., text-red-500, cursor-pointer).
    • The CircularProgressbarWithChildren is styled to change color based on the gas price trend (red for increasing, green for decreasing).

We also used ArrowUp, ArrowDown, InfoIcon icons from lucide-react. Now, all that is left is to import this component into the GetTicket.tsx component, so it will display below the total ticket prices.

Event ticketing dapp image 2

Continue building with the MetaMask Gas API


Awareness of the gas price enables buyers to choose the most cost-effective time to make their purchase, potentially saving money by transacting during periods of lower gas prices. For an event ticketing platform, providing this information enhances transparency and customer satisfaction, as users feel more in control of their spending and can avoid unexpected fees.

We have implemented the Gas API into an existing event ticketing dapp. You can find the code for this tutorial here

I also recommend checking the following resources:

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

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

Happy Building 🚀

Receive our Newsletter