Files
frontend/CryptoWallet-DeFi-Guide.md
2026-06-17 17:54:30 +03:00

19 KiB
Raw Blame History

CryptoWallet DeFi API — гайд для фронтендера

Сектор DeFi: Стейкинг · Пулы (Uniswap v3 LP) · Отвязка кошельков (approvals)

Базовый URL: https://app.cryptowallet.elcsa.ru/api


Общие правила (читать один раз)

Авторизация: все запросы требуют Authorization: Bearer <JWT>. CSRF (для всех POST): double-submit —

  • cookie csrf_token=<value> (сервер выдаёт)
  • header X-CSRF-Token: <тот же value>

Idempotency-Key (для мутирующих POST: stake/unstake/claim/lp add/remove/revoke/revoke-all): заголовок Idempotency-Key: <uuid>. Сгенерь UUID на фронте; при retry шли тот же — сервер вернёт кэшированный ответ, не выполнит операцию дважды (анти-дабл-спенд).

Конверт ответа:

  • успех → { "success": true, "data": { ... } }
  • ошибка → { "success": false, "error": "текст", "code": "OPTIONAL_CODE" }

Кастодиальность: ключи на сервере. Все транзакции подписывает и броадкастит сервер мнемоникой пользователя — фронту не нужно ничего подписывать. Достаточно вызвать endpoint.

Числа — два формата. Почти у каждого «сырого» поля сумм есть человекочитаемый спутник:

  • amount0Desired (smallest units, строка) + amount0DesiredHuman (обычное число, строка).
  • Для UI бери *Human. Для повторной отправки на сервер — сырое поле.

Газ. Любая мутация тратит газ нативной монетой (ETH для ETH/LP, SOL для Solana). Если на кошельке нет нативки — операция упадёт. Проверить заранее можно через баланс в LP-квоте (см. 2.2) или POST /wallets/{chain}/op-cost (см. примечание в конце).

Комиссия сервиса: 0.7% (70 bps) на стейк и на LP-депозит. На вывод (unstake / lp remove / revoke) комиссии нет.


2. ПУЛЫ — Uniswap v3 LP (только ETH)

Курируемый список пулов (например WETH/USDT, WBTC/WETH, XAUt/USDT). Пользователь выбирает пул, диапазон цен и суммы — сервер минтит позицию (NFT). Путь всегда chain=ETH.


2.1. GET /wallets/ETH/lp/pools

Что делает: список курируемых пулов со статистикой (TVL, объём, APR) и текущей ценой.

Вход: нет тела.

Возвращает:

{ "success": true, "data": [
  {
    "poolAddress": "0x4e68ccd3e89f51c3074ca5072bbac773960dfa36",
    "token0": "0xC02a...", "token1": "0xdAC1...",
    "symbol0": "WETH", "symbol1": "USDT",
    "decimals0": 18, "decimals1": 6,
    "feeTier": 3000,
    "currentPrice": 1665.75,
    "currentTick": -202300,
    "tvlUSD": 33000000, "volumeUSD24h": 5400000, "aprPercent": 40.33
  }
]}

Примечание:

  • feeTier в сотых долях процента (3000 = 0.3%, 500 = 0.05%).
  • currentPrice = сколько token1 за 1 token0 (та же конвенция, что в quote ниже).
  • tvlUSD/volumeUSD24h/aprPercent могут быть null, если внешняя статистика недоступна (не блокирует депозит).

2.2. POST /wallets/ETH/lp/quote (с комиссией и проверкой баланса)

Что делает: превью депозита — по диапазону цен и желаемым суммам считает тики, фактические суммы, slippage-минимумы, APR, нашу комиссию 0.7% (в токенах + USD + ETH) и достаточность баланса (хватает ли token0/token1 на взнос И ETH на газ). Ничего не подписывает.

Вход (body):

{
  "pool": "0x4e68ccd3e89f51c3074ca5072bbac773960dfa36",
  "priceLower": 1568.75,
  "priceUpper": 1665.75,
  "amount0Desired": "100000000000000000",
  "amount1Desired": "263359",
  "slippageBps": 100,
  "useNativeEth": true
}
  • pool — адрес из /lp/pools.
  • priceLower / priceUpper — границы диапазона (token1 за token0).
  • amount0Desired / amount1Desired — желаемые суммы в smallest units (по decimals каждого токена).
  • slippageBps — опционально, 0..5000 (дефолт 100 = 1%).
  • useNativeEth — опционально. Если у пула есть нога WETH, а у юзера на балансе ETH (не WETH) — поставь true: сервер сам завернёт ETH→WETH. Влияет на проверку баланса (проверяет ETH вместо WETH).

Возвращает:

{ "success": true, "data": {
  "poolAddress": "0x4e68ccd3...",
  "tickLower": -202740, "tickUpper": -202140,
  "priceLower": 1568.75, "priceUpper": 1665.75,
  "amount0Desired": "99999999999872", "amount0DesiredHuman": "0.000099999999999872",
  "amount1Desired": "263359",          "amount1DesiredHuman": "0.263359",
  "amount0Min": "98999999999873",      "amount0MinHuman": "0.000098999999999873",
  "amount1Min": "260725",              "amount1MinHuman": "0.260725",
  "liquidity": "354877171283",
  "aprPercent": 40.33,

  "appFee0Human": "0.0007", "appFee1Human": "0.001843",
  "appFee0Usd": 1.20, "appFee1Usd": 1.84,
  "appFeeTotalUsd": 3.04,
  "appFeeTotalEth": "0.00182",

  "balance": {
    "token0": { "symbol": "WETH", "have": "0.5", "need": "0.0001", "sufficient": true,  "shortfall": "0" },
    "token1": { "symbol": "USDT", "have": "0",   "need": "0.263359", "sufficient": false, "shortfall": "0.263359" },
    "gas":    { "symbol": "ETH",  "have": "0.02", "needReserve": "0.001", "sufficient": true },
    "sufficient": false
  }
}}

Примечание:

  • Комиссия: appFee0Human/appFee1Human — 0.7% по каждой ноге в самом токене. appFeeTotalUsd — суммарно в долларах, appFeeTotalEth — суммарно в ETH (монета сети). USD/ETH могут быть null, если у токена нет цены в whitelist (например XAUt) — это не ошибка.
  • Баланс: показывай красным, если balance.sufficient === false. sufficient у ноги может быть null (не проверялось / нога покрыта нативным ETH при useNativeEth) — это не «не хватает».
  • balance присутствует только если у юзера есть ETH-кошелёк (он есть всегда у custodial-юзера).
  • Реальные суммы депозита = amount0DesiredHuman/amount1DesiredHuman (могут быть меньше введённых — ограничивает диапазон).

2.3. POST /wallets/ETH/lp/add

Что делает: депозит в пул. Списывает комиссию 0.7% по каждому токену → approve×2 → NFPM.mint(). При useNativeEth сначала заворачивает ETH→WETH. Возвращает tokenId позиции.

Вход: header Idempotency-Key: <uuid> + body тот же, что в quote (pool, priceLower, priceUpper, amount0Desired, amount1Desired, slippageBps?, useNativeEth?).

Возвращает:

{ "success": true, "data": {
  "feeTxids": ["0x...", "0x..."],
  "approveTxids": ["0x...", "0x..."],
  "mintTxid": "0x...",
  "wrapTxid": "0x... (если useNativeEth)",
  "tickLower": -202740, "tickUpper": -202140,
  "appFee0": "700000000000", "appFee0Human": "0.0007",
  "appFee1": "1843", "appFee1Human": "0.001843"
}}

Примечание: сервер делает pre-check баланса ОБОИХ токенов ДО любых транзакций — если не хватает второй ноги, операция падает с 400 до wrap/комиссии (ничего не теряется). Поэтому сначала всегда показывай quote с balance.sufficient.


2.4. POST /wallets/ETH/lp/remove

Что делает: выводит ликвидность из позиции (decreaseLiquidity + collect тела и накопленных комиссий). При percent=100 ещё и сжигает NFT (burn). Без комиссии.

Вход: header Idempotency-Key: <uuid> + body:

{ "tokenId": "123456", "percent": 100 }
  • tokenId — id позиции из /lp/positions.
  • percent — опционально, (0..100], дефолт 100.

Возвращает:

{ "success": true, "data": {
  "removeTxid": "0x...",
  "removedLiquidity": "354877171283",
  "burned": true
}}

Примечание: сервер проверяет, что tokenId принадлежит кошельку юзера (ownerOf), иначе 400.


2.5. GET /wallets/ETH/lp/positions

Что делает: читает LP-позиции юзера (NFT) в курируемых пулах прямо из блокчейна: диапазон, текущая стоимость по каждому токену, накопленные комиссии, в диапазоне ли цена.

Вход: нет тела.

Возвращает:

{ "success": true, "data": {
  "chain": "ETH",
  "positions": [
    {
      "tokenId": "123456",
      "poolAddress": "0x4e68...", "symbol0": "WETH", "symbol1": "USDT", "feeTier": 3000,
      "tickLower": -202740, "tickUpper": -202140,
      "priceLower": 1568.75, "priceUpper": 1665.75,
      "liquidity": "354877171283",
      "currentAmount0": "99000000000000", "currentAmount0Human": "0.000099",
      "currentAmount1": "260000",          "currentAmount1Human": "0.26",
      "feesOwed0": "1200000000",           "feesOwed0Human": "0.0000000012",
      "feesOwed1": "500",                  "feesOwed1Human": "0.0005",
      "inRange": true
    }
  ],
  "totalNfts": 3, "scanned": 3, "truncated": false
}}

Примечание:

  • inRange:false → цена вышла за диапазон, позиция временно не зарабатывает комиссии (покажи ⚠️).
  • feesOwed* — накопленные комиссии, которые заберутся при remove.
  • truncated:trueу юзера слишком много NFT, показаны не все (на практике редко).

3. ОТВЯЗКА КОШЕЛЬКОВ (Approvals) — ETH / BSC

Назначение: ERC20-approval — это «разрешение» контракту тратить токен. Повисший unlimited approval (MAX) на роутер/солвер позволяет внешней стороне выводить будущие поступления токена → деньги «утекают». Этот блок даёт увидеть открытые двери и закрыть их. chain = ETH или BSC.


3.1. GET /wallets/{chain}/approvals

Что делает: сканирует кошелёк по известным спендерам (Relay-роутеры, Permit2, Uniswap NFPM) × известным токенам и возвращает только ненулевые allowance (открытые двери) с флагом unlimited. Read-only, ничего не подписывает.

Вход: только chain в пути.

Возвращает:

{ "success": true, "data": {
  "chain": "ETH",
  "owner": "0x9dB8...21ea9",
  "approvals": [
    {
      "token": "USDT", "tokenAddress": "0xdAC1...",
      "spender": "Relay Router v2", "spenderAddress": "0xb92f...",
      "allowance": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
      "allowanceHuman": "unlimited",
      "unlimited": true
    }
  ]
}}

Примечание:

  • Пустой approvals: [] → все двери закрыты .
  • unlimited:trueallowanceHuman:"unlimited") — главный риск, выделяй красным. Конечный allowance (например ровно на сумму свопа) низкорисковый.

3.2. POST /wallets/{chain}/approvals/revoke

Что делает: отзывает ОДНУ конкретную дверь — сервер подписывает token.approve(spender, 0). Сумма всегда 0 (можно только отозвать, не выдать).

Вход: header Idempotency-Key: <uuid> + body:

{
  "token":   "0xdAC17F958D2ee523a2206206994597C13D831ec7",
  "spender": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f"
}
  • token — адрес ERC20 (поле tokenAddress из скана).
  • spender — адрес спендера (поле spenderAddress из скана).

Возвращает:

{ "success": true, "data": {
  "chain": "ETH", "token": "0xdAC1...", "spender": "0xb92f...",
  "txid": "0x...", "previousAllowance": "1157920..."
}}

либо, если уже было 0:

{ "success": true, "data": { "txid": null, "alreadyZero": true,
  "message": "Allowance already 0 — nothing to revoke" } }

Примечание: если allowance уже 0 — газ не тратится (alreadyZero:true, txid:null). Иначе нужен небольшой газ (нативка) на одну транзакцию.


3.3. POST /wallets/{chain}/approvals/revoke-all «отвязать от всего»

Что делает: одним вызовом закрывает ВСЕ открытые двери: сервер сам сканирует ненулевые allowance и последовательно подписывает approve(spender, 0) по каждой.

Вход: header Idempotency-Key: <uuid> + body (опционально):

{ "onlyUnlimited": false }
  • onlyUnlimitedfalse (дефолт) = отозвать ВСЕ ненулевые; true = только unlimited/oversized.
  • Тело можно не передавать ({}) — эквивалент onlyUnlimited:false.

Возвращает:

{ "success": true, "data": {
  "chain": "ETH", "owner": "0x9dB8...",
  "total": 2,
  "revoked": [
    { "token": "USDT", "spender": "Relay Router v2",
      "tokenAddress": "0xdAC1...", "spenderAddress": "0xb92f...",
      "txid": "0x...", "previousAllowance": "1157920..." }
  ],
  "failed":  [],
  "skipped": []
}}

Если открытых дверей нет:

{ "success": true, "data": { "total": 0, "revoked": [], "failed": [], "skipped": [],
  "message": "no open approvals — nothing to revoke" } }

Примечание:

  • Выполняется последовательно (по 1 транзакции на дверь, ждёт подтверждения каждой) — может занять минуту-две. Покажи лоадер.
  • При нехватке газа: отзывает сколько успел → остальное помечает skipped («пополни нативку и повтори»). Повторный вызов безопасен (уже закрытые двери скан не покажет, газ не тратится).
  • total — сколько открытых дверей нашлось; разбивка по revoked / failed / skipped.

4. Типичные флоу (end-to-end)

Стейк ETH:

  1. POST /wallets/ETH/stake/quote → показать комиссию + ожидаемый stETH + APR.
  2. По «Подтвердить» → POST /wallets/ETH/stake (с Idempotency-Key).
  3. GET /wallets/ETH/stake/positions → обновить баланс stETH.

LP-депозит:

  1. GET /wallets/ETH/lp/pools → таблица пулов (APR/TVL).
  2. Юзер выбрал пул, ввёл диапазон + суммы → POST /wallets/ETH/lp/quote → показать комиссию (appFeeTotal*) и баннер при balance.sufficient === false.
  3. Если хватает → POST /wallets/ETH/lp/add (с Idempotency-Key).
  4. GET /wallets/ETH/lp/positions → список позиций; remove по кнопке.

Защита от слива (отвязка):

  1. GET /wallets/ETH/approvals (и/или BSC) → если есть unlimited — предупредить.
  2. POST /wallets/ETH/approvals/revoke-all → закрыть все двери одной кнопкой.
  3. Повторный GET /approvals → список пуст .

5. Коды ошибок

HTTP code Что значит / что делать
401 Нет/невалидный JWT.
403 JWT валиден, но нет кошелька для chain, либо CSRF не прошёл.
400 Невалидный body (chain вне whitelist, amount не число, requestId чужой, token/spender не адрес и т.п.).
400 INSUFFICIENT_BALANCE Не хватает токена/нативки (LP add при нехватке второй ноги — падает до списаний).
404 Нет кошелька / mnemonic не найден / endpoint вне whitelist.
409 Конфликт Idempotency-Key (тот же ключ с другим телом).
429 Rate limit.
502 Upstream (RPC / внешний API) или broadcast не прошёл.
503 Сервис аудита недоступен (мутация не выполнена — безопасно повторить).

Примечание: вспомогательный endpoint стоимости (опционально)

Если нужно показать комиссию 0.7% (в токене + native + USD) и проверку баланса до стейка/свопа в едином виде — есть read-only POST /wallets/{chain}/op-cost (ETH/BSC/SOL/TRX):

Вход: { "token": "USDT", "amountHuman": "10" } (или amount в smallest units; пусто/нативный символ = нативная монета). Возвращает: appCommission { inToken, inNative, usd } + balance { token, gas } + sufficient (всё обычными числами). В LP-секторе эта информация уже встроена в /lp/quote (см. 2.2).