Перейти к содержанию

Сети и токены

У каждого инвойса, платежа, баланса и вывода есть chainId и tokenId. И то и другое — UUID NullSwap, а не идентификаторы самой сети: маршрута, который принял бы вместо них "ethereum", 1 или адрес контракта, в API нет. Эти два эндпоинта и нужны, чтобы перевести то, что вы знаете, в идентификаторы, которые от вас требуются.

Сети

curl "https://api.pay.nullswap.com/api/v1/chains?type=EVM" \
  -H "x-api-key: sk_live_..."
{
  "data": [
    {
      "id": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
      "name": "Ethereum",
      "networkChainId": 1,
      "type": "EVM",
      "logoURI": "https://cdn.pay.nullswap.com/chains/ethereum.svg",
      "priority": 10,
      "confirmationBlocks": 12,
      "latestBlockNumber": 21345678,
      "explorerTxUrlTemplate": "https://etherscan.io/tx/{value}",
      "explorerAddressUrlTemplate": "https://etherscan.io/address/{value}"
    }
  ]
}

Фильтры: id, type, name. Все повторяются и сравниваются точно; name — без учёта регистра, но это не поиск по подстроке: name=Ethereum сработает, а name=eth не вернёт ничего.

Поле Примечания
networkChainId Идентификатор EIP-155. null в не-EVM сетях, где его попросту нет
type Семейство сетей: EVM, TRON, SOLANA, BITCOIN, LITECOIN, TON, MONERO
priority Подсказка для порядка отображения. Меньшее значение идёт первым
confirmationBlocks Глубина, которую должен набрать платёж, чтобы стать CONFIRMED
latestBlockNumber Вершина цепочки по последним данным нашего индексатора
explorerTxUrlTemplate Подставьте хеш транзакции вместо {value}. Пустая строка, если у сети нет обозревателя
explorerAddressUrlTemplate То же самое, только с адресом

Ответ — { "data": [...] } без объекта pagination: список короткий и отдаётся целиком, offset и limit здесь не принимаются.

latestBlockNumber — единственное поле, которое меняется от минуты к минуте; остальное достаточно стабильно, чтобы кешировать на сутки. В паре с blockNumber платежа оно показывает, на какой глубине платёж находится прямо сейчас:

depth = chain.latestBlockNumber - payment.blockNumber + 1

Считать глубину приходится самому: confirmations у платежа — снимок на момент последней записи, а не живой счётчик.

Токены

curl "https://api.pay.nullswap.com/api/v1/tokens?symbol=USDT&status=ENABLED" \
  -H "x-api-key: sk_live_..."
{
  "data": [
    {
      "id": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
      "chain": {
        "id": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
        "name": "Ethereum",
        "networkChainId": 1,
        "type": "EVM",
        "logoURI": "https://cdn.pay.nullswap.com/chains/ethereum.svg",
        "priority": 10,
        "confirmationBlocks": 12,
        "latestBlockNumber": 21345678,
        "explorerTxUrlTemplate": "https://etherscan.io/tx/{value}",
        "explorerAddressUrlTemplate": "https://etherscan.io/address/{value}"
      },
      "currency": {
        "id": "7d8e9f0a-1b2c-4d3e-8f40-5a6b7c8d9e0f",
        "name": "Tether USD",
        "symbol": "USDT",
        "logoURI": "https://cdn.pay.nullswap.com/tokens/usdt.svg",
        "decimals": 6,
        "priority": 20
      },
      "address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
      "type": "ERC20",
      "category": "HEDGE",
      "maxAmountOut": "500000000000",
      "decimals": 6,
      "price": "1000200000000000000",
      "status": "ENABLED",
      "monitoringCode": "usdt_eth"
    }
  ]
}

В каждый токен вложена сеть целиком, так что одного вызова хватит, чтобы отрисовать селектор, — склеивать ничего не придётся. Пагинации здесь тоже нет.

Фильтр Как сравнивается
query Подстрока без учёта регистра — по имени, символу и адресу контракта
id, chainId, currencyId, address, symbol Точно, без учёта регистра, можно повторять
status ENABLED, DISABLED
category NATIVE, HEDGE, ASSET

Разные фильтры объединяются через И, повторы одного фильтра — через ИЛИ. То есть ?chainId=A&chainId=B&status=ENABLED читается как (сеть A или сеть B) и включён.

Валюта и токен

Валюта — это актив таким, каким его называет человек: «Tether USD», USDT, 6 знаков после запятой. Токен — та же валюта, развёрнутая в одной конкретной сети по одному адресу. USDT в Ethereum и USDT в Tron — это два токена с общим currency.id.

Инвойсы, балансы и выводы всегда указывают на токен. currency.id пригодится разве что для группировки в вашем собственном интерфейсе.

decimals встречается в двух местах, и они не взаимозаменяемы

По token.decimals вы приводите к человеческому виду amount, expectedAmount, paidAmount и любые суммы в балансах и выводах. token.currency.decimals описывает валюту абстрактно. Почти у всех токенов из списка они совпадают, но переданная вам сумма масштабирована именно по token.decimals — его и читайте.

Остальные поля

Поле Примечания
address Адрес контракта. У токена NATIVE контракта нет, поэтому поле пустое
type NATIVE (собственная монета сети), ERC20 (контракты EVM и TRON), SPL (Solana)
category NATIVE, HEDGE (стейблкоин), ASSET (всё остальное)
maxAmountOut Наибольшая сумма в базовых единицах, которую мы выплатим в этом токене за раз
price Доллары США за целый токен, умноженные на 10^18
status На ENABLED можно создавать новые инвойсы, на DISABLED — нет
monitoringCode Внутренняя метка, не обращайте внимания

price — справочный курс, чтобы что-то показать или пересчитать. Инвойс по нему не рассчитывается: сумму NullSwap фиксирует в момент создания инвойса.

Список без фильтров включает и токены со статусом DISABLED

Так задумано. Инвойс, созданный месяц назад на токен, который с тех пор делистнули, всё ещё на него ссылается — и чтобы такой инвойс отрисовать, вам по-прежнему нужны его символ и decimals. Ставьте status=ENABLED, когда строите селектор, и убирайте фильтр, когда расшифровываете уже имеющийся у вас идентификатор.

Создать инвойс на токен со статусом DISABLED не выйдет — но откажут вам с 404 TokenWithIdNotFoundError, а не отдельной ошибкой «токен отключён».

Курсы

curl "https://api.pay.nullswap.com/api/v1/tokens/rate?srcTokenId=3b7e1a48-...&dstTokenId=8c1f2e3d-..." \
  -H "x-api-key: sk_live_..."
{
  "srcToken": { "id": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908", "…": "полный объект токена" },
  "dstToken": { "id": "8c1f2e3d-4a5b-4c6d-8e70-9f0a1b2c3d4e", "…": "полный объект токена" },
  "rate": "999400000000000000",
  "minAmountIn": "10000000",
  "maxAmountIn": "1000000000000",
  "maxAmountOut": "999000000000"
}

Оба query-параметра обязательны. rate — сколько единиц целевого токена даёт одна единица исходного, умноженное на 10^18. Комиссия платформы уже вычтена: вычитать что-либо самому не нужно.

amountOut ≈ amountIn × rate / 10^18

— с поправкой на разницу в decimals, если она у двух токенов есть.

minAmountIn и maxAmountIn ограничивают то, что маршрут примет, а maxAmountOut — то, что он выдаст. Так что пара может не пройти по ограничению на выход, даже если вход укладывается в свой диапазон.

Курс — это котировка, а не бронь

Ничего за вами не резервируется. Между этим вызовом и созданием инвойса курс успеет измениться, поэтому показывайте по нему цифру, а настоящим числом считайте expectedAmount в уже созданном инвойсе.

Этот эндпоинт — единственное место в API, где 404 отвечает в другом формате:

{ "statusCode": 404, "message": "Rate not found", "error": "Not Found" }

Текстовый message, лишний ключ error, timestamp отсутствует. Означает это одно: маршрута между двумя токенами нет — обычно пара не поддерживается, изредка временно не хватает ликвидности. В остальном API message — всегда имя класса ошибки; см. Ошибки.

Кеширование

Эндпоинт Кешировать на
/api/v1/chains Часы. Меняется тут только latestBlockNumber
/api/v1/tokens Часы для списка; минуты, если показываете price
/api/v1/tokens/rate Секунды — если кешировать вообще

Загружайте оба списка при старте и обновляйте по таймеру, а не дёргайте их на каждой оплате. Бессрочно не стоит кешировать только один случай — токен, идентификатор которого вы видите впервые: если на инвойсе, который должен был пройти, вернулся TokenWithIdNotFoundError, сперва обновите список, а потом делайте выводы.