initjirefr

This commit is contained in:
ZOMBIIIIIII
2026-05-28 23:29:18 +03:00
parent 4c00c6ca1b
commit 31aba0b681
10 changed files with 393 additions and 104 deletions

142
README.md
View File

@@ -6,10 +6,12 @@ Multi-chain **custodial** wallet API (ETH / BSC / BTC / TRX / SOL).
Auth — JWT, выданный сервисом **bitok** (внешний). Секреты — HashiCorp Vault (AppRole).
## Pre-deploy setup (один раз)
## Pre-deploy setup (только greenfield / первый раз)
**На уже работающем production НЕ выполнять** `vault kv put` для crypto/csrf/jwt и **НЕ** пересоздавать KeyDB/Postgres volumes.
```bash
# 1. Master-key в Vault
# 1. Master-key в Vault (ТОЛЬКО если ключа ещё нет — иначе все mnemonic станут мёртвыми)
vault kv put dev-secrets/crypto/master key=$(openssl rand -hex 32)
# 2. CSRF secret в Vault
@@ -28,38 +30,69 @@ vault kv put dev-secrets/jwt/kids/<kid-from-bitok> \
⚠️ **Master-key менять нельзя** — все existing `encrypted_mnemonic` станут нерасшифровываемыми. API на старте делает self-test: пытается декриптить любой существующий mnemonic и фейлится если ключ не подошёл.
## Deploy
## Bundle contents
Папка `deployserver/`**только исходники** (`apps/api/src`, `package.json`, `pnpm-lock.yaml`) и Docker-конфиги.
**Не должно быть** `node_modules/`, `dist/`, `.turbo/` — ни в git, ни при `scp` на сервер.
Локальная сборка bundle из монорепы:
```bash
# Залить bundle на сервер
scp -P 2222 -r deployserver/ server@<host>:~/cryptowallet/
# На сервере: убедись что .env заполнен (VAULT_*, JWT_*, CORS_ORIGINS, REDIS_PASSWORD, ...)
ssh server@<host> -p 2222
cd ~/cryptowallet/deployserver
cp .env.example .env
nano .env # VAULT_ADDR / VAULT_ROLE_ID / VAULT_SECRET_ID НЕ должны быть пустыми
docker compose up -d --build
docker compose logs -f api
curl http://localhost:3001/api/health
node scripts/sync-deployserver.mjs
```
API **не делает migrations / DROP / ALTER** при старте — только INSERT/UPDATE/SELECT. Schema (если нужны новые колонки/таблицы для нового функционала) обновляется только руками: `psql -f cryptowallet-schema.sql` (script append-only — `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE ADD COLUMN IF NOT EXISTS`, никаких DROP).
Если в логах есть `Vault not configured, using .env` и затем `Initial Vault refresh failed: vault_not_configured`, значит контейнер получил пустые `VAULT_ADDR`, `VAULT_ROLE_ID` или `VAULT_SECRET_ID`. Это не nginx-проблема: API падает на старте, пока AppRole не заполнен.
## Update / Rebuild
Проверка/сборка образа — **только через Docker** (на сервере или локально):
```bash
# Залить новый src + rebuild api (keydb данные не теряются)
cd deployserver && docker compose build api
```
Не запускайте `pnpm install` внутри `deployserver/` — зависимости ставятся в multi-stage Dockerfile.
## Deploy (первый раз на пустом сервере)
```bash
# Только код + Docker-файлы. НЕ заливать .env с локальной машины поверх серверного.
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'
deployserver/package.json deployserver/pnpm-lock.yaml deployserver/pnpm-workspace.yaml \
deployserver/start.sh deployserver/.env.example \
server@<host>:~/cryptowallet/
ssh server@<host> -p 2222
cd ~/cryptowallet
# .env только если файла ещё нет:
test -f .env || cp .env.example .env
chmod 600 .env
nano .env # заполни VAULT_*, JWT_*, CORS_ORIGINS, REDIS_PASSWORD
./start.sh
```
В `.env` **обязательны**: `VAULT_ADDR`, `VAULT_ROLE_ID`, `VAULT_SECRET_ID`, `JWT_ISSUER=bitok`, `JWT_AUDIENCE`, `CORS_ORIGINS`, `REDIS_PASSWORD`.
## Update / Rebuild (production — без потери данных)
**Разрешено:** пересобрать только контейнер `api` (образ из нового кода).
**Запрещено при обычном апдейте:**
- `docker compose down -v` (снесёт volume KeyDB `keydb_data`)
- `vault kv put` / patch crypto master, csrf secret, jwt keys
- `scp`/`rsync` всего `deployserver/` поверх `~/cryptowallet/` (может затереть `.env`)
- `--force-recreate` для `keydb`
- повторный `psql -f cryptowallet-schema.sql` без необходимости (только если осознанно нужны новые колонки)
```bash
# С локальной машины — только apps + lockfile + Dockerfile (не .env)
node scripts/sync-deployserver.mjs
scp -P 2222 -r deployserver/apps deployserver/Dockerfile deployserver/docker-compose.yml \
deployserver/package.json deployserver/pnpm-lock.yaml deployserver/pnpm-workspace.yaml \
server@<host>:~/cryptowallet/
# На сервере — только API, KeyDB volume не трогаем
ssh server@<host> -p 2222 'cd ~/cryptowallet && docker compose build api && docker compose up -d api'
```
## Endpoints
### Core wallet management
| Method | Path | Описание |
|---|---|---|
| GET | `/api/health` | Liveness (public) |
@@ -67,61 +100,13 @@ ssh server@<host> -p 2222 'cd cryptowallet/deployserver && docker compose up -d
| 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)
| POST | **`/api/wallets/{chain}/sign-raw-evm-tx`** | Подписать произвольную EVM tx (для Relay/Swap execute responses) |
| | `/api/btc/*` `/api/tron/*` `/api/sol/*` `/api/bsc/*` `/api/relay/*` | Proxy endpoints (swap quote/build, bridge quote/execute/status) |
## Security highlights
@@ -141,19 +126,12 @@ Fee рассчёт: `feeAmount = amount × 70 / 10000` (BigInt-precision, 0 ес
- **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`
- **Audit log в stdout** (структурированный JSON с `"level":"audit"`) — `wallet.create` / `wallet.send` / `mnemonic.reveal` / `wallet.sign_raw_evm`
- **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.
- **Bridge UI dropdown** ETH/BSC/SOL only (TRX через Relay убран — плохая ликвидность)
## Schema is non-destructive