feat: DELETE THIS
This commit is contained in:
@@ -6,8 +6,7 @@ import swaggerUi from 'swagger-ui-express';
|
|||||||
import { env } from './config/env';
|
import { env } from './config/env';
|
||||||
import { swaggerSpec } from './config/swagger';
|
import { swaggerSpec } from './config/swagger';
|
||||||
import { traceMiddleware } from './middleware/trace';
|
import { traceMiddleware } from './middleware/trace';
|
||||||
import { authMiddleware } from './middleware/auth';
|
import { fixedUserMiddleware } from './middleware/fixed-user';
|
||||||
import { csrfMiddleware } from './middleware/csrf';
|
|
||||||
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
|
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
|
||||||
import { errorHandler } from './middleware/error-handler';
|
import { errorHandler } from './middleware/error-handler';
|
||||||
import { WalletController } from './controllers/wallet.controller';
|
import { WalletController } from './controllers/wallet.controller';
|
||||||
@@ -82,24 +81,21 @@ app.get('/api/docs/swagger.json', docsGate, (_req, res) => {
|
|||||||
});
|
});
|
||||||
app.use('/api/docs', docsGate, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
app.use('/api/docs', docsGate, swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||||
|
|
||||||
// ── PROTECTED endpoints (JWT + CSRF) ─────────────────────────────────────────
|
const identify = [fixedUserMiddleware];
|
||||||
const protect = [authMiddleware, csrfMiddleware];
|
|
||||||
|
|
||||||
app.post('/api/wallets/create', sensitiveLimiter, WalletController.createWallet);
|
app.post('/api/wallets/create', sensitiveLimiter, WalletController.createWallet);
|
||||||
app.use('/api/wallets/mnemonic/reveal', ...protect, mnemonicRevealLimiter);
|
app.use('/api/wallets/mnemonic/reveal', ...identify, mnemonicRevealLimiter);
|
||||||
app.use('/api/wallets/:chain/send', ...protect, sensitiveLimiter);
|
app.use('/api/wallets/:chain/send', ...identify, sensitiveLimiter);
|
||||||
|
|
||||||
// Mutating (proxy + read endpoints) — повышенный лимит
|
app.use('/api/wallets', ...identify, mutateLimiter, walletRoutes);
|
||||||
app.use('/api/wallets', ...protect, mutateLimiter, walletRoutes);
|
app.use('/api/relay', ...identify, mutateLimiter, relayProxyRoutes);
|
||||||
app.use('/api/relay', ...protect, mutateLimiter, relayProxyRoutes);
|
app.use('/api/tron', ...identify, mutateLimiter, tronProxyRoutes);
|
||||||
app.use('/api/tron', ...protect, mutateLimiter, tronProxyRoutes);
|
app.use('/api/sol/swap', ...identify, mutateLimiter, solSwapProxyRoutes);
|
||||||
app.use('/api/sol/swap', ...protect, mutateLimiter, solSwapProxyRoutes);
|
app.use('/api/tron/swap', ...identify, mutateLimiter, tronSwapProxyRoutes);
|
||||||
app.use('/api/tron/swap', ...protect, mutateLimiter, tronSwapProxyRoutes);
|
app.use('/api/btc', ...identify, mutateLimiter, btcProxyRoutes);
|
||||||
app.use('/api/btc', ...protect, mutateLimiter, btcProxyRoutes);
|
app.use('/api/bsc/swap', ...identify, mutateLimiter, bscSwapProxyRoutes);
|
||||||
app.use('/api/bsc/swap', ...protect, mutateLimiter, bscSwapProxyRoutes);
|
|
||||||
|
|
||||||
// USD-цены (CoinGecko + KeyDB cache). GET-only, auth required, max 50 symbols.
|
app.use('/api/prices', ...identify, mutateLimiter, pricesRoutes);
|
||||||
app.use('/api/prices', ...protect, mutateLimiter, pricesRoutes);
|
|
||||||
|
|
||||||
// 404 для всего что не сматчилось выше — единый JSON-ответ, не express default text
|
// 404 для всего что не сматчилось выше — единый JSON-ответ, не express default text
|
||||||
app.use((_req, res) => {
|
app.use((_req, res) => {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import { acquireSendLock } from '../lib/send-lock';
|
|||||||
import { claimIdempotency, saveIdempotencyResponse, extractIdempotencyKey } from '../lib/idempotency';
|
import { claimIdempotency, saveIdempotencyResponse, extractIdempotencyKey } from '../lib/idempotency';
|
||||||
import { auditLog, auditLogStrict, completeAudit } from '../lib/audit-log';
|
import { auditLog, auditLogStrict, completeAudit } from '../lib/audit-log';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
import { FIXED_API_USER_ID } from '../middleware/fixed-user';
|
||||||
|
|
||||||
const ALLOWED_CHAINS = new Set<ChainCode>(ALL_CHAINS);
|
const ALLOWED_CHAINS = new Set<ChainCode>(ALL_CHAINS);
|
||||||
const MAX_TX_LIMIT = 100;
|
const MAX_TX_LIMIT = 100;
|
||||||
const HARDCODED_CREATE_WALLET_USER_ID = '01KR4V0RPJYPBHPRNY31GSZHXG';
|
|
||||||
|
|
||||||
class ConflictError extends Error {
|
class ConflictError extends Error {
|
||||||
constructor() { super('Wallet already exists'); }
|
constructor() { super('Wallet already exists'); }
|
||||||
@@ -33,7 +33,7 @@ export const WalletController = {
|
|||||||
*/
|
*/
|
||||||
async getWallets(req: Request, res: Response) {
|
async getWallets(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const wallets = await WalletModel.findByUserId(req.auth!.userId);
|
const wallets = await WalletModel.findByUserId(FIXED_API_USER_ID);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: wallets.map((w) => ({
|
data: wallets.map((w) => ({
|
||||||
@@ -43,7 +43,7 @@ export const WalletController = {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(`getWallets failed for user ${req.auth!.userId}: ${err.stack || err.message}`);
|
logger.error(`getWallets failed for user ${FIXED_API_USER_ID}: ${err.stack || err.message}`);
|
||||||
res.status(500).json({ success: false, error: 'Internal error' });
|
res.status(500).json({ success: false, error: 'Internal error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@ export const WalletController = {
|
|||||||
* Возвращает: ТОЛЬКО адреса. Mnemonic клиенту не отдаём.
|
* Возвращает: ТОЛЬКО адреса. Mnemonic клиенту не отдаём.
|
||||||
*/
|
*/
|
||||||
async createWallet(req: Request, res: Response) {
|
async createWallet(req: Request, res: Response) {
|
||||||
const userId = HARDCODED_CREATE_WALLET_USER_ID;
|
const userId = FIXED_API_USER_ID;
|
||||||
|
|
||||||
if (!isCryptoReady()) {
|
if (!isCryptoReady()) {
|
||||||
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
@@ -161,7 +161,7 @@ export const WalletController = {
|
|||||||
* Защита: POST + CSRF + body confirm token + rate-limit 5/час + audit-log.
|
* Защита: POST + CSRF + body confirm token + rate-limit 5/час + audit-log.
|
||||||
*/
|
*/
|
||||||
async revealMnemonic(req: Request, res: Response) {
|
async revealMnemonic(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
|
|
||||||
if (!isCryptoReady()) {
|
if (!isCryptoReady()) {
|
||||||
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
@@ -237,7 +237,7 @@ export const WalletController = {
|
|||||||
* GET /api/wallets/:chain/balance
|
* GET /api/wallets/:chain/balance
|
||||||
*/
|
*/
|
||||||
async getChainBalance(req: Request, res: Response) {
|
async getChainBalance(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const chain = String(req.params.chain).toUpperCase();
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
|
|
||||||
if (!isChain(chain)) {
|
if (!isChain(chain)) {
|
||||||
@@ -264,7 +264,7 @@ export const WalletController = {
|
|||||||
* GET /api/wallets/:chain/transactions
|
* GET /api/wallets/:chain/transactions
|
||||||
*/
|
*/
|
||||||
async getChainTransactions(req: Request, res: Response) {
|
async getChainTransactions(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const chain = String(req.params.chain).toUpperCase();
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
|
|
||||||
if (!isChain(chain)) {
|
if (!isChain(chain)) {
|
||||||
@@ -295,7 +295,7 @@ export const WalletController = {
|
|||||||
* мнемонику, деривит privkey, подписывает, broadcast'ит → возвращает txid.
|
* мнемонику, деривит privkey, подписывает, broadcast'ит → возвращает txid.
|
||||||
*/
|
*/
|
||||||
async sendFromChain(req: Request, res: Response) {
|
async sendFromChain(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const chain = String(req.params.chain).toUpperCase();
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
|
|
||||||
if (!isChain(chain)) {
|
if (!isChain(chain)) {
|
||||||
@@ -471,7 +471,7 @@ export const WalletController = {
|
|||||||
* нужно whitelist'ить `to` или требовать Relay attestation.
|
* нужно whitelist'ить `to` или требовать Relay attestation.
|
||||||
*/
|
*/
|
||||||
async signRawEvmTx(req: Request, res: Response) {
|
async signRawEvmTx(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const chain = String(req.params.chain).toUpperCase();
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
|
|
||||||
if (chain !== 'ETH' && chain !== 'BSC') {
|
if (chain !== 'ETH' && chain !== 'BSC') {
|
||||||
@@ -633,7 +633,7 @@ export const WalletController = {
|
|||||||
* Body для SOL: { inputMint, outputMint, amount, slippageBps? } — mint addresses.
|
* Body для SOL: { inputMint, outputMint, amount, slippageBps? } — mint addresses.
|
||||||
*/
|
*/
|
||||||
async swapOnChain(req: Request, res: Response) {
|
async swapOnChain(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const chain = String(req.params.chain).toUpperCase();
|
const chain = String(req.params.chain).toUpperCase();
|
||||||
if (chain !== 'BSC' && chain !== 'TRX' && chain !== 'SOL') {
|
if (chain !== 'BSC' && chain !== 'TRX' && chain !== 'SOL') {
|
||||||
res.status(400).json({ success: false, error: 'Swap supported only on BSC, TRX, SOL. For ETH use Relay quote→execute→sign-raw-evm-tx.' });
|
res.status(400).json({ success: false, error: 'Swap supported only on BSC, TRX, SOL. For ETH use Relay quote→execute→sign-raw-evm-tx.' });
|
||||||
@@ -763,7 +763,7 @@ export const WalletController = {
|
|||||||
* Body: { transaction: '<base64 serialized VersionedTransaction>' }
|
* Body: { transaction: '<base64 serialized VersionedTransaction>' }
|
||||||
*/
|
*/
|
||||||
async signSolanaTx(req: Request, res: Response) {
|
async signSolanaTx(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
if (!isCryptoReady()) {
|
if (!isCryptoReady()) {
|
||||||
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
25
apps/api/src/middleware/fixed-user.ts
Normal file
25
apps/api/src/middleware/fixed-user.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import type { AuthContext } from '../services/jwt.service';
|
||||||
|
|
||||||
|
|
||||||
|
export const FIXED_API_USER_ID = '01KR4V0RPJYPBHPRNY31GSZHXG';
|
||||||
|
|
||||||
|
|
||||||
|
const FIXED_AUTH: AuthContext = {
|
||||||
|
userId: FIXED_API_USER_ID,
|
||||||
|
sid: 'fixed',
|
||||||
|
token: {
|
||||||
|
sub: FIXED_API_USER_ID,
|
||||||
|
type: 'access',
|
||||||
|
sid: 'fixed',
|
||||||
|
iat: 0,
|
||||||
|
nbf: 0,
|
||||||
|
exp: 4102444800,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function fixedUserMiddleware(req: Request, _res: Response, next: NextFunction): void {
|
||||||
|
req.auth = FIXED_AUTH;
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response, Router } from 'express';
|
import { Request, Response, Router } from 'express';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
import { FIXED_API_USER_ID } from '../middleware/fixed-user';
|
||||||
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -117,7 +118,7 @@ async function buildSwapTx(req: Request, res: Response) {
|
|||||||
|
|
||||||
// C17 — bind userAddress to JWT. Иначе attacker может set userAddress=victim_addr
|
// C17 — bind userAddress to JWT. Иначе attacker может set userAddress=victim_addr
|
||||||
// и output swap'а пойдёт victim'у/контракту-attacker'у (reentrancy vector).
|
// и output swap'а пойдёт victim'у/контракту-attacker'у (reentrancy vector).
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
try {
|
try {
|
||||||
await assertUserOwnsAddress(userId, 'BSC', userAddress);
|
await assertUserOwnsAddress(userId, 'BSC', userAddress);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { env } from '../config/env';
|
|||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
import { WalletModel } from '../models/wallet.model';
|
import { WalletModel } from '../models/wallet.model';
|
||||||
import type { ChainCode } from '../lib/address-validators';
|
import type { ChainCode } from '../lib/address-validators';
|
||||||
|
import { FIXED_API_USER_ID } from '../middleware/fixed-user';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const RELAY_API_URL = 'https://api.relay.link';
|
const RELAY_API_URL = 'https://api.relay.link';
|
||||||
@@ -57,11 +58,7 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
|
|||||||
// Без этого authenticated user может set recipient=attacker → Relay строит quote →
|
// Без этого authenticated user может set recipient=attacker → Relay строит quote →
|
||||||
// victim signs → bridge funds к attacker'у.
|
// victim signs → bridge funds к attacker'у.
|
||||||
if (req.method === 'POST' && (relayPath === '/quote' || relayPath.startsWith('/execute/'))) {
|
if (req.method === 'POST' && (relayPath === '/quote' || relayPath.startsWith('/execute/'))) {
|
||||||
const userId = (req as any).auth?.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
if (!userId) {
|
|
||||||
res.status(401).json({ success: false, error: 'auth required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bodyUser = req.body?.user;
|
const bodyUser = req.body?.user;
|
||||||
const bodyRecipient = req.body?.recipient;
|
const bodyRecipient = req.body?.recipient;
|
||||||
const originChainId = Number(req.body?.originChainId);
|
const originChainId = Number(req.body?.originChainId);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response, Router } from 'express';
|
import { Request, Response, Router } from 'express';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
import { FIXED_API_USER_ID } from '../middleware/fixed-user';
|
||||||
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
|
|
||||||
@@ -135,7 +136,7 @@ async function buildSwap(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// C9 — bind userPublicKey to JWT (без bind: feeAccount/referral hijacking, ATA pre-staging)
|
// C9 — bind userPublicKey to JWT (без bind: feeAccount/referral hijacking, ATA pre-staging)
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
try {
|
try {
|
||||||
await assertUserOwnsAddress(userId, 'SOL', userPublicKey);
|
await assertUserOwnsAddress(userId, 'SOL', userPublicKey);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Request, Response, Router } from 'express';
|
import { Request, Response, Router } from 'express';
|
||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
import { FIXED_API_USER_ID } from '../middleware/fixed-user';
|
||||||
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
import { assertUserOwnsAddress } from '../lib/wallet-binding';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -202,7 +203,7 @@ async function buildSwapTx(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// H47 — bind userAddress to JWT (output идёт на userAddress; без bind output к attacker'у)
|
// H47 — bind userAddress to JWT (output идёт на userAddress; без bind output к attacker'у)
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
try {
|
try {
|
||||||
await assertUserOwnsAddress(userId, 'TRX', userAddress);
|
await assertUserOwnsAddress(userId, 'TRX', userAddress);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -338,7 +339,7 @@ async function broadcastTx(req: Request, res: Response) {
|
|||||||
|
|
||||||
// C6 — verify owner_address внутри signed tx равен JWT-bound TRX-адресу юзера.
|
// C6 — verify owner_address внутри signed tx равен JWT-bound TRX-адресу юзера.
|
||||||
// Иначе endpoint = open relay для arbitrary tx (replay leaked txs, evade IP bans).
|
// Иначе endpoint = open relay для arbitrary tx (replay leaked txs, evade IP bans).
|
||||||
const userId = req.auth!.userId;
|
const userId = FIXED_API_USER_ID;
|
||||||
const contract0 = signedTransaction?.raw_data?.contract?.[0];
|
const contract0 = signedTransaction?.raw_data?.contract?.[0];
|
||||||
const ownerAddr = contract0?.parameter?.value?.owner_address;
|
const ownerAddr = contract0?.parameter?.value?.owner_address;
|
||||||
if (typeof ownerAddr !== 'string' || !ownerAddr) {
|
if (typeof ownerAddr !== 'string' || !ownerAddr) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "CryptoWallet API",
|
"title": "CryptoWallet API",
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"description": "Multi-chain custodial wallet API (ETH/BSC/BTC/TRX/SOL). Сервер генерит mnemonic, шифрует AES-256-GCM (master-key из HashiCorp Vault), хранит её и сам подписывает транзакции. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)."
|
"description": "Multi-chain custodial wallet API (ETH/BSC/BTC/TRX/SOL). Сервер генерит mnemonic, шифрует AES-256-GCM (master-key из HashiCorp Vault), хранит её и сам подписывает транзакции. Все операции выполняются для фиксированного user_id на сервере; JWT/CSRF не используются."
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{ "url": "/api", "description": "API root" }
|
{ "url": "/api", "description": "API root" }
|
||||||
@@ -220,10 +220,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [],
|
||||||
{ "cookieAuth": [] },
|
|
||||||
{ "bearerAuth": [] }
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"/health": {
|
"/health": {
|
||||||
"get": {
|
"get": {
|
||||||
@@ -248,7 +245,7 @@
|
|||||||
"/wallets/create": {
|
"/wallets/create": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Создать custodial-кошелёк (server-side mnemonic)",
|
"summary": "Создать custodial-кошелёк (server-side mnemonic)",
|
||||||
"description": "**Публичный вызов (без JWT/CSRF).** Кошелёк всегда создаётся для фиксированного user_id на сервере. **Тело запроса не требуется.** Сервер генерит BIP39 mnemonic (12 слов), деривит адреса для 5 chains (BIP44: ETH m/44'/60'/0'/0/0, BTC m/84'/0'/0'/0/0, TRX m/44'/195'/0'/0/0, SOL m/44'/501'/0'/0', BSC = ETH path), шифрует mnemonic AES-256-GCM (master-key из HashiCorp Vault) и атомарно сохраняет. **Возвращает ТОЛЬКО адреса** — mnemonic клиенту не отдаётся. Чтобы потом увидеть seed — отдельный endpoint POST /wallets/mnemonic/reveal. Идемпотентность: 409 если у юзера уже есть кошелёк.",
|
"description": "**Без JWT/CSRF.** Кошелёк всегда создаётся для фиксированного user_id на сервере. **Тело запроса не требуется.** Сервер генерит BIP39 mnemonic (12 слов), деривит адреса для 5 chains (BIP44: ETH m/44'/60'/0'/0/0, BTC m/84'/0'/0'/0/0, TRX m/44'/195'/0'/0/0, SOL m/44'/501'/0'/0', BSC = ETH path), шифрует mnemonic AES-256-GCM (master-key из HashiCorp Vault) и атомарно сохраняет. **Возвращает ТОЛЬКО адреса** — mnemonic клиенту не отдаётся. Чтобы потом увидеть seed — отдельный endpoint POST /wallets/mnemonic/reveal. Идемпотентность: 409 если у юзера уже есть кошелёк.",
|
||||||
"tags": ["Wallets"],
|
"tags": ["Wallets"],
|
||||||
"security": [],
|
"security": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -263,7 +260,7 @@
|
|||||||
"/wallets/mnemonic/reveal": {
|
"/wallets/mnemonic/reveal": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Раскрыть mnemonic (settings-screen)",
|
"summary": "Раскрыть mnemonic (settings-screen)",
|
||||||
"description": "Расшифровывает и возвращает 12-словную BIP39 мнемонику юзера. POST + CSRF + body-confirmation. Rate-limit 5/час per-user. Каждый запрос пишется в audit-log.",
|
"description": "Расшифровывает и возвращает 12-словную BIP39 мнемонику для фиксированного user_id на сервере. POST + body-confirmation. Rate-limit 5/час. Каждый запрос пишется в audit-log.",
|
||||||
"tags": ["Wallets"],
|
"tags": ["Wallets"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -293,7 +290,7 @@
|
|||||||
"/wallets/{chain}/balance": {
|
"/wallets/{chain}/balance": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Balance for user wallet in chain (с USD-ценами)",
|
"summary": "Balance for user wallet in chain (с USD-ценами)",
|
||||||
"description": "Возвращает количество и USD-стоимость для native монеты + всех известных токенов сети. Каждый `FormattedAmount` содержит `raw` (smallest units), `formatted` (human-readable), `decimals`, `usdPrice` (цена 1 единицы), `usdValue` (стоимость holding'а). Цены — CoinGecko с 5-минутным KeyDB-кэшем. Если упал price oracle — `usdPrice`/`usdValue` = `null`, но количества всё равно возвращаются.\n\n**Пример curl:**\n```\ncurl -H \"Authorization: Bearer $JWT\" https://api.example.com/api/wallets/ETH/balance\n```",
|
"description": "Возвращает количество и USD-стоимость для native монеты + всех известных токенов сети. Каждый `FormattedAmount` содержит `raw` (smallest units), `formatted` (human-readable), `decimals`, `usdPrice` (цена 1 единицы), `usdValue` (стоимость holding'а). Цены — CoinGecko с 5-минутным KeyDB-кэшем. Если упал price oracle — `usdPrice`/`usdValue` = `null`, но количества всё равно возвращаются.\n\n**Пример curl:**\n```\ncurl https://api.example.com/api/wallets/ETH/balance\n```",
|
||||||
"tags": ["Wallet Ops"],
|
"tags": ["Wallet Ops"],
|
||||||
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Chain" } }],
|
"parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Chain" } }],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -692,7 +689,7 @@
|
|||||||
"/prices": {
|
"/prices": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "USD-цены для списка символов",
|
"summary": "USD-цены для списка символов",
|
||||||
"description": "Возвращает котировки USD для указанных символов (max 50). Символы должны быть из реестра поддерживаемых токенов (см. tag описание сетей в /wallets/{chain}/balance). Источник — CoinGecko free API, кэшируется в KeyDB 5 минут.\n\n**Resolution:**\n- Native символ совпадающий с chain code (BTC/ETH/BSC/TRX/SOL) → используется native CoinGecko id.\n- Иначе: ищется в реестре сети из `chain` query param.\n- Если `chain` не задан → fallback порядок ETH → BSC → SOL → TRX → BTC. Первый matched chain wins.\n\n**Безопасность:** symbols whitelisted, никакого user-input в URL CoinGecko (защита от SSRF). Max 50 символов на запрос. Auth обязательна (JWT Bearer или cookie).\n\n**Пример curl:**\n```\ncurl -H \"Authorization: Bearer $JWT\" \"https://api.example.com/api/prices?symbols=BTC,ETH,USDT,SOL,BONK\"\n```",
|
"description": "Возвращает котировки USD для указанных символов (max 50). Символы должны быть из реестра поддерживаемых токенов (см. tag описание сетей в /wallets/{chain}/balance). Источник — CoinGecko free API, кэшируется в KeyDB 5 минут.\n\n**Resolution:**\n- Native символ совпадающий с chain code (BTC/ETH/BSC/TRX/SOL) → используется native CoinGecko id.\n- Иначе: ищется в реестре сети из `chain` query param.\n- Если `chain` не задан → fallback порядок ETH → BSC → SOL → TRX → BTC. Первый matched chain wins.\n\n**Безопасность:** symbols whitelisted, никакого user-input в URL CoinGecko (защита от SSRF). Max 50 символов на запрос.\n\n**Пример curl:**\n```\ncurl \"https://api.example.com/api/prices?symbols=BTC,ETH,USDT,SOL,BONK\"\n```",
|
||||||
"tags": ["Prices"],
|
"tags": ["Prices"],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user