如何使用Shyft的gRPC服务实时流式传输Solana交易

  • Shyft_to
  • 发布于 2024-09-24 19:34
  • 阅读 19

本文介绍了如何使用Shyft的gRPC服务实时流式传输Solana交易。该方法利用gRPC的高性能通信框架,通过订阅特定数据流,可以近乎实时地接收链上发生的事件、账户更新或交易信息,并结合Shyft提供的工具,如交易解析器,快速解析和利用这些数据,适用于构建交易机器人、实时数据应用等。

Solana 实时交易:使用 Shyft 进行 gRPC 流式传输指南

Solana 的架构以极快的交易速度和低费用为特点,为构建去中心化应用程序 (dApp) 提供了一个对开发者友好的环境。Solana 上的许多应用程序,如交易机器人、分析和基于监控的应用程序,都依赖于实时数据来为用户提供有价值的见解并做出明智的决策。获取更新的传统方法可能会引入延迟,导致用户获得过时的数据从而导致错失机会和错误的决策。

这就是 gRPC 流式传输的用武之地。gRPC 是用于应用程序之间通信的高性能框架,并允许服务器和客户端之间持续的数据流(流)。这可以立即通知用户 任何事件、账户更新或交易,一旦它们在链上发生,使用户能够根据应用程序的需要对这些变化做出反应。gRPC 还提供强大的过滤功能,确保你的应用程序仅接收相关数据。在本文中,我们将探讨如何使用 Shyft 的 gRPC 服务实时流式传输 Solana 交易。

我们创建了一些 replit 示例,说明了使用 Shyft 进行 gRPC 流式传输,如果你想继续操作,请随时 fork 它们 —

注意:不要忘记在 secrets 部分添加 gRPC url 和 access token。有关更多详细信息,请参阅“开始之前”部分。

开始之前

要开始,我们需要一些东西。

身份验证:你的 Shyft API 密钥、gRPC 端点和 gRPC Token

你可以从 Shyft 网站 获取你自己的 Shyft API 密钥(Shyft 使用的身份验证参数)。你还可以在你的 Shyft 仪表板 上找到你所在区域的 gRPC 端点和访问Token。

用于接收 gRPC 数据的服务器端后端(如 NodeJS)

由于 web 浏览器不支持 gRPC 服务,因此你需要一个后端应用程序来接收 gRPC 数据。在本例中,我们使用了 NodeJS,但也可以使用任何其他后端 服务器端语言,如 C#、Go、Java、Kotlin、Python 或 PHP

涉及的步骤:摘要

虽然这篇博客探讨了 gRPC 流式传输在 Solana 上实时交易的更广泛应用,但让我们以 pump.fun 为例。在这里,我们将演示如何设置 gRPC 流以接收有关 pump.fun 交易的实时更新。但是,它们可以根据你的应用程序要求用于任何程序。

在幕后,Geyser 插件充当 Solana 网络和你的 dApp 之间的数据桥梁。它们捕获所有关键细节,如账户、区块,最重要的是交易。Shyft 的 Geyser 驱动的 gRPC 服务利用这些插件来提供此信息的连续流,确保你始终了解你正在为其设置流的程序上发生的事情。但是,设置流需要几个步骤:

  • 获取特定区域的 gRPC URL 和访问Token
  • 初始化用于接收交易的 gRPC 客户端
  • 请求流并获取实时数据
  • 处理和处理收到的数据

初始化 - gRPC 访问和客户端

从 Shyft 获取 gRPC url 和访问Token Shyft 的特定区域的 gRPC url 在仪表板中可用,访问Token在其 discord 服务器中可用。有关更多详细信息,请参阅 “开始之前” 部分。

初始化 gRPC 客户端 Shyft 提供 Dragon's Mouth gRPC 节点,最初由 Triton 开发,作为其 yellowstone 项目的一部分。它在各种语言中都有许多预构建的客户端库,可与 gRPC 接口交互。一些支持的语言及其客户端:

对于本文,我们使用了 NodeJS 客户端,但可以根据你的要求选择任何客户端。首先,我们使用以下命令在我们的项目中安装 NodeJS 客户端:

## 对于 npm,
npm install --save @triton-one/yellowstone-grpc
## 或者对于 yarn
yarn add @triton-one/yellowstone-grpc

安装 NodeJS 客户端库后,我们现在可以初始化客户端。我们只需从 @triton-one/yellowstone-grpc SDK 导入 'Client' 类。Client 类的构造函数接受两个必需的参数,首先是 Shyft gRPC 端点,其次是 gRPC 访问Token,这两个参数已经在上一步中获得。

可以按以下方式初始化 gRPC 客户端:

import Client from "@triton-one/yellowstone-grpc";

const client = new Client(
  "<https://grpc.us1.shyft.to>", //Your Region specific Shyft gRPC URL
  "hbdj-asjnf-access-token-asdh", //Shyft gRPC Access Token
  undefined,
);

初始化客户端后,我们现在可以调用任何客户端方法。请注意,客户端以异步方式运行,因此所有调用都应在异步块或异步函数中执行。

订阅实时交易

从 gRPC 接收 实时更新 的关键是通过 订阅流。Shyft gRPC 允许你使用 subscribe() 方法订阅特定的数据流。此方法返回一个流对象,该对象会在链上发生更新时立即发出更新。

const stream = await client.subscribe();

创建订阅流后,我们可以向 Shyft 的 gRPC 接口 发送 订阅请求,以便接收流式传输到我们后端服务器的特定更新。Shyft 的 gRPC 服务可以提供各种更新,包括账户更改、交易、新区块,甚至 slot 更新。但为了避免信息过载,你需要指定你实际需要的内容,为此有 不同类型的订阅请求,每种请求都有自己的一组参数。gRPC 上的订阅请求如下所示。

import { CommitmentLevel } from "@triton-one/yellowstone-grpc";

const req: SubscribeRequest = {
  accounts: {},
  slots: {},
  transactions: {},
  transactionsStatus: {},
  entry: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  ping: undefined,
  commitment: CommitmentLevel.CONFIRMED,
};

大多数请求参数都是不言自明的,并且与它们的名称完全一致,

  • “accounts” : 你可以通过指定此参数并根据提交级别(已处理、已确认、已完成)接收更新来订阅特定账户(例如,SOL-USDC OpenBook)。
  • 'accountDataSlice': 此字段可帮助你过滤 gRPC 流,以便你仅接收流式传输数据的相关部分。例如,你正在流式传输账户,其数据大小为 200 字节,但你只需要某个偏移量之后的 40 字节。此字段可以帮助你为流中的每次更新过滤这 40 字节。
  • “transactions” & “transactionsStatus”:你可以接收所有交易的更新,或根据特定标准(投票/失败交易,包括/排除账户)对其进行过滤。也可以使用此功能来监控程序。
  • “slots”、“blocks” & “blocksMeta” : 随时了解区块链上正在生成的新区块和 slot。
  • “commitment”:这指定任何更新的提交级别,无论是已处理、已确认还是已完成。

这是一个用于接收交易的示例订阅请求:

const req = {
  accounts: {},
  slots: {},
  transactions: {
    pumpFun: {
      vote: false,
      failed: false,
      signature: undefined,
      accountInclude: [PUMP_FUN_PROGRAM_ID.toBase58()], //Address 6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P
      accountExclude: [],
      accountRequired: [],
    },
  },
  transactionsStatus: {},
  entry: {},
  blocks: {},
  blocksMeta: {},
  accountsDataSlice: [],
  ping: undefined,
  commitment: CommitmentLevel.CONFIRMED, //for receiving confirmed txn updates
};

为了实时流式传输交易,我们使用 transactions 参数。你还可以使用 pumpFun 设置自定义标记(可以将其视为数据流的昵称)。votefailed 字段允许你选择是仅查看成功的交易 ( vote = true) 还是包括失败的交易 ( failed = true)。accountInclude 参数接受一组账户(任何 solana 地址),并向你发送提及账户参与的交易,在我们的例子中是程序地址(特定程序,例如 pump.fun)。最后,commitment 字段定义你希望交易达到的确认级别:processed(如草稿)、confirmed(更可靠)或 finalized(最可靠,但可能需要更长的时间)。请记住,将所有字段留空将广播所有交易。否则,这些字段像逻辑 AND 一样协同工作,数组中的值像逻辑 OR 一样工作。

修改和取消订阅

  • 修改订阅: 订阅流允许双向通信。你可以通过发送新的请求字符串来动态更新你的订阅过滤器。
  • 取消订阅: 要停止接收所有流的更新,请发送一个空的订阅请求。这将使连接保持打开状态,以供将来订阅。

成功设置流后,需要有一个函数来处理收到的交易,以便进一步操作。stream.on('data', callbackFunc()) 方法有助于处理流,如下所示。

//callback function that handles the stream
stream.on("data", (data) => {
    if (data?.transaction) {
      const txn = TXN_FORMATTER.formTransactionFromJson(
        data.transaction,
        Date.now(),
      ); //utility function
      const parsedTxn = decodePumpFunTxn(txn);
      //decoding the pump.fun transaction with SHYFT txn parser
      if (!parsedTxn) return;
        console.log(
          new Date(),
          ":",
          `New transaction <https://translator.shyft.to/tx/${txn.transaction.signatures[0]}> \\n`,
          JSON.stringify(parsedTxn, null, 2) + "\\n",
        ); //displaying the received transaction
      }
  });

预防措施 - 重新连接机制

虽然 gRPC 流式传输提供了一种可靠的方式来接收实时数据,但意外的网络问题仍然可能会中断连接。为了确保无缝体验,你可以在你的应用程序中实现 重新连接机制。以下是一个示例机制,如果 gRPC 流断开连接,它将自动尝试重新建立 gRPC 流。

async function subscribeCommand(client: Client, args: SubscribeRequest) {
  while (true) {
    try {
      await handleStream(client, args); //function which handles the stream
    } catch (error) {
        //iff the stream disconnects due to any error, this will wait for a second and restart the stream
      console.error("Stream error, restarting in 1 second...", error);
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
}

后续步骤 - 接收到的交易和解析

因此,我们已经了解了如何通过利用 Shyft 的 gRPC 服务无缝地实时流式传输 Solana 交易。以下是从 gRPC 接收到的原始交易:

{
  filters: [ 'pumpFun' ],
  account: undefined,
  slot: undefined,
  transaction: {
    transaction: {
      signature: Buffer(64) [Uint8Array] [\
        187,  60,  62,  68, 161, 246, 245, 181,   4,  69, 154.....\
      ],
      isVote: false,
      transaction: {
        signatures: [\
          Buffer(64) [Uint8Array] [\
            187,  60,  62,  68, 161, 246, 245, 181,   4,  69, 154...\
          ]\
        ],
        message: {
          header: {
            numRequiredSignatures: 1,
            numReadonlySignedAccounts: 0,
            numReadonlyUnsignedAccounts: 9
          },
          accountKeys: [\
            Buffer(32) [Uint8Array] [\
              223,  88, 182,  18, 209,  53, 153, 11.....\
            ]\
          ],
          recentBlockhash: Buffer(32) [Uint8Array] [\
            163,  84, 237, 169, 198,  24,  25, 131.....\
          ],
          instructions: [\
            {\
              programIdIndex: 14,\
              accounts: Uint8Array(0) [],\
              data: Buffer(9) [Uint8Array] [\
                3, 64, 66, 15, 0,\
                0,  0,  0,  0\
              ]\
            },\
            {\\
              programIdIndex: 15,\
              accounts: Buffer(2) [Uint8Array] [ 0, 1 ],\
              data: Buffer(124) [Uint8Array] [\
                  3,   0,   0,   0, 223,  88, 182,  18, 209,  53, 153,  11,\
                 ......\
              ]\
            },\
            {\
              programIdIndex: 16,\
              accounts: Buffer(4) [Uint8Array] [ 1, 17, 0, 18 ],\
              data: Buffer(1) [Uint8Array] [ 1 ]\
            },\
            {\
              programIdIndex: 19,\
              accounts: Buffer(18) [Uint8Array] [\
                16, 2, 20, 3,  4,  5,  6,\
                21, 7,  8, 9, 10, 11, 12,\
                22, 1, 13, 0\
              ],\
              data: Buffer(17) [Uint8Array] [\
                9, 184, 98, 141, 47,   0, 0,\
                0,   0, 79,  50, 51, 244, 1,\
                0,   0,  0\
              ]\
            },\
            {\
              programIdIndex: 16,\
              accounts: Buffer(3) [Uint8Array] [ 1, 0, 0 ],\
              data: Buffer(1) [Uint8Array] [ 9 ]\
            }\
          ],
          versioned: true,
          addressTableLookups: []
        }
      },
      meta: {
        err: undefined,
        fee: '805000',
        preBalances: [\
          '116096294303',  '0',\
          '6124800',       '23357760',\
          '16258560',      '2039280',\
          '7554878430798', '3591360',\
          '101977920',     '101977920',\
          '79594560',      '2039280',\
          '2039280',       '2039280',\
          '1',             '1',\
          '934087680',     '583216369927',\
          '1009200',       '1141440',\
          '4224577002',    '1141440',\
          '0'\
        ],
        postBalances: [\
          '115297694303',  '0',\
          '6124800',       '23357760',\
          '16258560',      '2039280',\
          '7555676225798', '3591360',\
          '101977920',     '101977920',\
          '79594560',      '2039280',\
          '2039280',       '2039280',\
          '1',             '1',\
          '934087680',     '583216369927',\
          '1009200',       '1141440',\
          '4224577002',    '1141440',\
          '0'\
        ],
        innerInstructions: [\
          {\
            index: 3,\
            instructions: [\
              {\
                programIdIndex: 16,\
                accounts: Buffer(3) [Uint8Array] [ 1, 6, 0 ],\
                data: Buffer(9) [Uint8Array] [\
                  3, 184, 98, 141, 47,\
                  0,   0,  0,   0\
                ],\
                stackHeight: 2\
              },\
              {\
                programIdIndex: 16,\
                accounts: Buffer(3) [Uint8Array] [ 5, 13, 20 ],\
                data: Buffer(9) [Uint8Array] [\
                  3, 96, 181, 51, 249,\
                  1,  0,   0,  0\
                ],\
                stackHeight: 2\
              }\
            ]\
          }\
        ],
        innerInstructionsNone: false,
        logMessages: [\
          'Program ComputeBudget111111111111111111111111111111 invoke [1]',\
          'Program ComputeBudget111111111111111111111111111111 success'\
          //shortend\
        ],
        logMessagesNone: false,
        preTokenBalances: [\
          {\
            accountIndex: 5,\
            mint: 'G2JNEiqhWunMnDGxGxgPYppWiCiovqKezS6XBgjiinP6',\
            uiTokenAmount: {\
              uiAmount: 80473675.722615,\
              decimals: 6,\
              amount: '80473675722615',\
              uiAmountString: '80473675.722615'\
            },\
            owner: '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1',\
            programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'\
          } //shortened\
        ],
        postTokenBalances: [\
          {\
            accountIndex: 5,\
            mint: 'G2JNEiqhWunMnDGxGxgPYppWiCiovqKezS6XBgjiinP6',\
            uiTokenAmount: {\
              uiAmount: 80465199.839767,\
              decimals: 6,\
              amount: '80465199839767',\
              uiAmountString: '80465199.839767'\
            },\
            owner: '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1',\
            programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'\
          } //shortened\
        ],
        rewards: [],
        loadedWritableAddresses: [],
        loadedReadonlyAddresses: [],
        returnData: undefined,
        returnDataNone: true,
        computeUnitsConsumed: '38821'
      },
      index: '322'
    },
    slot: '275598744'
  },
  block: undefined,
  ping: undefined,
  pong: undefined,
  blockMeta: undefined,
  entry: undefined
}

与传统的 RPC 响应相比,gRPC 交易流提供类似的数据,但有一些额外的字段。几个 相关的 额外字段 包括,isVote 用于识别投票交易,message headers 详细说明签名和签名者信息。该响应还表示它是否是 versioned transaction,并且它还为 blockpingentry 提供了规定。

为了将接收到的交易转换为类似于传统 RPC 的交易,我们使用了 Shyft 创建的以下实用程序函数。

import { TransactionFormatter } from "./utils/transaction-formatter";
const TXN_FORMATTER = new TransactionFormatter();

const txn = TXN_FORMATTER.formTransactionFromJson(
    data.transaction,
    Date.now(),
);

此函数返回 slot、version、blocktime、metatransactions,类似于传统的 Solana getTransaction RPC 调用 . 这也有助于你将流式传输的 gRPC 交易(和 gRPC 服务)集成到你现有的 Solana 应用程序中,而无需太多麻烦。

在后端接收到的原始交易需要进行解析,才能在任何应用程序中有效利用。 对于本示例,我们使用了 Shyft 的 交易解析器 来解析从 gRPC 接收到的交易。

function decodePumpFunTxn(tx: VersionedTransactionResponse) {
  if (tx.meta?.err) return;

  const paredIxs = PUMP_FUN_IX_PARSER.parseTransactionData(
    tx.transaction.message,
    tx.meta.loadedAddresses,
  );

  const pumpFunIxs = paredIxs.filter((ix) =>
    ix.programId.equals(PUMP_FUN_PROGRAM_ID),
  );

  if (pumpFunIxs.length === 0) return;
  const events = PUMP_FUN_EVENT_PARSER.parseEvent(tx);
  const result = { instructions: pumpFunIxs, events };
  bnLayoutFormatter(result);
  return result;
}

要了解有关解析交易的更多信息,请参阅我们的博客 如何在 Solana 中解析原始交易

一个特别有用的场景是 实时跟踪 Pump.fun 交换。以下是已解析的 pump.fun 交换交易的示例,

Solana 上已解析的 Pump.fun 交易

从这个解析的数据中,我们可以获得各种信息,例如 交换中涉及的代币交换的金额交换者的地址。通过实时流式传输交易,可以立即解析此信息并将其用于任何必要的应用程序中。

示例项目

我们已经在 replit 上创建了两个示例项目供你参考。只需在 secrets 部分中添加你的 gRPC urlaccess token 即可试用。立即在 Shyft Discord 上申请你的访问Token!

结论

在本文中,我们展示了如何使用 SHYFT 的 gRPC 服务 实时流式传输 Raydium v4 交易。凭借 gRPC 的速度和 SHYFT 的全球网络,设置实时数据流非常快速高效。这种经济实惠的解决方案非常适合自托管 API、运行交易机器人和构建实时数据应用程序。 获取你的 API 密钥,并立即开始流式传输!

如果你喜欢这篇关于 Solana 上 gRPC 流式传输的文章,请查看我们关于 使用 Shyft 和 Jito Bundle 构建 Telegram 交易机器人跟踪实时 orca 事件 的其他文章。我们希望你在使用 SHYFT 构建 dApp 时玩得开心。

资源

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

0 条评论

请先 登录 后评论
Shyft_to
Shyft_to
在 Solana上更快更智能地构建,使用Shyft的SuperIndexer、gRPC、RPC、API和SDK