update project
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
|
.turbo
|
||||||
24
.env.example
24
.env.example
@@ -1,24 +1,24 @@
|
|||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/cryptowallet_devphase3
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_NAME=cryptowallet_v2
|
DB_NAME=cryptowallet_devphase3
|
||||||
|
|
||||||
# JWT (external auth service)
|
|
||||||
JWT_JWKS_URL=
|
|
||||||
JWT_PUBLIC_KEY=
|
|
||||||
JWT_ALGORITHM=RS256
|
|
||||||
JWT_ISSUER=
|
|
||||||
JWT_AUDIENCE=
|
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
API_PORT=3001
|
API_PORT=3001
|
||||||
FRONTEND_URL=http://localhost:3000
|
FRONTEND_URL=http://localhost:3000
|
||||||
RELAY_API_KEY=
|
RELAY_API_KEY=
|
||||||
|
|
||||||
# TRON
|
# BITOK auth service
|
||||||
TRON_API_KEY=
|
BITOK_JWKS_URL=http://localhost:8000/.well-known/jwks.json
|
||||||
|
BITOK_ISSUER=auth-service
|
||||||
|
BITOK_AUDIENCE=wallet-service
|
||||||
|
|
||||||
# Jupiter (Solana DEX aggregator)
|
# RabbitMQ
|
||||||
JUPITER_API_KEY=
|
RABBITMQ_URL=amqp://guest:guest@localhost:5672/
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_LOGIN_MAX=5
|
||||||
|
RATE_LIMIT_LOGIN_WINDOW_MS=900000
|
||||||
|
|||||||
22
BITOK/.dockerignore
Normal file
22
BITOK/.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
141
BITOK/.gitignore
vendored
Normal file
141
BITOK/.gitignore
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
*.pyd
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Type checkers / linters
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
.pyre/
|
||||||
|
.pytype/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# Pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# Hatch
|
||||||
|
.hatch/
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Local databases
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Secrets / credentials
|
||||||
|
secrets.json
|
||||||
|
credentials.json
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# PyCharm / IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
out/
|
||||||
|
|
||||||
|
# VS Code (optional)
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Sphinx docs
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# mkdocs
|
||||||
|
site/
|
||||||
|
|
||||||
|
# celery
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# mypy compiled cache
|
||||||
|
.mypy_cache/
|
||||||
|
|
||||||
|
# pyinstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# pytest debug
|
||||||
|
pytestdebug.log
|
||||||
|
|
||||||
|
# Local config overrides
|
||||||
|
config.local.py
|
||||||
|
settings.local.py
|
||||||
|
|
||||||
|
# Vault / local dev secrets
|
||||||
|
.env.vault
|
||||||
|
vault.token
|
||||||
@@ -1,28 +1,47 @@
|
|||||||
FROM node:20-alpine AS base
|
# Build stage
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM base AS deps
|
# Copy workspace config
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
|
||||||
COPY apps/api/package.json apps/api/
|
COPY apps/api/package.json apps/api/
|
||||||
COPY packages/shared/package.json packages/shared/
|
COPY packages/shared/package.json packages/shared/
|
||||||
RUN pnpm install --frozen-lockfile --prod=false
|
|
||||||
|
|
||||||
FROM base AS build
|
# Enable hoisting so tsc can find all deps
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
RUN echo "node-linker=hoisted" > .npmrc
|
||||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
RUN pnpm install --frozen-lockfile
|
||||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules 2>/dev/null || true
|
|
||||||
COPY . .
|
# Copy source
|
||||||
RUN cd apps/api && pnpm build
|
COPY apps/api/ apps/api/
|
||||||
|
COPY packages/shared/ packages/shared/
|
||||||
|
|
||||||
|
# Build api (node_modules are hoisted, tsc available at root)
|
||||||
|
RUN cd apps/api && ../../node_modules/.bin/tsc \
|
||||||
|
&& rm -f dist/db/migrations/*.d.ts dist/db/migrations/*.d.ts.map dist/db/migrations/*.js.map
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
FROM node:20-alpine AS runtime
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
|
||||||
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
|
||||||
COPY --from=build /app/apps/api/package.json ./apps/api/
|
|
||||||
COPY --from=build /app/packages/shared ./packages/shared
|
|
||||||
|
|
||||||
WORKDIR /app/apps/api
|
# Copy built output (includes compiled migrations + knexfile in dist/db/)
|
||||||
|
COPY --from=builder /app/apps/api/dist ./dist
|
||||||
|
COPY --from=builder /app/apps/api/package.json ./
|
||||||
|
|
||||||
|
# Copy node_modules (runtime deps including bcrypt native)
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/apps/api/node_modules ./apps_node_modules
|
||||||
|
|
||||||
|
# Entrypoint
|
||||||
|
COPY apps/api/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||||
|
RUN chmod +x ./docker-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||||
|
|||||||
15
apps/api/docker-entrypoint.sh
Normal file
15
apps/api/docker-entrypoint.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# If Vault init-keys exist, extract root token
|
||||||
|
if [ -f /vault/file/init-keys.json ]; then
|
||||||
|
export VAULT_TOKEN=$(tr -d ' \n' < /vault/file/init-keys.json | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
echo "[API] Vault token loaded from init-keys.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
node node_modules/knex/bin/cli.js migrate:latest --knexfile dist/db/knexfile.js
|
||||||
|
echo "[API] Migrations complete"
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
exec node dist/index.js
|
||||||
@@ -8,30 +8,39 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"migrate": "node --require ts-node/register node_modules/knex/bin/cli.js migrate:latest --knexfile src/db/knexfile.ts",
|
"migrate": "node --require ts-node/register node_modules/knex/bin/cli.js migrate:latest --knexfile src/db/knexfile.ts",
|
||||||
"migrate:rollback": "node --require ts-node/register node_modules/knex/bin/cli.js migrate:rollback --knexfile src/db/knexfile.ts",
|
"migrate:rollback": "node --require ts-node/register node_modules/knex/bin/cli.js migrate:rollback --knexfile src/db/knexfile.ts",
|
||||||
|
"db:fresh": "pnpm db:reset && pnpm migrate",
|
||||||
|
"db:reset": "node --require ts-node/register src/db/reset-db.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cryptowallet/shared": "workspace:*",
|
"@cryptowallet/shared": "workspace:*",
|
||||||
|
"amqplib": "^1.0.3",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
"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",
|
||||||
"ethers": "5.7.2",
|
"ethers": "5.7.2",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
"express-rate-limit": "^7.4.0",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"jose": "^6.2.2",
|
"jose": "^6.2.2",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"swagger-ui-express": "^5.0.1",
|
|
||||||
"ulidx": "^2.4.1",
|
"ulidx": "^2.4.1",
|
||||||
|
"uuid": "^11.0.0",
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.23.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/amqplib": "^0.10.8",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/express-serve-static-core": "^5.1.1",
|
"@types/express-serve-static-core": "^5.1.1",
|
||||||
|
"@types/jsonwebtoken": "^9.0.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/uuid": "^10.0.0",
|
||||||
"ts-node": "^10.9.0",
|
"ts-node": "^10.9.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import express from 'express';
|
|||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
|
||||||
import { env } from './config/env';
|
import { env } from './config/env';
|
||||||
import { swaggerSpec } from './config/swagger';
|
|
||||||
import { errorHandler } from './middleware/error-handler';
|
import { errorHandler } from './middleware/error-handler';
|
||||||
|
import walletSetupRoutes from './routes/wallet-setup.routes';
|
||||||
import walletRoutes from './routes/wallet.routes';
|
import walletRoutes from './routes/wallet.routes';
|
||||||
|
import vaultRoutes from './routes/vault.routes';
|
||||||
import relayProxyRoutes from './routes/relay-proxy.routes';
|
import relayProxyRoutes from './routes/relay-proxy.routes';
|
||||||
import tronProxyRoutes from './routes/tron-proxy.routes';
|
import tronProxyRoutes from './routes/tron-proxy.routes';
|
||||||
import solSwapProxyRoutes from './routes/sol-swap-proxy.routes';
|
import solSwapProxyRoutes from './routes/sol-swap-proxy.routes';
|
||||||
@@ -25,12 +25,9 @@ app.get('/api/health', (_req, res) => {
|
|||||||
res.json({ success: true, data: { status: 'ok' } });
|
res.json({ success: true, data: { status: 'ok' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
app.use('/api/wallet', walletSetupRoutes);
|
||||||
app.get('/api/docs/swagger.json', (_req, res) => {
|
|
||||||
res.json(swaggerSpec);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/api/wallets', walletRoutes);
|
app.use('/api/wallets', walletRoutes);
|
||||||
|
app.use('/api/vault', vaultRoutes);
|
||||||
app.use('/api/relay', relayProxyRoutes);
|
app.use('/api/relay', relayProxyRoutes);
|
||||||
app.use('/api/tron', tronProxyRoutes);
|
app.use('/api/tron', tronProxyRoutes);
|
||||||
app.use('/api/sol/swap', solSwapProxyRoutes);
|
app.use('/api/sol/swap', solSwapProxyRoutes);
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { fetchVaultSecrets } from './vault';
|
||||||
|
|
||||||
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
|
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
|
||||||
|
|
||||||
export const env = {
|
export let env = {
|
||||||
db: {
|
db: {
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '5432'),
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
user: process.env.DB_USER || 'postgres',
|
user: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
name: process.env.DB_NAME || 'cryptowallet_v2',
|
name: process.env.DB_NAME || 'cryptowallet',
|
||||||
},
|
|
||||||
jwt: {
|
|
||||||
jwksUrl: process.env.JWT_JWKS_URL || '',
|
|
||||||
publicKey: process.env.JWT_PUBLIC_KEY || '',
|
|
||||||
algorithm: process.env.JWT_ALGORITHM || 'RS256',
|
|
||||||
issuer: process.env.JWT_ISSUER || '',
|
|
||||||
audience: process.env.JWT_AUDIENCE || '',
|
|
||||||
},
|
},
|
||||||
port: parseInt(process.env.API_PORT || '3001'),
|
port: parseInt(process.env.API_PORT || '3001'),
|
||||||
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
|
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||||
@@ -24,5 +18,38 @@ export const env = {
|
|||||||
tronApiKey: process.env.TRON_API_KEY || null,
|
tronApiKey: process.env.TRON_API_KEY || null,
|
||||||
jupiterApiKey: process.env.JUPITER_API_KEY || null,
|
jupiterApiKey: process.env.JUPITER_API_KEY || null,
|
||||||
jupiterReferralAccount: process.env.JUPITER_REFERRAL_ACCOUNT || null,
|
jupiterReferralAccount: process.env.JUPITER_REFERRAL_ACCOUNT || null,
|
||||||
jupiterFeeBps: parseInt(process.env.JUPITER_FEE_BPS || '70'),
|
jupiterFeeBps: parseInt(process.env.JUPITER_FEE_BPS || '70'), // 0.7%
|
||||||
|
|
||||||
|
// BITOK auth service
|
||||||
|
bitokJwksUrl: process.env.BITOK_JWKS_URL || 'http://localhost:8000/.well-known/jwks.json',
|
||||||
|
bitokIssuer: process.env.BITOK_ISSUER || 'auth-service',
|
||||||
|
bitokAudience: process.env.BITOK_AUDIENCE || 'wallet-service',
|
||||||
|
|
||||||
|
// RabbitMQ
|
||||||
|
rabbitmqUrl: process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672/',
|
||||||
|
rabbitmqExchange: process.env.RABBITMQ_EXCHANGE || 'bitok.events',
|
||||||
|
rabbitmqWalletQueue: process.env.RABBITMQ_WALLET_QUEUE || 'wallet.user_events',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function initEnv(): Promise<void> {
|
||||||
|
const secrets = await fetchVaultSecrets();
|
||||||
|
|
||||||
|
if (secrets) {
|
||||||
|
console.log('[ENV] Loaded secrets from Vault');
|
||||||
|
env = {
|
||||||
|
...env,
|
||||||
|
db: {
|
||||||
|
host: secrets.db_host,
|
||||||
|
port: parseInt(secrets.db_port),
|
||||||
|
user: secrets.db_user,
|
||||||
|
password: secrets.db_password,
|
||||||
|
name: secrets.db_name,
|
||||||
|
},
|
||||||
|
relayApiKey: secrets.relay_api_key || null,
|
||||||
|
tronApiKey: secrets.tron_api_key || env.tronApiKey,
|
||||||
|
jupiterApiKey: secrets.jupiter_api_key || env.jupiterApiKey,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log('[ENV] Vault not available, using env vars');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const swaggerPath = path.resolve(__dirname, '../../swagger.json');
|
|
||||||
export const swaggerSpec = JSON.parse(fs.readFileSync(swaggerPath, 'utf-8'));
|
|
||||||
30
apps/api/src/config/vault.ts
Normal file
30
apps/api/src/config/vault.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
interface VaultSecrets {
|
||||||
|
db_host: string;
|
||||||
|
db_port: string;
|
||||||
|
db_user: string;
|
||||||
|
db_password: string;
|
||||||
|
db_name: string;
|
||||||
|
relay_api_key: string;
|
||||||
|
tron_api_key: string;
|
||||||
|
jupiter_api_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchVaultSecrets(): Promise<VaultSecrets | null> {
|
||||||
|
const vaultAddr = process.env.VAULT_ADDR;
|
||||||
|
const vaultToken = process.env.VAULT_TOKEN;
|
||||||
|
|
||||||
|
if (!vaultAddr || !vaultToken) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${vaultAddr}/v1/kv/data/cryptowallet`, {
|
||||||
|
headers: { 'X-Vault-Token': vaultToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const body = (await res.json()) as { data: { data: VaultSecrets } };
|
||||||
|
return body.data.data;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
apps/api/src/controllers/vault.controller.ts
Normal file
23
apps/api/src/controllers/vault.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { UserModel } from '../models/user.model';
|
||||||
|
|
||||||
|
export const VaultController = {
|
||||||
|
async getVault(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const user = await UserModel.findByBitokUserId(req.user!.bitokUserId);
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ success: false, error: 'User not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
encryptedVault: user.encrypted_vault,
|
||||||
|
vaultSalt: user.vault_salt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
118
apps/api/src/controllers/wallet-setup.controller.ts
Normal file
118
apps/api/src/controllers/wallet-setup.controller.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { UserModel } from '../models/user.model';
|
||||||
|
import { WalletModel } from '../models/wallet.model';
|
||||||
|
import { db } from '../config/database';
|
||||||
|
import { generateUlid } from '../utils/ulid';
|
||||||
|
|
||||||
|
export const WalletSetupController = {
|
||||||
|
async setup(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { bitokUserId, email } = req.user!;
|
||||||
|
const { encryptedVault, vaultSalt, wallets } = req.body;
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existing = await UserModel.findByBitokUserId(bitokUserId);
|
||||||
|
if (existing) {
|
||||||
|
res.status(409).json({ success: false, error: 'Wallet already set up for this user' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await db.transaction(async (trx) => {
|
||||||
|
const [user] = await trx('users')
|
||||||
|
.insert({
|
||||||
|
id: generateUlid(),
|
||||||
|
bitok_user_id: bitokUserId,
|
||||||
|
email: email || null,
|
||||||
|
encrypted_vault: encryptedVault,
|
||||||
|
vault_salt: vaultSalt,
|
||||||
|
})
|
||||||
|
.returning('*');
|
||||||
|
|
||||||
|
const walletRows = await trx('wallets')
|
||||||
|
.insert(
|
||||||
|
wallets.map((w: { chain: string; address: string; derivationPath: string }) => ({
|
||||||
|
id: generateUlid(),
|
||||||
|
user_id: user.id,
|
||||||
|
chain: w.chain,
|
||||||
|
address: w.address,
|
||||||
|
derivation_path: w.derivationPath,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.returning('*');
|
||||||
|
|
||||||
|
return { user, wallets: walletRows };
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: result.user.id,
|
||||||
|
bitokUserId: result.user.bitok_user_id,
|
||||||
|
email: result.user.email,
|
||||||
|
},
|
||||||
|
wallets: result.wallets.map((w: any) => ({
|
||||||
|
chain: w.chain,
|
||||||
|
address: w.address,
|
||||||
|
derivationPath: w.derivation_path,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[WalletSetup] Error:', err.message);
|
||||||
|
res.status(500).json({ success: false, error: 'Failed to set up wallet' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async confirmMnemonic(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { bitokUserId } = req.user!;
|
||||||
|
const user = await UserModel.findByBitokUserId(bitokUserId);
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ success: false, error: 'Wallet not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await UserModel.setMnemonicShown(user.id);
|
||||||
|
res.json({ success: true, data: { mnemonicShown: true } });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[ConfirmMnemonic] Error:', err.message);
|
||||||
|
res.status(500).json({ success: false, error: 'Failed to confirm mnemonic' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async unlock(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { bitokUserId } = req.user!;
|
||||||
|
|
||||||
|
const user = await UserModel.findByBitokUserId(bitokUserId);
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ success: false, error: 'Wallet not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.deleted) {
|
||||||
|
res.status(403).json({ success: false, error: 'Account has been deleted' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallets = await WalletModel.findByUserId(user.id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
encryptedVault: user.encrypted_vault,
|
||||||
|
vaultSalt: user.vault_salt,
|
||||||
|
wallets: wallets.map((w) => ({
|
||||||
|
chain: w.chain,
|
||||||
|
address: w.address,
|
||||||
|
derivationPath: w.derivation_path,
|
||||||
|
})),
|
||||||
|
mnemonicShown: user.mnemonic_shown,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[WalletUnlock] Error:', err.message);
|
||||||
|
res.status(500).json({ success: false, error: 'Failed to unlock wallet' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { UserModel } from '../models/user.model';
|
||||||
import { WalletModel } from '../models/wallet.model';
|
import { WalletModel } from '../models/wallet.model';
|
||||||
|
|
||||||
export const WalletController = {
|
export const WalletController = {
|
||||||
async getWallets(req: Request, res: Response) {
|
async getWallets(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const wallets = await WalletModel.findByUserId(req.auth!.userId);
|
const user = await UserModel.findByBitokUserId(req.user!.bitokUserId);
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ success: false, error: 'User not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallets = await WalletModel.findByUserId(user.id);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: wallets.map((w) => ({
|
data: wallets.map((w) => ({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const config: Knex.Config = {
|
|||||||
port: parseInt(process.env.DB_PORT || '5432'),
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
user: process.env.DB_USER || 'postgres',
|
user: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
database: process.env.DB_NAME || 'cryptowallet_v2',
|
database: process.env.DB_NAME || 'cryptowallet',
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: path.resolve(__dirname, 'migrations'),
|
directory: path.resolve(__dirname, 'migrations'),
|
||||||
|
|||||||
@@ -3,21 +3,12 @@ import type { Knex } from 'knex';
|
|||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
await knex.schema.createTable('users', (t) => {
|
await knex.schema.createTable('users', (t) => {
|
||||||
t.string('id', 26).primary();
|
t.string('id', 26).primary();
|
||||||
t.string('email', 255).notNullable().unique();
|
t.string('username', 64).notNullable().unique();
|
||||||
t.string('password_hash', 255).notNullable();
|
t.text('password_hash').notNullable();
|
||||||
t.string('last_name', 128).nullable();
|
t.text('pin_hash').notNullable();
|
||||||
t.string('first_name', 128).nullable();
|
t.text('encrypted_vault').notNullable();
|
||||||
t.string('middle_name', 128).nullable();
|
t.string('vault_salt', 128).notNullable();
|
||||||
t.date('birth_date').nullable();
|
t.boolean('mnemonic_shown').notNullable().defaultTo(false);
|
||||||
t.string('crypto_wallet', 255).nullable();
|
|
||||||
t.string('phone', 16).nullable();
|
|
||||||
t.string('bik', 9).nullable();
|
|
||||||
t.string('account_number', 20).nullable();
|
|
||||||
t.string('card_number', 19).nullable();
|
|
||||||
t.string('inn', 12).nullable();
|
|
||||||
t.boolean('kyc_verified').notNullable().defaultTo(false);
|
|
||||||
t.timestamp('kyc_verified_at', { useTz: true }).nullable();
|
|
||||||
t.boolean('is_deleted').notNullable().defaultTo(false);
|
|
||||||
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
t.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
t.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,22 +3,16 @@ import type { Knex } from 'knex';
|
|||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
await knex.schema.createTable('sessions', (t) => {
|
await knex.schema.createTable('sessions', (t) => {
|
||||||
t.string('id', 26).primary();
|
t.string('id', 26).primary();
|
||||||
t.string('sid', 26).notNullable().unique();
|
|
||||||
t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE');
|
t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE');
|
||||||
t.string('device_id', 26).nullable();
|
t.text('refresh_token_hash').notNullable();
|
||||||
t.string('user_agent', 500).nullable();
|
t.string('user_agent').nullable();
|
||||||
t.string('first_ip', 64).nullable();
|
t.specificType('ip_address', 'inet').nullable();
|
||||||
t.string('last_ip', 64).nullable();
|
t.timestamp('expires_at', { useTz: true }).notNullable();
|
||||||
t.timestamp('last_seen_at', { useTz: true }).nullable();
|
|
||||||
t.timestamp('revoked_at', { useTz: true }).nullable();
|
|
||||||
t.string('refresh_jti_hash', 255).nullable();
|
|
||||||
t.timestamp('refresh_expires_at', { useTz: true }).nullable();
|
|
||||||
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
t.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.raw('CREATE INDEX idx_sessions_user_id ON sessions(user_id)');
|
await knex.schema.raw('CREATE INDEX idx_sessions_user_id ON sessions(user_id)');
|
||||||
await knex.schema.raw('CREATE INDEX idx_sessions_sid ON sessions(sid)');
|
await knex.schema.raw('CREATE INDEX idx_sessions_expires ON sessions(expires_at)');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
|||||||
18
apps/api/src/db/migrations/004_create_login_attempts.ts
Normal file
18
apps/api/src/db/migrations/004_create_login_attempts.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.createTable('login_attempts', (t) => {
|
||||||
|
t.string('id', 26).primary();
|
||||||
|
t.string('username', 64).notNullable();
|
||||||
|
t.specificType('ip_address', 'inet').notNullable();
|
||||||
|
t.boolean('success').notNullable().defaultTo(false);
|
||||||
|
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.raw('CREATE INDEX idx_login_attempts_username_created ON login_attempts(username, created_at)');
|
||||||
|
await knex.schema.raw('CREATE INDEX idx_login_attempts_ip_created ON login_attempts(ip_address, created_at)');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists('login_attempts');
|
||||||
|
}
|
||||||
33
apps/api/src/db/migrations/005_simplify_users_for_bitok.ts
Normal file
33
apps/api/src/db/migrations/005_simplify_users_for_bitok.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable('users', (t) => {
|
||||||
|
t.dropColumn('username');
|
||||||
|
t.dropColumn('password_hash');
|
||||||
|
t.dropColumn('pin_hash');
|
||||||
|
|
||||||
|
t.string('bitok_user_id', 26).notNullable().unique();
|
||||||
|
t.string('email', 255).nullable();
|
||||||
|
t.boolean('kyc_verified').notNullable().defaultTo(false);
|
||||||
|
t.string('kyc_level', 20).nullable();
|
||||||
|
t.boolean('deleted').notNullable().defaultTo(false);
|
||||||
|
|
||||||
|
t.index(['bitok_user_id'], 'idx_users_bitok_user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable('users', (t) => {
|
||||||
|
t.dropIndex(['bitok_user_id'], 'idx_users_bitok_user_id');
|
||||||
|
|
||||||
|
t.dropColumn('bitok_user_id');
|
||||||
|
t.dropColumn('email');
|
||||||
|
t.dropColumn('kyc_verified');
|
||||||
|
t.dropColumn('kyc_level');
|
||||||
|
t.dropColumn('deleted');
|
||||||
|
|
||||||
|
t.string('username', 64).notNullable().unique();
|
||||||
|
t.text('password_hash').notNullable();
|
||||||
|
t.text('pin_hash').notNullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists('sessions');
|
||||||
|
await knex.schema.dropTableIfExists('login_attempts');
|
||||||
|
|
||||||
|
await knex.schema.createTable('processed_events', (t) => {
|
||||||
|
t.string('event_id', 26).primary();
|
||||||
|
t.string('event_type', 64).notNullable();
|
||||||
|
t.string('payload_hash', 64).notNullable();
|
||||||
|
t.timestamp('processed_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists('processed_events');
|
||||||
|
|
||||||
|
await knex.schema.createTable('sessions', (t) => {
|
||||||
|
t.string('id', 26).primary();
|
||||||
|
t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE');
|
||||||
|
t.text('refresh_token_hash').notNullable();
|
||||||
|
t.string('user_agent').nullable();
|
||||||
|
t.specificType('ip_address', 'inet').nullable();
|
||||||
|
t.timestamp('expires_at', { useTz: true }).notNullable();
|
||||||
|
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.createTable('login_attempts', (t) => {
|
||||||
|
t.string('id', 26).primary();
|
||||||
|
t.string('username', 64).notNullable();
|
||||||
|
t.specificType('ip_address', 'inet').notNullable();
|
||||||
|
t.boolean('success').notNullable().defaultTo(false);
|
||||||
|
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
49
apps/api/src/db/reset-db.ts
Normal file
49
apps/api/src/db/reset-db.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
import knex from 'knex';
|
||||||
|
|
||||||
|
// Load .env from repo root (works when running from apps/api)
|
||||||
|
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
|
||||||
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||||
|
|
||||||
|
const dbName = process.env.DB_NAME || 'cryptowallet_devphase3';
|
||||||
|
if (!/^[a-zA-Z0-9_]+$/.test(dbName)) {
|
||||||
|
console.error('[DB Reset] Invalid DB_NAME');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseConnection = {
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
user: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
const admin = knex({
|
||||||
|
client: 'pg',
|
||||||
|
connection: { ...baseConnection, database: 'postgres' },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await admin.raw(
|
||||||
|
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = ? AND pid <> pg_backend_pid()`,
|
||||||
|
[dbName]
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Ignore if no connections
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeName = dbName.replace(/"/g, '""');
|
||||||
|
await admin.raw(`DROP DATABASE IF EXISTS "${safeName}"`);
|
||||||
|
await admin.raw(`CREATE DATABASE "${safeName}"`);
|
||||||
|
|
||||||
|
await admin.destroy();
|
||||||
|
console.log('[DB Reset] Database dropped and recreated:', dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset().catch((err: unknown) => {
|
||||||
|
console.error('[DB Reset] Failed:', err instanceof Error ? err.message : String(err));
|
||||||
|
if (err instanceof Error && err.stack) console.error(err.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
59
apps/api/src/events/connection.ts
Normal file
59
apps/api/src/events/connection.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import amqplib, { type Channel, type ChannelModel } from 'amqplib';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
|
||||||
|
let connectionModel: ChannelModel | null = null;
|
||||||
|
let channel: Channel | null = null;
|
||||||
|
|
||||||
|
const DLX_EXCHANGE = `${env.rabbitmqExchange}.dlx`;
|
||||||
|
const DLQ_NAME = `${env.rabbitmqWalletQueue}.dlq`;
|
||||||
|
|
||||||
|
export async function createRabbitConnection(): Promise<Channel> {
|
||||||
|
connectionModel = await amqplib.connect(env.rabbitmqUrl);
|
||||||
|
|
||||||
|
connectionModel.on('error', (err) => {
|
||||||
|
console.error('[RabbitMQ] Connection error:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
connectionModel.on('close', () => {
|
||||||
|
console.warn('[RabbitMQ] Connection closed. Reconnecting in 5s...');
|
||||||
|
setTimeout(() => createRabbitConnection().catch(console.error), 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
channel = await connectionModel.createChannel();
|
||||||
|
await channel.prefetch(1);
|
||||||
|
|
||||||
|
// Declare main exchange
|
||||||
|
await channel.assertExchange(env.rabbitmqExchange, 'topic', { durable: true });
|
||||||
|
|
||||||
|
// Declare DLX and DLQ
|
||||||
|
await channel.assertExchange(DLX_EXCHANGE, 'topic', { durable: true });
|
||||||
|
await channel.assertQueue(DLQ_NAME, { durable: true });
|
||||||
|
await channel.bindQueue(DLQ_NAME, DLX_EXCHANGE, '#');
|
||||||
|
|
||||||
|
// Declare main queue with DLX
|
||||||
|
await channel.assertQueue(env.rabbitmqWalletQueue, {
|
||||||
|
durable: true,
|
||||||
|
arguments: {
|
||||||
|
'x-dead-letter-exchange': DLX_EXCHANGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind routing keys
|
||||||
|
await channel.bindQueue(env.rabbitmqWalletQueue, env.rabbitmqExchange, 'user.kyc_verified');
|
||||||
|
await channel.bindQueue(env.rabbitmqWalletQueue, env.rabbitmqExchange, 'user.deleted');
|
||||||
|
|
||||||
|
console.log('[RabbitMQ] Connected and queues declared');
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeRabbitConnection(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (channel) await channel.close();
|
||||||
|
if (connectionModel) await connectionModel.close();
|
||||||
|
} catch {
|
||||||
|
// ignore close errors
|
||||||
|
}
|
||||||
|
channel = null;
|
||||||
|
connectionModel = null;
|
||||||
|
}
|
||||||
121
apps/api/src/events/consumer.ts
Normal file
121
apps/api/src/events/consumer.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import type { Channel, ConsumeMessage } from 'amqplib';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { db } from '../config/database';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
import { handleKycVerified } from './handlers/kyc-verified.handler';
|
||||||
|
import { handleUserDeleted } from './handlers/deleted.handler';
|
||||||
|
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
|
||||||
|
interface BitokEvent {
|
||||||
|
event_id: string;
|
||||||
|
event_type: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
occurred_at: string;
|
||||||
|
schema_version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidEvent(msg: unknown): msg is BitokEvent {
|
||||||
|
if (!msg || typeof msg !== 'object') return false;
|
||||||
|
const e = msg as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof e.event_id === 'string' &&
|
||||||
|
typeof e.event_type === 'string' &&
|
||||||
|
typeof e.payload === 'object' &&
|
||||||
|
e.payload !== null &&
|
||||||
|
typeof e.occurred_at === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRetryCount(msg: ConsumeMessage): number {
|
||||||
|
const xDeath = msg.properties.headers?.['x-death'] as Array<{ count: number }> | undefined;
|
||||||
|
if (!xDeath || xDeath.length === 0) return 0;
|
||||||
|
return xDeath[0].count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashPayload(payload: Record<string, unknown>): string {
|
||||||
|
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isAlreadyProcessed(eventId: string): Promise<boolean> {
|
||||||
|
const row = await db('processed_events').where({ event_id: eventId }).first();
|
||||||
|
return !!row;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markProcessed(eventId: string, eventType: string, payloadHash: string): Promise<void> {
|
||||||
|
await db('processed_events').insert({
|
||||||
|
event_id: eventId,
|
||||||
|
event_type: eventType,
|
||||||
|
payload_hash: payloadHash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startConsumer(channel: Channel): Promise<void> {
|
||||||
|
console.log('[Consumer] Listening on queue:', env.rabbitmqWalletQueue);
|
||||||
|
|
||||||
|
await channel.consume(env.rabbitmqWalletQueue, async (msg) => {
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(msg.content.toString());
|
||||||
|
} catch {
|
||||||
|
console.error('[Consumer] Invalid JSON, nacking without requeue');
|
||||||
|
channel.nack(msg, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidEvent(parsed)) {
|
||||||
|
console.error('[Consumer] Schema validation failed, nacking without requeue');
|
||||||
|
channel.nack(msg, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = parsed;
|
||||||
|
|
||||||
|
// Idempotency check
|
||||||
|
try {
|
||||||
|
if (await isAlreadyProcessed(event.event_id)) {
|
||||||
|
console.log(`[Consumer] Event ${event.event_id} already processed, acking`);
|
||||||
|
channel.ack(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Consumer] DB error checking idempotency, nacking with requeue');
|
||||||
|
channel.nack(msg, false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check retry count
|
||||||
|
const retries = getRetryCount(msg);
|
||||||
|
if (retries >= MAX_RETRIES) {
|
||||||
|
console.error(`[Consumer] Event ${event.event_id} exceeded max retries (${MAX_RETRIES}), sending to DLQ`);
|
||||||
|
channel.nack(msg, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.event_type) {
|
||||||
|
case 'user.kyc_verified':
|
||||||
|
await handleKycVerified(event.payload);
|
||||||
|
break;
|
||||||
|
case 'user.deleted':
|
||||||
|
await handleUserDeleted(event.payload);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[Consumer] Unknown event type: ${event.event_type}, acking`);
|
||||||
|
channel.ack(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadHash = hashPayload(event.payload);
|
||||||
|
await markProcessed(event.event_id, event.event_type, payloadHash);
|
||||||
|
channel.ack(msg);
|
||||||
|
console.log(`[Consumer] Processed event: ${event.event_id} (${event.event_type})`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[Consumer] Handler error for ${event.event_id}:`, err.message);
|
||||||
|
// DB/handler error -- requeue for retry
|
||||||
|
channel.nack(msg, false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
17
apps/api/src/events/handlers/deleted.handler.ts
Normal file
17
apps/api/src/events/handlers/deleted.handler.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { UserModel } from '../../models/user.model';
|
||||||
|
|
||||||
|
interface UserDeletedPayload {
|
||||||
|
bitok_user_id: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUserDeleted(payload: Record<string, unknown>): Promise<void> {
|
||||||
|
const data = payload as unknown as UserDeletedPayload;
|
||||||
|
|
||||||
|
if (!data.bitok_user_id) {
|
||||||
|
throw new Error('Invalid user.deleted payload: missing bitok_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserModel.softDelete(data.bitok_user_id);
|
||||||
|
console.log(`[UserDeleted] Soft-deleted user ${data.bitok_user_id} reason=${data.reason}`);
|
||||||
|
}
|
||||||
18
apps/api/src/events/handlers/kyc-verified.handler.ts
Normal file
18
apps/api/src/events/handlers/kyc-verified.handler.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { UserModel } from '../../models/user.model';
|
||||||
|
|
||||||
|
interface KycVerifiedPayload {
|
||||||
|
bitok_user_id: string;
|
||||||
|
kyc_verified: boolean;
|
||||||
|
kyc_level: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleKycVerified(payload: Record<string, unknown>): Promise<void> {
|
||||||
|
const data = payload as unknown as KycVerifiedPayload;
|
||||||
|
|
||||||
|
if (!data.bitok_user_id || typeof data.kyc_verified !== 'boolean') {
|
||||||
|
throw new Error('Invalid kyc_verified payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserModel.updateKyc(data.bitok_user_id, data.kyc_verified, data.kyc_level || null);
|
||||||
|
console.log(`[KYC] Updated KYC for user ${data.bitok_user_id}: verified=${data.kyc_verified}, level=${data.kyc_level}`);
|
||||||
|
}
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import knex from 'knex';
|
|
||||||
import knexConfig from './db/knexfile';
|
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import { env } from './config/env';
|
import { env, initEnv } from './config/env';
|
||||||
|
import { createRabbitConnection } from './events/connection';
|
||||||
|
import { startConsumer } from './events/consumer';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const db = knex(knexConfig);
|
await initEnv();
|
||||||
|
|
||||||
console.log('[API] Running migrations...');
|
// Start RabbitMQ consumer
|
||||||
await db.migrate.latest();
|
try {
|
||||||
console.log('[API] Migrations complete.');
|
const channel = await createRabbitConnection();
|
||||||
|
await startConsumer(channel);
|
||||||
await db.destroy();
|
console.log('[API] RabbitMQ consumer started');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn('[API] RabbitMQ not available, events will not be consumed:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
app.listen(env.port, () => {
|
app.listen(env.port, () => {
|
||||||
console.log(`[API] Server running on port ${env.port}`);
|
console.log(`[API] Server running on port ${env.port}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch(console.error);
|
||||||
console.error('[API] Failed to start:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import { verifyAccessToken, AuthContext } from '../services/jwt.service';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface Request {
|
|
||||||
auth?: AuthContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractToken(req: Request): string | null {
|
|
||||||
const cookie = req.cookies?.access_token;
|
|
||||||
if (cookie) return cookie;
|
|
||||||
|
|
||||||
const auth = req.headers.authorization;
|
|
||||||
if (auth) {
|
|
||||||
const [scheme, token] = auth.split(' ');
|
|
||||||
if (scheme?.toLowerCase() === 'bearer' && token) return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
||||||
const token = extractToken(req);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
res.status(401).json({ success: false, error: 'Not authenticated' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
req.auth = await verifyAccessToken(token);
|
|
||||||
next();
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(err.status || 401).json({ success: false, error: err.message || 'Invalid token' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
apps/api/src/middleware/bitok-auth.ts
Normal file
60
apps/api/src/middleware/bitok-auth.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { jwtVerify, decodeProtectedHeader } from 'jose';
|
||||||
|
import { getSigningKey } from '../services/jwks.service';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: { bitokUserId: string; email?: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bitokAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
|
const header = req.headers.authorization;
|
||||||
|
if (!header?.startsWith('Bearer ')) {
|
||||||
|
res.status(401).json({ success: false, error: 'No token provided' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = header.slice(7);
|
||||||
|
|
||||||
|
// Decode header to get kid
|
||||||
|
const protectedHeader = decodeProtectedHeader(token);
|
||||||
|
if (protectedHeader.alg !== 'RS256') {
|
||||||
|
res.status(401).json({ success: false, error: 'Invalid token algorithm' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!protectedHeader.kid) {
|
||||||
|
res.status(401).json({ success: false, error: 'Token missing kid' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the signing key for this kid
|
||||||
|
const key = await getSigningKey(protectedHeader.kid);
|
||||||
|
|
||||||
|
// Verify the token
|
||||||
|
const { payload } = await jwtVerify(token, key, {
|
||||||
|
issuer: env.bitokIssuer,
|
||||||
|
audience: env.bitokAudience,
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!payload.sub) {
|
||||||
|
res.status(401).json({ success: false, error: 'Token missing subject' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = {
|
||||||
|
bitokUserId: payload.sub,
|
||||||
|
email: payload.email as string | undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch {
|
||||||
|
res.status(401).json({ success: false, error: 'Invalid or expired token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
25
apps/api/src/middleware/rate-limit.ts
Normal file
25
apps/api/src/middleware/rate-limit.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import rateLimit from 'express-rate-limit';
|
||||||
|
|
||||||
|
export const loginLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 20,
|
||||||
|
message: { success: false, error: 'Too many login attempts, try again later' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
max: 3,
|
||||||
|
message: { success: false, error: 'Too many registration attempts, try again later' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const seedPhraseLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 3,
|
||||||
|
message: { success: false, error: 'Too many attempts. Try again in 15 minutes.' },
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { db } from '../config/database';
|
|
||||||
import { generateUlid } from '../utils/ulid';
|
|
||||||
|
|
||||||
export interface SessionRow {
|
|
||||||
id: string;
|
|
||||||
sid: string;
|
|
||||||
user_id: string;
|
|
||||||
device_id: string | null;
|
|
||||||
user_agent: string | null;
|
|
||||||
first_ip: string | null;
|
|
||||||
last_ip: string | null;
|
|
||||||
last_seen_at: Date | null;
|
|
||||||
revoked_at: Date | null;
|
|
||||||
refresh_jti_hash: string | null;
|
|
||||||
refresh_expires_at: Date | null;
|
|
||||||
created_at: Date;
|
|
||||||
updated_at: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SessionModel = {
|
|
||||||
async findBySid(sid: string): Promise<SessionRow | undefined> {
|
|
||||||
return db('sessions').where({ sid }).whereNull('revoked_at').first();
|
|
||||||
},
|
|
||||||
|
|
||||||
async findByUserId(userId: string): Promise<SessionRow[]> {
|
|
||||||
return db('sessions').where({ user_id: userId }).whereNull('revoked_at');
|
|
||||||
},
|
|
||||||
|
|
||||||
async create(data: {
|
|
||||||
sid: string;
|
|
||||||
user_id: string;
|
|
||||||
device_id?: string;
|
|
||||||
user_agent?: string;
|
|
||||||
first_ip?: string;
|
|
||||||
refresh_jti_hash?: string;
|
|
||||||
refresh_expires_at?: Date;
|
|
||||||
}): Promise<SessionRow> {
|
|
||||||
const [session] = await db('sessions')
|
|
||||||
.insert({
|
|
||||||
id: generateUlid(),
|
|
||||||
...data,
|
|
||||||
last_ip: data.first_ip || null,
|
|
||||||
})
|
|
||||||
.returning('*');
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
|
|
||||||
async revoke(sid: string): Promise<void> {
|
|
||||||
await db('sessions')
|
|
||||||
.where({ sid })
|
|
||||||
.update({ revoked_at: db.fn.now(), updated_at: db.fn.now() });
|
|
||||||
},
|
|
||||||
|
|
||||||
async revokeAllForUser(userId: string): Promise<void> {
|
|
||||||
await db('sessions')
|
|
||||||
.where({ user_id: userId })
|
|
||||||
.whereNull('revoked_at')
|
|
||||||
.update({ revoked_at: db.fn.now(), updated_at: db.fn.now() });
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateLastSeen(sid: string, ip: string): Promise<void> {
|
|
||||||
await db('sessions')
|
|
||||||
.where({ sid })
|
|
||||||
.update({ last_seen_at: db.fn.now(), last_ip: ip, updated_at: db.fn.now() });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -3,47 +3,71 @@ import { generateUlid } from '../utils/ulid';
|
|||||||
|
|
||||||
export interface UserRow {
|
export interface UserRow {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
bitok_user_id: string;
|
||||||
password_hash: string;
|
email: string | null;
|
||||||
last_name: string | null;
|
encrypted_vault: string;
|
||||||
first_name: string | null;
|
vault_salt: string;
|
||||||
middle_name: string | null;
|
mnemonic_shown: boolean;
|
||||||
birth_date: string | null;
|
|
||||||
crypto_wallet: string | null;
|
|
||||||
phone: string | null;
|
|
||||||
bik: string | null;
|
|
||||||
account_number: string | null;
|
|
||||||
card_number: string | null;
|
|
||||||
inn: string | null;
|
|
||||||
kyc_verified: boolean;
|
kyc_verified: boolean;
|
||||||
kyc_verified_at: Date | null;
|
kyc_level: string | null;
|
||||||
is_deleted: boolean;
|
deleted: boolean;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserModel = {
|
export const UserModel = {
|
||||||
async findByEmail(email: string): Promise<UserRow | undefined> {
|
|
||||||
return db('users').where({ email, is_deleted: false }).first();
|
|
||||||
},
|
|
||||||
|
|
||||||
async findById(id: string): Promise<UserRow | undefined> {
|
async findById(id: string): Promise<UserRow | undefined> {
|
||||||
return db('users').where({ id, is_deleted: false }).first();
|
return db('users').where({ id }).first();
|
||||||
},
|
},
|
||||||
|
|
||||||
async create(data: {
|
async findByBitokUserId(bitokUserId: string): Promise<UserRow | undefined> {
|
||||||
email: string;
|
return db('users').where({ bitok_user_id: bitokUserId }).first();
|
||||||
password_hash: string;
|
},
|
||||||
|
|
||||||
|
async createFromBitok(data: {
|
||||||
|
bitokUserId: string;
|
||||||
|
email?: string | null;
|
||||||
|
encryptedVault: string;
|
||||||
|
vaultSalt: string;
|
||||||
}): Promise<UserRow> {
|
}): Promise<UserRow> {
|
||||||
const [user] = await db('users').insert({ id: generateUlid(), ...data }).returning('*');
|
|
||||||
return user;
|
|
||||||
},
|
|
||||||
|
|
||||||
async update(id: string, data: Partial<Omit<UserRow, 'id' | 'created_at'>>): Promise<UserRow | undefined> {
|
|
||||||
const [user] = await db('users')
|
const [user] = await db('users')
|
||||||
.where({ id })
|
.insert({
|
||||||
.update({ ...data, updated_at: db.fn.now() })
|
id: generateUlid(),
|
||||||
|
bitok_user_id: data.bitokUserId,
|
||||||
|
email: data.email || null,
|
||||||
|
encrypted_vault: data.encryptedVault,
|
||||||
|
vault_salt: data.vaultSalt,
|
||||||
|
})
|
||||||
.returning('*');
|
.returning('*');
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async setMnemonicShown(id: string): Promise<void> {
|
||||||
|
await db('users').where({ id }).update({ mnemonic_shown: true, updated_at: db.fn.now() });
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateVault(id: string, encrypted_vault: string, vault_salt: string): Promise<void> {
|
||||||
|
await db('users')
|
||||||
|
.where({ id })
|
||||||
|
.update({ encrypted_vault, vault_salt, updated_at: db.fn.now() });
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateKyc(bitokUserId: string, kycVerified: boolean, kycLevel: string | null): Promise<void> {
|
||||||
|
await db('users')
|
||||||
|
.where({ bitok_user_id: bitokUserId })
|
||||||
|
.update({
|
||||||
|
kyc_verified: kycVerified,
|
||||||
|
kyc_level: kycLevel,
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async softDelete(bitokUserId: string): Promise<void> {
|
||||||
|
await db('users')
|
||||||
|
.where({ bitok_user_id: bitokUserId })
|
||||||
|
.update({
|
||||||
|
deleted: true,
|
||||||
|
updated_at: db.fn.now(),
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
9
apps/api/src/routes/vault.routes.ts
Normal file
9
apps/api/src/routes/vault.routes.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { VaultController } from '../controllers/vault.controller';
|
||||||
|
import { bitokAuth } from '../middleware/bitok-auth';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', bitokAuth, VaultController.getVault);
|
||||||
|
|
||||||
|
export default router;
|
||||||
25
apps/api/src/routes/wallet-setup.routes.ts
Normal file
25
apps/api/src/routes/wallet-setup.routes.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { WalletSetupController } from '../controllers/wallet-setup.controller';
|
||||||
|
import { validate } from '../middleware/validate';
|
||||||
|
import { bitokAuth } from '../middleware/bitok-auth';
|
||||||
|
|
||||||
|
const setupSchema = z.object({
|
||||||
|
encryptedVault: z.string().min(1),
|
||||||
|
vaultSalt: z.string().min(1),
|
||||||
|
wallets: z.array(
|
||||||
|
z.object({
|
||||||
|
chain: z.enum(['ETH', 'BTC', 'SOL', 'TRX', 'BSC']),
|
||||||
|
address: z.string().min(1),
|
||||||
|
derivationPath: z.string().min(1),
|
||||||
|
})
|
||||||
|
).min(4).max(5),
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/setup', bitokAuth, validate(setupSchema), WalletSetupController.setup);
|
||||||
|
router.get('/unlock', bitokAuth, WalletSetupController.unlock);
|
||||||
|
router.post('/confirm-mnemonic', bitokAuth, WalletSetupController.confirmMnemonic);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { WalletController } from '../controllers/wallet.controller';
|
import { WalletController } from '../controllers/wallet.controller';
|
||||||
import { authMiddleware } from '../middleware/auth';
|
import { bitokAuth } from '../middleware/bitok-auth';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', authMiddleware, WalletController.getWallets);
|
router.get('/', bitokAuth, WalletController.getWallets);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
46
apps/api/src/services/jwks.service.ts
Normal file
46
apps/api/src/services/jwks.service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { importJWK, type JWK, type CryptoKey } from 'jose';
|
||||||
|
import { env } from '../config/env';
|
||||||
|
|
||||||
|
interface CachedKey {
|
||||||
|
key: CryptoKey | Uint8Array;
|
||||||
|
fetchedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
const keyCache = new Map<string, CachedKey>();
|
||||||
|
|
||||||
|
async function fetchJwks(): Promise<{ keys: JWK[] }> {
|
||||||
|
const res = await fetch(env.bitokJwksUrl);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to fetch JWKS: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res.json() as Promise<{ keys: JWK[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshKeys(): Promise<void> {
|
||||||
|
const jwks = await fetchJwks();
|
||||||
|
|
||||||
|
for (const jwk of jwks.keys) {
|
||||||
|
if (!jwk.kid) continue;
|
||||||
|
const key = await importJWK(jwk, 'RS256');
|
||||||
|
keyCache.set(jwk.kid, { key, fetchedAt: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSigningKey(kid: string): Promise<CryptoKey | Uint8Array> {
|
||||||
|
const cached = keyCache.get(kid);
|
||||||
|
|
||||||
|
if (cached && Date.now() - cached.fetchedAt < KEY_TTL_MS) {
|
||||||
|
return cached.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown kid or expired -- force refresh
|
||||||
|
await refreshKeys();
|
||||||
|
|
||||||
|
const refreshed = keyCache.get(kid);
|
||||||
|
if (!refreshed) {
|
||||||
|
throw new Error(`No key found for kid: ${kid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshed.key;
|
||||||
|
}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as jose from 'jose';
|
|
||||||
import { env } from '../config/env';
|
|
||||||
|
|
||||||
export interface AccessTokenPayload {
|
|
||||||
sub: string;
|
|
||||||
type: string;
|
|
||||||
sid: string;
|
|
||||||
iat: number;
|
|
||||||
nbf: number;
|
|
||||||
exp: number;
|
|
||||||
iss?: string;
|
|
||||||
aud?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthContext {
|
|
||||||
userId: string;
|
|
||||||
sid: string;
|
|
||||||
token: AccessTokenPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
let jwks: ReturnType<typeof jose.createRemoteJWKSet> | null = null;
|
|
||||||
let localKey: Awaited<ReturnType<typeof jose.importSPKI>> | null = null;
|
|
||||||
|
|
||||||
function getJWKS(): ReturnType<typeof jose.createRemoteJWKSet> {
|
|
||||||
if (!jwks && env.jwt.jwksUrl) {
|
|
||||||
jwks = jose.createRemoteJWKSet(new URL(env.jwt.jwksUrl));
|
|
||||||
}
|
|
||||||
if (!jwks) {
|
|
||||||
throw new Error('JWT_JWKS_URL is not configured');
|
|
||||||
}
|
|
||||||
return jwks;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLocalKey(): Promise<Awaited<ReturnType<typeof jose.importSPKI>>> {
|
|
||||||
if (!localKey && env.jwt.publicKey) {
|
|
||||||
localKey = await jose.importSPKI(env.jwt.publicKey, env.jwt.algorithm);
|
|
||||||
}
|
|
||||||
if (!localKey) {
|
|
||||||
throw new Error('No JWT public key available');
|
|
||||||
}
|
|
||||||
return localKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyAccessToken(token: string): Promise<AuthContext> {
|
|
||||||
let payload: jose.JWTPayload;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verifyOptions: jose.JWTVerifyOptions = {
|
|
||||||
algorithms: [env.jwt.algorithm],
|
|
||||||
clockTolerance: 10,
|
|
||||||
};
|
|
||||||
if (env.jwt.issuer) verifyOptions.issuer = env.jwt.issuer;
|
|
||||||
if (env.jwt.audience) verifyOptions.audience = env.jwt.audience;
|
|
||||||
|
|
||||||
if (env.jwt.jwksUrl) {
|
|
||||||
const result = await jose.jwtVerify(token, getJWKS(), verifyOptions);
|
|
||||||
payload = result.payload;
|
|
||||||
} else {
|
|
||||||
const key = await getLocalKey();
|
|
||||||
const result = await jose.jwtVerify(token, key, verifyOptions);
|
|
||||||
payload = result.payload;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code === 'ERR_JWT_EXPIRED') {
|
|
||||||
throw Object.assign(new Error('Token expired'), { status: 401 });
|
|
||||||
}
|
|
||||||
throw Object.assign(new Error('Invalid token'), { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.type !== 'access') {
|
|
||||||
throw Object.assign(new Error('Invalid token type'), { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!payload.sub || !payload.sid) {
|
|
||||||
throw Object.assign(new Error('Missing token claims'), { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: payload.sub,
|
|
||||||
sid: payload.sid as string,
|
|
||||||
token: {
|
|
||||||
sub: payload.sub,
|
|
||||||
type: payload.type as string,
|
|
||||||
sid: payload.sid as string,
|
|
||||||
iat: payload.iat!,
|
|
||||||
nbf: payload.nbf!,
|
|
||||||
exp: payload.exp!,
|
|
||||||
iss: payload.iss,
|
|
||||||
aud: typeof payload.aud === 'string' ? payload.aud : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
{
|
|
||||||
"openapi": "3.0.0",
|
|
||||||
"info": {
|
|
||||||
"title": "CryptoWallet API",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"description": "Multi-chain cryptocurrency wallet API with blockchain proxy services"
|
|
||||||
},
|
|
||||||
"servers": [
|
|
||||||
{ "url": "/api", "description": "API" }
|
|
||||||
],
|
|
||||||
"components": {
|
|
||||||
"securitySchemes": {
|
|
||||||
"bearerAuth": {
|
|
||||||
"type": "http",
|
|
||||||
"scheme": "bearer",
|
|
||||||
"bearerFormat": "JWT"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {
|
|
||||||
"Error": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": { "type": "boolean", "example": false },
|
|
||||||
"error": { "type": "string" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Wallet": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"chain": { "type": "string", "enum": ["ETH", "BTC", "SOL", "TRX", "BSC"] },
|
|
||||||
"address": { "type": "string" },
|
|
||||||
"derivationPath": { "type": "string" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HealthResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": { "type": "boolean", "example": true },
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"status": { "type": "string", "example": "ok" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"/health": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Health check",
|
|
||||||
"tags": ["System"],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Service is healthy",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": { "$ref": "#/components/schemas/HealthResponse" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/wallets": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Get user wallets",
|
|
||||||
"tags": ["Wallets"],
|
|
||||||
"security": [{ "bearerAuth": [] }],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "List of wallets",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"success": { "type": "boolean", "example": true },
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "$ref": "#/components/schemas/Wallet" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Not authenticated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": { "$ref": "#/components/schemas/Error" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
apps/web/Dockerfile
Normal file
34
apps/web/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy everything (filtered by .dockerignore)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install deps with hoisting
|
||||||
|
RUN echo "node-linker=hoisted" > .npmrc
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Build web
|
||||||
|
ENV NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||||
|
RUN cd apps/web && ../../node_modules/.bin/next build
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy standalone output
|
||||||
|
COPY --from=builder /app/apps/web/.next/standalone ./
|
||||||
|
COPY --from=builder /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
|
COPY --from=builder /app/apps/web/public ./apps/web/public
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["node", "apps/web/server.js"]
|
||||||
@@ -42,6 +42,9 @@ export default function BridgePage() {
|
|||||||
const [amount, setAmount] = useState('');
|
const [amount, setAmount] = useState('');
|
||||||
const [confirmed, setConfirmed] = useState(false);
|
const [confirmed, setConfirmed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) router.push('/login');
|
||||||
|
}, [router, user]);
|
||||||
|
|
||||||
const destChainOptions = useMemo(() => getDestinationChainOptions(sourceChain), [sourceChain]);
|
const destChainOptions = useMemo(() => getDestinationChainOptions(sourceChain), [sourceChain]);
|
||||||
const sourceTokenOptions = useMemo(() => getTokenOptions(sourceChain), [sourceChain]);
|
const sourceTokenOptions = useMemo(() => getTokenOptions(sourceChain), [sourceChain]);
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useBalances } from '@/hooks/useBalances';
|
import { useBalances } from '@/hooks/useBalances';
|
||||||
import type { ChainBalance } from '@/lib/balances/types';
|
import type { ChainBalance } from '@/lib/balances/types';
|
||||||
import { useAuthStore } from '@/store/auth-store';
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { user, wallets } = useAuthStore();
|
const router = useRouter();
|
||||||
|
const { user, wallets, logout } = useAuthStore();
|
||||||
const { portfolio, loading, refreshing, error, refresh } = useBalances();
|
const { portfolio, loading, refreshing, error, refresh } = useBalances();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}, [user, router]);
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 820, margin: '50px auto', padding: 20 }}>
|
<div style={{ maxWidth: 820, margin: '50px auto', padding: 20 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||||
<span>{user?.email || 'Not authenticated'}</span>
|
<span>{user.email}</span>
|
||||||
<Link href="/send" style={navButtonStyle}>
|
<Link href="/send" style={navButtonStyle}>
|
||||||
Send
|
Send
|
||||||
</Link>
|
</Link>
|
||||||
@@ -33,6 +44,9 @@ export default function DashboardPage() {
|
|||||||
<button onClick={() => void refresh()} style={navButtonStyle} disabled={loading || refreshing}>
|
<button onClick={() => void refresh()} style={navButtonStyle} disabled={loading || refreshing}>
|
||||||
{refreshing ? 'Refreshing...' : 'Refresh'}
|
{refreshing ? 'Refreshing...' : 'Refresh'}
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={logout} style={{ padding: '6px 12px' }}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
125
apps/web/src/app/login/page.tsx
Normal file
125
apps/web/src/app/login/page.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
|
|
||||||
|
type Step = 'email' | 'code';
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { loginStart, loginComplete, loading, error, clearError } = useAuthStore();
|
||||||
|
const [step, setStep] = useState<Step>('email');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
|
const handleEmailSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearError();
|
||||||
|
await loginStart(email);
|
||||||
|
|
||||||
|
const state = useAuthStore.getState();
|
||||||
|
if (!state.error) {
|
||||||
|
setStep('code');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCodeSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearError();
|
||||||
|
await loginComplete(email, password, code);
|
||||||
|
|
||||||
|
const state = useAuthStore.getState();
|
||||||
|
if (state.user) {
|
||||||
|
if (!state.mnemonicShown) {
|
||||||
|
router.push('/mnemonic');
|
||||||
|
} else {
|
||||||
|
router.push('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20 }}>
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
{step === 'email' && (
|
||||||
|
<form onSubmit={handleEmailSubmit}>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Email</label><br />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{loading ? 'Sending code...' : 'Send verification code'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'code' && (
|
||||||
|
<form onSubmit={handleCodeSubmit}>
|
||||||
|
<p style={{ color: '#666', fontSize: 13, marginBottom: 16 }}>
|
||||||
|
A verification code was sent to <strong>{email}</strong>.
|
||||||
|
</p>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Verification code</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
maxLength={6}
|
||||||
|
style={{ width: '100%', padding: 8, letterSpacing: 4, fontSize: 18, textAlign: 'center' }}
|
||||||
|
placeholder="000000"
|
||||||
|
inputMode="numeric"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Password</label><br />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{loading ? 'Logging in...' : 'Login'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStep('email'); setCode(''); setPassword(''); clearError(); }}
|
||||||
|
style={{ marginLeft: 8, padding: '8px 16px', background: '#fff', border: '1px solid #ccc', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p style={{ marginTop: 16 }}>
|
||||||
|
No account? <a href="/register">Register</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
apps/web/src/app/mnemonic/page.tsx
Normal file
148
apps/web/src/app/mnemonic/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
|
|
||||||
|
export default function MnemonicPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { mnemonic, wallets, confirmMnemonic } = useAuthStore();
|
||||||
|
const [step, setStep] = useState<'show' | 'verify' | 'keys'>('show');
|
||||||
|
const [answers, setAnswers] = useState<Record<number, string>>({});
|
||||||
|
const [verifyError, setVerifyError] = useState('');
|
||||||
|
|
||||||
|
const words = useMemo(() => mnemonic?.split(' ') || [], [mnemonic]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mnemonic) {
|
||||||
|
router.push('/dashboard');
|
||||||
|
}
|
||||||
|
}, [mnemonic, router]);
|
||||||
|
|
||||||
|
const quizIndices = useMemo(() => {
|
||||||
|
if (words.length < 3) return [];
|
||||||
|
const indices: number[] = [];
|
||||||
|
while (indices.length < 3) {
|
||||||
|
const idx = Math.floor(Math.random() * words.length);
|
||||||
|
if (!indices.includes(idx)) indices.push(idx);
|
||||||
|
}
|
||||||
|
return indices.sort((a, b) => a - b);
|
||||||
|
}, [words.length]);
|
||||||
|
|
||||||
|
if (!mnemonic) return null;
|
||||||
|
|
||||||
|
const handleVerify = () => {
|
||||||
|
setVerifyError('');
|
||||||
|
for (const idx of quizIndices) {
|
||||||
|
if (answers[idx]?.trim().toLowerCase() !== words[idx]) {
|
||||||
|
setVerifyError(`Wrong word for position #${idx + 1}. Try again.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setStep('keys');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
await confirmMnemonic();
|
||||||
|
router.push('/dashboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
void navigator.clipboard.writeText('');
|
||||||
|
} catch {
|
||||||
|
// Document may have lost focus; ignore
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
} catch {
|
||||||
|
// Fallback for older browsers or denied permission
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step === 'show') {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
|
||||||
|
<h1>Save Your Mnemonic Phrase</h1>
|
||||||
|
<p style={{ color: 'red', fontWeight: 'bold' }}>
|
||||||
|
Write these words down and store them safely. You will NOT be able to see them again!
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, margin: '20px 0' }}>
|
||||||
|
{words.map((word, i) => (
|
||||||
|
<div key={i} style={{ padding: 8, border: '1px solid #ccc' }}>
|
||||||
|
<strong>{i + 1}.</strong> {word}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => copyToClipboard(mnemonic)} style={{ padding: '8px 16px', marginRight: 8 }}>
|
||||||
|
Copy Mnemonic
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setStep('verify')} style={{ padding: '8px 16px' }}>
|
||||||
|
Next: Verify
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 'verify') {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 400, margin: '50px auto', padding: 20 }}>
|
||||||
|
<h1>Verify Mnemonic</h1>
|
||||||
|
<p>Enter the following words from your mnemonic to confirm you saved it.</p>
|
||||||
|
{quizIndices.map((idx) => (
|
||||||
|
<div key={idx} style={{ marginBottom: 12 }}>
|
||||||
|
<label>Word #{idx + 1}</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={answers[idx] || ''}
|
||||||
|
onChange={(e) => setAnswers({ ...answers, [idx]: e.target.value })}
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{verifyError && <p style={{ color: 'red' }}>{verifyError}</p>}
|
||||||
|
<button onClick={() => setStep('show')} style={{ padding: '8px 16px', marginRight: 8 }}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button onClick={handleVerify} style={{ padding: '8px 16px' }}>
|
||||||
|
Verify
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 600, margin: '50px auto', padding: 20 }}>
|
||||||
|
<h1>Your Private Keys</h1>
|
||||||
|
<p style={{ color: 'red', fontWeight: 'bold' }}>
|
||||||
|
Save these private keys. They will NOT be shown again!
|
||||||
|
</p>
|
||||||
|
{wallets.map((w) => (
|
||||||
|
<div key={w.chain} style={{ border: '1px solid #ccc', padding: 12, marginBottom: 12 }}>
|
||||||
|
<h3>{w.chain}</h3>
|
||||||
|
<p><strong>Address:</strong> {w.address}</p>
|
||||||
|
<p style={{ wordBreak: 'break-all' }}>
|
||||||
|
<strong>Private Key:</strong> {w.privateKey}
|
||||||
|
</p>
|
||||||
|
<button onClick={() => copyToClipboard(w.privateKey)} style={{ padding: '4px 12px' }}>
|
||||||
|
Copy Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="confirm-checkbox" />
|
||||||
|
{' '}I have saved all my private keys
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
style={{ padding: '8px 24px', marginTop: 12 }}
|
||||||
|
>
|
||||||
|
Continue to Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
redirect('/dashboard');
|
redirect('/login');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export default function ReceivePage() {
|
|||||||
const [amount, setAmount] = useState('');
|
const [amount, setAmount] = useState('');
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) router.push('/login');
|
||||||
|
}, [user, router]);
|
||||||
|
|
||||||
// Reset token when chain changes
|
// Reset token when chain changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
144
apps/web/src/app/register/page.tsx
Normal file
144
apps/web/src/app/register/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
|
|
||||||
|
type Step = 'email' | 'code';
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { registerStart, registerComplete, loading, error, clearError } = useAuthStore();
|
||||||
|
const [step, setStep] = useState<Step>('email');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [code, setCode] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleEmailSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearError();
|
||||||
|
setLocalError(null);
|
||||||
|
await registerStart(email);
|
||||||
|
|
||||||
|
const state = useAuthStore.getState();
|
||||||
|
if (!state.error) {
|
||||||
|
setStep('code');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCodeSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearError();
|
||||||
|
setLocalError(null);
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setLocalError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerComplete(email, password, code);
|
||||||
|
|
||||||
|
const state = useAuthStore.getState();
|
||||||
|
if (state.user) {
|
||||||
|
router.push('/mnemonic');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayError = localError || error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20 }}>
|
||||||
|
<h1>Register</h1>
|
||||||
|
|
||||||
|
{step === 'email' && (
|
||||||
|
<form onSubmit={handleEmailSubmit}>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Email</label><br />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{displayError && <p style={{ color: 'red' }}>{displayError}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{loading ? 'Sending code...' : 'Send verification code'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 'code' && (
|
||||||
|
<form onSubmit={handleCodeSubmit}>
|
||||||
|
<p style={{ color: '#666', fontSize: 13, marginBottom: 16 }}>
|
||||||
|
A verification code was sent to <strong>{email}</strong>.
|
||||||
|
</p>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Verification code</label><br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={code}
|
||||||
|
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
maxLength={6}
|
||||||
|
style={{ width: '100%', padding: 8, letterSpacing: 4, fontSize: 18, textAlign: 'center' }}
|
||||||
|
placeholder="000000"
|
||||||
|
inputMode="numeric"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Password (min 8 characters)</label><br />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<label>Confirm password</label><br />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
style={{ width: '100%', padding: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{displayError && <p style={{ color: 'red' }}>{displayError}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
style={{ padding: '8px 24px', background: '#007bff', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{loading ? 'Creating account...' : 'Register'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStep('email'); setCode(''); setPassword(''); setConfirmPassword(''); clearError(); setLocalError(null); }}
|
||||||
|
style={{ marginLeft: 8, padding: '8px 16px', background: '#fff', border: '1px solid #ccc', borderRadius: 4, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p style={{ marginTop: 16 }}>
|
||||||
|
Already have an account? <a href="/login">Login</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ export default function SendPage() {
|
|||||||
const [result, setResult] = useState<SendResult | null>(null);
|
const [result, setResult] = useState<SendResult | null>(null);
|
||||||
const [scannerOpen, setScannerOpen] = useState(false);
|
const [scannerOpen, setScannerOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) router.push('/login');
|
||||||
|
}, [user, router]);
|
||||||
|
|
||||||
// Reset token on chain change
|
// Reset token on chain change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useAuthStore } from '@/store/auth-store';
|
import { useAuthStore } from '@/store/auth-store';
|
||||||
|
import { SeedPhraseModal } from '@/components/SeedPhraseModal';
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
const router = useRouter();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
|
const [showSeedModal, setShowSeedModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}, [user, router]);
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 520, margin: '50px auto', padding: 20 }}>
|
<div style={{ maxWidth: 520, margin: '50px auto', padding: 20 }}>
|
||||||
@@ -16,15 +28,40 @@ export default function SettingsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Security Section */}
|
||||||
|
<div style={{ border: '1px solid #ccc', borderRadius: 4, padding: 16, marginBottom: 16 }}>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Security</h3>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
|
||||||
|
<div>
|
||||||
|
<p style={{ margin: 0, fontWeight: 600 }}>Seed Phrase</p>
|
||||||
|
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>
|
||||||
|
View your 12-word recovery phrase. Requires password verification.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSeedModal(true)}
|
||||||
|
style={primaryButtonStyle}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Account Section */}
|
{/* Account Section */}
|
||||||
<div style={{ border: '1px solid #ccc', borderRadius: 4, padding: 16, marginBottom: 16 }}>
|
<div style={{ border: '1px solid #ccc', borderRadius: 4, padding: 16, marginBottom: 16 }}>
|
||||||
<h3 style={{ marginTop: 0 }}>Account</h3>
|
<h3 style={{ marginTop: 0 }}>Account</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p style={{ margin: 0, fontWeight: 600 }}>Email</p>
|
<p style={{ margin: 0, fontWeight: 600 }}>Email</p>
|
||||||
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>{user?.email || 'Not authenticated'}</p>
|
<p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>{user.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SeedPhraseModal
|
||||||
|
isOpen={showSeedModal}
|
||||||
|
onClose={() => setShowSeedModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,3 +80,15 @@ const navButtonStyle: React.CSSProperties = {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const primaryButtonStyle: React.CSSProperties = {
|
||||||
|
padding: '8px 16px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: '#333',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
};
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export default function SwapPage() {
|
|||||||
[chain, amount, fromSymbol, slippageBps, toSymbol]
|
[chain, amount, fromSymbol, slippageBps, toSymbol]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
router.push('/login');
|
||||||
|
}
|
||||||
|
}, [router, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
estimateOutput(request);
|
estimateOutput(request);
|
||||||
|
|||||||
283
apps/web/src/components/SeedPhraseModal.tsx
Normal file
283
apps/web/src/components/SeedPhraseModal.tsx
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { walletApi } from '@/lib/api';
|
||||||
|
import { decryptVault } from '@/lib/crypto/vault';
|
||||||
|
|
||||||
|
interface SeedPhraseModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTO_HIDE_SECONDS = 60;
|
||||||
|
const CLIPBOARD_CLEAR_SECONDS = 30;
|
||||||
|
|
||||||
|
export function SeedPhraseModal({ isOpen, onClose }: SeedPhraseModalProps) {
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [mnemonic, setMnemonic] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(AUTO_HIDE_SECONDS);
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const clipboardTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const clearSensitiveData = useCallback(() => {
|
||||||
|
setPassword('');
|
||||||
|
setMnemonic(null);
|
||||||
|
setError(null);
|
||||||
|
setRevealed(false);
|
||||||
|
setCountdown(AUTO_HIDE_SECONDS);
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
if (clipboardTimerRef.current) {
|
||||||
|
clearTimeout(clipboardTimerRef.current);
|
||||||
|
clipboardTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
clearSensitiveData();
|
||||||
|
onClose();
|
||||||
|
}, [clearSensitiveData, onClose]);
|
||||||
|
|
||||||
|
// Auto-hide countdown
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mnemonic) return;
|
||||||
|
|
||||||
|
setCountdown(AUTO_HIDE_SECONDS);
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
setCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
handleClose();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [mnemonic, handleClose]);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearSensitiveData();
|
||||||
|
};
|
||||||
|
}, [clearSensitiveData]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get vault data from backend
|
||||||
|
const result = await walletApi.unlock();
|
||||||
|
|
||||||
|
// Attempt client-side decryption with password only
|
||||||
|
const decrypted = await decryptVault(
|
||||||
|
result.encryptedVault,
|
||||||
|
result.vaultSalt,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
|
||||||
|
setMnemonic(decrypted);
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg = err?.message || '';
|
||||||
|
if (msg.includes('Too many attempts')) {
|
||||||
|
setError('Too many attempts. Try again in 15 minutes.');
|
||||||
|
} else {
|
||||||
|
setError('Wrong password');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!mnemonic) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(mnemonic);
|
||||||
|
|
||||||
|
clipboardTimerRef.current = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
void navigator.clipboard.writeText('');
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}, CLIPBOARD_CLEAR_SECONDS * 1000);
|
||||||
|
} catch {
|
||||||
|
// Clipboard API not available
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const words = mnemonic?.split(' ') || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={overlayStyle} onClick={handleClose}>
|
||||||
|
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<h3 style={{ margin: 0 }}>
|
||||||
|
{mnemonic ? 'Seed Phrase' : 'Verify Identity'}
|
||||||
|
</h3>
|
||||||
|
<button onClick={handleClose} style={{ padding: '4px 8px', cursor: 'pointer' }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!mnemonic ? (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<p style={{ color: '#666', fontSize: 13 }}>
|
||||||
|
Enter your password to view your seed phrase.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', fontSize: 13, marginBottom: 4 }}>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
style={inputStyle}
|
||||||
|
placeholder="Enter password"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p style={{ color: 'red', fontSize: 13, margin: '0 0 12px' }}>{error}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleVerify}
|
||||||
|
disabled={loading || password.length < 8}
|
||||||
|
style={{
|
||||||
|
...primaryButtonStyle,
|
||||||
|
width: '100%',
|
||||||
|
opacity: loading || password.length < 8 ? 0.5 : 1,
|
||||||
|
cursor: loading || password.length < 8 ? 'not-allowed' : 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Verifying...' : 'Verify & Show Seed Phrase'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
}}>
|
||||||
|
<p style={{ color: '#b45309', fontSize: 13, margin: 0, fontWeight: 600 }}>
|
||||||
|
Auto-hide in {countdown}s
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setRevealed(!revealed)}
|
||||||
|
style={navButtonStyle}
|
||||||
|
>
|
||||||
|
{revealed ? 'Hide' : 'Reveal'}
|
||||||
|
</button>
|
||||||
|
<button onClick={handleCopy} style={navButtonStyle}>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr 1fr',
|
||||||
|
gap: 8,
|
||||||
|
}}>
|
||||||
|
{words.map((word, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
<span style={{ color: '#666', fontSize: 11 }}>{i + 1}.</span>{' '}
|
||||||
|
<span style={{
|
||||||
|
filter: revealed ? 'none' : 'blur(6px)',
|
||||||
|
transition: 'filter 0.2s',
|
||||||
|
userSelect: revealed ? 'text' : 'none',
|
||||||
|
}}>
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ color: '#666', fontSize: 11, marginTop: 12, textAlign: 'center' }}>
|
||||||
|
Clipboard will be cleared in {CLIPBOARD_CLEAR_SECONDS}s after copying.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Styles --
|
||||||
|
|
||||||
|
const overlayStyle: React.CSSProperties = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalStyle: React.CSSProperties = {
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
maxWidth: 480,
|
||||||
|
width: '90%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyle: React.CSSProperties = {
|
||||||
|
width: '100%',
|
||||||
|
padding: '8px 12px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 14,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
};
|
||||||
|
|
||||||
|
const navButtonStyle: React.CSSProperties = {
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '6px 12px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: '#fff',
|
||||||
|
fontSize: 13,
|
||||||
|
};
|
||||||
|
|
||||||
|
const primaryButtonStyle: React.CSSProperties = {
|
||||||
|
padding: '10px 16px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: '#333',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
};
|
||||||
@@ -1,13 +1,84 @@
|
|||||||
import { webEnv } from './env';
|
import { webEnv } from './env';
|
||||||
|
|
||||||
const API_URL = webEnv.apiUrl;
|
const API_URL = webEnv.apiUrl;
|
||||||
|
const BITOK_BASE = process.env.NEXT_PUBLIC_BITOK_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
let accessToken: string | null = null;
|
||||||
|
|
||||||
|
export function setAccessToken(token: string | null) {
|
||||||
|
accessToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessToken() {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BITOK auth calls (httpOnly cookies + access_token in body) ──
|
||||||
|
|
||||||
|
async function bitokRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(options.headers as Record<string, string>),
|
...(options.headers as Record<string, string>),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${BITOK_BASE}${path}`, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(body.detail || body.error || `Request failed (${res.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bitokAuth = {
|
||||||
|
registrationStart: (email: string) =>
|
||||||
|
bitokRequest<{ success: boolean }>('/v1/auth/registration/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
registrationComplete: (email: string, password: string, code: string) =>
|
||||||
|
bitokRequest<{ id: string; email: string; access_token: string }>('/v1/auth/registration/complete', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password, code }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
loginStart: (email: string) =>
|
||||||
|
bitokRequest<{ success: boolean }>('/v1/auth/login/start', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
loginComplete: (email: string, password: string, code: string) =>
|
||||||
|
bitokRequest<{ id: string; email: string; access_token: string }>('/v1/auth/login/complete', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password, code }),
|
||||||
|
}),
|
||||||
|
|
||||||
|
refresh: () =>
|
||||||
|
bitokRequest<{ result: boolean; access_token: string }>('/v1/jwt/refresh', { method: 'POST' }),
|
||||||
|
|
||||||
|
logout: () =>
|
||||||
|
bitokRequest<{ ok: boolean }>('/v1/auth/logout', { method: 'POST' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Wallet API calls (uses Bearer token from BITOK) ──
|
||||||
|
|
||||||
|
async function walletRequest<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options.headers as Record<string, string>),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}${path}`, {
|
const res = await fetch(`${API_URL}${path}`, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers,
|
||||||
@@ -23,6 +94,40 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export interface WalletSetupPayload {
|
||||||
getWallets: () => request<any>('/api/wallets'),
|
encryptedVault: string;
|
||||||
|
vaultSalt: string;
|
||||||
|
wallets: { chain: string; address: string; derivationPath: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletUnlockResponse {
|
||||||
|
encryptedVault: string;
|
||||||
|
vaultSalt: string;
|
||||||
|
wallets: { chain: string; address: string; derivationPath: string }[];
|
||||||
|
mnemonicShown: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const walletApi = {
|
||||||
|
setup: (data: WalletSetupPayload) =>
|
||||||
|
walletRequest<any>('/api/wallet/setup', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
|
||||||
|
unlock: () =>
|
||||||
|
walletRequest<WalletUnlockResponse>('/api/wallet/unlock'),
|
||||||
|
|
||||||
|
getWallets: () => walletRequest<any>('/api/wallets'),
|
||||||
|
|
||||||
|
confirmMnemonic: () =>
|
||||||
|
walletRequest<any>('/api/wallet/confirm-mnemonic', { method: 'POST' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Legacy api object (keep for components that still reference it) ──
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
getWallets: () => walletRequest<any>('/api/wallets'),
|
||||||
|
getVault: () => walletRequest<any>('/api/vault'),
|
||||||
|
confirmMnemonic: () =>
|
||||||
|
walletRequest<any>('/api/wallet/confirm-mnemonic', { method: 'POST' }),
|
||||||
};
|
};
|
||||||
|
|||||||
54
apps/web/src/lib/crypto/derive-keys.ts
Normal file
54
apps/web/src/lib/crypto/derive-keys.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { generateMnemonic, mnemonicToSeedBytes } from './mnemonic';
|
||||||
|
import { deriveEthWallet } from './eth';
|
||||||
|
import { deriveBtcWallet } from './btc';
|
||||||
|
import { deriveSolWallet } from './sol';
|
||||||
|
import { deriveTrxWallet } from './trx';
|
||||||
|
|
||||||
|
export type Chain = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
||||||
|
|
||||||
|
const DERIVATION_PATHS: Record<Chain, string> = {
|
||||||
|
ETH: "m/44'/60'/0'/0/0",
|
||||||
|
BTC: "m/84'/0'/0'/0/0",
|
||||||
|
SOL: "m/44'/501'/0'/0'",
|
||||||
|
TRX: "m/44'/195'/0'/0/0",
|
||||||
|
BSC: "m/44'/60'/0'/0/0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DerivedWallet {
|
||||||
|
chain: Chain;
|
||||||
|
address: string;
|
||||||
|
privateKey: string;
|
||||||
|
derivationPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DerivedKeys {
|
||||||
|
mnemonic: string;
|
||||||
|
wallets: DerivedWallet[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateWallets(): Promise<DerivedKeys> {
|
||||||
|
const mnemonic = generateMnemonic();
|
||||||
|
return deriveWalletsFromMnemonic(mnemonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deriveWalletsFromMnemonic(mnemonic: string): Promise<DerivedKeys> {
|
||||||
|
const seed = await mnemonicToSeedBytes(mnemonic);
|
||||||
|
|
||||||
|
const eth = deriveEthWallet(mnemonic);
|
||||||
|
const btc = deriveBtcWallet(seed);
|
||||||
|
const sol = deriveSolWallet(seed);
|
||||||
|
const trx = deriveTrxWallet(mnemonic);
|
||||||
|
// BSC uses the same secp256k1 key as ETH (identical derivation path m/44'/60'/0'/0/0)
|
||||||
|
const bsc = deriveEthWallet(mnemonic);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mnemonic,
|
||||||
|
wallets: [
|
||||||
|
{ chain: 'ETH', address: eth.address, privateKey: eth.privateKey, derivationPath: DERIVATION_PATHS.ETH },
|
||||||
|
{ chain: 'BTC', address: btc.address, privateKey: btc.privateKey, derivationPath: DERIVATION_PATHS.BTC },
|
||||||
|
{ chain: 'SOL', address: sol.address, privateKey: sol.privateKey, derivationPath: DERIVATION_PATHS.SOL },
|
||||||
|
{ chain: 'TRX', address: trx.address, privateKey: trx.privateKey, derivationPath: DERIVATION_PATHS.TRX },
|
||||||
|
{ chain: 'BSC', address: bsc.address, privateKey: bsc.privateKey, derivationPath: DERIVATION_PATHS.BSC },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
107
apps/web/src/lib/crypto/vault.ts
Normal file
107
apps/web/src/lib/crypto/vault.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
const PBKDF2_ITERATIONS = 600_000;
|
||||||
|
|
||||||
|
export async function encryptVault(
|
||||||
|
mnemonic: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<{ encryptedVault: string; vaultSalt: string }> {
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(32));
|
||||||
|
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(password),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
const aesKey = await crypto.subtle.deriveKey(
|
||||||
|
{ name: 'PBKDF2', salt: salt as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
|
const ciphertext = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv as BufferSource },
|
||||||
|
aesKey,
|
||||||
|
new TextEncoder().encode(mnemonic)
|
||||||
|
);
|
||||||
|
|
||||||
|
const blob = new Uint8Array(iv.length + ciphertext.byteLength);
|
||||||
|
blob.set(iv, 0);
|
||||||
|
blob.set(new Uint8Array(ciphertext), iv.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
encryptedVault: uint8ToBase64(blob),
|
||||||
|
vaultSalt: uint8ToHex(salt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decryptVault(
|
||||||
|
encryptedVault: string,
|
||||||
|
vaultSalt: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const salt = hexToUint8(vaultSalt);
|
||||||
|
const raw = base64ToUint8(encryptedVault);
|
||||||
|
|
||||||
|
const iv = raw.slice(0, 12);
|
||||||
|
const ciphertextWithTag = raw.slice(12);
|
||||||
|
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(password),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveKey']
|
||||||
|
);
|
||||||
|
|
||||||
|
const aesKey = await crypto.subtle.deriveKey(
|
||||||
|
{ name: 'PBKDF2', salt: salt as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
|
||||||
|
keyMaterial,
|
||||||
|
{ name: 'AES-GCM', length: 256 },
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
|
||||||
|
const plaintext = await crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: iv as BufferSource },
|
||||||
|
aesKey,
|
||||||
|
ciphertextWithTag
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ToBase64(arr: Uint8Array): string {
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
binary += String.fromCharCode(arr[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToUint8(b64: string): Uint8Array {
|
||||||
|
const binary = atob(b64);
|
||||||
|
const arr = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
arr[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uint8ToHex(arr: Uint8Array): string {
|
||||||
|
return Array.from(arr)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToUint8(hex: string): Uint8Array {
|
||||||
|
const arr = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
arr[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
@@ -1,48 +1,137 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { api } from '@/lib/api';
|
import { bitokAuth, walletApi, setAccessToken } from '@/lib/api';
|
||||||
|
import { encryptVault, decryptVault } from '@/lib/crypto/vault';
|
||||||
interface Wallet {
|
import { generateWallets, deriveWalletsFromMnemonic, type DerivedWallet } from '@/lib/crypto/derive-keys';
|
||||||
chain: string;
|
|
||||||
address: string;
|
|
||||||
derivationPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
user: { id: string; email: string } | null;
|
user: { id: string; email: string } | null;
|
||||||
wallets: Wallet[];
|
wallets: DerivedWallet[];
|
||||||
|
mnemonic: string | null;
|
||||||
|
mnemonicShown: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
init: () => Promise<void>;
|
// 2-step registration
|
||||||
|
registerStart: (email: string) => Promise<void>;
|
||||||
|
registerComplete: (email: string, password: string, code: string) => Promise<void>;
|
||||||
|
|
||||||
|
// 2-step login
|
||||||
|
loginStart: (email: string) => Promise<void>;
|
||||||
|
loginComplete: (email: string, password: string, code: string) => Promise<void>;
|
||||||
|
|
||||||
|
confirmMnemonic: () => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
clearMnemonic: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||||
user: null,
|
user: null,
|
||||||
wallets: [],
|
wallets: [],
|
||||||
|
mnemonic: null,
|
||||||
|
mnemonicShown: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
init: async () => {
|
registerStart: async (email) => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const wallets = await api.getWallets();
|
await bitokAuth.registrationStart(email);
|
||||||
|
set({ loading: false });
|
||||||
|
} catch (err: any) {
|
||||||
|
set({ loading: false, error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
registerComplete: async (email, password, code) => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
// Step 1: Complete BITOK registration, get JWT
|
||||||
|
const authData = await bitokAuth.registrationComplete(email, password, code);
|
||||||
|
setAccessToken(authData.access_token);
|
||||||
|
|
||||||
|
// Step 2: Generate mnemonic & derive wallets
|
||||||
|
const { mnemonic, wallets } = await generateWallets();
|
||||||
|
|
||||||
|
// Step 3: Encrypt vault with password only
|
||||||
|
const { encryptedVault, vaultSalt } = await encryptVault(mnemonic, password);
|
||||||
|
|
||||||
|
// Step 4: Send wallet data to backend
|
||||||
|
await walletApi.setup({
|
||||||
|
encryptedVault,
|
||||||
|
vaultSalt,
|
||||||
|
wallets: wallets.map((w) => ({
|
||||||
|
chain: w.chain,
|
||||||
|
address: w.address,
|
||||||
|
derivationPath: w.derivationPath,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
set({
|
set({
|
||||||
user: { id: '', email: '' },
|
user: { id: authData.id, email: authData.email },
|
||||||
wallets,
|
wallets,
|
||||||
|
mnemonic,
|
||||||
|
mnemonicShown: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
set({ user: null, wallets: [], loading: false });
|
set({ loading: false, error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loginStart: async (email) => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
await bitokAuth.loginStart(email);
|
||||||
|
set({ loading: false });
|
||||||
|
} catch (err: any) {
|
||||||
|
set({ loading: false, error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loginComplete: async (email, password, code) => {
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
try {
|
||||||
|
// Step 1: Complete BITOK login, get JWT
|
||||||
|
const authData = await bitokAuth.loginComplete(email, password, code);
|
||||||
|
setAccessToken(authData.access_token);
|
||||||
|
|
||||||
|
// Step 2: Get vault data from wallet API
|
||||||
|
const vaultData = await walletApi.unlock();
|
||||||
|
|
||||||
|
// Step 3: Decrypt vault client-side with password only
|
||||||
|
const mnemonic = await decryptVault(vaultData.encryptedVault, vaultData.vaultSalt, password);
|
||||||
|
const { wallets } = await deriveWalletsFromMnemonic(mnemonic);
|
||||||
|
|
||||||
|
set({
|
||||||
|
user: { id: authData.id, email: authData.email },
|
||||||
|
wallets,
|
||||||
|
mnemonic: vaultData.mnemonicShown ? null : mnemonic,
|
||||||
|
mnemonicShown: vaultData.mnemonicShown,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
set({ loading: false, error: err.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmMnemonic: async () => {
|
||||||
|
try {
|
||||||
|
await walletApi.confirmMnemonic();
|
||||||
|
set({ mnemonicShown: true, mnemonic: null });
|
||||||
|
} catch (err: any) {
|
||||||
|
set({ error: err.message });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
set({ user: null, wallets: [] });
|
bitokAuth.logout().catch(() => {});
|
||||||
|
setAccessToken(null);
|
||||||
|
set({ user: null, wallets: [], mnemonic: null, mnemonicShown: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearMnemonic: () => set({ mnemonic: null }),
|
||||||
clearError: () => set({ error: null }),
|
clearError: () => set({ error: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
35
docker-check.bat
Normal file
35
docker-check.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === Docker diagnostic ===
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 1. Docker version:
|
||||||
|
docker version
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 2. Docker Compose:
|
||||||
|
docker compose version 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo docker compose failed, trying docker-compose...
|
||||||
|
docker-compose --version 2>nul
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 3. Docker context:
|
||||||
|
docker context show
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 4. Docker info ^(engine status^):
|
||||||
|
docker info 2>&1 | findstr /C:"Server Version" /C:"ERROR" /C:"cannot" /C:"failed"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo 5. Attempting: docker compose up -d postgres
|
||||||
|
echo.
|
||||||
|
docker compose up -d postgres
|
||||||
|
echo.
|
||||||
|
echo Exit code: %errorlevel%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pause
|
||||||
179
docker-compose.yml
Normal file
179
docker-compose.yml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: cryptowallet-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: cryptowallet_devphase3
|
||||||
|
ports:
|
||||||
|
- '5432:5432'
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
vault:
|
||||||
|
image: hashicorp/vault:1.18
|
||||||
|
container_name: cryptowallet-vault
|
||||||
|
cap_add:
|
||||||
|
- IPC_LOCK
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: "http://127.0.0.1:8200"
|
||||||
|
ports:
|
||||||
|
- '8200:8200'
|
||||||
|
volumes:
|
||||||
|
- ./vault/vault.hcl:/vault/config/vault.hcl:ro
|
||||||
|
- vault_data:/vault/file
|
||||||
|
command: ["vault", "server", "-config=/vault/config/vault.hcl"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8200/v1/sys/seal-status > /dev/null 2>&1 || exit 0"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
vault-init:
|
||||||
|
image: hashicorp/vault:1.18
|
||||||
|
container_name: cryptowallet-vault-init
|
||||||
|
entrypoint: /bin/sh
|
||||||
|
command: ["/scripts/vault-init.sh"]
|
||||||
|
volumes:
|
||||||
|
- ./scripts/vault-init.sh:/scripts/vault-init.sh:ro
|
||||||
|
- vault_data:/vault/file
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: http://vault:8200
|
||||||
|
depends_on:
|
||||||
|
vault:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/api/Dockerfile
|
||||||
|
container_name: cryptowallet-api
|
||||||
|
ports:
|
||||||
|
- '3001:3001'
|
||||||
|
volumes:
|
||||||
|
- vault_data:/vault/file:ro
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: http://vault:8200
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: postgres
|
||||||
|
DB_NAME: cryptowallet_devphase3
|
||||||
|
API_PORT: 3001
|
||||||
|
FRONTEND_URL: http://localhost:3000
|
||||||
|
RELAY_API_KEY: ${RELAY_API_KEY:-}
|
||||||
|
BITOK_JWKS_URL: http://bitok-auth:8000/.well-known/jwks.json
|
||||||
|
BITOK_ISSUER: auth-service
|
||||||
|
BITOK_AUDIENCE: wallet-service
|
||||||
|
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
vault-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
rabbitmq:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/web/Dockerfile
|
||||||
|
container_name: cryptowallet-web
|
||||||
|
ports:
|
||||||
|
- '3000:3000'
|
||||||
|
environment:
|
||||||
|
NEXT_PUBLIC_API_URL: http://localhost:3001
|
||||||
|
NEXT_PUBLIC_BITOK_URL: http://localhost:8000
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3.13-management-alpine
|
||||||
|
container_name: cryptowallet-rabbitmq
|
||||||
|
ports:
|
||||||
|
- '5672:5672'
|
||||||
|
- '15672:15672'
|
||||||
|
environment:
|
||||||
|
RABBITMQ_DEFAULT_USER: guest
|
||||||
|
RABBITMQ_DEFAULT_PASS: guest
|
||||||
|
volumes:
|
||||||
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
keydb:
|
||||||
|
image: eqalpha/keydb
|
||||||
|
container_name: cryptowallet-keydb
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
volumes:
|
||||||
|
- keydb_data:/data
|
||||||
|
command:
|
||||||
|
- keydb-server
|
||||||
|
- --requirepass
|
||||||
|
- keydb
|
||||||
|
- --appendonly
|
||||||
|
- "yes"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "keydb", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
bitok-auth:
|
||||||
|
build:
|
||||||
|
context: ./BITOK
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: cryptowallet-bitok
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
volumes:
|
||||||
|
- vault_data:/vault/file:ro
|
||||||
|
- ./scripts/bitok-entrypoint.sh:/app/entrypoint.sh:ro
|
||||||
|
entrypoint: ["sh", "/app/entrypoint.sh"]
|
||||||
|
environment:
|
||||||
|
VAULT_ADDR: http://vault:8200
|
||||||
|
VAULT_MOUNT_POINT: secrets
|
||||||
|
REDIS_HOST: keydb
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: keydb
|
||||||
|
JWT_AUDIENCE: "bitforce,wallet-service"
|
||||||
|
JWT_ISSUER: auth-service
|
||||||
|
CORS_ORIGINS: "http://localhost:3000,http://localhost:8000"
|
||||||
|
RABBIT_HOST: rabbitmq
|
||||||
|
RABBIT_PORT: 5672
|
||||||
|
RABBIT_USER: guest
|
||||||
|
RABBIT_PASSWORD: guest
|
||||||
|
RABBIT_EVENTS_EXCHANGE: bitok.events
|
||||||
|
RABBIT_EMAIL_CODE_QUEUE: email.verification_code
|
||||||
|
OUTBOX_POLL_INTERVAL_MS: 500
|
||||||
|
APP_HOST: "0.0.0.0"
|
||||||
|
APP_PORT: "8000"
|
||||||
|
APP_WORKERS: "1"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
vault-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
keydb:
|
||||||
|
condition: service_healthy
|
||||||
|
rabbitmq:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
vault_data:
|
||||||
|
rabbitmq_data:
|
||||||
|
keydb_data:
|
||||||
43
docs/plans/2026-03-14-tron-backend-proxy-design.md
Normal file
43
docs/plans/2026-03-14-tron-backend-proxy-design.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# TRON Backend Proxy Design
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Frontend calls TronGrid API directly from the browser. This causes:
|
||||||
|
- 429 rate-limit errors (API key passed as query param, not recognized properly)
|
||||||
|
- API key exposed in `NEXT_PUBLIC_` env var (visible to clients)
|
||||||
|
- CORS issues possible depending on browser/TronGrid config
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Route TRON balance requests through the backend API proxy, matching the existing relay-proxy pattern.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser -> GET /api/tron/account/:address -> Express API -> GET https://api.trongrid.io/v1/accounts/:address
|
||||||
|
Header: TRON-PRO-API-KEY: <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
1. **New file: `apps/api/src/routes/tron-proxy.routes.ts`**
|
||||||
|
- `GET /account/:address` - proxies to TronGrid `/v1/accounts/:address`
|
||||||
|
- Validates address format (starts with T, 34 chars, base58)
|
||||||
|
- Sends `TRON-PRO-API-KEY` header (correct TronGrid auth method)
|
||||||
|
- 10s timeout with AbortController
|
||||||
|
- Returns TronGrid JSON response as-is
|
||||||
|
|
||||||
|
2. **`apps/api/src/config/env.ts`** - add `tronApiKey` field
|
||||||
|
3. **`apps/api/src/app.ts`** - register `/api/tron` route
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
4. **`apps/web/src/lib/balances/trx-balances.ts`** - call own API instead of TronGrid
|
||||||
|
5. **`apps/web/src/lib/env.ts`** - remove `tronApiUrl` and `tronApiKey`
|
||||||
|
6. **`apps/web/.env.local`** - remove `NEXT_PUBLIC_TRON_*` vars
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
7. **`.env`** - add `TRON_API_KEY=b874d775-4adc-4273-965b-cd6be5f66d68`
|
||||||
82
docs/plans/2026-03-15-send-receive-design.md
Normal file
82
docs/plans/2026-03-15-send-receive-design.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Phase 6: Send & Receive
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Кошелёк поддерживает ETH, SOL, TRX, BTC. Балансы работают для ETH/SOL/TRX. BTC имеет деривированный адрес, но нет RPC/балансов. Нужен полноценный Send/Receive с QR кодами.
|
||||||
|
|
||||||
|
## Receive — генерация QR
|
||||||
|
|
||||||
|
**Формат QR**: стандартные URI схемы (совместимы с другими кошельками):
|
||||||
|
- ETH: `ethereum:0xAddress` / `ethereum:0xAddress@1/transfer?address=0xTokenContract&uint256=amount` (EIP-681)
|
||||||
|
- SOL: `solana:Address` / `solana:Address?spl-token=MintAddress&amount=1.5`
|
||||||
|
- TRX: `tron:TAddress` / `tron:TAddress?token=TR7...&amount=100`
|
||||||
|
- BTC: `bitcoin:bc1Address` / `bitcoin:bc1Address?amount=0.01`
|
||||||
|
|
||||||
|
**UI flow**: Выбрал сеть → выбрал токен → опционально ввёл сумму → QR + адрес текстом + Copy.
|
||||||
|
|
||||||
|
## Send — отправка + сканер QR
|
||||||
|
|
||||||
|
**UI flow**: Выбрал сеть → выбрал токен → ввёл адрес (или Scan QR) → ввёл сумму → Gas (ETH only) → Review → Confirm → tx.
|
||||||
|
|
||||||
|
**QR сканер**: кнопка "Scan QR" → камера → распознаёт URI → заполняет chain/token/address/amount.
|
||||||
|
|
||||||
|
**Отправка по сетям**:
|
||||||
|
- **ETH**: `ethers.Wallet.sendTransaction()` для ETH, `contract.transfer()` для ERC20
|
||||||
|
- **SOL**: `SystemProgram.transfer()` для SOL, `createTransferInstruction()` для SPL tokens
|
||||||
|
- **TRX**: TronGrid `createtransaction` для native TRX, `triggersmartcontract` для TRC20
|
||||||
|
- **BTC**: `bitcoinjs-lib` PSBT → broadcast через Blockstream/Mempool API
|
||||||
|
|
||||||
|
## Библиотеки
|
||||||
|
|
||||||
|
- `qrcode.react` — генерация QR
|
||||||
|
- `@yudiel/react-qr-scanner` — сканирование QR камерой
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
| Принцип | Реализация |
|
||||||
|
|---------|-----------|
|
||||||
|
| Address validation | Regex + checksum для каждого формата |
|
||||||
|
| QR parsing validation | URI парсится → chain/address валидируются → token в whitelist |
|
||||||
|
| Confirm before send | Review экран + checkbox |
|
||||||
|
| Private keys | Подписание в браузере, ключи не покидают клиент |
|
||||||
|
| Amount validation | Проверка баланса перед отправкой |
|
||||||
|
|
||||||
|
## Файлы
|
||||||
|
|
||||||
|
### Новые (8)
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|------|-----------|
|
||||||
|
| `apps/web/src/app/send/page.tsx` | Send page |
|
||||||
|
| `apps/web/src/app/receive/page.tsx` | Receive page |
|
||||||
|
| `apps/web/src/lib/send/execute.ts` | Транзакции по 4 сетям |
|
||||||
|
| `apps/web/src/lib/send/validate.ts` | Валидация адресов |
|
||||||
|
| `apps/web/src/lib/send/constants.ts` | Токены, decimals, контракты |
|
||||||
|
| `apps/web/src/lib/qr/generate.ts` | Генерация URI для QR |
|
||||||
|
| `apps/web/src/lib/qr/parse.ts` | Парсинг URI из QR |
|
||||||
|
| `apps/api/src/routes/btc-proxy.routes.ts` | Bitcoin RPC proxy |
|
||||||
|
|
||||||
|
### Изменяемые (3)
|
||||||
|
|
||||||
|
| Файл | Изменение |
|
||||||
|
|------|-----------|
|
||||||
|
| `apps/web/src/app/dashboard/page.tsx` | Send/Receive кнопки |
|
||||||
|
| `apps/web/package.json` | qrcode.react, @yudiel/react-qr-scanner |
|
||||||
|
| `apps/api/src/app.ts` | Регистрация btc-proxy routes |
|
||||||
|
|
||||||
|
## Порядок выполнения
|
||||||
|
|
||||||
|
| # | Задача |
|
||||||
|
|---|--------|
|
||||||
|
| 1 | install qrcode.react + @yudiel/react-qr-scanner |
|
||||||
|
| 2 | send/constants.ts — токены, decimals, контракты для 4 сетей |
|
||||||
|
| 3 | send/validate.ts — валидация адресов по сетям |
|
||||||
|
| 4 | qr/generate.ts — URI генератор |
|
||||||
|
| 5 | qr/parse.ts — URI парсер |
|
||||||
|
| 6 | btc-proxy.routes.ts — Bitcoin UTXO + broadcast |
|
||||||
|
| 7 | app.ts — регистрация btc routes |
|
||||||
|
| 8 | send/execute.ts — отправка по 4 сетям |
|
||||||
|
| 9 | receive/page.tsx — Receive page |
|
||||||
|
| 10 | send/page.tsx — Send page + QR scanner |
|
||||||
|
| 11 | dashboard/page.tsx — Send/Receive кнопки |
|
||||||
|
| 12 | typecheck |
|
||||||
489
docs/plans/2026-03-18-add-tokens-and-bsc.md
Normal file
489
docs/plans/2026-03-18-add-tokens-and-bsc.md
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
# Add Tokens + BSC Chain Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Add new tokens to ETH/SOL swap+balance+send, add BNB Smart Chain as a new EVM network with PancakeSwap V2 swap.
|
||||||
|
|
||||||
|
**Architecture:** Extend existing multi-chain config-driven architecture. BSC reuses ETH's EVM signing logic (same derivation path, same ethers.Wallet). BSC swap via PancakeSwap V2 Router (Uniswap V2 fork) through a new API proxy. All new SOL tokens go through Jupiter (existing proxy). All new ETH tokens go through Uniswap (existing flow).
|
||||||
|
|
||||||
|
**Tech Stack:** ethers.js v5, Next.js, Express, PancakeSwap V2 ABI, Jupiter API, Uniswap V3/V4 SDK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Token Reference
|
||||||
|
|
||||||
|
### New ETH Tokens (Uniswap swap)
|
||||||
|
| Symbol | Contract (checksummed) | Decimals | CoinGecko ID |
|
||||||
|
|--------|------------------------|----------|--------------|
|
||||||
|
| stETH | `0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84` | 18 | `staked-ether` |
|
||||||
|
| SHIB | `0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE` | 18 | `shiba-inu` |
|
||||||
|
| LINK | `0x514910771AF9Ca656af840dff83E8264EcF986CA` | 18 | `chainlink` |
|
||||||
|
| POL | `0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6` | 18 | `polygon-ecosystem-token` |
|
||||||
|
| WLFI | `0x66f85e3865D0cFDc009aCF6280A8621f12e46CCf` | 18 | `world-liberty-financial` |
|
||||||
|
| AAVE | `0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9` | 18 | `aave` |
|
||||||
|
|
||||||
|
### New SOL Tokens (Jupiter swap)
|
||||||
|
| Symbol | Mint | Decimals | CoinGecko ID |
|
||||||
|
|--------|------|----------|--------------|
|
||||||
|
| WIF | `EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm` | 6 | `dogwifcoin` |
|
||||||
|
| POPCAT | `7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr` | 9 | `popcat` |
|
||||||
|
| TRUMP | `6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN` | 6 | `official-trump` |
|
||||||
|
| PYTH | `HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3` | 6 | `pyth-network` |
|
||||||
|
| JTO | `jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL` | 9 | `jito-governance-token` |
|
||||||
|
| W | `85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ` | 6 | `wormhole` |
|
||||||
|
| BONK | `DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263` | 5 | `bonk` |
|
||||||
|
| ORCA | `orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE` | 6 | `orca` |
|
||||||
|
| PENGU | `2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv` | 6 | `pudgy-penguins` |
|
||||||
|
| RAY | `4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R` | 6 | `raydium` |
|
||||||
|
|
||||||
|
### BSC Chain Config
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| ChainId | 56 |
|
||||||
|
| Native | BNB (18 decimals) |
|
||||||
|
| WBNB | `0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c` |
|
||||||
|
| DOGE (BEP-20) | `0xbA2aE424d960c26247Dd6c32edC70B295c744C43` (8 decimals) |
|
||||||
|
| PancakeSwap V2 Router | `0x10ED43C718714eb63d5aA57B78B54704E256024E` |
|
||||||
|
| Derivation | Same as ETH: `m/44'/60'/0'/0/0` |
|
||||||
|
| RPC | `https://bsc-dataseed.binance.org` + fallbacks |
|
||||||
|
| Explorer | `https://bscscan.com/tx/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add new ETH tokens to swap constants
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/swap/constants.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add 6 new Token instances after PEPE:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const STETH = new Token(ETHEREUM_CHAIN_ID, '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', 18, 'stETH', 'Lido Staked Ether');
|
||||||
|
const SHIB = new Token(ETHEREUM_CHAIN_ID, '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', 18, 'SHIB', 'Shiba Inu');
|
||||||
|
const LINK = new Token(ETHEREUM_CHAIN_ID, '0x514910771AF9Ca656af840dff83E8264EcF986CA', 18, 'LINK', 'Chainlink');
|
||||||
|
const POL = new Token(ETHEREUM_CHAIN_ID, '0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6', 18, 'POL', 'Polygon');
|
||||||
|
const WLFI = new Token(ETHEREUM_CHAIN_ID, '0x66f85e3865D0cFDc009aCF6280A8621f12e46CCf', 18, 'WLFI', 'World Liberty Financial');
|
||||||
|
const AAVE_TOKEN = new Token(ETHEREUM_CHAIN_ID, '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', 18, 'AAVE', 'Aave');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update `SwapTokenSymbol` type to include new symbols:
|
||||||
|
```ts
|
||||||
|
export type SwapTokenSymbol = 'ETH' | 'USDT' | 'USDC' | 'XAUT' | 'UNI' | 'PEPE' | 'stETH' | 'SHIB' | 'LINK' | 'POL' | 'WLFI' | 'AAVE';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add entries to `SWAP_TOKENS` record for each new token (same pattern as existing).
|
||||||
|
|
||||||
|
4. Update `SWAP_TOKEN_OPTIONS` and `SWAP_TOKEN_OPTIONS_BY_CHAIN.ETH` to include new symbols.
|
||||||
|
|
||||||
|
5. Add V3 pool candidates for new tokens (WETH pairs with MEDIUM/HIGH fees):
|
||||||
|
```ts
|
||||||
|
// stETH
|
||||||
|
{ tokenA: WETH, tokenB: STETH, fee: FeeAmount.LOW },
|
||||||
|
{ tokenA: WETH, tokenB: STETH, fee: FeeAmount.MEDIUM },
|
||||||
|
// SHIB
|
||||||
|
{ tokenA: WETH, tokenB: SHIB, fee: FeeAmount.MEDIUM },
|
||||||
|
{ tokenA: WETH, tokenB: SHIB, fee: FeeAmount.HIGH },
|
||||||
|
// LINK
|
||||||
|
{ tokenA: WETH, tokenB: LINK, fee: FeeAmount.LOW },
|
||||||
|
{ tokenA: WETH, tokenB: LINK, fee: FeeAmount.MEDIUM },
|
||||||
|
// POL
|
||||||
|
{ tokenA: WETH, tokenB: POL, fee: FeeAmount.MEDIUM },
|
||||||
|
{ tokenA: WETH, tokenB: POL, fee: FeeAmount.HIGH },
|
||||||
|
// WLFI
|
||||||
|
{ tokenA: WETH, tokenB: WLFI, fee: FeeAmount.MEDIUM },
|
||||||
|
{ tokenA: WETH, tokenB: WLFI, fee: FeeAmount.HIGH },
|
||||||
|
// AAVE
|
||||||
|
{ tokenA: WETH, tokenB: AAVE_TOKEN, fee: FeeAmount.LOW },
|
||||||
|
{ tokenA: WETH, tokenB: AAVE_TOKEN, fee: FeeAmount.MEDIUM },
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Add V4 pool key candidates similarly.
|
||||||
|
|
||||||
|
7. Update `getSlippageBps` to include new volatile tokens:
|
||||||
|
```ts
|
||||||
|
const volatileTokens: SwapTokenSymbol[] = ['PEPE', 'XAUT', 'UNI', 'SHIB', 'WLFI', 'stETH', 'LINK', 'POL', 'AAVE'];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Add new ETH tokens to balance fetcher
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/balances/eth-balances.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add 6 new entries to `ETH_TOKENS` array (after PEPE):
|
||||||
|
```ts
|
||||||
|
{ chain: 'ETH', symbol: 'stETH', decimals: 18, contractAddress: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', coinGeckoId: 'staked-ether', isNative: false },
|
||||||
|
{ chain: 'ETH', symbol: 'SHIB', decimals: 18, contractAddress: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', coinGeckoId: 'shiba-inu', isNative: false },
|
||||||
|
{ chain: 'ETH', symbol: 'LINK', decimals: 18, contractAddress: '0x514910771AF9Ca656af840dff83E8264EcF986CA', coinGeckoId: 'chainlink', isNative: false },
|
||||||
|
{ chain: 'ETH', symbol: 'POL', decimals: 18, contractAddress: '0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6', coinGeckoId: 'polygon-ecosystem-token', isNative: false },
|
||||||
|
{ chain: 'ETH', symbol: 'WLFI', decimals: 18, contractAddress: '0x66f85e3865D0cFDc009aCF6280A8621f12e46CCf', coinGeckoId: 'world-liberty-financial', isNative: false },
|
||||||
|
{ chain: 'ETH', symbol: 'AAVE', decimals: 18, contractAddress: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', coinGeckoId: 'aave', isNative: false },
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Add new ETH tokens to send constants
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/send/constants.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add 6 new token entries under `SEND_CHAINS.ETH.tokens`:
|
||||||
|
```ts
|
||||||
|
stETH: { symbol: 'stETH', contractAddress: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', decimals: 18 },
|
||||||
|
SHIB: { symbol: 'SHIB', contractAddress: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', decimals: 18 },
|
||||||
|
LINK: { symbol: 'LINK', contractAddress: '0x514910771AF9Ca656af840dff83E8264EcF986CA', decimals: 18 },
|
||||||
|
POL: { symbol: 'POL', contractAddress: '0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6', decimals: 18 },
|
||||||
|
WLFI: { symbol: 'WLFI', contractAddress: '0x66f85e3865D0cFDc009aCF6280A8621f12e46CCf', decimals: 18 },
|
||||||
|
AAVE: { symbol: 'AAVE', contractAddress: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', decimals: 18 },
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add corresponding `CONTRACT_TO_SYMBOL` entries (lowercase addresses).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Add new SOL tokens to swap constants
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/swap/constants.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add new entries to `SOL_TOKEN_MINTS`:
|
||||||
|
```ts
|
||||||
|
WIF: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm',
|
||||||
|
POPCAT: '7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr',
|
||||||
|
TRUMP: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN',
|
||||||
|
PYTH: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
|
||||||
|
JTO: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL',
|
||||||
|
W: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ',
|
||||||
|
BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
|
||||||
|
ORCA: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE',
|
||||||
|
PENGU: '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv',
|
||||||
|
RAY: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add entries to `SOL_TOKEN_DECIMALS`:
|
||||||
|
```ts
|
||||||
|
WIF: 6, POPCAT: 9, TRUMP: 6, PYTH: 6, JTO: 9, W: 6, BONK: 5, ORCA: 6, PENGU: 6, RAY: 6,
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update `SWAP_TOKEN_OPTIONS_BY_CHAIN.SOL`:
|
||||||
|
```ts
|
||||||
|
SOL: ['SOL', 'USDT', 'USDC', 'PUMP', 'JUP', 'WIF', 'POPCAT', 'TRUMP', 'PYTH', 'JTO', 'W', 'BONK', 'ORCA', 'PENGU', 'RAY'],
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Add new SOL tokens to balance fetcher + Jupiter whitelist
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/balances/sol-balances.ts`
|
||||||
|
- Modify: `apps/api/src/routes/sol-swap-proxy.routes.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add 10 new entries to `SOL_TOKENS` array in `sol-balances.ts`:
|
||||||
|
```ts
|
||||||
|
{ chain: 'SOL', symbol: 'WIF', decimals: 6, contractAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', coinGeckoId: 'dogwifcoin', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'POPCAT', decimals: 9, contractAddress: '7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr', coinGeckoId: 'popcat', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'TRUMP', decimals: 6, contractAddress: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', coinGeckoId: 'official-trump', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'PYTH', decimals: 6, contractAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', coinGeckoId: 'pyth-network', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'JTO', decimals: 9, contractAddress: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', coinGeckoId: 'jito-governance-token', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'W', decimals: 6, contractAddress: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ', coinGeckoId: 'wormhole', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'BONK', decimals: 5, contractAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', coinGeckoId: 'bonk', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'ORCA', decimals: 6, contractAddress: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', coinGeckoId: 'orca', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'PENGU', decimals: 6, contractAddress: '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv', coinGeckoId: 'pudgy-penguins', isNative: false },
|
||||||
|
{ chain: 'SOL', symbol: 'RAY', decimals: 6, contractAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', coinGeckoId: 'raydium', isNative: false },
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add all 10 new mint addresses to `ALLOWED_MINTS` in `sol-swap-proxy.routes.ts`:
|
||||||
|
```ts
|
||||||
|
const ALLOWED_MINTS = new Set([
|
||||||
|
'So11111111111111111111111111111111111111112', // SOL
|
||||||
|
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', // USDT
|
||||||
|
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
|
||||||
|
'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn', // PUMP
|
||||||
|
'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', // JUP
|
||||||
|
'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', // WIF
|
||||||
|
'7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr', // POPCAT
|
||||||
|
'6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', // TRUMP
|
||||||
|
'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', // PYTH
|
||||||
|
'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', // JTO
|
||||||
|
'85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ', // W
|
||||||
|
'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', // BONK
|
||||||
|
'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', // ORCA
|
||||||
|
'2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv', // PENGU
|
||||||
|
'4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', // RAY
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Add new SOL tokens to send constants
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/send/constants.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add 10 new token entries under `SEND_CHAINS.SOL.tokens` (same mint addresses as balance fetcher).
|
||||||
|
|
||||||
|
2. Add corresponding `CONTRACT_TO_SYMBOL` entries for the new SOL mints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Add BSC chain — wallet derivation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/crypto/derive-keys.ts`
|
||||||
|
- Modify: `apps/web/src/lib/balances/types.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. In `derive-keys.ts`, update `Chain` type:
|
||||||
|
```ts
|
||||||
|
export type Chain = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add BSC derivation path (same as ETH):
|
||||||
|
```ts
|
||||||
|
BSC: "m/44'/60'/0'/0/0",
|
||||||
|
```
|
||||||
|
|
||||||
|
3. In `deriveWalletsFromMnemonic`, add BSC wallet. Since BSC uses the same key as ETH:
|
||||||
|
```ts
|
||||||
|
const bsc = deriveEthWallet(mnemonic); // same key derivation
|
||||||
|
// ...
|
||||||
|
{ chain: 'BSC', address: bsc.address, privateKey: bsc.privateKey, derivationPath: DERIVATION_PATHS.BSC },
|
||||||
|
```
|
||||||
|
|
||||||
|
4. In `types.ts`, update `BalanceChain`:
|
||||||
|
```ts
|
||||||
|
export type BalanceChain = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Add BSC chain — environment + balance fetcher
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/env.ts`
|
||||||
|
- Create: `apps/web/src/lib/balances/bsc-balances.ts`
|
||||||
|
- Modify: `apps/web/src/lib/balances/index.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Add BSC RPC to `env.ts`:
|
||||||
|
```ts
|
||||||
|
bscRpcUrl: readUrlEnv('NEXT_PUBLIC_BSC_RPC_URL', 'https://bsc-dataseed.binance.org'),
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `bsc-balances.ts` — copy `eth-balances.ts` pattern with:
|
||||||
|
- `BSC_CHAIN_ID = 56`
|
||||||
|
- `BSC_RPC_CANDIDATES`: `webEnv.bscRpcUrl`, `https://bsc-dataseed1.defibit.io`, `https://bsc-dataseed1.ninicoin.io`
|
||||||
|
- `BSC_TOKENS`: BNB native (18 dec, coinGeckoId `binancecoin`) + DOGE BEP-20 (8 dec, `0xbA2aE424d960c26247Dd6c32edC70B295c744C43`, coinGeckoId `dogecoin`)
|
||||||
|
- `fetchBscBalances(address)` — same logic as `fetchEthBalances` but with BSC provider
|
||||||
|
- All internal references use `chain: 'BSC'`
|
||||||
|
|
||||||
|
3. Register in `balances/index.ts`:
|
||||||
|
```ts
|
||||||
|
import { fetchBscBalances } from './bsc-balances';
|
||||||
|
// ...
|
||||||
|
const SUPPORTED_CHAINS: BalanceChain[] = ['ETH', 'BTC', 'SOL', 'TRX', 'BSC'];
|
||||||
|
// ...
|
||||||
|
BSC: fetchBscBalances,
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Add BSC chain — swap via PancakeSwap V2 API proxy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `apps/api/src/routes/bsc-swap-proxy.routes.ts`
|
||||||
|
- Modify: `apps/api/src/app.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Create `bsc-swap-proxy.routes.ts` — PancakeSwap V2 swap proxy (same pattern as `tron-swap-proxy.routes.ts` but simpler since it's EVM):
|
||||||
|
|
||||||
|
```
|
||||||
|
Router endpoints:
|
||||||
|
GET /quote — calls PancakeSwap V2 Router getAmountsOut via BSC RPC
|
||||||
|
POST /build — builds swap calldata (approve + swap tx)
|
||||||
|
|
||||||
|
Key constants:
|
||||||
|
PANCAKE_ROUTER = '0x10ED43C718714eb63d5aA57B78B54704E256024E'
|
||||||
|
WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'
|
||||||
|
DOGE = '0xbA2aE424d960c26247Dd6c32edC70B295c744C43'
|
||||||
|
BSC_RPC = 'https://bsc-dataseed.binance.org'
|
||||||
|
|
||||||
|
Quote logic:
|
||||||
|
- Create ethers provider with BSC RPC
|
||||||
|
- Call router.getAmountsOut(amountIn, [fromToken, toToken])
|
||||||
|
- Return { amountIn, amountOut, from, to }
|
||||||
|
|
||||||
|
Build logic:
|
||||||
|
- For BNB->Token: encode swapExactETHForTokensSupportingFeeOnTransferTokens calldata
|
||||||
|
- For Token->BNB: check/build approve tx + encode swapExactTokensForETHSupportingFeeOnTransferTokens calldata
|
||||||
|
- Return { calldata, to, value, approveTx? }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register in `app.ts`:
|
||||||
|
```ts
|
||||||
|
import bscSwapProxyRoutes from './routes/bsc-swap-proxy.routes';
|
||||||
|
// ...
|
||||||
|
app.use('/api/bsc/swap', bscSwapProxyRoutes);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Add BSC to swap constants + UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/swap/constants.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Update `SwapChain`:
|
||||||
|
```ts
|
||||||
|
export type SwapChain = 'ETH' | 'SOL' | 'TRX' | 'BSC';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add BSC token configs:
|
||||||
|
```ts
|
||||||
|
export const BSC_TOKEN_ADDRESSES: Record<string, string> = {
|
||||||
|
BNB: 'native',
|
||||||
|
DOGE: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BSC_TOKEN_DECIMALS: Record<string, number> = {
|
||||||
|
BNB: 18,
|
||||||
|
DOGE: 8,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add BSC to `SWAP_TOKEN_OPTIONS_BY_CHAIN`:
|
||||||
|
```ts
|
||||||
|
BSC: ['BNB', 'DOGE'],
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add BSC to `CHAIN_DEFAULT_TOKENS`:
|
||||||
|
```ts
|
||||||
|
BSC: { from: 'BNB', to: 'DOGE' },
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Update `getSlippageBpsForChain` to handle BSC:
|
||||||
|
```ts
|
||||||
|
if (chain === 'BSC') return 50; // 0.50%
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Update `getExplorerTxUrl`:
|
||||||
|
```ts
|
||||||
|
case 'BSC': return `https://bscscan.com/tx/${txHash}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11: Add BSC swap execution (client-side)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `apps/web/src/lib/swap/bsc/execute.ts`
|
||||||
|
- Modify: swap UI hooks/page to add BSC chain option
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Create `bsc/execute.ts`:
|
||||||
|
```ts
|
||||||
|
// Similar to TRX swap execute but using ethers.Wallet directly
|
||||||
|
// 1. Fetch quote from /api/bsc/swap/quote
|
||||||
|
// 2. Build tx from /api/bsc/swap/build
|
||||||
|
// 3. If approve needed: sign and send approve tx, wait for confirmation
|
||||||
|
// 4. Sign and send swap tx via ethers.Wallet connected to BSC RPC
|
||||||
|
// 5. Return { hash, explorerUrl }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update swap page `CHAINS` array:
|
||||||
|
```ts
|
||||||
|
const CHAINS: SwapChain[] = ['ETH', 'SOL', 'TRX', 'BSC'];
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update `useSwap` hook to handle BSC chain (dispatch to `executeBscSwap`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 12: Add BSC to send/receive
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/web/src/lib/send/constants.ts`
|
||||||
|
- Modify: `apps/web/src/lib/send/execute.ts`
|
||||||
|
- Modify: `apps/web/src/lib/send/validate.ts`
|
||||||
|
- Modify: `apps/web/src/lib/qr/generate.ts`
|
||||||
|
- Modify: `apps/web/src/lib/qr/parse.ts`
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. In `send/constants.ts`:
|
||||||
|
- Update `SendChain`: `'ETH' | 'SOL' | 'TRX' | 'BTC' | 'BSC'`
|
||||||
|
- Add BSC config:
|
||||||
|
```ts
|
||||||
|
BSC: {
|
||||||
|
key: 'BSC',
|
||||||
|
label: 'BNB Smart Chain',
|
||||||
|
walletChain: 'BSC',
|
||||||
|
tokens: {
|
||||||
|
BNB: { symbol: 'BNB', contractAddress: null, decimals: 18 },
|
||||||
|
DOGE: { symbol: 'DOGE', contractAddress: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43', decimals: 8 },
|
||||||
|
},
|
||||||
|
explorerTxUrl: 'https://bscscan.com/tx/',
|
||||||
|
},
|
||||||
|
```
|
||||||
|
- Add to `SEND_CHAIN_OPTIONS`
|
||||||
|
|
||||||
|
2. In `send/execute.ts`:
|
||||||
|
- Add BSC case — same as ETH but with BSC provider:
|
||||||
|
```ts
|
||||||
|
case 'BSC':
|
||||||
|
return executeBscSend(params);
|
||||||
|
```
|
||||||
|
- `executeBscSend`: create `ethers.providers.StaticJsonRpcProvider(bscRpcUrl, 56)`, use `ethers.Wallet` for native BNB or BEP-20 transfer (identical to ETH logic).
|
||||||
|
|
||||||
|
3. In `send/validate.ts`:
|
||||||
|
- BSC uses same address format as ETH — add `case 'BSC': return validateEthAddress(address);`
|
||||||
|
- Update `detectChainFromAddress` — BSC can't be auto-detected (same format as ETH)
|
||||||
|
|
||||||
|
4. In `qr/generate.ts`:
|
||||||
|
- Add BSC case using `ethereum:` URI scheme with `@56` chain discriminator:
|
||||||
|
```ts
|
||||||
|
case 'BSC':
|
||||||
|
return generateBscUri(address, token, amount);
|
||||||
|
```
|
||||||
|
- `generateBscUri`: `ethereum:${address}@56` for native, `ethereum:${contract}@56/transfer?address=${to}` for BEP-20
|
||||||
|
|
||||||
|
5. In `qr/parse.ts`:
|
||||||
|
- Handle `@56` chain discriminator in ethereum: URIs to distinguish BSC from ETH
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 13: Run typecheck and verify
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Run `pnpm typecheck` from monorepo root
|
||||||
|
2. Fix any TypeScript errors
|
||||||
|
3. Verify all imports resolve correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Strategy
|
||||||
|
|
||||||
|
- After Tasks 1-3 (ETH tokens): commit "feat: add stETH, SHIB, LINK, POL, WLFI, AAVE tokens to ETH swap/balance/send"
|
||||||
|
- After Tasks 4-6 (SOL tokens): commit "feat: add WIF, POPCAT, TRUMP, PYTH, JTO, W, BONK, ORCA, PENGU, RAY to SOL swap/balance/send"
|
||||||
|
- After Tasks 7-12 (BSC chain): commit "feat: add BNB Smart Chain with PancakeSwap V2 swap, BNB + DOGE tokens"
|
||||||
|
- After Task 13: commit "fix: resolve typecheck errors" (if needed)
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export { CHAINS, DERIVATION_PATHS } from './constants/chains';
|
export { CHAINS, DERIVATION_PATHS } from './constants/chains';
|
||||||
export type { Chain } from './constants/chains';
|
export type { Chain } from './constants/chains';
|
||||||
export type { ApiResponse, AccessTokenPayload, AuthContext } from './types/auth';
|
export type { RegisterRequest, LoginRequest, AuthResponse, ApiResponse } from './types/auth';
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
|
import { Chain } from '../constants/chains';
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
pin: string;
|
||||||
|
encryptedVault: string;
|
||||||
|
vaultSalt: string;
|
||||||
|
wallets: { chain: Chain; address: string; derivationPath: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
pin: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
accessToken: string;
|
||||||
|
user: { id: string; username: string };
|
||||||
|
encryptedVault?: string;
|
||||||
|
vaultSalt?: string;
|
||||||
|
wallets?: { chain: Chain; address: string; derivationPath: string }[];
|
||||||
|
mnemonicShown: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T = unknown> {
|
export interface ApiResponse<T = unknown> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: T;
|
data?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessTokenPayload {
|
|
||||||
sub: string;
|
|
||||||
type: string;
|
|
||||||
sid: string;
|
|
||||||
iat: number;
|
|
||||||
nbf: number;
|
|
||||||
exp: number;
|
|
||||||
iss?: string;
|
|
||||||
aud?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthContext {
|
|
||||||
userId: string;
|
|
||||||
sid: string;
|
|
||||||
token: AccessTokenPayload;
|
|
||||||
}
|
|
||||||
|
|||||||
473
pnpm-lock.yaml
generated
473
pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ importers:
|
|||||||
'@cryptowallet/shared':
|
'@cryptowallet/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared
|
version: link:../../packages/shared
|
||||||
|
amqplib:
|
||||||
|
specifier: ^1.0.3
|
||||||
|
version: 1.0.3
|
||||||
|
bcrypt:
|
||||||
|
specifier: ^5.1.1
|
||||||
|
version: 5.1.1
|
||||||
cookie-parser:
|
cookie-parser:
|
||||||
specifier: ^1.4.7
|
specifier: ^1.4.7
|
||||||
version: 1.4.7
|
version: 1.4.7
|
||||||
@@ -31,32 +37,44 @@ importers:
|
|||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
ethers:
|
ethers:
|
||||||
specifier: 5.7.2
|
specifier: 5.7.2
|
||||||
version: 5.7.2(bufferutil@4.1.0)
|
version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
||||||
express:
|
express:
|
||||||
specifier: ^4.21.0
|
specifier: ^4.21.0
|
||||||
version: 4.22.1
|
version: 4.22.1
|
||||||
|
express-rate-limit:
|
||||||
|
specifier: ^7.4.0
|
||||||
|
version: 7.5.1(express@4.22.1)
|
||||||
helmet:
|
helmet:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.1.0
|
version: 8.1.0
|
||||||
jose:
|
jose:
|
||||||
specifier: ^6.2.2
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.3
|
||||||
knex:
|
knex:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(pg@8.20.0)
|
version: 3.1.0(pg@8.20.0)
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.13.0
|
specifier: ^8.13.0
|
||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
swagger-ui-express:
|
|
||||||
specifier: ^5.0.1
|
|
||||||
version: 5.0.1(express@4.22.1)
|
|
||||||
ulidx:
|
ulidx:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.1.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.23.0
|
specifier: ^3.23.0
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/amqplib':
|
||||||
|
specifier: ^0.10.8
|
||||||
|
version: 0.10.8
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^5.0.2
|
||||||
|
version: 5.0.2
|
||||||
'@types/cookie-parser':
|
'@types/cookie-parser':
|
||||||
specifier: ^1.4.7
|
specifier: ^1.4.7
|
||||||
version: 1.4.10(@types/express@5.0.6)
|
version: 1.4.10(@types/express@5.0.6)
|
||||||
@@ -69,12 +87,15 @@ importers:
|
|||||||
'@types/express-serve-static-core':
|
'@types/express-serve-static-core':
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.10
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.0.0
|
specifier: ^20.0.0
|
||||||
version: 20.19.37
|
version: 20.19.37
|
||||||
'@types/swagger-ui-express':
|
'@types/uuid':
|
||||||
specifier: ^4.1.8
|
specifier: ^10.0.0
|
||||||
version: 4.1.8
|
version: 10.0.0
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.0
|
specifier: ^10.9.0
|
||||||
version: 10.9.2(@types/node@20.19.37)(typescript@5.9.3)
|
version: 10.9.2(@types/node@20.19.37)(typescript@5.9.3)
|
||||||
@@ -524,6 +545,10 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
|
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@next/env@16.1.6':
|
'@next/env@16.1.6':
|
||||||
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
|
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
|
||||||
|
|
||||||
@@ -692,9 +717,6 @@ packages:
|
|||||||
'@openzeppelin/contracts@5.0.2':
|
'@openzeppelin/contracts@5.0.2':
|
||||||
resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==}
|
resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==}
|
||||||
|
|
||||||
'@scarf/scarf@1.4.0':
|
|
||||||
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
|
||||||
|
|
||||||
'@scure/base@1.1.9':
|
'@scure/base@1.1.9':
|
||||||
resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==}
|
resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==}
|
||||||
|
|
||||||
@@ -791,6 +813,12 @@ packages:
|
|||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/amqplib@0.10.8':
|
||||||
|
resolution: {integrity: sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==}
|
||||||
|
|
||||||
|
'@types/bcrypt@5.0.2':
|
||||||
|
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -817,6 +845,12 @@ packages:
|
|||||||
'@types/http-errors@2.0.5':
|
'@types/http-errors@2.0.5':
|
||||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@12.20.55':
|
'@types/node@12.20.55':
|
||||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||||
|
|
||||||
@@ -849,9 +883,6 @@ packages:
|
|||||||
'@types/strip-json-comments@0.0.30':
|
'@types/strip-json-comments@0.0.30':
|
||||||
resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==}
|
resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==}
|
||||||
|
|
||||||
'@types/swagger-ui-express@4.1.8':
|
|
||||||
resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==}
|
|
||||||
|
|
||||||
'@types/uuid@10.0.0':
|
'@types/uuid@10.0.0':
|
||||||
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||||
|
|
||||||
@@ -923,6 +954,9 @@ packages:
|
|||||||
react: ^17 || ^18 || ^19
|
react: ^17 || ^18 || ^19
|
||||||
react-dom: ^17 || ^18 || ^19
|
react-dom: ^17 || ^18 || ^19
|
||||||
|
|
||||||
|
abbrev@1.1.1:
|
||||||
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -955,6 +989,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
amqplib@1.0.3:
|
||||||
|
resolution: {integrity: sha512-nsrXa59tvX4HPjPPW8JJGuBb/Db0MOq3DOhGdSbIY7bTsQDMV2fmF9c3i74g/8Gt9+QWyrxfEPppOOoV9lK1uQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
ansi-align@3.0.1:
|
ansi-align@3.0.1:
|
||||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||||
|
|
||||||
@@ -978,6 +1016,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
aproba@2.1.0:
|
||||||
|
resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==}
|
||||||
|
|
||||||
|
are-we-there-yet@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
arg@4.1.3:
|
arg@4.1.3:
|
||||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||||
|
|
||||||
@@ -1014,6 +1060,10 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcrypt@5.1.1:
|
||||||
|
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
bech32@1.1.4:
|
bech32@1.1.4:
|
||||||
resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==}
|
resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==}
|
||||||
|
|
||||||
@@ -1084,9 +1134,15 @@ packages:
|
|||||||
bs58check@4.0.0:
|
bs58check@4.0.0:
|
||||||
resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==}
|
resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==}
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
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==}
|
||||||
|
|
||||||
|
buffer-more-ints@1.0.0:
|
||||||
|
resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
@@ -1133,6 +1189,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
|
chownr@2.0.0:
|
||||||
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
ci-info@2.0.0:
|
ci-info@2.0.0:
|
||||||
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
|
resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
|
||||||
|
|
||||||
@@ -1161,6 +1221,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-support@1.1.3:
|
||||||
|
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
colorette@2.0.19:
|
colorette@2.0.19:
|
||||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||||
|
|
||||||
@@ -1185,6 +1249,9 @@ packages:
|
|||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
console-control-strings@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1271,6 +1338,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
delegates@1.0.0:
|
||||||
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1306,6 +1376,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==}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ecpair@3.0.1:
|
ecpair@3.0.1:
|
||||||
resolution: {integrity: sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==}
|
resolution: {integrity: sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -1386,6 +1459,12 @@ packages:
|
|||||||
eventemitter3@5.0.4:
|
eventemitter3@5.0.4:
|
||||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
|
express-rate-limit@7.5.1:
|
||||||
|
resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
peerDependencies:
|
||||||
|
express: '>= 4.11'
|
||||||
|
|
||||||
express@4.22.1:
|
express@4.22.1:
|
||||||
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
@@ -1450,6 +1529,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
||||||
engines: {node: '>=6 <7 || >=8'}
|
engines: {node: '>=6 <7 || >=8'}
|
||||||
|
|
||||||
|
fs-minipass@2.1.0:
|
||||||
|
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
fs.realpath@1.0.0:
|
fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
@@ -1461,6 +1544,11 @@ packages:
|
|||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
gauge@3.0.2:
|
||||||
|
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
get-caller-file@2.0.5:
|
get-caller-file@2.0.5:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
@@ -1532,6 +1620,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
has-unicode@2.0.1:
|
||||||
|
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
|
||||||
|
|
||||||
hash-base@3.1.2:
|
hash-base@3.1.2:
|
||||||
resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==}
|
resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -1676,6 +1767,16 @@ packages:
|
|||||||
jsonfile@4.0.0:
|
jsonfile@4.0.0:
|
||||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.3:
|
||||||
|
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jwa@2.0.1:
|
||||||
|
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||||
|
|
||||||
|
jws@4.0.1:
|
||||||
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
keccak@3.0.4:
|
keccak@3.0.4:
|
||||||
resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==}
|
resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -1715,6 +1816,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
|
||||||
|
lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
lodash@4.17.23:
|
lodash@4.17.23:
|
||||||
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
||||||
|
|
||||||
@@ -1725,6 +1847,10 @@ packages:
|
|||||||
lru_map@0.3.3:
|
lru_map@0.3.3:
|
||||||
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
|
resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==}
|
||||||
|
|
||||||
|
make-dir@3.1.0:
|
||||||
|
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
make-error@1.3.6:
|
make-error@1.3.6:
|
||||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
|
||||||
@@ -1785,6 +1911,18 @@ packages:
|
|||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
minipass@3.3.6:
|
||||||
|
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
minipass@5.0.0:
|
||||||
|
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
minizlib@2.1.2:
|
||||||
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
mkdirp@1.0.4:
|
mkdirp@1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1840,6 +1978,9 @@ packages:
|
|||||||
node-addon-api@2.0.2:
|
node-addon-api@2.0.2:
|
||||||
resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==}
|
resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==}
|
||||||
|
|
||||||
|
node-addon-api@5.1.0:
|
||||||
|
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -1853,10 +1994,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nopt@5.0.0:
|
||||||
|
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
npmlog@5.0.1:
|
||||||
|
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
|
||||||
|
deprecated: This package is no longer supported.
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2057,6 +2207,11 @@ 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
|
||||||
|
|
||||||
|
rimraf@3.0.2:
|
||||||
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
ripemd160@2.0.3:
|
ripemd160@2.0.3:
|
||||||
resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==}
|
resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2106,6 +2261,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
set-blocking@2.0.0:
|
||||||
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2138,6 +2296,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
signal-exit@3.0.7:
|
||||||
|
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||||
|
|
||||||
solc@0.8.26:
|
solc@0.8.26:
|
||||||
resolution: {integrity: sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==}
|
resolution: {integrity: sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -2224,19 +2385,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
swagger-ui-dist@5.32.1:
|
|
||||||
resolution: {integrity: sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==}
|
|
||||||
|
|
||||||
swagger-ui-express@5.0.1:
|
|
||||||
resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
|
|
||||||
engines: {node: '>= v0.10.32'}
|
|
||||||
peerDependencies:
|
|
||||||
express: '>=4.0.0 || >=5.0.0-beta'
|
|
||||||
|
|
||||||
tagged-tag@1.0.0:
|
tagged-tag@1.0.0:
|
||||||
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
tar@6.2.1:
|
||||||
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
tarn@3.0.2:
|
tarn@3.0.2:
|
||||||
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -2473,6 +2630,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
wide-align@1.1.5:
|
||||||
|
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
|
||||||
|
|
||||||
widest-line@3.1.0:
|
widest-line@3.1.0:
|
||||||
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
|
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2534,6 +2694,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yallist@4.0.0:
|
||||||
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
yargs-parser@20.2.9:
|
yargs-parser@20.2.9:
|
||||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2861,32 +3024,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@ethersproject/logger': 5.8.0
|
'@ethersproject/logger': 5.8.0
|
||||||
|
|
||||||
'@ethersproject/providers@5.7.2(bufferutil@4.1.0)':
|
|
||||||
dependencies:
|
|
||||||
'@ethersproject/abstract-provider': 5.8.0
|
|
||||||
'@ethersproject/abstract-signer': 5.8.0
|
|
||||||
'@ethersproject/address': 5.8.0
|
|
||||||
'@ethersproject/base64': 5.8.0
|
|
||||||
'@ethersproject/basex': 5.8.0
|
|
||||||
'@ethersproject/bignumber': 5.8.0
|
|
||||||
'@ethersproject/bytes': 5.8.0
|
|
||||||
'@ethersproject/constants': 5.8.0
|
|
||||||
'@ethersproject/hash': 5.8.0
|
|
||||||
'@ethersproject/logger': 5.8.0
|
|
||||||
'@ethersproject/networks': 5.8.0
|
|
||||||
'@ethersproject/properties': 5.8.0
|
|
||||||
'@ethersproject/random': 5.8.0
|
|
||||||
'@ethersproject/rlp': 5.8.0
|
|
||||||
'@ethersproject/sha2': 5.8.0
|
|
||||||
'@ethersproject/strings': 5.8.0
|
|
||||||
'@ethersproject/transactions': 5.8.0
|
|
||||||
'@ethersproject/web': 5.8.0
|
|
||||||
bech32: 1.1.4
|
|
||||||
ws: 7.4.6(bufferutil@4.1.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
'@ethersproject/providers@5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)':
|
'@ethersproject/providers@5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ethersproject/abstract-provider': 5.8.0
|
'@ethersproject/abstract-provider': 5.8.0
|
||||||
@@ -3181,6 +3318,21 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
https-proxy-agent: 5.0.1
|
||||||
|
make-dir: 3.1.0
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
nopt: 5.0.0
|
||||||
|
npmlog: 5.0.1
|
||||||
|
rimraf: 3.0.2
|
||||||
|
semver: 7.7.4
|
||||||
|
tar: 6.2.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@next/env@16.1.6': {}
|
'@next/env@16.1.6': {}
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@16.1.6':
|
'@next/swc-darwin-arm64@16.1.6':
|
||||||
@@ -3298,8 +3450,6 @@ snapshots:
|
|||||||
|
|
||||||
'@openzeppelin/contracts@5.0.2': {}
|
'@openzeppelin/contracts@5.0.2': {}
|
||||||
|
|
||||||
'@scarf/scarf@1.4.0': {}
|
|
||||||
|
|
||||||
'@scure/base@1.1.9': {}
|
'@scure/base@1.1.9': {}
|
||||||
|
|
||||||
'@scure/base@1.2.6': {}
|
'@scure/base@1.2.6': {}
|
||||||
@@ -3444,6 +3594,14 @@ snapshots:
|
|||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/amqplib@0.10.8':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.37
|
||||||
|
|
||||||
|
'@types/bcrypt@5.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.37
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -3478,6 +3636,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/http-errors@2.0.5': {}
|
'@types/http-errors@2.0.5': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 20.19.37
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@12.20.55': {}
|
'@types/node@12.20.55': {}
|
||||||
|
|
||||||
'@types/node@20.19.37':
|
'@types/node@20.19.37':
|
||||||
@@ -3509,11 +3674,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/strip-json-comments@0.0.30': {}
|
'@types/strip-json-comments@0.0.30': {}
|
||||||
|
|
||||||
'@types/swagger-ui-express@4.1.8':
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 5.0.6
|
|
||||||
'@types/serve-static': 2.2.0
|
|
||||||
|
|
||||||
'@types/uuid@10.0.0': {}
|
'@types/uuid@10.0.0': {}
|
||||||
|
|
||||||
'@types/ws@7.4.7':
|
'@types/ws@7.4.7':
|
||||||
@@ -3651,6 +3811,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/emscripten'
|
- '@types/emscripten'
|
||||||
|
|
||||||
|
abbrev@1.1.1: {}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
@@ -3681,6 +3843,10 @@ snapshots:
|
|||||||
clean-stack: 2.2.0
|
clean-stack: 2.2.0
|
||||||
indent-string: 4.0.0
|
indent-string: 4.0.0
|
||||||
|
|
||||||
|
amqplib@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
buffer-more-ints: 1.0.0
|
||||||
|
|
||||||
ansi-align@3.0.1:
|
ansi-align@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -3702,6 +3868,13 @@ snapshots:
|
|||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
aproba@2.1.0: {}
|
||||||
|
|
||||||
|
are-we-there-yet@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
delegates: 1.0.0
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
arg@4.1.3: {}
|
arg@4.1.3: {}
|
||||||
|
|
||||||
argparse@2.0.1: {}
|
argparse@2.0.1: {}
|
||||||
@@ -3732,6 +3905,14 @@ snapshots:
|
|||||||
|
|
||||||
baseline-browser-mapping@2.10.0: {}
|
baseline-browser-mapping@2.10.0: {}
|
||||||
|
|
||||||
|
bcrypt@5.1.1:
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/node-pre-gyp': 1.0.11
|
||||||
|
node-addon-api: 5.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
bech32@1.1.4: {}
|
bech32@1.1.4: {}
|
||||||
|
|
||||||
bech32@2.0.0: {}
|
bech32@2.0.0: {}
|
||||||
@@ -3837,8 +4018,12 @@ snapshots:
|
|||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
bs58: 6.0.0
|
bs58: 6.0.0
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer-more-ints@1.0.0: {}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -3895,6 +4080,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
|
chownr@2.0.0: {}
|
||||||
|
|
||||||
ci-info@2.0.0: {}
|
ci-info@2.0.0: {}
|
||||||
|
|
||||||
cipher-base@1.0.7:
|
cipher-base@1.0.7:
|
||||||
@@ -3921,6 +4108,8 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
color-support@1.1.3: {}
|
||||||
|
|
||||||
colorette@2.0.19: {}
|
colorette@2.0.19: {}
|
||||||
|
|
||||||
command-exists@1.2.9: {}
|
command-exists@1.2.9: {}
|
||||||
@@ -3935,6 +4124,8 @@ snapshots:
|
|||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
console-control-strings@1.1.0: {}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -4008,12 +4199,13 @@ snapshots:
|
|||||||
|
|
||||||
delay@5.0.0: {}
|
delay@5.0.0: {}
|
||||||
|
|
||||||
|
delegates@1.0.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
destroy@1.2.0: {}
|
destroy@1.2.0: {}
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
diff@4.0.4: {}
|
diff@4.0.4: {}
|
||||||
|
|
||||||
@@ -4033,6 +4225,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xtend: 4.0.2
|
xtend: 4.0.2
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ecpair@3.0.1(typescript@5.9.3):
|
ecpair@3.0.1(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
uint8array-tools: 0.0.8
|
uint8array-tools: 0.0.8
|
||||||
@@ -4117,42 +4313,6 @@ snapshots:
|
|||||||
'@scure/bip32': 1.4.0
|
'@scure/bip32': 1.4.0
|
||||||
'@scure/bip39': 1.3.0
|
'@scure/bip39': 1.3.0
|
||||||
|
|
||||||
ethers@5.7.2(bufferutil@4.1.0):
|
|
||||||
dependencies:
|
|
||||||
'@ethersproject/abi': 5.7.0
|
|
||||||
'@ethersproject/abstract-provider': 5.7.0
|
|
||||||
'@ethersproject/abstract-signer': 5.7.0
|
|
||||||
'@ethersproject/address': 5.7.0
|
|
||||||
'@ethersproject/base64': 5.7.0
|
|
||||||
'@ethersproject/basex': 5.7.0
|
|
||||||
'@ethersproject/bignumber': 5.7.0
|
|
||||||
'@ethersproject/bytes': 5.7.0
|
|
||||||
'@ethersproject/constants': 5.7.0
|
|
||||||
'@ethersproject/contracts': 5.7.0
|
|
||||||
'@ethersproject/hash': 5.7.0
|
|
||||||
'@ethersproject/hdnode': 5.7.0
|
|
||||||
'@ethersproject/json-wallets': 5.7.0
|
|
||||||
'@ethersproject/keccak256': 5.7.0
|
|
||||||
'@ethersproject/logger': 5.7.0
|
|
||||||
'@ethersproject/networks': 5.7.1
|
|
||||||
'@ethersproject/pbkdf2': 5.7.0
|
|
||||||
'@ethersproject/properties': 5.7.0
|
|
||||||
'@ethersproject/providers': 5.7.2(bufferutil@4.1.0)
|
|
||||||
'@ethersproject/random': 5.7.0
|
|
||||||
'@ethersproject/rlp': 5.7.0
|
|
||||||
'@ethersproject/sha2': 5.7.0
|
|
||||||
'@ethersproject/signing-key': 5.7.0
|
|
||||||
'@ethersproject/solidity': 5.7.0
|
|
||||||
'@ethersproject/strings': 5.7.0
|
|
||||||
'@ethersproject/transactions': 5.7.0
|
|
||||||
'@ethersproject/units': 5.7.0
|
|
||||||
'@ethersproject/wallet': 5.7.0
|
|
||||||
'@ethersproject/web': 5.7.1
|
|
||||||
'@ethersproject/wordlists': 5.7.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
ethers@5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6):
|
ethers@5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ethersproject/abi': 5.7.0
|
'@ethersproject/abi': 5.7.0
|
||||||
@@ -4191,6 +4351,10 @@ snapshots:
|
|||||||
|
|
||||||
eventemitter3@5.0.4: {}
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
|
express-rate-limit@7.5.1(express@4.22.1):
|
||||||
|
dependencies:
|
||||||
|
express: 4.22.1
|
||||||
|
|
||||||
express@4.22.1:
|
express@4.22.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
@@ -4278,6 +4442,10 @@ snapshots:
|
|||||||
jsonfile: 4.0.0
|
jsonfile: 4.0.0
|
||||||
universalify: 0.1.2
|
universalify: 0.1.2
|
||||||
|
|
||||||
|
fs-minipass@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
|
||||||
fs.realpath@1.0.0: {}
|
fs.realpath@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
@@ -4285,6 +4453,18 @@ snapshots:
|
|||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
|
gauge@3.0.2:
|
||||||
|
dependencies:
|
||||||
|
aproba: 2.1.0
|
||||||
|
color-support: 1.1.3
|
||||||
|
console-control-strings: 1.1.0
|
||||||
|
has-unicode: 2.0.1
|
||||||
|
object-assign: 4.1.1
|
||||||
|
signal-exit: 3.0.7
|
||||||
|
string-width: 4.2.3
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wide-align: 1.1.5
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
@@ -4400,6 +4580,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
has-unicode@2.0.1: {}
|
||||||
|
|
||||||
hash-base@3.1.2:
|
hash-base@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@@ -4542,6 +4724,30 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
jsonwebtoken@9.0.3:
|
||||||
|
dependencies:
|
||||||
|
jws: 4.0.1
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.7.4
|
||||||
|
|
||||||
|
jwa@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
jws@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
jwa: 2.0.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
keccak@3.0.4:
|
keccak@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api: 2.0.2
|
node-addon-api: 2.0.2
|
||||||
@@ -4575,6 +4781,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
||||||
|
lodash.includes@4.3.0: {}
|
||||||
|
|
||||||
|
lodash.isboolean@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isinteger@4.0.4: {}
|
||||||
|
|
||||||
|
lodash.isnumber@3.0.3: {}
|
||||||
|
|
||||||
|
lodash.isplainobject@4.0.6: {}
|
||||||
|
|
||||||
|
lodash.isstring@4.0.1: {}
|
||||||
|
|
||||||
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
lodash@4.17.23: {}
|
lodash@4.17.23: {}
|
||||||
|
|
||||||
log-symbols@4.1.0:
|
log-symbols@4.1.0:
|
||||||
@@ -4584,6 +4804,10 @@ snapshots:
|
|||||||
|
|
||||||
lru_map@0.3.3: {}
|
lru_map@0.3.3: {}
|
||||||
|
|
||||||
|
make-dir@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 6.3.1
|
||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
@@ -4634,6 +4858,17 @@ snapshots:
|
|||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
|
minipass@3.3.6:
|
||||||
|
dependencies:
|
||||||
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
minipass@5.0.0: {}
|
||||||
|
|
||||||
|
minizlib@2.1.2:
|
||||||
|
dependencies:
|
||||||
|
minipass: 3.3.6
|
||||||
|
yallist: 4.0.0
|
||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mnemonist@0.38.5:
|
mnemonist@0.38.5:
|
||||||
@@ -4699,14 +4934,27 @@ snapshots:
|
|||||||
|
|
||||||
node-addon-api@2.0.2: {}
|
node-addon-api@2.0.2: {}
|
||||||
|
|
||||||
|
node-addon-api@5.1.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
|
nopt@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
abbrev: 1.1.1
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
|
npmlog@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
are-we-there-yet: 2.0.0
|
||||||
|
console-control-strings: 1.1.0
|
||||||
|
gauge: 3.0.2
|
||||||
|
set-blocking: 2.0.0
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
object-inspect@1.13.4: {}
|
object-inspect@1.13.4: {}
|
||||||
@@ -4885,6 +5133,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
|
rimraf@3.0.2:
|
||||||
|
dependencies:
|
||||||
|
glob: 7.2.3
|
||||||
|
|
||||||
ripemd160@2.0.3:
|
ripemd160@2.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
hash-base: 3.1.2
|
hash-base: 3.1.2
|
||||||
@@ -4919,8 +5171,7 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.4:
|
semver@7.7.4: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4953,6 +5204,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
set-blocking@2.0.0: {}
|
||||||
|
|
||||||
set-function-length@1.2.2:
|
set-function-length@1.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
define-data-property: 1.1.4
|
define-data-property: 1.1.4
|
||||||
@@ -5030,6 +5283,8 @@ snapshots:
|
|||||||
side-channel-map: 1.0.1
|
side-channel-map: 1.0.1
|
||||||
side-channel-weakmap: 1.0.2
|
side-channel-weakmap: 1.0.2
|
||||||
|
|
||||||
|
signal-exit@3.0.7: {}
|
||||||
|
|
||||||
solc@0.8.26(debug@4.4.3):
|
solc@0.8.26(debug@4.4.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
command-exists: 1.2.9
|
command-exists: 1.2.9
|
||||||
@@ -5102,17 +5357,17 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
swagger-ui-dist@5.32.1:
|
|
||||||
dependencies:
|
|
||||||
'@scarf/scarf': 1.4.0
|
|
||||||
|
|
||||||
swagger-ui-express@5.0.1(express@4.22.1):
|
|
||||||
dependencies:
|
|
||||||
express: 4.22.1
|
|
||||||
swagger-ui-dist: 5.32.1
|
|
||||||
|
|
||||||
tagged-tag@1.0.0: {}
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
|
tar@6.2.1:
|
||||||
|
dependencies:
|
||||||
|
chownr: 2.0.0
|
||||||
|
fs-minipass: 2.1.0
|
||||||
|
minipass: 5.0.0
|
||||||
|
minizlib: 2.1.2
|
||||||
|
mkdirp: 1.0.4
|
||||||
|
yallist: 4.0.0
|
||||||
|
|
||||||
tarn@3.0.2: {}
|
tarn@3.0.2: {}
|
||||||
|
|
||||||
text-encoding-utf-8@1.0.2: {}
|
text-encoding-utf-8@1.0.2: {}
|
||||||
@@ -5321,6 +5576,10 @@ snapshots:
|
|||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
wide-align@1.1.5:
|
||||||
|
dependencies:
|
||||||
|
string-width: 4.2.3
|
||||||
|
|
||||||
widest-line@3.1.0:
|
widest-line@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -5339,10 +5598,6 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
ws@7.4.6(bufferutil@4.1.0):
|
|
||||||
optionalDependencies:
|
|
||||||
bufferutil: 4.1.0
|
|
||||||
|
|
||||||
ws@7.4.6(bufferutil@4.1.0)(utf-8-validate@6.0.6):
|
ws@7.4.6(bufferutil@4.1.0)(utf-8-validate@6.0.6):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
bufferutil: 4.1.0
|
bufferutil: 4.1.0
|
||||||
@@ -5362,6 +5617,8 @@ snapshots:
|
|||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
yallist@4.0.0: {}
|
||||||
|
|
||||||
yargs-parser@20.2.9: {}
|
yargs-parser@20.2.9: {}
|
||||||
|
|
||||||
yargs-unparser@2.0.0:
|
yargs-unparser@2.0.0:
|
||||||
|
|||||||
18
scripts/bitok-entrypoint.sh
Normal file
18
scripts/bitok-entrypoint.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Read Vault root token from shared volume (written by vault-init)
|
||||||
|
TOKEN_FILE="/vault/file/root-token"
|
||||||
|
if [ -f "$TOKEN_FILE" ]; then
|
||||||
|
export VAULT_TOKEN=$(cat "$TOKEN_FILE" | tr -d '\n\r ')
|
||||||
|
echo "[bitok-entrypoint] Loaded VAULT_TOKEN from $TOKEN_FILE"
|
||||||
|
else
|
||||||
|
echo "[bitok-entrypoint] WARNING: $TOKEN_FILE not found, using VAULT_TOKEN from env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start BITOK auth service
|
||||||
|
exec granian --interface asgi ${APP_MODULE:-src.main:app} \
|
||||||
|
--host ${APP_HOST:-0.0.0.0} \
|
||||||
|
--port ${APP_PORT:-8000} \
|
||||||
|
--workers ${APP_WORKERS:-1} \
|
||||||
|
--loop uvloop
|
||||||
128
scripts/vault-init.sh
Normal file
128
scripts/vault-init.sh
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export VAULT_ADDR=http://vault:8200
|
||||||
|
INIT_FILE="/vault/file/init-keys.json"
|
||||||
|
TOKEN_FILE="/vault/file/root-token"
|
||||||
|
|
||||||
|
echo "[vault-init] Waiting for Vault to respond..."
|
||||||
|
until wget -qO- ${VAULT_ADDR}/v1/sys/seal-status > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if Vault is initialized (use HTTP API — vault CLI may suppress output when sealed)
|
||||||
|
STATUS_JSON=$(wget -qO- ${VAULT_ADDR}/v1/sys/seal-status 2>/dev/null || echo '{}')
|
||||||
|
INITIALIZED=$(echo "$STATUS_JSON" | grep -o '"initialized":[a-z]*' | cut -d: -f2)
|
||||||
|
echo "[vault-init] Vault initialized=$INITIALIZED"
|
||||||
|
|
||||||
|
FIRST_RUN="false"
|
||||||
|
|
||||||
|
if [ "$INITIALIZED" != "true" ]; then
|
||||||
|
echo "[vault-init] First run — initializing Vault..."
|
||||||
|
FIRST_RUN="true"
|
||||||
|
|
||||||
|
# Init with 1 key share for dev simplicity
|
||||||
|
vault operator init -key-shares=1 -key-threshold=1 -format=json > "$INIT_FILE"
|
||||||
|
|
||||||
|
UNSEAL_KEY=$(tr -d ' \n' < "$INIT_FILE" | grep -o '"unseal_keys_b64":\["[^"]*"' | cut -d'"' -f4)
|
||||||
|
ROOT_TOKEN=$(tr -d ' \n' < "$INIT_FILE" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
|
||||||
|
echo "[vault-init] Unsealing with key: ${UNSEAL_KEY:0:8}..."
|
||||||
|
vault operator unseal "$UNSEAL_KEY"
|
||||||
|
export VAULT_TOKEN="$ROOT_TOKEN"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "[vault-init] Vault already initialized."
|
||||||
|
|
||||||
|
SEALED=$(echo "$STATUS_JSON" | grep -o '"sealed":[a-z]*' | cut -d: -f2)
|
||||||
|
echo "[vault-init] Vault sealed=$SEALED"
|
||||||
|
|
||||||
|
if [ "$SEALED" = "true" ]; then
|
||||||
|
echo "[vault-init] Vault is sealed, unsealing..."
|
||||||
|
if [ ! -f "$INIT_FILE" ]; then
|
||||||
|
echo "[vault-init] ERROR: init-keys.json not found. Cannot unseal."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
UNSEAL_KEY=$(tr -d ' \n' < "$INIT_FILE" | grep -o '"unseal_keys_b64":\["[^"]*"' | cut -d'"' -f4)
|
||||||
|
vault operator unseal "$UNSEAL_KEY"
|
||||||
|
echo "[vault-init] Vault unsealed."
|
||||||
|
else
|
||||||
|
echo "[vault-init] Vault already unsealed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load root token
|
||||||
|
if [ -f "$INIT_FILE" ]; then
|
||||||
|
ROOT_TOKEN=$(tr -d ' \n' < "$INIT_FILE" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
export VAULT_TOKEN="$ROOT_TOKEN"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Write root-token file for other containers ──
|
||||||
|
echo "$ROOT_TOKEN" > "$TOKEN_FILE"
|
||||||
|
|
||||||
|
# ── Ensure secrets engines exist ──
|
||||||
|
# Enable kv mount (wallet) — ignore error if already enabled
|
||||||
|
vault secrets enable -path=kv -version=2 kv 2>/dev/null || true
|
||||||
|
|
||||||
|
# Enable secrets mount (BITOK) — ignore error if already enabled
|
||||||
|
vault secrets enable -path=secrets -version=2 kv 2>/dev/null || true
|
||||||
|
|
||||||
|
# ── Wallet infrastructure secrets ──
|
||||||
|
echo "[vault-init] Writing wallet secrets..."
|
||||||
|
vault kv put kv/cryptowallet \
|
||||||
|
db_host=postgres \
|
||||||
|
db_port=5432 \
|
||||||
|
db_user=postgres \
|
||||||
|
db_password=postgres \
|
||||||
|
db_name=cryptowallet_devphase3 \
|
||||||
|
relay_api_key=""
|
||||||
|
|
||||||
|
# ── BITOK database secret ──
|
||||||
|
echo "[vault-init] Writing BITOK secrets..."
|
||||||
|
vault kv put secrets/database \
|
||||||
|
HOST=postgres \
|
||||||
|
PORT=5432 \
|
||||||
|
USER=postgres \
|
||||||
|
PASSWORD=postgres \
|
||||||
|
NAME=bitok_dev
|
||||||
|
|
||||||
|
# ── BITOK RabbitMQ secret ──
|
||||||
|
vault kv put secrets/rabbitmq \
|
||||||
|
HOST=rabbitmq \
|
||||||
|
PORT=5672 \
|
||||||
|
USER=guest \
|
||||||
|
PASSWORD=guest \
|
||||||
|
VHOST=/
|
||||||
|
|
||||||
|
# ── BITOK CSRF secret ──
|
||||||
|
vault kv put secrets/csrf \
|
||||||
|
KEY=dev-csrf-secret-key-minimum-32-characters-long
|
||||||
|
|
||||||
|
# ── BITOK JWT RS256 key pair (only generate on first run) ──
|
||||||
|
# Check if JWT keys already exist
|
||||||
|
JWT_EXISTS=$(vault kv get -format=json secrets/jwt/kid 2>/dev/null && echo "yes" || echo "no")
|
||||||
|
|
||||||
|
if [ "$JWT_EXISTS" = "no" ]; then
|
||||||
|
echo "[vault-init] Generating RSA-2048 key pair for JWT..."
|
||||||
|
apk add --no-cache openssl > /dev/null 2>&1 || true
|
||||||
|
openssl genrsa -out /tmp/jwt_private.pem 2048 2>/dev/null
|
||||||
|
openssl rsa -in /tmp/jwt_private.pem -pubout -out /tmp/jwt_public.pem 2>/dev/null
|
||||||
|
|
||||||
|
PRIVATE_KEY=$(cat /tmp/jwt_private.pem)
|
||||||
|
PUBLIC_KEY=$(cat /tmp/jwt_public.pem)
|
||||||
|
|
||||||
|
vault kv put secrets/jwt/kid \
|
||||||
|
active=kid-dev-001 \
|
||||||
|
previous=""
|
||||||
|
|
||||||
|
vault kv put secrets/jwt/kids/kid-dev-001 \
|
||||||
|
private_key="$PRIVATE_KEY" \
|
||||||
|
public_key="$PUBLIC_KEY"
|
||||||
|
|
||||||
|
rm -f /tmp/jwt_private.pem /tmp/jwt_public.pem
|
||||||
|
echo "[vault-init] JWT keys generated."
|
||||||
|
else
|
||||||
|
echo "[vault-init] JWT keys already exist, skipping generation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[vault-init] All secrets ready (wallet + BITOK). Done."
|
||||||
208
start.bat
208
start.bat
@@ -4,7 +4,7 @@ cd /d "%~dp0"
|
|||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo CryptoWallet - Local Dev Starter
|
echo CryptoWallet + BITOK Auth - Local Dev
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
@@ -16,68 +16,123 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: ── 2. Locate PostgreSQL binaries ─────────────────────────────────────────────
|
:: ── 2. Check Docker ──────────────────────────────────────────────────────────
|
||||||
set "PGBIN="
|
where docker >nul 2>&1
|
||||||
if exist "C:\Program Files\PostgreSQL\16\bin\pg_isready.exe" set "PGBIN=C:\Program Files\PostgreSQL\16\bin"
|
if errorlevel 1 (
|
||||||
if not defined PGBIN (
|
echo [ERROR] Docker not found. Install Docker Desktop and ensure it is running.
|
||||||
where pg_isready >nul 2>&1
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] PostgreSQL 16 not found. Expected at C:\Program Files\PostgreSQL\16\bin
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
set "PGPASSWORD=postgres"
|
|
||||||
|
|
||||||
:: ── 3. Start local PostgreSQL service ────────────────────────────────────────
|
|
||||||
echo [1/4] Starting PostgreSQL...
|
|
||||||
net start postgresql-x64-16 2>nul
|
|
||||||
timeout /t 2 /nobreak >nul
|
|
||||||
|
|
||||||
set /a pg_attempts=0
|
|
||||||
:waitpg
|
|
||||||
set /a pg_attempts+=1
|
|
||||||
if !pg_attempts! gtr 15 (
|
|
||||||
echo [ERROR] PostgreSQL not responding after 30 seconds.
|
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
if defined PGBIN (
|
|
||||||
"%PGBIN%\pg_isready.exe" -U postgres -q 2>nul
|
:: ── 3. Stop local PostgreSQL (if any) so Docker gets port 5432 ─────────────────
|
||||||
) else (
|
echo [1/8] Preparing environment...
|
||||||
pg_isready -U postgres -q 2>nul
|
net stop postgresql-x64-16 2>nul
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
|
||||||
|
:: ── 4. Wait for Docker Engine ─────────────────────────────────────────────────
|
||||||
|
echo Waiting for Docker Engine...
|
||||||
|
set /a docker_attempts=0
|
||||||
|
:waitdocker
|
||||||
|
set /a docker_attempts+=1
|
||||||
|
if !docker_attempts! gtr 30 (
|
||||||
|
echo [ERROR] Docker Engine did not respond after 60 seconds.
|
||||||
|
echo Make sure Docker Desktop is fully started ^(whale icon in tray^).
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
docker info >nul 2>&1
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
timeout /t 2 /nobreak >nul
|
timeout /t 2 /nobreak >nul
|
||||||
goto waitpg
|
goto waitdocker
|
||||||
)
|
)
|
||||||
echo PostgreSQL is ready.
|
echo Docker ready.
|
||||||
|
|
||||||
:: ── 4. Ensure DB exists (create only if missing) ────────────────────────────
|
:: ── 5. Start infrastructure via Docker Compose ────────────────────────────────
|
||||||
echo [2/4] Ensuring database...
|
echo [2/8] Starting infrastructure (PostgreSQL, Vault, RabbitMQ, KeyDB)...
|
||||||
if defined PGBIN (
|
docker compose up -d postgres vault vault-init rabbitmq keydb 2>&1
|
||||||
"%PGBIN%\psql.exe" -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='cryptowallet_v2'" 2>nul | findstr "1" >nul 2>nul
|
if errorlevel 1 (
|
||||||
) else (
|
echo Retrying with docker-compose...
|
||||||
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='cryptowallet_v2'" 2>nul | findstr "1" >nul 2>nul
|
docker-compose up -d postgres vault vault-init rabbitmq keydb 2>&1
|
||||||
)
|
)
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo Creating database cryptowallet_v2...
|
echo.
|
||||||
if defined PGBIN (
|
echo [ERROR] Could not start Docker Compose.
|
||||||
"%PGBIN%\psql.exe" -U postgres -c "CREATE DATABASE cryptowallet_v2" 2>nul
|
echo.
|
||||||
) else (
|
echo Common fixes:
|
||||||
psql -U postgres -c "CREATE DATABASE cryptowallet_v2" 2>nul
|
echo 1. Wait 30-60 sec after opening Docker Desktop
|
||||||
)
|
echo 2. Restart Docker Desktop ^(right-click tray icon -^> Restart^)
|
||||||
) else (
|
echo 3. Run: docker compose up -d postgres vault vault-init rabbitmq keydb
|
||||||
echo Database exists.
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: ── 5. Install deps + migrate ────────────────────────────────────────────────
|
:: Wait for PostgreSQL to be ready
|
||||||
echo [3/4] Installing dependencies...
|
set /a pg_attempts=0
|
||||||
|
:waitpg
|
||||||
|
set /a pg_attempts+=1
|
||||||
|
if !pg_attempts! gtr 30 (
|
||||||
|
echo [ERROR] PostgreSQL not responding after 60 seconds.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
docker exec cryptowallet-db pg_isready -U postgres -q 2>nul
|
||||||
|
if errorlevel 1 goto waitpg
|
||||||
|
echo PostgreSQL is ready.
|
||||||
|
|
||||||
|
:: Wait for RabbitMQ to be ready
|
||||||
|
echo Waiting for RabbitMQ...
|
||||||
|
set /a rmq_attempts=0
|
||||||
|
:waitrmq
|
||||||
|
set /a rmq_attempts+=1
|
||||||
|
if !rmq_attempts! gtr 30 (
|
||||||
|
echo [WARN] RabbitMQ slow to start, continuing anyway...
|
||||||
|
goto rmq_done
|
||||||
|
)
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
docker exec cryptowallet-rabbitmq rabbitmq-diagnostics check_port_connectivity >nul 2>&1
|
||||||
|
if errorlevel 1 goto waitrmq
|
||||||
|
:rmq_done
|
||||||
|
echo RabbitMQ is ready.
|
||||||
|
|
||||||
|
:: ── 6. Create databases ──────────────────────────────────────────────────────
|
||||||
|
echo [3/8] Ensuring databases...
|
||||||
|
docker exec cryptowallet-db psql -U postgres -c "CREATE DATABASE cryptowallet_devphase3" 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo cryptowallet_devphase3 may already exist, continuing...
|
||||||
|
)
|
||||||
|
docker exec cryptowallet-db psql -U postgres -c "CREATE DATABASE bitok_dev" 2>nul
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo bitok_dev may already exist, continuing...
|
||||||
|
)
|
||||||
|
echo Databases ready.
|
||||||
|
|
||||||
|
:: ── 7. Wait for vault-init to finish ─────────────────────────────────────────
|
||||||
|
echo [4/8] Waiting for Vault initialization...
|
||||||
|
set /a vault_attempts=0
|
||||||
|
:waitvault
|
||||||
|
set /a vault_attempts+=1
|
||||||
|
if !vault_attempts! gtr 30 (
|
||||||
|
echo [WARN] vault-init taking long, continuing...
|
||||||
|
goto vault_done
|
||||||
|
)
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
docker inspect cryptowallet-vault-init --format="{{.State.Status}}" 2>nul | findstr /C:"exited" >nul 2>&1
|
||||||
|
if errorlevel 1 goto waitvault
|
||||||
|
:vault_done
|
||||||
|
echo Vault initialized.
|
||||||
|
|
||||||
|
:: ── 8. Update .env ───────────────────────────────────────────────────────────
|
||||||
|
echo [5/8] Updating .env...
|
||||||
if not exist .env (
|
if not exist .env (
|
||||||
copy .env.example .env >nul
|
copy .env.example .env >nul
|
||||||
echo Created .env from .env.example
|
|
||||||
)
|
)
|
||||||
|
powershell -NoProfile -Command "(Get-Content .env) -replace '^DB_NAME=.*', 'DB_NAME=cryptowallet_devphase3' -replace '^DATABASE_URL=.*', 'DATABASE_URL=postgresql://postgres:postgres@localhost:5432/cryptowallet_devphase3' | Set-Content .env"
|
||||||
|
echo .env updated.
|
||||||
|
|
||||||
|
:: ── 9. Install deps and run migrations ────────────────────────────────────────
|
||||||
|
echo [6/8] Installing dependencies...
|
||||||
call pnpm install
|
call pnpm install
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [ERROR] pnpm install failed.
|
echo [ERROR] pnpm install failed.
|
||||||
@@ -86,39 +141,82 @@ if errorlevel 1 (
|
|||||||
)
|
)
|
||||||
echo Dependencies installed.
|
echo Dependencies installed.
|
||||||
|
|
||||||
echo Running migrations...
|
echo Running database migrations...
|
||||||
cd apps\api
|
cd apps\api
|
||||||
call pnpm migrate
|
call pnpm migrate
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo [ERROR] Migrations failed.
|
echo Migration failed - resetting DB and retrying...
|
||||||
|
call pnpm db:reset
|
||||||
|
call pnpm migrate
|
||||||
|
)
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [ERROR] Migrations failed - check output above.
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
cd ..\..
|
cd ..\..
|
||||||
echo Migrations done.
|
echo Migrations done.
|
||||||
|
|
||||||
:: ── 6. Stop old processes and start servers ──────────────────────────────────
|
:: ── 10. Initialize BITOK database tables ──────────────────────────────────────
|
||||||
echo [4/4] Starting servers...
|
echo [7/8] Initializing BITOK database...
|
||||||
|
if exist BITOK\sql\tables.sql (
|
||||||
|
docker exec -i cryptowallet-db psql -U postgres -d bitok_dev < BITOK\sql\tables.sql 2>nul
|
||||||
|
echo BITOK tables initialized.
|
||||||
|
) else (
|
||||||
|
echo BITOK/sql/tables.sql not found, skipping...
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ── 11. Start BITOK auth service (Docker) ─────────────────────────────────────
|
||||||
|
echo Starting BITOK auth service...
|
||||||
|
docker compose up -d bitok-auth 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo [WARN] BITOK auth container failed to start. Check: docker logs cryptowallet-bitok
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Wait briefly for BITOK to be responsive
|
||||||
|
set /a bitok_attempts=0
|
||||||
|
:waitbitok
|
||||||
|
set /a bitok_attempts+=1
|
||||||
|
if !bitok_attempts! gtr 15 (
|
||||||
|
echo [WARN] BITOK slow to start, continuing...
|
||||||
|
goto bitok_done
|
||||||
|
)
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
powershell -NoProfile -Command "try { $r = Invoke-WebRequest -Uri http://localhost:8000/ping -TimeoutSec 2 -UseBasicParsing; exit 0 } catch { exit 1 }" >nul 2>&1
|
||||||
|
if errorlevel 1 goto waitbitok
|
||||||
|
:bitok_done
|
||||||
|
echo BITOK auth service ready.
|
||||||
|
|
||||||
|
:: ── 12. Stop old dev servers and clear lock ─────────────────────────────────────
|
||||||
|
echo [8/8] Starting wallet servers...
|
||||||
powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort 3000,3001 -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"
|
powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort 3000,3001 -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"
|
||||||
if exist apps\web\.next\dev\lock del apps\web\.next\dev\lock 2>nul
|
if exist apps\web\.next\dev\lock del apps\web\.next\dev\lock 2>nul
|
||||||
timeout /t 1 /nobreak >nul
|
timeout /t 1 /nobreak >nul
|
||||||
|
|
||||||
|
:: Start wallet API and Web
|
||||||
start "CryptoWallet API [port 3001]" cmd /k "cd /d %~dp0apps\api && pnpm dev"
|
start "CryptoWallet API [port 3001]" cmd /k "cd /d %~dp0apps\api && pnpm dev"
|
||||||
timeout /t 2 /nobreak >nul
|
timeout /t 2 /nobreak >nul
|
||||||
start "CryptoWallet Web [port 3000]" cmd /k "cd /d %~dp0apps\web && pnpm dev"
|
start "CryptoWallet Web [port 3000]" cmd /k "cd /d %~dp0apps\web && pnpm dev"
|
||||||
|
|
||||||
|
:: ── Done ─────────────────────────────────────────────────────────────────────
|
||||||
echo.
|
echo.
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo All services started!
|
echo All services started!
|
||||||
echo.
|
echo.
|
||||||
echo Web: http://localhost:3000
|
echo Web: http://localhost:3000
|
||||||
echo API: http://localhost:3001
|
echo API: http://localhost:3001
|
||||||
echo DB: localhost:5432 / cryptowallet_v2
|
echo BITOK: http://localhost:8000
|
||||||
|
echo RabbitMQ: http://localhost:15672 (guest/guest)
|
||||||
|
echo DB: localhost:5432
|
||||||
|
echo wallet DB: cryptowallet_devphase3
|
||||||
|
echo BITOK DB: bitok_dev
|
||||||
|
echo user: postgres pass: postgres
|
||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
timeout /t 3 /nobreak >nul
|
timeout /t 3 /nobreak >nul
|
||||||
start http://localhost:3000
|
start http://localhost:3000
|
||||||
|
|
||||||
|
echo Press any key to close this launcher...
|
||||||
pause >nul
|
pause >nul
|
||||||
endlocal
|
endlocal
|
||||||
|
|||||||
12
vault/vault.hcl
Normal file
12
vault/vault.hcl
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
ui = true
|
||||||
|
|
||||||
|
storage "file" {
|
||||||
|
path = "/vault/file"
|
||||||
|
}
|
||||||
|
|
||||||
|
listener "tcp" {
|
||||||
|
address = "0.0.0.0:8200"
|
||||||
|
tls_disable = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_mlock = true
|
||||||
Reference in New Issue
Block a user