diff --git a/.env.example b/.env.example index 63aace5..33c3b83 100644 --- a/.env.example +++ b/.env.example @@ -10,11 +10,6 @@ VAULT_JWT_KIDS_PREFIX=jwt/kids # CSRF загружается если указан путь (оставь пустым чтобы отключить CSRF) VAULT_CSRF_PATH= -# Crypto master-key для шифрования мнемоник юзеров (AES-256-GCM). -# В Vault лежит hex-строка длиной 64 (32 байта). -# Положить: vault kv put dev-secrets/crypto/master key=$(openssl rand -hex 32) -VAULT_CRYPTO_KEY_PATH=crypto/master - # ── JWT ──────────────────────────────────────────────────────────── # Allowed: RS256 / RS384 / RS512 / ES256 / ES384 / ES512 / EdDSA / PS256 / PS384 / PS512 JWT_ALGORITHM=RS256 diff --git a/Dockerfile b/Dockerfile index 692edc3..06b1582 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,6 @@ COPY --from=build --chown=app:app /app/apps/api/dist ./dist COPY --from=build --chown=app:app /app/apps/api/swagger.json ./swagger.json COPY --from=build --chown=app:app /app/apps/api/package.json ./package.json -RUN mkdir -p /app/logs && chown -R app:app /app/logs - USER app EXPOSE 3001 diff --git a/apps/api/.eslintrc.json b/apps/api/.eslintrc.json new file mode 100644 index 0000000..9223e0a --- /dev/null +++ b/apps/api/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/no-namespace": "off", + "no-console": "off" + }, + "ignorePatterns": ["dist/", "node_modules/"] +} diff --git a/apps/api/package.json b/apps/api/package.json index 09fe938..0205a6f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,14 +11,11 @@ }, "dependencies": { "@solana/web3.js": "^1.98.4", - "bip32": "^4.0.0", - "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.5", "bs58": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.4.0", - "ed25519-hd-key": "^1.3.0", "ethers": "5.7.2", "express": "^4.21.0", "express-rate-limit": "^8.4.1", @@ -27,7 +24,6 @@ "knex": "^3.1.0", "pg": "^8.13.0", "swagger-ui-express": "^5.0.1", - "tiny-secp256k1": "^2.2.3", "ulidx": "^2.4.1" }, "devDependencies": { diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 79e6e55..c8b9fc9 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -8,7 +8,7 @@ import { swaggerSpec } from './config/swagger'; import { traceMiddleware } from './middleware/trace'; import { authMiddleware } from './middleware/auth'; import { csrfMiddleware } from './middleware/csrf'; -import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit'; +import { globalLimiter, mutateLimiter, sensitiveLimiter } from './middleware/rate-limit'; import { errorHandler } from './middleware/error-handler'; import walletRoutes from './routes/wallet.routes'; import relayProxyRoutes from './routes/relay-proxy.routes'; @@ -50,9 +50,8 @@ app.use('/api', globalLimiter); // ── PROTECTED endpoints (JWT + CSRF) ───────────────────────────────────────── const protect = [authMiddleware, csrfMiddleware]; -// Sensitive — самый строгий лимит. Каждый POST/PUT защищён JWT + CSRF. +// Sensitive — самый строгий лимит. Каждый POST защищён JWT + CSRF. app.use('/api/wallets/create', ...protect, sensitiveLimiter); -app.use('/api/wallets/mnemonic/reveal', ...protect, mnemonicRevealLimiter); app.use('/api/wallets/:chain/send', ...protect, sensitiveLimiter); // Mutating (proxy + read endpoints) — повышенный лимит diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index 952a874..85b53e2 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -31,7 +31,6 @@ export let env = { jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid', jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids', csrfPath: p.VAULT_CSRF_PATH || '', - cryptoKeyPath: p.VAULT_CRYPTO_KEY_PATH || 'crypto/master', }, cors: { // No default — каждый деплой явно указывает CORS_ORIGINS, иначе пустой массив = нет cross-origin доступа. diff --git a/apps/api/src/controllers/wallet.controller.ts b/apps/api/src/controllers/wallet.controller.ts index adffe29..60dc2db 100644 --- a/apps/api/src/controllers/wallet.controller.ts +++ b/apps/api/src/controllers/wallet.controller.ts @@ -1,21 +1,15 @@ import { Request, Response } from 'express'; -import { db } from '../config/database'; import { WalletModel } from '../models/wallet.model'; import { UserModel } from '../models/user.model'; -import { getBalance, getTransactions } from '../services/wallet-ops.service'; +import { getBalance, getTransactions, buildSend } from '../services/wallet-ops.service'; import { isValidAddress, isValidAmount, ChainCode } from '../lib/address-validators'; -import { generateMnemonic, deriveAllAddresses, ALL_CHAINS } from '../services/wallet-generator.service'; -import { encryptMnemonic, decryptMnemonic, isCryptoReady } from '../services/crypto.service'; -import { signAndBroadcast } from '../services/wallet-signer.service'; -import { auditLog } from '../lib/audit-log'; import { logger } from '../lib/logger'; -const ALLOWED_CHAINS = new Set(ALL_CHAINS); +const ALLOWED_CHAINS = new Set(['ETH', 'BTC', 'SOL', 'TRX', 'BSC']); +const MAX_WALLETS_PER_REQUEST = 20; +const MAX_DERIVATION_PATH = 64; const MAX_TX_LIMIT = 100; - -class ConflictError extends Error { - constructor() { super('Wallet already exists'); } -} +const BIP32_PATH_RE = /^m(\/[0-9]+'?)*$/; function isChain(value: unknown): value is ChainCode { return typeof value === 'string' && ALLOWED_CHAINS.has(value as ChainCode); @@ -23,7 +17,7 @@ function isChain(value: unknown): value is ChainCode { export const WalletController = { /** - * GET /api/wallets — все адреса юзера (chain + address + derivationPath). + * GET /api/wallets — все адреса юзера. */ async getWallets(req: Request, res: Response) { try { @@ -43,155 +37,69 @@ export const WalletController = { }, /** - * POST /api/wallets/create — custodial wallet bootstrap. - * - * 1) Проверка: у юзера ещё нет коша (иначе 409) - * 2) Генерим BIP39 mnemonic (128 bit, 12 слов) - * 3) Деривим адреса для всех 5 chains - * 4) Шифруем mnemonic AES-GCM (master-key из Vault) - * 5) Транзакционно пишем encrypted_mnemonic + wallets - * 6) Возвращаем ТОЛЬКО адреса. Mnemonic клиенту не отдаём. + * POST /api/wallets/create — non-custodial upsert. + * Клиент сам деривит mnemonic и шлёт массив { chain, address, derivationPath }. + * Сервер валидирует и сохраняет (upsert по user_id+chain). */ - async createWallet(req: Request, res: Response) { + async createWallets(req: Request, res: Response) { const userId = req.auth!.userId; + const { wallets } = req.body ?? {}; - if (!isCryptoReady()) { - res.status(503).json({ success: false, error: 'Crypto service not ready' }); + if (!Array.isArray(wallets) || wallets.length === 0 || wallets.length > MAX_WALLETS_PER_REQUEST) { + res.status(400).json({ + success: false, + error: `wallets must be a non-empty array (max ${MAX_WALLETS_PER_REQUEST})`, + }); return; } - let mnemonic: string | null = null; + for (const w of wallets) { + if (!w || typeof w !== 'object') { + res.status(400).json({ success: false, error: 'Invalid wallet entry' }); + return; + } + if (!isChain(w.chain)) { + res.status(400).json({ success: false, error: 'Invalid chain' }); + return; + } + if (!isValidAddress(w.chain, w.address)) { + res.status(400).json({ success: false, error: 'Invalid address format for chain' }); + return; + } + if ( + typeof w.derivationPath !== 'string' || + w.derivationPath.length === 0 || + w.derivationPath.length > MAX_DERIVATION_PATH || + !BIP32_PATH_RE.test(w.derivationPath) + ) { + res.status(400).json({ success: false, error: 'Invalid derivationPath (BIP32 m/.. format expected)' }); + return; + } + } + try { await UserModel.ensureExists(userId); - // Сначала проверка для быстрого 409 (не атомарна — реальная защита ниже) - if (await UserModel.hasMnemonic(userId)) { - res.status(409).json({ success: false, error: 'Wallet already exists' }); - return; - } - - mnemonic = generateMnemonic(); - const derived = await deriveAllAddresses(mnemonic); - const blob = encryptMnemonic(mnemonic); - - // ── АТОМАРНО: UPDATE users WHERE encrypted_mnemonic IS NULL + INSERT wallets ── - // Защита от funds-loss race: если параллельный запрос успел проставить mnemonic, - // наш UPDATE вернёт affected=0 → откатываем транзакцию, возвращаем 409. - const created = await db.transaction(async (trx) => { - const claimed = await UserModel.setEncryptedMnemonicIfAbsent(userId, blob, trx); - if (!claimed) { - // Кто-то другой выиграл race — наш mnemonic становится бессмысленным - throw new ConflictError(); - } - await WalletModel.createMany( - derived.map((w) => ({ - user_id: userId, - chain: w.chain, - address: w.address, - derivation_path: w.derivationPath, - })), - trx, - ); - return derived; - }); - - await auditLog({ - event: 'wallet.create', - userId, - ip: req.ip || null, - result: 'success', - meta: { chains: created.map((w) => w.chain) }, - }); + const rows = await WalletModel.upsertMany( + wallets.map((w: { chain: ChainCode; address: string; derivationPath: string }) => ({ + user_id: userId, + chain: w.chain, + address: w.address, + derivation_path: w.derivationPath, + })), + ); res.status(201).json({ success: true, - data: created.map((w) => ({ + data: rows.map((w) => ({ chain: w.chain, address: w.address, - derivationPath: w.derivationPath, + derivationPath: w.derivation_path, })), }); } catch (err: any) { - if (err instanceof ConflictError) { - res.status(409).json({ success: false, error: 'Wallet already exists' }); - return; - } - logger.error(`createWallet failed for user ${userId}: ${err.message}`); - await auditLog({ - event: 'wallet.create', - userId, - ip: req.ip || null, - result: 'failure', - errorCode: err.code || 'INTERNAL', - }); - res.status(500).json({ success: false, error: 'Failed to create wallet' }); - } finally { - mnemonic = null; // best-effort GC hint - } - }, - - /** - * POST /api/wallets/mnemonic/reveal — reveal mnemonic для settings-страницы. - * - * Защита (defense-in-depth): - * 1. POST (CSRF token обязателен) - * 2. Тело должно содержать { confirm: "I_UNDERSTAND_SEED_IS_SECRET" } — защита - * от случайного XHR / image-tag / CSRF-attack which forge только URL - * 3. Rate-limit 5/час per-user - * 4. Audit-log на каждый вызов (success + failure) - */ - async revealMnemonic(req: Request, res: Response) { - const userId = req.auth!.userId; - - if (!isCryptoReady()) { - res.status(503).json({ success: false, error: 'Crypto service not ready' }); - return; - } - - // Confirmation token защищает от случайных XHR из чужих origin (даже если CSRF - // фронтенд сломан, атакующему нужно знать секретную фразу — она задокументирована в API) - if (req.body?.confirm !== 'I_UNDERSTAND_SEED_IS_SECRET') { - res.status(400).json({ - success: false, - error: 'Missing or invalid confirm token. Send { "confirm": "I_UNDERSTAND_SEED_IS_SECRET" } in body.', - }); - return; - } - - try { - const blob = await UserModel.getEncryptedMnemonic(userId); - if (!blob) { - await auditLog({ - event: 'mnemonic.reveal', - userId, - ip: req.ip || null, - result: 'failure', - errorCode: 'NOT_FOUND', - }); - res.status(404).json({ success: false, error: 'Wallet not created yet' }); - return; - } - - const mnemonic = decryptMnemonic(blob); - - await auditLog({ - event: 'mnemonic.reveal', - userId, - ip: req.ip || null, - result: 'success', - }); - - res.json({ success: true, data: { mnemonic } }); - } catch (err: any) { - logger.error(`revealMnemonic failed for user ${userId}: ${err.message}`); - await auditLog({ - event: 'mnemonic.reveal', - userId, - ip: req.ip || null, - result: 'failure', - errorCode: 'INTERNAL', - }); - res.status(500).json({ success: false, error: 'Failed to reveal mnemonic' }); + logger.error(`createWallets failed for user ${userId}: ${err.stack || err.message}`); + res.status(500).json({ success: false, error: 'Internal error' }); } }, @@ -252,9 +160,8 @@ export const WalletController = { }, /** - * POST /api/wallets/:chain/send — custodial sign + broadcast. - * Сервер расшифровывает мнемонику, деривит privkey, подписывает, broadcast'ит. - * Возвращает { txid }. + * POST /api/wallets/:chain/send — build unsigned tx (non-custodial). + * Возвращает unsigned tx; клиент подписывает приватом и broadcast'ит сам. */ async sendFromChain(req: Request, res: Response) { const userId = req.auth!.userId; @@ -265,11 +172,6 @@ export const WalletController = { return; } - if (!isCryptoReady()) { - res.status(503).json({ success: false, error: 'Crypto service not ready' }); - return; - } - const { to, amount, token } = req.body ?? {}; if (!isValidAddress(chain, String(to))) { @@ -290,7 +192,6 @@ export const WalletController = { normalizedToken = token.toUpperCase(); } - let mnemonic: string | null = null; try { const wallet = await WalletModel.findByUserAndChain(userId, chain); if (!wallet) { @@ -298,51 +199,21 @@ export const WalletController = { return; } - const blob = await UserModel.getEncryptedMnemonic(userId); - if (!blob) { - res.status(404).json({ success: false, error: 'Encrypted mnemonic not found' }); - return; - } - - mnemonic = decryptMnemonic(blob); - - const result = await signAndBroadcast({ + const tx = await buildSend({ chain, - mnemonic, + from: wallet.address, to: String(to), amount: String(amount), token: normalizedToken, - expectedFromAddress: wallet.address, }); - await auditLog({ - event: 'wallet.send', - userId, - ip: req.ip || null, - result: 'success', - meta: { chain, hasToken: !!normalizedToken, txid: result.txid }, - }); - - res.json({ success: true, data: { txid: result.txid, chain } }); + res.json({ success: true, data: tx }); } catch (err: any) { - logger.error(`send failed for user ${userId} chain ${chain}: ${err.message}`); - await auditLog({ - event: 'wallet.send', - userId, - ip: req.ip || null, - result: 'failure', - meta: { chain }, - errorCode: 'BROADCAST_FAILED', - }); - const msg = err?.message?.toLowerCase?.().includes('insufficient') - ? 'Insufficient balance' - : err?.message?.toLowerCase?.().includes('not supported') - ? 'Token/chain combination not supported' - : 'Failed to broadcast transaction'; + logger.error(`buildSend ${chain} failed for user ${userId}: ${err.stack || err.message}`); + const msg = err?.message?.toLowerCase?.().includes('not supported') + ? 'Token/chain combination not supported' + : 'Failed to build transaction'; res.status(502).json({ success: false, error: msg }); - } finally { - // Best-effort cleanup mnemonic в RAM - mnemonic = null; } }, }; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4b10b46..d86ed2d 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,7 +1,6 @@ import app from './app'; import { env, initEnv } from './config/env'; import { refreshAllKeys, startKeyRotation, stopKeyRotation } from './services/key-rotation.service'; -import { isCryptoReady } from './services/crypto.service'; import { logger } from './lib/logger'; async function main() { @@ -9,13 +8,6 @@ async function main() { await initEnv(); await refreshAllKeys(); - - // Custodial: без master-key сервис не может расшифровать ни одну мнемонику — fail fast. - if (!isCryptoReady()) { - logger.error('Crypto master key not loaded — refusing to start (custodial wallets require it)'); - process.exit(1); - } - startKeyRotation(); const server = app.listen(env.port, () => { diff --git a/apps/api/src/lib/audit-log.ts b/apps/api/src/lib/audit-log.ts deleted file mode 100644 index 4238173..0000000 --- a/apps/api/src/lib/audit-log.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Audit log — append-only JSON lines в отдельный файл `logs/audit.log`. - * Используется для критических операций: mnemonic reveal, custodial sign. - * - * НИКОГДА не пишет sensitive данные (mnemonic / privkey / etc.) — только мета. - */ - -import { promises as fs } from 'fs'; -import path from 'path'; -import { logger } from './logger'; -import { getTraceId } from './trace-store'; - -const AUDIT_DIR = path.resolve(__dirname, '../../../../logs'); -const AUDIT_FILE = path.join(AUDIT_DIR, 'audit.log'); - -let initialized = false; - -async function ensureFile(): Promise { - if (initialized) return; - try { - await fs.mkdir(AUDIT_DIR, { recursive: true }); - // Создать с правами 0600 если файла нет - const handle = await fs.open(AUDIT_FILE, 'a', 0o600); - await handle.close(); - try { - await fs.chmod(AUDIT_FILE, 0o600); - } catch { - // chmod может не работать на Windows — игнор - } - initialized = true; - } catch (err: any) { - logger.error(`Audit log init failed: ${err.message}`); - } -} - -export interface AuditEntry { - event: string; // 'mnemonic.reveal', 'wallet.create', 'wallet.send', etc. - userId: string; - ip?: string | null; - meta?: Record; - result?: 'success' | 'failure'; - errorCode?: string; -} - -export async function auditLog(entry: AuditEntry): Promise { - await ensureFile(); - - const line = JSON.stringify({ - timestamp: new Date().toISOString(), - trace_id: getTraceId(), - ...entry, - }); - - try { - await fs.appendFile(AUDIT_FILE, line + '\n', { mode: 0o600 }); - } catch (err: any) { - // Если audit-log не записался — логируем в обычный logger как ошибку, - // но НЕ блокируем основной флоу - logger.error(`Audit log write failed: ${err.message}`); - } -} diff --git a/apps/api/src/middleware/rate-limit.ts b/apps/api/src/middleware/rate-limit.ts index bb77af2..a18fe11 100644 --- a/apps/api/src/middleware/rate-limit.ts +++ b/apps/api/src/middleware/rate-limit.ts @@ -31,7 +31,7 @@ export const mutateLimiter = rateLimit({ message: { success: false, error: 'Too many mutating requests' }, }); -// Самый строгий — для send / vault PUT / wallet create (anti-abuse / spam tx prevention) +// Самый строгий — для send / wallet create (anti-abuse / spam tx prevention) export const sensitiveLimiter = rateLimit({ windowMs: 60 * 1000, limit: 10, @@ -40,14 +40,3 @@ export const sensitiveLimiter = rateLimit({ keyGenerator: keyByUserOrIp, message: { success: false, error: 'Too many sensitive requests' }, }); - -// Экстремально строгий — для GET /api/wallets/mnemonic. -// Reveal seed phrase — критическая операция: 5 запросов в час per-user. -export const mnemonicRevealLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - limit: 5, - standardHeaders: 'draft-7', - legacyHeaders: false, - keyGenerator: keyByUserOrIp, - message: { success: false, error: 'Too many mnemonic reveal requests' }, -}); diff --git a/apps/api/src/models/user.model.ts b/apps/api/src/models/user.model.ts index 69a61d7..2e45dd0 100644 --- a/apps/api/src/models/user.model.ts +++ b/apps/api/src/models/user.model.ts @@ -1,4 +1,3 @@ -import type { Knex } from 'knex'; import { db } from '../config/database'; export interface UserRow { @@ -18,9 +17,9 @@ export interface UserRow { kyc_verified: boolean; kyc_verified_at: Date | null; is_deleted: boolean; - encrypted_vault: string | null; - vault_salt: string | null; - encrypted_mnemonic: string | null; + encrypted_vault: string | null; // legacy, unused + vault_salt: string | null; // legacy, unused + encrypted_mnemonic: string | null; // legacy from custodial-experiment, unused created_at: Date; updated_at: Date; } @@ -36,8 +35,7 @@ export const UserModel = { /** * Создать запись пользователя если её нет. - * id берётся из JWT (sub). Email/password_hash — заглушки, потому что реальный - * учёт авторизации в BITOK; мы только проксируем wallet-специфичные данные. + * id берётся из JWT (sub). Email/password_hash — заглушки, реальная auth у BITOK. */ async ensureExists(id: string): Promise { await db('users') @@ -60,41 +58,4 @@ export const UserModel = { .returning('*'); return user; }, - - /** - * Custodial: атомарно записать зашифрованную мнемонику. - * Используется set-once семантика: UPDATE WHERE encrypted_mnemonic IS NULL, - * возвращает true только если этот вызов реально сел в slot. - * - * Защищает от TOCTOU race: два параллельных createWallet не могут оба - * перезаписать друг друга. Без этого ВТОРОЙ запрос сохранил бы свою mnemonic, - * но wallet-rows остались бы от первого → funds permanently lost. - */ - async setEncryptedMnemonicIfAbsent(id: string, blob: string, trx?: Knex.Transaction): Promise { - const q = (trx || db)('users') - .where({ id }) - .whereNull('encrypted_mnemonic') - .update({ - encrypted_mnemonic: blob, - updated_at: db.fn.now(), - }); - const affected = await q; - return affected === 1; - }, - - async getEncryptedMnemonic(id: string): Promise { - const row = await db('users') - .where({ id, is_deleted: false }) - .select('encrypted_mnemonic') - .first(); - return row?.encrypted_mnemonic ?? null; - }, - - async hasMnemonic(id: string): Promise { - const row = await db('users') - .where({ id, is_deleted: false }) - .select(db.raw('encrypted_mnemonic IS NOT NULL AS has')) - .first(); - return Boolean(row?.has); - }, }; diff --git a/apps/api/src/routes/wallet.routes.ts b/apps/api/src/routes/wallet.routes.ts index 1250ab4..61b905d 100644 --- a/apps/api/src/routes/wallet.routes.ts +++ b/apps/api/src/routes/wallet.routes.ts @@ -3,12 +3,9 @@ import { WalletController } from '../controllers/wallet.controller'; const router = Router(); -// Lifecycle -router.post('/create', WalletController.createWallet); router.get('/', WalletController.getWallets); -router.post('/mnemonic/reveal', WalletController.revealMnemonic); +router.post('/create', WalletController.createWallets); -// Per-chain operations router.get('/:chain/balance', WalletController.getChainBalance); router.get('/:chain/transactions', WalletController.getChainTransactions); router.post('/:chain/send', WalletController.sendFromChain); diff --git a/apps/api/src/services/crypto.service.ts b/apps/api/src/services/crypto.service.ts deleted file mode 100644 index 50171ec..0000000 --- a/apps/api/src/services/crypto.service.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { randomBytes, createCipheriv, createDecipheriv, timingSafeEqual } from 'crypto'; -import { fetchVaultKV2 } from '../config/vault'; - -/** - * Symmetric encryption (AES-256-GCM) для хранения мнемоник юзеров в БД. - * Master-key читается из Vault при старте + при каждой ротации ключей. - * - * Storage layout (base64): - * IV(12) || ciphertext(N) || authTag(16) - * - * Ключ — 32 байта (256 бит), храним в Buffer, нигде на диск не пишем. - * Если ключ не загружен — encrypt/decrypt бросают ошибку (fail-secure). - */ - -const KEY_LEN = 32; // 256-bit AES key -const IV_LEN = 12; // GCM standard nonce -const TAG_LEN = 16; // GCM auth tag - -let masterKey: Buffer | null = null; - -/** - * Установить master-key. Вызывается ОДНОКРАТНО при первом старте. - * Передача null или повторная установка после успешной загрузки — запрещено, - * это бы убило все существующие encrypted_mnemonic. - */ -export function swapMasterKey(newKey: Buffer): void { - if (!newKey || newKey.length !== KEY_LEN) { - throw new Error(`swapMasterKey: invalid key (expected ${KEY_LEN} bytes)`); - } - if (masterKey) { - // Уже загружен — повторная установка опасна. Если ключ совпадает — silent no-op. - // Если отличается — это либо ротация (запрещена), либо bug, либо атака. - throw new Error('swapMasterKey: master key already loaded; rotation is not supported'); - } - masterKey = newKey; -} - -/** - * Проверить, отличается ли свежий fetched-ключ от установленного in-memory. - * Используется для WARN-логирования при ротации в Vault (операторская ошибка). - */ -export function masterKeyMatches(candidate: Buffer): boolean { - if (!masterKey || !candidate || candidate.length !== KEY_LEN) return false; - return masterKey.equals(candidate); -} - -export function isCryptoReady(): boolean { - return masterKey !== null && masterKey.length === KEY_LEN; -} - -/** - * Pre-fetch master-key из Vault. НЕ мутирует глобал — возвращает Buffer. - * Throws при отсутствии или невалидном формате. - */ -export async function fetchMasterKey( - addr: string, - token: string, - mount: string, - path: string, -): Promise { - const secrets = await fetchVaultKV2(addr, token, mount, path); - if (!secrets) { - throw new Error('Failed to load crypto master key from Vault'); - } - - const raw = secrets.key || secrets.master_key || secrets.MASTER_KEY; - if (!raw || typeof raw !== 'string') { - throw new Error('Crypto master key invalid: expected hex string in Vault field "key"'); - } - - // Принимаем только hex 64 chars = 32 bytes - if (!/^[0-9a-fA-F]{64}$/.test(raw)) { - throw new Error('Crypto master key invalid: must be 64-char hex (32 bytes)'); - } - - const buf = Buffer.from(raw, 'hex'); - if (buf.length !== KEY_LEN) { - throw new Error(`Crypto master key invalid: got ${buf.length} bytes, expected ${KEY_LEN}`); - } - - return buf; -} - -/** - * Зашифровать строку (мнемонику) → base64 blob. - * Используется при создании коша. - */ -export function encryptMnemonic(plaintext: string): string { - if (!masterKey) { - throw new Error('Crypto service not ready'); - } - if (typeof plaintext !== 'string' || plaintext.length === 0) { - throw new Error('encryptMnemonic: plaintext must be non-empty string'); - } - - const iv = randomBytes(IV_LEN); - const cipher = createCipheriv('aes-256-gcm', masterKey, iv); - const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); - const tag = cipher.getAuthTag(); - - return Buffer.concat([iv, ct, tag]).toString('base64'); -} - -/** - * Расшифровать base64 blob → исходная строка. - * Используется при send + reveal. - */ -export function decryptMnemonic(blob: string): string { - if (!masterKey) { - throw new Error('Crypto service not ready'); - } - if (typeof blob !== 'string' || blob.length === 0) { - throw new Error('decryptMnemonic: blob must be non-empty string'); - } - - const buf = Buffer.from(blob, 'base64'); - if (buf.length < IV_LEN + TAG_LEN + 1) { - throw new Error('decryptMnemonic: blob too short'); - } - - const iv = buf.subarray(0, IV_LEN); - const tag = buf.subarray(buf.length - TAG_LEN); - const ct = buf.subarray(IV_LEN, buf.length - TAG_LEN); - - const decipher = createDecipheriv('aes-256-gcm', masterKey, iv); - decipher.setAuthTag(tag); - - try { - return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8'); - } catch { - // Не пробрасываем оригинальную ошибку — она может содержать sensitive info - throw new Error('decryptMnemonic: authentication failed'); - } -} - -/** - * Сравнить два base64-blob'а constant-time (нужно для тестов / sanity). - */ -export function constantTimeEqual(a: string, b: string): boolean { - const ba = Buffer.from(a); - const bb = Buffer.from(b); - if (ba.length !== bb.length) return false; - return timingSafeEqual(ba, bb); -} diff --git a/apps/api/src/services/key-rotation.service.ts b/apps/api/src/services/key-rotation.service.ts index 1e36277..305e652 100644 --- a/apps/api/src/services/key-rotation.service.ts +++ b/apps/api/src/services/key-rotation.service.ts @@ -2,7 +2,6 @@ import { env, getVaultToken } from '../config/env'; import { vaultAppRoleLogin } from '../config/vault'; import { fetchJwtKeysFromVault, swapKeyMap, getKeyMapSize } from './jwt.service'; import { fetchCsrfConfig, swapCsrfConfig } from './csrf.service'; -import { fetchMasterKey, swapMasterKey, masterKeyMatches, isCryptoReady } from './crypto.service'; import { logger } from '../lib/logger'; const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour @@ -15,7 +14,7 @@ let currentVaultToken: string | null = null; * При любой ошибке оставляем старые значения в памяти, сервис продолжает работать. */ export async function refreshAllKeys(): Promise { - const { addr, roleId, secretId, mount, jwtKidPath, jwtKidsPrefix, csrfPath, cryptoKeyPath } = env.vault; + const { addr, roleId, secretId, mount, jwtKidPath, jwtKidsPrefix, csrfPath } = env.vault; if (!addr || !roleId || !secretId) { logger.warn('Vault not configured, skipping key refresh'); @@ -34,14 +33,11 @@ export async function refreshAllKeys(): Promise { currentVaultToken = fresh; } - // ── Pre-fetch всех секретов параллельно (НЕ мутируя глобал) ─────────── + // ── Pre-fetch обоих секретов параллельно (НЕ мутируя глобал) ─────────── const jwtPromise = fetchJwtKeysFromVault(addr, token, mount, jwtKidPath, jwtKidsPrefix); const csrfPromise = csrfPath ? fetchCsrfConfig(addr, token, mount, csrfPath) : Promise.resolve(null); - // Master-key: первая загрузка обязательна (custodial без него работать не может), - // последующие тики толерантны (если упало — оставляем старый ключ). - const cryptoPromise = cryptoKeyPath ? fetchMasterKey(addr, token, mount, cryptoKeyPath) : Promise.resolve(null); - const [jwtResult, csrfResult, cryptoResult] = await Promise.allSettled([jwtPromise, csrfPromise, cryptoPromise]); + const [jwtResult, csrfResult] = await Promise.allSettled([jwtPromise, csrfPromise]); // ── Атомарность: если хоть один обязательный fetch упал — НИЧЕГО не меняем ── if (jwtResult.status === 'rejected') { @@ -52,36 +48,16 @@ export async function refreshAllKeys(): Promise { logger.error(`Key refresh ABORTED — CSRF fetch failed: ${csrfResult.reason?.message || csrfResult.reason}`); return; } - // Master-key: если он ещё не загружен — это критическая ошибка (отказ при первом запуске). - // Если уже был — оставляем старый (ротация ключа = ломает всю расшифровку, не делаем on rotation). - if (cryptoKeyPath && !isCryptoReady() && cryptoResult.status === 'rejected') { - logger.error(`Key refresh ABORTED — Crypto master key fetch failed: ${cryptoResult.reason?.message || cryptoResult.reason}`); - return; - } // ── Atomic swap (синхронные операции, нельзя прервать) ────────────────── swapKeyMap(jwtResult.value); if (csrfResult.status === 'fulfilled' && csrfResult.value) { swapCsrfConfig(csrfResult.value); } - // Master-key загружаем ТОЛЬКО при первой инициализации (потом не ротируем — иначе сломаем расшифровку). - // Если в Vault положили НОВЫЙ ключ — WARN-log, операторская ошибка. - if (cryptoResult.status === 'fulfilled' && cryptoResult.value) { - if (!isCryptoReady()) { - swapMasterKey(cryptoResult.value); - logger.info('Crypto master key loaded'); - } else if (!masterKeyMatches(cryptoResult.value)) { - logger.warn( - 'Vault crypto/master key DIFFERS from in-memory key. Service continues with old key. ' + - 'Rotating master-key bricks all existing encrypted_mnemonic — revert Vault or plan re-encryption migration.' - ); - } - } logger.info( `Keys refreshed atomically: JWT keys=${getKeyMapSize()}` + - (csrfPath ? `, CSRF=${csrfResult.status === 'fulfilled' ? 'updated' : 'unchanged'}` : '') + - `, Crypto=${isCryptoReady() ? 'ready' : 'NOT-READY'}` + (csrfPath ? `, CSRF=${csrfResult.status === 'fulfilled' ? 'updated' : 'unchanged'}` : '') ); } diff --git a/apps/api/src/services/wallet-generator.service.ts b/apps/api/src/services/wallet-generator.service.ts deleted file mode 100644 index 1612678..0000000 --- a/apps/api/src/services/wallet-generator.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Wallet generation: BIP39 mnemonic + multi-chain address derivation. - * Server-side для custodial-флоу. - * - * Поддерживаемые chains (BIP44): - * ETH/BSC — m/44'/60'/0'/0/0 (secp256k1, EIP-55 checksum) - * BTC — m/84'/0'/0'/0/0 (P2WPKH bech32) - * TRX — m/44'/195'/0'/0/0 (secp256k1, base58check + prefix 0x41) - * SOL — m/44'/501'/0'/0' (ed25519) - */ - -import { ethers } from 'ethers'; -import { createHash } from 'crypto'; -import bs58 from 'bs58'; -import * as bip39 from 'bip39'; -import { BIP32Factory } from 'bip32'; -import * as ecc from 'tiny-secp256k1'; -import * as bitcoin from 'bitcoinjs-lib'; -import { Keypair } from '@solana/web3.js'; -import { derivePath } from 'ed25519-hd-key'; -import type { ChainCode } from '../lib/address-validators'; - -const bip32 = BIP32Factory(ecc); - -export const DERIVATION_PATHS: Record = { - ETH: "m/44'/60'/0'/0/0", - BSC: "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'", -}; - -export const ALL_CHAINS: ChainCode[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL']; - -export interface DerivedWallet { - chain: ChainCode; - address: string; - derivationPath: string; -} - -/** - * Сгенерить 12-словную BIP39 мнемонику. - */ -export function generateMnemonic(): string { - return bip39.generateMnemonic(128); -} - -/** - * Валидация существующей mnemonic (не используется в текущем флоу — оставлено на будущее). - */ -export function validateMnemonic(m: string): boolean { - return bip39.validateMnemonic(m); -} - -/** - * Деривить адреса для всех chains из одной mnemonic. - */ -export async function deriveAllAddresses(mnemonic: string): Promise { - if (!bip39.validateMnemonic(mnemonic)) { - throw new Error('Invalid mnemonic'); - } - - const seed = await bip39.mnemonicToSeed(mnemonic); - const seedHex = seed.toString('hex'); - - // ETH (BSC использует тот же адрес) - const ethWallet = ethers.Wallet.fromMnemonic(mnemonic, DERIVATION_PATHS.ETH); - const ethAddress = ethers.utils.getAddress(ethWallet.address); // EIP-55 checksum - - // BTC (P2WPKH bech32) - const btcRoot = bip32.fromSeed(seed); - const btcChild = btcRoot.derivePath(DERIVATION_PATHS.BTC); - if (!btcChild.publicKey) { - throw new Error('BTC derivation failed: no public key'); - } - const btcPayment = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(btcChild.publicKey), - network: bitcoin.networks.bitcoin, - }); - if (!btcPayment.address) { - throw new Error('BTC derivation failed: no address'); - } - - // TRX (derive privkey same curve as ETH, convert pubkey → TRX base58check address) - const trxWallet = ethers.Wallet.fromMnemonic(mnemonic, DERIVATION_PATHS.TRX); - const trxAddress = ethAddressToTron(trxWallet.address); - - // SOL (ed25519 derivation) - const { key: solKey } = derivePath(DERIVATION_PATHS.SOL, seedHex); - if (!solKey || solKey.length !== 32) { - throw new Error('SOL derivation produced invalid seed length'); - } - const solKeypair = Keypair.fromSeed(solKey); - const solAddress = solKeypair.publicKey.toBase58(); - - return [ - { chain: 'ETH', address: ethAddress, derivationPath: DERIVATION_PATHS.ETH }, - { chain: 'BSC', address: ethAddress, derivationPath: DERIVATION_PATHS.BSC }, - { chain: 'BTC', address: btcPayment.address, derivationPath: DERIVATION_PATHS.BTC }, - { chain: 'TRX', address: trxAddress, derivationPath: DERIVATION_PATHS.TRX }, - { chain: 'SOL', address: solAddress, derivationPath: DERIVATION_PATHS.SOL }, - ]; -} - -/** - * ETH-style address (0x...) → TRX base58check (T...). - * TRX и ETH используют одну curve и одну keccak256-логику для получения 20-байтного хеша. - * Различие только в префиксе (0x41 vs ничего) и в кодировке (base58check vs hex). - */ -export function ethAddressToTron(ethAddr: string): string { - const hex = ethAddr.toLowerCase().replace(/^0x/, ''); - if (hex.length !== 40) { - throw new Error('ethAddressToTron: invalid input length'); - } - const bytes = Buffer.from(hex, 'hex'); - const prefixed = Buffer.concat([Buffer.from([0x41]), bytes]); // 21 байт - const h1 = createHash('sha256').update(prefixed).digest(); - const h2 = createHash('sha256').update(h1).digest(); - const checksum = h2.subarray(0, 4); - return bs58.encode(new Uint8Array(Buffer.concat([prefixed, checksum]))); -} diff --git a/apps/api/src/services/wallet-signer.service.ts b/apps/api/src/services/wallet-signer.service.ts deleted file mode 100644 index 1f82740..0000000 --- a/apps/api/src/services/wallet-signer.service.ts +++ /dev/null @@ -1,442 +0,0 @@ -/** - * Server-side signing + broadcasting для custodial flow. - * Caller передаёт расшифрованную mnemonic, мы деривим privkey, подписываем, broadcast'им. - * - * Никогда не логируем mnemonic / privkey / signed tx hex. - */ - -import { ethers } from 'ethers'; -import { createHash } from 'crypto'; -import * as bip39 from 'bip39'; -import { BIP32Factory } from 'bip32'; -import * as ecc from 'tiny-secp256k1'; -import * as bitcoin from 'bitcoinjs-lib'; -import { Keypair, Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; -import { derivePath } from 'ed25519-hd-key'; -import { env } from '../config/env'; -import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service'; -import type { ChainCode } from '../lib/address-validators'; - -const bip32 = BIP32Factory(ecc); - -const ETH_RPC = 'https://ethereum-rpc.publicnode.com'; -const BSC_RPC = 'https://bsc-dataseed.binance.org'; -const SOL_RPC = 'https://api.mainnet-beta.solana.com'; -const TRONGRID = 'https://api.trongrid.io'; -const BLOCKSTREAM = 'https://blockstream.info/api'; - -const USDT_TRC20 = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; -const USDT_BEP20 = '0x55d398326f99059fF775485246999027B3197955'; -const USDT_ERC20 = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; - -const ERC20_ABI = [ - 'function transfer(address to, uint256 amount) returns (bool)', -]; - -const HTTP_TIMEOUT_MS = 20_000; - -export interface SendParams { - chain: ChainCode; - mnemonic: string; - to: string; - amount: string; // smallest units (wei / sat / sun / lamport) - token?: string; - /** - * Адрес из БД (wallets.address) для текущего юзера+chain. - * Signer верифицирует: derived(mnemonic, path) === expectedFromAddress. - * Если нет — отказ от подписи. Защита от случайной смены DERIVATION_PATHS - * или подмены mnemonic в БД (например в результате backup-восстановления). - */ - expectedFromAddress: string; -} - -export async function signAndBroadcast(p: SendParams): Promise<{ txid: string }> { - switch (p.chain) { - case 'ETH': return sendEvm(p, ETH_RPC, 1, USDT_ERC20); - case 'BSC': return sendEvm(p, BSC_RPC, 56, USDT_BEP20); - case 'BTC': return sendBtc(p); - case 'TRX': return sendTrx(p); - case 'SOL': return sendSol(p); - } -} - -function assertAddressMatch(derived: string, expected: string, chain: ChainCode): void { - // EVM адреса case-insensitive (EIP-55 — только display) - const norm = (s: string) => - chain === 'ETH' || chain === 'BSC' ? s.toLowerCase() : s; - if (norm(derived) !== norm(expected)) { - throw new Error(`Derived ${chain} address ${derived} does not match stored ${expected}`); - } -} - -// ─── EVM (ETH / BSC) ─── - -// Жёсткий cap на gas price — защита от fee-storm. ETH historically peaks at ~500 gwei, -// нормальный диапазон 5-50 gwei. BSC ~3-10 gwei. -const MAX_GAS_PRICE_GWEI = 500; - -async function sendEvm(p: SendParams, rpc: string, chainId: number, usdtAddr: string): Promise<{ txid: string }> { - const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH); - assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain); - const provider = new ethers.providers.StaticJsonRpcProvider(rpc, chainId); - const signer = wallet.connect(provider); - - // 1) Fee cap — fetch feeData и режем по верхней границе - const feeData = await provider.getFeeData(); - const capWei = ethers.utils.parseUnits(String(MAX_GAS_PRICE_GWEI), 'gwei'); - const effectiveGasPrice = feeData.maxFeePerGas ?? feeData.gasPrice; - if (!effectiveGasPrice || effectiveGasPrice.gt(capWei)) { - throw new Error(`Gas price exceeds cap (${MAX_GAS_PRICE_GWEI} gwei)`); - } - - // 2) Явный nonce — fail loud если provider лажает - const nonce = await provider.getTransactionCount(wallet.address, 'pending'); - - // 3) Fee fields для tx — закрепляем cap, чтобы ethers не сходил за свежими ценами - // во время broadcast (TOCTOU). - const isEip1559 = !!feeData.maxFeePerGas; - const feeFields: Partial = isEip1559 - ? { - type: 2, - maxFeePerGas: feeData.maxFeePerGas!, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? ethers.BigNumber.from(0), - } - : { gasPrice: effectiveGasPrice }; - - let tx: ethers.providers.TransactionRequest; - if (!p.token) { - // Native: pre-check balance >= value + gas estimate - const value = ethers.BigNumber.from(p.amount); - const balance = await provider.getBalance(wallet.address); - const estGas = ethers.BigNumber.from(21000); // simple native transfer - const totalNeeded = value.add(effectiveGasPrice.mul(estGas)); - if (balance.lt(totalNeeded)) { - throw new Error('Insufficient balance (value + gas)'); - } - tx = { to: p.to, value, chainId, nonce, gasLimit: estGas, ...feeFields }; - } else if (p.token.toUpperCase() === 'USDT') { - // ERC20: pre-check token balance + native gas balance - const iface = new ethers.utils.Interface([ - ...ERC20_ABI, - 'function balanceOf(address) view returns (uint256)', - ]); - const erc20 = new ethers.Contract(usdtAddr, iface, provider); - const tokenBal: ethers.BigNumber = await erc20.balanceOf(wallet.address); - if (tokenBal.lt(ethers.BigNumber.from(p.amount))) { - throw new Error('Insufficient token balance'); - } - const nativeBal = await provider.getBalance(wallet.address); - const estGas = ethers.BigNumber.from(80000); // ERC20 transfer ~50-65k, запас - if (nativeBal.lt(effectiveGasPrice.mul(estGas))) { - throw new Error('Insufficient native balance for gas'); - } - - const data = iface.encodeFunctionData('transfer', [p.to, p.amount]); - tx = { to: usdtAddr, data, value: 0, chainId, nonce, gasLimit: estGas, ...feeFields }; - } else { - throw new Error(`Token ${p.token} not supported on chainId ${chainId}`); - } - - const sent = await signer.sendTransaction(tx); - return { txid: sent.hash }; -} - -// ─── SOLANA ─── - -async function sendSol(p: SendParams): Promise<{ txid: string }> { - if (p.token) { - throw new Error('SOL SPL-token signing не реализовано (только native SOL)'); - } - - const seed = await bip39.mnemonicToSeed(p.mnemonic); - const { key } = derivePath(DERIVATION_PATHS.SOL, seed.toString('hex')); - if (!key || key.length !== 32) { - throw new Error('SOL derivation produced invalid seed length'); - } - const keypair = Keypair.fromSeed(key); - assertAddressMatch(keypair.publicKey.toBase58(), p.expectedFromAddress, 'SOL'); - - const conn = new Connection(SOL_RPC, 'confirmed'); - const toPk = new PublicKey(p.to); - const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash(); - - const tx = new Transaction({ - feePayer: keypair.publicKey, - blockhash, - lastValidBlockHeight, - }); - tx.add( - SystemProgram.transfer({ - fromPubkey: keypair.publicKey, - toPubkey: toPk, - lamports: BigInt(p.amount), - }), - ); - tx.sign(keypair); - - const sig = await conn.sendRawTransaction(tx.serialize()); - - // Wait for confirmation — иначе sendRawTransaction только подтверждает что leader увидел. - // Solana дропает 5-15% unconfirmed во время congestion. - try { - await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed'); - } catch (err: any) { - // Tx уже broadcastнут — может ещё пройти. Audit-log в caller'е покажет txid для reconciliation. - throw new Error(`SOL tx submitted but confirmation timed out (sig=${sig}): ${err.message}`); - } - - return { txid: sig }; -} - -// ─── BITCOIN (P2WPKH bech32) ─── - -async function sendBtc(p: SendParams): Promise<{ txid: string }> { - if (p.token) throw new Error('BTC tokens не поддерживаются'); - - const seed = await bip39.mnemonicToSeed(p.mnemonic); - const root = bip32.fromSeed(seed); - const child = root.derivePath(DERIVATION_PATHS.BTC); - if (!child.publicKey) throw new Error('BTC derivation failed'); - - const network = bitcoin.networks.bitcoin; - const pubkeyBuf = Buffer.from(child.publicKey); - const payment = bitcoin.payments.p2wpkh({ pubkey: pubkeyBuf, network }); - if (!payment.address || !payment.output) throw new Error('BTC payment build failed'); - - const fromAddr = payment.address; - assertAddressMatch(fromAddr, p.expectedFromAddress, 'BTC'); - - // Fetch UTXOs + fee estimate - const [utxosRes, feesRes] = await Promise.all([ - fetchJson(`${BLOCKSTREAM}/address/${fromAddr}/utxo`), - fetchJson(`${BLOCKSTREAM}/fee-estimates`), - ]); - const utxos = ((utxosRes as any[]) || []).filter((u) => u.status?.confirmed); - // Fee fallback приоритеты: 1 блок > 3 блока > 6 блоков > 15 sat/vB (защита от - // отказа broadcast по min-relay-fee на загруженном mempool). - const feeMap = feesRes as Record; - const feeRate = Math.ceil(feeMap['1'] ?? feeMap['3'] ?? feeMap['6'] ?? 15); - - const amountSat = BigInt(p.amount); - if (amountSat > BigInt(Number.MAX_SAFE_INTEGER)) { - throw new Error('BTC amount exceeds safe integer range'); - } - - // Сортируем UTXO по убыванию value — greedy выбор - utxos.sort((a, b) => b.value - a.value); - - const psbt = new bitcoin.Psbt({ network }); - let totalIn = 0n; - - // Оценка fee для P2WPKH: input ≈ 68 vB, output ≈ 31 vB, overhead ≈ 11 vB. - // * 1.1 safety multiplier — защита от незначительных изменений mempool fee - // между fetch и broadcast. - const feeFor = (ins: number, outs: number) => - BigInt(Math.ceil((ins * 68 + outs * 31 + 11) * feeRate * 1.1)); - - const selectedUtxos: typeof utxos = []; - for (const u of utxos) { - selectedUtxos.push(u); - totalIn += BigInt(u.value); - if (totalIn >= amountSat + feeFor(selectedUtxos.length, 2)) break; - } - - if (totalIn < amountSat + feeFor(selectedUtxos.length, 2)) { - throw new Error('Insufficient BTC balance (incl. fee)'); - } - - for (const u of selectedUtxos) { - psbt.addInput({ - hash: u.txid, - index: u.vout, - witnessUtxo: { script: payment.output, value: u.value }, - }); - } - - psbt.addOutput({ address: p.to, value: Number(amountSat) }); - - const fee = feeFor(selectedUtxos.length, 2); - const change = totalIn - amountSat - fee; - // P2WPKH dust threshold = 294 sat (vs 546 для legacy P2PKH). - // Если change < dust — донатим miner'у как extra fee. - if (change > 294n) { - psbt.addOutput({ address: fromAddr, value: Number(change) }); - } - - for (let i = 0; i < selectedUtxos.length; i++) { - psbt.signInput(i, { - publicKey: pubkeyBuf, - sign: (hash: Buffer) => Buffer.from(child.sign(hash)), - }); - } - psbt.finalizeAllInputs(); - - const txHex = psbt.extractTransaction().toHex(); - - // Broadcast с явным timeout + content-type (иначе fetch может зависнуть навечно) - const broadcastController = new AbortController(); - const tBroadcast = setTimeout(() => broadcastController.abort(), HTTP_TIMEOUT_MS); - let broadcast: Response; - try { - broadcast = await fetch(`${BLOCKSTREAM}/tx`, { - method: 'POST', - body: txHex, - headers: { 'Content-Type': 'text/plain' }, - signal: broadcastController.signal, - }); - } finally { - clearTimeout(tBroadcast); - } - if (!broadcast.ok) { - const body = await broadcast.text().catch(() => ''); - throw new Error(`BTC broadcast failed (${broadcast.status}): ${body.slice(0, 200)}`); - } - const txid = (await broadcast.text()).trim(); - return { txid }; -} - -// ─── TRON ─── - -async function sendTrx(p: SendParams): Promise<{ txid: string }> { - const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.TRX); - const fromTronAddr = ethAddressToTron(wallet.address); - assertAddressMatch(fromTronAddr, p.expectedFromAddress, 'TRX'); - - const headers: Record = { 'Content-Type': 'application/json' }; - if (env.tronApiKey) headers['TRON-PRO-API-KEY'] = env.tronApiKey; - - let txBody: any; - if (!p.token) { - const built = await fetchJson(`${TRONGRID}/wallet/createtransaction`, { - method: 'POST', - headers, - body: JSON.stringify({ - owner_address: fromTronAddr, - to_address: p.to, - amount: Number(p.amount), - visible: true, - }), - }); - txBody = built; - } else if (p.token.toUpperCase() === 'USDT') { - const param = - tronAddressToHex(p.to).padStart(64, '0') + - BigInt(p.amount).toString(16).padStart(64, '0'); - const built = await fetchJson(`${TRONGRID}/wallet/triggersmartcontract`, { - method: 'POST', - headers, - body: JSON.stringify({ - owner_address: fromTronAddr, - contract_address: USDT_TRC20, - function_selector: 'transfer(address,uint256)', - parameter: param, - fee_limit: 100_000_000, - call_value: 0, - visible: true, - }), - }); - txBody = built.transaction; - } else { - throw new Error(`Token ${p.token} not supported on TRX`); - } - - if (!txBody?.txID || !txBody?.raw_data_hex || !txBody?.raw_data) { - throw new Error('TRX tx build failed (incomplete response)'); - } - - // ── ВЕРИФИКАЦИЯ против скомпрометированного RPC / MITM ──────────────────── - // 1. Recompute txID локально: SHA256(raw_data_hex) должен совпасть с тем что прислал RPC. - // Если не совпало — RPC лжёт о txID и мог подсунуть raw_data, дренирующее на attacker. - const expectedTxId = createHash('sha256') - .update(Buffer.from(txBody.raw_data_hex, 'hex')) - .digest('hex'); - if (expectedTxId !== txBody.txID) { - throw new Error('TRX txID mismatch — possible MITM/compromised RPC'); - } - - // 2. Verify что raw_data действительно содержит наш intent (to_address + amount) - const contractValue = txBody.raw_data?.contract?.[0]?.parameter?.value; - if (!contractValue) { - throw new Error('TRX tx malformed (no contract value)'); - } - if (!p.token) { - // Native TRX: visible=true → to_address это base58 строка - if (contractValue.to_address !== p.to) { - throw new Error(`TRX to_address mismatch: expected ${p.to}, got ${contractValue.to_address}`); - } - if (String(contractValue.amount) !== String(p.amount)) { - throw new Error(`TRX amount mismatch: expected ${p.amount}, got ${contractValue.amount}`); - } - } else { - // TRC20: contract_address и parameter (encoded to+amount). Проверяем что contract правильный. - if (contractValue.contract_address !== USDT_TRC20) { - throw new Error(`TRX contract mismatch: expected ${USDT_TRC20}, got ${contractValue.contract_address}`); - } - // Decode parameter: первые 32 байта = to (TRX-hex prefixed by 0x41 padded), вторые = amount - const data = String(contractValue.data || ''); - if (data.length !== 128 + 8) { - // method id (8 hex chars) + 2 * 32 bytes (64 hex chars each) - throw new Error('TRX trc20 data length wrong'); - } - const expectedParam = - tronAddressToHex(p.to).padStart(64, '0') + - BigInt(p.amount).toString(16).padStart(64, '0'); - const actualParam = data.slice(8); // strip method id - if (actualParam.toLowerCase() !== expectedParam.toLowerCase()) { - throw new Error('TRX trc20 parameter mismatch (to/amount tampering)'); - } - } - - // Подпись txID (теперь верифицированного локально) - const sk = new ethers.utils.SigningKey(wallet.privateKey); - const sig = sk.signDigest('0x' + txBody.txID); - const sigHex = - sig.r.slice(2) + - sig.s.slice(2) + - (sig.recoveryParam ?? 0).toString(16).padStart(2, '0'); - - txBody.signature = [sigHex]; - - const broadcast = await fetchJson(`${TRONGRID}/wallet/broadcasttransaction`, { - method: 'POST', - headers, - body: JSON.stringify(txBody), - }); - - if (!broadcast?.result) { - const msg = (broadcast?.message && Buffer.from(broadcast.message, 'hex').toString()) || 'unknown'; - throw new Error(`TRX broadcast failed: ${msg.slice(0, 200)}`); - } - - return { txid: txBody.txID }; -} - -// ─── HELPERS ─── - -const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - -function tronAddressToHex(address: string): string { - let num = 0n; - for (const ch of address) { - const i = BASE58_ALPHABET.indexOf(ch); - if (i === -1) throw new Error('Invalid base58 character in TRON address'); - num = num * 58n + BigInt(i); - } - const hex = num.toString(16).padStart(50, '0'); - return hex.slice(2, 42); // strip 0x41 prefix + checksum bytes -} - -async function fetchJson(url: string, init?: RequestInit): Promise { - const controller = new AbortController(); - const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS); - try { - const res = await fetch(url, { ...init, signal: controller.signal }); - if (!res.ok) { - const body = await res.text().catch(() => ''); - throw new Error(`Upstream ${res.status}: ${body.slice(0, 200)}`); - } - return await res.json(); - } finally { - clearTimeout(t); - } -} diff --git a/apps/api/swagger.json b/apps/api/swagger.json index 04f3143..6be3a42 100644 --- a/apps/api/swagger.json +++ b/apps/api/swagger.json @@ -2,8 +2,8 @@ "openapi": "3.0.0", "info": { "title": "CryptoWallet API", - "version": "3.0.0", - "description": "Multi-chain crypto wallet API. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK). CUSTODIAL: server генерит мнемонику, хранит её AES-GCM-зашифрованной (master-key из Vault) и сам подписывает транзакции." + "version": "4.0.0", + "description": "Multi-chain crypto wallet API (non-custodial). Клиент сам деривит mnemonic и шлёт публичные адреса; сервер хранит только адреса и строит unsigned tx для отправки. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK)." }, "servers": [ { "url": "/api", "description": "API root" } @@ -52,31 +52,25 @@ "properties": { "chain": { "$ref": "#/components/schemas/Chain" }, "address": { "type": "string" }, - "derivationPath": { "type": "string" } + "derivationPath": { "type": "string", "description": "BIP32 path, например m/44'/60'/0'/0/0" } } }, - "MnemonicResponse": { + "WalletInput": { "type": "object", + "required": ["chain", "address", "derivationPath"], "properties": { - "success": { "type": "boolean", "example": true }, - "data": { - "type": "object", - "properties": { - "mnemonic": { "type": "string", "description": "BIP39 mnemonic (12 words)" } - } - } + "chain": { "$ref": "#/components/schemas/Chain" }, + "address": { "type": "string", "maxLength": 64, "description": "Публичный адрес (chain-specific checksum-валидируется)" }, + "derivationPath": { "type": "string", "maxLength": 64, "description": "BIP32 m/.. (например m/44'/60'/0'/0/0)" } } }, - "TxBroadcastResponse": { + "CreateWalletsRequest": { "type": "object", + "required": ["wallets"], "properties": { - "success": { "type": "boolean", "example": true }, - "data": { - "type": "object", - "properties": { - "txid": { "type": "string", "description": "Идентификатор отправленной транзакции" }, - "chain": { "$ref": "#/components/schemas/Chain" } - } + "wallets": { + "type": "array", "minItems": 1, "maxItems": 20, + "items": { "$ref": "#/components/schemas/WalletInput" } } } }, @@ -140,7 +134,7 @@ "success": { "type": "boolean" }, "data": { "type": "object", - "description": "Unsigned tx — формат зависит от chain (kind: btc | tron | evm | solana). Клиент подписывает приватом и broadcast'ит через соответствующий /api/{btc,tron}/broadcast endpoint" + "description": "Unsigned tx — формат зависит от chain (kind: btc | tron | evm | solana). Клиент подписывает приватом и broadcast'ит через соответствующий /api/{btc,tron}/broadcast endpoint или RPC своей цепи." } } } @@ -173,44 +167,17 @@ "/wallets/create": { "post": { - "summary": "Создать custodial-кошелёк (mnemonic генерится на сервере)", - "description": "Сервер генерит BIP39 mnemonic (12 слов), деривит адреса для 5 chains, шифрует mnemonic AES-GCM (master-key из Vault) и сохраняет. **Возвращает ТОЛЬКО адреса** — mnemonic клиенту НЕ отдаётся. Чтобы потом увидеть seed — отдельный endpoint GET /wallets/mnemonic. Идемпотентность: 409 если у юзера уже есть коша.", - "tags": ["Wallets"], - "responses": { - "201": { "description": "Wallet created (returns addresses only)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } }, - "401": { "description": "Not authenticated" }, - "409": { "description": "Wallet already exists", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, - "503": { "description": "Crypto service not ready" } - } - } - }, - - "/wallets/mnemonic/reveal": { - "post": { - "summary": "Раскрыть mnemonic (settings-screen)", - "description": "Расшифровывает и возвращает 12-словную BIP39 мнемонику юзера. POST + CSRF + рукопожатие в body — защита от случайного XHR / image-tag CSRF / стороннего origin. Rate-limit 5/час. Каждый запрос пишется в audit-log.", + "summary": "Upsert wallets для авторизованного юзера", + "description": "Клиент сам генерит mnemonic и деривит публичные адреса (BIP44 для ETH/BSC/BTC/TRX/SOL). Тело — массив `{chain, address, derivationPath}`. На конфликт (user_id, chain) сервер обновляет address+derivationPath. Mnemonic клиенту не нужно слать — сервер её не хранит.", "tags": ["Wallets"], "requestBody": { "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["confirm"], - "properties": { - "confirm": { "type": "string", "enum": ["I_UNDERSTAND_SEED_IS_SECRET"] } - } - } - } - } + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWalletsRequest" } } } }, "responses": { - "200": { "description": "Mnemonic revealed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MnemonicResponse" } } } }, - "400": { "description": "Missing/invalid confirm token" }, - "401": { "description": "Not authenticated" }, - "404": { "description": "Wallet not created yet" }, - "429": { "description": "Rate limit (5/hour) exceeded" }, - "503": { "description": "Crypto service not ready" } + "201": { "description": "Created/updated (вернёт сохранённые адреса)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } }, + "400": { "description": "Invalid input (chain/address/derivationPath)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, + "401": { "description": "Not authenticated" } } } }, @@ -245,8 +212,8 @@ "/wallets/{chain}/send": { "post": { - "summary": "Custodial send: server signs + broadcasts", - "description": "Сервер расшифровывает мнемонику → деривит chain-privkey → подписывает → broadcast'ит через RPC. Возвращает txid.", + "summary": "Build unsigned send transaction (non-custodial)", + "description": "Возвращает unsigned tx. Клиент подписывает приватным ключом и broadcast'ит через /api/{btc,tron}/broadcast или RPC своей цепи.", "tags": ["Wallet Ops"], "parameters": [{ "name": "chain", "in": "path", "required": true, "schema": { "$ref": "#/components/schemas/Chain" } }], "requestBody": { @@ -254,16 +221,14 @@ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } } }, "responses": { - "200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } }, + "200": { "description": "Unsigned tx", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnsignedTxResponse" } } } }, "400": { "description": "Invalid input" }, - "404": { "description": "Wallet/mnemonic not found" }, - "502": { "description": "Broadcast failed (upstream RPC error / insufficient balance / unsupported token)" }, - "503": { "description": "Crypto service not ready" } + "404": { "description": "Wallet not found" }, + "502": { "description": "Upstream RPC error" } } } }, - "/btc/utxos/{address}": { "get": { "summary": "Confirmed UTXOs for Bitcoin address", diff --git a/cryptowallet-schema.sql b/cryptowallet-schema.sql index c87aa8b..6b7b61b 100644 --- a/cryptowallet-schema.sql +++ b/cryptowallet-schema.sql @@ -1,4 +1,4 @@ --- CryptoWallet API — DB schema (idempotent, custodial v3.0) +-- CryptoWallet API — DB schema (idempotent, non-custodial v4.0) CREATE TABLE IF NOT EXISTS users ( id VARCHAR(26) PRIMARY KEY, @@ -17,34 +17,13 @@ CREATE TABLE IF NOT EXISTS users ( kyc_verified BOOLEAN NOT NULL DEFAULT FALSE, kyc_verified_at TIMESTAMPTZ, is_deleted BOOLEAN NOT NULL DEFAULT FALSE, - encrypted_vault TEXT, - vault_salt VARCHAR(128), - encrypted_mnemonic TEXT, + encrypted_vault TEXT, -- legacy, unused + vault_salt VARCHAR(128), -- legacy, unused + encrypted_mnemonic TEXT, -- legacy, unused created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'users' AND column_name = 'encrypted_mnemonic' - ) THEN - ALTER TABLE users ADD COLUMN encrypted_mnemonic TEXT; - END IF; -END $$; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'users_encrypted_mnemonic_size' - ) THEN - ALTER TABLE users - ADD CONSTRAINT users_encrypted_mnemonic_size - CHECK (encrypted_mnemonic IS NULL OR (char_length(encrypted_mnemonic) BETWEEN 140 AND 512)); - END IF; -END $$; - CREATE TABLE IF NOT EXISTS wallets ( id VARCHAR(26) PRIMARY KEY, user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/docker-compose.yml b/docker-compose.yml index aa2a7e1..b1437f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,6 @@ services: - .env environment: API_PORT: "3001" - volumes: - - ./logs:/app/logs healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"] interval: 10s diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5948401..a390ec4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,6 @@ importers: '@solana/web3.js': specifier: ^1.98.4 version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) - bip32: - specifier: ^4.0.0 - version: 4.0.0 - bip39: - specifier: ^3.1.0 - version: 3.1.0 bitcoinjs-lib: specifier: ^6.1.5 version: 6.1.7 @@ -41,9 +35,6 @@ importers: dotenv: specifier: ^16.4.0 version: 16.6.1 - ed25519-hd-key: - specifier: ^1.3.0 - version: 1.3.0 ethers: specifier: 5.7.2 version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) @@ -68,9 +59,6 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.22.1) - tiny-secp256k1: - specifier: ^2.2.3 - version: 2.2.4 ulidx: specifier: ^2.4.1 version: 2.4.1 @@ -351,9 +339,6 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@scure/base@1.2.6': - resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} - '@solana/buffer-layout@4.0.1': resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} engines: {node: '>=5.10'} @@ -568,10 +553,6 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -601,13 +582,6 @@ packages: resolution: {integrity: sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==} engines: {node: '>=8.0.0'} - bip32@4.0.0: - resolution: {integrity: sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==} - engines: {node: '>=6.0.0'} - - bip39@3.1.0: - resolution: {integrity: sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==} - bitcoinjs-lib@6.1.7: resolution: {integrity: sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==} engines: {node: '>=8.0.0'} @@ -647,9 +621,6 @@ packages: bs58@6.0.0: resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - bs58check@2.1.2: - resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} - bs58check@3.0.1: resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} @@ -671,10 +642,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -695,10 +662,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - cipher-base@1.0.7: - resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} - engines: {node: '>= 0.10'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -745,19 +708,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.6: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} - create-hash@1.2.0: - resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} - - create-hmac@1.1.7: - resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -785,10 +739,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - delay@5.0.0: resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} engines: {node: '>=10'} @@ -824,9 +774,6 @@ packages: dynamic-dedupe@0.3.0: resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} - ed25519-hd-key@1.3.0: - resolution: {integrity: sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -973,10 +920,6 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1042,21 +985,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hash-base@3.1.2: - resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} - engines: {node: '>= 0.8'} - hash.js@1.1.7: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} @@ -1120,10 +1052,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -1144,16 +1072,6 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1244,9 +1162,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - md5.js@1.3.5: - resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -1431,10 +1346,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -1455,9 +1366,6 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1481,9 +1389,6 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1519,19 +1424,12 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - ripemd160@2.0.3: - resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} - engines: {node: '>= 0.8'} - rpc-websockets@9.3.8: resolution: {integrity: sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==} run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -1554,18 +1452,9 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.12: - resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} - engines: {node: '>= 0.10'} - hasBin: true - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1615,9 +1504,6 @@ packages: stream-json@1.9.1: resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1669,14 +1555,6 @@ packages: resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} engines: {node: '>=8'} - tiny-secp256k1@2.2.4: - resolution: {integrity: sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==} - engines: {node: '>=14.0.0'} - - to-buffer@1.2.2: - resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} - engines: {node: '>= 0.4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1763,9 +1641,6 @@ packages: resolution: {integrity: sha512-ERZf7pKOR155NKs/PZt1+83NrSEJfUL7+p9/TGZg/8xzDVMntXEFQlX4CsNJQTyu4h3j+dZYiQWOOlv5pssuHQ==} hasBin: true - tweetnacl@1.0.3: - resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1778,10 +1653,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - typeforce@1.18.0: resolution: {integrity: sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==} @@ -1790,10 +1661,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uint8array-tools@0.0.7: - resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==} - engines: {node: '>=14.0.0'} - ulidx@2.4.1: resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==} engines: {node: '>=16'} @@ -1812,9 +1679,6 @@ packages: resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} engines: {node: '>=6.14.2'} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -1844,18 +1708,11 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true - wif@2.0.6: - resolution: {integrity: sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2421,8 +2278,6 @@ snapshots: '@scarf/scarf@1.4.0': {} - '@scure/base@1.2.6': {} - '@solana/buffer-layout@4.0.1': dependencies: buffer: 6.0.3 @@ -2679,10 +2534,6 @@ snapshots: array-union@2.1.0: {} - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - balanced-match@1.0.2: {} base-x@3.0.11: @@ -2703,17 +2554,6 @@ snapshots: bip174@2.1.1: {} - bip32@4.0.0: - dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - typeforce: 1.18.0 - wif: 2.0.6 - - bip39@3.1.0: - dependencies: - '@noble/hashes': 1.8.0 - bitcoinjs-lib@6.1.7: dependencies: '@noble/hashes': 1.8.0 @@ -2777,12 +2617,6 @@ snapshots: dependencies: base-x: 5.0.1 - bs58check@2.1.2: - dependencies: - bs58: 4.0.1 - create-hash: 1.2.0 - safe-buffer: 5.2.1 - bs58check@3.0.1: dependencies: '@noble/hashes': 1.8.0 @@ -2807,13 +2641,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2840,12 +2667,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - cipher-base@1.0.7: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2879,30 +2700,11 @@ snapshots: cookie@0.7.2: {} - core-util-is@1.0.3: {} - cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 - create-hash@1.2.0: - dependencies: - cipher-base: 1.0.7 - inherits: 2.0.4 - md5.js: 1.3.5 - ripemd160: 2.0.3 - sha.js: 2.4.12 - - create-hmac@1.1.7: - dependencies: - cipher-base: 1.0.7 - create-hash: 1.2.0 - inherits: 2.0.4 - ripemd160: 2.0.3 - safe-buffer: 5.2.1 - sha.js: 2.4.12 - create-require@1.1.1: {} cross-spawn@7.0.6: @@ -2921,12 +2723,6 @@ snapshots: deep-is@0.1.4: {} - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - delay@5.0.0: {} depd@2.0.0: {} @@ -2955,11 +2751,6 @@ snapshots: dependencies: xtend: 4.0.2 - ed25519-hd-key@1.3.0: - dependencies: - create-hmac: 1.1.7 - tweetnacl: 1.0.3 - ee-first@1.1.1: {} elliptic@6.5.4: @@ -3210,10 +3001,6 @@ snapshots: flatted@3.4.2: {} - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - forwarded@0.2.0: {} fresh@0.5.2: {} @@ -3283,23 +3070,8 @@ snapshots: has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - has-symbols@1.1.0: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hash-base@3.1.2: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - hash.js@1.1.7: dependencies: inherits: 2.0.4 @@ -3361,8 +3133,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-callable@1.2.7: {} - is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -3377,14 +3147,6 @@ snapshots: is-path-inside@3.0.3: {} - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.20 - - isarray@1.0.0: {} - - isarray@2.0.5: {} - isexe@2.0.0: {} isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)): @@ -3469,12 +3231,6 @@ snapshots: math-intrinsics@1.1.0: {} - md5.js@1.3.5: - dependencies: - hash-base: 3.1.2 - inherits: 2.0.4 - safe-buffer: 5.2.1 - media-typer@0.3.0: {} merge-descriptors@1.0.3: {} @@ -3617,8 +3373,6 @@ snapshots: picomatch@2.3.1: {} - possible-typed-array-names@1.1.0: {} - postgres-array@2.0.0: {} postgres-bytea@1.0.1: {} @@ -3631,8 +3385,6 @@ snapshots: prelude-ls@1.2.1: {} - process-nextick-args@2.0.1: {} - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -3655,16 +3407,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -3693,11 +3435,6 @@ snapshots: dependencies: glob: 7.2.3 - ripemd160@2.0.3: - dependencies: - hash-base: 3.1.2 - inherits: 2.0.4 - rpc-websockets@9.3.8: dependencies: '@swc/helpers': 0.5.21 @@ -3715,8 +3452,6 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} - safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -3752,23 +3487,8 @@ snapshots: transitivePeerDependencies: - supports-color - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - setprototypeof@1.2.0: {} - sha.js@2.4.12: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3822,10 +3542,6 @@ snapshots: dependencies: stream-chain: 2.2.5 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3861,16 +3577,6 @@ snapshots: tildify@2.0.0: {} - tiny-secp256k1@2.2.4: - dependencies: - uint8array-tools: 0.0.7 - - to-buffer@1.2.2: - dependencies: - isarray: 2.0.5 - safe-buffer: 5.2.1 - typed-array-buffer: 1.0.3 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3957,8 +3663,6 @@ snapshots: turbo-windows-64: 2.8.15 turbo-windows-arm64: 2.8.15 - tweetnacl@1.0.3: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -3970,18 +3674,10 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - typeforce@1.18.0: {} typescript@5.9.3: {} - uint8array-tools@0.0.7: {} - ulidx@2.4.1: dependencies: layerr: 3.0.0 @@ -3999,8 +3695,6 @@ snapshots: node-gyp-build: 4.8.4 optional: true - util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} uuid@11.1.1: {} @@ -4022,24 +3716,10 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - which-typed-array@1.1.20: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which@2.0.2: dependencies: isexe: 2.0.0 - wif@2.0.6: - dependencies: - bs58check: 2.1.2 - word-wrap@1.2.5: {} wrappy@1.0.2: {}