Files
cryptowallet/README.md
ZOMBIIIIIII a636bd573a init2121212
2026-05-28 14:01:10 +03:00

223 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (один раз)
```bash
# 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 (через docker compose)
```bash
# 1. Залить bundle на сервер
scp -P 2222 -r deployserver/ server@<host>:~/cryptowallet/
# 2. На сервере: заполнить .env
ssh server@<host> -p 2222
cd ~/cryptowallet/deployserver
cp .env.example .env
chmod 600 .env
nano .env # заполни VAULT_*, JWT_*, REDIS_PASSWORD, POSTGRES_PASSWORD, CORS_ORIGINS
# 3. Поднять stack (api + postgres + keydb)
docker compose up -d --build
# 4. Проверить
docker compose ps
docker compose logs -f api
curl http://localhost:3001/api/health
```
В `.env` **обязательны**: `VAULT_ADDR`, `VAULT_ROLE_ID`, `VAULT_SECRET_ID`, `JWT_ISSUER=bitok`, `JWT_AUDIENCE`, `CORS_ORIGINS`, `POSTGRES_PASSWORD`, `REDIS_PASSWORD`.
**Что в compose:**
- `api` — наш Node API из multi-stage Dockerfile (read-only fs, uid 1001, port 127.0.0.1:3001)
- `postgres` — хранилище (internal only, без exposed ports)
- `keydb` — Redis-compatible для idempotency + asset map cache (internal only)
**Чего НЕТ в compose** (production не использует — оператор настраивает отдельно):
- HashiCorp Vault — production использует HA cluster (URL в `VAULT_ADDR`)
- JWT issuer — production принимает JWT от bitok external service
- Web UI — frontend деплоится отдельно (Vercel / nginx / CDN)
- Nginx reverse proxy + TLS — оператор сам ставит перед `127.0.0.1:3001`
**Если используешь managed Postgres** (e.g. AWS RDS):
- Раскомментируй `DATABASE_URL` в `.env`
- Закомментируй `postgres` service в `docker-compose.yml` + убери `depends_on.postgres` из `api`
## Update / Rebuild
```bash
# Только app (postgres/keydb остаются — данные не теряются)
scp -P 2222 -r deployserver/apps deployserver/Dockerfile deployserver/docker-compose.yml \
server@<host>:~/cryptowallet/deployserver/
ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build api'
# Полный rebuild
ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d --build --force-recreate'
```
## 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, +`bridgeAmount` meta)
- `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_feeHistory` p25/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-binds `fromAddress` к 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_call` dry-run; если revert → 400 `SIMULATION_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` / `wallets`
- `ALTER 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'а:
```bash
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`:
```bash
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 под реальный трафик