신규: Helius가 Light Protocol을 인수했습니다
getTransfersByAddress
블로그/업데이트

getTransfersByAddress: 한 번의 호출로 파싱된 Solana 전송 내역 조회

Helius 제품팀X의 Kiryl MiranovichLinkedIn의 Kiryl Miranovich
읽는 데 5분

getTransfersByAddress는 지갑 주소의 파싱된 읽기 쉬운 토큰 및 SOL 전송 기록을 반환하는 Helius 전용 신규 Solana RPC 메서드입니다. 민트, 시간, 금액, 슬롯, 방향, 거래 상대방을 기준으로 필터링할 수 있습니다.

getTransactionsForAddress(gTFA)를 완벽하게 보완합니다. gTFA가 전체 트랜잭션 페이로드를 반환한다면, getTransfersByAddress는 누가 무엇을 누구에게 언제 얼마나 보냈는지 보여주는 간결한 전송 객체를 반환합니다.

전송 전용 RPC 메서드가 필요한 이유

대부분의 지갑, 결제, 포트폴리오 제품에는 전체 트랜잭션 페이로드가 필요하지 않습니다. 필요한 것은 전송 내역입니다.

그렇다면 지금까지 어떻게 처리했을까요? 각 팀이 동일한 전송 파서를 제각기 구현했고, 안타깝게도 대부분 엣지 케이스를 제대로 처리하지 못했습니다.

지금까지 깔끔한 Solana 전송 내역을 구축하려면 개발자가 다음 작업을 수행해야 했습니다.

  1. getSignaturesForAddress로 서명 가져오기
  2. getTransaction로 각 서명 가져오기
  3. 이전/이후 잔액, 토큰 잔액, 내부 명령 파싱하기
  4. 전송을 재구성하고, SPL Token과 Token-2022의 수수료 의미 체계 차이를 처리하며, WSOL 래핑/언래핑 노이즈 정리하기
  5. 여러 페이지에 걸쳐 반복하고, 재시도를 처리하며, 결과 저장하기

getTransactionsForAddress 메서드로 1단계와 2단계를 한 번의 호출로 줄여도, 3~5단계는 여전히 개발자가 처리해야 합니다.

이제 getTransfersByAddress가 이 작업을 대신 수행하고 결과를 구조화된 목록으로 반환합니다.

getTransfersByAddress 응답

각 전송 객체에는 서명, 슬롯, 블록 시간, 전송 유형, 발신자, 수신자, 민트, 금액(원시 값 및 UI 값), 소수 자릿수, 확인 상태, 정확한 명령 인덱스가 포함됩니다. 따라서 각 전송을 원본 트랜잭션과 연결할 수 있습니다.

코드
{
  "signature": "<TX_SIGNATURE>",
  "slot": 315073428,
  "blockTime": 1736159420,
  "type": "transfer",
  "fromUserAccount": "<SENDER_WALLET>",
  "toUserAccount": "<RECIPIENT_WALLET>",
  "fromTokenAccount": "<SENDER_TOKEN_ACCOUNT>",
  "toTokenAccount": "<RECIPIENT_TOKEN_ACCOUNT>",
  "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "amount": "2500000",
  "decimals": 6,
  "uiAmount": "2.5",
  "confirmationStatus": "finalized",
  "transactionIdx": 35,
  "instructionIdx": 1,
  "innerInstructionIdx": 0
}

type 필드는 어떤 일이 발생했는지 정확히 알려줍니다. transfer, transferFee, mint, burn, wrap, unwrap, changeAccountOwner, withdrawWithheldFee 중 하나로 표시되므로 원시 프로그램 데이터에서 동작을 추론할 필요가 없습니다.

Solana 전송 파싱이 어려운 이유

Solana 트랜잭션에서 전송은 하나의 단순한 개념이 아닙니다.

여러 엣지 케이스를 아우르는 범주이며, 그중 하나라도 잘못 처리하면 데이터가 손상됩니다.

SOL과 WSOL

네이티브 SOL과 Wrapped SOL은 사용자에게 같은 자산처럼 보이지만, 트랜잭션에서는 서로 다른 영역에 존재합니다.

네이티브 SOL은 시스템 계정의 이전/이후 lamport 잔액을 통해 이동합니다. WSOL은 토큰 계정의 SPL 토큰 잔액을 통해 이동합니다.

사용자가 Jupiter에서 스왑하면 SOL을 WSOL로 래핑하고, WSOL을 USDC로 스왑한 뒤, 언래핑하지 않아 WSOL 토큰 계정이 남을 수 있습니다.

사용자 관점에서는 SOL을 지출한 것입니다. 네트워크 관점에서는 세 번의 전송과 한 번의 래핑이 발생한 것입니다.

더 큰 문제는 래핑 자체가 다른 소유자에게 보내는 전송이 아니라는 점입니다. 동일한 지갑이 lamport를 자체 토큰 계정으로 옮기는 작업입니다. 이를 전송으로 집계하면 사용자의 활동이 중복 계산됩니다.

Token-2022 전송 수수료

Token-2022는 발신자의 차감액과 수신자의 입금액이 일치하지 않는 TransferCheckedWithFee을 도입했습니다.

차액은 수수료로 수신자의 토큰 계정에 보류되며, 이후 withdrawWithheldFee을 통해 수수료 권한자에게 지급할 수 있습니다.

단순한 파서는 이를 한 번의 전송으로 보고 금액을 잘못 계산합니다. 정교한 파서는 수수료 확장을 감지하고, 명령을 전송과 보류 수수료 누적으로 분리하며, 수수료 계정을 별도로 추적합니다.

민팅과 소각

계정에 민팅된 토큰에는 발신자가 없습니다. 소각된 토큰에는 수신자가 없습니다. 둘 다 이전/이후 잔액 변동에서는 "전송"처럼 보이지만, 이를 지갑 간 전송과 혼동하면 거래 상대방 분석이 왜곡됩니다. 지갑이 제로 주소에서 자금을 "받고" 허공으로 자금을 "보내는" 것처럼 표시됩니다.

getTransfersByAddress는 이를 mint 및 burn 유형으로 나타내고, fromUserAccount 또는 toUserAccount을 null으로 설정합니다. 구축하는 제품에 따라 이를 포함하거나 제외할 수 있습니다.

getTransfersByAddress의 이점

getTransfersByAddress 메서드는 이전까지 클라이언트 측에서 전체 트랜잭션 내역을 가져와 파싱해야만 적용할 수 있었던 필터를 지원합니다. 

민트로 검색

특정 토큰의 전송만 반환합니다.

코드
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "getTransfersByAddress",
  "params": [
    "<WALLET_ADDRESS>",
    { "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }
  ]
}

금액으로 검색

gt, gte, lt, lte 비교 연산으로 원시 금액을 필터링합니다. 고래를 식별하거나, 더스트(즉, 토큰 금액이 미미한 계정)를 제외하거나, 비정상적인 활동을 표시할 때 유용합니다.

코드
{
  "params": [
    "<WALLET_ADDRESS>",
    {
      "mint": "So11111111111111111111111111111111111111112",
      "filters": {
        "amount": { "gte": 1000000000, "lt": 10000000000 }
      }
    }
  ]
}

시간으로 검색

블록 시간을 Unix 타임스탬프 범위로 지정할 수 있습니다. 슬롯 범위도 동일하게 작동하므로 슬롯 단위의 정밀한 쿼리가 가능합니다.

코드
{
  "params": [
    "<WALLET_ADDRESS>",
    {
      "filters": {
        "blockTime": { "gte": 1735718400, "lt": 1738396800 }
      }
    }
  ]
}

거래 상대방으로 검색

with 및 direction 매개변수를 조합해 두 특정 지갑 사이의 양방향 전송을 조회할 수 있습니다.

코드
{
  "params": [
    "<WALLET_ADDRESS>",
    {
      "with": "<COUNTERPARTY_WALLET>",
      "direction": "in"
    }
  ]
}

SOL 모드

네이티브 SOL과 WSOL은 Solana에서 서로 다르게 표시되지만 사용자에게는 대개 같은 의미이므로, getTransfersByAddress 메서드는 solMode 매개변수를 제공합니다.

merged (기본값)

WSOL을 네이티브 SOL로 처리합니다.

래핑 및 언래핑 행은 제외되며, 네이티브 SOL 민트로 조회하면 네이티브 SOL과 WSOL 전송이 모두 반환됩니다.

separate

이 모드에서는 WSOL이 별도의 민트로 유지되며, 완전한 감사 가능성을 위해 래핑 및 언래핑 수명 주기 행이 포함됩니다.

대부분의 제품 사용 사례에는 merged이 적합합니다. 조정, 회계, 프로토콜 수준 분석에는 separate이 적합합니다.

페이지네이션 및 정렬

paginationToken을 사용하는 표준 커서 기반 페이지네이션으로, 페이지당 최대 100개 레코드를 제공합니다. sortOrder은 asc 및 desc을 지원합니다.

코드
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "getTransfersByAddress",
  "params": [
    "<WALLET_ADDRESS>",
    { "limit": 50, "paginationToken": "315069220:308:2:1" }
  ]
}

getTransfersByAddress를 사용해야 하는 경우

getTransfersByAddress와 getTransactionsForAddress은 비슷하지만 용도가 다릅니다. 

필요 사항메서드
필터가 적용된 파싱된 토큰 및 SOL 전송getTransfersByAddress
전체 트랜잭션 페이로드 또는 전송 외 활동getTransactionsForAddress
모든 서명 또는 주소의 디코딩된 명령Parsed Events API
서명만 조회transactionDetails: 'signatures'을 사용하는 getTransactionsForAddress
전송 실시간 스트리밍LaserStream

시작하기

getTransfersByAddress 메서드는 현재 Developer 플랜부터 모든 유료 플랜에서 사용할 수 있습니다. 요청당 10크레딧이 부과되며 표준 RPC 속도 제한 그룹에 포함됩니다.

기존 Helius RPC URL에서 사용하세요.

코드
const response = await fetch("https://mainnet.helius-rpc.com/?api-key=YOUR_API_KEY", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "1",
    method: "getTransfersByAddress",
    params: ["<WALLET_ADDRESS>"]
  })
});

const data = await response.json();
console.log(data.result.data);

전체 매개변수와 응답에 관한 자세한 내용은 API 레퍼런스를 확인하세요.

Helius 구독하기

최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요