{"content":{"title":"如何使用Typescript创建Websocket订阅以连接Solana区块链","body":"# 实时监控 Solana 数据的工具\r\n\r\n有几种方法可以监控 Solana 上的实时变化。如果你不确定哪种方法适合你的使用案例，请查看我们的 [博客文章](https://blog.quicknode.com/access-real-time-solana-data-3-tools-compared/)，了解选项的比较。如果你准备开始使用 Solana Web3js（版本 1.x）进行 WebSockets 开发，请继续阅读（如果你更喜欢版本 2.x，请查看我们的指南， [这里](https://learnblockchain.cn/article/11176)）！\r\n\r\n## 概述\r\n\r\n创建事件监听器是有效地提醒你的应用程序或用户你正在监控的内容已经变化的一种方式。Solana 有几个内置的实用事件监听器，也称为订阅，使得在 Solana 区块链上监听变化变得轻而易举。你不知道如何使用它吗？以下是一些可能会派上用场的示例：一个 Discord 机器人寻找与链上程序的交互（例如，销售机器人），一个 dApp 检查用户交易的错误，或者用户钱包余额变化时的手机通知。\r\n\r\n> 想要视频演示吗？请跟随 Sahil 学习[如何在 4 分钟内创建 Websocket 订阅以连接 Solana 区块链](https://www.youtube.com/watch?v=5NwFUXelPkQ)。\r\n\r\n \r\n\r\n#### **你将要做什么**\r\n\r\n在本指南中，你将学习如何使用几种 Solana 事件监听器方法和 QuickNode 的 Websocket 端点 (WSS://) 监听链上的变化。具体来说，你将创建一个简单的 TypeScript 应用程序以跟踪一个帐户（或钱包）的变化。接下来，你将学习如何使用 Solana 的取消订阅方法从应用程序中移除一个监听器。我们还将简要介绍一些 Solana 的其他监听器方法的基础知识。\r\n\r\n#### **你需要哪些准备**\r\n\r\n- 安装 [Nodejs](https://nodejs.org/en/)（版本 16.15 或更高）\r\n- 安装 npm 或 yarn（我们将使用 yarn 初始化我们的项目并安装必要的包。如果你更喜欢使用 npm，也可以照常使用）\r\n- 具备 TypeScript 经验并安装 [ts-node](https://www.npmjs.com/package/ts-node)\r\n- 安装 [Solana Web3](https://www.npmjs.com/package/@solana/web3.js)\r\n\r\n## 设置你的环境\r\n\r\n在终端中创建一个新项目目录：\r\n\r\n```bash\r\nmkdir solana-subscriptions\r\ncd solana-subscriptions\r\n```\r\n\r\n创建一个文件，**app.ts**：\r\n\r\n```bash\r\necho > app.ts\r\n```\r\n\r\n使用 \"yes\" 标志初始化你的项目以使用默认值创建新包：\r\n\r\n```bash\r\nyarn init --yes\r\n# 或者\r\nnpm init --yes\r\n```\r\n\r\n#### **安装 Solana Web3 依赖项：**\r\n\r\n```bash\r\nyarn add @solana/web3.js@1\r\n# 或者\r\nnpm install @solana/web3.js@1\r\n```\r\n\r\n在你喜欢的代码编辑器中打开 **app.ts**，在第 1 行从 Solana Web3 库中导入 **Connection**、**PublicKey** 和 **LAMPORTS_PER_SOL**：\r\n\r\n```typescript\r\nimport { Connection, PublicKey, LAMPORTS_PER_SOL } from \"@solana/web3.js\";\r\n```\r\n\r\n简化调试的日志\r\n\r\n你现在可以访问 RPC 端点的日志，帮助你更有效地排查问题。如果你在 RPC 调用中遇到问题，只需检查 QuickNode 信息面板中的日志，以快速识别和解决问题。了解更多关于日志历史限制的内容，请访问 [我们的定价页面](https://www.quicknode.com/pricing#features)\r\n\r\n好的！我们已经准备好开始了。\r\n\r\n## 设置你的 Quicknode 端点\r\n\r\n要在 Solana 上构建，你需要一个 API 端点来连接网络。你可以使用公共节点或部署并管理自己的基础设施；但是，如果你想要 8 倍更快的响应时间，你可以把繁重的工作交给我们。\r\n\r\n \r\n我们将使用一个 Solana Devnet 节点。复制 HTTP 提供程序 **和** WSS 提供程序链接：\r\n\r\n![新 Solana 端点](https://img.learnblockchain.cn/2025/02/25/0-9b8c136f77deeffa7ef3d7e66edc9bb8.png)\r\n\r\n在 **app.js** 的第 3 行和第 4 行上创建两个新变量来存储这些 URL：\r\n\r\n```typescript\r\nconst WSS_ENDPOINT = 'wss://example.solana-devnet.quiknode.pro/000/'; // 替换为你的 URL\r\nconst HTTP_ENDPOINT = 'https://example.solana-devnet.quiknode.pro/000/'; // 替换为你的 URL\r\n```\r\n\r\n## 建立与 Solana 的连接\r\n\r\n在第 5 行上，创建一个新的 **Connection** 连接到 Solana：\r\n\r\n```typescript\r\nconst solanaConnection = new Connection(HTTP_ENDPOINT, { wsEndpoint: WSS_ENDPOINT });\r\n```\r\n\r\n如果你以前创建过与 Solana 的 Connection 实例，你可能会注意到我们的参数有所不同，特别是包含了 `{wsEndpoint:WSS_ENDPOINT}`。让我们更深入地探讨。\r\n\r\n**Connection** 类的 _构造函数_ 允许我们传递一个可选的 **commitmentOrConfig**。我们可以包括一些有趣的选项，使用 [**ConnectionConfig**](https://solana-labs.github.io/solana-web3.js/modules.html#ConnectionConfig)，但今天我们要关注可选参数 **wsEndpoint**。这是一个选项，你可以提供指向完整节点 JSON RPC PubSub Websocket 端点的 URL。在我们的情况下，那是我们之前定义的 WSS 端点 **WSS_ENDPOINT**。\r\n\r\n> 如果你不传递 **wsEndpoint** 会发生什么？well，Solana 为此提供了一个函数 **makeWebsocketUrl**，它将你的端点 URL 中的 _https_ 替换为 _wss_ 或 _http_ 替换为 _ws_ （ [source](https://github.com/solana-labs/solana/blob/627d91fb20272afa5ce790fbec01b40a29fb9852/web3.js/src/connection.ts#L2452)）。由于所有 QuickNode HTTP 端点都有一个与之对应的的相同身份验证令牌的 WSS 端点，因此你可以省略此参数，除非你想使用单独的端点进行 Websocket 查询。\r\n\r\n让我们创建一些订阅吧！\r\n\r\n## 创建帐户订阅\r\n\r\n要在 Solana 上跟踪一个钱包，我们需要在 **solanaConnection** 上调用 **onAccountChange** 方法。我们将传递 **ACCOUNT_TO_WATCH**，我们要监控的钱包的公钥，以及一个回调函数。我们创建了一个简单的日志，提醒我们检测到事件并记录账户的新余额。在你的 **solanaConnection** 声明后的第 7 行添加以下代码片段：\r\n\r\n```typescript\r\n(async () => {\r\n    const ACCOUNT_TO_WATCH = new PublicKey('vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg'); // 替换为你自己的钱包地址\r\n    const subscriptionId = await solanaConnection.onAccountChange(\r\n        ACCOUNT_TO_WATCH,\r\n        (updatedAccountInfo) =>\r\n            console.log(`---事件通知 ${ACCOUNT_TO_WATCH.toString()}--- \\n新帐户余额:`, updatedAccountInfo.lamports / LAMPORTS_PER_SOL, ' SOL'),\r\n        \"confirmed\"\r\n    );\r\n    console.log('正在启动 web socket，订阅 ID: ', subscriptionId);\r\n})()\r\n```\r\n\r\n## 构建简单测试\r\n\r\n这段代码可以直接运行，但我们会添加一个额外的功能来帮助我们测试它是否正常工作。我们通过添加一个 **sleep** 函数（以添加时间延迟）和一个 **airdrop** 请求来实现这一点。在第 6 行的 _async_ 代码块之前，添加：\r\n\r\n```typescript\r\nconst sleep = (ms:number) => {\r\n    return new Promise(resolve => setTimeout(resolve, ms));\r\n}\r\n```\r\n\r\n然后在 _async_ 代码块中，\"Starting web socket\" 日志之后添加这个 airdrop 调用：\r\n\r\n```typescript\r\n    await sleep(10000); // 等待 10 秒以进行 socket 测试\r\n    await solanaConnection.requestAirdrop(ACCOUNT_TO_WATCH, LAMPORTS_PER_SOL);\r\n```\r\n\r\n你的代码将在 socket 启动后有效地等待 10 秒，然后请求钱包进行 airdrop（注意：这仅适用于 devnet 和 testnet）。\r\n\r\n我们的代码现在看起来像这样：\r\n\r\n```typescript\r\nimport { Connection, PublicKey, LAMPORTS_PER_SOL } from \"@solana/web3.js\";\r\n\r\nconst WSS_ENDPOINT = 'wss://example.solana-devnet.quiknode.pro/000/'; // 替换为你的 URL\r\nconst HTTP_ENDPOINT = 'https://example.solana-devnet.quiknode.pro/000/'; // 替换为你的 URL\r\nconst solanaConnection = new Connection(HTTP_ENDPOINT, { wsEndpoint: WSS_ENDPOINT });\r\nconst sleep = (ms:number) => {\r\n    return new Promise(resolve => setTimeout(resolve, ms));\r\n}\r\n\r\n(async () => {\r\n    const ACCOUNT_TO_WATCH = new PublicKey('vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg');\r\n    const subscriptionId = await solanaConnection.onAccountChange(\r\n        ACCOUNT_TO_WATCH,\r\n        (updatedAccountInfo) =>\r\n            console.log(`---事件通知 ${ACCOUNT_TO_WATCH.toString()}--- \\n新帐户余额:`, updatedAccountInfo.lamports / LAMPORTS_PER_SOL, ' SOL'),\r\n        \"confirmed\"\r\n    );\r\n    console.log('正在启动 web socket，订阅 ID: ', subscriptionId);\r\n    await sleep(10000); // 等待 10 秒以进行 socket 测试\r\n    await solanaConnection.requestAirdrop(ACCOUNT_TO_WATCH, LAMPORTS_PER_SOL);\r\n})()\r\n```\r\n\r\n## 开启你的 Socket！\r\n\r\n让我们测试一下。在终端中输入 **ts-node app.ts** 来启动你的 WebSocket！大约 10 秒后，你应该会看到一个终端回调日志，如下所示：\r\n\r\n```bash\r\nsolana-subscriptions % ts-node app.ts\r\n正在启动 web socket，订阅 ID:  0\r\n---事件通知 for vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg---\r\n新帐户余额: 88790.51694709  SOL\r\n```\r\n\r\n很好！你应该会注意到，即使在事件通知后，你的应用程序仍保持打开状态。这是因为我们的订阅仍然正在监听我们账户的变化。我们需要一种方法来 **取消订阅** 监听器。按 Ctrl+C 停止该过程。\r\n\r\n## 取消帐号变更监听器的订阅\r\n\r\nSolana 创建了一个内置方法，可用于从我们的帐户变更监听器中取消订阅 **removeAccountChangeListener**。该方法仅接受有效的 **subscriptionId** （数字）作为参数。在你的 _async_ 块内，在 **airdrop** 之后添加另一个 **sleep**，以允许时间处理事务，然后调用 **removeAccountChangeListener**：\r\n\r\n```typescript\r\n    await sleep(10000); // 等待 10 秒以进行 socket 测试\r\n    await solanaConnection.removeAccountChangeListener(subscriptionId);\r\n    console.log(`Websocket ID: ${subscriptionId} 已关闭。`);\r\n```\r\n\r\n现在再次运行你的代码，你将看到相同的序列，后跟关闭的 WebSocket：\r\n\r\n```bash\r\nsolana-subscriptions % ts-node app.ts\r\n正在启动 web socket，订阅 ID:  0\r\n---事件通知 for vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg---\r\n新帐户余额: 88791.51694709  SOL\r\nWebsocket ID: 0 已关闭。\r\n```\r\n\r\n做得不错！如你所见，这在你希望在某件事情发生后禁用听众时非常有用（例如时间流逝、实现特定阈值、达到通知次数等）。\r\n\r\n我们已将此脚本的最终代码发布在 [我们的 Github 仓库](https://github.com/quiknode-labs/technical-content/blob/main/solana/websockets/app.ts) 以供参考。\r\n\r\n## 其他 Solana WebSocket 订阅\r\n\r\nSolana 还有几种其他类似的 WebSocket 订阅/取消订阅方法，也很有用。我们将在本节中简要描述它们。\r\n\r\n- **onProgramAccountChange**：注册一个回调，当特定程序拥有的账户发生变化时将被调用。传递一个程序的 PublicKey 和可选的账户过滤器数组。除了其他功能外，这对于跟踪用户的 Token 账户的变化可能很有用。使用 **removeProgramAccountChangeListener** 取消订阅。\r\n- **onLogs**：注册一个回调，当日志被发出时将被调用——通过传递有效的 _PublicKey_ 可以与 **onAccountChange** 的方式类似使用。该回调将返回最近的交易 ID。使用 **removeOnLogsListener** 取消订阅。\r\n- **onSlotChange**：注册一个回调，在 Slot 变化时将被调用。仅将回调函数传递到该方法中。使用 **removeSlotChangeListener** 取消订阅。\r\n- **onSignature**：注册一个回调，当签名更新时将被调用，通过传递有效的 _交易签名_。该回调将返回交易是否经历了错误。使用 **removeSignatureListener** 取消订阅。\r\n- **onRootChange**：注册一个回调，在根变化时将被调用。仅将回调函数传递到该方法中。使用 **removeRootChangeListener** 取消订阅。\r\n\r\n_\\*注意：所有 [取消订阅方法](https://www.quicknode.com/guides/solana-development/getting-started/how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript#unsubscribe-methods)都需要_ **subscriptionId** _（数字）参数_。有关这些方法的更多信息，请查看我们的文档 [quicknode.com/docs/solana](https://www.quicknode.com/docs/solana?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)。你可以通过对 **app.js** 进行小的修改来尝试其他订阅。\r\n\r\n## 计费和优化\r\n\r\nWebSocket 方法的计费积分基于收到的响应数量，而不是订阅数量。例如，如果你打开一个 `accountChange` 订阅并收到 100 个响应，你的账户将被收取 5,000 积分（ [每个响应 50 积分](https://www.quicknode.com/api-credits/sol#:~:text=Value-,accountsubscribe,-50) X 100 响应）。\r\n\r\n为了优化你的订阅并确保你没有为不必要的订阅或无关的响应付费，你应该考虑以下几点：\r\n\r\n- 使用 _unsubscribe_ 方法在你不再感兴趣时移除监听器。\r\n- 使用适用于的方法的过滤器，以仅接收相关数据。\r\n\r\n### 取消订阅方法\r\n\r\n以下方法用于从订阅中移除监听器：\r\n\r\n- [`accountUnsubscribe`](https://www.quicknode.com/docs/solana/accountUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅账户更新。\r\n- [`logsUnsubscribe`](https://www.quicknode.com/docs/solana/logUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅日志。\r\n- [`programUnsubscribe`](https://www.quicknode.com/docs/solana/programUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅程序账户更新。\r\n- [`slotUnsubscribe`](https://www.quicknode.com/docs/solana/slotUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅槽更新。\r\n- [`signatureUnsubscribe`](https://www.quicknode.com/docs/solana/signatureUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅签名更新。\r\n- [`rootUnsubscribe`](https://www.quicknode.com/docs/solana/rootUnsubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：取消订阅根更新。\r\n\r\n### 使用 logsSubscribe 方法的过滤器\r\n\r\n[`logsSubscribe`](https://www.quicknode.com/docs/solana/logsSubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript) 方法允许你过滤事务日志，可以显著减少你收到的响应数量。以下是一个示例：\r\n\r\n```typescript\r\n// 订阅所有日志（昂贵且不推荐）：\r\nconst subscriptionId = await connection.onLogs(\r\n  'all',\r\n  (logs) => {\r\n    console.log('新日志:', logs);\r\n  }\r\n);\r\n\r\n// 仅订阅提到特定地址的日志：\r\nconst EXAMPLE_PUBLIC_KEY = new PublicKey('YOUR_ADDRESS_HERE');\r\nconst specificAddressSubscriptionId = await connection.onLogs(\r\n  EXAMPLE_PUBLIC_KEY,\r\n  (logs) => {\r\n    console.log('提到特定地址的日志:', logs);\r\n  }\r\n);\r\n```\r\n\r\n### 使用 programSubscribe 方法的过滤器\r\n\r\n[`programSubscribe`](https://www.quicknode.com/docs/solana/programSubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript) 方法功能强大，但可能会生成许多响应。使用过滤器来缩小你接收的数据：\r\n\r\n```typescript\r\nconst filters = [\r\n  {\r\n    memcmp: {\r\n      offset: 0, // 指定账户数据中的偏移量\r\n      bytes: 'base58_encoded_bytes_here' // 指定要比较的字节\r\n    }\r\n  },\r\n  {\r\n    dataSize: 128 // 如果你的账户数据是固定的，可以通过数据大小过滤——更新此值以匹配你账户的数据大小\r\n  }\r\n];\r\n\r\nconst subscriptionId = await connection.onProgramAccountChange(\r\n  new PublicKey('Your_Program_ID_Here'),\r\n  (accountInfo) => {\r\n    console.log('账户已更改:', accountInfo);\r\n  },\r\n  'confirmed',\r\n  filters\r\n);\r\n```\r\n\r\n在这个示例中，我们使用了 `memcmp` 和 `dataSize` 过滤器。`memcmp` 过滤器在账户数据的特定偏移量比较一系列字节，而 `dataSize` 过滤器则根据数据大小过滤账户。这一组合可以显著减少你收到的响应数量，专注于匹配你特定标准的账户。有关使用 _GetProgramAccountsFilter_ 的更多信息，请参见 [Solana RPC 文档](https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-get-all-tokens-held-by-a-wallet-in-solana/?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)。\r\n\r\n### 优化的最佳实践\r\n\r\n1. **确保过滤器的具体性**：你的过滤器越具体，收到的无关响应就越少。\r\n\r\n2. **根据你的使用情况使用适当的承诺级别**：更高的承诺级别（例如 \"finalized\"）可能会导致更新较少，但更确定。\r\n\r\n3. **定期审核并移除未使用的订阅**：审核你的活动订阅，并删除任何不再需要的订阅。\r\n\r\n4. **监控你的使用情况**：跟踪你接收的响应数量，必要时调整你的过滤器或订阅策略（ [QuickNode 信息面板](https://dashboard.quicknode.com/?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)）。\r\n\r\n通过实施这些优化策略，你可以确保只接收所需的数据，从而最大限度地减少不必要的计费积分消耗，同时保持你 Solana 应用程序的有效性。\r\n\r\n## 替代解决方案\r\n\r\nQuickNode 提供多种解决方案，以获取 Solana 的实时数据。查看以下选项，找到适合你用例的工具：\r\n\r\n- [WebSockets](https://www.quicknode.com/docs/solana/accountSubscribe?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：正如本指南所讨论，WebSockets 提供了直接连接到 Solana 节点的实时更新——这些适合简单应用程序和快速开发。如果你想与 Solana WebSockets 一起使用 Solana Web3.js 2.0+，请查看我们的 [指南：使用 WebSockets 和 Solana Web3.js 2.0 监控 Solana 账户](https://learnblockchain.cn/article/11176)。\r\n- [Yellowstone gRPC Geyser 插件](https://marketplace.quicknode.com/add-on/yellowstone-grpc-geyser-plugin?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：Yellowstone gRPC Geyser 插件为流式 Solana 数据提供强大的 gRPC 接口，支持内置过滤和历史数据。\r\n- [Streams](https://www.quicknode.com/streams/solana?utm_source=internal&utm_campaign=guides&utm_content=how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)：用于处理和路由 Solana 数据到多个目的地的托管解决方案，支持内置过滤和历史数据。\r\n\r\n有关更多信息，请查看我们的 [博客文章](https://blog.quicknode.com/access-real-time-solana-data-3-tools-compared/)。\r\n\r\n## 结论\r\n\r\n做得不错！现在你应该掌握如何使用 Solana 的 WebSocket 订阅。你将如何使用 Solana WebSockets？我们希望看到你正在创建的内容！在 [Discord](https://discord.gg/quicknode) 或 [Twitter](https://twitter.com/QuickNode) 与我们分享你的应用程序。\r\n\r\n要了解更多信息，请查看我们其他的 Solana 教程 [这里](https://www.quicknode.com/guides/)，如果你在使用 WebSocket 订阅中获得乐趣，请考虑订阅我们的 [新闻通讯](https://go.quicknode.com/newsletter)。\r\n \r\n\r\n>- 原文链接： [quicknode.com/guides/sol...](https://www.quicknode.com/guides/solana-development/getting-started/how-to-create-websocket-subscriptions-to-solana-blockchain-using-typescript)\r\n>- 登链社区 AI 助手，为大家转译优秀英文文章，如有翻译不通的地方，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":null,"timestamp":1740449386,"version":1}