如何构建Solana浏览器克隆(第2部分) - 使用动态路由的交易详细信息

  • QuickNode
  • 发布于 2025-01-30 12:51
  • 阅读 22

本文是Solana Explorer开发系列的第二部分,主要介绍如何建立一个可访问特定交易详情的动态页面。文章详细说明了构建组件的步骤,包括代码示例和组件之间的关系,为读者提供了一个实践项目来加深对Solana技术栈的理解。

概述

Solana Explorer 提供了大量关于 Solana 区块链的优秀信息:交易历史和详细信息、代币余额、NFT 元数据等。在我们看来,浏览 Solana Explorer 是理解 Solana 的最佳方式之一,但构建一个 Solana Explorer 能够将你的知识提升到一个新高度!在这个三部分系列中,我们将介绍构建 Solana Explorer 简单克隆所需的步骤!

在我们的第一篇指南中,我们获取了交易历史并将其以表格形式展示给用户。在本指南中,我们将创建一个可通过点击交易历史表中的任何交易访问的可变交易详细信息页面。

你将要做的事情

在本指南中,你将使用我们之前构建的 Solana Explorer 示例 如何构建 Solana Explorer 克隆(第 1 部分:交易历史)。你将利用该框架构建一个组件,使用户能够通过带有动态 URL 的新页面查看用户交易历史中的任何特定交易的详细信息。

Explorer 交易详细信息示例输出

你将需要的工具

要顺利完成本指南,你需要完成我们关于 如何构建 Solana Explorer 克隆(第 1 部分:交易历史) 的指南。为什么?因为我们将在该代码基础上进行构建。

如何构建 Solana Explorer 克隆(第 1 部分:交易历史)指南中的最终代码可在 这个 QuickNode Github 仓库 找到。该指南将作为本指南的起点。确保你已经按照该指南中的说明完成所有步骤。

快速入门:如果你没有机会完成第一份指南,这里有一个简单的方法可以直接跳入。在你的终端创建一个项目目录并克隆示例:

mkdir solana-explorer-demo
cd solana-explorer-demo
git clone https://github.com/quiknode-labs/technical-content.git .
cd solana/explorer-clone-part-2/starter
yarn install
echo > .env

然后用你的 QuickNode RPC 更新 .env 文件:

REACT_APP_SOLANA_RPC_HOST=https://example.solana-devnet.quiknode.pro/000000/
REACT_APP_NETWORK=devnet

然后在你的终端中输入 yarn dev

你应该在本地代码编辑器中拥有来自这个 Github 仓库的最终代码,并且你的 https://localhost:3000/explorer 应该呈现如下视图:

Explorer 克隆指南 2 起点

好的!让我们开始吧。

创建交易详细信息组件

让我们在我们的交易工具基础上工作,以便用户可以点击某个交易来获取该交易的更多详细信息。好消息是我们已经拥有了执行此操作的工具。

在你的项目目录中,复制你的组件模板并将其命名为 TransactionDetail.tsx

cp ./src/components/template.tsx ./src/components/TransactionDetail.tsx

TransactionDetail.tsx 现在应该存在于你的组件文件夹中。用代码编辑器打开它。

更新依赖

先将你的函数组件重命名为 TransactionDetail。替换

export const Template: FC = () => {

export const TransactionDetail: FC = () => {

对于这个组件,我们需要以下导入。将你模板中现有的导入替换为:

import { useRouter } from 'next/router'
import { useConnection } from '@solana/wallet-adapter-react';
import { LAMPORTS_PER_SOL, ParsedTransactionWithMeta} from '@solana/web3.js';
import { FC, useEffect, useState } from 'react';

这与 TransactionLog 非常相似,但包括 useRouter,它将允许我们从 URL 中提取信息,以便我们可以进行动态域搜索(例如,./tx/TRANSACTION_ID 将实现我们在应用中使用 TRANSACTION_ID 作为变量)。

由于此组件的组装方式与我们之前的组件相似,我们将先分享完整代码,然后逐步解释一些重要部分:

import { useRouter } from 'next/router'
import { useConnection } from '@solana/wallet-adapter-react';
import { LAMPORTS_PER_SOL, ParsedTransactionWithMeta} from '@solana/web3.js';
import { FC, useEffect, useState } from 'react';

export const TransactionDetail: FC = () => {
    const router = useRouter();
    const { txid } = router.query;
    const { connection } = useConnection();
    const [transactionDetail, setTransactionDetail] = useState<ParsedTransactionWithMeta>(null);
    const [transactionCard, setTransactionCard] = useState<JSX.Element>(null);
    let search = Array.isArray(txid) ? txid[0] : txid;

    useEffect(()=>{
        if(!router.isReady) return;
        if(search) {getTransaction(search);}
    },[router.isReady]);

    useEffect(() => {
        if (transactionDetail) {
            buildView();
        }
      }, [transactionDetail, connection]);

    async function getTransaction(txid: string) {
        //获取交易的解析详情
        let transactionDetails = await connection.getParsedTransaction(txid, {maxSupportedTransactionVersion:0});
        //更新状态
        setTransactionDetail(transactionDetails);
    }

    function buildView() {
        if(transactionDetail) {
            let overviewTable = buildOverviewTable();
            let accountsTable = buildAccountsTable();
            let tokensTable = buildTokensTable();
            let programsTable = buildProgramsTable();
            let view = (<>
                <p className="text-left text-lg font-bold">概览:</p>
                {overviewTable}
                <br/>
                <p className="text-left text-lg font-bold">账户输入:</p>
                {accountsTable}
                <br/>
                <p className="text-left text-lg font-bold">SPL 代币变化:</p>
                {tokensTable}
                <br/>
                <p className="text-left text-lg font-bold">程序:</p>
                {programsTable}
            </>)
            setTransactionCard(view)
        }
        else {
            setTransactionCard(null);
        }
    }

    function buildOverviewTable() {
        if(transactionDetail) {
            let date = new Date(transactionDetail.blockTime*1000).toLocaleDateString();
            let table =
            (<table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                <tbody>

                <tr  className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                    <td className="px-6 py-3">签名</td>
                    <td className="px-6 py-3">{transactionDetail.transaction.signatures[0]}</td>
                </tr>
                <tr  className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                    <td className="px-6 py-3">时间戳</td>
                    <td className="px-6 py-3">{date}</td>
                </tr>
                <tr  className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                    <td className="px-6 py-3">状态</td>
                    <td className="px-6 py-3">{transactionDetail.meta.err ? '失败' : '成功'}</td>
                </tr>
                </tbody>
            </table>
            );
            return(table)
        }
        else {
            return(null);
        }
    }

    function buildAccountsTable() {
        if(transactionDetail) {
            let {preBalances, postBalances} = transactionDetail.meta;
            let header =
                <thead className="text-xs text-gray-700 uppercase bg-zinc-50 dark:bg-gray-700 dark:text-gray-400"><tr>
                    <td className="px-6 py-3">#</td>
                    <td className="px-6 py-3">地址</td>
                    <td className="px-6 py-3 text-center">变化</td>
                    <td className="px-6 py-3 text-center">后余额</td>
                </tr></thead>;
            let rows = (transactionDetail.transaction.message.accountKeys.map((account,i)=>{
                let solChange = (postBalances[i] - preBalances[i]) / LAMPORTS_PER_SOL;
                return (
                    <tr key={i+1} className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                        <td className="px-6 py-3">{i+1}</td>
                        <td className="px-6 py-3">{account.pubkey.toString()}</td>
                        <td className="px-6 py-3 text-center">{solChange === 0 ? '-' : '◎ ' + solChange.toFixed(6)}</td>
                        <td className="px-6 py-3 text-center">◎ {(postBalances[i] / LAMPORTS_PER_SOL).toFixed(3)}</td>
                    </tr>)
                }
            ));
            let table = (
            <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                {header}
                <tbody>{rows}</tbody>
            </table>);
            return(table)
        }
        else {
            return(null)
        }
    }

    function buildTokensTable() {
        if(transactionDetail) {
            let {preTokenBalances, postTokenBalances} = transactionDetail.meta;
            let header =
                <thead className="text-xs text-gray-700 uppercase bg-zinc-50 dark:bg-gray-700 dark:text-gray-400"><tr>
                    <td className="px-6 py-3">#</td>
                    <td className="px-6 py-3">所有者</td>
                    <td className="px-6 py-3">铸币</td>
                    <td className="px-6 py-3 text-center">变化</td>
                    <td className="px-6 py-3 text-center">后余额</td>
                </tr></thead>;
            let rows = (preTokenBalances.map((account,i)=>{
                let tokenChange = (postTokenBalances[i].uiTokenAmount.uiAmount - account.uiTokenAmount.uiAmount);
                return (
                    <tr key={i+1} className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                        <td className="px-6 py-3">{i+1}</td>
                        <td className="px-6 py-3">{account.owner}</td>
                        <td className="px-6 py-3">{account.mint}</td>
                        <td className="px-6 py-3 text-center">{tokenChange === 0 ? '-' : tokenChange.toFixed(2)}</td>
                        <td className="px-6 py-3 text-center">{postTokenBalances[i].uiTokenAmount.uiAmount ? (postTokenBalances[i].uiTokenAmount.uiAmount).toFixed(2): '-'}</td>
                    </tr>)
                }
            ));
            let table = (
            <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                {header}
                <tbody>{rows}</tbody>
            </table>);
            return(table)
        }
        else {
            return(null)
        }
    }

    function buildProgramsTable() {
        if(transactionDetail) {
            const transactionInstructions = transactionDetail.transaction.message.instructions;
            let header =
                <thead className="text-xs text-gray-700 uppercase bg-zinc-50 dark:bg-gray-700 dark:text-gray-400"><tr>
                    <td className="px-6 py-3">#</td>
                    <td className="px-6 py-3">程序</td>
                </tr></thead>;

            let rows = (transactionInstructions.map((instruction,i)=>{
                return (
                    <tr key={i+1} className="bg-white border-b bg-zinc-800 dark:border-zinc-700">
                        <td className="px-6 py-3">{i+1}</td>
                        <td className="px-6 py-3">{instruction.programId.toString() }</td>
                    </tr>)
                }
            ));
            let table = (
            <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                {header}
                <tbody>{rows}</tbody>
            </table>);
            return(table)
        }
        else {
            return(null)
        }
    }

    return(<div>
    {/* 在这里渲染结果 */}
        <div>{transactionCard}</div>
    </div>)
}

在此函数组件中发生了什么:

更改摘要表

创建交易视图和页面

干得漂亮!就像上一个指南一样,在我们看到最终产品之前,我们需要告诉我们的应用在哪里显示它。我们需要做几件事情,以使我们的组件在 Nextjs 中可见:

  • 创建交易视图

  • 创建动态交易页面

创建交易视图

在你的项目目录中,创建一个新文件 tx.tsx,放在 ./src/views/explorer/ 文件夹中:

echo > ./src/views/explorer/tx.tsx

打开 tx.tsx 并粘贴以下代码:

import { FC } from "react";
import { TransactionDetail } from "components/TransactionDetail";

export const TransactionView: FC = ( ) => {
  return (
    <div className="md:hero mx-auto p-4">
      <div className="md:hero-content flex flex-col">
        <h1 className="text-center text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-tr from-[#9945FF] to-[#14F195]">
          快速查看浏览器
        </h1>
        <div className="text-center">
        </div>
        <div className="text-center">
            <TransactionDetail />
        </div>
      </div>
    </div>
  );
};

这个视图相当简单,因为我们在步骤中编写了一个相当全面的 TransactionDetail 组件。我们为页面包含一个标题,然后调用 TransactionDetail

创建发布我们视图的交易页面:

在你的项目目录中,创建一个新页面目录 /tx/。然后在该目录中创建一个新文件 [txid].tsx。方括号将允许我们使用动态 URL 参数,将不同的交易 ID 传递给我们的 /tx/ 目录:

mkdir ./src/pages/tx
echo > ./src/pages/tx/\[txid\].tsx

将以下代码粘贴到 [txid].tsx 中:

import Head from "next/head";
import { TransactionView } from 'views/explorer/tx';

const Tx = () => {

  return (
    <div>
    <Head>
      <title>Solana Scaffold</title>
      <meta
        name="description"
        content="基本功能"
      />
    </Head>
    <TransactionView />
  </div>
  )
}

export default Tx

这是实际将呈现的页面,/tx/YOUR_TRANSACTION_ID。

是的!!!你应该准备好了,但如果你想要一些额外的元素来获得 Solana Explorer 的感觉,我们可以在我们的网站上添加一些有用的链接。

连接它

返回到你的 TransactionLog 组件,./src/components/TransactionsLog.tsx 找到你渲染交易签名的代码 transaction.transaction.signature[0](对我们来说是第 51 行):

    <td className="px-6 py-3">
         {/* 某些交易返回多个签名 - 我们只希望获取第一个 */}
         {transaction.transaction.signatures[0]}
    </td>

让我们在它周围包裹一个 HTML <a> 标签,并将位置传递给 href 属性:

    <td className="px-6 py-3">
        <a href={'../tx/'+transaction.transaction.signatures[0]}>
            {transaction.transaction.signatures[0]}
        </a>
    </td>

这将使每个交易 ID 转换为一个 URL,指向我们的新 TransactionDetail 页面!

最后,让我们在标题和侧边栏中为我们的 Explorer 页面添加一些链接,以改善可导航性。

打开 ./src/components/ContentContainer.tsx,在第 30 行添加指向 explorer 的链接:

          <li>
            <NavElement
                label="Explorer"
                href="/explorer"
            />
          </li>

类似地,打开 ./src/components/AppBar.tsx,在第 60 行添加指向 explorer 的链接:

          <NavElement
            label="Explorer"
            href="/explorer"
            navigationStarts={() => setIsNavOpen(false)}
          />

工作做得好!这是我们最近更改的简要回顾:

更改文件的总结

好的,让我们看看效果!返回到你的终端并输入:

yarn dev

打开 http://localhost:3000/ 并进入你方便的新 Explorer 链接以导航到 http://localhost:3000/explorer。现在,点击任何一个交易 ID。你应该被重定向到 http://localhost:3000/tx/YOUR_TRANSACTION_ID,并看到如下内容!

最终结果 - 交易详细信息页面

击掌!🙌

结论

恭喜!你已经通过动态路由将交易详细信息添加到 Solana Explorer 克隆中。现在你可以随意自定义外观和感觉,或从解析的交易结果中获取不同的数据。你可以使用这些相同的概念来呈现各种链上查询。花一些时间来玩弄这个,你会惊讶于你能通过一些简单的调整做到什么。你在交易视图中添加了有趣的内容吗?加入我们在 Discord 或通过 Twitter 联系我们,分享你的创作。

我们 ❤️ 反馈!

如果你对这份指南有任何反馈或问题, 请告诉我们。我们很想听到你的声音!

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。