新消息:Helius 收购 Light Protocol
如何使用 Geyser Enhanced Websockets 监控 Solana 交易
博客/开发

如何使用 Geyser Enhanced Websockets 监控 Solana 交易

集成工程师X 上的 zerkerzzz
阅读需 7 分钟

简介

很长一段时间里,我一直想知道这些监控应用和机器人是如何工作的。经过一番艰难的搜索,我发现了 Helius 的 Geyser Enhanced Websockets。虽然它们并非免费(你需要 Business 或 Professional 计划),但却是非常强大的工具。

通过 Helius 使用 Geyser Enhanced Websockets 很简单——粘贴你要监控的地址,然后运行代码即可。你可以监控任何对象:NFT、钱包、程序、平台,真的什么都可以。你可以制作钱包追踪器、代币追踪器、买卖监控器和交易量监控器等工具。

这类 API 和工具有时可能要花费数千美元,但本文将通过几个示例,展示如何用很少的成本实现这些功能。只需 499 美元(Business 计划),自制工具的 ROI 就近乎无限;无论是与他人共享还是自己使用,它都能让你获得优势,并提供无数种处理 Solana 实时数据的方式。

如果你刚开始使用 Helius Geyser Websockets,请阅读这篇博客文章和文档。你可以跟着操作,因为第一个示例相当容易理解。

Raydium 新资金池监控器

代码
const WebSocket = require('ws');

// Create a WebSocket connection
const ws = new WebSocket('wss://atlas-mainnet.helius-rpc.com?api-key=YOUR_API_KEY');

// Function to send a request to the WebSocket server
function sendRequest(ws) {
    const request = {
        jsonrpc: "2.0",
        id: 420,
        method: "transactionSubscribe",
        params: [
            {   failed: false,
                accountInclude:    ["675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"]
            },
            {
                commitment: "confirmed",
                encoding: "jsonParsed",
                transactionDetails: "full",
                maxSupportedTransactionVersion: 0
            }
        ]
    };
    ws.send(JSON.stringify(request));
}

这段代码的第一部分非常直观。我们设置 API 密钥,并使用要监控的账户发送请求,这里的账户是 675kPX9…,也就是 Raydium 链上程序。通过此请求,我们将获得所有与 Raydium 交互、已确认且未失败的交易。通常,发送此请求每秒会返回数千笔交易,因此我们来重点介绍一种过滤噪声的简单方法。

过滤噪声

下面的代码片段是主要的事件处理和过滤逻辑。on message 函数根据日志解析交易返回的数据。在本例中,我们要查看所有包含“initialize2: InitializeInstruction2”日志的交易——该日志会告诉我们用户何时在 Raydium 上创建了新的流动性池。你也可以根据监控目标使用任何其他日志。我建议针对要监控的操作创建一笔测试交易。例如,你可以向资金池添加流动性,查看该交易的日志内容,然后过滤这些日志,以获得所有添加了流动性的交易。

代码
ws.on('open', function open() {
    console.log('WebSocket is open');
    sendRequest(ws);  // Send a request once the WebSocket is open
});

ws.on('message', async function incoming(data) {
    const messageStr = data.toString('utf8');
    try {
        const messageObj = JSON.parse(messageStr);

        const result = messageObj.params.result;
        const logs = result.transaction.meta.logMessages;
        const signature = result.signature; // Extract the signature
        const accountKeys = result.transaction.transaction.message.accountKeys.map(ak => ak.pubkey); // Extract only pubkeys

        if (logs && logs.some(log => log.includes("initialize2: InitializeInstruction2"))) {
            // Log the signature, and the public keys of the AMM ID
            console.log('Transaction signature:', signature);
            console.log('AMM ID:', accountKeys[2]); // Corrected to the third account for AMM ID
        }
    } catch (e) {
        
    }
});

ws.on('error', function error(err) {
    console.error('WebSocket error:', err);
});

ws.on('close', function close() {
    console.log('WebSocket is closed');
});

确认交易包含该日志后,我们会提取两项内容:一是签名,用于比较并验证程序的准确性;二是账户密钥,也就是 AMM ID(即该资金池的 AMM 地址),因为许多机器人和狙击机器人会使用 AMM ID 发起交易。

如果需要,你还可以提取更多数据。例如,你可以获取创建者(通常是 accountKeys 中的第 17 个公钥)、用于创建资金池的代币(可从交易前后的代币余额或内部指令中获取)、代币数量,甚至是创建者收到的 LP 代币。创建者收到的 LP 代币数量可用于制作流动性销毁监控器,因为当资金池创建者销毁其 LP 代币时——这些代币本质上是其资金池份额的凭证——他们便放弃了移除流动性的能力(但要注意,如果铸造权限仍在他们手中,他们仍可铸造更多代币并将其卖入资金池)。

JSON 结构

要获取更多数据,你可以查看交易的 JSON 结构,相关信息可参阅 Solana 官方文档。或者,你也可以保存 JSON 响应,并使用这个格式化工具查看其结构。这一点非常重要,因为一旦了解交易的基本结构,你就可以提取使用区块浏览器时能看到的任何数据。

下面是通用 JSON,其中包含两个重要且深度嵌套的对象:Transaction 和 Meta。在 Transaction 中,有一个 message 对象,其中保存了最近的区块哈希、accountKeys 和 instructions。在 Meta 中,有交易前后的 Balances(lamport 余额)、innerInstructions、logMessages,以及交易前后的 token balances。

Pump.Fun 监控器示例

考虑到 Solana 上当前的 meme 币趋势,我制作了一个非常简单的 pump.fun 监控器。只需将我们监控的地址替换为“6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P”(即 pump.fun 链上程序),然后使用这个 on message 函数:

代码
ws.on('message', function incoming(data) {
    const messageStr = data.toString('utf8');
    try {
        const messageObj = JSON.parse(messageStr);

        const result = messageObj.params.result;
        const logs = result.transaction.meta.logMessages;
        const signature = result.signature; // Extract the signature
        const accountKeys = result.transaction.transaction.message.accountKeys.map(ak => ak.pubkey);

        if (logs && logs.some(log => log.includes('Program log: Instruction: InitializeMint2'))) {
            console.log('New pump.fun token!');
            console.log('tx:', signature);
            console.log('Creator:', accountKeys[0]);
            console.log('Token:', accountKeys[1]);

            // Log the first and second account keys if they exist
          
        }
    } catch (e) {
        
    }
});

与处理 Raydium 的方式类似,我们查看所有与 pump.fun 程序交互的交易,并根据所需日志进行过滤(规律已经很明显了:日志、日志、日志)。获得带有所需日志的交易后,我们会提取签名、创建者以及代币本身!

你可以看到,accountKeys 和签名在 JSON 中的嵌套位置与 Raydium 示例相同,因此可以轻松获取创建者、代币和签名。大多数交易的 JSON 结构基本相同,但某些值的位置或顺序可能有所不同——你将在下一个示例中看到这一点。

总之,这是一个相当简单的 pump.fun 新代币监控器。利用这些信息,你可以购买代币,也可以仅出于监控目的了解它的存在。你还可以将其与不同的 Helius DAS API 结合,打造更强大的工具,例如获取元数据或创建者的相关信息。

Jupiter DCA 监控器

这个示例相对小众,但它展示了如何通过日志查看一切。通过以下请求,我们将查看 Jupiter 的 DCA(定投)程序,更具体地说,是查看通过 Jupiter 完成的每一笔 DCA 买入。这个示例可用于套利交易计算,也可用于分析潜在的市场影响。利用 DCA 买入数据分析潜在市场影响的做法目前远未得到充分应用。

如你所见,到目前为止还没有任何复杂操作。我们在顶部添加了 base58 导入,并将地址改为 Jupiter DCA 程序。接下来的部分可能看起来相当复杂——某种程度上确实如此——但我认为提供一个稍复杂的示例很重要。

代码
const WebSocket = require("ws");
const bs58 = require("bs58");
// Create a WebSocket connection
const ws = new WebSocket(
  "wss://atlas-mainnet.helius-rpc.com?api-key=YOUR_API_KEY"
);

// Function to send a request to the WebSocket server
function sendRequest(ws) {
  const request = {
    jsonrpc: "2.0",
    id: 420,
    method: "transactionSubscribe",
    params: [
      {
        failed: false,
        accountInclude: ["DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M"],
      },
      {
        commitment: "confirmed",
        encoding: "jsonParsed",
        transactionDetails: "full",
        maxSupportedTransactionVersion: 0,
      },
    ],
  };
  ws.send(JSON.stringify(request));
}

这里所做的操作包括:

  • 使用 DCA 日志,仅过滤出此类交易。
  • 从交易中提取 User、Input Mint 和 Output Mint。
  • 获取 Instruction Data Raw,并将字节转换为输入参数,例如用户输入的数量、卖出频率以及每次卖出的数量。
代码
ws.on("message", async function incoming(data) {
  const messageStr = data.toString("utf8");
  try {
    const messageObj = JSON.parse(messageStr);

    const instructions = messageObj.params.result.transaction.transaction.message.instructions;
    const result = messageObj.params.result;
    const logs = result.transaction.meta.logMessages;
    // Extract only pubkeys

    if (
      logs &&
      logs.some((log) => log.includes("Program log: Instruction: OpenDcaV2"))
    ) {
      instructions.forEach((instruction) => {
        if (instruction.programId.includes("DCA265")) {
          if (instruction.accounts.length === 13) {
            console.log("User:", instruction.accounts[2]);
            console.log("Input Mint:", instruction.accounts[3]);
            console.log("Output Mint:", instruction.accounts[4]);

            const data = instruction.data;
            const bytedata = bs58.decode(data);

            const hexString = bytedata.toString("hex");
            const inAmountbytes = hexString.substring(16 * 2, 24 * 2);
            const cycleFrequencyBytes = hexString.substring(32 * 2, (32 + 8) * 2);
            const inAmountPerCycleBytes = hexString.substring(24 * 2, 32 * 2);

            // Reverse the byte order for little-endian interpretation
            const reversedCycleFrequencyBytes = cycleFrequencyBytes
              .match(/.{1,2}/g)
              .reverse()
              .join("");
            const reversedInAmountBytes = inAmountbytes
              .match(/.{1,2}/g)
              .reverse()
              .join("");
            const reversedInAmountPerCycleBytes = inAmountPerCycleBytes
              .match(/.{1,2}/g)
              .reverse()
              .join("");
            const cycleFrequency = BigInt("0x" + reversedCycleFrequencyBytes);
            const inAmount = BigInt("0x" + reversedInAmountBytes);
            const inAmountPerCycle = BigInt("0x" + reversedInAmountPerCycleBytes);

            console.log("Cycle Frequency every", cycleFrequency.toString() + " seconds");
            console.log("Amount input:", inAmount.toString());
            console.log("Amount  per cycle:", inAmountPerCycle.toString());
          
          }
        }
      });
      
    }
  } catch (e) {}
});*

解密原始指令数据

你看到的原始指令数据通常与输入参数相对应。通过这种方式获取数据并不困难,而且我们不需要进行任何反序列化——这通常会让很多人望而却步。

例如,我们来看看这笔交易中的数据:

8e772b6da2340bb12e783a66000000006d9415754e00000037ca8a3a270000003c00000000000000010000000000000000010000000000000000010000000000000000

现在,打开 hexed.it 并粘贴这些数据:

现在,我们来查找这些输入参数:

在右侧的搜索框中输入 336971797613。点击“查找下一个”,即可看到 inAmount 值在字节中的位置。

如你所见,它表明从 6D 开始,到 37 之前最后一个 00 为止的字节保存着 inAmount 的值。我们监控的这种格式的每笔 DCA 交易中,inAmount 都位于相同的字节位置。接下来的 8 个字节对保存着 inAmountPerCycle 的值。这是一种很巧妙的方法,可用于查找交易 JSON 中通常不明确的值,因为 JSON 主要包含地址和余额,而不是输入参数。

你做到了!

如果你坚持读到了这里,现在应该已经打下了使用 Helius Geyser WebSockets 的扎实基础。最难的部分是迈出第一步。现在,你可以监控钱包、程序、资金池,以及任何你想监控的对象。无需使用价格高得离谱的 API 或类似工具。如果遇到任何问题或有任何疑问,你随时可以在 Helius Discord 中提问!

资源

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片