Обозреватель блокчейна — это инструмент, который позволяет пользователям искать, просматривать и проверять содержимое блокчейна. Доступны многие обозреватели блокчейнов, такие как Биткойн, Эфириум и Солана.

Solana Blockchain — это высокопроизводительная блокчейн-платформа, которая поддерживает крупномасштабные децентрализованные приложения. Некоторые варианты использования блокчейна Solana включают децентрализованные финансы (DeFi), невзаимозаменяемые токены (NFT), игры и социальные сети.

В этой статье мы рассмотрим создание обозревателя блокчейна для блокчейна Solana с использованием Next.js.

Настройка проекта Next.js

Мы будем создавать приложение Next.js для взаимодействия с блокчейном Solana. Next.js — это фреймворк для создания приложений React. Это популярный выбор для создания приложений React, поскольку он поставляется с множеством функций из коробки. Это включает в себя:

  • Файловая маршрутизация
  • Рендеринг на стороне сервера
  • Генерация статического сайта
  • Автоматическое разделение кода

Чтобы создать приложение Next.js, убедитесь, что на вашем компьютере установлен Node.js версии 14.16.0 или новее. Как только это будет подтверждено, откройте терминал и запустите код ниже.

npx create-next-app@latest

Приведенная выше команда загружает приложение Next.js. Вам будет предложено

  • Укажите имя для приложения
  • Выберите между Typescript и Javascript для начальной загрузки приложения.
  • Установка Эслинта

После установки перейдите в каталог только что созданного приложения и запустите приведенный ниже код, чтобы запустить приложение.

npm run dev

Откройте браузер и перейдите по адресу http://localhost:3000, чтобы просмотреть приложение.

Для стилизации этого проекта мы будем использовать Tailwind CSS, утилиту CSS-фреймворк. Запустите приведенный ниже код, чтобы добавить в проект Tailwind CSS.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Приведенная выше команда установит библиотеку CSS Tailwind и создаст файл tailwind.config.js. Откройте файл в редакторе кода и замените свойство content. Ваша конфигурация должна быть похожа на код ниже.

Блок content помогает сообщить попутному ветру, в каких каталогах файлов искать стили попутного ветра. Затем перейдите в каталог стилей и откройте файл global.css. Добавьте следующие импорты вверху файла.

@tailwind base; 
@tailwind components; 
@tailwind utilities;

Теперь мы можем использовать попутный ветер в нашем проекте. Перейдите к файлу index.js в нашем каталоге pages и замените код кодом ниже.

import Head from "next/head";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className="w-full h-full max-w-2xl p-6 flex flex-col items-center justify-between gap-6 mx-auto relative">
        <h1 className="text-2xl">Solana Blockchain Explorer</h1>
      </main>
    </>
  );
}

Создание компонентов

Прежде чем получить историю транзакций, давайте уделим минуту созданию компонентов, которые мы будем использовать для отображения информации о транзакциях.

Запустите приведенный ниже код, чтобы установить библиотеки, которые нам понадобятся.

npm i axios date-fns

Это установит Axios, библиотеку получения данных на основе обещаний для JavaScript, и date-fns, библиотеку для управления датами JavaScript.

После установки перейдите в корневой каталог и создайте каталог components. В каталоге components создайте файл TransactionList.js и вставьте приведенный ниже код.

import React from "react";
import { fromUnixTime, format, formatDistanceToNow } from "date-fns";
import Link from "next/link";

const TransactionList = ({ transactionList, balance }) => {
  return (
    <div className="first-line:overflow-hidden transition-all duration-300 max-h-fit w-full h-full">
      {balance && (
        <h2 className="flex justify-between text-lg mb-4">
          Balance: <span>◎{balance}</span>
        </h2>
      )}
      {transactionList?.length > 0 && (
        <div className="overflow-x-auto">
          <table className="w-full border-spacing-x-4 -ml-4 border-separate">
            <thead className="text-left">
              <tr>
                <th className="font-medium">Signature</th>
                <th className="font-medium">Block</th>
                <th className="font-medium">Age</th>
                <th className="font-medium">Status</th>
              </tr>
            </thead>
            <tbody>
              {transactionList.map((transaction) => (
                <tr key={transaction?.signature}>
                  <td className="truncate max-w-[230px] text-blue-600 hover:underline">
                    <Link href={`/transaction/${transaction?.signature}`}>
                      {transaction?.signature}
                    </Link>
                  </td>
                  <td>{transaction?.slot}</td>
                  <td
                    className="whitespace-nowrap"
                    title={format(
                      fromUnixTime(transaction?.blockTime),
                      "MMMM d, yyyy 'at' HH:mm:ss OOOO"
                    )}
                  >
                    {formatDistanceToNow(fromUnixTime(transaction?.blockTime), {
                      includeSeconds: true,
                    })}
                  </td>
                  <td>
                    <span
                      className={`inline-block px-2 py-1 rounded-full text-xs font-bold leading-none text-white ${
                        transaction?.confirmationStatus === "finalized"
                          ? "bg-green-500"
                          : "bg-yellow-400"
                      }`}
                    >
                      {transaction?.confirmationStatus}
                    </span>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
      {transactionList?.length <= 0 && (
        <div className="text-center">No transaction to display</div>
      )}
    </div>
  );
};

export default TransactionList;

Этот компонент будет использоваться для отображения списка транзакций, которые будут получены из блокчейна Solana. Он принимает transactionList и balance в качестве реквизита и отображает их в пользовательском интерфейсе.

В каталоге components создайте еще один файл с именем SearchTransactionForm.js и вставьте приведенный ниже код.

import React from "react";

const SearchTransactionForm = ({
  handleFormSubmit,
  address,
  loading,
  setAddress,
  errorMessage,
}) => {
  return (
    <form onSubmit={handleFormSubmit} className="flex flex-wrap w-full">
      <label htmlFor="address" className="w-full shrink-0 text-lg mb-2">
        Transaction address
      </label>
      <input
        type="text"
        name="address"
        value={address}
        onChange={(event) => setAddress(event.target.value)}
        className="w-3/4 border-2 border-r-0 border-gray-500 h-12 rounded-l-lg px-4 focus:outline-none focus:border-blue-600 disabled:bg-gray-500 transition-colors duration-150"
        placeholder="CHrNmjoRzaGCL..."
        disabled={loading}
        required
      />
      <button
        type="submit"
        disabled={loading}
        className="flex-grow bg-blue-600 flex items-center justify-center rounded-r-lg text-white text-sm hover:bg-blue-900 disabled:bg-gray-500 transition-colors duration-150"
      >
        Search
      </button>
      {errorMessage && (
        <p className="text-red-600 text-base my-1">{errorMessage}</p>
      )}
    </form>
  );
};

export default SearchTransactionForm;

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

Наконец, вставьте приведенный ниже код, чтобы создать еще один файл с именем TransactionListDetail.js.

import React from "react";
import { fromUnixTime, format } from "date-fns";

const TransactionListDetail = ({ loading, transactionData }) => {
  return (
    <div className="w-full">
      {!loading && transactionData && (
        <div className="rounded-lg border max-w-xl overflow-x-auto mx-auto">
          <table className="table-auto w-full border-collapse p-4">
            <tbody className="overflow-x-scroll">
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Signature</td>
                <td className="p-4">
                  {transactionData.transaction.signatures[0]}
                </td>
              </tr>
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Timestamp</td>
                <td className="p-4">
                  {format(
                    fromUnixTime(transactionData?.blockTime),
                    "MMMM d, yyyy 'at' HH:mm:ss OOOO"
                  )}
                </td>
              </tr>
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Recent Blockhash</td>
                <td className="p-4">
                  {transactionData.transaction.message.recentBlockhash}
                </td>
              </tr>
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Slot</td>
                <td className="p-4">
                  {Intl.NumberFormat().format(transactionData.slot)}
                </td>
              </tr>
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Fee</td>
                <td className="p-4">
                  ◎{transactionData.meta.fee / 1_000_000_000}
                </td>
              </tr>
              <tr className="border-b">
                <td className="font-medium text-sm p-4">Amount</td>
                <td className="p-4">
                  ◎
                  {transactionData.transaction.message.instructions[0].parsed
                    .info.lamports / 1_000_000_000}
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      )}
      {!loading && !transactionData && (
        <p className="text-center">No transaction to display</p>
      )}
    </div>
  );
};

export default TransactionListDetail;

Мы будем использовать этот компонент для отображения деталей конкретной транзакции. Он примет transactionData в качестве реквизита и будет использовать его детали для отображения в пользовательском интерфейсе.

Повтор сеанса для разработчиков

Раскройте разочарования, выявите ошибки и устраните замедления работы, как никогда раньше, с помощью OpenReplay — набора для воспроизведения сеансов с открытым исходным кодом для разработчиков. Его можно разместить самостоятельно за несколько минут, что дает вам полный контроль над данными клиентов.

Удачной отладки! Попробуйте использовать OpenReplay сегодня.

Получить историю транзакций от Соланы

Теперь, когда наше приложение Next.js запущено и работает, следующим шагом будет добавление Solana в наше приложение. К счастью, Solana предоставляет хорошо поддерживаемую библиотеку javascript для взаимодействия с блокчейном Solana под названием @solana/web3.js. Запустите приведенный ниже код, чтобы установить библиотеку.

npm install @solana/web3.js

После установки перейдите в pages/api и создайте файл transactions.js. Мы будем использовать маршруты API Next.js для получения пользовательских транзакций. Это позволяет нам отделить конфигурацию Solana и бизнес-логику от клиента. Откройте файл transactions.js и вставьте приведенный ниже код.

import * as solanaWeb3 from "@solana/web3.js";

const DEV_NET = solanaWeb3.clusterApiUrl("devnet");
const solanaConnection = new solanaWeb3.Connection(DEV_NET);

const getAddressInfo = async (address, numTx = 3) => {
  const pubKey = new solanaWeb3.PublicKey(address);
  const transactionList = await solanaConnection.getSignaturesForAddress(
    pubKey,
    { limit: numTx }
  );
  const accountBalance = await solanaConnection.getBalance(pubKey);

  return { transactionList, accountBalance };
};

const handler = async (req, res) => {
  const queryAddress = req.query?.address;
  if (!queryAddress) {
    return res.status(401).json({
      message: "Invalid address",
    });
  }
  try {
    const { accountBalance, transactionList } = await getAddressInfo(
      queryAddress
    );
    return res.status(200).json({ transactionList, accountBalance });
  } catch (error) {
    console.log(error);
    return res.status(500).json({
      message: "Something went wrong. Please try again later",
    });
  }
};

export default handler;

Мы должны импортировать библиотеку для использования Solana в нашем файле transactions.js. После этого мы создаем подключение к узлу Solana RPC.

const DEV_NET = solanaWeb3.clusterApiUrl('devnet');
const solanaConnection = new solanaWeb3.Connection(DEV_NET);

Узел Solana RPC (удаленный процедурный вызов) — это узел, который отвечает на запросы о сети и позволяет пользователям отправлять транзакции. Солана поддерживает несколько общедоступных узлов, в том числе DEV_NET. Мы создадим соединение с узлом DEV_NET RPC, что позволит нам получить историю транзакций и баланс адреса, совершённого на узле.

Следующим шагом является создание функции getAddressInfo для получения необходимой информации от RPC-узла Solana. Функция принимает адрес и количество транзакций для получения, для которых по умолчанию установлено число 3. Чтобы получить транзакции и выполнить большинство операций с @solana/web3.js, нам понадобится открытый ключ, общий идентификатор на Solana. Открытый ключ может быть сгенерирован из строки в кодировке base58, буфера, Uint8Array, числа и массива чисел. Мы генерируем наш открытый ключ из адреса пользователя, строки в кодировке base58.

const pubKey = new solanaWeb3.PublicKey(address);

Чтобы получить список транзакций, мы используем метод getSignaturesForAddress, который возвращает список транзакций. Метод требует publicKey и необязательный объект для разбиения на страницы.

const transactionList = await solanaConnection.getSignaturesForAddress(pubKey, { limit: numTx });

Метод getBalance возвращает баланс пользователя и требует открытого ключа.

const accountBalance = await solanaConnection.getBalance(pubKey);

Функция handler связывает все вместе и возвращает детали в презентабельном виде, который можно отобразить клиенту.

Запустите приведенный ниже код в терминале, чтобы установить библиотеки, которые помогут нам правильно получать и форматировать транзакции.

Теперь вернитесь к файлу index.js и вставьте приведенный ниже код.

import Head from "next/head";
import { useState } from "react";
import axios from "axios";
import TransactionList from "../components/TransactionList";
import SearchTransactionForm from "../components/SearchTransactionForm";

export default function Home() {
  const [loading, setLoading] = useState(false);
  const [transactionList, setTransactionList] = useState([]);
  const [balance, setBalance] = useState(null);
  const [address, setAddress] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const handleFormSubmit = async (event) => {
    try {
      event.preventDefault();
      setLoading(true);
      setErrorMessage("");

      const response = await axios.get(`/api/transactions/?address=${address}`);
      if (response.status === 200) {
        setTransactionList(response.data.transactionList);
        const accountBalanceText = response.data.accountBalance;
        const accountBalance = parseInt(accountBalanceText) / 1_000_000_000;

        accountBalance && setBalance(accountBalance);
      }
    } catch (error) {
      console.log("client", error);
      setErrorMessage(
        error?.response.data?.message ||
          "Unable to fetch transactions. Please try again later."
      );
    } finally {
    }

    setLoading(false);
  };
  return (
    <>
      <Head>
        <title>Solana Blockchain Explorer</title>
      </Head>
      <main className="w-full h-full max-w-2xl p-6 flex flex-col items-center justify-between gap-6 mx-auto relative">
        <h1 className="text-2xl">Solana Blockchain Explorer</h1>
        <SearchTransactionForm
          handleFormSubmit={handleFormSubmit}
          address={address}
          setAddress={setAddress}
          loading={loading}
          errorMessage={errorMessage}
        />

        <TransactionList transactionList={transactionList} balance={balance} />

        {loading && (
          <div className="absolute inset-0 bg-white/70 flex items-center justify-center">
            Loading
          </div>
        )}
      </main>
    </>
  );
}

Что мы сделали на этой странице, так это связали все вместе. Выводим созданный ранее компонент SearchTransactionForm для сбора адреса от пользователя. Когда пользователь отправляет форму, вызывается функция handleFormSubmit, которая вызывает transactions API, который мы создали ранее, передавая address в качестве параметра. Если поиск успешен, запрос API возвращает transactionData и balance, которые передаются как props компоненту TransactionList для отображения.

Сохраните и перезагрузите браузер. Теперь вы можете ввести адрес Solana и нажать кнопку поиска, чтобы получить историю транзакций. Вы должны получить результат, аналогичный скриншоту ниже.

Получить Единую транзакцию

Мы рассмотрели, как получить список транзакций из библиотеки Solana web3 и отобразить его. В этом разделе мы рассмотрим, как получить сведения об отдельной транзакции. Перейдите в каталог api и создайте файл transaction.js. Откройте файл и вставьте код ниже.

import * as solanaWeb3 from "@solana/web3.js";

const DEV_NET = solanaWeb3.clusterApiUrl("devnet");
const solanaConnection = new solanaWeb3.Connection(DEV_NET);

const handler = async (req, res) => {
  const transactionHash = req.body.transactionHash;
  if (!transactionHash) {
    return res.status(401).json({
      error: "Invalid transaction hash",
    });
  }
  try {
    const transaction = await solanaConnection.getParsedTransaction(
      transactionHash
    );
    return res.status(200).json(transaction);
  } catch (error) {
    console.log("Error:", error);
    return res.status(500).json({
      error: "Server error",
    });
  }
};

export default handler;

Чтобы получить детали отдельной транзакции, мы используем метод getParsedTransaction, для которого требуется хэш транзакции. Хэш транзакции получается из тела запроса, которое Next.js предоставляет функции handler. В зависимости от результата мы возвращаем ответ клиенту.

Следующим шагом является создание страницы для отображения сведений о транзакции, полученных из API. Создайте каталог transaction в каталоге pages. Перейдите в каталог transaction и создайте файл с именем [id].js. Эта страница представляет собой динамический маршрут; всякий раз, когда пользователь посещает /transaction/gm12 или transaction/12gm, эта страница будет отображаться в браузере. Откройте файл в редакторе кода и вставьте приведенный ниже код.

import Head from "next/head";
import { useState, useEffect } from "react";
import axios from "axios";
import { useRouter } from "next/router";
import TransactionListDetail from "../../components/TransactionListDetail";

export default function TransactionDetail() {
  const [loading, setLoading] = useState(false);
  const [transactionData, setTransactionData] = useState();
  const [errorMessage, setErrorMessage] = useState("");
  const router = useRouter();

  useEffect(() => {
    const getTransaction = async () => {
      try {
        setLoading(true);
        setErrorMessage("");

        const response = await axios.post("/api/transaction", {
          transactionHash: router.query?.id,
        });

        if (response.status === 200) {
          setTransactionData(response.data.transaction);
        }
      } catch (error) {
        setErrorMessage(
          error?.response.data?.message ||
            "Unable to fetch transaction. Please try again later."
        );
      } finally {
        setLoading(false);
      }
    };

    getTransaction();
  }, [router.query?.id]);

  return (
    <>
      <Head>
        <title>Solana Blockchain Explorer: Transaction</title>
      </Head>
      <main className="w-full h-full p-6 flex flex-col items-center justify-between gap-6 mx-auto relative">
        <h1 className="text-2xl">Transaction</h1>
        {errorMessage && (
          <p className="text-red-600 text-base text-center my-1">
            {errorMessage}
          </p>
        )}

        <TransactionListDetail
          loading={loading}
          transactionData={transactionData}
        />

        {loading && (
          <div className="absolute inset-0 bg-white/70 flex items-center justify-center">
            Loading
          </div>
        )}
      </main>
    </>
  );
}

Мы делаем что-то похожее на то, что мы делали на домашней странице, но вместо того, чтобы принимать ввод от пользователя и передавать его функции, которая вызывает API, мы получаем ввод, который нам нужен, из URL-адреса. Когда пользователь посещает маршрут /transaction/[id], вызывается функция getTransaction. Функция запрашивает конечную точку /api/transaction с хэшем транзакции, полученным из маршрута. Если запрос выполнен успешно, он возвращает данные, которые отображаются на странице. Соответствующее сообщение об ошибке также отображается на странице, если возникает ошибка при получении сведений о транзакции.

Теперь зайдите в приложение в браузере и найдите адрес. Когда появятся результаты, нажмите на транзакцию, чтобы открыть подробности на странице транзакции.

Заключение

В этой статье мы рассмотрели создание обозревателя блокчейна на блокчейне Solana. Обозреватель блокчейна имеет множество применений, и он может быть частью приложения web3, которое позволяет пользователям просматривать историю своих транзакций, а также может быть отдельным приложением, подобным тому, которое мы создали выше.

Код этого проекта доступен здесь. Вы также можете посмотреть рабочий пример здесь.

Первоначально опубликовано на https://blog.openreplay.com.