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

Сети и токены

У каждого инвойса, платежа, баланса и вывода есть 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}",
      "standard": "ERC20"
    }
  ]
}

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

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

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

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

depth = chain.latestBlockNumber - payment.blockNumber + 1

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

На RIPPLE, то есть в XRP Ledger, блок — это леджер: latestBlockNumber и blockNumber платежа — индексы леджеров, и читаются только леджеры, уже валидированные сетью. Монета — XRP с decimals: 6, так что "1000000" — это 1 XRP. Депозит там задаётся адресом и тегом назначения (destination tag) — см. инвойсы.

Токены

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}",
        "standard": "ERC20"
      },
      "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": "100000000000",
      "decimals": 6,
      "price": "1000200000000000000",
      "status": "ENABLED",
      "srcStatus": "ENABLED",
      "dstStatus": "ENABLED",
      "monitoringCode": "usdt_eth"
    }
  ]
}

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

Фильтр Как сравнивается
query Подстрока без учёта регистра — по имени и символу валюты. По адресам контрактов не ищет — для этого есть address
id, chainId, currencyId, address, symbol Точно, без учёта регистра, можно повторять
status ENABLED, DISABLED. Сравнивается с srcStatus
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 Адрес контракта, для JETTON — jetton master. У токена NATIVE контракта нет, и его address — не адрес контракта: монету сети определяйте по type, а не по address
type NATIVE (собственная монета сети), ERC20 (контракты EVM и TRON), SPL (Solana), JETTON (TON)
category NATIVE, HEDGE (опорный стейблкоин сети, не больше одного на сеть), ASSET (всё остальное)
maxAmountOut Фиксированный номинальный потолок в 100 000 целых токенов, в базовых единицах. За ликвидностью он не следует: сколько реально можно выплатить, смотрите в maxAmountOut курса
price Доллары США за целый токен, умноженные на 10^18
srcStatus Можно ли платить в этом токене. На ENABLED можно создавать новые инвойсы, на DISABLED — нет
dstStatus Выплачиваем ли мы сейчас этот токен как результат обмена. Ничего из того, что вы делаете через этот API, от него не зависит, в том числе выводы
status Устарело. Всегда равно srcStatus. Читайте srcStatus
monitoringCode Внутренняя метка, для мерчанта смысла не имеет. Может быть null

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

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

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

Создать инвойс в токене, у которого srcStatus равен DISABLED, не выйдет: ответ — 422 TOKEN_IS_DISABLED. Отключение останавливает только новые инвойсы: платёж, который приходит в этом токене позже — на инвойс, созданный раньше, или на статический кошелёк, — всё равно записывается и свипается.

У токена два статуса, и status — только один из них

status появился раньше разделения и теперь повторяет srcStatus, так что код, который его читает, для платежей продолжает работать. Не читайте его как «токен полностью работает»: токен может быть открыт для оплаты и закрыт для выплаты или наоборот. Новое стройте на srcStatus.

Курсы

curl "https://api.pay.nullswap.com/api/v1/tokens/rate?srcTokenId=3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908&dstTokenId=8c1f2e3d-4a5b-4c6d-8e70-9f0a1b2c3d4e&amountIn=250000000" \
  -H "x-api-key: sk_live_..."
Параметр Тип Обязателен Примечания
srcTokenId uuid да Продаваемый токен
dstTokenId uuid да Покупаемый токен
amountIn string нет Котировать ровно столько srcTokenId. Базовые единицы, положительное целое. Не вместе с amountOut
amountOut string нет Котировать то, что выплачивает ровно столько dstTokenId. Базовые единицы, положительное целое. Не вместе с amountIn
{
  "srcToken": { "id": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908", "…": "полный объект токена" },
  "dstToken": { "id": "8c1f2e3d-4a5b-4c6d-8e70-9f0a1b2c3d4e", "…": "полный объект токена" },
  "rate": "396560000000000",
  "minAmountIn": "10000000",
  "maxAmountIn": "4180000000",
  "maxAmountOut": "1608910000000000000",
  "amountIn": "250000000",
  "amountOut": "99140000000000000",
  "hedgeAmountIn": "248747000"
}

Передайте сумму — и получите котировку ровно на неё: заполнены и amountIn, и amountOut, а amountOut — это то, что выплачивает эта сумма за вычетом всех комиссий: платформы и, для пары с конвертацией через пул ликвидности, комиссии пула и влияния вашей суммы на его цену. Вычитать что-либо самому не нужно. Если вместо этого передан amountOut, вернувшийся amountIn округлён вверх, так что он выплачивает не меньше запрошенного amountOut.

Поле Примечания
rate Сколько единиц целевого токена даёт одна единица исходного, умноженное на 10^18. С котировкой — курс, который фактически получает ваша сумма. Без суммы — предельный курс: лучший, какой получает любая сумма, без влияния на цену
minAmountIn Наименьший вход, который мы котируем, в базовых единицах srcTokenId: фиксированный минимум в USD по текущей цене токена
maxAmountIn Наибольший вход, который мы котируем: сколько стоит maxAmountOut по курсу пары
maxAmountOut Наибольшая выплата, которую мы котируем. Наименьший из трёх лимитов: фиксированный максимум в USD на один своп; для пары с конвертацией через пул ликвидности — то, что пул выплачивает при влиянии на цену не больше 3% (собственная комиссия пула не в счёт); и сколько dstTokenId мы можем выплатить в его сети прямо сейчас. "0", если выплатить нельзя ничего
amountIn, amountOut Котировка. Оба null, если сумма не передана, и если сумма вне лимитов
hedgeAmountIn Сколько базовых единиц HEDGE-токена сети назначения стоит покупка ровно amountOut. null без котировки, а также если dstTokenId сам HEDGE-токен или находится в сети без него. Для работы с котировкой он не нужен

rate — это amountOut / amountIn с поправкой на decimals, так что

amountOut ≈ amountIn × rate × 10^(dstDecimals − srcDecimals) / 10^18

воспроизводит котировку с точностью до округления. Если amountOut у вас есть, берите его.

Без суммы rate — лучший случай

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

Сумма вне лимитов — всё равно 200

Сумму меньше minAmountIn или больше maxAmountIn (для amountOut — такую, которой нужен такой вход) мы не котируем. Ответ — обычный 200 с amountIn и amountOut, равными null; rate становится предельным, а лимиты показывают, какую границу вы пересекли. Проверяйте amountOut на null, прежде чем его показывать.

maxAmountIn и maxAmountOut следуют за ликвидностью, доступной для пары, и за тем, сколько dstTokenId мы можем выплатить в данный момент, поэтому движутся вместе с рынком и для малоликвидного токена могут быть намного ниже максимума в USD. Берите их из того же ответа, по которому котируете, а не из более раннего.

Нулевые лимиты значат, что сейчас ничего нельзя выплатить, а не что пары нет

Когда мы сейчас не можем выплатить dstTokenId в его сети, maxAmountIn и maxAmountOut оба равны "0", minAmountIn больше maxAmountIn, и ни одна сумма не котируется — по-прежнему с 200, а не 404. Обычно это проходит; запросите позже, а не убирайте пару из селектора.

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

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

Статус message Причина
400 INVALID_QUERY_PARAMETER srcTokenId или dstTokenId не передан или не UUID, сумма — не положительное целое в базовых единицах, или переданы и amountIn, и amountOut
401 INVALID_API_KEY x-api-key отсутствует, искажён, неизвестен или отозван
403 CALLER_IP_NOT_ALLOWED Ключ верный, но адреса отправителя нет в его белом списке
403 MERCHANT_BLOCKED Мерчант заблокирован
404 TOKENS_RATE_NOT_FOUND Пару сейчас нельзя оценить, см. ниже

TOKENS_RATE_NOT_FOUND означает, что пару нельзя оценить: одного из токенов нет или у него ещё нет цены, пара не поддерживается, или данные о её ликвидности временно недоступны. Последнее проходит само, так что пару, которая работала минуту назад, стоит запросить ещё раз. Формат ошибки описан в разделе Ошибки.

Курс не смотрит на srcStatus и dstStatus: пара с токеном в DISABLED всё равно котируется. Проверьте статусы токена, прежде чем её предлагать.

Кеширование

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

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