Solana区块浏览器开发实战

区块链浏览器是一种允许用户搜索、查看和验证区块链内容的工具。本文介绍如何使用 Next.js 开发Solana区块链浏览器。

Solana区块浏览器开发实战
一键发币: x402兼容 | Aptos | X Layer | SUI | SOL | BNB | ETH | BASE | ARB | OP | Polygon | Avalanche

区块链浏览器是一种允许用户搜索、查看和验证区块链内容的工具。 许多区块链浏览器都可用,例如比特币、以太坊和 Solana。

Solana 区块链是一个支持大规模去中心化应用的高性能区块链平台。 Solana 区块链的一些用例包括去中心化金融 (DeFi)、不可替代代币 (NFT)、游戏和社交媒体。

本文将着眼于使用 Next.js 为 Solana 区块链构建区块链浏览器。

1、设置 Next.js 项目

我们将构建一个 Next.js 应用程序来与 Solana 区块链交互。 Next.js 是一个用于构建 React 应用程序的框架。 它是构建 React 应用程序的流行选择,因为它具有许多开箱即用的功能。 这包括:

  • 基于文件的路由
  • 服务端渲染
  • 静态站点生成
  • 自动代码分割

要创建 Next.js 应用程序,请确保你的计算机上安装了 Node.js v14.16.0 或更高版本。 确认后,打开终端并运行下面的代码。

npx create-next-app@latest

上面的命令引导 Next.js 应用程序。 你将被要求

  • 提供应用程序的名称
  • 选择 Typescript 和 Javascript 来引导应用程序
  • 安装 Eslint

安装后,导航到新创建的应用程序的目录并运行以下代码来启动应用程序。

npm run dev

打开浏览器并导航到 http://localhost:3000 以查看该应用程序:

为了设计这个项目,我们将使用 Tailwind CSS,一个实用的 CSS 框架。 运行以下代码将 Tailwind CSS 添加到项目中。

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

上面的命令将安装 Tailwind CSS 库并创建 tailwind.config.js 文件。 在代码编辑器中打开文件并替换内容属性。 你的配置应该类似于下面的代码。

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

content块帮助告诉 tailwind 文件目录以查找 tailwind 样式。 接下来,导航到 styles 目录并打开 global.css 文件。 在文件顶部添加以下导入。

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

我们现在可以在项目中使用 Tailwind。 导航到页面目录中的index.js 文件并将代码替换为以下代码。

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

2、构建组件

在获取交易历史记录之前,让我们花一点时间构建将用于显示交易信息的组件。

运行下面的代码来安装我们需要的库

npm i axios date-fns

这将安装 Axios(一个基于 Promise 的 JavaScript 数据获取库)和 date-fns(一个用于操作 JavaScript 日期的库)。

安装后,导航到根目录并创建一个 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作为 props 并将它们显示在 UI 上。

在 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 作为 props 并使用其详细信息显示在 UI 上。

3、从 Solana 获取交易历史记录

现在我们已经启动并运行了 Next.js 应用程序,下一步是将 Solana 添加到我们的应用程序中。 幸运的是,Solana 提供了一个维护良好的 JavaScript 库,用于与 Solana 区块链交互,称为 @solana/web3.js。 运行下面的代码来安装该库。

npm install @solana/web3.js

安装后,转到 pages/api 并创建一个 transactions.js文件。 我们将使用 Next.js API 路由来获取用户交易。 这使我们能够将 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;

我们必须导入该库才能在 transactions.js 文件中使用 Solana。 之后,我们创建与 Solana RPC 节点的连接。

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

Solana RPC(远程过程调用)节点是响应网络请求并允许用户提交交易的节点。 Solana 维护一些公开可用的节点,其中包括 DEV_NET。 我们将创建与 DEV_NET RPC 节点的连接,使我们能够获取节点上交易的地址的交易历史记录和余额。

下一步是创建一个函数 getAddressInfo 以从 Solana RPC 节点获取我们需要的信息。 该函数接受一个地址和要获取的交易数量,默认数量为 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函数,该函数调用我们之前创建的交易API,并将 address作为参数传递。 如果搜索成功,API 请求将返回 transactionData 和 balance,它们将作为 props 传递给要显示的 TransactionList 组件。

保存并重新加载你的浏览器。 现在可以输入 Solana 地址并单击搜索按钮来获取交易历史记录。 应该得到类似于下面的屏幕截图的结果。

4、获取单笔交易

我们研究了如何从 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 提供给处理函数。 根据结果,我们向客户端返回响应。

下一步是构建一个页面来显示从 API 获取的交易详细信息。 在 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 端点。 如果请求成功,则返回数据,并显示在页面上。 如果获取交易详细信息时出现错误,页面上还会显示相应的错误消息。

现在在浏览器中访问该应用程序并搜索地址。 当结果显示时,单击交易以打开交易页面上的详细信息。

5、结束语

在本文中,我们研究了在 Solana 区块链上创建一个区块链浏览器。 区块链浏览器有很多用途,它可以是允许用户查看其交易历史记录的 web3 应用程序的一部分,也可以是像我们上面构建的那样的独立应用程序。

该项目的代码可在这里获取。 你还可以在此处查看工作示例。


原文链接:Build A Blockchain Explorer With Solana And Next.Js

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。