14 KiB
CryptoWallet API — Deployment Bundle
Multi-chain custodial wallet API (ETH / BSC / BTC / TRX / SOL).
- Сервер генерит mnemonic, хранит зашифрованной (AES-256-GCM, master-key из HashiCorp Vault)
- Сервер сам подписывает tx по запросу юзера (юзер на клиенте жмёт "подтвердить")
Auth — JWT, выданный сервисом bitok (внешний). Секреты — HashiCorp Vault (AppRole).
Pre-deploy setup (один раз)
# 1. Master-key в Vault
vault kv put dev-secrets/crypto/master key=$(openssl rand -hex 32)
# 2. CSRF secret в Vault
vault kv put dev-secrets/csrf secret_key=$(openssl rand -hex 32) salt=csrf-salt digest=sha256
# 3. DB schema — APPEND-ONLY / NON-DESTRUCTIVE
# Безопасно прогонять на existing БД. См. ниже "Schema is non-destructive".
psql -h <db-host> -U postgres_user -d postgres -f cryptowallet-schema.sql
# 4. bitok public key в Vault (для kid из JWT header)
vault kv put dev-secrets/jwt/kid active=<kid-from-bitok>
vault kv put dev-secrets/jwt/kids/<kid-from-bitok> \
algorithm=RS256 \
public_key="$(cat /path/to/bitok-public.pem)"
⚠️ Master-key менять нельзя — все existing encrypted_mnemonic станут нерасшифровываемыми. API на старте делает self-test: пытается декриптить любой существующий mnemonic и фейлится если ключ не подошёл.
Deploy
# Залить bundle на сервер
scp -P 2222 -r deployserver/ server@<host>:~/cryptowallet/
# На сервере: заполнить .env, поднять
ssh server@<host> -p 2222
cd ~/cryptowallet
cp .env.example .env
chmod 600 .env
nano .env # заполни VAULT_*, JWT_*, CORS_ORIGINS
./start.sh
В .env обязательны: VAULT_ADDR, VAULT_ROLE_ID, VAULT_SECRET_ID, JWT_ISSUER=bitok, JWT_AUDIENCE, CORS_ORIGINS.
Update / Rebuild
scp -P 2222 -r deployserver/apps server@<host>:~/cryptowallet/
ssh server@<host> -p 2222 'cd cryptowallet && docker compose up -d --build'
Endpoints
Core wallet management
| Method | Path | Описание |
|---|---|---|
| GET | /api/health |
Liveness (public) |
| GET | /api/docs |
Swagger UI |
| GET | /api/docs/swagger.json |
OpenAPI JSON |
| POST | /api/wallets/create |
Сервер создаёт коша (no body, returns 5 addresses) |
| GET | /api/wallets |
Список адресов юзера |
| GET | /api/wallets/portfolio |
Аггрегированный USD portfolio по всем 5 chains (KeyDB cached 1h, stale fallback) |
| POST | /api/wallets/mnemonic/reveal |
Reveal seed (body confirm + 5/час rate-limit) |
| GET | /api/wallets/{chain}/balance |
Баланс (native + все известные токены чейна) |
| GET | /api/wallets/{chain}/transactions |
История tx |
| POST | /api/wallets/{chain}/send |
Сервер подписывает + broadcast. Body: {to, amount, token?, feeTier?} |
| POST | /api/wallets/{chain}/send/cost-estimate |
Pre-flight gas/fee estimate (no broadcast) |
| GET | /api/wallets/{chain}/gas-suggestions |
Slow/normal/fast tiers (ETH/BSC, parsed из eth_feeHistory) |
| POST | /api/wallets/{chain}/sign-raw-evm-tx |
Подписать произвольную EVM tx (для Relay execute steps). Поддерживает opt-in bridgeAmount+bridgeToken → автоматически взимает 0.7% app fee на EVM fee wallet ПЕРЕД main tx (atomic). |
| POST | /api/wallets/SOL/sign-and-broadcast-tx |
Sign+broadcast serialized VersionedTransaction (Jupiter/Relay SOL steps) |
Bridge & Swap (NEW)
| Method | Path | Описание |
|---|---|---|
| POST | /api/bridge/execute |
One-click bridge orchestrator. Auto-routes: TRX source → NearIntents 1Click; EVM/SOL source → Jumper (LiFi) или Relay. Atomic fee tx ДО main bridge. Idempotency-Key support. Returns {feeTxid?, approveTxid?, bridgeTxid, trackerUrl, provider}. |
| GET | /api/jumper/quote-best |
Bridge quote с NearIntents priority + smart LiFi fallback. JWT-binds fromAddress. |
| GET | /api/jumper/{chains,tools,tokens,connections,status} |
LiFi metadata proxies (JWT-protected) |
| POST | /api/jumper/advanced/{routes,stepTransaction} |
LiFi multi-route preview / step tx fetcher |
| POST | /api/wallets/{chain}/swap/quote |
Custodial swap quote (BSC PancakeSwap / SOL Jupiter / TRX SunSwap) — 30s locked minOut |
| POST | /api/wallets/{chain}/swap |
Confirm custodial swap (BSC/SOL/TRX). BSC + SOL: 0.7% atomic fee ДО main swap. TRX: existing on-chain FeeSwapRouter. |
| POST | /api/wallets/{chain}/swap/cost-estimate |
Swap pre-flight cost estimate |
| — | /api/relay/* |
Relay.link bridge proxy (quote / execute / status / cost-estimate). Unchanged from previous deploys. |
App fee 0.7% (NEW — hardcoded recipients)
| Method | Path | Описание |
|---|---|---|
| POST | /api/wallets/{chain}/app-fee |
Standalone fee transfer endpoint. Для Relay frontend hook — клиент явно вызывает после Relay execute. Body: {amount, token?}. Server validates JWT-bind + computes 0.7% + signs + broadcasts. Idempotency-Key support. |
Fee wallets (захардкожены в lib/app-fee.ts, нельзя override через env):
| Chain | Recipient address |
|---|---|
| EVM (ETH+BSC) | 0xeb9fbf0d137ef5ea7b9959044c2ed44ec1206c68 |
| SOL | DQkQegoX698XkcXZ6VX9P1qUpbV64Sgjz1BCPFgfWpjD |
| TRX | TRwpFjnfMBe4aDJbHYEqeUVCG1auF8wFXP |
| BTC | (not collectable — no fee wallet) |
Fee рассчёт: feeAmount = amount × 70 / 10000 (BigInt-precision, 0 если < 144 smallest units).
Tokens & Prices (NEW)
| Method | Path | Описание |
|---|---|---|
| GET | /api/tokens ?chain={ETH,BSC,BTC,TRX,SOL} ?bridgeable=true |
Token registry. bridgeable=true фильтрует только tokens с реальным bridge route (используется Jumper UI dropdowns) |
| GET | /api/prices ?symbols=BTC,ETH,USDT ?chain={...} |
CoinGecko cached USD prices |
| GET | /api/prices/dynamics ?symbols=... |
24h price change % для нескольких символов одним запросом |
Provider integrations (NEW)
- NearIntents 1Click (
https://1click.chaindefuser.com) — direct TRX bridges. Asset map fetched dynamically через/v0/tokens(cached 1h in-memory). Quote → user transfers TRX/USDT to depositAddress → solver delivers cross-chain. No JWT для test env (0.2% NearIntents fee included в quote). Implementation:src/lib/nearintents-client.ts. - Jumper / LiFi (
https://li.quest/v1) — multi-chain bridge proxy + quote-best smart routing с NearIntents priority. Implementation:src/routes/jumper-proxy.routes.ts. - Relay.link (
https://api.relay.link) — EVM/SOL/BTC bridge proxy. Unchanged (preservation invariant). Implementation:src/routes/relay-proxy.routes.ts.
Audit log events (extended)
wallet.create,wallet.send,wallet.swap(existing)wallet.sign_raw_evm(existing, +bridgeAmountmeta)wallet.bsc_fee(existing) /wallet.app_fee(NEW — standalone fee endpoint)bridge.execute,bridge.execute.broadcast(NEW — bridge orchestrator)
Security highlights
- AES-256-GCM для encrypted_mnemonic (12-byte random IV, 16-byte auth tag, fail-secure)
- Master-key set-once (rotation запрещена в коде)
- Crypto self-test на старте — fail-fast если master-key не декриптит existing mnemonics
- Race-safe createWallet —
db.transaction+UPDATE WHERE encrypted_mnemonic IS NULL(set-once primitive) - Atomic erc20 update — ETH-адрес кладётся в
users.erc20внутри той же транзакции - TRX MITM defense — local recompute txID + 4-layer raw_data verification перед подписью
- EVM gas cap 500 gwei (применён к tx, не только check)
- EVM gas oracle через
eth_feeHistoryp25/p50/p75 — minimum-but-works fees (BSC floor 0.05, ETH 0.5 gwei) - BTC fee tier-based (slow=144 blocks, normal=6, fast=1) + floor 2 sat/vB
- TRX fee_limit cap 30 TRX (раньше 100, излишне)
- Address checksum validation (BTC bitcoinjs-lib, TRX bs58check, SOL PublicKey, EVM EIP-55)
- assertAddressMatch — derived(mnemonic, path) === DB.address перед подписью
- SOL confirmTransaction — ждём подтверждения сети
- BTC P2WPKH bech32, dust 294, broadcast 20s timeout
- POST mnemonic/reveal + CSRF + body confirm token + 5/час rate-limit + audit-log
- Logger sanitization — password/token/mnemonic/hex64/BIP39-phrase patterns маскируются
- Audit log в stdout (структурированный JSON с
"level":"audit") —wallet.create/wallet.send/wallet.swap/wallet.sign_raw_evm/wallet.bsc_fee/wallet.app_fee/mnemonic.reveal/bridge.execute/bridge.execute.broadcast - Hourly key rotation — JWT keys + CSRF secret из Vault (master-key НЕ ротируется)
- Fail-fast — сервис не стартует без master-key, JWT_ISSUER, JWT_AUDIENCE
- Container hardening — read-only fs, cap_drop ALL, no-new-privileges, pids/mem/cpu limits, non-root uid 1001, loopback-only port
- Relay proxy whitelist method+path —
/quote(POST),/intents/status/v3(GET),/execute/{swap|bridge}(POST). Никаких freeform action'ов - Jumper proxy whitelist аналогично —
/quote,/quote-best,/chains,/tools,/tokens,/connections,/status,/advanced/{routes,stepTransaction}. JWT-bindsfromAddressк user wallet. - NearIntents direct integration для TRX bridges —
signAndBroadcast(TRX)через battle-tested sendTrx (4-layer MITM defense сохраняется). Asset map fetched через/v0/tokens(auth source) и cached 1h в-memory. depositAddress validated через Tron base58 regex^T[1-9A-HJ-NP-Za-km-z]{33}$. - App fee 0.7% atomic enforcement в bridge-execute orchestrator + custodial swap orchestrator. Fee tx FIRST → если fail, main tx НЕ broadcast (atomic abort). Recipients hardcoded — нельзя override через body/env.
- BridgeSimulationError для EVM bridges — pre-broadcast
eth_calldry-run; если revert → 400SIMULATION_FAILED, fees не сгорают. - InsufficientBalanceError pre-check на bridge — отвергаем 400 ДО подписи если balance < amount + reserve.
- Anti-MEV в bridge-execute — server re-quotes ПЕРЕД sign, валидирует
minAmountOut ≥ acceptedMinOut(50 bps cushion). Если price moved → 409. - Idempotency-Key на mutating endpoints (send / sign-raw / swap / bridge-execute / app-fee) — KeyDB cache 10 min, retry-safe.
- TronGrid 429 retry с exponential backoff (0/1.5/4/8s) для free-tier rate limits.
Schema is non-destructive
cryptowallet-schema.sql append-only. Re-run на боксе с уже настроенной БД = zero DDL changes. Если оператор добавил кастомные таблицы / индексы / constraints вручную — они никогда не будут перезаписаны или удалены.
Что делает script:
CREATE TABLE IF NOT EXISTS users/walletsALTER TABLE users ADD COLUMN <X>(только если колонки нет —encrypted_mnemonic,erc20,passport_data)CREATE UNIQUE INDEX users_email_lower_unique(если индекса нет)CREATE INDEX idx_users_active/idx_wallets_*(если индексов нет)ADD CONSTRAINT× 4 (только если данного constraint name нет)
Что script НЕ делает:
- ❌ Никогда не
DROP TABLE - ❌ Никогда не
DROP CONSTRAINT - ❌ Никогда не
DROP COLUMN - ❌ Никогда не перезаписывает существующие constraints / indexes
Legacy cleanup (audit_log, idempotency_keys, sessions от старых версий) — manual one-time операторская задача, не часть этого script'а:
psql ... -c "DROP TABLE IF EXISTS audit_log CASCADE;"
psql ... -c "DROP TABLE IF EXISTS idempotency_keys CASCADE;"
psql ... -c "DROP TABLE IF EXISTS sessions CASCADE;"
Logs
Файловых логов нет. Весь код пишет в process.stdout (см. apps/api/src/lib/logger.ts и lib/audit-log.ts). Docker подбирает stdout через json-file driver и показывает через docker compose logs:
docker compose logs -f api # все логи (structured JSON)
docker compose logs api | grep '"level":"audit"' # только audit events
docker compose logs api | grep '"level":"ERROR"' # только ошибки
Production hardening checklist (опционально)
- Vault server-mode (raft/file backend) с unseal flow
- TLS termination на reverse-proxy (Caddy / Nginx) перед
127.0.0.1:3001 - Swagger UI скрыть за basic-auth (endpoints всё ещё доступны через
/api/docs/swagger.json) - Postgres backups (pg_dump → S3 по cron)
- Vault root token ротация
- Mnemonic-reveal endpoint — 2FA / time-based confirmation tokens
- Rate-limit tune под реальный трафик