通过Yellowstone gRPC Geyser插件监控Solana程序

  • QuickNode
  • 发布于 2024-10-10 19:59
  • 阅读 20

本文介绍了如何使用Solana的Geyser插件和Yellowstone gRPC来监控链上活动,具体示例是使用TypeScript追踪Pump.fun程序的新代币铸造。文章详细说明了Geyser的作用、Yellowstone的功能,逐步指导读者创建一个实时监控应用,并强调如何优化过滤以减少无关数据。提供了实际代码示例和运行结果。

概述

在本指南中,我们将学习 Solana Geyser 插件,并探索如何使用 Yellowstone gRPC,一个强大的 Solana Geyser 插件,来监测实时链上活动。具体来说,我们将创建一个 TypeScript 应用程序,跟踪 Solana 主网 Pump.fun 程序的新代币铸造。该项目将演示如何利用 Geyser 的低延迟数据访问能力构建响应灵敏且高效的监测工具。

喜欢视频格式吗?观看视频,学习如何在 9 分钟内使用 Yellowstone gRPC 附加组件监测 Solana 程序数据。

YouTube

订阅我们的 YouTube 频道以获取更多视频! 订阅

你将要做什么

  • 了解 Geyser 和 Yellowstone gRPC
  • 编写脚本使用 Yellowstone 监控 Solana 上的 Pump.fun 铸币
  • 运行和测试你的应用程序

示例输出:

Yellowstone Output

你将需要什么

什么是 Geyser?

Geyser 是 Solana 验证者的一种插件系统,提供了低延迟的区块链数据访问,而不至于通过密集的 RPC 请求(如 getProgramAccounts)来过载验证者。Geyser 插件不会直接查询验证者,而是将关于账户、交易、槽和块的实时信息流式传输到外部数据存储,例如关系数据库、NoSQL 数据库或像 Kafka 这样的流处理平台。这种方法显著减少了验证者的负载,同时提高了数据访问的效率。

Geyser 插件的关键优势在于它们能够与高流量的 Solana 应用程序一起扩展。通过将数据查询路由到外部存储,开发者可以实现优化的访问模式,如缓存和索引,这对于需要频繁访问大型数据集或历史信息的应用程序尤其有价值。这种分离允许验证者专注于他们的主要角色,即处理交易,同时确保开发者拥有所需的全面、实时数据访问。

什么是 Yellowstone Dragon's Mouth?

Yellowstone Dragon's Mouth(简称“Yellowstone”)是构建于 Solana 的 Geyser 插件系统上的开源 gRPC 接口。它利用 gRPC,Google 的高性能框架,将协议缓冲区与 HTTP/2 结合,实现分布式系统之间快速且类型安全的通信。

Yellowstone 提供实时流式传输以下内容:

  • 账户更新
  • 交易
  • 条目
  • 块通知
  • 槽通知

与传统的 WebSocket 实现相比,Yellowstone 的 gRPC 接口提供了更低的延迟和更高的稳定性。它还包括用于快速、一时性数据检索的单一操作。gRPC 的高效性和类型安全性结合,使 Yellowstone 特别适用于基于云的服务和数据库更新。QuickNode 通过我们的 Yellowstone gRPC Marketplace 附加组件 支持 Yellowstone。

让我们通过编写监控 Solana 上的新 Pump.fun 铸币的脚本来看看 Yellowstone 的实际应用。

创建新项目

让我们开始设置一个新的 TypeScript 项目:

  1. 创建一个新目录用于你的项目并进入该目录:
mkdir pump-fun-monitor && cd pump-fun-monitor
  1. 初始化一个新的 Node.js 项目:
npm init -y
  1. 安装所需的依赖项:
npm install @triton-one/yellowstone-grpc @solana/web3.js@1 bs58 @types/node
  1. 在项目根目录下创建一个 tsconfig.json 文件:
npx tsc --init
  1. 打开 tsconfig.json 文件,并确保它包含以下选项:
{
     "compilerOptions": {
       "target": "es2020",
       "module": "commonjs",
       "strict": true,
       "esModuleInterop": true,
       "outDir": "./dist",
       "rootDir": ""
     }
}
  1. 创建一个新文件 index.ts 并添加以下命令:
echo > index.ts

现在你准备开始编写你的脚本了!

编写脚本

让我们创建我们的脚本以使用 Yellowstone 监控 Pump.fun 铸币。我们将这个过程分为几个步骤:

第一步:导入依赖项并定义常量

创建一个新文件 index.ts,并以以下代码开始:

import Client, {
    CommitmentLevel,
    SubscribeRequest,
    SubscribeUpdate,
    SubscribeUpdateTransaction,
} from "@triton-one/yellowstone-grpc";
import { Message, CompiledInstruction } from "@triton-one/yellowstone-grpc/dist/grpc/solana-storage";
import { ClientDuplexStream } from '@grpc/grpc-js';
import { PublicKey } from '@solana/web3.js';
import bs58 from 'bs58';

// 常量
const ENDPOINT = "https://example.solana-mainnet.quiknode.pro:10000";
const TOKEN = "TOKEN_ID";
const PUMP_FUN_PROGRAM_ID = '6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P';
const PUMP_FUN_CREATE_IX_DISCRIMINATOR = Buffer.from([24, 30, 200, 40, 5, 28, 7, 119]);
const COMMITMENT = CommitmentLevel.CONFIRMED;

// 配置
const FILTER_CONFIG = {
    programIds: [PUMP_FUN_PROGRAM_ID],
    instructionDiscriminators: [PUMP_FUN_CREATE_IX_DISCRIMINATOR]
};

const ACCOUNTS_TO_INCLUDE = [{
    name: "mint",
    index: 0
}];

该部分导入必要的依赖项并定义我们将使用的常量。

  • 确保将 ENDPOINTTOKEN 替换为你的 QuickNode 端点和Token。
  • 我们从非官方 SDK 此处 获取 Pump.fun 程序的 create 指令鉴别符。
  • 我们定义了一个要监视的账户数组;在本例中,我们只关注新铸造代币的 mint 账户。随意增加你要监视的其他账户(你也可以从 IDL 中获取它们的索引)。

第二步:定义类型和接口

让我们为我们将生成的输出数据创建一个接口。添加以下类型定义:

// 类型定义
interface FormattedTransactionData {
    signature: string;
    slot: string;
    [accountName: string]: string;
}

该接口定义了我们格式化的交易数据的结构。我们使用一个可变长度数组 acccountName: string 来存储我们选择通过 ACCOUNTS_TO_INCLUDE 常量监视的每个账户的数据。在我们的例子中,它将是 mint: string(新铸造代币的地址)。

第三步:实现主函数

将主函数添加到你的脚本:

// 主函数
async function main(): Promise<void> {
    const client = new Client(ENDPOINT, TOKEN, {});
    const stream = await client.subscribe();
    const request = createSubscribeRequest();

    try {
        await sendSubscribeRequest(stream, request);
        console.log('Geyser 连接建立 - 正在监视新的 Pump.fun 铸造。 \n');
        await handleStreamEvents(stream);
    } catch (error) {
        console.error('订阅过程中发生错误:', error);
        stream.end();
    }
}

让我们逐步了解这些函数:

  • 该函数使用 @triton-one/yellowstone-grpc 库初始化 Yellowstone gRPC 客户端。
  • 我们使用客户端的 subscribe 方法创建一个流,该方法返回一个 Stream 对象。
  • 然后,我们使用一系列助理函数(我们将在下一个步骤中定义)来处理流事件并处理传入的数据:
    • 我们使用助理函数 createSubscribeRequest 创建一个订阅请求,该函数将定义我们要监视的账户、我们要监视的槽以及我们要监视的交易。
    • 然后我们将订阅请求发送到流,使用 sendSubscribeRequest 函数。
    • 我们调用 handleStreamEvents 来处理传入的数据并记录结果。

很好。现在,让我们添加这些助理函数。

第四步:实现助理函数

首先,让我们定义我们的 createSubscribeRequest 函数。该函数将简单地返回一个常量 SubscribeRequest 对象,我们将用它来配置我们的订阅。添加以下代码:

// 助理函数
function createSubscribeRequest(): SubscribeRequest {
    return {
        accounts: {},
        slots: {},
        transactions: {
            pumpFun: {
                accountInclude: FILTER_CONFIG.programIds,
                accountExclude: [],
                accountRequired: []
            }
        },
        transactionsStatus: {},
        entry: {},
        blocks: {},
        blocksMeta: {},
        commitment: COMMITMENT,
        accountsDataSlice: [],
        ping: undefined,
    };
}

这些代码仅仅指定我们将查找包括我们定义的 programIds(在这种情况下,即我们在常量中指定的 Pump.fun 程序)在内的交易数据即可。结构上,这个对象是你可以自定义所需接收数据的地方。有关可用选项的更多信息,请查看 文档

接下来,创建你的 sendSubscribeRequest 函数。该函数将接受订阅请求并将其写入流。添加以下代码:

function sendSubscribeRequest(
    stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>,
    request: SubscribeRequest
): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        stream.write(request, (err: Error | null) => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

最后,一旦我们建立了流,我们需要一个函数来处理正在传播的数据。让我们创建一个简单的函数,利用流的 on 方法来监听各种事件。添加以下代码:

function handleStreamEvents(stream: ClientDuplexStream<SubscribeRequest, SubscribeUpdate>): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        stream.on('data', handleData);
        stream.on("error", (error: Error) => {
            console.error('流错误:', error);
            reject(error);
            stream.end();
        });
        stream.on("end", () => {
            console.log('流结束');
            resolve();
        });
        stream.on("close", () => {
            console.log('流关闭');
            resolve();
        });
    });
}

我们只是指定流应如何处理 dataerrorendclose 事件。当我们接收到数据时,我们调用 handleData。我们还需要定义几个函数来处理数据处理--让我们现在就这样做。

第五步:实现数据处理函数

添加以下数据处理函数。我们将逐一详细描述它们。

function handleData(data: SubscribeUpdate): void {
    if (!isSubscribeUpdateTransaction(data) || !data.filters.includes('pumpFun')) {
        return;
    }

    const transaction = data.transaction?.transaction;
    const message = transaction?.transaction?.message;

    if (!transaction || !message) {
        return;
    }

    const matchingInstruction = message.instructions.find(matchesInstructionDiscriminator);
    if (!matchingInstruction) {
        return;
    }

    const formattedSignature = convertSignature(transaction.signature);
    const formattedData = formatData(message, formattedSignature.base58, data.transaction.slot);

    if (formattedData) {
        console.log("======================================💊 检测到新的 Pump.fun 铸造! ======================================");
        console.table(formattedData);
        console.log("\n");
    }
}

function isSubscribeUpdateTransaction(data: SubscribeUpdate): data is SubscribeUpdate & { transaction: SubscribeUpdateTransaction } {
    return (
        'transaction' in data &&
        typeof data.transaction === 'object' &&
        data.transaction !== null &&
        'slot' in data.transaction &&
        'transaction' in data.transaction
    );
}

function convertSignature(signature: Uint8Array): { base58: string } {
    return { base58: bs58.encode(Buffer.from(signature)) };
}

function formatData(message: Message, signature: string, slot: string): FormattedTransactionData | undefined {
    const matchingInstruction = message.instructions.find(matchesInstructionDiscriminator);

    if (!matchingInstruction) {
        return undefined;
    }

    const accountKeys = message.accountKeys;
    const includedAccounts = ACCOUNTS_TO_INCLUDE.reduce<Record<string, string>>((acc, { name, index }) => {
        const accountIndex = matchingInstruction.accounts[index];
        const publicKey = accountKeys[accountIndex];
        acc[name] = new PublicKey(publicKey).toBase58();
        return acc;
    }, {});

    return {
        signature,
        slot,
        ...includedAccounts
    };
}

function matchesInstructionDiscriminator(ix: CompiledInstruction): boolean {
    return ix?.data && FILTER_CONFIG.instructionDiscriminators.some(discriminator =>
        Buffer.from(discriminator).equals(ix.data.slice(0, 8))
    );
}

这些函数处理传入数据的处理,过滤相关交易并格式化输出。让我们详细看看这些函数:

  • handleData:此函数针对每个传入的数据块进行调用。它首先检查指令是否为预期格式,并包括我们的 pumpFun 过滤器。然后,我们过滤传入的结果,仅包括具有符合我们过滤标准的指令的交易。最后,我们格式化输出,包括交易签名和指令数据。然后我们记录结果。
  • convertSignature:此辅助函数使用 bs58 库将交易签名转换为 base58 字符串。
  • formatData:此辅助函数格式化输出,包括交易签名和交易中指定账户的数据
  • matchesInstructionDiscriminator:此辅助函数检查指令数据是否与我们过滤配置中的任何指令鉴别符匹配。

第六步:运行主函数

最后,在脚本的末尾添加这一行以运行主函数:

main().catch((err) => {
    console.error('主函数中未处理的错误:', err);
    process.exit(1);
});

这将执行我们的脚本并处理任何未处理的错误。通过这些步骤,你已经创建了一个完整的脚本,以使用 Yellowstone 监测 Pump.fun 铸币。让我们试试吧!

运行你的代码

现在我们已经编写了我们的脚本,让我们运行它看看实际效果。在你的终端中,运行以下命令:

ts-node index.ts

如果一切配置正确,你应该看到类似于以下的输出:

Geyser 连接建立 - 正在监视新的 Pump.fun 铸造

======================================💊 检测到新的 Pump.fun 铸造! =======================================
┌───────────┬────────────────────────────────────────────────────────────────────────┐
│ (index)   │ 值                                                                      │
├───────────┼────────────────────────────────────────────────────────────────────────┤
│ 签名      │ '4vzEaCkQnKym4TdDv67JF9VYMbvoMRwWU5E6TMZPSAbHJh4tXhsbcU8dkaFey1kFn...' │
│ 槽       │ '291788725'                                                            │
│ mint      │ 'AWcvL1GSNX8VDLm1nFWzB9u2o4guAmXM341imLaHpump'                         │
└───────────┴────────────────────────────────────────────────────────────────────────┘

该脚本将继续运行,实时监测新的 Pump.fun 铸币。每当检测到新铸币时,它将显示交易签名、槽号和新铸造代币的地址。

干得好!

减少响应大小

每个 Yellowstone 响应都会消耗 QuickNode API 信誉。为了最小化脚本中过多的无关响应,你应该尝试构建一个仅包含所需数据的过滤器。在上面的示例中,我们的 matchesInstructionDiscriminator 函数确实很好地过滤掉无关的交易,然而,这发生在客户端。我们可以在 createSubscribeRequest 函数中添加额外的过滤器,以减少从服务器接收到的数据量。如果我们回顾我们的交易账户过滤器,我们可以看到三个选项:accountIncludeaccountExcludeaccountRequired。让我们仔细看看它们的作用以及如何使用它们减少我们接收的数据量:

  • accountInclude:过滤出使用数组中任何账户的交易,
  • accountExclude:排除使用数组中任何账户的交易(与 accountInclude 的相反),
  • accountRequired:仅包括使用数组中 所有 账户的交易。

在我们的演示中,我们通过将 Pump.fun 程序 ID 传递到 accountInclude 数组中。这意味着服务器将返回涉及 Pump.fun 程序的所有交易。由于我们在此示例中知道我们只在寻找一组指令(在这种情况下,为 create 指令),我们可以查看 IDL 并识别出可能仅在 create 指令中传递的额外账户。在这种情况下,我们可能选择要求 Pump.fun 程序 ID 和 Pump.fun 代币铸造权限(此权限用于 create 指令)。这将减少我们接收的数据量,并使我们的脚本更高效。

这可以通过对我们的代码进行三处简单的修改来实现。首先,将铸造权限地址添加到我们的常量中:

// 常量
const PUMP_FUN_MINT_AUTHORITY = 'TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM';

更新你的 FILTER_CONFIG 常量以包括 requiredAccounts 数组:

// 配置
const FILTER_CONFIG = {
    programIds: [PUMP_FUN_PROGRAM_ID],
    requiredAccounts: [PUMP_FUN_PROGRAM_ID, PUMP_FUN_MINT_AUTHORITY],
    instructionDiscriminators: [PUMP_FUN_CREATE_IX_DISCRIMINATOR]
};

接下来,修改 createSubscribeRequest 函数以包括新的 requiredAccounts 过滤器:

// 助理函数
function createSubscribeRequest(): SubscribeRequest {
    return {
        accounts: {},
        slots: {},
        transactions: {
            pumpFun: {
                accountInclude: [],
                accountExclude: [],
                accountRequired: FILTER_CONFIG.requiredAccounts
            }
        },
        transactionsStatus: {},
        entry: {},
        blocks: {},
        blocksMeta: {},
        commitment: COMMITMENT,
        accountsDataSlice: [],
        ping: undefined,
    };
}

你现在可以重新运行你的脚本,应该会注意到与之前完全相同的示例输出,但处理的无关交易更少。这将帮助你节省 API 信誉并使你的脚本更加高效。干得不错!你可以使用这一实践来聚焦于你特定用例所需的确切数据。

总结

在本指南中,我们探讨了如何使用 Yellowstone,这个强大的 Geyser 插件,实时监测 Solana 程序。我们专注于跟踪 Pump.fun 程序的新代币铸造,但我们所覆盖的原则可以应用于监测任何 Solana 程序或账户更新。

随着你继续在 Solana 上开发,考虑如何利用像 Yellowstone 这样的 Geyser 插件来创建更具响应性和高效的应用程序。无论你是在构建交易机器人、分析仪表板,还是复杂的 DeFi 应用程序,实时数据访问都能为你提供显著的优势。

资源

让我们联系!

我们很想听听你是如何使用 Yellowstone 的。通过 TwitterDiscord 发送给我们你的经验、问题或反馈。

我们 ❤️ 反馈!

告诉我们 如果你有任何反馈或新主题的请求。我们很想听到你的声音。

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

0 条评论

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