Перейти к содержимому

Как внедрить MetaMask Gas API в Dapp для продажи билетов на мероприятия

В этом блоге мы рассмотрели интеграцию MetaMask Gas API в существующее dApp для продажи билетов, созданное с помощью Next.js и ShadcnUI.

6 мин чтения
Как внедрить MetaMask Gas API в Dapp для продажи билетов на мероприятия

Понимание цены газа перед покупкой билетов на мероприятия на основе блокчейна имеет ключевое значение как для прозрачности затрат, так и для эффективного финансового планирования. В контексте блокчейн-транзакций «газ» — это комиссия, необходимая для успешного проведения транзакции или выполнения контракта в блокчейне.

Размер этой комиссии варьируется в зависимости от нагрузки на сеть. Зная цену газа заранее, покупатели билетов могут принимать более взвешенные решения об итоговой стоимости покупки. Это особенно важно, поскольку комиссия за газ может существенно увеличить общую стоимость билета, особенно в периоды высокой загруженности сети.

В этом руководстве мы интегрируем MetaMask Gas API в существующий dapp для продажи билетов на мероприятия, чтобы пользователи могли видеть цены на газ при покупке билетов.

Предварительные требования и зависимости

  • Действующий Web3 API key и API key secret.

  • Установленные Node.js и npm или yarn.

  • Знакомство с Next.js и React.

В качестве фронтенд-фреймворка мы будем использовать Next.js, а для UI — ShadcnUI. Установим следующие компоненты ShadcnUI:

npx shadcn-ui@latest add hover-card
npm i swr
Копировать

Приведённый выше код добавит компонент hover-card, который мы будем использовать для отображения цен на газ, а также установит библиотеку React-хуков SWR. Библиотека 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"); — получает параметр chainId из URL, который используется для указания сети Ethereum.

  1. Настройка авторизации: переменные окружения INFURA_API_KEY и INFURA_API_SECRET объединяются в Buffer, затем кодируются в строку Base64. Эта строка используется для базовой HTTP-аутентификации при запросах к Infura API.

  2. Получение цен на газ и данных о процентиле базовой комиссии: к Infura API выполняются два асинхронных fetch-запроса:

    • gasPricesResp получает рекомендуемые комиссии за газ для указанной сети Ethereum (chainId).

    • baseFeePercentileRes получает процентиль базовой комиссии для той же сети.

    • В заголовке Authorization передаются учётные данные, закодированные в Base64.

  3. Парсинг и возврат данных: ответы обоих 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 для получения текущего chainId Ethereum.

    • Инициализируются переменные состояния countdown и currentChainId.

    • Первый хук useEffect обновляет текущий ID цепочки и запускает повторное получение данных (mutate) при его изменении, а также сбрасывает таймер обратного отсчёта.

    • Второй хук useEffect устанавливает интервал, уменьшающий обратный отсчёт и запускающий повторный запрос данных при достижении нуля. При размонтировании интервал очищается.

    • fetcher — функция, получающая данные о газе из серверного API-эндпоинта с передачей текущего ID цепочки в качестве параметра запроса.

    • useSWR — хук для получения данных о газе с помощью функции fetcher, предоставляющий состояния данных, загрузки и ошибки.

  4. Логика рендеринга:

    • Компонент условно отображает состояния загрузки и ошибки.

    • trendingUp и trendingDown — булевы значения, определяемые по baseFeeTrend из полученных данных о газе.

    • Основной UI состоит из HoverCard с CircularProgressbarWithChildren, отображающим прогресс обратного отсчёта, и иконкой стрелки, показывающей тенденцию цены газа. Отображается расчётная базовая комиссия в Gwei.

    • HoverCardContent показывает более подробную информацию о расчётной базовой комиссии и историческом процентиле базовой комиссии.

  5. Стилизация:

    • В коде используются классы Tailwind CSS (например, text-red-500, cursor-pointer).

    • CircularProgressbarWithChildren стилизован так, чтобы менять цвет в зависимости от тенденции цены газа (красный — при росте, зелёный — при снижении).

Также мы использовали иконки ArrowUp, ArrowDown и InfoIcon из lucide-react. Осталось только импортировать этот компонент в компонент GetTicket.tsx, чтобы он отображался под итоговой стоимостью билетов.

Продолжайте разработку с MetaMask Gas API

Осведомлённость о цене газа позволяет покупателям выбирать наиболее выгодное время для совершения покупки, потенциально экономя средства за счёт проведения транзакций в периоды низких цен на газ. Для платформы продажи билетов на мероприятия предоставление этой информации повышает прозрачность и удовлетворённость пользователей: они чувствуют больший контроль над своими расходами и могут избежать неожиданных комиссий.

Мы интегрировали Gas API в существующий dapp для продажи билетов на мероприятия. Код для этого руководства можно найти здесь.

Также рекомендую ознакомиться со следующими ресурсами:

Документация Gas API: https://docs.infura.io/infura-expansion-apis/gas-api

Шаблон Gas API: https://github.com/meowyx/GasAPI-Template

Удачной разработки 🚀

Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.

Оцените перевод

Автор статьи: