security: remove .env from tracking (contains secrets)
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
node_modules/
|
**/node_modules
|
||||||
dist/
|
**/dist
|
||||||
.next/
|
**/.git
|
||||||
.env
|
**/.env
|
||||||
.env.local
|
**/.env.local
|
||||||
*.log
|
**/*.log
|
||||||
.turbo/
|
**/logs
|
||||||
.git/
|
**/.DS_Store
|
||||||
.gitea/
|
**/.vscode
|
||||||
coverage/
|
**/.idea
|
||||||
.DS_Store
|
|
||||||
docs/
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ VAULT_JWT_KIDS_PREFIX=jwt/kids
|
|||||||
# CSRF загружается если указан путь (оставь пустым чтобы отключить CSRF)
|
# CSRF загружается если указан путь (оставь пустым чтобы отключить CSRF)
|
||||||
VAULT_CSRF_PATH=
|
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 ────────────────────────────────────────────────────────────
|
# ── JWT ────────────────────────────────────────────────────────────
|
||||||
# Allowed: RS256 / RS384 / RS512 / ES256 / ES384 / ES512 / EdDSA / PS256 / PS384 / PS512
|
# Allowed: RS256 / RS384 / RS512 / ES256 / ES384 / ES512 / EdDSA / PS256 / PS384 / PS512
|
||||||
JWT_ALGORITHM=RS256
|
JWT_ALGORITHM=RS256
|
||||||
|
|||||||
53
Dockerfile
Normal file
53
Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Multi-stage build: base → deps → build → prod-deps → runtime
|
||||||
|
# Build context: deployserver/ root.
|
||||||
|
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate \
|
||||||
|
&& apk add --no-cache python3 make g++
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ── deps: install ВСЕ зависимости (включая dev) для сборки TS ───────────────
|
||||||
|
FROM base AS deps
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
|
COPY apps/api/package.json apps/api/
|
||||||
|
RUN pnpm install --frozen-lockfile --prod=false
|
||||||
|
|
||||||
|
# ── build: компилируем TypeScript ───────────────────────────────────────────
|
||||||
|
FROM base AS build
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN cd apps/api && pnpm build
|
||||||
|
|
||||||
|
# ── prod-deps: только production node_modules ──────────────────────────────
|
||||||
|
FROM base AS prod-deps
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||||
|
COPY apps/api/package.json apps/api/
|
||||||
|
RUN pnpm install --frozen-lockfile --prod
|
||||||
|
|
||||||
|
# ── runtime: минимальный образ для прода ───────────────────────────────────
|
||||||
|
FROM node:20-alpine AS runtime
|
||||||
|
RUN apk add --no-cache tini wget \
|
||||||
|
&& addgroup -S app -g 1001 \
|
||||||
|
&& adduser -S app -G app -u 1001
|
||||||
|
|
||||||
|
WORKDIR /app/apps/api
|
||||||
|
|
||||||
|
COPY --from=prod-deps --chown=app:app /app/node_modules /app/node_modules
|
||||||
|
COPY --from=prod-deps --chown=app:app /app/apps/api/node_modules ./node_modules
|
||||||
|
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
|
||||||
|
|
||||||
|
# logs/ создаётся для audit-log
|
||||||
|
RUN mkdir -p /app/logs && chown -R app:app /app/logs
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:3001/api/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
@@ -11,10 +11,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solana/web3.js": "^1.98.4",
|
"@solana/web3.js": "^1.98.4",
|
||||||
|
"bip32": "^4.0.0",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
|
"bitcoinjs-lib": "^6.1.5",
|
||||||
"bs58": "^6.0.0",
|
"bs58": "^6.0.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.0",
|
"dotenv": "^16.4.0",
|
||||||
|
"ed25519-hd-key": "^1.3.0",
|
||||||
"ethers": "5.7.2",
|
"ethers": "5.7.2",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"express-rate-limit": "^8.4.1",
|
"express-rate-limit": "^8.4.1",
|
||||||
@@ -23,6 +27,7 @@
|
|||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"tiny-secp256k1": "^2.2.3",
|
||||||
"ulidx": "^2.4.1"
|
"ulidx": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { swaggerSpec } from './config/swagger';
|
|||||||
import { traceMiddleware } from './middleware/trace';
|
import { traceMiddleware } from './middleware/trace';
|
||||||
import { authMiddleware } from './middleware/auth';
|
import { authMiddleware } from './middleware/auth';
|
||||||
import { csrfMiddleware } from './middleware/csrf';
|
import { csrfMiddleware } from './middleware/csrf';
|
||||||
import { globalLimiter, mutateLimiter, sensitiveLimiter } 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 walletRoutes from './routes/wallet.routes';
|
import walletRoutes from './routes/wallet.routes';
|
||||||
import vaultRoutes from './routes/vault.routes';
|
import vaultRoutes from './routes/vault.routes';
|
||||||
@@ -51,11 +51,13 @@ app.use('/api', globalLimiter);
|
|||||||
// ── PROTECTED endpoints (JWT + CSRF) ─────────────────────────────────────────
|
// ── PROTECTED endpoints (JWT + CSRF) ─────────────────────────────────────────
|
||||||
const protect = [authMiddleware, csrfMiddleware];
|
const protect = [authMiddleware, csrfMiddleware];
|
||||||
|
|
||||||
// Sensitive (send / vault) — самый строгий лимит
|
// Sensitive — самый строгий лимит. Каждый POST/PUT защищён 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);
|
app.use('/api/wallets/:chain/send', ...protect, sensitiveLimiter);
|
||||||
app.use('/api/vault', ...protect, sensitiveLimiter, vaultRoutes);
|
app.use('/api/vault', ...protect, sensitiveLimiter, vaultRoutes);
|
||||||
|
|
||||||
// Mutating (создание кошельков / broadcast / build) — повышенный лимит
|
// Mutating (proxy + read endpoints) — повышенный лимит
|
||||||
app.use('/api/wallets', ...protect, mutateLimiter, walletRoutes);
|
app.use('/api/wallets', ...protect, mutateLimiter, walletRoutes);
|
||||||
app.use('/api/relay', ...protect, mutateLimiter, relayProxyRoutes);
|
app.use('/api/relay', ...protect, mutateLimiter, relayProxyRoutes);
|
||||||
app.use('/api/tron', ...protect, mutateLimiter, tronProxyRoutes);
|
app.use('/api/tron', ...protect, mutateLimiter, tronProxyRoutes);
|
||||||
@@ -64,6 +66,11 @@ app.use('/api/tron/swap', ...protect, mutateLimiter, tronSwapProxyRoutes);
|
|||||||
app.use('/api/btc', ...protect, mutateLimiter, btcProxyRoutes);
|
app.use('/api/btc', ...protect, mutateLimiter, btcProxyRoutes);
|
||||||
app.use('/api/bsc/swap', ...protect, mutateLimiter, bscSwapProxyRoutes);
|
app.use('/api/bsc/swap', ...protect, mutateLimiter, bscSwapProxyRoutes);
|
||||||
|
|
||||||
|
// 404 для всего что не сматчилось выше — единый JSON-ответ, не express default text
|
||||||
|
app.use((_req, res) => {
|
||||||
|
res.status(404).json({ success: false, error: 'Not found' });
|
||||||
|
});
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ export let env = {
|
|||||||
},
|
},
|
||||||
jwt: {
|
jwt: {
|
||||||
algorithm: p.JWT_ALGORITHM || 'RS256',
|
algorithm: p.JWT_ALGORITHM || 'RS256',
|
||||||
issuer: p.JWT_ISSUER || 'auth-service',
|
// Намеренно без default — каждый деплой ЯВНО указывает iss/aud, иначе сервис
|
||||||
audience: p.JWT_AUDIENCE || 'elcsa',
|
// примет любой токен подписанный нашими ключами с любым iss/aud.
|
||||||
|
issuer: p.JWT_ISSUER || '',
|
||||||
|
audience: p.JWT_AUDIENCE || '',
|
||||||
},
|
},
|
||||||
vault: {
|
vault: {
|
||||||
addr: p.VAULT_ADDR || '',
|
addr: p.VAULT_ADDR || '',
|
||||||
@@ -29,6 +31,7 @@ export let env = {
|
|||||||
jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid',
|
jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid',
|
||||||
jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids',
|
jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids',
|
||||||
csrfPath: p.VAULT_CSRF_PATH || '',
|
csrfPath: p.VAULT_CSRF_PATH || '',
|
||||||
|
cryptoKeyPath: p.VAULT_CRYPTO_KEY_PATH || 'crypto/master',
|
||||||
},
|
},
|
||||||
cors: {
|
cors: {
|
||||||
// No default — каждый деплой явно указывает CORS_ORIGINS, иначе пустой массив = нет cross-origin доступа.
|
// No default — каждый деплой явно указывает CORS_ORIGINS, иначе пустой массив = нет cross-origin доступа.
|
||||||
@@ -52,6 +55,11 @@ export function getVaultToken(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initEnv(): Promise<void> {
|
export async function initEnv(): Promise<void> {
|
||||||
|
// Fail-fast на отсутствующие критические env vars
|
||||||
|
if (!env.jwt.issuer || !env.jwt.audience) {
|
||||||
|
throw new Error('JWT_ISSUER and JWT_AUDIENCE must be explicitly set (no defaults)');
|
||||||
|
}
|
||||||
|
|
||||||
const { addr, roleId, secretId, mount, secretPath } = env.vault;
|
const { addr, roleId, secretId, mount, secretPath } = env.vault;
|
||||||
|
|
||||||
if (!addr || !roleId || !secretId) {
|
if (!addr || !roleId || !secretId) {
|
||||||
@@ -106,4 +114,11 @@ export async function initEnv(): Promise<void> {
|
|||||||
tronApiKey: s('TRON_API_KEY') || env.tronApiKey,
|
tronApiKey: s('TRON_API_KEY') || env.tronApiKey,
|
||||||
jupiterApiKey: s('JUPITER_API_KEY') || env.jupiterApiKey,
|
jupiterApiKey: s('JUPITER_API_KEY') || env.jupiterApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Re-validate after Vault load. Vault мог переписать iss/aud — если они теперь пустые
|
||||||
|
// или невалидные, fail-fast.
|
||||||
|
if (!env.jwt.issuer || !env.jwt.audience) {
|
||||||
|
throw new Error('JWT_ISSUER and JWT_AUDIENCE became empty after Vault load');
|
||||||
|
}
|
||||||
|
logger.info(`JWT validation: iss="${env.jwt.issuer}", aud="${env.jwt.audience}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { UserModel } from '../models/user.model';
|
import { UserModel } from '../models/user.model';
|
||||||
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
const MAX_VAULT_SIZE = 8192; // base64 encrypted blob upper limit
|
const MAX_VAULT_SIZE = 8192; // base64 encrypted blob upper limit
|
||||||
const MAX_SALT_LEN = 128;
|
const MAX_SALT_LEN = 128;
|
||||||
@@ -29,7 +30,8 @@ export const VaultController = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
logger.error(`getVault failed for user ${userId}: ${err.stack || err.message}`);
|
||||||
|
res.status(500).json({ success: false, error: 'Internal error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -61,7 +63,8 @@ export const VaultController = {
|
|||||||
await UserModel.setVault(userId, encryptedVault, vaultSalt);
|
await UserModel.setVault(userId, encryptedVault, vaultSalt);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
logger.error(`putVault failed for user ${userId}: ${err.stack || err.message}`);
|
||||||
|
res.status(500).json({ success: false, error: 'Internal error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { db } from '../config/database';
|
||||||
import { WalletModel } from '../models/wallet.model';
|
import { WalletModel } from '../models/wallet.model';
|
||||||
import { UserModel } from '../models/user.model';
|
import { UserModel } from '../models/user.model';
|
||||||
import { getBalance, getTransactions, buildSend } from '../services/wallet-ops.service';
|
import { getBalance, getTransactions } from '../services/wallet-ops.service';
|
||||||
import { isValidAddress, isValidAmount, ChainCode } from '../lib/address-validators';
|
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<ChainCode>(['ETH', 'BTC', 'SOL', 'TRX', 'BSC']);
|
const ALLOWED_CHAINS = new Set<ChainCode>(ALL_CHAINS);
|
||||||
const MAX_WALLETS_PER_REQUEST = 20;
|
|
||||||
const MAX_DERIVATION_PATH = 64;
|
|
||||||
const MAX_TX_LIMIT = 100;
|
const MAX_TX_LIMIT = 100;
|
||||||
|
|
||||||
|
class ConflictError extends Error {
|
||||||
|
constructor() { super('Wallet already exists'); }
|
||||||
|
}
|
||||||
|
|
||||||
function isChain(value: unknown): value is ChainCode {
|
function isChain(value: unknown): value is ChainCode {
|
||||||
return typeof value === 'string' && ALLOWED_CHAINS.has(value as ChainCode);
|
return typeof value === 'string' && ALLOWED_CHAINS.has(value as ChainCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WalletController = {
|
export const WalletController = {
|
||||||
/**
|
/**
|
||||||
* GET /api/wallets — все кошельки юзера
|
* GET /api/wallets — все адреса юзера (chain + address + derivationPath).
|
||||||
*/
|
*/
|
||||||
async getWallets(req: Request, res: Response) {
|
async getWallets(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
@@ -28,76 +36,167 @@ export const WalletController = {
|
|||||||
derivationPath: w.derivation_path,
|
derivationPath: w.derivation_path,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
logger.error(`getWallets failed for user ${req.auth!.userId}: ${err.stack || err.message}`);
|
||||||
res.status(500).json({ success: false, error: 'Internal error' });
|
res.status(500).json({ success: false, error: 'Internal error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/wallets — upsert массива кошельков для юзера из JWT.
|
* 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 клиенту не отдаём.
|
||||||
*/
|
*/
|
||||||
async createWallets(req: Request, res: Response) {
|
async createWallet(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = req.auth!.userId;
|
||||||
const { wallets } = req.body ?? {};
|
|
||||||
|
|
||||||
if (!Array.isArray(wallets) || wallets.length === 0 || wallets.length > MAX_WALLETS_PER_REQUEST) {
|
if (!isCryptoReady()) {
|
||||||
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mnemonic: string | null = null;
|
||||||
|
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) },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: created.map((w) => ({
|
||||||
|
chain: w.chain,
|
||||||
|
address: w.address,
|
||||||
|
derivationPath: w.derivationPath,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} 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({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `wallets must be a non-empty array (max ${MAX_WALLETS_PER_REQUEST})`,
|
error: 'Missing or invalid confirm token. Send { "confirm": "I_UNDERSTAND_SEED_IS_SECRET" } in body.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ||
|
|
||||||
!/^m(\/[0-9]+'?)*$/.test(w.derivationPath)
|
|
||||||
) {
|
|
||||||
res.status(400).json({ success: false, error: 'Invalid derivationPath (BIP32 m/.. format expected)' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await UserModel.ensureExists(userId);
|
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 rows = await WalletModel.upsertMany(
|
const mnemonic = decryptMnemonic(blob);
|
||||||
wallets.map((w: { chain: ChainCode; address: string; derivationPath: string }) => ({
|
|
||||||
user_id: userId,
|
await auditLog({
|
||||||
chain: w.chain,
|
event: 'mnemonic.reveal',
|
||||||
address: w.address,
|
userId,
|
||||||
derivation_path: w.derivationPath,
|
ip: req.ip || null,
|
||||||
}))
|
result: 'success',
|
||||||
);
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: rows.map((w) => ({
|
|
||||||
chain: w.chain,
|
|
||||||
address: w.address,
|
|
||||||
derivationPath: w.derivation_path,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
res.status(500).json({ success: false, error: 'Internal error' });
|
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' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/wallets/:chain/balance — баланс для адреса юзера в данной chain.
|
* GET /api/wallets/:chain/balance
|
||||||
*/
|
*/
|
||||||
async getChainBalance(req: Request, res: Response) {
|
async getChainBalance(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = req.auth!.userId;
|
||||||
@@ -117,7 +216,8 @@ export const WalletController = {
|
|||||||
|
|
||||||
const balance = await getBalance(chain, wallet.address);
|
const balance = await getBalance(chain, wallet.address);
|
||||||
res.json({ success: true, data: balance });
|
res.json({ success: true, data: balance });
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
logger.error(`getChainBalance ${chain} failed for user ${userId}: ${err.stack || err.message}`);
|
||||||
res.status(502).json({ success: false, error: 'Upstream RPC error' });
|
res.status(502).json({ success: false, error: 'Upstream RPC error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,13 +245,16 @@ export const WalletController = {
|
|||||||
|
|
||||||
const txs = await getTransactions(chain, wallet.address, limit);
|
const txs = await getTransactions(chain, wallet.address, limit);
|
||||||
res.json({ success: true, data: txs });
|
res.json({ success: true, data: txs });
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
logger.error(`getChainTransactions ${chain} failed for user ${userId}: ${err.stack || err.message}`);
|
||||||
res.status(502).json({ success: false, error: 'Upstream RPC error' });
|
res.status(502).json({ success: false, error: 'Upstream RPC error' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/wallets/:chain/send — build unsigned транзакцию.
|
* POST /api/wallets/:chain/send — custodial sign + broadcast.
|
||||||
|
* Сервер расшифровывает мнемонику, деривит privkey, подписывает, broadcast'ит.
|
||||||
|
* Возвращает { txid }.
|
||||||
*/
|
*/
|
||||||
async sendFromChain(req: Request, res: Response) {
|
async sendFromChain(req: Request, res: Response) {
|
||||||
const userId = req.auth!.userId;
|
const userId = req.auth!.userId;
|
||||||
@@ -162,6 +265,11 @@ export const WalletController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isCryptoReady()) {
|
||||||
|
res.status(503).json({ success: false, error: 'Crypto service not ready' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { to, amount, token } = req.body ?? {};
|
const { to, amount, token } = req.body ?? {};
|
||||||
|
|
||||||
if (!isValidAddress(chain, String(to))) {
|
if (!isValidAddress(chain, String(to))) {
|
||||||
@@ -182,6 +290,7 @@ export const WalletController = {
|
|||||||
normalizedToken = token.toUpperCase();
|
normalizedToken = token.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mnemonic: string | null = null;
|
||||||
try {
|
try {
|
||||||
const wallet = await WalletModel.findByUserAndChain(userId, chain);
|
const wallet = await WalletModel.findByUserAndChain(userId, chain);
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
@@ -189,21 +298,51 @@ export const WalletController = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = await buildSend({
|
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({
|
||||||
chain,
|
chain,
|
||||||
from: wallet.address,
|
mnemonic,
|
||||||
to,
|
to: String(to),
|
||||||
amount,
|
amount: String(amount),
|
||||||
token: normalizedToken,
|
token: normalizedToken,
|
||||||
|
expectedFromAddress: wallet.address,
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, data: tx });
|
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 } });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Не возвращаем raw upstream message — может содержать sensitive info
|
logger.error(`send failed for user ${userId} chain ${chain}: ${err.message}`);
|
||||||
const safeMsg = err?.message?.toLowerCase().includes('not implemented')
|
await auditLog({
|
||||||
? 'Send not supported for this chain/token combination'
|
event: 'wallet.send',
|
||||||
: 'Failed to build transaction';
|
userId,
|
||||||
res.status(502).json({ success: false, error: safeMsg });
|
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';
|
||||||
|
res.status(502).json({ success: false, error: msg });
|
||||||
|
} finally {
|
||||||
|
// Best-effort cleanup mnemonic в RAM
|
||||||
|
mnemonic = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import app from './app';
|
import app from './app';
|
||||||
import { env, initEnv } from './config/env';
|
import { env, initEnv } from './config/env';
|
||||||
import { refreshAllKeys, startKeyRotation, stopKeyRotation } from './services/key-rotation.service';
|
import { refreshAllKeys, startKeyRotation, stopKeyRotation } from './services/key-rotation.service';
|
||||||
|
import { isCryptoReady } from './services/crypto.service';
|
||||||
import { logger } from './lib/logger';
|
import { logger } from './lib/logger';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -8,6 +9,13 @@ async function main() {
|
|||||||
|
|
||||||
await initEnv();
|
await initEnv();
|
||||||
await refreshAllKeys();
|
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();
|
startKeyRotation();
|
||||||
|
|
||||||
const server = app.listen(env.port, () => {
|
const server = app.listen(env.port, () => {
|
||||||
|
|||||||
@@ -1,33 +1,81 @@
|
|||||||
/**
|
/**
|
||||||
* Chain-specific address format validators.
|
* Chain-specific address validators с CHECKSUM проверкой.
|
||||||
* НЕ заменяет реальную чеканку checksum — это первый barrier.
|
* Принципиально: regex/length недостаточно — TRX/BTC используют base58check,
|
||||||
|
* один испорченный символ может пройти regex, но кошелёк по такому адресу
|
||||||
|
* не восстановим → funds permanently lost.
|
||||||
*/
|
*/
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import * as bitcoin from 'bitcoinjs-lib';
|
||||||
|
import { PublicKey } from '@solana/web3.js';
|
||||||
|
|
||||||
const BTC_RE = /^(bc1[a-zA-HJ-NP-Z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$/;
|
|
||||||
const TRX_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;
|
const TRX_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;
|
||||||
const SOL_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
||||||
|
|
||||||
export type ChainCode = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
export type ChainCode = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
||||||
|
|
||||||
export function isValidAddress(chain: ChainCode, address: string): boolean {
|
export function isValidAddress(chain: ChainCode, address: string): boolean {
|
||||||
if (typeof address !== 'string' || address.length === 0 || address.length > 256) return false;
|
// Любой блокчейн-адрес помещается в ~64 chars. 256 был оверкилл и open vector
|
||||||
|
// для DoS (тратим CPU на bs58.decode 200-char garbage).
|
||||||
|
if (typeof address !== 'string' || address.length === 0 || address.length > 64) return false;
|
||||||
|
|
||||||
switch (chain) {
|
switch (chain) {
|
||||||
case 'BTC':
|
case 'BTC':
|
||||||
return BTC_RE.test(address);
|
return isValidBtcAddress(address);
|
||||||
case 'TRX':
|
case 'TRX':
|
||||||
return TRX_RE.test(address);
|
return isValidTrxAddress(address);
|
||||||
case 'ETH':
|
case 'ETH':
|
||||||
case 'BSC':
|
case 'BSC':
|
||||||
return ethers.utils.isAddress(address);
|
return ethers.utils.isAddress(address);
|
||||||
case 'SOL':
|
case 'SOL':
|
||||||
return SOL_RE.test(address);
|
return isValidSolAddress(address);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── BTC: bitcoinjs-lib проверяет version byte + checksum (P2PKH/P2SH/bech32) ──
|
||||||
|
function isValidBtcAddress(address: string): boolean {
|
||||||
|
try {
|
||||||
|
bitcoin.address.toOutputScript(address, bitcoin.networks.bitcoin);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TRX: base58check + первый байт 0x41 ──
|
||||||
|
function isValidTrxAddress(address: string): boolean {
|
||||||
|
if (!TRX_RE.test(address)) return false;
|
||||||
|
let decoded: Uint8Array;
|
||||||
|
try {
|
||||||
|
decoded = bs58.decode(address);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (decoded.length !== 25) return false; // 1 prefix + 20 payload + 4 checksum
|
||||||
|
if (decoded[0] !== 0x41) return false; // TRX mainnet prefix
|
||||||
|
const payload = decoded.subarray(0, 21);
|
||||||
|
const checksum = decoded.subarray(21);
|
||||||
|
const h1 = createHash('sha256').update(payload).digest();
|
||||||
|
const h2 = createHash('sha256').update(h1).digest();
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (h2[i] !== checksum[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SOL: реальное base58-декодирование через PublicKey ──
|
||||||
|
function isValidSolAddress(address: string): boolean {
|
||||||
|
try {
|
||||||
|
const pk = new PublicKey(address);
|
||||||
|
// PublicKey принимает 32-байтовое значение; isOnCurve дополнительный sanity
|
||||||
|
return pk.toBytes().length === 32;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isValidAmount(amount: string): boolean {
|
export function isValidAmount(amount: string): boolean {
|
||||||
if (typeof amount !== 'string' || amount.length === 0 || amount.length > 78) return false;
|
if (typeof amount !== 'string' || amount.length === 0 || amount.length > 78) return false;
|
||||||
if (!/^\d+$/.test(amount)) return false;
|
if (!/^\d+$/.test(amount)) return false;
|
||||||
|
|||||||
61
apps/api/src/lib/audit-log.ts
Normal file
61
apps/api/src/lib/audit-log.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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<void> {
|
||||||
|
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<string, unknown>;
|
||||||
|
result?: 'success' | 'failure';
|
||||||
|
errorCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function auditLog(entry: AuditEntry): Promise<void> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,10 +35,16 @@ function getCallerInfo(): { file: string; line: number } {
|
|||||||
const SENSITIVE_PATTERNS: { regex: RegExp; replace: string }[] = [
|
const SENSITIVE_PATTERNS: { regex: RegExp; replace: string }[] = [
|
||||||
{ regex: /(password|passwd|pwd)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
{ regex: /(password|passwd|pwd)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||||
{ regex: /(token|secret|api[_-]?key|auth)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
{ regex: /(token|secret|api[_-]?key|auth)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||||
|
{ regex: /(mnemonic|seed[_-]?phrase|private[_-]?key|priv[_-]?key)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||||
{ regex: /\bhvs\.[A-Za-z0-9]+/g, replace: 'hvs.***' },
|
{ regex: /\bhvs\.[A-Za-z0-9]+/g, replace: 'hvs.***' },
|
||||||
{ regex: /\bs\.[A-Za-z0-9]{20,}/g, replace: 's.***' },
|
{ regex: /\bs\.[A-Za-z0-9]{20,}/g, replace: 's.***' },
|
||||||
{ regex: /Bearer\s+[A-Za-z0-9._-]+/gi, replace: 'Bearer ***' },
|
{ regex: /Bearer\s+[A-Za-z0-9._-]+/gi, replace: 'Bearer ***' },
|
||||||
{ regex: /Authorization:\s*\S+/gi, replace: 'Authorization: ***' },
|
{ regex: /Authorization:\s*\S+/gi, replace: 'Authorization: ***' },
|
||||||
|
// BIP39 mnemonic phrase (12+ lowercase английских слов через пробел) — рискованный паттерн,
|
||||||
|
// но лучше пере-санитайзить чем пропустить mnemonic в логи
|
||||||
|
{ regex: /\b([a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g, replace: '[REDACTED_MNEMONIC]' },
|
||||||
|
// Hex privkey (64 hex chars подряд, optional 0x)
|
||||||
|
{ regex: /\b(0x)?[0-9a-fA-F]{64}\b/g, replace: '[REDACTED_HEX64]' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function sanitize(msg: string): string {
|
function sanitize(msg: string): string {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function errorHandler(err: HttpError, _req: Request, res: Response, _next
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Серверные ошибки (5xx) — generic message, детали только в логи
|
// Серверные ошибки (5xx) — generic message, детали (со stack) только в логи
|
||||||
logger.error(`Server error: ${err.message}`);
|
logger.error(`Server error: ${err.stack || err.message}`);
|
||||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const mutateLimiter = rateLimit({
|
|||||||
message: { success: false, error: 'Too many mutating requests' },
|
message: { success: false, error: 'Too many mutating requests' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Самый строгий — для send / vault PUT (anti-abuse / spam tx prevention)
|
// Самый строгий — для send / vault PUT / wallet create (anti-abuse / spam tx prevention)
|
||||||
export const sensitiveLimiter = rateLimit({
|
export const sensitiveLimiter = rateLimit({
|
||||||
windowMs: 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@@ -40,3 +40,14 @@ export const sensitiveLimiter = rateLimit({
|
|||||||
keyGenerator: keyByUserOrIp,
|
keyGenerator: keyByUserOrIp,
|
||||||
message: { success: false, error: 'Too many sensitive requests' },
|
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' },
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Knex } from 'knex';
|
||||||
import { db } from '../config/database';
|
import { db } from '../config/database';
|
||||||
|
|
||||||
export interface UserRow {
|
export interface UserRow {
|
||||||
@@ -19,6 +20,7 @@ export interface UserRow {
|
|||||||
is_deleted: boolean;
|
is_deleted: boolean;
|
||||||
encrypted_vault: string | null;
|
encrypted_vault: string | null;
|
||||||
vault_salt: string | null;
|
vault_salt: string | null;
|
||||||
|
encrypted_mnemonic: string | null;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
@@ -75,4 +77,41 @@ export const UserModel = {
|
|||||||
.select('encrypted_vault', 'vault_salt')
|
.select('encrypted_vault', 'vault_salt')
|
||||||
.first();
|
.first();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<boolean> {
|
||||||
|
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<string | null> {
|
||||||
|
const row = await db('users')
|
||||||
|
.where({ id, is_deleted: false })
|
||||||
|
.select('encrypted_mnemonic')
|
||||||
|
.first();
|
||||||
|
return row?.encrypted_mnemonic ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async hasMnemonic(id: string): Promise<boolean> {
|
||||||
|
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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Knex } from 'knex';
|
||||||
import { db } from '../config/database';
|
import { db } from '../config/database';
|
||||||
import { generateUlid } from '../utils/ulid';
|
import { generateUlid } from '../utils/ulid';
|
||||||
|
|
||||||
@@ -20,10 +21,11 @@ export const WalletModel = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async createMany(
|
async createMany(
|
||||||
wallets: { user_id: string; chain: string; address: string; derivation_path: string }[]
|
wallets: { user_id: string; chain: string; address: string; derivation_path: string }[],
|
||||||
|
trx?: Knex.Transaction,
|
||||||
): Promise<WalletRow[]> {
|
): Promise<WalletRow[]> {
|
||||||
const withIds = wallets.map((w) => ({ id: generateUlid(), ...w }));
|
const withIds = wallets.map((w) => ({ id: generateUlid(), ...w }));
|
||||||
return db('wallets').insert(withIds).returning('*');
|
return (trx || db)('wallets').insert(withIds).returning('*');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import { WalletController } from '../controllers/wallet.controller';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
router.post('/create', WalletController.createWallet);
|
||||||
router.get('/', WalletController.getWallets);
|
router.get('/', WalletController.getWallets);
|
||||||
router.post('/', WalletController.createWallets);
|
router.post('/mnemonic/reveal', WalletController.revealMnemonic);
|
||||||
|
|
||||||
|
// Per-chain operations
|
||||||
router.get('/:chain/balance', WalletController.getChainBalance);
|
router.get('/:chain/balance', WalletController.getChainBalance);
|
||||||
router.get('/:chain/transactions', WalletController.getChainTransactions);
|
router.get('/:chain/transactions', WalletController.getChainTransactions);
|
||||||
router.post('/:chain/send', WalletController.sendFromChain);
|
router.post('/:chain/send', WalletController.sendFromChain);
|
||||||
|
|||||||
144
apps/api/src/services/crypto.service.ts
Normal file
144
apps/api/src/services/crypto.service.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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<Buffer> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { env, getVaultToken } from '../config/env';
|
|||||||
import { vaultAppRoleLogin } from '../config/vault';
|
import { vaultAppRoleLogin } from '../config/vault';
|
||||||
import { fetchJwtKeysFromVault, swapKeyMap, getKeyMapSize } from './jwt.service';
|
import { fetchJwtKeysFromVault, swapKeyMap, getKeyMapSize } from './jwt.service';
|
||||||
import { fetchCsrfConfig, swapCsrfConfig } from './csrf.service';
|
import { fetchCsrfConfig, swapCsrfConfig } from './csrf.service';
|
||||||
|
import { fetchMasterKey, swapMasterKey, masterKeyMatches, isCryptoReady } from './crypto.service';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
const DEFAULT_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
@@ -14,7 +15,7 @@ let currentVaultToken: string | null = null;
|
|||||||
* При любой ошибке оставляем старые значения в памяти, сервис продолжает работать.
|
* При любой ошибке оставляем старые значения в памяти, сервис продолжает работать.
|
||||||
*/
|
*/
|
||||||
export async function refreshAllKeys(): Promise<void> {
|
export async function refreshAllKeys(): Promise<void> {
|
||||||
const { addr, roleId, secretId, mount, jwtKidPath, jwtKidsPrefix, csrfPath } = env.vault;
|
const { addr, roleId, secretId, mount, jwtKidPath, jwtKidsPrefix, csrfPath, cryptoKeyPath } = env.vault;
|
||||||
|
|
||||||
if (!addr || !roleId || !secretId) {
|
if (!addr || !roleId || !secretId) {
|
||||||
logger.warn('Vault not configured, skipping key refresh');
|
logger.warn('Vault not configured, skipping key refresh');
|
||||||
@@ -33,11 +34,14 @@ export async function refreshAllKeys(): Promise<void> {
|
|||||||
currentVaultToken = fresh;
|
currentVaultToken = fresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pre-fetch обоих секретов параллельно (НЕ мутируя глобал) ───────────
|
// ── Pre-fetch всех секретов параллельно (НЕ мутируя глобал) ───────────
|
||||||
const jwtPromise = fetchJwtKeysFromVault(addr, token, mount, jwtKidPath, jwtKidsPrefix);
|
const jwtPromise = fetchJwtKeysFromVault(addr, token, mount, jwtKidPath, jwtKidsPrefix);
|
||||||
const csrfPromise = csrfPath ? fetchCsrfConfig(addr, token, mount, csrfPath) : Promise.resolve(null);
|
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] = await Promise.allSettled([jwtPromise, csrfPromise]);
|
const [jwtResult, csrfResult, cryptoResult] = await Promise.allSettled([jwtPromise, csrfPromise, cryptoPromise]);
|
||||||
|
|
||||||
// ── Атомарность: если хоть один обязательный fetch упал — НИЧЕГО не меняем ──
|
// ── Атомарность: если хоть один обязательный fetch упал — НИЧЕГО не меняем ──
|
||||||
if (jwtResult.status === 'rejected') {
|
if (jwtResult.status === 'rejected') {
|
||||||
@@ -48,16 +52,36 @@ export async function refreshAllKeys(): Promise<void> {
|
|||||||
logger.error(`Key refresh ABORTED — CSRF fetch failed: ${csrfResult.reason?.message || csrfResult.reason}`);
|
logger.error(`Key refresh ABORTED — CSRF fetch failed: ${csrfResult.reason?.message || csrfResult.reason}`);
|
||||||
return;
|
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 (синхронные операции, нельзя прервать) ──────────────────
|
// ── Atomic swap (синхронные операции, нельзя прервать) ──────────────────
|
||||||
swapKeyMap(jwtResult.value);
|
swapKeyMap(jwtResult.value);
|
||||||
if (csrfResult.status === 'fulfilled' && csrfResult.value) {
|
if (csrfResult.status === 'fulfilled' && csrfResult.value) {
|
||||||
swapCsrfConfig(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(
|
logger.info(
|
||||||
`Keys refreshed atomically: JWT keys=${getKeyMapSize()}` +
|
`Keys refreshed atomically: JWT keys=${getKeyMapSize()}` +
|
||||||
(csrfPath ? `, CSRF=${csrfResult.status === 'fulfilled' ? 'updated' : 'unchanged'}` : '')
|
(csrfPath ? `, CSRF=${csrfResult.status === 'fulfilled' ? 'updated' : 'unchanged'}` : '') +
|
||||||
|
`, Crypto=${isCryptoReady() ? 'ready' : 'NOT-READY'}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
apps/api/src/services/wallet-generator.service.ts
Normal file
121
apps/api/src/services/wallet-generator.service.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 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<ChainCode, string> = {
|
||||||
|
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<DerivedWallet[]> {
|
||||||
|
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])));
|
||||||
|
}
|
||||||
442
apps/api/src/services/wallet-signer.service.ts
Normal file
442
apps/api/src/services/wallet-signer.service.ts
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
/**
|
||||||
|
* 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<ethers.providers.TransactionRequest> = 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<string, number>;
|
||||||
|
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<string, string> = { '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<any> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"openapi": "3.0.0",
|
"openapi": "3.0.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "CryptoWallet API",
|
"title": "CryptoWallet API",
|
||||||
"version": "2.1.0",
|
"version": "3.0.0",
|
||||||
"description": "Multi-chain crypto wallet API. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK). Non-custodial: server NEVER signs transactions, только строит unsigned tx + хранит зашифрованный vault."
|
"description": "Multi-chain crypto wallet API. Auth via JWT (cookie/Bearer), issued by external auth-service (BITOK). CUSTODIAL: server генерит мнемонику, хранит её AES-GCM-зашифрованной (master-key из Vault) и сам подписывает транзакции."
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{ "url": "/api", "description": "API root" }
|
{ "url": "/api", "description": "API root" }
|
||||||
@@ -56,22 +56,28 @@
|
|||||||
"derivationPath": { "type": "string" }
|
"derivationPath": { "type": "string" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WalletInput": {
|
"MnemonicResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["chain", "address", "derivationPath"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"chain": { "$ref": "#/components/schemas/Chain" },
|
"success": { "type": "boolean", "example": true },
|
||||||
"address": { "type": "string", "maxLength": 256 },
|
"data": {
|
||||||
"derivationPath": { "type": "string", "maxLength": 64 }
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mnemonic": { "type": "string", "description": "BIP39 mnemonic (12 words)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CreateWalletsRequest": {
|
"TxBroadcastResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["wallets"],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"wallets": {
|
"success": { "type": "boolean", "example": true },
|
||||||
"type": "array", "minItems": 1, "maxItems": 20,
|
"data": {
|
||||||
"items": { "$ref": "#/components/schemas/WalletInput" }
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"txid": { "type": "string", "description": "Идентификатор отправленной транзакции" },
|
||||||
|
"chain": { "$ref": "#/components/schemas/Chain" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -184,19 +190,49 @@
|
|||||||
"200": { "description": "List of wallets", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
|
"200": { "description": "List of wallets", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
|
||||||
"401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
|
"401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"/wallets/create": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Upsert wallets for authenticated user",
|
"summary": "Создать custodial-кошелёк (mnemonic генерится на сервере)",
|
||||||
"description": "user_id берётся из JWT (sub). При первом обращении создаёт user-row автоматически. На конфликт (user_id, chain) — обновляет address + derivationPath.",
|
"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.",
|
||||||
"tags": ["Wallets"],
|
"tags": ["Wallets"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWalletsRequest" } } }
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["confirm"],
|
||||||
|
"properties": {
|
||||||
|
"confirm": { "type": "string", "enum": ["I_UNDERSTAND_SEED_IS_SECRET"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": { "description": "Created/updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletsResponse" } } } },
|
"200": { "description": "Mnemonic revealed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MnemonicResponse" } } } },
|
||||||
"400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
|
"400": { "description": "Missing/invalid confirm token" },
|
||||||
"401": { "description": "Not authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
|
"401": { "description": "Not authenticated" },
|
||||||
|
"404": { "description": "Wallet not created yet" },
|
||||||
|
"429": { "description": "Rate limit (5/hour) exceeded" },
|
||||||
|
"503": { "description": "Crypto service not ready" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -231,8 +267,8 @@
|
|||||||
|
|
||||||
"/wallets/{chain}/send": {
|
"/wallets/{chain}/send": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Build unsigned send transaction (non-custodial)",
|
"summary": "Custodial send: server signs + broadcasts",
|
||||||
"description": "Возвращает unsigned tx. Клиент подписывает приватным ключом и broadcast'ит через /api/{btc,tron}/broadcast или RPC своей цепи.",
|
"description": "Сервер расшифровывает мнемонику → деривит chain-privkey → подписывает → broadcast'ит через RPC. Возвращает txid.",
|
||||||
"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" } }],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -240,10 +276,11 @@
|
|||||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } }
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendRequest" } } }
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": { "description": "Unsigned tx", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UnsignedTxResponse" } } } },
|
"200": { "description": "Broadcast successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TxBroadcastResponse" } } } },
|
||||||
"400": { "description": "Invalid input" },
|
"400": { "description": "Invalid input" },
|
||||||
"404": { "description": "Wallet not found" },
|
"404": { "description": "Wallet/mnemonic not found" },
|
||||||
"502": { "description": "Upstream RPC error" }
|
"502": { "description": "Broadcast failed (upstream RPC error / insufficient balance / unsupported token)" },
|
||||||
|
"503": { "description": "Crypto service not ready" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
81
cryptowallet-schema.sql
Normal file
81
cryptowallet-schema.sql
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
|
-- CryptoWallet API — DB schema (idempotent)
|
||||||
|
-- Применить один раз на внешнюю БД при первом деплое.
|
||||||
|
-- Версия 3.0 (custodial: AES-GCM encrypted_mnemonic на сервере).
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- ── USERS ────────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id VARCHAR(26) PRIMARY KEY, -- ULID из JWT.sub
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL, -- "EXTERNAL_AUTH" (auth у BITOK)
|
||||||
|
last_name VARCHAR(255),
|
||||||
|
first_name VARCHAR(255),
|
||||||
|
middle_name VARCHAR(255),
|
||||||
|
birth_date DATE,
|
||||||
|
crypto_wallet VARCHAR(255),
|
||||||
|
phone VARCHAR(64),
|
||||||
|
bik VARCHAR(64),
|
||||||
|
account_number VARCHAR(64),
|
||||||
|
card_number VARCHAR(64),
|
||||||
|
inn VARCHAR(64),
|
||||||
|
kyc_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
kyc_verified_at TIMESTAMPTZ,
|
||||||
|
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
encrypted_vault TEXT, -- legacy client-side AES blob (opt)
|
||||||
|
vault_salt VARCHAR(128), -- legacy client KDF salt (opt)
|
||||||
|
encrypted_mnemonic TEXT, -- AES-256-GCM blob (custodial)
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Добавляем encrypted_mnemonic если таблица существует с прошлой версии
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- CHECK: encrypted_mnemonic при значении должен иметь разумный размер.
|
||||||
|
-- AES-GCM blob: 12 IV + plaintext + 16 tag.
|
||||||
|
-- 12-word mnemonic ≈ 11*7 + 11 = 88 байт → 12+88+16 = 116 байт → ~156 base64 chars.
|
||||||
|
-- 24-word mnemonic ≈ 23*7 + 23 = 184 байт → 12+184+16 = 212 байт → ~284 base64 chars.
|
||||||
|
-- Минимум 140 (12 слов с маленькими словами), максимум 512 запас на 24 слова + padding.
|
||||||
|
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 $$;
|
||||||
|
|
||||||
|
-- ── WALLETS ──────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
|
id VARCHAR(26) PRIMARY KEY, -- ULID
|
||||||
|
user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
chain VARCHAR(16) NOT NULL, -- ETH / BTC / SOL / TRX / BSC
|
||||||
|
address VARCHAR(256) NOT NULL,
|
||||||
|
derivation_path VARCHAR(64) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (user_id, chain)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_wallets_user_id ON wallets(user_id);
|
||||||
|
|
||||||
|
-- ── SESSIONS (placeholder, не используется в текущей версии) ─────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id VARCHAR(26) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: cryptowallet-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
API_PORT: "3001"
|
||||||
|
# Внешняя БД (72.56.9.76) — postgres-сервис не нужен.
|
||||||
|
# DB-creds + master-key читаются из Vault через AppRole.
|
||||||
|
volumes:
|
||||||
|
# Audit-log: mnemonic reveal / wallet create / send — для compliance/forensics
|
||||||
|
- ./logs:/app/logs
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 15s
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "20m"
|
||||||
|
max-file: "5"
|
||||||
379
pnpm-lock.yaml
generated
379
pnpm-lock.yaml
generated
@@ -20,6 +20,15 @@ importers:
|
|||||||
'@solana/web3.js':
|
'@solana/web3.js':
|
||||||
specifier: ^1.98.4
|
specifier: ^1.98.4
|
||||||
version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)
|
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
|
||||||
bs58:
|
bs58:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -32,6 +41,9 @@ importers:
|
|||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.0
|
specifier: ^16.4.0
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
|
ed25519-hd-key:
|
||||||
|
specifier: ^1.3.0
|
||||||
|
version: 1.3.0
|
||||||
ethers:
|
ethers:
|
||||||
specifier: 5.7.2
|
specifier: 5.7.2
|
||||||
version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
||||||
@@ -56,6 +68,9 @@ importers:
|
|||||||
swagger-ui-express:
|
swagger-ui-express:
|
||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1(express@4.22.1)
|
version: 5.0.1(express@4.22.1)
|
||||||
|
tiny-secp256k1:
|
||||||
|
specifier: ^2.2.3
|
||||||
|
version: 2.2.4
|
||||||
ulidx:
|
ulidx:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
@@ -336,6 +351,9 @@ packages:
|
|||||||
'@scarf/scarf@1.4.0':
|
'@scarf/scarf@1.4.0':
|
||||||
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
||||||
|
|
||||||
|
'@scure/base@1.2.6':
|
||||||
|
resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==}
|
||||||
|
|
||||||
'@solana/buffer-layout@4.0.1':
|
'@solana/buffer-layout@4.0.1':
|
||||||
resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
|
resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==}
|
||||||
engines: {node: '>=5.10'}
|
engines: {node: '>=5.10'}
|
||||||
@@ -550,12 +568,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||||
engines: {node: '>=8'}
|
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:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
base-x@3.0.11:
|
base-x@3.0.11:
|
||||||
resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==}
|
resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==}
|
||||||
|
|
||||||
|
base-x@4.0.1:
|
||||||
|
resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==}
|
||||||
|
|
||||||
base-x@5.0.1:
|
base-x@5.0.1:
|
||||||
resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==}
|
resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==}
|
||||||
|
|
||||||
@@ -565,10 +590,28 @@ packages:
|
|||||||
bech32@1.1.4:
|
bech32@1.1.4:
|
||||||
resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==}
|
resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==}
|
||||||
|
|
||||||
|
bech32@2.0.0:
|
||||||
|
resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==}
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bip174@2.1.1:
|
||||||
|
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'}
|
||||||
|
|
||||||
bn.js@4.12.3:
|
bn.js@4.12.3:
|
||||||
resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==}
|
resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==}
|
||||||
|
|
||||||
@@ -598,9 +641,18 @@ packages:
|
|||||||
bs58@4.0.1:
|
bs58@4.0.1:
|
||||||
resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
|
resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==}
|
||||||
|
|
||||||
|
bs58@5.0.0:
|
||||||
|
resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==}
|
||||||
|
|
||||||
bs58@6.0.0:
|
bs58@6.0.0:
|
||||||
resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
|
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==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@@ -619,6 +671,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
call-bound@1.0.4:
|
||||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -639,6 +695,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
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:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -685,10 +745,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
cors@2.8.6:
|
cors@2.8.6:
|
||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
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:
|
create-require@1.1.1:
|
||||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
|
|
||||||
@@ -716,6 +785,10 @@ packages:
|
|||||||
deep-is@0.1.4:
|
deep-is@0.1.4:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
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:
|
delay@5.0.0:
|
||||||
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -751,6 +824,9 @@ packages:
|
|||||||
dynamic-dedupe@0.3.0:
|
dynamic-dedupe@0.3.0:
|
||||||
resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==}
|
resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==}
|
||||||
|
|
||||||
|
ed25519-hd-key@1.3.0:
|
||||||
|
resolution: {integrity: sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@@ -897,6 +973,10 @@ packages:
|
|||||||
flatted@3.4.2:
|
flatted@3.4.2:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||||
|
|
||||||
|
for-each@0.3.5:
|
||||||
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -962,10 +1042,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
has-property-descriptors@1.0.2:
|
||||||
|
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
|
||||||
|
|
||||||
has-symbols@1.1.0:
|
has-symbols@1.1.0:
|
||||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
hash.js@1.1.7:
|
||||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||||
|
|
||||||
@@ -1029,6 +1120,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
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:
|
is-core-module@2.16.1:
|
||||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1049,6 +1144,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
||||||
engines: {node: '>=8'}
|
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:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -1139,6 +1244,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
md5.js@1.3.5:
|
||||||
|
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1323,6 +1431,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
possible-typed-array-names@1.1.0:
|
||||||
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
postgres-array@2.0.0:
|
postgres-array@2.0.0:
|
||||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1343,6 +1455,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -1366,6 +1481,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
readdirp@3.6.0:
|
readdirp@3.6.0:
|
||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
@@ -1401,12 +1519,19 @@ packages:
|
|||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ripemd160@2.0.3:
|
||||||
|
resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
rpc-websockets@9.3.8:
|
rpc-websockets@9.3.8:
|
||||||
resolution: {integrity: sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==}
|
resolution: {integrity: sha512-7r+fm4tSJmLf9GvZfL1DJ1SJwpagpp6AazqM0FUaeV7CA+7+NYINSk1syWa4tU/6OF2CyBicLtzENGmXRJH6wQ==}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
@@ -1429,9 +1554,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||||
engines: {node: '>= 0.8.0'}
|
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:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
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:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1481,6 +1615,9 @@ packages:
|
|||||||
stream-json@1.9.1:
|
stream-json@1.9.1:
|
||||||
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
|
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:
|
strip-ansi@6.0.1:
|
||||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1532,6 +1669,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
|
resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
|
||||||
engines: {node: '>=8'}
|
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:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -1618,6 +1763,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ERZf7pKOR155NKs/PZt1+83NrSEJfUL7+p9/TGZg/8xzDVMntXEFQlX4CsNJQTyu4h3j+dZYiQWOOlv5pssuHQ==}
|
resolution: {integrity: sha512-ERZf7pKOR155NKs/PZt1+83NrSEJfUL7+p9/TGZg/8xzDVMntXEFQlX4CsNJQTyu4h3j+dZYiQWOOlv5pssuHQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tweetnacl@1.0.3:
|
||||||
|
resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1630,11 +1778,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
engines: {node: '>= 0.6'}
|
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==}
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@5.9.3:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uint8array-tools@0.0.7:
|
||||||
|
resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
ulidx@2.4.1:
|
ulidx@2.4.1:
|
||||||
resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==}
|
resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -1653,6 +1812,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==}
|
resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==}
|
||||||
engines: {node: '>=6.14.2'}
|
engines: {node: '>=6.14.2'}
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
utils-merge@1.0.1:
|
utils-merge@1.0.1:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
@@ -1669,6 +1831,9 @@ packages:
|
|||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
|
varuint-bitcoin@1.1.2:
|
||||||
|
resolution: {integrity: sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==}
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1679,11 +1844,18 @@ packages:
|
|||||||
whatwg-url@5.0.0:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
|
which-typed-array@1.1.20:
|
||||||
|
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
wif@2.0.6:
|
||||||
|
resolution: {integrity: sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==}
|
||||||
|
|
||||||
word-wrap@1.2.5:
|
word-wrap@1.2.5:
|
||||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2249,6 +2421,8 @@ snapshots:
|
|||||||
|
|
||||||
'@scarf/scarf@1.4.0': {}
|
'@scarf/scarf@1.4.0': {}
|
||||||
|
|
||||||
|
'@scure/base@1.2.6': {}
|
||||||
|
|
||||||
'@solana/buffer-layout@4.0.1':
|
'@solana/buffer-layout@4.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
@@ -2505,20 +2679,50 @@ snapshots:
|
|||||||
|
|
||||||
array-union@2.1.0: {}
|
array-union@2.1.0: {}
|
||||||
|
|
||||||
|
available-typed-arrays@1.0.7:
|
||||||
|
dependencies:
|
||||||
|
possible-typed-array-names: 1.1.0
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
base-x@3.0.11:
|
base-x@3.0.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
base-x@4.0.1: {}
|
||||||
|
|
||||||
base-x@5.0.1: {}
|
base-x@5.0.1: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
bech32@1.1.4: {}
|
bech32@1.1.4: {}
|
||||||
|
|
||||||
|
bech32@2.0.0: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
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
|
||||||
|
bech32: 2.0.0
|
||||||
|
bip174: 2.1.1
|
||||||
|
bs58check: 3.0.1
|
||||||
|
typeforce: 1.18.0
|
||||||
|
varuint-bitcoin: 1.1.2
|
||||||
|
|
||||||
bn.js@4.12.3: {}
|
bn.js@4.12.3: {}
|
||||||
|
|
||||||
bn.js@5.2.3: {}
|
bn.js@5.2.3: {}
|
||||||
@@ -2565,10 +2769,25 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base-x: 3.0.11
|
base-x: 3.0.11
|
||||||
|
|
||||||
|
bs58@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
base-x: 4.0.1
|
||||||
|
|
||||||
bs58@6.0.0:
|
bs58@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
base-x: 5.0.1
|
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
|
||||||
|
bs58: 5.0.0
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
@@ -2588,6 +2807,13 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
function-bind: 1.1.2
|
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:
|
call-bound@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -2614,6 +2840,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
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:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -2647,11 +2879,30 @@ snapshots:
|
|||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cors@2.8.6:
|
cors@2.8.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
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: {}
|
create-require@1.1.1: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
@@ -2670,6 +2921,12 @@ snapshots:
|
|||||||
|
|
||||||
deep-is@0.1.4: {}
|
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: {}
|
delay@5.0.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
@@ -2698,6 +2955,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
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: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
elliptic@6.5.4:
|
elliptic@6.5.4:
|
||||||
@@ -2948,6 +3210,10 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.2: {}
|
||||||
|
|
||||||
|
for-each@0.3.5:
|
||||||
|
dependencies:
|
||||||
|
is-callable: 1.2.7
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
@@ -3017,8 +3283,23 @@ snapshots:
|
|||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
|
has-property-descriptors@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
es-define-property: 1.0.1
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
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:
|
hash.js@1.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@@ -3080,6 +3361,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.3.0
|
binary-extensions: 2.3.0
|
||||||
|
|
||||||
|
is-callable@1.2.7: {}
|
||||||
|
|
||||||
is-core-module@2.16.1:
|
is-core-module@2.16.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
@@ -3094,6 +3377,14 @@ snapshots:
|
|||||||
|
|
||||||
is-path-inside@3.0.3: {}
|
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: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)):
|
isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)):
|
||||||
@@ -3178,6 +3469,12 @@ snapshots:
|
|||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
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: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
merge-descriptors@1.0.3: {}
|
merge-descriptors@1.0.3: {}
|
||||||
@@ -3320,6 +3617,8 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
||||||
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postgres-array@2.0.0: {}
|
postgres-array@2.0.0: {}
|
||||||
|
|
||||||
postgres-bytea@1.0.1: {}
|
postgres-bytea@1.0.1: {}
|
||||||
@@ -3332,6 +3631,8 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@@ -3354,6 +3655,16 @@ snapshots:
|
|||||||
iconv-lite: 0.4.24
|
iconv-lite: 0.4.24
|
||||||
unpipe: 1.0.0
|
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:
|
readdirp@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
@@ -3382,6 +3693,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
ripemd160@2.0.3:
|
||||||
|
dependencies:
|
||||||
|
hash-base: 3.1.2
|
||||||
|
inherits: 2.0.4
|
||||||
|
|
||||||
rpc-websockets@9.3.8:
|
rpc-websockets@9.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.21
|
'@swc/helpers': 0.5.21
|
||||||
@@ -3399,6 +3715,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
@@ -3434,8 +3752,23 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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: {}
|
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:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
@@ -3489,6 +3822,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
stream-chain: 2.2.5
|
stream-chain: 2.2.5
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
strip-ansi@6.0.1:
|
strip-ansi@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 5.0.1
|
ansi-regex: 5.0.1
|
||||||
@@ -3524,6 +3861,16 @@ snapshots:
|
|||||||
|
|
||||||
tildify@2.0.0: {}
|
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:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
@@ -3610,6 +3957,8 @@ snapshots:
|
|||||||
turbo-windows-64: 2.8.15
|
turbo-windows-64: 2.8.15
|
||||||
turbo-windows-arm64: 2.8.15
|
turbo-windows-arm64: 2.8.15
|
||||||
|
|
||||||
|
tweetnacl@1.0.3: {}
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -3621,8 +3970,18 @@ snapshots:
|
|||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
mime-types: 2.1.35
|
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: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
uint8array-tools@0.0.7: {}
|
||||||
|
|
||||||
ulidx@2.4.1:
|
ulidx@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
layerr: 3.0.0
|
layerr: 3.0.0
|
||||||
@@ -3640,6 +3999,8 @@ snapshots:
|
|||||||
node-gyp-build: 4.8.4
|
node-gyp-build: 4.8.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
uuid@11.1.1: {}
|
uuid@11.1.1: {}
|
||||||
@@ -3648,6 +4009,10 @@ snapshots:
|
|||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
|
varuint-bitcoin@1.1.2:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
webidl-conversions@3.0.1: {}
|
||||||
@@ -3657,10 +4022,24 @@ snapshots:
|
|||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
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:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
wif@2.0.6:
|
||||||
|
dependencies:
|
||||||
|
bs58check: 2.1.2
|
||||||
|
|
||||||
word-wrap@1.2.5: {}
|
word-wrap@1.2.5: {}
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|||||||
52
start.sh
Normal file
52
start.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " CryptoWallet API — Docker Deploy"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# 1. Docker check
|
||||||
|
command -v docker >/dev/null 2>&1 || { echo "[ERROR] Docker not installed"; exit 1; }
|
||||||
|
docker compose version >/dev/null 2>&1 || { echo "[ERROR] docker compose plugin missing"; exit 1; }
|
||||||
|
|
||||||
|
# 2. .env check
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
if [ -f .env.example ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
echo "[INFO] .env создан из примера — заполни Vault креды и запусти снова"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "[ERROR] нет ни .env, ни .env.example"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Build & start
|
||||||
|
echo "[INFO] Building and starting containers..."
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# 4. Wait healthy
|
||||||
|
echo "[INFO] Waiting for API to become healthy..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if curl -sf http://localhost:3001/api/health >/dev/null 2>&1; then
|
||||||
|
echo "[OK] API is healthy"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" = "30" ]; then
|
||||||
|
echo "[ERROR] API not healthy after 60s. Logs:"
|
||||||
|
docker compose logs --tail=50 api
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Up!"
|
||||||
|
echo " API: http://localhost:3001"
|
||||||
|
echo " Health: http://localhost:3001/api/health"
|
||||||
|
echo " Docs: http://localhost:3001/api/docs"
|
||||||
|
echo " Logs: docker compose logs -f api"
|
||||||
|
echo "=========================================="
|
||||||
Reference in New Issue
Block a user