Сети и токены¶
У каждого инвойса, платежа, баланса и вывода есть chainId и tokenId. И то и другое — UUID
NullSwap, а не идентификаторы самой сети: маршрута, который принял бы вместо них "ethereum", 1
или адрес контракта, в API нет. Эти два эндпоинта и нужны, чтобы перевести то, что вы знаете, в
идентификаторы, которые от вас требуются.
Сети¶
{
"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 платежа оно показывает, на какой глубине
платёж находится прямо сейчас:
Считать глубину приходится самому: 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 у вас есть, берите его.
Без суммы 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, сначала обновите
список — и только потом считайте это ошибкой.