НОВОЕ: Helius приобретает Light Protocol
getTransfersByAddress
Блог/Обновления

getTransfersByAddress: история переводов Solana в разобранном виде за 1 вызов

Продуктовая команда @ HeliusKiryl Miranovich в XKiryl Miranovich в LinkedIn
Чтение: 5 мин

getTransfersByAddress — новый эксклюзивный RPC-метод Helius для Solana, который возвращает разобранные и понятные человеку записи о переводах токенов и SOL для адреса кошелька. Он поддерживает встроенные фильтры по минту, времени, сумме, слоту, направлению и контрагенту.

Это идеальное дополнение к 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

Каждый объект перевода содержит подпись, слот, время блока, тип перевода, отправителя, получателя, минт, сумму (исходную и отображаемую), количество десятичных знаков, статус подтверждения и точные индексы инструкций. Это позволяет сопоставить каждый перевод с исходной транзакцией.

Код
{
  "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
}

Поле типа точно указывает, что произошло: transfer, transferFee, mint, burn, wrap, unwrap, changeAccountOwner или withdrawWithheldFee. Вам не придётся определять операцию по необработанным данным программы.

Почему переводы Solana сложно разбирать?

Перевод в транзакции Solana — не единая концепция.

Это категория, за которой скрывается множество пограничных случаев. Ошибка в любом из них искажает ваши данные.

SOL и WSOL

Для пользователя нативный SOL и Wrapped SOL выглядят как один актив, но в транзакции они находятся в разных местах.

Нативный SOL перемещается через балансы лампортов до и после транзакции в системных аккаунтах. WSOL перемещается через балансы SPL-токенов в токен-аккаунтах.

Пользователь при обмене на Jupiter может обернуть SOL в WSOL, обменять WSOL на USDC, а затем не развернуть остаток, оставив токен-аккаунт WSOL.

С точки зрения пользователя, он потратил SOL. С точки зрения сети, произошло три перевода и одно обёртывание.

Более того, само обёртывание не является переводом другому владельцу: тот же кошелёк перемещает лампорты в собственный токен-аккаунт. Если считать это переводом, активность пользователя будет учтена дважды.

Комиссии за переводы 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
Только подписиgetTransactionsForAddress с transactionDetails: 'signatures'
Потоковая передача переводов в реальном времениLaserStream

Начало работы

Метод getTransfersByAddress уже доступен на всех платных планах, начиная с плана Developer. Один запрос стоит 10 кредитов и учитывается в стандартной группе лимитов запросов RPC.

Используйте его с существующим RPC URL Helius:

Код
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 и получайте уведомления о наших публикациях