Solana交易传播 - 处理丢失的交易

  • QuickNode
  • 发布于 2025-01-30 11:45
  • 阅读 20

本指南介绍了如何在Solana上监控交易,确保交易在一定时间内被成功添加到区块,或则判断交易是否过期。通过创建一个工具,开发者可以在其去中心化应用中提高用户体验。文中详细描述了设置项目的步骤,包括安装依赖、创建钱包、发送交易等,不仅提供了代码示例,还包括了详细的解释与注意事项。

概述

Solana 有一个独特的功能,可以让交易在一定时间内如果没有被提交到区块中则过期。这有助于确保验证者不必处理相同的交易多次。此外,交易过期还通过快速提供交易成功或失败的确定性感觉,创造了更清晰的用户体验。

由于 Solana 允许交易过期,在你的 dApps 中,你必须检查交易是成功还是失败,然后再处理用户的下一步操作。在本指南中,我们将创建一个工具来监视 Solana 交易,以验证其是否成功被添加到区块中或已经过期。

你将需要的条件

设置你的项目

在终端中创建一个新的项目目录,命令如下:

mkdir solana-tx-propagation
cd solana-tx-propagation

为你的应用创建一个文件,命名为 app.ts

echo > app.ts

使用 "yes" 标志初始化项目,以使用新包的默认值:

yarn init --yes
#或
npm init --yes

创建一个启用 .json 导入的 tsconfig.json 文件:

tsc -init --resolveJsonModule true

安装 Solana Web3 依赖

我们需要为这个练习添加 Solana Web3 库。在终端中输入:

yarn add @solana/web3.js@1
#或
npm install @solana/web3.js@1

创建钱包并领取 SOL

你需要创建一个 Solana 文件系统钱包(将秘密密钥写入 guideSecret.json 文件)并给它空投一些 SOL。你可以使用 Solana CLI 或使用 我们为你创建的这个脚本。如果你已经有一个纸钱包并且只需要一些开发网络 SOL,你可以在下面请求空投:

🪂请求开发网络 SOL

空投 1 SOL(开发网络)

确保将你的钱包保存到你的项目目录,命名为 guideSecret.json

导入必要的依赖

打开 app.ts,并在 第 1-2 行 粘贴以下导入:

import { Connection, Keypair, LAMPORTS_PER_SOL, SystemProgram, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js';
import secret from './guideSecret.json';

第 1 行导入了 Solana Web3 库的必要类和常量,第 2 行导入了我们在前一步创建的钱包。

设置你的 QuickNode 端点

要在 Solana 上进行构建,你需要一个 API 端点与网络连接。你可以使用公共节点或部署和管理自己的基础设施;不过,如果你希望有 8 倍更快的响应时间,你可以将繁重的工作交给我们。

查看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 此处 注册一个免费账户。我们将使用 Solana Devnet 节点。

复制 HTTP 提供程序链接:

app.ts 中的导入语句下,声明你的 RPC 并建立与 Solana 的 Connection 连接:

const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const SOLANA_CONNECTION = new Connection(QUICKNODE_RPC);

设置完成后,你的环境应该类似于下面的样子:

设置你的应用

定义钱包和转账指令

创建两个钱包变量,DESTINATION_WALLETSIGNER_WALLET,我们将用于我们的交易。我们将使用 Keypair.generate() 来生成一个随机钱包发送 SOL,并使用 Keypair.fromSecretKey() 从导入的秘密密钥派生我们的钱包:

const DESTINATION_WALLET = Keypair.generate();
const SIGNER_WALLET = Keypair.fromSecretKey(new Uint8Array(secret));

将这些钱包传入一个新的 TransactionInstruction,使其从 SIGNER_WALLETDESTINATION_WALLET 发送 0.1 SOL:

const INSTRUCTIONS: TransactionInstruction =
    SystemProgram.transfer({
        fromPubkey: SIGNER_WALLET.publicKey,
        toPubkey: DESTINATION_WALLET.publicKey,
        lamports: 0.01 * LAMPORTS_PER_SOL,
    });

获取开始时间

创建一个新变量 START_TIME,获取我们启动应用的时间(我们将用它来计算整个过程的持续时间):

const START_TIME = new Date();

创建一个睡眠函数

我们需要创建一个延迟函数 sleep,它允许我们等一段时间后再重新查询链:

const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

好了,我们准备好了。让我们构建我们的应用程序!

检查区块哈希是否过期

首先,我们创建一个函数 isBlockhashExpired,以检查区块是否已经过期。我们需要传入一个 Solana 网络的 Connection 和一个初始的 lastValidBlockHeight 进行检查:

async function isBlockhashExpired(connection: Connection, lastValidBlockHeight: number) {
    let currentBlockHeight = (await connection.getBlockHeight('finalized'));
    console.log('                           ');
    console.log('当前区块高度:             ', currentBlockHeight);
    console.log('最后有效区块高度 - 150:     ', lastValidBlockHeight - 150);
    console.log('--------------------------------------------');
    console.log('差异:                      ',currentBlockHeight - (lastValidBlockHeight-150)); // 如果差异为正,区块哈希已过期。
    console.log('                           ');

    return (currentBlockHeight > lastValidBlockHeight - 150);
}

我们的函数执行了 3 个操作:

  1. 使用 getBlockHeight 获取当前的区块高度。

  2. (可选)输出区块哈希过期的时间变化。(这可选添加,以帮助演示过期如何变化)

  3. 通过检查我们当前的区块高度是否大于原始区块高度(减去 150)来返回布尔值。

为什么要减去 150?Solana 要求交易的区块哈希不得超过 150 个槽位。

创建并发送交易

创建一个 async 代码块,我们将在其中运行我们的脚本:

(async()=>{

})()

在你的代码块中,让我们根据之前定义的 TransactionInstruction 创建并发送交易:

(async()=>{
    // 步骤 1 - 获取最新的区块哈希
    const blockhashResponse = await SOLANA_CONNECTION.getLatestBlockhashAndContext('finalized');
    const lastValidHeight = blockhashResponse.value.lastValidBlockHeight;

    // 步骤 2 - 创建一个 SOL 转账交易
    const messageV0 = new TransactionMessage({
        payerKey: SIGNER_WALLET.publicKey,
        recentBlockhash: blockhashResponse.value.blockhash,
        instructions: [INSTRUCTIONS]
    }).compileToV0Message();
    const transaction = new VersionedTransaction(messageV0);
    transaction.sign([SIGNER_WALLET]);

    // 步骤 3 - 将交易发送到网络
    const txId = await SOLANA_CONNECTION.sendTransaction(transaction);
})()

让我们分解一下:

  • 步骤 1:我们获取最新的区块哈希。我们需要这个用于我们的交易(稍后将传递给我们的 isBlockhashExpired)。
  • 步骤 2:创建一个转账交易。我们将使用版本化交易。如果你以前没有使用过这些,查看我们的 指南:如何在 Solana 上使用版本化交易
  • 步骤 3:调用 sendTransaction 将交易发送到网络。这将给我们一个交易签名 txId,我们可以稍后查询。

检查交易是否成功或区块哈希是否过期

好了!我们已将交易发送到网络——现在我们需要运行两项检查:首先,我们要检查交易是否已被网络 确认最终确认,如果没有,我们将检查区块哈希是否过期。如果两者都不为真,我们将等待几秒钟,然后重试,直到其中之一为真。

在你的 async 代码块中,添加步骤 4:

    // 步骤 4 - 检查交易状态和区块哈希状态,直到交易成功或区块哈希过期
    let hashExpired = false;
    let txSuccess = false;
    while (!hashExpired && !txSuccess) {
        const { value: statuses } = await SOLANA_CONNECTION.getSignatureStatuses([txId]);

        if (!statuses || statuses.length === 0) {
            throw new Error('无法获取签名状态');
        }

        const status = statuses[0];

        if (status.err) {
            throw new Error(`交易失败: ${JSON.stringify(status.err)}`);
        }

        // 当交易成功时退出循环
        if (status && ((status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized'))) {
            txSuccess = true;
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`交易成功。经过时间: ${elapsed} 秒。`);
            console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
            break;
        }

        hashExpired = await isBlockhashExpired(SOLANA_CONNECTION, lastValidHeight);

        // 当区块哈希过期时退出循环
        if (hashExpired) {
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`区块哈希已过期。经过时间: ${elapsed} 秒。`);
            // (添加你自己的逻辑来获取新的区块哈希并重新发送交易或者抛出错误)
            break;
        }

        // 每 2.5 秒再次检查
        await sleep(2500);
    }

这里有很多内容——让我们分解一下:

  • 首先,我们定义两个 boolean 变量 hashExpiredtxSuccess,如果满足任一条件都将切换为真。
  • 创建一个 while 循环,直到区块哈希过期或交易成功为止。
  • 将我们的 txId 传入 getSignatureStatuses 以检查交易的状态。
  • 检查返回的状态是否为 confirmedfinalized。如果是,我们记录经过的时间以及 Solana Explorer 上的交易链接。然后我们退出循环。
  • 如果交易尚未成功,我们通过将 lastValidHeight 传递给我们的 isBlockhashExpired 函数检查我们的哈希是否已过期。
  • 如果区块哈希已过期,我们记录经过的时间。然后我们退出循环。你可以添加自己的重试逻辑以重新发送交易或抛出错误,如果你愿意。
  • 如果两者都未满足,我们将使用我们的 sleep 函数等待 2.5 秒。然后该过程将重复。

运行你的代码

好了,你已经准备好了。在终端中运行你的应用:

ts-node app

太棒了!更多情况下,你将看看区块哈希循环的一到两个迭代,并有一个成功的交易:

干得好。我们发现,如果不向网络发送交易而是重新运行此脚本以观察区块哈希过期是很有帮助的。我们可以通过注释掉几行代码(我们的 sendTransaction 调用和我们的交易状态检查)来实现:

    //const txId = await SOLANA_CONNECTION.sendTransaction(transaction);

        /* const { value: statuses } = await SOLANA_CONNECTION.getSignatureStatuses([txId]);

        if (!statuses || statuses.length === 0) {
            throw new Error('无法获取签名状态');
        }

        const status = statuses[0];

        if (status.err) {
            throw new Error(`交易失败: ${JSON.stringify(status.err)}`);
        } */

        // 当交易成功时退出循环
        /*if (status && ((status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized'))) {
            txSuccess = true;
            const endTime = new Date();
            const elapsed = (endTime.getTime() - START_TIME.getTime())/1000;
            console.log(`交易成功。经过时间: ${elapsed} 秒。`);
            console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`);
            break;
        } */

现在,去重新运行你的代码:

ts-node app

你应该看到控制台日志隔几槽一次,且槽间差异从 -1500 逐渐变化。该过程应该大约耗时一分钟,你应该看到如下内容:

总结

就是这样!现在你已经在提高用户体验的道路上。随着 Solana 生态系统的不断发展,交易处理的相关过程也会不断演变。要将这些概念付诸实践,请查看我们其他的 Solana 教程

要获取最新的 Solana 新闻和信息,请订阅我们的 时事通讯 或关注我们的 Twitter。如果你有任何问题或想讨论你的项目,请在 Discord 上联系我们。

我们 ❤️ 反馈!

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

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

0 条评论

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