Solana区块浏览器开发实战

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

Solana区块浏览器开发实战
一键发币: SUI | SOL | BNB | ETH | BASE | ARB | OP | POLYGON | AVAX | FTM | OK

区块链浏览器是一种允许用户搜索、查看和验证区块链内容的工具。 许多区块链浏览器都可用,例如比特币、以太坊和 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 区块链获取的交易列表。 它将 transactionListbalance作为 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 请求将返回 transactionDatabalance,它们将作为 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/gm12transaction/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翻译整理,转载请标明出处

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