신규: 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과 상호작용하는 확인 완료된 모든 성공 트랜잭션을 가져올 수 있습니다. 일반적으로 이 요청을 보내면 매초 수천 건의 트랜잭션이 반환됩니다. 따라서 노이즈를 필터링하는 간단한 방법에 집중해 보겠습니다.

노이즈 필터링

아래 코드 스니펫은 핵심 이벤트 처리 및 필터링 로직입니다. 메시지 수신 함수는 로그를 기준으로 트랜잭션에서 반환된 데이터를 파싱합니다. 여기서는 “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 구조

더 많은 데이터를 가져오려면 공식 Solana 문서에서 트랜잭션의 JSON 구조를 확인하세요. 또는 JSON 응답을 저장한 후 이 포매터를 사용해 구조를 살펴볼 수 있습니다. 이는 매우 중요합니다. 트랜잭션의 일반적인 구조를 이해하면 블록 탐색기에서 볼 수 있는 모든 데이터를 추출할 수 있기 때문입니다.

아래는 일반적인 JSON입니다. 여기에는 깊이 중첩된 두 개의 중요한 객체인 Transaction와 Meta가 있습니다. Transaction 내부에는 최근 블록해시, accountKeys, instructions를 담은 메시지 객체가 있습니다. Meta 내부에는 사전·사후 Balances(lamport 잔액), innerInstructions, logMessages, 사전·사후 token balances가 있습니다.

Pump.Fun 모니터 예제

현재 Solana 밈 코인의 흐름을 보고 매우 간단한 pump.fun 모니터를 만들었습니다. 모니터링할 주소를 pump.fun 온체인 프로그램인 “6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P”로 바꾸고 다음 메시지 수신 함수를 사용하면 됩니다.

코드
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 예제와 마찬가지로 accountKeys와 서명이 JSON의 동일한 위치에 중첩되어 있습니다. 따라서 생성자, 토큰, 서명을 쉽게 가져올 수 있습니다. 대부분의 트랜잭션은 JSON 구조가 거의 같지만 일부 값은 위치나 순서가 다를 수 있습니다. 다음 예제에서 이를 확인할 수 있습니다.

어쨌든 신규 pump.fun 토큰을 위한 매우 간단한 모니터입니다. 이 정보를 사용해 토큰을 매수하거나 모니터링 목적으로 토큰의 존재를 파악할 수 있습니다. 또한 다양한 Helius DAS API와 결합해 더 강력한 도구를 만들 수 있습니다. 예를 들어 메타데이터나 생성자 관련 정보를 가져올 수 있습니다.

Jupiter DCA 모니터

이 예제는 비교적 특수하지만 로그를 사용하면 모든 것을 확인할 수 있다는 점을 보여줍니다. 다음 요청을 사용해 Jupiter의 DCA(Dollar-Cost Average) 프로그램을 살펴보고, 구체적으로 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를 가져와 바이트를 사용자의 입력 수량, 매도 주기, 회당 매도 수량과 같은 입력 인수로 변환합니다.
코드
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 개발 소식을 확인하고 새 게시물 알림을 받아보세요

확대 이미지