新着: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キーを設定し、監視対象のアカウントを指定してリクエストを送信します。ここでは、Raydiumのオンチェーンプログラムである**675kPX9…**を指定します。このリクエストにより、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');
});

トランザクションに対象のログが含まれることを確認したら、2つの情報を抽出します。1つはプログラムの精度を比較・検証するための署名、もう1つはAMM ID、つまり対象プールのAMMアドレスであるアカウントキーです。多くのボットやスナイパーは、取引の開始にAMM IDを使用します。

必要に応じて、さらに多くのデータを抽出できます。たとえば、作成者(通常はaccountKeys内の17番目の公開鍵)、プールの作成に使用されたトークン(トークン残高の事前・事後データ、または内部命令から取得)、トークン数、さらには作成者が受け取るLPトークンも取得できます。作成者が受け取るLPトークンの数は、流動性バーンモニターの作成に利用できます。プールの作成者が、プール内の持分を示す受領トークンであるLPトークンをバーンすると、流動性を削除する権利を放棄することになるためです。ただし、ミント権限がまだ作成者の手元にある場合は、トークンを追加発行してプールで売却できるため注意してください。

JSON構造

さらに多くのデータを取得するには、公式SolanaドキュメントでトランザクションのJSON構造を確認できます。または、JSONレスポンスを保存し、このフォーマッター を使用して構造を表示できます。これは非常に重要です。トランザクションの一般的な構造を理解すれば、ブロックエクスプローラーで確認できるあらゆるデータを抽出できるようになります。

以下は一般的なJSONで、深くネストされた2つの重要なオブジェクト、TransactionとMetaが含まれています。Transaction内にはmessageオブジェクトがあり、recent blockhash、accountKeys、instructionsが格納されています。Meta内には、事前・事後のBalances(lamport残高)、innerInstructions、logMessages、事前・事後のtoken balancesがあります。

Pump.Funモニターの例

現在のSolanaミームコイン市場の動向を踏まえ、非常にシンプルなpump.funモニターを作成しました。監視するアドレスをpump.funのオンチェーンプログラムである「6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P」に変更し、次の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プログラムとやり取りするすべてのトランザクションを確認し、目的のログに基づいてフィルタリングします。ここでもポイントはログ、ログ、ログです。目的のログを含むトランザクションが見つかったら、署名、作成者、そしてトークン自体を抽出します。

Raydiumの例と同じく、JSON内ではaccountKeysと署名が同じ場所にネストされているため、作成者、トークン、署名を簡単に取得できます。JSON構造はほとんどのトランザクションで基本的に同じですが、一部の値は場所や順序が異なる場合があります。次の例でそれを確認します。

いずれにしても、これは新しいpump.funトークンを監視するための非常にシンプルなモニターです。この情報を使ってトークンを購入したり、監視目的でその存在を把握したりできます。また、各種のHelius DAS APIと組み合わせれば、メタデータや作成者に関する情報を取得するなど、さらに強力なツールを作成できます。

Jupiter DCAモニター

この例は比較的ニッチですが、ログを使えばあらゆる情報を確認できることを示しています。次のリクエストでは、JupiterのDCA(ドルコスト平均法)プログラムを調べます。より具体的には、Jupiterを利用して行われたすべてのDCA購入を確認します。この例は、アービトラージ取引の計算や潜在的な市場への影響の把握に利用できます。DCA購入データは、市場への潜在的な影響を確認する用途ではまだ十分に活用されていません。

ご覧のとおり、ここまでは複雑なことをしていません。先頭にbase58のimportを追加し、アドレスを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を取得し、バイトを入力引数に変換します。これには、ユーザーが入力した数量、売却頻度、1回あたりの売却量などが含まれます。
コード
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開発の最新情報や新しい記事の公開通知を受け取れます

拡大画像