From a81e29807c2b478a0957604665873a64d0989014 Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:11:27 +0300 Subject: [PATCH] add project --- .env.example | 24 + .gitignore | 11 + apps/api/Dockerfile | 28 + apps/api/package.json | 39 + apps/api/src/app.ts | 43 + apps/api/src/config/database.ts | 32 + apps/api/src/config/env.ts | 28 + apps/api/src/config/swagger.ts | 5 + apps/api/src/controllers/wallet.controller.ts | 20 + apps/api/src/db/knexfile.ts | 23 + .../api/src/db/migrations/001_create_users.ts | 28 + .../src/db/migrations/002_create_wallets.ts | 20 + .../src/db/migrations/003_create_sessions.ts | 26 + apps/api/src/index.ts | 23 + apps/api/src/middleware/auth.ts | 39 + apps/api/src/middleware/error-handler.ts | 6 + apps/api/src/middleware/validate.ts | 17 + apps/api/src/models/session.model.ts | 66 + apps/api/src/models/user.model.ts | 49 + apps/api/src/models/wallet.model.ts | 24 + apps/api/src/routes/bsc-swap-proxy.routes.ts | 192 + apps/api/src/routes/btc-proxy.routes.ts | 148 + apps/api/src/routes/relay-proxy.routes.ts | 52 + apps/api/src/routes/sol-swap-proxy.routes.ts | 166 + apps/api/src/routes/tron-proxy.routes.ts | 268 + apps/api/src/routes/tron-swap-proxy.routes.ts | 471 ++ apps/api/src/routes/wallet.routes.ts | 9 + apps/api/src/services/jwt.service.ts | 92 + apps/api/src/utils/ulid.ts | 5 + apps/api/swagger.json | 101 + apps/api/tsconfig.json | 19 + apps/web/.gitignore | 41 + apps/web/README.md | 36 + apps/web/next.config.ts | 28 + apps/web/package.json | 38 + apps/web/pnpm-lock.yaml | 1770 ++++++ apps/web/pnpm-workspace.yaml | 3 + apps/web/public/file.svg | 1 + apps/web/public/globe.svg | 1 + apps/web/public/next.svg | 1 + apps/web/public/vercel.svg | 1 + apps/web/public/window.svg | 1 + apps/web/src/app/bridge/page.tsx | 327 + apps/web/src/app/dashboard/page.tsx | 152 + apps/web/src/app/favicon.ico | Bin 0 -> 25931 bytes apps/web/src/app/globals.css | 42 + apps/web/src/app/layout.tsx | 17 + apps/web/src/app/page.module.css | 141 + apps/web/src/app/page.tsx | 5 + apps/web/src/app/receive/page.tsx | 225 + apps/web/src/app/send/page.tsx | 561 ++ apps/web/src/app/settings/page.tsx | 45 + apps/web/src/app/swap/page.tsx | 328 + apps/web/src/hooks/useBalances.ts | 52 + apps/web/src/hooks/useBridge.ts | 191 + apps/web/src/hooks/useGasPrice.ts | 36 + apps/web/src/hooks/useGasSettings.ts | 64 + apps/web/src/hooks/useSwap.ts | 274 + apps/web/src/lib/api.ts | 28 + apps/web/src/lib/balances/bsc-balances.ts | 185 + apps/web/src/lib/balances/btc-balances.ts | 101 + apps/web/src/lib/balances/eth-balances.ts | 257 + apps/web/src/lib/balances/index.ts | 120 + apps/web/src/lib/balances/prices.ts | 70 + apps/web/src/lib/balances/sol-balances.ts | 325 + apps/web/src/lib/balances/trx-balances.ts | 133 + apps/web/src/lib/balances/types.ts | 33 + apps/web/src/lib/bridge/constants.ts | 112 + apps/web/src/lib/bridge/execute.ts | 482 ++ apps/web/src/lib/bridge/quote.ts | 184 + apps/web/src/lib/bridge/status.ts | 40 + apps/web/src/lib/crypto/bsc-constants.ts | 4 + apps/web/src/lib/crypto/btc.ts | 17 + apps/web/src/lib/crypto/eth.ts | 9 + apps/web/src/lib/crypto/mnemonic.ts | 14 + apps/web/src/lib/crypto/sol.ts | 13 + apps/web/src/lib/crypto/trx.ts | 58 + apps/web/src/lib/env.ts | 16 + apps/web/src/lib/eth-provider.ts | 25 + apps/web/src/lib/gas-price.ts | 66 + apps/web/src/lib/qr/generate.ts | 146 + apps/web/src/lib/qr/parse.ts | 203 + apps/web/src/lib/send/constants.ts | 136 + apps/web/src/lib/send/execute.ts | 622 ++ apps/web/src/lib/send/validate.ts | 100 + apps/web/src/lib/swap/approve.ts | 96 + apps/web/src/lib/swap/bsc/execute.ts | 203 + apps/web/src/lib/swap/bsc/quote.ts | 95 + apps/web/src/lib/swap/constants.ts | 392 ++ apps/web/src/lib/swap/errors.ts | 23 + apps/web/src/lib/swap/execute.ts | 131 + apps/web/src/lib/swap/quote.ts | 365 ++ apps/web/src/lib/swap/sol/execute.ts | 65 + apps/web/src/lib/swap/sol/quote.ts | 99 + apps/web/src/lib/swap/trx/execute.ts | 91 + apps/web/src/lib/swap/trx/quote.ts | 90 + apps/web/src/store/auth-store.ts | 48 + apps/web/src/store/balance-store.ts | 114 + apps/web/tsconfig.json | 34 + contracts/DEPLOY.md | 240 + contracts/FeeSetup_SOL.md | 113 + contracts/FeeSwapRouter_BSC.sol | 154 + contracts/FeeSwapRouter_ETH.sol | 186 + contracts/FeeSwapRouter_TRX.sol | 287 + docker-start.bat | 14 + package.json | 21 + packages/shared/package.json | 13 + packages/shared/src/constants/chains.ts | 9 + packages/shared/src/index.ts | 3 + packages/shared/src/types/auth.ts | 22 + packages/shared/tsconfig.json | 11 + pnpm-lock.yaml | 5398 +++++++++++++++++ pnpm-workspace.yaml | 3 + start.bat | 124 + turbo.json | 15 + 115 files changed, 18413 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/package.json create mode 100644 apps/api/src/app.ts create mode 100644 apps/api/src/config/database.ts create mode 100644 apps/api/src/config/env.ts create mode 100644 apps/api/src/config/swagger.ts create mode 100644 apps/api/src/controllers/wallet.controller.ts create mode 100644 apps/api/src/db/knexfile.ts create mode 100644 apps/api/src/db/migrations/001_create_users.ts create mode 100644 apps/api/src/db/migrations/002_create_wallets.ts create mode 100644 apps/api/src/db/migrations/003_create_sessions.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/src/middleware/error-handler.ts create mode 100644 apps/api/src/middleware/validate.ts create mode 100644 apps/api/src/models/session.model.ts create mode 100644 apps/api/src/models/user.model.ts create mode 100644 apps/api/src/models/wallet.model.ts create mode 100644 apps/api/src/routes/bsc-swap-proxy.routes.ts create mode 100644 apps/api/src/routes/btc-proxy.routes.ts create mode 100644 apps/api/src/routes/relay-proxy.routes.ts create mode 100644 apps/api/src/routes/sol-swap-proxy.routes.ts create mode 100644 apps/api/src/routes/tron-proxy.routes.ts create mode 100644 apps/api/src/routes/tron-swap-proxy.routes.ts create mode 100644 apps/api/src/routes/wallet.routes.ts create mode 100644 apps/api/src/services/jwt.service.ts create mode 100644 apps/api/src/utils/ulid.ts create mode 100644 apps/api/swagger.json create mode 100644 apps/api/tsconfig.json create mode 100644 apps/web/.gitignore create mode 100644 apps/web/README.md create mode 100644 apps/web/next.config.ts create mode 100644 apps/web/package.json create mode 100644 apps/web/pnpm-lock.yaml create mode 100644 apps/web/pnpm-workspace.yaml create mode 100644 apps/web/public/file.svg create mode 100644 apps/web/public/globe.svg create mode 100644 apps/web/public/next.svg create mode 100644 apps/web/public/vercel.svg create mode 100644 apps/web/public/window.svg create mode 100644 apps/web/src/app/bridge/page.tsx create mode 100644 apps/web/src/app/dashboard/page.tsx create mode 100644 apps/web/src/app/favicon.ico create mode 100644 apps/web/src/app/globals.css create mode 100644 apps/web/src/app/layout.tsx create mode 100644 apps/web/src/app/page.module.css create mode 100644 apps/web/src/app/page.tsx create mode 100644 apps/web/src/app/receive/page.tsx create mode 100644 apps/web/src/app/send/page.tsx create mode 100644 apps/web/src/app/settings/page.tsx create mode 100644 apps/web/src/app/swap/page.tsx create mode 100644 apps/web/src/hooks/useBalances.ts create mode 100644 apps/web/src/hooks/useBridge.ts create mode 100644 apps/web/src/hooks/useGasPrice.ts create mode 100644 apps/web/src/hooks/useGasSettings.ts create mode 100644 apps/web/src/hooks/useSwap.ts create mode 100644 apps/web/src/lib/api.ts create mode 100644 apps/web/src/lib/balances/bsc-balances.ts create mode 100644 apps/web/src/lib/balances/btc-balances.ts create mode 100644 apps/web/src/lib/balances/eth-balances.ts create mode 100644 apps/web/src/lib/balances/index.ts create mode 100644 apps/web/src/lib/balances/prices.ts create mode 100644 apps/web/src/lib/balances/sol-balances.ts create mode 100644 apps/web/src/lib/balances/trx-balances.ts create mode 100644 apps/web/src/lib/balances/types.ts create mode 100644 apps/web/src/lib/bridge/constants.ts create mode 100644 apps/web/src/lib/bridge/execute.ts create mode 100644 apps/web/src/lib/bridge/quote.ts create mode 100644 apps/web/src/lib/bridge/status.ts create mode 100644 apps/web/src/lib/crypto/bsc-constants.ts create mode 100644 apps/web/src/lib/crypto/btc.ts create mode 100644 apps/web/src/lib/crypto/eth.ts create mode 100644 apps/web/src/lib/crypto/mnemonic.ts create mode 100644 apps/web/src/lib/crypto/sol.ts create mode 100644 apps/web/src/lib/crypto/trx.ts create mode 100644 apps/web/src/lib/env.ts create mode 100644 apps/web/src/lib/eth-provider.ts create mode 100644 apps/web/src/lib/gas-price.ts create mode 100644 apps/web/src/lib/qr/generate.ts create mode 100644 apps/web/src/lib/qr/parse.ts create mode 100644 apps/web/src/lib/send/constants.ts create mode 100644 apps/web/src/lib/send/execute.ts create mode 100644 apps/web/src/lib/send/validate.ts create mode 100644 apps/web/src/lib/swap/approve.ts create mode 100644 apps/web/src/lib/swap/bsc/execute.ts create mode 100644 apps/web/src/lib/swap/bsc/quote.ts create mode 100644 apps/web/src/lib/swap/constants.ts create mode 100644 apps/web/src/lib/swap/errors.ts create mode 100644 apps/web/src/lib/swap/execute.ts create mode 100644 apps/web/src/lib/swap/quote.ts create mode 100644 apps/web/src/lib/swap/sol/execute.ts create mode 100644 apps/web/src/lib/swap/sol/quote.ts create mode 100644 apps/web/src/lib/swap/trx/execute.ts create mode 100644 apps/web/src/lib/swap/trx/quote.ts create mode 100644 apps/web/src/store/auth-store.ts create mode 100644 apps/web/src/store/balance-store.ts create mode 100644 apps/web/tsconfig.json create mode 100644 contracts/DEPLOY.md create mode 100644 contracts/FeeSetup_SOL.md create mode 100644 contracts/FeeSwapRouter_BSC.sol create mode 100644 contracts/FeeSwapRouter_ETH.sol create mode 100644 contracts/FeeSwapRouter_TRX.sol create mode 100644 docker-start.bat create mode 100644 package.json create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/constants/chains.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/types/auth.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 start.bat create mode 100644 turbo.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9fd08bb --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# PostgreSQL +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=postgres +DB_NAME=cryptowallet_v2 + +# JWT (external auth service) +JWT_JWKS_URL= +JWT_PUBLIC_KEY= +JWT_ALGORITHM=RS256 +JWT_ISSUER= +JWT_AUDIENCE= + +# Server +API_PORT=3001 +FRONTEND_URL=http://localhost:3000 +RELAY_API_KEY= + +# TRON +TRON_API_KEY= + +# Jupiter (Solana DEX aggregator) +JUPITER_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a8b8a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.next/ +.env +.env.local +*.log +.turbo/ +coverage/ +.DS_Store +vault/data/ +vault/init-keys.json diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..4eaf708 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20-alpine AS base +RUN corepack enable && corepack prepare pnpm@latest --activate +WORKDIR /app + +FROM base AS deps +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/api/package.json apps/api/ +COPY packages/shared/package.json packages/shared/ +RUN pnpm install --frozen-lockfile --prod=false + +FROM base AS build +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules +COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules 2>/dev/null || true +COPY . . +RUN cd apps/api && pnpm build + +FROM node:20-alpine AS runtime +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 +EXPOSE 3001 +CMD ["node", "dist/index.js"] diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..ce13400 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,39 @@ +{ + "name": "@cryptowallet/api", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "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:rollback": "node --require ts-node/register node_modules/knex/bin/cli.js migrate:rollback --knexfile src/db/knexfile.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cryptowallet/shared": "workspace:*", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "ethers": "5.7.2", + "express": "^4.21.0", + "helmet": "^8.0.0", + "jose": "^6.2.2", + "knex": "^3.1.0", + "pg": "^8.13.0", + "swagger-ui-express": "^5.0.1", + "ulidx": "^2.4.1", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/cookie-parser": "^1.4.7", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/express-serve-static-core": "^5.1.1", + "@types/node": "^20.0.0", + "@types/swagger-ui-express": "^4.1.8", + "ts-node": "^10.9.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.6.0" + } +} diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..717f5d4 --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,43 @@ +import express from 'express'; +import helmet from 'helmet'; +import cors from 'cors'; +import cookieParser from 'cookie-parser'; +import swaggerUi from 'swagger-ui-express'; +import { env } from './config/env'; +import { swaggerSpec } from './config/swagger'; +import { errorHandler } from './middleware/error-handler'; +import walletRoutes from './routes/wallet.routes'; +import relayProxyRoutes from './routes/relay-proxy.routes'; +import tronProxyRoutes from './routes/tron-proxy.routes'; +import solSwapProxyRoutes from './routes/sol-swap-proxy.routes'; +import tronSwapProxyRoutes from './routes/tron-swap-proxy.routes'; +import btcProxyRoutes from './routes/btc-proxy.routes'; +import bscSwapProxyRoutes from './routes/bsc-swap-proxy.routes'; + +const app = express(); + +app.use(helmet()); +app.use(cors({ origin: env.frontendUrl, credentials: true })); +app.use(express.json()); +app.use(cookieParser()); + +app.get('/api/health', (_req, res) => { + res.json({ success: true, data: { status: 'ok' } }); +}); + +app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); +app.get('/api/docs/swagger.json', (_req, res) => { + res.json(swaggerSpec); +}); + +app.use('/api/wallets', walletRoutes); +app.use('/api/relay', relayProxyRoutes); +app.use('/api/tron', tronProxyRoutes); +app.use('/api/sol/swap', solSwapProxyRoutes); +app.use('/api/tron/swap', tronSwapProxyRoutes); +app.use('/api/btc', btcProxyRoutes); +app.use('/api/bsc/swap', bscSwapProxyRoutes); + +app.use(errorHandler); + +export default app; diff --git a/apps/api/src/config/database.ts b/apps/api/src/config/database.ts new file mode 100644 index 0000000..43aeef5 --- /dev/null +++ b/apps/api/src/config/database.ts @@ -0,0 +1,32 @@ +import knex, { Knex } from 'knex'; +import { env } from './env'; + +let _db: Knex | null = null; + +function getDb(): Knex { + if (!_db) { + _db = knex({ + client: 'pg', + connection: { + host: env.db.host, + port: env.db.port, + user: env.db.user, + password: env.db.password, + database: env.db.name, + }, + pool: { min: 2, max: 10 }, + }); + } + return _db; +} + +const callableDb = (() => undefined) as unknown as Knex; + +export const db = new Proxy(callableDb, { + apply(_target, _thisArg, args) { + return (getDb() as any)(...args); + }, + get(_target, prop) { + return (getDb() as any)[prop]; + }, +}) as Knex; diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts new file mode 100644 index 0000000..f6ce866 --- /dev/null +++ b/apps/api/src/config/env.ts @@ -0,0 +1,28 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../../../../.env') }); + +export const env = { + db: { + 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', + name: process.env.DB_NAME || 'cryptowallet_v2', + }, + 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'), + frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000', + relayApiKey: process.env.RELAY_API_KEY || null, + tronApiKey: process.env.TRON_API_KEY || null, + jupiterApiKey: process.env.JUPITER_API_KEY || null, + jupiterReferralAccount: process.env.JUPITER_REFERRAL_ACCOUNT || null, + jupiterFeeBps: parseInt(process.env.JUPITER_FEE_BPS || '70'), +}; diff --git a/apps/api/src/config/swagger.ts b/apps/api/src/config/swagger.ts new file mode 100644 index 0000000..b78c3e4 --- /dev/null +++ b/apps/api/src/config/swagger.ts @@ -0,0 +1,5 @@ +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')); diff --git a/apps/api/src/controllers/wallet.controller.ts b/apps/api/src/controllers/wallet.controller.ts new file mode 100644 index 0000000..97ef59e --- /dev/null +++ b/apps/api/src/controllers/wallet.controller.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { WalletModel } from '../models/wallet.model'; + +export const WalletController = { + async getWallets(req: Request, res: Response) { + try { + const wallets = await WalletModel.findByUserId(req.auth!.userId); + res.json({ + success: true, + data: wallets.map((w) => ({ + chain: w.chain, + address: w.address, + derivationPath: w.derivation_path, + })), + }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } + }, +}; diff --git a/apps/api/src/db/knexfile.ts b/apps/api/src/db/knexfile.ts new file mode 100644 index 0000000..03d53be --- /dev/null +++ b/apps/api/src/db/knexfile.ts @@ -0,0 +1,23 @@ +import type { Knex } from 'knex'; +import path from 'path'; +import dotenv from 'dotenv'; + +// Load .env from repo root when running migrations directly +dotenv.config({ path: path.resolve(__dirname, '../../../../.env') }); + +const config: Knex.Config = { + client: 'pg', + connection: { + 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', + database: process.env.DB_NAME || 'cryptowallet_v2', + }, + migrations: { + directory: path.resolve(__dirname, 'migrations'), + extension: __filename.endsWith('.js') ? 'js' : 'ts', + }, +}; + +export default config; diff --git a/apps/api/src/db/migrations/001_create_users.ts b/apps/api/src/db/migrations/001_create_users.ts new file mode 100644 index 0000000..6d2c69b --- /dev/null +++ b/apps/api/src/db/migrations/001_create_users.ts @@ -0,0 +1,28 @@ +import type { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('users', (t) => { + t.string('id', 26).primary(); + t.string('email', 255).notNullable().unique(); + t.string('password_hash', 255).notNullable(); + t.string('last_name', 128).nullable(); + t.string('first_name', 128).nullable(); + t.string('middle_name', 128).nullable(); + t.date('birth_date').nullable(); + 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('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now()); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('users'); +} diff --git a/apps/api/src/db/migrations/002_create_wallets.ts b/apps/api/src/db/migrations/002_create_wallets.ts new file mode 100644 index 0000000..bb8fd95 --- /dev/null +++ b/apps/api/src/db/migrations/002_create_wallets.ts @@ -0,0 +1,20 @@ +import type { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('wallets', (t) => { + t.string('id', 26).primary(); + t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE'); + t.string('chain', 10).notNullable(); + t.string('address', 256).notNullable(); + t.string('derivation_path', 64).notNullable(); + t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now()); + t.unique(['user_id', 'chain']); + }); + + await knex.schema.raw('CREATE INDEX idx_wallets_user_id ON wallets(user_id)'); + await knex.schema.raw('CREATE INDEX idx_wallets_address ON wallets(address)'); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('wallets'); +} diff --git a/apps/api/src/db/migrations/003_create_sessions.ts b/apps/api/src/db/migrations/003_create_sessions.ts new file mode 100644 index 0000000..86929e9 --- /dev/null +++ b/apps/api/src/db/migrations/003_create_sessions.ts @@ -0,0 +1,26 @@ +import type { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('sessions', (t) => { + 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('device_id', 26).nullable(); + t.string('user_agent', 500).nullable(); + t.string('first_ip', 64).nullable(); + t.string('last_ip', 64).nullable(); + 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('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_sid ON sessions(sid)'); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists('sessions'); +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..ed46826 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,23 @@ +import knex from 'knex'; +import knexConfig from './db/knexfile'; +import app from './app'; +import { env } from './config/env'; + +async function main() { + const db = knex(knexConfig); + + console.log('[API] Running migrations...'); + await db.migrate.latest(); + console.log('[API] Migrations complete.'); + + await db.destroy(); + + app.listen(env.port, () => { + console.log(`[API] Server running on port ${env.port}`); + }); +} + +main().catch((err) => { + console.error('[API] Failed to start:', err); + process.exit(1); +}); diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..18d64cb --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,39 @@ +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 { + 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' }); + } +} diff --git a/apps/api/src/middleware/error-handler.ts b/apps/api/src/middleware/error-handler.ts new file mode 100644 index 0000000..46f9b5f --- /dev/null +++ b/apps/api/src/middleware/error-handler.ts @@ -0,0 +1,6 @@ +import { Request, Response, NextFunction } from 'express'; + +export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void { + console.error('[ERROR]', err.message); + res.status(500).json({ success: false, error: 'Internal server error' }); +} diff --git a/apps/api/src/middleware/validate.ts b/apps/api/src/middleware/validate.ts new file mode 100644 index 0000000..e42bc76 --- /dev/null +++ b/apps/api/src/middleware/validate.ts @@ -0,0 +1,17 @@ +import { Request, Response, NextFunction } from 'express'; +import { ZodSchema } from 'zod'; + +export function validate(schema: ZodSchema) { + return (req: Request, res: Response, next: NextFunction): void => { + const result = schema.safeParse(req.body); + if (!result.success) { + res.status(400).json({ + success: false, + error: result.error.errors.map((e) => e.message).join(', '), + }); + return; + } + req.body = result.data; + next(); + }; +} diff --git a/apps/api/src/models/session.model.ts b/apps/api/src/models/session.model.ts new file mode 100644 index 0000000..033940a --- /dev/null +++ b/apps/api/src/models/session.model.ts @@ -0,0 +1,66 @@ +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 { + return db('sessions').where({ sid }).whereNull('revoked_at').first(); + }, + + async findByUserId(userId: string): Promise { + 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 { + const [session] = await db('sessions') + .insert({ + id: generateUlid(), + ...data, + last_ip: data.first_ip || null, + }) + .returning('*'); + return session; + }, + + async revoke(sid: string): Promise { + await db('sessions') + .where({ sid }) + .update({ revoked_at: db.fn.now(), updated_at: db.fn.now() }); + }, + + async revokeAllForUser(userId: string): Promise { + 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 { + await db('sessions') + .where({ sid }) + .update({ last_seen_at: db.fn.now(), last_ip: ip, updated_at: db.fn.now() }); + }, +}; diff --git a/apps/api/src/models/user.model.ts b/apps/api/src/models/user.model.ts new file mode 100644 index 0000000..db0863f --- /dev/null +++ b/apps/api/src/models/user.model.ts @@ -0,0 +1,49 @@ +import { db } from '../config/database'; +import { generateUlid } from '../utils/ulid'; + +export interface UserRow { + id: string; + email: string; + password_hash: string; + last_name: string | null; + first_name: string | null; + middle_name: string | null; + 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_at: Date | null; + is_deleted: boolean; + created_at: Date; + updated_at: Date; +} + +export const UserModel = { + async findByEmail(email: string): Promise { + return db('users').where({ email, is_deleted: false }).first(); + }, + + async findById(id: string): Promise { + return db('users').where({ id, is_deleted: false }).first(); + }, + + async create(data: { + email: string; + password_hash: string; + }): Promise { + const [user] = await db('users').insert({ id: generateUlid(), ...data }).returning('*'); + return user; + }, + + async update(id: string, data: Partial>): Promise { + const [user] = await db('users') + .where({ id }) + .update({ ...data, updated_at: db.fn.now() }) + .returning('*'); + return user; + }, +}; diff --git a/apps/api/src/models/wallet.model.ts b/apps/api/src/models/wallet.model.ts new file mode 100644 index 0000000..98c63d4 --- /dev/null +++ b/apps/api/src/models/wallet.model.ts @@ -0,0 +1,24 @@ +import { db } from '../config/database'; +import { generateUlid } from '../utils/ulid'; + +export interface WalletRow { + id: string; + user_id: string; + chain: string; + address: string; + derivation_path: string; + created_at: Date; +} + +export const WalletModel = { + async findByUserId(userId: string): Promise { + return db('wallets').where({ user_id: userId }); + }, + + async createMany( + wallets: { user_id: string; chain: string; address: string; derivation_path: string }[] + ): Promise { + const withIds = wallets.map((w) => ({ id: generateUlid(), ...w })); + return db('wallets').insert(withIds).returning('*'); + }, +}; diff --git a/apps/api/src/routes/bsc-swap-proxy.routes.ts b/apps/api/src/routes/bsc-swap-proxy.routes.ts new file mode 100644 index 0000000..09da839 --- /dev/null +++ b/apps/api/src/routes/bsc-swap-proxy.routes.ts @@ -0,0 +1,192 @@ +import { Request, Response, Router } from 'express'; +import { ethers } from 'ethers'; + +const router = Router(); + +const BSC_RPC = 'https://bsc-dataseed.binance.org'; +const BSC_CHAIN_ID = 56; +const BSC_TIMEOUT_MS = 15_000; + +// PancakeSwap V2 Router +const PANCAKE_ROUTER = '0x10ED43C718714eb63d5aA57B78B54704E256024E'; +const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'; + +// Supported tokens +const TOKEN_MAP: Record = { + BNB: WBNB, + USDT: '0x55d398326f99059fF775485246999027B3197955', + DOGE: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43', +}; + +const TOKEN_DECIMALS: Record = { + BNB: 18, + USDT: 18, + DOGE: 8, +}; + +const ROUTER_ABI = [ + 'function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)', + 'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable', + 'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external', +]; + +const ERC20_ABI = [ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function allowance(address owner, address spender) external view returns (uint256)', +]; + +router.get('/quote', getSwapQuote); +router.post('/build', buildSwapTx); + +export default router; + +// ─── GET /quote ─── + +async function getSwapQuote(req: Request, res: Response) { + const from = String(req.query.from || '').toUpperCase(); + const to = String(req.query.to || '').toUpperCase(); + const amount = String(req.query.amount || ''); + + if (!TOKEN_MAP[from] || !TOKEN_MAP[to]) { + res.status(400).json({ success: false, error: 'Invalid from/to. Supported: BNB, USDT, DOGE' }); + return; + } + if (from === to) { + res.status(400).json({ success: false, error: 'from and to must be different' }); + return; + } + + const amountBigInt = BigInt(amount || '0'); + if (amountBigInt <= 0n) { + res.status(400).json({ success: false, error: 'amount must be positive' }); + return; + } + + try { + const provider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC, BSC_CHAIN_ID); + const routerContract = new ethers.Contract(PANCAKE_ROUTER, ROUTER_ABI, provider); + + const path = [TOKEN_MAP[from], TOKEN_MAP[to]]; + const amounts: ethers.BigNumber[] = await withTimeout( + routerContract.getAmountsOut(amount, path), + BSC_TIMEOUT_MS, + 'PancakeSwap quote timed out' + ); + + const amountOut = amounts[amounts.length - 1].toString(); + + res.json({ + success: true, + amountIn: amountBigInt.toString(), + amountOut, + from, + to, + fromDecimals: TOKEN_DECIMALS[from], + toDecimals: TOKEN_DECIMALS[to], + }); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to get BSC swap quote'; + res.status(502).json({ success: false, error: msg }); + } +} + +// ─── POST /build ─── + +async function buildSwapTx(req: Request, res: Response) { + const { from, to, amount, amountOutMin, userAddress } = req.body; + + if (!from || !to || !amount || !amountOutMin || !userAddress) { + res.status(400).json({ success: false, error: 'Missing required fields: from, to, amount, amountOutMin, userAddress' }); + return; + } + + const fromUpper = String(from).toUpperCase(); + const toUpper = String(to).toUpperCase(); + + if (!TOKEN_MAP[fromUpper] || !TOKEN_MAP[toUpper] || fromUpper === toUpper) { + res.status(400).json({ success: false, error: 'Invalid from/to pair' }); + return; + } + + if (!ethers.utils.isAddress(userAddress)) { + res.status(400).json({ success: false, error: 'Invalid BSC address' }); + return; + } + + try { + const provider = new ethers.providers.StaticJsonRpcProvider(BSC_RPC, BSC_CHAIN_ID); + const routerContract = new ethers.Contract(PANCAKE_ROUTER, ROUTER_ABI, provider); + const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 minutes + const path = [TOKEN_MAP[fromUpper], TOKEN_MAP[toUpper]]; + + const transactions: Array<{ type: string; to: string; data: string; value: string }> = []; + + if (fromUpper === 'BNB') { + // BNB → Token: swapExactETHForTokensSupportingFeeOnTransferTokens + const data = routerContract.interface.encodeFunctionData( + 'swapExactETHForTokensSupportingFeeOnTransferTokens', + [amountOutMin, path, userAddress, deadline] + ); + + transactions.push({ + type: 'swap', + to: PANCAKE_ROUTER, + data, + value: amount, // BNB amount in wei + }); + } else { + // Token → BNB: check allowance, build approve if needed, then swap + const tokenAddress = TOKEN_MAP[fromUpper]; + const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider); + const currentAllowance: ethers.BigNumber = await withTimeout( + tokenContract.allowance(userAddress, PANCAKE_ROUTER), + BSC_TIMEOUT_MS, + 'Allowance check timed out' + ); + + if (currentAllowance.lt(ethers.BigNumber.from(amount))) { + // Build approve tx + const approveData = tokenContract.interface.encodeFunctionData( + 'approve', + [PANCAKE_ROUTER, ethers.constants.MaxUint256] + ); + + transactions.push({ + type: 'approve', + to: tokenAddress, + data: approveData, + value: '0', + }); + } + + // Build swap tx + const swapData = routerContract.interface.encodeFunctionData( + 'swapExactTokensForETHSupportingFeeOnTransferTokens', + [amount, amountOutMin, path, userAddress, deadline] + ); + + transactions.push({ + type: 'swap', + to: PANCAKE_ROUTER, + data: swapData, + value: '0', + }); + } + + res.json({ success: true, transactions }); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to build BSC swap'; + res.status(502).json({ success: false, error: msg }); + } +} + +// ─── Utils ─── + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + promise + .then((value) => { clearTimeout(timeoutId); resolve(value); }) + .catch((error) => { clearTimeout(timeoutId); reject(error); }); + }); +} diff --git a/apps/api/src/routes/btc-proxy.routes.ts b/apps/api/src/routes/btc-proxy.routes.ts new file mode 100644 index 0000000..4d2fcad --- /dev/null +++ b/apps/api/src/routes/btc-proxy.routes.ts @@ -0,0 +1,148 @@ +import { Request, Response, Router } from 'express'; + +const router = Router(); +const BLOCKSTREAM_BASE = 'https://blockstream.info/api'; +const BTC_TIMEOUT_MS = 10_000; + +// Validate Bitcoin address format (mainnet only) +const BTC_ADDRESS_RE = /^(bc1[a-zA-HJ-NP-Z0-9]{25,62}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})$/; + +router.get('/utxos/:address', getUtxos); +router.get('/fee-estimates', getFeeEstimates); +router.post('/broadcast', broadcastTx); + +export default router; + +/** + * GET /api/btc/utxos/:address + * Returns confirmed UTXOs for the given address. + */ +async function getUtxos(req: Request, res: Response) { + const address = String(req.params.address); + + if (!BTC_ADDRESS_RE.test(address)) { + res.status(400).json({ success: false, error: 'Invalid Bitcoin address' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), BTC_TIMEOUT_MS); + + try { + const response = await fetch(`${BLOCKSTREAM_BASE}/address/${address}/utxo`, { + headers: { Accept: 'application/json' }, + signal: controller.signal, + }); + + if (!response.ok) { + res.status(response.status).json({ success: false, error: 'Blockstream API error' }); + return; + } + + const utxos = await response.json(); + + // Filter to confirmed only + const confirmed = (utxos as Array<{ status: { confirmed: boolean }; txid: string; vout: number; value: number }>) + .filter((u) => u.status?.confirmed) + .map((u) => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + })); + + res.json({ success: true, data: confirmed }); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Blockstream request timeout' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach Blockstream' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * GET /api/btc/fee-estimates + * Returns fee rate estimates in sat/vB for different confirmation targets. + */ +async function getFeeEstimates(_req: Request, res: Response) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), BTC_TIMEOUT_MS); + + try { + const response = await fetch(`${BLOCKSTREAM_BASE}/fee-estimates`, { + headers: { Accept: 'application/json' }, + signal: controller.signal, + }); + + if (!response.ok) { + res.status(response.status).json({ success: false, error: 'Blockstream fee estimates error' }); + return; + } + + const data = await response.json(); + + // Return top 3 tiers: 1-block, 3-block, 6-block confirmation targets + const estimates = data as Record; + res.json({ + success: true, + data: { + fast: Math.ceil(estimates['1'] ?? estimates['2'] ?? 10), + normal: Math.ceil(estimates['3'] ?? estimates['6'] ?? 5), + slow: Math.ceil(estimates['6'] ?? estimates['12'] ?? 2), + }, + }); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Blockstream fee estimates timeout' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach Blockstream' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * POST /api/btc/broadcast + * Broadcasts a raw transaction hex. + */ +async function broadcastTx(req: Request, res: Response) { + const { hex } = req.body; + + if (!hex || typeof hex !== 'string' || !/^[0-9a-fA-F]+$/.test(hex)) { + res.status(400).json({ success: false, error: 'Invalid transaction hex' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), BTC_TIMEOUT_MS); + + try { + const response = await fetch(`${BLOCKSTREAM_BASE}/tx`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: hex, + signal: controller.signal, + }); + + const text = await response.text(); + + if (!response.ok) { + res.status(response.status).json({ success: false, error: text || 'Broadcast failed' }); + return; + } + + // Blockstream returns the txid as plain text + res.json({ success: true, data: { txid: text.trim() } }); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Broadcast timeout' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to broadcast transaction' }); + } finally { + clearTimeout(timeout); + } +} diff --git a/apps/api/src/routes/relay-proxy.routes.ts b/apps/api/src/routes/relay-proxy.routes.ts new file mode 100644 index 0000000..04183b8 --- /dev/null +++ b/apps/api/src/routes/relay-proxy.routes.ts @@ -0,0 +1,52 @@ +import { NextFunction, Request, Response, Router } from 'express'; +import { env } from '../config/env'; + +const router = Router(); +const RELAY_API_URL = 'https://api.relay.link'; +const ALLOWED_PATHS = new Set(['/quote/v2', '/intents/status/v3']); + +router.use(proxyRelayRequest); + +export default router; + +async function proxyRelayRequest(req: Request, res: Response, next: NextFunction) { + try { + const relayPath = req.path; + if (!relayPath.startsWith('/execute/') && !ALLOWED_PATHS.has(relayPath)) { + res.status(404).json({ success: false, error: 'Relay endpoint not allowed' }); + return; + } + + const relayUrl = new URL(`${RELAY_API_URL}${relayPath}`); + + Object.entries(req.query).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => relayUrl.searchParams.append(key, String(item))); + return; + } + + if (typeof value !== 'undefined') { + relayUrl.searchParams.set(key, String(value)); + } + }); + + const response = await fetch(relayUrl.toString(), { + method: req.method, + headers: { + Accept: 'application/json', + ...(req.method !== 'GET' ? { 'Content-Type': 'application/json' } : {}), + ...(env.relayApiKey ? { Authorization: `Bearer ${env.relayApiKey}` } : {}), + }, + body: req.method === 'GET' ? undefined : JSON.stringify(req.body ?? {}), + }); + + const contentType = response.headers.get('content-type') ?? 'application/json'; + const payload = await response.text(); + + res.status(response.status); + res.type(contentType); + res.send(payload); + } catch (error) { + next(error); + } +} diff --git a/apps/api/src/routes/sol-swap-proxy.routes.ts b/apps/api/src/routes/sol-swap-proxy.routes.ts new file mode 100644 index 0000000..1c3d5c3 --- /dev/null +++ b/apps/api/src/routes/sol-swap-proxy.routes.ts @@ -0,0 +1,166 @@ +import { Request, Response, Router } from 'express'; +import { env } from '../config/env'; + +const router = Router(); +const JUPITER_BASE = 'https://api.jup.ag/swap/v1'; +const JUPITER_TIMEOUT_MS = 15_000; + +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 +]); + +router.get('/quote', getQuote); +router.post('/build', buildSwap); + +export default router; + +/** + * GET /api/sol/swap/quote + * Proxies to Jupiter GET /v6/quote + */ +async function getQuote(req: Request, res: Response) { + const { inputMint, outputMint, amount, slippageBps } = req.query; + + if (!inputMint || !outputMint || !amount || !slippageBps) { + res.status(400).json({ success: false, error: 'Missing required params: inputMint, outputMint, amount, slippageBps' }); + return; + } + + if (!ALLOWED_MINTS.has(String(inputMint)) || !ALLOWED_MINTS.has(String(outputMint))) { + res.status(400).json({ success: false, error: 'Token mint not in whitelist' }); + return; + } + + if (inputMint === outputMint) { + res.status(400).json({ success: false, error: 'inputMint and outputMint must be different' }); + return; + } + + const parsedAmount = parseInt(String(amount), 10); + if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) { + res.status(400).json({ success: false, error: 'amount must be a positive integer' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), JUPITER_TIMEOUT_MS); + + try { + const url = new URL(`${JUPITER_BASE}/quote`); + url.searchParams.set('inputMint', String(inputMint)); + url.searchParams.set('outputMint', String(outputMint)); + url.searchParams.set('amount', String(parsedAmount)); + url.searchParams.set('slippageBps', String(slippageBps)); + + // Platform fee (0.7%) — Jupiter deducts this natively + if (env.jupiterFeeBps > 0) { + url.searchParams.set('platformFeeBps', String(env.jupiterFeeBps)); + } + + const headers: Record = { Accept: 'application/json' }; + if (env.jupiterApiKey) { + headers['x-api-key'] = env.jupiterApiKey; + } + + const response = await fetch(url.toString(), { headers, signal: controller.signal }); + + if (!response.ok) { + const text = await response.text().catch(() => 'Unknown error'); + res.status(response.status).json({ success: false, error: `Jupiter API error: ${text}` }); + return; + } + + const data = await response.json(); + res.json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Jupiter quote request timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach Jupiter API' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * POST /api/sol/swap/build + * Proxies to Jupiter POST /v6/swap — returns serialized transaction for client signing + */ +async function buildSwap(req: Request, res: Response) { + const { quoteResponse, userPublicKey } = req.body; + + if (!quoteResponse || typeof quoteResponse !== 'object') { + res.status(400).json({ success: false, error: 'Missing quoteResponse object' }); + return; + } + + if (!userPublicKey || typeof userPublicKey !== 'string') { + res.status(400).json({ success: false, error: 'Missing userPublicKey string' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), JUPITER_TIMEOUT_MS); + + try { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.jupiterApiKey) { + headers['x-api-key'] = env.jupiterApiKey; + } + + const swapBody: Record = { + quoteResponse, + userPublicKey, + wrapAndUnwrapSol: true, + dynamicComputeUnitLimit: true, + prioritizationFeeLamports: 'auto', + }; + + // Attach referral fee account for Jupiter to route platform fees + if (env.jupiterReferralAccount) { + swapBody.feeAccount = env.jupiterReferralAccount; + } + + const response = await fetch(`${JUPITER_BASE}/swap`, { + method: 'POST', + headers, + signal: controller.signal, + body: JSON.stringify(swapBody), + }); + + if (!response.ok) { + const text = await response.text().catch(() => 'Unknown error'); + res.status(response.status).json({ success: false, error: `Jupiter swap build error: ${text}` }); + return; + } + + const data = await response.json(); + res.json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Jupiter swap build timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach Jupiter API' }); + } finally { + clearTimeout(timeout); + } +} diff --git a/apps/api/src/routes/tron-proxy.routes.ts b/apps/api/src/routes/tron-proxy.routes.ts new file mode 100644 index 0000000..562409a --- /dev/null +++ b/apps/api/src/routes/tron-proxy.routes.ts @@ -0,0 +1,268 @@ +import { Request, Response, Router } from 'express'; +import { env } from '../config/env'; + +const router = Router(); +const TRONGRID_BASE = 'https://api.trongrid.io'; +const TRON_TIMEOUT_MS = 10_000; +const TRON_ADDRESS_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/; +const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +router.get('/account/:address', getAccount); +router.post('/createtransaction', createTransaction); +router.post('/triggersmartcontract', triggerSmartContract); +router.post('/broadcasttransaction', broadcastTransaction); + +export default router; + +/** + * Decode a TRON base58check address to its 20-byte hex (without 0x41 prefix). + */ +function tronAddressToHex(address: string): string { + let num = 0n; + for (const char of address) { + const index = BASE58_ALPHABET.indexOf(char); + if (index === -1) throw new Error('Invalid base58 character'); + num = num * 58n + BigInt(index); + } + const hex = num.toString(16).padStart(50, '0'); // 25 bytes = 1 prefix + 20 addr + 4 checksum + return hex.slice(2, 42); // skip 0x41 prefix, take 20 bytes +} + +/** + * Call balanceOf(address) on a TRC20 contract via triggerconstantcontract. + */ +async function fetchTrc20Balance( + ownerAddress: string, + contractAddress: string, + signal: AbortSignal +): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + + const addressHex = tronAddressToHex(ownerAddress); + const parameter = addressHex.padStart(64, '0'); + + const response = await fetch(`${TRONGRID_BASE}/wallet/triggerconstantcontract`, { + method: 'POST', + headers, + signal, + body: JSON.stringify({ + owner_address: ownerAddress, + contract_address: contractAddress, + function_selector: 'balanceOf(address)', + parameter, + visible: true, + }), + }); + + if (!response.ok) return '0'; + + const body = (await response.json()) as { + constant_result?: string[]; + }; + + const hex = body.constant_result?.[0]; + if (!hex || /^0+$/.test(hex)) return '0'; + + return BigInt('0x' + hex).toString(); +} + +async function getAccount(req: Request, res: Response) { + const address = String(req.params.address); + + if (!TRON_ADDRESS_RE.test(address)) { + res.status(400).json({ success: false, error: 'Invalid TRON address' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const headers: Record = { Accept: 'application/json' }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + + // Fetch account data and USDT balance in parallel + const [accountRes, usdtBalance] = await Promise.all([ + fetch(`${TRONGRID_BASE}/v1/accounts/${address}`, { + headers, + signal: controller.signal, + }), + fetchTrc20Balance(address, USDT_CONTRACT, controller.signal), + ]); + + if (!accountRes.ok) { + res.status(accountRes.status).json({ success: false, error: 'TronGrid error' }); + return; + } + + const accountData = (await accountRes.json()) as { + data?: Array<{ + balance?: number; + trc20?: Array>; + [key: string]: unknown; + }>; + }; + + // Ensure data array has at least one entry + if (!accountData.data || accountData.data.length === 0) { + accountData.data = [{ balance: 0, trc20: [] }]; + } + + const account = accountData.data[0]; + + // Inject USDT balance from contract call (always more reliable) + if (usdtBalance !== '0') { + if (!account.trc20) account.trc20 = []; + + const existingIdx = account.trc20.findIndex((t) => t[USDT_CONTRACT] !== undefined); + if (existingIdx >= 0) { + account.trc20[existingIdx] = { [USDT_CONTRACT]: usdtBalance }; + } else { + account.trc20.push({ [USDT_CONTRACT]: usdtBalance }); + } + } + + res.json(accountData); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'TronGrid request timeout' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach TronGrid' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * POST /api/tron/createtransaction + * Proxies to TronGrid /wallet/createtransaction — builds unsigned TRX transfer transaction + */ +async function createTransaction(req: Request, res: Response) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const { owner_address, to_address, amount } = req.body; + + if (!owner_address || !to_address || amount === undefined) { + res.status(400).json({ success: false, error: 'Missing required fields: owner_address, to_address, amount' }); + return; + } + + if (!TRON_ADDRESS_RE.test(owner_address) || !TRON_ADDRESS_RE.test(to_address)) { + res.status(400).json({ success: false, error: 'Invalid TRON address format' }); + return; + } + + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + + const response = await fetch(`${TRONGRID_BASE}/wallet/createtransaction`, { + method: 'POST', + headers, + signal: controller.signal, + body: JSON.stringify({ owner_address, to_address, amount, visible: true }), + }); + + const data = await response.json(); + res.status(response.ok ? 200 : 502).json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'TronGrid createtransaction timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach TronGrid' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * POST /api/tron/triggersmartcontract + * Proxies to TronGrid /wallet/triggersmartcontract — builds unsigned transaction + */ +async function triggerSmartContract(req: Request, res: Response) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + + const response = await fetch(`${TRONGRID_BASE}/wallet/triggersmartcontract`, { + method: 'POST', + headers, + signal: controller.signal, + body: JSON.stringify(req.body), + }); + + const data = await response.json(); + res.status(response.ok ? 200 : 502).json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'TronGrid triggersmartcontract timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach TronGrid' }); + } finally { + clearTimeout(timeout); + } +} + +/** + * POST /api/tron/broadcasttransaction + * Proxies to TronGrid /wallet/broadcasttransaction + */ +async function broadcastTransaction(req: Request, res: Response) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + + const response = await fetch(`${TRONGRID_BASE}/wallet/broadcasttransaction`, { + method: 'POST', + headers, + signal: controller.signal, + body: JSON.stringify(req.body), + }); + + const data = await response.json(); + res.status(response.ok ? 200 : 502).json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'TronGrid broadcast timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to broadcast transaction' }); + } finally { + clearTimeout(timeout); + } +} diff --git a/apps/api/src/routes/tron-swap-proxy.routes.ts b/apps/api/src/routes/tron-swap-proxy.routes.ts new file mode 100644 index 0000000..76091a3 --- /dev/null +++ b/apps/api/src/routes/tron-swap-proxy.routes.ts @@ -0,0 +1,471 @@ +import { Request, Response, Router } from 'express'; +import { env } from '../config/env'; + +const router = Router(); +const TRONGRID_BASE = 'https://api.trongrid.io'; +const TRON_TIMEOUT_MS = 15_000; +const TRON_ADDRESS_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/; + +// Contracts +const SUNSWAP_SMART_ROUTER = 'TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax'; +const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; +const WTRX_CONTRACT = 'TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR'; + +// FeeSwapRouter_TRX — deployed contract, 0.7% fee +const FEE_SWAP_ROUTER_TRX = 'TX8E6X7X1FWYRYuYR2LTvS7zm1KchcVs5E'; +const FEE_BPS = 70n; +const BPS_DENOMINATOR = 10_000n; + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +// Token map +const TOKEN_MAP: Record = { + TRX: WTRX_CONTRACT, + USDT: USDT_CONTRACT, +}; + +const TOKEN_DECIMALS: Record = { + TRX: 6, + USDT: 6, +}; + +router.get('/quote', getSwapQuote); +router.post('/build', buildSwapTx); +router.post('/broadcast', broadcastTx); + +export default router; + +// ─── Helpers ─── + +function tronAddressToHex(address: string): string { + let num = 0n; + for (const char of address) { + const index = BASE58_ALPHABET.indexOf(char); + if (index === -1) throw new Error('Invalid base58 character'); + num = num * 58n + BigInt(index); + } + const hex = num.toString(16).padStart(50, '0'); + return hex.slice(2, 42); // skip 0x41, take 20 bytes +} + +function encodeUint256(value: bigint): string { + return value.toString(16).padStart(64, '0'); +} + +function encodeAddress(tronAddress: string): string { + const hex = tronAddressToHex(tronAddress); + return hex.padStart(64, '0'); +} + +function tronHeaders(): Record { + const headers: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + if (env.tronApiKey) { + headers['TRON-PRO-API-KEY'] = env.tronApiKey; + } + return headers; +} + +// Encode bytes calldata as ABI dynamic bytes parameter +function encodeDynamicBytes(hexData: string): string { + // Remove 0x prefix if present + const data = hexData.startsWith('0x') ? hexData.slice(2) : hexData; + const byteLength = data.length / 2; + const lengthEncoded = encodeUint256(BigInt(byteLength)); + // Pad data to 32-byte boundary + const paddedData = data.padEnd(Math.ceil(data.length / 64) * 64, '0'); + return lengthEncoded + paddedData; +} + +// ─── GET /quote ─── + +async function getSwapQuote(req: Request, res: Response) { + const from = String(req.query.from || '').toUpperCase(); + const to = String(req.query.to || '').toUpperCase(); + const amount = String(req.query.amount || ''); + + if (!TOKEN_MAP[from] || !TOKEN_MAP[to]) { + res.status(400).json({ success: false, error: 'Invalid from/to. Use TRX or USDT' }); + return; + } + if (from === to) { + res.status(400).json({ success: false, error: 'from and to must be different' }); + return; + } + + const amountBigInt = BigInt(amount || '0'); + if (amountBigInt <= 0n) { + res.status(400).json({ success: false, error: 'amount must be positive' }); + return; + } + + // Deduct 0.7% fee — SunSwap will only receive 99.3% + const feeAmount = (amountBigInt * FEE_BPS) / BPS_DENOMINATOR; + const amountAfterFee = amountBigInt - feeAmount; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const fromToken = TOKEN_MAP[from]; + const toToken = TOKEN_MAP[to]; + + // ABI: getAmountsOut(uint256 amountIn, address[] path) + const amountHex = encodeUint256(amountAfterFee); + const offsetHex = encodeUint256(64n); + const lengthHex = encodeUint256(2n); + const addr0Hex = encodeAddress(fromToken); + const addr1Hex = encodeAddress(toToken); + const parameter = amountHex + offsetHex + lengthHex + addr0Hex + addr1Hex; + + const response = await fetch(`${TRONGRID_BASE}/wallet/triggerconstantcontract`, { + method: 'POST', + headers: tronHeaders(), + signal: controller.signal, + body: JSON.stringify({ + owner_address: SUNSWAP_SMART_ROUTER, + contract_address: SUNSWAP_SMART_ROUTER, + function_selector: 'getAmountsOut(uint256,address[])', + parameter, + visible: true, + }), + }); + + if (!response.ok) { + res.status(response.status).json({ success: false, error: 'TronGrid error' }); + return; + } + + const body = (await response.json()) as { + constant_result?: string[]; + result?: { result?: boolean; message?: string }; + }; + + if (!body.constant_result?.[0]) { + const errorMsg = body.result?.message + ? Buffer.from(body.result.message, 'hex').toString('utf8') + : 'No result from getAmountsOut'; + res.status(502).json({ success: false, error: errorMsg }); + return; + } + + const resultHex = body.constant_result[0]; + const amountOutHex = resultHex.slice(-64); + const amountOut = BigInt('0x' + amountOutHex).toString(); + + res.json({ + success: true, + amountIn: amountBigInt.toString(), + amountOut, + fee: feeAmount.toString(), + from, + to, + fromDecimals: TOKEN_DECIMALS[from], + toDecimals: TOKEN_DECIMALS[to], + }); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'TronGrid quote request timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to reach TronGrid' }); + } finally { + clearTimeout(timeout); + } +} + +// ─── POST /build ─── + +async function buildSwapTx(req: Request, res: Response) { + const { from, to, amount, amountOutMin, userAddress } = req.body; + + if (!from || !to || !amount || !amountOutMin || !userAddress) { + res.status(400).json({ success: false, error: 'Missing required fields: from, to, amount, amountOutMin, userAddress' }); + return; + } + + const fromUpper = String(from).toUpperCase(); + const toUpper = String(to).toUpperCase(); + + if (!TOKEN_MAP[fromUpper] || !TOKEN_MAP[toUpper] || fromUpper === toUpper) { + res.status(400).json({ success: false, error: 'Invalid from/to pair' }); + return; + } + + if (!TRON_ADDRESS_RE.test(userAddress)) { + res.status(400).json({ success: false, error: 'Invalid TRON address' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const transactions: Array<{ txID: string; raw_data: unknown; raw_data_hex: string; type: string }> = []; + const amountBigInt = BigInt(amount); + const minOutBigInt = BigInt(amountOutMin); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200); // 20 minutes + + // Calculate fee and swap amounts + const feeAmount = (amountBigInt * FEE_BPS) / BPS_DENOMINATOR; + const swapAmount = amountBigInt - feeAmount; + + if (fromUpper === 'TRX') { + // ═══ TRX → USDT: through FeeSwapRouter.swapNativeWithFee(bytes routerCalldata) ═══ + + // Step 1: Build the SunSwap calldata for swapExactETHForTokens + // SunSwap will receive swapAmount (99.3%) of TRX from FeeSwapRouter + // SunSwap sends output tokens to `to` address — must be userAddress + const sunswapCalldata = buildSwapExactETHForTokensCalldata( + minOutBigInt, + [WTRX_CONTRACT, USDT_CONTRACT], + userAddress, + deadline, + ); + + // Step 2: Wrap in swapNativeWithFee(bytes routerCalldata) + // ABI: swapNativeWithFee(bytes) — single dynamic bytes param + const offsetToBytes = encodeUint256(32n); // offset to dynamic bytes + const feeRouterParam = offsetToBytes + encodeDynamicBytes(sunswapCalldata); + + const swapTx = await buildTriggerSmartContract({ + ownerAddress: userAddress, + contractAddress: FEE_SWAP_ROUTER_TRX, + functionSelector: 'swapNativeWithFee(bytes)', + parameter: feeRouterParam, + callValue: Number(amountBigInt), // full amount — contract takes 0.7% + feeLimit: 200_000_000, // 200 TRX + signal: controller.signal, + }); + + if (swapTx) { + transactions.push({ ...swapTx, type: 'swap' }); + } + + } else { + // ═══ USDT → TRX: through FeeSwapRouter.swapTokenWithFee(address, uint256, bytes) ═══ + + // Step 1: Approve USDT to FeeSwapRouter (not SunSwap!) + const allowance = await checkAllowance(userAddress, USDT_CONTRACT, FEE_SWAP_ROUTER_TRX, controller.signal); + + if (allowance < amountBigInt) { + const approveTx = await buildTriggerSmartContract({ + ownerAddress: userAddress, + contractAddress: USDT_CONTRACT, + functionSelector: 'approve(address,uint256)', + parameter: encodeAddress(FEE_SWAP_ROUTER_TRX) + encodeUint256(BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')), + callValue: 0, + feeLimit: 100_000_000, + signal: controller.signal, + }); + + if (approveTx) { + transactions.push({ ...approveTx, type: 'approve' }); + } + } + + // Step 2: Build SunSwap calldata for swapExactTokensForETH + // FeeSwapRouter will approve swapAmount (99.3%) to SunSwap and forward this calldata + const sunswapCalldata = buildSwapExactTokensForETHCalldata( + swapAmount, // 99.3% — what SunSwap actually receives + minOutBigInt, + [USDT_CONTRACT, WTRX_CONTRACT], + userAddress, // output TRX goes to user + deadline, + ); + + // Step 3: Wrap in swapTokenWithFee(address tokenIn, uint256 amountIn, bytes routerCalldata) + const tokenInEncoded = encodeAddress(USDT_CONTRACT); + const amountInEncoded = encodeUint256(amountBigInt); // full amount — contract takes 0.7% + const offsetToBytes = encodeUint256(96n); // offset to dynamic bytes (3 * 32) + const feeRouterParam = tokenInEncoded + amountInEncoded + offsetToBytes + encodeDynamicBytes(sunswapCalldata); + + const swapTx = await buildTriggerSmartContract({ + ownerAddress: userAddress, + contractAddress: FEE_SWAP_ROUTER_TRX, + functionSelector: 'swapTokenWithFee(address,uint256,bytes)', + parameter: feeRouterParam, + callValue: 0, + feeLimit: 200_000_000, + signal: controller.signal, + }); + + if (swapTx) { + transactions.push({ ...swapTx, type: 'swap' }); + } + } + + if (!transactions.length) { + res.status(502).json({ success: false, error: 'Failed to build swap transactions' }); + return; + } + + res.json({ success: true, transactions }); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Build request timed out' }); + return; + } + const msg = error instanceof Error ? error.message : 'Failed to build swap'; + res.status(502).json({ success: false, error: msg }); + } finally { + clearTimeout(timeout); + } +} + +// ─── POST /broadcast ─── + +async function broadcastTx(req: Request, res: Response) { + const { signedTransaction } = req.body; + + if (!signedTransaction) { + res.status(400).json({ success: false, error: 'Missing signedTransaction' }); + return; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), TRON_TIMEOUT_MS); + + try { + const response = await fetch(`${TRONGRID_BASE}/wallet/broadcasttransaction`, { + method: 'POST', + headers: tronHeaders(), + signal: controller.signal, + body: JSON.stringify(signedTransaction), + }); + + const data = await response.json(); + res.status(response.ok ? 200 : 502).json(data); + } catch (error) { + if (controller.signal.aborted) { + res.status(504).json({ success: false, error: 'Broadcast timed out' }); + return; + } + res.status(502).json({ success: false, error: 'Failed to broadcast transaction' }); + } finally { + clearTimeout(timeout); + } +} + +// ─── SunSwap Calldata Builders ─── + +// Build raw calldata hex for swapExactETHForTokens(uint256,address[],address,uint256) +function buildSwapExactETHForTokensCalldata( + amountOutMin: bigint, + path: string[], // TRON base58 addresses + to: string, // TRON base58 address + deadline: bigint, +): string { + // Function selector: keccak256("swapExactETHForTokens(uint256,address[],address,uint256)") first 4 bytes + const selector = 'b6f9de95'; + + const amountOutMinEnc = encodeUint256(amountOutMin); + const offsetToPath = encodeUint256(128n); // 4 * 32 bytes offset + const toEnc = encodeAddress(to); + const deadlineEnc = encodeUint256(deadline); + const pathLenEnc = encodeUint256(BigInt(path.length)); + const pathElements = path.map((addr) => encodeAddress(addr)).join(''); + + return selector + amountOutMinEnc + offsetToPath + toEnc + deadlineEnc + pathLenEnc + pathElements; +} + +// Build raw calldata hex for swapExactTokensForETH(uint256,uint256,address[],address,uint256) +function buildSwapExactTokensForETHCalldata( + amountIn: bigint, + amountOutMin: bigint, + path: string[], // TRON base58 addresses + to: string, // TRON base58 address + deadline: bigint, +): string { + // Function selector: keccak256("swapExactTokensForETH(uint256,uint256,address[],address,uint256)") first 4 bytes + const selector = '18cbafe5'; + + const amountInEnc = encodeUint256(amountIn); + const amountOutMinEnc = encodeUint256(amountOutMin); + const offsetToPath = encodeUint256(160n); // 5 * 32 bytes offset + const toEnc = encodeAddress(to); + const deadlineEnc = encodeUint256(deadline); + const pathLenEnc = encodeUint256(BigInt(path.length)); + const pathElements = path.map((addr) => encodeAddress(addr)).join(''); + + return selector + amountInEnc + amountOutMinEnc + offsetToPath + toEnc + deadlineEnc + pathLenEnc + pathElements; +} + +// ─── Internal Helpers ─── + +async function checkAllowance( + owner: string, + tokenContract: string, + spender: string, + signal: AbortSignal +): Promise { + const parameter = encodeAddress(owner) + encodeAddress(spender); + + const response = await fetch(`${TRONGRID_BASE}/wallet/triggerconstantcontract`, { + method: 'POST', + headers: tronHeaders(), + signal, + body: JSON.stringify({ + owner_address: owner, + contract_address: tokenContract, + function_selector: 'allowance(address,address)', + parameter, + visible: true, + }), + }); + + if (!response.ok) return 0n; + + const body = (await response.json()) as { constant_result?: string[] }; + const hex = body.constant_result?.[0]; + if (!hex || /^0+$/.test(hex)) return 0n; + + return BigInt('0x' + hex); +} + +interface TriggerSmartContractParams { + ownerAddress: string; + contractAddress: string; + functionSelector: string; + parameter: string; + callValue: number; + feeLimit: number; + signal: AbortSignal; +} + +async function buildTriggerSmartContract( + params: TriggerSmartContractParams +): Promise<{ txID: string; raw_data: unknown; raw_data_hex: string } | null> { + const response = await fetch(`${TRONGRID_BASE}/wallet/triggersmartcontract`, { + method: 'POST', + headers: tronHeaders(), + signal: params.signal, + body: JSON.stringify({ + owner_address: params.ownerAddress, + contract_address: params.contractAddress, + function_selector: params.functionSelector, + parameter: params.parameter, + call_value: params.callValue, + fee_limit: params.feeLimit, + visible: true, + }), + }); + + if (!response.ok) return null; + + const body = (await response.json()) as { + result?: { result?: boolean; message?: string }; + transaction?: { txID: string; raw_data: unknown; raw_data_hex: string }; + }; + + if (!body.result?.result || !body.transaction) { + const errorMsg = body.result?.message + ? Buffer.from(body.result.message, 'hex').toString('utf8') + : 'Transaction build failed'; + throw new Error(errorMsg); + } + + return body.transaction; +} diff --git a/apps/api/src/routes/wallet.routes.ts b/apps/api/src/routes/wallet.routes.ts new file mode 100644 index 0000000..c407d48 --- /dev/null +++ b/apps/api/src/routes/wallet.routes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { WalletController } from '../controllers/wallet.controller'; +import { authMiddleware } from '../middleware/auth'; + +const router = Router(); + +router.get('/', authMiddleware, WalletController.getWallets); + +export default router; diff --git a/apps/api/src/services/jwt.service.ts b/apps/api/src/services/jwt.service.ts new file mode 100644 index 0000000..241ff04 --- /dev/null +++ b/apps/api/src/services/jwt.service.ts @@ -0,0 +1,92 @@ +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 | null = null; +let localKey: Awaited> | null = null; + +function getJWKS(): ReturnType { + 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>> { + 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 { + 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, + }, + }; +} diff --git a/apps/api/src/utils/ulid.ts b/apps/api/src/utils/ulid.ts new file mode 100644 index 0000000..6900ea1 --- /dev/null +++ b/apps/api/src/utils/ulid.ts @@ -0,0 +1,5 @@ +import { ulid } from 'ulidx'; + +export function generateUlid(): string { + return ulid(); +} diff --git a/apps/api/swagger.json b/apps/api/swagger.json new file mode 100644 index 0000000..601415e --- /dev/null +++ b/apps/api/swagger.json @@ -0,0 +1,101 @@ +{ + "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" } + } + } + } + } + } + } + } +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..50572da --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts new file mode 100644 index 0000000..a122cf5 --- /dev/null +++ b/apps/web/next.config.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + output: 'standalone', + turbopack: { + // Keep Turbopack pinned to the monorepo root so dev HMR + // does not mis-detect `apps/web` as a standalone project. + root: path.resolve(__dirname, '../..'), + }, + webpack(config) { + // Enable WebAssembly for tiny-secp256k1 (used by ecpair/bitcoinjs-lib) + config.experiments = { + ...config.experiments, + asyncWebAssembly: true, + }; + + // Prevent webpack from changing the output of WASM imports + config.module.rules.push({ + test: /\.wasm$/, + type: 'webassembly/async', + }); + + return config; + }, +}; + +export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..4da3ed5 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,38 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --webpack", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@solana/web3.js": "^1.98.4", + "@uniswap/router-sdk": "^2.7.1", + "@uniswap/sdk-core": "^7.12.1", + "@uniswap/universal-router-sdk": "^4.34.0", + "@uniswap/v3-sdk": "^3.29.1", + "@uniswap/v4-sdk": "^1.29.1", + "@yudiel/react-qr-scanner": "^2.5.1", + "bip32": "^5.0.1", + "bitcoinjs-lib": "^7.0.1", + "ecpair": "^3.0.1", + "ed25519-hd-key": "^1.3.0", + "ethers": "5.7.2", + "next": "16.1.6", + "qrcode.react": "^4.2.0", + "react": "19.2.3", + "react-dom": "19.2.3", + "tiny-secp256k1": "^2.2.4", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml new file mode 100644 index 0000000..8524033 --- /dev/null +++ b/apps/web/pnpm-lock.yaml @@ -0,0 +1,1770 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@scure/bip32': + specifier: ^2.0.1 + version: 2.0.1 + '@scure/bip39': + specifier: ^2.0.1 + version: 2.0.1 + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@yudiel/react-qr-scanner': + specifier: ^2.5.1 + version: 2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + bip32: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.9.3) + bitcoinjs-lib: + specifier: ^7.0.1 + version: 7.0.1(typescript@5.9.3) + ecpair: + specifier: ^3.0.1 + version: 3.0.1(typescript@5.9.3) + ed25519-hd-key: + specifier: ^1.3.0 + version: 1.3.0 + ethers: + specifier: ^6.16.0 + version: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + next: + specifier: 16.1.6 + version: 16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + tiny-secp256k1: + specifier: ^2.2.4 + version: 2.2.4 + zustand: + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.14)(react@19.2.3) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.37 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: ^5 + version: 5.9.3 + +packages: + + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + + '@solana/buffer-layout@4.0.1': + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@yudiel/react-qr-scanner@2.5.1': + resolution: {integrity: sha512-FWzHaCneu30mQpE80VNWx4IPtBjXFEiTzhwWunZy3afvvAy/x0aVIgYijJKEbROoaAeDfcJ/gIyUCqPBP7bMOw==} + peerDependencies: + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + barcode-detector@3.0.8: + resolution: {integrity: sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==} + + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + bech32@2.0.0: + resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + + bip174@3.0.0: + resolution: {integrity: sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==} + engines: {node: '>=18.0.0'} + + bip32@5.0.1: + resolution: {integrity: sha512-PWlHIAgYCfVhwqNpZyeakHXuLAGyN6rEQZnhxHxKI3BoFJRVWLl26455fhRlHsmbYcV986HqtPnt33Edu5sTCw==} + engines: {node: '>=18.0.0'} + + bitcoinjs-lib@7.0.1: + resolution: {integrity: sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==} + engines: {node: '>=18.0.0'} + + bn.js@5.2.3: + resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecpair@3.0.1: + resolution: {integrity: sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==} + engines: {node: '>=20.0.0'} + + ed25519-hd-key@1.3.0: + resolution: {integrity: sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + + ethers@6.16.0: + resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} + engines: {node: '>=14.0.0'} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + + fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + jayson@4.3.0: + resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==} + engines: {node: '>=8'} + hasBin: true + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + + rpc-websockets@9.3.5: + resolution: {integrity: sha512-4mAmr+AEhPYJ9TmDtxF3r3ZcbWy7W8kvZ4PoZYw/Xgp2J7WixjwTgiQZsoTDvch5nimmg3Ay6/0Kuh9oIvVs9A==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + sdp@3.2.1: + resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + + tiny-secp256k1@2.2.4: + resolution: {integrity: sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==} + engines: {node: '>=14.0.0'} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-tools@0.0.7: + resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==} + engines: {node: '>=14.0.0'} + + uint8array-tools@0.0.8: + resolution: {integrity: sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==} + engines: {node: '>=14.0.0'} + + uint8array-tools@0.0.9: + resolution: {integrity: sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==} + engines: {node: '>=14.0.0'} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + utf-8-validate@6.0.6: + resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + varuint-bitcoin@2.0.0: + resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webrtc-adapter@9.0.3: + resolution: {integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + wif@5.0.0: + resolution: {integrity: sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zxing-wasm@2.2.4: + resolution: {integrity: sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==} + peerDependencies: + '@types/emscripten': '>=1.39.6' + +snapshots: + + '@adraffy/ens-normalize@1.10.1': {} + + '@babel/runtime@7.28.6': {} + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@1.3.2': {} + + '@noble/hashes@1.8.0': {} + + '@noble/hashes@2.0.1': {} + + '@scure/base@1.2.6': {} + + '@scure/base@2.0.0': {} + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@solana/buffer-layout@4.0.1': + dependencies: + buffer: 6.0.3 + + '@solana/codecs-core@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.3 + + '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@babel/runtime': 7.28.6 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.6.0 + bn.js: 5.2.3 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + node-fetch: 2.7.0 + rpc-websockets: 9.3.5 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.37 + + '@types/emscripten@1.41.5': {} + + '@types/node@12.20.55': {} + + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/uuid@10.0.0': {} + + '@types/ws@7.4.7': + dependencies: + '@types/node': 20.19.37 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.37 + + '@yudiel/react-qr-scanner@2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + barcode-detector: 3.0.8(@types/emscripten@1.41.5) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + webrtc-adapter: 9.0.3 + transitivePeerDependencies: + - '@types/emscripten' + + aes-js@4.0.0-beta.5: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + barcode-detector@3.0.8(@types/emscripten@1.41.5): + dependencies: + zxing-wasm: 2.2.4(@types/emscripten@1.41.5) + transitivePeerDependencies: + - '@types/emscripten' + + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.0: {} + + bech32@2.0.0: {} + + bip174@3.0.0: + dependencies: + uint8array-tools: 0.0.9 + varuint-bitcoin: 2.0.0 + + bip32@5.0.1(typescript@5.9.3): + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + uint8array-tools: 0.0.8 + valibot: 1.2.0(typescript@5.9.3) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + + bitcoinjs-lib@7.0.1(typescript@5.9.3): + dependencies: + '@noble/hashes': 1.8.0 + bech32: 2.0.0 + bip174: 3.0.0 + bs58check: 4.0.0 + uint8array-tools: 0.0.9 + valibot: 1.2.0(typescript@5.9.3) + varuint-bitcoin: 2.0.0 + transitivePeerDependencies: + - typescript + + bn.js@5.2.3: {} + + borsh@0.7.0: + dependencies: + bn.js: 5.2.3 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001777: {} + + chalk@5.6.2: {} + + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + client-only@0.0.1: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + core-util-is@1.0.3: {} + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + + csstype@3.2.3: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + delay@5.0.0: {} + + detect-libc@2.1.2: + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecpair@3.0.1(typescript@5.9.3): + dependencies: + uint8array-tools: 0.0.8 + valibot: 1.2.0(typescript@5.9.3) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + + ed25519-hd-key@1.3.0: + dependencies: + create-hmac: 1.1.7 + tweetnacl: 1.0.3 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + + ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + eventemitter3@5.0.4: {} + + eyes@0.1.8: {} + + fast-stable-stringify@1.0.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + is-callable@1.2.7: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + + jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + json-stringify-safe@5.0.1: {} + + math-intrinsics@1.1.0: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + next@16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: + optional: true + + picocolors@1.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + process-nextick-args@2.0.1: {} + + qrcode.react@4.2.0(react@19.2.3): + dependencies: + react: 19.2.3 + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react@19.2.3: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + + rpc-websockets@9.3.5: + dependencies: + '@swc/helpers': 0.5.15 + '@types/uuid': 10.0.0 + '@types/ws': 8.18.1 + buffer: 6.0.3 + eventemitter3: 5.0.4 + uuid: 11.1.0 + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + scheduler@0.27.0: {} + + sdp@3.2.1: {} + + semver@7.7.4: + optional: true + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + source-map-js@1.2.1: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + styled-jsx@5.1.6(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + + superstruct@2.0.2: {} + + tagged-tag@1.0.0: {} + + text-encoding-utf-8@1.0.2: {} + + tiny-secp256k1@2.2.4: + dependencies: + uint8array-tools: 0.0.7 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + tr46@0.0.3: {} + + tslib@2.7.0: {} + + tslib@2.8.1: {} + + tweetnacl@1.0.3: {} + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.3: {} + + uint8array-tools@0.0.7: {} + + uint8array-tools@0.0.8: {} + + uint8array-tools@0.0.9: {} + + undici-types@6.19.8: {} + + undici-types@6.21.0: {} + + utf-8-validate@6.0.6: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + varuint-bitcoin@2.0.0: + dependencies: + uint8array-tools: 0.0.8 + + webidl-conversions@3.0.1: {} + + webrtc-adapter@9.0.3: + dependencies: + sdp: 3.2.1 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + wif@5.0.0: + dependencies: + bs58check: 4.0.0 + + ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + zustand@5.0.11(@types/react@19.2.14)(react@19.2.3): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.3 + + zxing-wasm@2.2.4(@types/emscripten@1.41.5): + dependencies: + '@types/emscripten': 1.41.5 + type-fest: 5.4.4 diff --git a/apps/web/pnpm-workspace.yaml b/apps/web/pnpm-workspace.yaml new file mode 100644 index 0000000..581a9d5 --- /dev/null +++ b/apps/web/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +ignoredBuiltDependencies: + - sharp + - unrs-resolver diff --git a/apps/web/public/file.svg b/apps/web/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/apps/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/globe.svg b/apps/web/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/apps/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/next.svg b/apps/web/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/apps/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/vercel.svg b/apps/web/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/apps/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/window.svg b/apps/web/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/apps/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/app/bridge/page.tsx b/apps/web/src/app/bridge/page.tsx new file mode 100644 index 0000000..223d41a --- /dev/null +++ b/apps/web/src/app/bridge/page.tsx @@ -0,0 +1,327 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { + BRIDGE_CHAINS, + BRIDGE_CHAIN_OPTIONS, + getDestinationChainOptions, + getTokenOptions, + getDefaultToken, + type BridgeChainKey, +} from '@/lib/bridge/constants'; +import { useBridge } from '@/hooks/useBridge'; +import { useGasPrice } from '@/hooks/useGasPrice'; +import { useGasSettings, type GasMode } from '@/hooks/useGasSettings'; +import { useAuthStore } from '@/store/auth-store'; + +const GAS_MODE_LABELS: Record = { + slow: 'Slow', + normal: 'Normal', + fast: 'Fast', + custom: 'Custom', +}; + +const GAS_MODES: GasMode[] = ['slow', 'normal', 'fast', 'custom']; + +export default function BridgePage() { + const router = useRouter(); + const user = useAuthStore((state) => state.user); + const { data: gasPriceData, loading: gasLoading } = useGasPrice(); + const gas = useGasSettings(gasPriceData); + const { + status, quote, bridgeStatus, requestId, txHashes, error, + sourceChain, setSourceChain, sourceWallet, + fetchQuote, submitBridge, resetBridge, + } = useBridge(); + + const [sourceToken, setSourceToken] = useState(() => getDefaultToken('ETH')); + const [destChain, setDestChain] = useState('SOL'); + const [destToken, setDestToken] = useState(() => getDefaultToken('SOL')); + const [amount, setAmount] = useState(''); + const [confirmed, setConfirmed] = useState(false); + + + const destChainOptions = useMemo(() => getDestinationChainOptions(sourceChain), [sourceChain]); + const sourceTokenOptions = useMemo(() => getTokenOptions(sourceChain), [sourceChain]); + const destTokenOptions = useMemo(() => getTokenOptions(destChain), [destChain]); + + const handleSourceChainChange = (newChain: BridgeChainKey) => { + setSourceChain(newChain); + setSourceToken(getDefaultToken(newChain)); + // If dest chain is same as new source, switch dest + const newDestOptions = getDestinationChainOptions(newChain); + if (!newDestOptions.includes(destChain)) { + setDestChain(newDestOptions[0]); + setDestToken(getDefaultToken(newDestOptions[0])); + } + handleReset(); + }; + + const handleDestChainChange = (newChain: BridgeChainKey) => { + setDestChain(newChain); + setDestToken(getDefaultToken(newChain)); + handleReset(); + }; + + if (!user) return null; + + const canQuote = + Number(amount) > 0 && + status !== 'quoting' && + status !== 'executing' && + status !== 'monitoring'; + const canBridge = !!quote && confirmed && status !== 'executing' && status !== 'monitoring'; + const isEvmSource = sourceChain === 'ETH' || sourceChain === 'BSC'; + const showGasControls = sourceChain === 'ETH'; // BSC uses fixed gas price + + const handleQuote = async () => { + setConfirmed(false); + await fetchQuote({ sourceChain, sourceToken, destChain, destToken, amount }); + }; + + const handleBridge = async () => { + await submitBridge( + { sourceChain, sourceToken, destChain, destToken, amount }, + isEvmSource ? gas.effectiveMaxFee : null, + isEvmSource ? gas.effectivePriorityFee : null, + ); + }; + + const handleReset = () => { + setConfirmed(false); + resetBridge(); + }; + + const tierGwei = (mode: GasMode): string => { + if (mode === 'custom') return ''; + if (!gasPriceData) return '...'; + const v = gasPriceData[mode].maxFeePerGas; + if (v >= 1) return v.toFixed(2); + const s = v.toFixed(4); + return s.replace(/0+$/, '').replace(/\.$/, ''); + }; + + const sourceExplorerBase = BRIDGE_CHAINS[sourceChain].explorerTxBaseUrl; + const destExplorerBase = BRIDGE_CHAINS[destChain].explorerTxBaseUrl; + + return ( +
+
+

Bridge

+ + Back to Dashboard + +
+ +
+ {/* Source Chain */} +
+ + +
+ + {/* Source Token */} +
+ + +
+ + {/* Destination Chain */} +
+ + +
+ + {/* Destination Token */} +
+ + +
+ + {/* Amount */} +
+ + { setAmount(e.target.value); handleReset(); }} + type="number" + min="0" + step="any" + style={inputStyle} + /> +
+ + {/* Gas Speed — only for ETH source */} + {showGasControls ? ( +
+ +
+ {GAS_MODES.map((mode) => ( + + ))} +
+ {gas.gasMode === 'custom' && ( + gas.setCustomGwei(e.target.value)} + type="number" + min="0" + step="0.01" + placeholder="Enter gwei" + style={{ ...inputStyle, marginTop: 6 }} + /> + )} +

+ Effective: {gas.displayGwei} +

+
+ ) : sourceChain === 'BSC' ? ( +
+ +

Fixed: 0.055 gwei (BSC)

+
+ ) : ( +
+ +

Auto (managed by Relay)

+
+ )} + + +
+ + {/* Quote Review */} + {quote && ( +
+

Review

+

Expected output: {quote.outputAmountFormatted} {quote.outputSymbol}

+

Minimum output: {quote.minimumAmountFormatted}

+

Estimated fee: {quote.feeSummary}

+

Estimated time: {quote.timeEstimateSeconds ? `${quote.timeEstimateSeconds}s` : 'Unavailable'}

+ {showGasControls && ( +

Gas: {gas.displayGwei} ({GAS_MODE_LABELS[gas.gasMode]})

+ )} + {sourceChain === 'BSC' && ( +

Gas: 0.055 gwei (BSC fixed)

+ )} + + + + +
+ )} + + {/* Status */} + {(requestId || txHashes.length > 0 || bridgeStatus) && ( +
+

Status

+ {requestId &&

Request ID: {requestId}

} + {bridgeStatus &&

Relay status: {bridgeStatus.status}

} + {txHashes.map((hash) => ( +

+ Origin tx:{' '} + + {hash} + +

+ ))} + {(bridgeStatus?.txHashes ?? []).map((hash) => ( +

+ Destination tx:{' '} + + {hash} + +

+ ))} +
+ )} + + {(error || status === 'error') && ( +

+ {error ?? 'Bridge failed'} +

+ )} +
+ ); +} + +const fieldGroupStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 6, + marginBottom: 12, +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: 8, +}; + +const navButtonStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: 4, +}; diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx new file mode 100644 index 0000000..c9ddcf6 --- /dev/null +++ b/apps/web/src/app/dashboard/page.tsx @@ -0,0 +1,152 @@ +'use client'; + +import Link from 'next/link'; +import { useBalances } from '@/hooks/useBalances'; +import type { ChainBalance } from '@/lib/balances/types'; +import { useAuthStore } from '@/store/auth-store'; + +export default function DashboardPage() { + const { user, wallets } = useAuthStore(); + const { portfolio, loading, refreshing, error, refresh } = useBalances(); + + return ( +
+
+

Dashboard

+
+ {user?.email || 'Not authenticated'} + + Send + + + Receive + + + Swap + + + Bridge + + + Settings + + +
+
+ +
+

+ Total Portfolio USD +

+

{formatUsd(portfolio?.totalUsd ?? null)}

+

+ {portfolio?.updatedAt + ? `Updated ${new Date(portfolio.updatedAt).toLocaleTimeString()}` + : loading + ? 'Loading balances...' + : 'Balances will appear after the first refresh.'} +

+
+ + {error && ( +

+ {error} +

+ )} + + {portfolio?.priceError && ( +

+ USD pricing is partially unavailable: {portfolio.priceError} +

+ )} + +

Your Wallets

+ {wallets.map((w) => { + const chainBalance = getChainBalance(w.chain, portfolio?.chains); + + return ( +
+
+

{w.chain}

+ {formatUsd(chainBalance?.totalUsd ?? null)} +
+

+ Address: {w.address} +

+ + {chainBalance?.error && chainBalance.error !== '__transient__' && ( +

+ {chainBalance.error} +

+ )} + +
+ {chainBalance?.tokens.length ? ( + chainBalance.tokens.map((token) => ( +
+ {token.symbol} + {formatTokenAmount(token.balanceFormatted)} + {formatUsd(token.valueUsd)} +
+ )) + ) : ( +

+ {loading ? 'Loading balances...' : 'No balances loaded yet.'} +

+ )} +
+
+ ); + })} +
+ ); +} + +function getChainBalance(chain: string, chains?: ChainBalance[]): ChainBalance | undefined { + return chains?.find((item) => item.chain === chain); +} + +function formatUsd(value: number | null): string { + if (typeof value !== 'number') { + return 'Unavailable'; + } + + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + maximumFractionDigits: 2, + }).format(value); +} + +function formatTokenAmount(value: string): string { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) { + return value; + } + + return new Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: 6, + }).format(numericValue); +} + +const navButtonStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: 4, +}; diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css new file mode 100644 index 0000000..e3734be --- /dev/null +++ b/apps/web/src/app/globals.css @@ -0,0 +1,42 @@ +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx new file mode 100644 index 0000000..4e4e439 --- /dev/null +++ b/apps/web/src/app/layout.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Crypto Wallet", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/page.module.css b/apps/web/src/app/page.module.css new file mode 100644 index 0000000..59dea42 --- /dev/null +++ b/apps/web/src/app/page.module.css @@ -0,0 +1,141 @@ +.page { + --background: #fafafa; + --foreground: #fff; + + --text-primary: #000; + --text-secondary: #666; + + --button-primary-hover: #383838; + --button-secondary-hover: #f2f2f2; + --button-secondary-border: #ebebeb; + + display: flex; + min-height: 100vh; + align-items: center; + justify-content: center; + font-family: var(--font-geist-sans); + background-color: var(--background); +} + +.main { + display: flex; + min-height: 100vh; + width: 100%; + max-width: 800px; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + background-color: var(--foreground); + padding: 120px 60px; +} + +.intro { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + gap: 24px; +} + +.intro h1 { + max-width: 320px; + font-size: 40px; + font-weight: 600; + line-height: 48px; + letter-spacing: -2.4px; + text-wrap: balance; + color: var(--text-primary); +} + +.intro p { + max-width: 440px; + font-size: 18px; + line-height: 32px; + text-wrap: balance; + color: var(--text-secondary); +} + +.intro a { + font-weight: 500; + color: var(--text-primary); +} + +.ctas { + display: flex; + flex-direction: row; + width: 100%; + max-width: 440px; + gap: 16px; + font-size: 14px; +} + +.ctas a { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + padding: 0 16px; + border-radius: 128px; + border: 1px solid transparent; + transition: 0.2s; + cursor: pointer; + width: fit-content; + font-weight: 500; +} + +a.primary { + background: var(--text-primary); + color: var(--background); + gap: 8px; +} + +a.secondary { + border-color: var(--button-secondary-border); +} + +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + a.primary:hover { + background: var(--button-primary-hover); + border-color: transparent; + } + + a.secondary:hover { + background: var(--button-secondary-hover); + border-color: transparent; + } +} + +@media (max-width: 600px) { + .main { + padding: 48px 24px; + } + + .intro { + gap: 16px; + } + + .intro h1 { + font-size: 32px; + line-height: 40px; + letter-spacing: -1.92px; + } +} + +@media (prefers-color-scheme: dark) { + .logo { + filter: invert(); + } + + .page { + --background: #000; + --foreground: #000; + + --text-primary: #ededed; + --text-secondary: #999; + + --button-primary-hover: #ccc; + --button-secondary-hover: #1a1a1a; + --button-secondary-border: #1a1a1a; + } +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx new file mode 100644 index 0000000..f889cb6 --- /dev/null +++ b/apps/web/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function Home() { + redirect('/dashboard'); +} diff --git a/apps/web/src/app/receive/page.tsx b/apps/web/src/app/receive/page.tsx new file mode 100644 index 0000000..ac1a5b1 --- /dev/null +++ b/apps/web/src/app/receive/page.tsx @@ -0,0 +1,225 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { QRCodeSVG } from 'qrcode.react'; +import { useAuthStore } from '@/store/auth-store'; +import { + SEND_CHAIN_OPTIONS, + SEND_CHAINS, + getTokenOptions, + getDefaultToken, + type SendChain, +} from '@/lib/send/constants'; +import { generateReceiveUri } from '@/lib/qr/generate'; + +export default function ReceivePage() { + const router = useRouter(); + const user = useAuthStore((state) => state.user); + const wallets = useAuthStore((state) => state.wallets); + + const [chain, setChain] = useState('ETH'); + const [token, setToken] = useState(getDefaultToken('ETH')); + const [amount, setAmount] = useState(''); + const [copied, setCopied] = useState(false); + + + // Reset token when chain changes + useEffect(() => { + setToken(getDefaultToken(chain)); + setAmount(''); + }, [chain]); + + const wallet = useMemo( + () => wallets.find((w) => w.chain === SEND_CHAINS[chain].walletChain), + [wallets, chain], + ); + + const address = wallet?.address ?? ''; + + // Ensure token is valid for the current chain (guards against stale state during chain switch) + const effectiveToken = useMemo(() => { + const options = getTokenOptions(chain); + return options.includes(token) ? token : getDefaultToken(chain); + }, [chain, token]); + + const qrUri = useMemo(() => { + if (!address) return ''; + return generateReceiveUri({ + chain, + token: effectiveToken, + address, + amount: amount.trim() || undefined, + }); + }, [chain, effectiveToken, address, amount]); + + const handleCopy = async () => { + if (!address) return; + try { + await navigator.clipboard.writeText(address); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback + const textArea = document.createElement('textarea'); + textArea.value = address; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + if (!user) return null; + + const tokenOptions = getTokenOptions(chain); + + return ( +
+
+

Receive

+
+ Send + Dashboard +
+
+ + {/* Chain selector */} +
+ + +
+ + {/* Token selector */} +
+ + +
+ + {/* Amount (optional) */} +
+ + { + const v = e.target.value; + if (/^\d*\.?\d*$/.test(v)) setAmount(v); + }} + style={inputStyle} + /> +
+ + {/* QR Code */} + {address && ( +
+
+ +
+
+ )} + + {/* Address display */} +
+ +
+ {address || 'No wallet found for this chain'} +
+
+ + {/* Copy button */} + + + {/* URI preview */} + {qrUri && ( +
+ QR URI: {qrUri} +
+ )} +
+ ); +} + +const labelStyle: React.CSSProperties = { + display: 'block', + marginBottom: 4, + fontSize: 13, + fontWeight: 600, +}; + +const selectStyle: React.CSSProperties = { + width: '100%', + padding: '8px 12px', + border: '1px solid #ccc', + borderRadius: 4, + fontSize: 14, +}; + +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, + textDecoration: 'none', + color: 'inherit', +}; diff --git a/apps/web/src/app/send/page.tsx b/apps/web/src/app/send/page.tsx new file mode 100644 index 0000000..c6950fb --- /dev/null +++ b/apps/web/src/app/send/page.tsx @@ -0,0 +1,561 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { Scanner } from '@yudiel/react-qr-scanner'; +import { useAuthStore } from '@/store/auth-store'; +import { useBalances } from '@/hooks/useBalances'; +import { useGasPrice } from '@/hooks/useGasPrice'; +import { useGasSettings, type GasMode } from '@/hooks/useGasSettings'; +import { + SEND_CHAIN_OPTIONS, + SEND_CHAINS, + getTokenOptions, + getDefaultToken, + type SendChain, +} from '@/lib/send/constants'; +import { validateAddress } from '@/lib/send/validate'; +import { parseQrUri } from '@/lib/qr/parse'; +import { executeSend, type SendResult } from '@/lib/send/execute'; +import type { ChainBalance } from '@/lib/balances/types'; + +const GAS_MODE_LABELS: Record = { + slow: 'Slow', + normal: 'Normal', + fast: 'Fast', + custom: 'Custom', +}; +const GAS_MODES: GasMode[] = ['slow', 'normal', 'fast', 'custom']; + +type SendStatus = 'idle' | 'review' | 'sending' | 'success' | 'error'; + +export default function SendPage() { + const router = useRouter(); + const user = useAuthStore((state) => state.user); + const wallets = useAuthStore((state) => state.wallets); + const { portfolio } = useBalances(); + const { data: gasPriceData } = useGasPrice(); + const gas = useGasSettings(gasPriceData); + + const [chain, setChain] = useState('ETH'); + const [token, setToken] = useState(getDefaultToken('ETH')); + const [recipient, setRecipient] = useState(''); + const [amount, setAmount] = useState(''); + const [confirmed, setConfirmed] = useState(false); + const [status, setStatus] = useState('idle'); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + const [scannerOpen, setScannerOpen] = useState(false); + + + // Reset token on chain change + useEffect(() => { + setToken(getDefaultToken(chain)); + setRecipient(''); + setAmount(''); + setConfirmed(false); + setStatus('idle'); + setError(null); + setResult(null); + }, [chain]); + + const wallet = useMemo( + () => wallets.find((w) => w.chain === SEND_CHAINS[chain].walletChain), + [wallets, chain], + ); + + const fromAddress = wallet?.address ?? ''; + + // Get available balance for the selected token + const availableBalance = useMemo(() => { + if (!portfolio?.chains) return null; + const chainBalance: ChainBalance | undefined = portfolio.chains.find( + (c) => c.chain === SEND_CHAINS[chain].walletChain, + ); + if (!chainBalance) return null; + const tokenBalance = chainBalance.tokens.find((t) => t.symbol === token); + return tokenBalance?.balanceFormatted ?? null; + }, [portfolio, chain, token]); + + // Validate address + const addressValidation = useMemo(() => { + if (!recipient.trim()) return null; + return validateAddress(chain, recipient); + }, [chain, recipient]); + + // Handle QR scan + const handleScan = useCallback((results: Array<{ rawValue: string }>) => { + if (!results.length) return; + const raw = results[0].rawValue; + if (!raw) return; + + const parsed = parseQrUri(raw); + setScannerOpen(false); + + if (parsed.chain) { + setChain(parsed.chain); + // Wait for chain useEffect, then set token/recipient/amount + setTimeout(() => { + if (parsed.token) setToken(parsed.token); + if (parsed.address) setRecipient(parsed.address); + if (parsed.amount) setAmount(parsed.amount); + }, 50); + } else if (parsed.address) { + setRecipient(parsed.address); + if (parsed.amount) setAmount(parsed.amount); + } + }, []); + + const handleReview = () => { + setError(null); + + if (!recipient.trim()) { + setError('Recipient address is required'); + return; + } + + const validation = validateAddress(chain, recipient); + if (!validation.valid) { + setError(validation.error || 'Invalid address'); + return; + } + + if (!amount || Number(amount) <= 0) { + setError('Enter a valid amount'); + return; + } + + setStatus('review'); + }; + + const handleSend = async () => { + if (!wallet) { + setError('Wallet not found for this chain'); + return; + } + + setStatus('sending'); + setError(null); + + try { + const sendResult = await executeSend({ + chain, + token, + toAddress: recipient.trim(), + amount, + privateKey: wallet.privateKey, + fromAddress, + maxFeeGwei: chain === 'ETH' ? gas.effectiveMaxFee : null, + priorityFeeGwei: chain === 'ETH' ? gas.effectivePriorityFee : null, + }); + + setResult(sendResult); + setStatus('success'); + } catch (err: any) { + setError(err.message || 'Transaction failed'); + setStatus('error'); + } + }; + + const handleReset = () => { + setRecipient(''); + setAmount(''); + setConfirmed(false); + setStatus('idle'); + setError(null); + setResult(null); + }; + + const handleMax = () => { + if (availableBalance) { + setAmount(availableBalance); + } + }; + + if (!user) return null; + + const tokenOptions = getTokenOptions(chain); + + return ( +
+
+

Send

+
+ Receive + Dashboard +
+
+ + {/* QR Scanner Modal */} + {scannerOpen && ( +
+
+
+

Scan QR Code

+ +
+
+ { + console.error('QR scanner error:', err); + setScannerOpen(false); + }} + formats={['qr_code']} + styles={{ container: { width: '100%' } }} + /> +
+

+ Point your camera at a QR code to auto-fill send details +

+
+
+ )} + + {/* Success state */} + {status === 'success' && result && ( +
+

Transaction Sent!

+

+ TX Hash: {result.hash} +

+ + View on Explorer + +
+ +
+
+ )} + + {/* Form (hidden during success) */} + {status !== 'success' && ( + <> + {/* Chain selector */} +
+ + +
+ + {/* Token selector */} +
+ + + {availableBalance !== null && ( +

+ Available: {availableBalance} {token} +

+ )} +
+ + {/* Recipient address */} +
+ +
+ setRecipient(e.target.value)} + style={{ ...inputStyle, flex: 1 }} + disabled={status === 'sending'} + /> + +
+ {addressValidation && !addressValidation.valid && ( +

{addressValidation.error}

+ )} + {addressValidation?.valid && ( +

Valid address

+ )} +
+ + {/* Amount */} +
+ +
+ { + const v = e.target.value; + if (/^\d*\.?\d*$/.test(v)) setAmount(v); + }} + style={{ ...inputStyle, flex: 1 }} + disabled={status === 'sending'} + /> + {availableBalance !== null && ( + + )} +
+
+ + {/* Gas settings (ETH only) */} + {chain === 'ETH' && ( +
+ +
+ {GAS_MODES.map((mode) => ( + + ))} +
+ {gas.gasMode === 'custom' && ( + gas.setCustomGwei(e.target.value)} + style={inputStyle} + /> + )} +

+ Estimated gas: {gas.displayGwei} +

+
+ )} + + {/* Fee info for non-ETH chains */} + {chain !== 'ETH' && ( +
+

+ {chain === 'SOL' && 'Fee: Auto (~0.000005 SOL)'} + {chain === 'TRX' && 'Fee: Auto (Energy/Bandwidth)'} + {chain === 'BTC' && 'Fee: Auto (market rate sat/vB)'} +

+
+ )} + +

+ Platform fee: 0.7% per transaction +

+ + {/* Review section */} + {status === 'review' && ( +
+

Review Transaction

+
+ From: + {fromAddress} +
+
+ To: + {recipient} +
+
+ Amount: + {amount} {token} +
+
+ Network: + {SEND_CHAINS[chain].label} +
+ +
+ +
+ +
+ + +
+
+ )} + + {/* Sending state */} + {status === 'sending' && ( +
+

Sending transaction...

+

Please wait, do not close this page.

+
+ )} + + {/* Error display */} + {error && ( +

{error}

+ )} + + {/* Action buttons */} + {(status === 'idle' || status === 'error') && ( + + )} + + )} +
+ ); +} + +// ─── Styles ─── + +const labelStyle: React.CSSProperties = { + display: 'block', + marginBottom: 4, + fontSize: 13, + fontWeight: 600, +}; + +const selectStyle: React.CSSProperties = { + width: '100%', + padding: '8px 12px', + border: '1px solid #ccc', + borderRadius: 4, + fontSize: 14, +}; + +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, + textDecoration: 'none', + color: 'inherit', + cursor: 'pointer', + background: '#fff', +}; + +const primaryButtonStyle: React.CSSProperties = { + width: '100%', + padding: '10px 16px', + border: '1px solid #333', + borderRadius: 4, + cursor: 'pointer', + background: '#333', + color: '#fff', + fontSize: 14, + fontWeight: 600, +}; + +const reviewRowStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + gap: 12, + padding: '6px 0', + borderBottom: '1px solid #f0f0f0', +}; + +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: 440, + width: '90%', +}; diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx new file mode 100644 index 0000000..1283d1e --- /dev/null +++ b/apps/web/src/app/settings/page.tsx @@ -0,0 +1,45 @@ +'use client'; + +import Link from 'next/link'; +import { useAuthStore } from '@/store/auth-store'; + +export default function SettingsPage() { + const { user } = useAuthStore(); + + + return ( +
+
+

Settings

+ + Dashboard + +
+ + {/* Account Section */} +
+

Account

+ +
+

Email

+

{user?.email || 'Not authenticated'}

+
+
+
+ ); +} + +// ── Styles ── + +const navButtonStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: 4, + textDecoration: 'none', + color: 'inherit', + cursor: 'pointer', + background: '#fff', +}; diff --git a/apps/web/src/app/swap/page.tsx b/apps/web/src/app/swap/page.tsx new file mode 100644 index 0000000..c4fce3c --- /dev/null +++ b/apps/web/src/app/swap/page.tsx @@ -0,0 +1,328 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { + type SwapChain, + SWAP_TOKEN_OPTIONS_BY_CHAIN, + CHAIN_DEFAULT_TOKENS, + getSlippageBpsForChain, + getExplorerTxUrl, +} from '@/lib/swap/constants'; +import { useSwap, type MultiChainSwapRequest } from '@/hooks/useSwap'; +import { useGasPrice } from '@/hooks/useGasPrice'; +import { useGasSettings, type GasMode } from '@/hooks/useGasSettings'; +import { useAuthStore } from '@/store/auth-store'; + +const GAS_MODE_LABELS: Record = { + slow: 'Slow', + normal: 'Normal', + fast: 'Fast', + custom: 'Custom', +}; + +const GAS_MODES: GasMode[] = ['slow', 'normal', 'fast', 'custom']; +const CHAINS: SwapChain[] = ['ETH', 'SOL', 'TRX', 'BSC']; + +export default function SwapPage() { + const router = useRouter(); + const user = useAuthStore((state) => state.user); + const { data: gasPriceData, loading: gasLoading } = useGasPrice(); + const gas = useGasSettings(gasPriceData); + const { + status, + quote, + error, + txHash, + approvalHashes, + liveEstimate, + chain, + setChain, + fetchQuote, + submitSwap, + resetSwap, + estimateOutput, + } = useSwap(); + + const tokenOptions = SWAP_TOKEN_OPTIONS_BY_CHAIN[chain]; + const defaults = CHAIN_DEFAULT_TOKENS[chain]; + const [fromSymbol, setFromSymbol] = useState(defaults.from); + const [toSymbol, setToSymbol] = useState(defaults.to); + const [amount, setAmount] = useState(''); + const [confirmed, setConfirmed] = useState(false); + + const slippageBps = useMemo(() => getSlippageBpsForChain(chain, fromSymbol, toSymbol), [chain, fromSymbol, toSymbol]); + const slippagePercent = (slippageBps / 100).toFixed(2); + + const request = useMemo( + () => ({ + chain, + fromSymbol, + toSymbol, + amount, + slippageBps, + }), + [chain, amount, fromSymbol, slippageBps, toSymbol] + ); + + + useEffect(() => { + estimateOutput(request); + }, [estimateOutput, request]); + + if (!user) { + return null; + } + + const canQuote = fromSymbol !== toSymbol && Number(amount) > 0 && request.slippageBps > 0; + const canSwap = !!quote && confirmed && status !== 'approving' && status !== 'swapping' && status !== 'quoting'; + + const handleChainChange = (newChain: SwapChain) => { + setChain(newChain); + const newDefaults = CHAIN_DEFAULT_TOKENS[newChain]; + setFromSymbol(newDefaults.from); + setToSymbol(newDefaults.to); + setAmount(''); + setConfirmed(false); + resetSwap(); + }; + + const handleQuote = async () => { + setConfirmed(false); + await fetchQuote(request); + }; + + const handleSwap = async () => { + await submitSwap(request, gas.effectiveMaxFee, gas.effectivePriorityFee); + }; + + const handleFieldReset = () => { + setConfirmed(false); + resetSwap(); + }; + + const tierGwei = (mode: GasMode): string => { + if (mode === 'custom') return ''; + if (!gasPriceData) return '...'; + const v = gasPriceData[mode].maxFeePerGas; + if (v >= 1) return v.toFixed(2); + const s = v.toFixed(4); + return s.replace(/0+$/, '').replace(/\.$/, ''); + }; + + return ( +
+
+

Swap

+ + Back to Dashboard + +
+ +
+ {/* Chain selector */} +
+ {CHAINS.map((c) => ( + + ))} +
+ +
+ + +
+ +
+ + +
+ +
+ + { setAmount(event.target.value); handleFieldReset(); }} type="number" min="0" step="any" style={inputStyle} /> + {liveEstimate && fromSymbol !== toSymbol && ( +

+ {liveEstimate.loading ? '...' : `~${liveEstimate.amountOut} ${toSymbol}`} +

+ )} +
+ + {/* Gas speed — only for ETH */} + {chain === 'ETH' && ( +
+ +
+ {GAS_MODES.map((mode) => ( + + ))} +
+ {gas.gasMode === 'custom' && ( + gas.setCustomGwei(event.target.value)} + type="number" + min="0" + step="0.01" + placeholder="Enter gwei" + style={{ ...inputStyle, marginTop: 6 }} + /> + )} +

+ Effective: {gas.displayGwei} +

+
+ )} + + {/* Fee info for non-ETH */} + {chain === 'SOL' && ( +

+ Fee: Auto (Jupiter Priority) +

+ )} + {chain === 'TRX' && ( +

+ Fee: Auto (Energy/Bandwidth) +

+ )} + {chain === 'BSC' && ( +

+ Fee: 0.055 gwei (BSC fixed) +

+ )} + +

+ Slippage: {slippagePercent}% (auto) +

+

+ Platform fee: 0.7% per swap +

+ + {fromSymbol === toSymbol && ( +

From and To tokens must be different.

+ )} + + +
+ + {quote && ( +
+

Review

+

Expected output: {quote.amountOutFormatted} {toSymbol}

+

Minimum output after slippage: {quote.minimumAmountOutFormatted} {toSymbol}

+ {'executionPrice' in quote &&

Execution price: {quote.executionPrice}

} + {'priceImpact' in quote &&

Price impact: {quote.priceImpact}%

} + {'routeSymbols' in quote &&

Route: {quote.routeSymbols.join(' -> ')}

} + {'routeFees' in quote &&

Pool fees: {quote.routeFees.join(' / ')}

} + {'routeLabels' in quote && (quote as any).routeLabels?.length > 0 && ( +

Route: {(quote as any).routeLabels.join(' → ')}

+ )} + {chain === 'ETH' &&

Gas: {gas.displayGwei} ({GAS_MODE_LABELS[gas.gasMode]})

} +

Slippage: {slippagePercent}%

+ + + + +
+ )} + + {(approvalHashes.length > 0 || txHash) && ( +
+

Transaction Status

+ {approvalHashes.map((hash) => ( +

+ Approval tx:{' '} + + {hash.slice(0, 16)}... + +

+ ))} + {txHash && ( +

+ Swap tx:{' '} + + {txHash.slice(0, 16)}... + +

+ )} +
+ )} + + {(error || status === 'error') && ( +

+ {error ?? 'Swap failed'} +

+ )} +
+ ); +} + +const fieldGroupStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 6, + marginBottom: 12, +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: 8, +}; + +const navButtonStyle: React.CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '6px 12px', + border: '1px solid #ccc', + borderRadius: 4, +}; diff --git a/apps/web/src/hooks/useBalances.ts b/apps/web/src/hooks/useBalances.ts new file mode 100644 index 0000000..5709ad2 --- /dev/null +++ b/apps/web/src/hooks/useBalances.ts @@ -0,0 +1,52 @@ +'use client'; + +import { useCallback, useEffect } from 'react'; +import { useAuthStore } from '@/store/auth-store'; +import { useBalanceStore } from '@/store/balance-store'; + +const BALANCE_REFRESH_INTERVAL_MS = 30_000; + +export function useBalances() { + const user = useAuthStore((state) => state.user); + const wallets = useAuthStore((state) => state.wallets); + const portfolio = useBalanceStore((state) => state.portfolio); + const loading = useBalanceStore((state) => state.loading); + const refreshing = useBalanceStore((state) => state.refreshing); + const error = useBalanceStore((state) => state.error); + const fetchBalances = useBalanceStore((state) => state.fetchBalances); + const clearBalances = useBalanceStore((state) => state.clearBalances); + + const refresh = useCallback(async () => { + if (!user || !wallets.length) { + clearBalances(); + return; + } + + await fetchBalances(wallets); + }, [clearBalances, fetchBalances, user, wallets]); + + useEffect(() => { + if (!user || !wallets.length) { + clearBalances(); + return; + } + + void fetchBalances(wallets); + + const intervalId = window.setInterval(() => { + void fetchBalances(wallets); + }, BALANCE_REFRESH_INTERVAL_MS); + + return () => { + window.clearInterval(intervalId); + }; + }, [clearBalances, fetchBalances, user, wallets]); + + return { + portfolio, + loading, + refreshing, + error, + refresh, + }; +} diff --git a/apps/web/src/hooks/useBridge.ts b/apps/web/src/hooks/useBridge.ts new file mode 100644 index 0000000..be1355d --- /dev/null +++ b/apps/web/src/hooks/useBridge.ts @@ -0,0 +1,191 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { executeBridge, type ExecuteBridgeResult } from '@/lib/bridge/execute'; +import { getBridgeQuote, type BridgeQuoteResult } from '@/lib/bridge/quote'; +import { getBridgeStatus, isBridgeTerminalStatus, type BridgeStatusResult } from '@/lib/bridge/status'; +import { + BRIDGE_CHAINS, + getTokenConfig, + type BridgeChainKey, +} from '@/lib/bridge/constants'; +import { useAuthStore } from '@/store/auth-store'; + +export type BridgeStatus = 'idle' | 'quoting' | 'quoted' | 'executing' | 'monitoring' | 'success' | 'error'; + +const BRIDGE_POLL_INTERVAL_MS = 1_000; + +export interface BridgeRequestParams { + sourceChain: BridgeChainKey; + sourceToken: string; + destChain: BridgeChainKey; + destToken: string; + amount: string; +} + +export function useBridge() { + const wallets = useAuthStore((state) => state.wallets); + + const [status, setStatus] = useState('idle'); + const [quote, setQuote] = useState(null); + const [bridgeStatus, setBridgeStatus] = useState(null); + const [requestId, setRequestId] = useState(null); + const [txHashes, setTxHashes] = useState([]); + const [error, setError] = useState(null); + const [sourceChain, setSourceChain] = useState('ETH'); + const pollTimeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (pollTimeoutRef.current) { + window.clearTimeout(pollTimeoutRef.current); + } + }; + }, []); + + const sourceWallet = useMemo( + () => wallets.find((w) => w.chain === BRIDGE_CHAINS[sourceChain].walletChain) ?? null, + [wallets, sourceChain], + ); + + function getWalletAddress(chainKey: BridgeChainKey): string | null { + const chain = BRIDGE_CHAINS[chainKey]; + const wallet = wallets.find((w) => w.chain === chain.walletChain); + return wallet?.address ?? null; + } + + const fetchQuote = async (request: BridgeRequestParams) => { + const userAddress = getWalletAddress(request.sourceChain); + if (!userAddress) { + throw new Error(`${request.sourceChain} wallet is not available`); + } + + const recipientAddress = getWalletAddress(request.destChain); + if (!recipientAddress) { + throw new Error(`${request.destChain} wallet is not available`); + } + + setStatus('quoting'); + setError(null); + setBridgeStatus(null); + setRequestId(null); + setTxHashes([]); + + try { + const nextQuote = await getBridgeQuote({ + ...request, + userAddress, + recipientAddress, + }); + setQuote(nextQuote); + setStatus('quoted'); + return nextQuote; + } catch (nextError) { + setQuote(null); + setStatus('error'); + setError(getErrorMessage(nextError)); + throw nextError; + } + }; + + const submitBridge = async ( + request: BridgeRequestParams, + maxFeeGwei?: string | null, + priorityFeeGwei?: string | null, + ) => { + if (!sourceWallet?.privateKey) { + throw new Error(`${request.sourceChain} private key is not available`); + } + + if (!quote) { + throw new Error('Get a bridge quote before executing'); + } + + setStatus('executing'); + setError(null); + + try { + const tokenConfig = getTokenConfig(request.sourceChain, request.sourceToken); + const execution = await executeBridge({ + sourceChain: request.sourceChain, + sourceToken: request.sourceToken, + originalAmount: request.amount, + sourceTokenDecimals: tokenConfig.decimals, + sourceTokenAddress: tokenConfig.address, + privateKey: sourceWallet.privateKey, + quote: quote.quote, + maxFeeGwei, + priorityFeeGwei, + }); + + setTxHashes(execution.txHashes); + if (!execution.requestId) { + throw new Error('Relay request ID was not returned'); + } + + setRequestId(execution.requestId); + setStatus('monitoring'); + await pollBridgeStatus(execution.requestId); + return execution; + } catch (nextError) { + setStatus('error'); + setError(getErrorMessage(nextError)); + throw nextError; + } + }; + + const resetBridge = () => { + if (pollTimeoutRef.current) { + window.clearTimeout(pollTimeoutRef.current); + pollTimeoutRef.current = null; + } + + setStatus('idle'); + setQuote(null); + setBridgeStatus(null); + setRequestId(null); + setTxHashes([]); + setError(null); + }; + + const pollBridgeStatus = async (nextRequestId: string): Promise => { + const nextStatus = await getBridgeStatus(nextRequestId); + setBridgeStatus(nextStatus); + + if (isBridgeTerminalStatus(nextStatus.status)) { + if (nextStatus.status === 'success') { + setStatus('success'); + } else { + setStatus('error'); + setError(nextStatus.details || `Bridge finished with status: ${nextStatus.status}`); + } + return nextStatus; + } + + await new Promise((resolve) => { + pollTimeoutRef.current = window.setTimeout(() => resolve(), BRIDGE_POLL_INTERVAL_MS); + }); + + return pollBridgeStatus(nextRequestId); + }; + + return { + status, + quote, + bridgeStatus, + requestId, + txHashes, + error, + sourceChain, + setSourceChain, + sourceWallet, + fetchQuote, + submitBridge, + resetBridge, + }; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return 'Bridge request failed'; +} diff --git a/apps/web/src/hooks/useGasPrice.ts b/apps/web/src/hooks/useGasPrice.ts new file mode 100644 index 0000000..2394f4d --- /dev/null +++ b/apps/web/src/hooks/useGasPrice.ts @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { fetchGasPrices, type GasPriceData } from '@/lib/gas-price'; + +const GAS_REFRESH_INTERVAL_MS = 30_000; + +export function useGasPrice() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + const refresh = async () => { + try { + const next = await fetchGasPrices(); + if (!cancelled) setData(next); + } catch { + /* keep last known data */ + } finally { + if (!cancelled) setLoading(false); + } + }; + + void refresh(); + const intervalId = setInterval(refresh, GAS_REFRESH_INTERVAL_MS); + + return () => { + cancelled = true; + clearInterval(intervalId); + }; + }, []); + + return { data, loading }; +} diff --git a/apps/web/src/hooks/useGasSettings.ts b/apps/web/src/hooks/useGasSettings.ts new file mode 100644 index 0000000..28ed036 --- /dev/null +++ b/apps/web/src/hooks/useGasSettings.ts @@ -0,0 +1,64 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import type { GasPriceData } from '@/lib/gas-price'; + +export type GasMode = 'slow' | 'normal' | 'fast' | 'custom'; + +export interface GasSettings { + gasMode: GasMode; + setGasMode: (mode: GasMode) => void; + customGwei: string; + setCustomGwei: (value: string) => void; + effectiveMaxFee: string | null; + effectivePriorityFee: string | null; + displayGwei: string; +} + +function fmt(n: number): string { + if (n >= 1) return n.toFixed(2); + const s = n.toFixed(6); + return s.replace(/0+$/, '').replace(/\.$/, ''); +} + +export function useGasSettings(gasPriceData: GasPriceData | null): GasSettings { + const [gasMode, setGasMode] = useState('normal'); + const [customGwei, setCustomGwei] = useState(''); + + const { effectiveMaxFee, effectivePriorityFee, displayGwei } = useMemo(() => { + if (gasMode === 'custom') { + const v = customGwei.trim(); + if (!v || Number(v) <= 0) { + return { effectiveMaxFee: null, effectivePriorityFee: null, displayGwei: '-' }; + } + const base = Number(v); + const priority = Math.max(0.01, base * 0.1); + return { + effectiveMaxFee: v, + effectivePriorityFee: fmt(priority), + displayGwei: `${v} gwei`, + }; + } + + if (!gasPriceData) { + return { effectiveMaxFee: null, effectivePriorityFee: null, displayGwei: '...' }; + } + + const tier = gasPriceData[gasMode]; + return { + effectiveMaxFee: fmt(tier.maxFeePerGas), + effectivePriorityFee: fmt(tier.maxPriorityFeePerGas), + displayGwei: `${fmt(tier.maxFeePerGas)} gwei`, + }; + }, [gasMode, customGwei, gasPriceData]); + + return { + gasMode, + setGasMode, + customGwei, + setCustomGwei, + effectiveMaxFee, + effectivePriorityFee, + displayGwei, + }; +} diff --git a/apps/web/src/hooks/useSwap.ts b/apps/web/src/hooks/useSwap.ts new file mode 100644 index 0000000..68f6109 --- /dev/null +++ b/apps/web/src/hooks/useSwap.ts @@ -0,0 +1,274 @@ +'use client'; + +import { useCallback, useMemo, useRef, useState } from 'react'; +import { ensureSwapApproval } from '@/lib/swap/approve'; +import { executeSwap } from '@/lib/swap/execute'; +import { getSwapQuote, type SwapQuoteResult } from '@/lib/swap/quote'; +import { getSolSwapQuote, type SolSwapQuoteResult } from '@/lib/swap/sol/quote'; +import { executeSolSwap } from '@/lib/swap/sol/execute'; +import { getTrxSwapQuote, type TrxSwapQuoteResult } from '@/lib/swap/trx/quote'; +import { executeTrxSwap } from '@/lib/swap/trx/execute'; +import { getBscSwapQuote, type BscSwapQuoteResult } from '@/lib/swap/bsc/quote'; +import { executeBscSwap } from '@/lib/swap/bsc/execute'; +import { mapSwapError } from '@/lib/swap/errors'; +import type { SwapQuoteRequest } from '@/lib/swap/constants'; +import { type SwapChain, getSlippageBpsForChain } from '@/lib/swap/constants'; +import { useAuthStore } from '@/store/auth-store'; + +const ESTIMATE_DEBOUNCE_MS = 500; + +export type SwapStatus = 'idle' | 'quoting' | 'quoted' | 'approving' | 'swapping' | 'success' | 'error'; + +export interface LiveEstimate { + amountOut: string; + loading: boolean; +} + +export type AnyQuoteResult = SwapQuoteResult | SolSwapQuoteResult | TrxSwapQuoteResult | BscSwapQuoteResult; + +export interface MultiChainSwapRequest { + chain: SwapChain; + fromSymbol: string; + toSymbol: string; + amount: string; + slippageBps: number; +} + +export function useSwap() { + const wallets = useAuthStore((state) => state.wallets); + const [status, setStatus] = useState('idle'); + const [quote, setQuote] = useState(null); + const [error, setError] = useState(null); + const [txHash, setTxHash] = useState(null); + const [approvalHashes, setApprovalHashes] = useState([]); + const [liveEstimate, setLiveEstimate] = useState(null); + const [chain, setChain] = useState('ETH'); + const estimateTimeoutRef = useRef | null>(null); + + const currentWallet = useMemo( + () => wallets.find((w) => w.chain === chain) ?? null, + [wallets, chain] + ); + + const fetchQuote = async (request: MultiChainSwapRequest) => { + setStatus('quoting'); + setError(null); + setTxHash(null); + setApprovalHashes([]); + + try { + const nextQuote = await fetchQuoteForChain(request); + setQuote(nextQuote); + setStatus('quoted'); + return nextQuote; + } catch (nextError) { + setQuote(null); + setStatus('error'); + setError(mapSwapError(request.chain, nextError)); + throw nextError; + } + }; + + const submitSwap = async ( + request: MultiChainSwapRequest, + maxFeeGwei?: string | null, + priorityFeeGwei?: string | null, + ) => { + if (!currentWallet?.privateKey) { + const walletError = new Error(`${request.chain} private key is not available`); + setStatus('error'); + setError(walletError.message); + throw walletError; + } + + if (!quote) { + const quoteError = new Error('Get a quote before swapping'); + setStatus('error'); + setError(quoteError.message); + throw quoteError; + } + + setError(null); + setApprovalHashes([]); + + try { + let result: { hash: string }; + + switch (request.chain) { + case 'ETH': { + // Existing ETH swap logic + if (request.fromSymbol !== 'ETH') { + setStatus('approving'); + const approvalResult = await ensureSwapApproval({ + privateKey: currentWallet.privateKey, + tokenSymbol: request.fromSymbol as any, + amount: request.amount, + maxFeeGwei, + priorityFeeGwei, + }); + setApprovalHashes(approvalResult.approvalHashes); + } + + setStatus('swapping'); + const ethQuote = quote as SwapQuoteResult; + result = await executeSwap({ + privateKey: currentWallet.privateKey, + request: request as SwapQuoteRequest, + quote: ethQuote, + maxFeeGwei, + priorityFeeGwei, + }); + break; + } + + case 'SOL': { + setStatus('swapping'); + const solQuote = quote as SolSwapQuoteResult; + result = await executeSolSwap({ + privateKeyHex: currentWallet.privateKey, + userPublicKey: currentWallet.address, + quoteResponse: solQuote.quoteResponse, + }); + break; + } + + case 'TRX': { + setStatus('swapping'); + const trxQuote = quote as TrxSwapQuoteResult; + const trxResult = await executeTrxSwap({ + privateKeyHex: currentWallet.privateKey, + from: request.fromSymbol, + to: request.toSymbol, + amount: trxQuote.amountInRaw, + amountOutMin: trxQuote.minimumAmountOutRaw, + userAddress: currentWallet.address, + }); + setApprovalHashes(trxResult.approvalHashes); + result = trxResult; + break; + } + + case 'BSC': { + setStatus('swapping'); + const bscQuote = quote as BscSwapQuoteResult; + const bscResult = await executeBscSwap({ + privateKeyHex: currentWallet.privateKey, + from: request.fromSymbol, + to: request.toSymbol, + amount: bscQuote.amountIn, + amountOutMin: bscQuote.minimumAmountOutRaw, + userAddress: currentWallet.address, + }); + setApprovalHashes(bscResult.approvalHashes); + result = bscResult; + break; + } + } + + setTxHash(result.hash); + setStatus('success'); + return result; + } catch (nextError) { + setStatus('error'); + setError(mapSwapError(request.chain, nextError)); + throw nextError; + } + }; + + const estimateOutput = useCallback((request: MultiChainSwapRequest) => { + if ( + request.fromSymbol === request.toSymbol || + !request.amount || + Number(request.amount) <= 0 || + request.slippageBps <= 0 + ) { + setLiveEstimate(null); + return; + } + + if (estimateTimeoutRef.current) { + clearTimeout(estimateTimeoutRef.current); + estimateTimeoutRef.current = null; + } + + setLiveEstimate({ amountOut: '', loading: true }); + + estimateTimeoutRef.current = setTimeout(async () => { + estimateTimeoutRef.current = null; + try { + const result = await fetchQuoteForChain(request); + setLiveEstimate({ + amountOut: result.amountOutFormatted, + loading: false, + }); + } catch { + setLiveEstimate(null); + } + }, ESTIMATE_DEBOUNCE_MS); + }, []); + + const resetSwap = useCallback(() => { + if (estimateTimeoutRef.current) { + clearTimeout(estimateTimeoutRef.current); + estimateTimeoutRef.current = null; + } + setStatus('idle'); + setQuote(null); + setError(null); + setTxHash(null); + setApprovalHashes([]); + setLiveEstimate(null); + }, []); + + return { + status, + quote, + error, + txHash, + approvalHashes, + liveEstimate, + chain, + setChain, + currentWallet, + fetchQuote, + submitSwap, + resetSwap, + estimateOutput, + }; +} + +async function fetchQuoteForChain(request: MultiChainSwapRequest): Promise { + switch (request.chain) { + case 'ETH': + return getSwapQuote({ + fromSymbol: request.fromSymbol as any, + toSymbol: request.toSymbol as any, + amount: request.amount, + slippageBps: request.slippageBps, + }); + + case 'SOL': + return getSolSwapQuote({ + fromSymbol: request.fromSymbol, + toSymbol: request.toSymbol, + amount: request.amount, + slippageBps: request.slippageBps, + }); + + case 'TRX': + return getTrxSwapQuote({ + fromSymbol: request.fromSymbol, + toSymbol: request.toSymbol, + amount: request.amount, + slippageBps: request.slippageBps, + }); + + case 'BSC': + return getBscSwapQuote({ + fromSymbol: request.fromSymbol, + toSymbol: request.toSymbol, + amount: request.amount, + slippageBps: request.slippageBps, + }); + } +} diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts new file mode 100644 index 0000000..da85480 --- /dev/null +++ b/apps/web/src/lib/api.ts @@ -0,0 +1,28 @@ +import { webEnv } from './env'; + +const API_URL = webEnv.apiUrl; + +async function request(path: string, options: RequestInit = {}): Promise { + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + }; + + const res = await fetch(`${API_URL}${path}`, { + ...options, + headers, + credentials: 'include', + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error || 'Request failed'); + } + + return data.data; +} + +export const api = { + getWallets: () => request('/api/wallets'), +}; diff --git a/apps/web/src/lib/balances/bsc-balances.ts b/apps/web/src/lib/balances/bsc-balances.ts new file mode 100644 index 0000000..2451c79 --- /dev/null +++ b/apps/web/src/lib/balances/bsc-balances.ts @@ -0,0 +1,185 @@ +import { ethers } from 'ethers'; +import { webEnv } from '@/lib/env'; +import type { ChainBalance, TokenBalance, TokenDefinition } from './types'; + +const BSC_CHAIN_ID = 56; + +const BSC_BALANCE_TIMEOUT_MS = 6_000; +const BSC_RPC_HEALTHCHECK_TIMEOUT_MS = 4_000; +const BEP20_ABI = ['function balanceOf(address owner) view returns (uint256)']; + +const BSC_RPC_CANDIDATES = dedupeUrls([ + webEnv.bscRpcUrl, + 'https://bsc-dataseed1.defibit.io', + 'https://bsc-dataseed1.ninicoin.io', + 'https://bsc-dataseed.binance.org', +]); + +const BSC_TOKENS: TokenDefinition[] = [ + { + chain: 'BSC', + symbol: 'BNB', + decimals: 18, + contractAddress: 'native', + coinGeckoId: 'binancecoin', + isNative: true, + }, + { + chain: 'BSC', + symbol: 'USDT', + decimals: 18, + contractAddress: '0x55d398326f99059fF775485246999027B3197955', + coinGeckoId: 'tether', + isNative: false, + }, + { + chain: 'BSC', + symbol: 'DOGE', + decimals: 8, + contractAddress: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43', + coinGeckoId: 'dogecoin', + isNative: false, + }, +]; + +export async function fetchBscBalances(address: string): Promise { + let provider: ethers.providers.StaticJsonRpcProvider; + try { + provider = await getHealthyBscProvider(); + } catch (error) { + return { + chain: 'BSC', + address, + tokens: BSC_TOKENS.map(createEmptyTokenBalance), + totalUsd: null, + error: getErrorMessage(error), + }; + } + + const settled = await Promise.allSettled( + BSC_TOKENS.map(async (token) => readBscTokenBalance(provider, address, token)) + ); + + const tokens: TokenBalance[] = []; + const errors: Array<{ symbol: string; message: string }> = []; + + settled.forEach((result, index) => { + const token = BSC_TOKENS[index]; + + if (result.status === 'fulfilled') { + tokens.push(result.value); + return; + } + + tokens.push(createEmptyTokenBalance(token)); + errors.push({ symbol: token.symbol, message: getErrorMessage(result.reason) }); + }); + + const uniqueMessages = [...new Set(errors.map((item) => item.message))]; + const error = + errors.length === BSC_TOKENS.length && uniqueMessages.length === 1 + ? uniqueMessages[0] + : errors.length + ? errors.map((item) => `${item.symbol}: ${item.message}`).join(' | ') + : null; + + return { + chain: 'BSC', + address, + tokens, + totalUsd: null, + error, + }; +} + +async function getHealthyBscProvider(): Promise { + let lastError: unknown = new Error('No BSC RPC endpoints configured'); + for (const rpcUrl of BSC_RPC_CANDIDATES) { + const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl, BSC_CHAIN_ID); + try { + await withTimeout( + provider.getBlockNumber(), + BSC_RPC_HEALTHCHECK_TIMEOUT_MS, + `BSC RPC health-check timed out for ${rpcUrl}` + ); + return provider; + } catch (error) { + lastError = error; + } + } + throw lastError; +} + +async function readBscTokenBalance(provider: ethers.providers.StaticJsonRpcProvider, address: string, token: TokenDefinition): Promise { + if (token.isNative) { + const balance = await withTimeout( + provider.getBalance(address), + BSC_BALANCE_TIMEOUT_MS, + 'BNB balance request timed out' + ); + + return { + ...token, + balanceRaw: balance.toString(), + balanceFormatted: ethers.utils.formatEther(balance), + priceUsd: null, + valueUsd: null, + }; + } + + const contract = new ethers.Contract(token.contractAddress, BEP20_ABI, provider); + const balance = (await withTimeout( + contract.balanceOf(address), + BSC_BALANCE_TIMEOUT_MS, + `${token.symbol} balance request timed out` + )) as ethers.BigNumber; + + return { + ...token, + balanceRaw: balance.toString(), + balanceFormatted: ethers.utils.formatUnits(balance, token.decimals), + priceUsd: null, + valueUsd: null, + }; +} + +function createEmptyTokenBalance(token: TokenDefinition): TokenBalance { + return { + ...token, + balanceRaw: '0', + balanceFormatted: '0', + priceUsd: null, + valueUsd: null, + }; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + + promise + .then((value) => { + clearTimeout(timeoutId); + resolve(value); + }) + .catch((error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + if (error.message.includes('Failed to fetch')) { + return 'BSC RPC is temporarily unavailable'; + } + return error.message; + } + + return 'Unable to load token balance'; +} + +function dedupeUrls(urls: string[]): string[] { + return [...new Set(urls.map((url) => url.trim()).filter(Boolean))]; +} diff --git a/apps/web/src/lib/balances/btc-balances.ts b/apps/web/src/lib/balances/btc-balances.ts new file mode 100644 index 0000000..4885e1a --- /dev/null +++ b/apps/web/src/lib/balances/btc-balances.ts @@ -0,0 +1,101 @@ +import { webEnv } from '@/lib/env'; +import type { ChainBalance, TokenDefinition } from './types'; + +const BTC_BALANCE_TIMEOUT_MS = 12_000; + +const BTC_TOKEN: TokenDefinition = { + chain: 'BTC', + symbol: 'BTC', + decimals: 8, + contractAddress: 'native', + coinGeckoId: 'bitcoin', + isNative: true, +}; + +interface BlockstreamAddressResponse { + chain_stats?: { + funded_txo_sum?: number; + spent_txo_sum?: number; + }; +} + +export async function fetchBtcBalances(address: string): Promise { + try { + const response = await fetchJsonWithTimeout( + `${webEnv.btcApiUrl}/address/${address}`, + BTC_BALANCE_TIMEOUT_MS + ); + + const funded = response.chain_stats?.funded_txo_sum ?? 0; + const spent = response.chain_stats?.spent_txo_sum ?? 0; + const sats = Math.max(funded - spent, 0); + + return { + chain: 'BTC', + address, + tokens: [ + { + ...BTC_TOKEN, + balanceRaw: sats.toString(), + balanceFormatted: formatFixedBalance(sats, BTC_TOKEN.decimals), + priceUsd: null, + valueUsd: null, + }, + ], + totalUsd: null, + error: null, + }; + } catch (error) { + return { + chain: 'BTC', + address, + tokens: [ + { + ...BTC_TOKEN, + balanceRaw: '0', + balanceFormatted: '0', + priceUsd: null, + valueUsd: null, + }, + ], + totalUsd: null, + error: getErrorMessage(error), + }; + } +} + +async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + signal: controller.signal, + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error(`BTC API returned ${response.status}`); + } + + return (await response.json()) as T; + } finally { + clearTimeout(timeoutId); + } +} + +function formatFixedBalance(rawValue: number, decimals: number): string { + if (rawValue === 0) { + return '0'; + } + + return (rawValue / 10 ** decimals).toString(); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return 'Unable to load BTC balance'; +} diff --git a/apps/web/src/lib/balances/eth-balances.ts b/apps/web/src/lib/balances/eth-balances.ts new file mode 100644 index 0000000..7724861 --- /dev/null +++ b/apps/web/src/lib/balances/eth-balances.ts @@ -0,0 +1,257 @@ +import { ethers } from 'ethers'; +import { webEnv } from '@/lib/env'; +import type { ChainBalance, TokenBalance, TokenDefinition } from './types'; + +const ETH_CHAIN_ID = 1; + +const ETH_BALANCE_TIMEOUT_MS = 6_000; +const ETH_RPC_HEALTHCHECK_TIMEOUT_MS = 4_000; +const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)']; + +const ETH_RPC_CANDIDATES = dedupeUrls([ + webEnv.ethRpcUrl, + 'https://ethereum-rpc.publicnode.com', + 'https://rpc.ankr.com/eth', + 'https://eth.llamarpc.com', +]); + +const ETH_TOKENS: TokenDefinition[] = [ + { + chain: 'ETH', + symbol: 'ETH', + decimals: 18, + contractAddress: 'native', + coinGeckoId: 'ethereum', + isNative: true, + }, + { + chain: 'ETH', + symbol: 'USDT', + decimals: 6, + contractAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + coinGeckoId: 'tether', + isNative: false, + }, + { + chain: 'ETH', + symbol: 'USDC', + decimals: 6, + contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + coinGeckoId: 'usd-coin', + isNative: false, + }, + { + chain: 'ETH', + symbol: 'XAUT', + decimals: 6, + contractAddress: '0x68749665FF8D2d112Fa859AA293F07A622782F38', + coinGeckoId: 'tether-gold', + isNative: false, + }, + { + chain: 'ETH', + symbol: 'UNI', + decimals: 18, + contractAddress: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', + coinGeckoId: 'uniswap', + isNative: false, + }, + { + chain: 'ETH', + symbol: 'PEPE', + decimals: 18, + contractAddress: '0x6982508145454Ce325dDbE47a25d4ec3d2311933', + coinGeckoId: 'pepe', + isNative: false, + }, + { + 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, + }, +]; + +export async function fetchEthBalances(address: string): Promise { + let provider: ethers.providers.StaticJsonRpcProvider; + try { + provider = await getHealthyEthProvider(); + } catch (error) { + return { + chain: 'ETH', + address, + tokens: ETH_TOKENS.map(createEmptyTokenBalance), + totalUsd: null, + error: getErrorMessage(error), + }; + } + + const settled = await Promise.allSettled( + ETH_TOKENS.map(async (token) => readEthTokenBalance(provider, address, token)) + ); + + const tokens: TokenBalance[] = []; + const errors: Array<{ symbol: string; message: string }> = []; + + settled.forEach((result, index) => { + const token = ETH_TOKENS[index]; + + if (result.status === 'fulfilled') { + tokens.push(result.value); + return; + } + + tokens.push(createEmptyTokenBalance(token)); + errors.push({ symbol: token.symbol, message: getErrorMessage(result.reason) }); + }); + + const uniqueMessages = [...new Set(errors.map((item) => item.message))]; + const error = + errors.length === ETH_TOKENS.length && uniqueMessages.length === 1 + ? uniqueMessages[0] + : errors.length + ? errors.map((item) => `${item.symbol}: ${item.message}`).join(' | ') + : null; + + return { + chain: 'ETH', + address, + tokens, + totalUsd: null, + error, + }; +} + +async function getHealthyEthProvider(): Promise { + let lastError: unknown = new Error('No Ethereum RPC endpoints configured'); + for (const rpcUrl of ETH_RPC_CANDIDATES) { + const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl, ETH_CHAIN_ID); + try { + await withTimeout( + provider.getBlockNumber(), + ETH_RPC_HEALTHCHECK_TIMEOUT_MS, + `ETH RPC health-check timed out for ${rpcUrl}` + ); + return provider; + } catch (error) { + lastError = error; + } + } + throw lastError; +} + +async function readEthTokenBalance(provider: ethers.providers.StaticJsonRpcProvider, address: string, token: TokenDefinition): Promise { + if (token.isNative) { + const balance = await withTimeout( + provider.getBalance(address), + ETH_BALANCE_TIMEOUT_MS, + 'ETH balance request timed out' + ); + + return { + ...token, + balanceRaw: balance.toString(), + balanceFormatted: ethers.utils.formatEther(balance), + priceUsd: null, + valueUsd: null, + }; + } + + const contract = new ethers.Contract(token.contractAddress, ERC20_ABI, provider); + const balance = (await withTimeout( + contract.balanceOf(address), + ETH_BALANCE_TIMEOUT_MS, + `${token.symbol} balance request timed out` + )) as ethers.BigNumber; + + return { + ...token, + balanceRaw: balance.toString(), + balanceFormatted: ethers.utils.formatUnits(balance, token.decimals), + priceUsd: null, + valueUsd: null, + }; +} + +function createEmptyTokenBalance(token: TokenDefinition): TokenBalance { + return { + ...token, + balanceRaw: '0', + balanceFormatted: '0', + priceUsd: null, + valueUsd: null, + }; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + + promise + .then((value) => { + clearTimeout(timeoutId); + resolve(value); + }) + .catch((error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + if (error.message.includes('Failed to fetch')) { + return 'Ethereum RPC is temporarily unavailable'; + } + return error.message; + } + + return 'Unable to load token balance'; +} + +function dedupeUrls(urls: string[]): string[] { + return [...new Set(urls.map((url) => url.trim()).filter(Boolean))]; +} diff --git a/apps/web/src/lib/balances/index.ts b/apps/web/src/lib/balances/index.ts new file mode 100644 index 0000000..17d9df8 --- /dev/null +++ b/apps/web/src/lib/balances/index.ts @@ -0,0 +1,120 @@ +import type { DerivedWallet } from '@/lib/crypto/derive-keys'; +import { fetchBtcBalances } from './btc-balances'; +import { fetchEthBalances } from './eth-balances'; +import { fetchUsdPrices } from './prices'; +import { fetchSolBalances } from './sol-balances'; +import { fetchTrxBalances } from './trx-balances'; +import { fetchBscBalances } from './bsc-balances'; +import type { BalanceChain, ChainBalance, PortfolioBalance, TokenBalance } from './types'; + +const SUPPORTED_CHAINS: BalanceChain[] = ['ETH', 'BTC', 'SOL', 'TRX', 'BSC']; + +const balanceFetchers: Record Promise> = { + ETH: fetchEthBalances, + BTC: fetchBtcBalances, + SOL: fetchSolBalances, + TRX: fetchTrxBalances, + BSC: fetchBscBalances, +}; + +export async function fetchAllBalances(wallets: DerivedWallet[]): Promise { + const settled = await Promise.allSettled( + SUPPORTED_CHAINS.map(async (chain) => { + const wallet = wallets.find((item) => item.chain === chain); + + if (!wallet) { + return createMissingChainBalance(chain); + } + + return balanceFetchers[chain](wallet.address); + }) + ); + + const rawChains = settled.map((result, index) => { + if (result.status === 'fulfilled') { + return result.value; + } + + return createMissingChainBalance(SUPPORTED_CHAINS[index], getErrorMessage(result.reason)); + }); + + let prices: Record = {}; + let priceError: string | null = null; + + try { + const coinIds = rawChains.flatMap((chain) => chain.tokens.map((token) => token.coinGeckoId)); + prices = await fetchUsdPrices(coinIds); + } catch (error) { + priceError = getErrorMessage(error); + } + + const chains = rawChains.map((chain) => enrichChain(chain, prices)); + + return { + chains, + totalUsd: sumNullable(chains.map((chain) => chain.totalUsd)), + errors: chains.reduce((acc, chain) => { + if (chain.error && chain.error !== '__transient__') { + acc[chain.chain] = chain.error; + } + return acc; + }, {}), + priceError, + updatedAt: new Date().toISOString(), + }; +} + +function enrichChain(chain: ChainBalance, prices: Record): ChainBalance { + const tokens = chain.tokens.map((token) => enrichToken(token, prices)); + + return { + ...chain, + tokens, + totalUsd: sumNullable(tokens.map((token) => token.valueUsd)), + }; +} + +function enrichToken(token: TokenBalance, prices: Record): TokenBalance { + const priceUsd = prices[token.coinGeckoId]; + + if (typeof priceUsd !== 'number') { + return token; + } + + const balance = Number(token.balanceFormatted); + const valueUsd = Number.isFinite(balance) ? balance * priceUsd : null; + + return { + ...token, + priceUsd, + valueUsd, + }; +} + +function createMissingChainBalance(chain: BalanceChain, error = 'Wallet not available'): ChainBalance { + return { + chain, + address: '', + tokens: [], + totalUsd: null, + error, + }; +} + +function sumNullable(values: Array): number | null { + const filtered = values.filter((value): value is number => typeof value === 'number'); + + if (!filtered.length) { + return null; + } + + return filtered.reduce((total, value) => total + value, 0); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return 'Unable to load balances'; +} diff --git a/apps/web/src/lib/balances/prices.ts b/apps/web/src/lib/balances/prices.ts new file mode 100644 index 0000000..83cfcab --- /dev/null +++ b/apps/web/src/lib/balances/prices.ts @@ -0,0 +1,70 @@ +const PRICE_CACHE_TTL_MS = 60_000; +const PRICE_REQUEST_TIMEOUT_MS = 10_000; + +let cachedPrices: Record | null = null; +let cachedAt = 0; + +interface CoinGeckoPriceResponse { + [coinId: string]: { + usd?: number; + }; +} + +export async function fetchUsdPrices(coinIds: string[]): Promise> { + const uniqueCoinIds = Array.from(new Set(coinIds.filter(Boolean))); + + if (!uniqueCoinIds.length) { + return {}; + } + + if ( + cachedPrices && + Date.now() - cachedAt < PRICE_CACHE_TTL_MS && + uniqueCoinIds.every((coinId) => coinId in cachedPrices!) + ) { + return uniqueCoinIds.reduce>((acc, coinId) => { + acc[coinId] = cachedPrices![coinId]; + return acc; + }, {}); + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), PRICE_REQUEST_TIMEOUT_MS); + + try { + const url = new URL('https://api.coingecko.com/api/v3/simple/price'); + url.searchParams.set('ids', uniqueCoinIds.join(',')); + url.searchParams.set('vs_currencies', 'usd'); + + const response = await fetch(url.toString(), { + signal: controller.signal, + cache: 'no-store', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`CoinGecko returned ${response.status}`); + } + + const payload = (await response.json()) as CoinGeckoPriceResponse; + const prices = uniqueCoinIds.reduce>((acc, coinId) => { + const price = payload[coinId]?.usd; + if (typeof price === 'number') { + acc[coinId] = price; + } + return acc; + }, {}); + + cachedPrices = { + ...(cachedPrices ?? {}), + ...prices, + }; + cachedAt = Date.now(); + + return prices; + } finally { + clearTimeout(timeoutId); + } +} diff --git a/apps/web/src/lib/balances/sol-balances.ts b/apps/web/src/lib/balances/sol-balances.ts new file mode 100644 index 0000000..258cb9b --- /dev/null +++ b/apps/web/src/lib/balances/sol-balances.ts @@ -0,0 +1,325 @@ +import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js'; +import { webEnv } from '@/lib/env'; +import type { ChainBalance, TokenBalance, TokenDefinition } from './types'; + +const SOL_BALANCE_TIMEOUT_MS = 6_000; +const SOL_RPC_CANDIDATES = dedupeUrls([ + webEnv.solRpcUrl, + 'https://solana.publicnode.com', + 'https://api.mainnet-beta.solana.com', +]); + +const SOL_TOKENS: TokenDefinition[] = [ + { + chain: 'SOL', + symbol: 'SOL', + decimals: 9, + contractAddress: 'native', + coinGeckoId: 'solana', + isNative: true, + }, + { + chain: 'SOL', + symbol: 'USDT', + decimals: 6, + contractAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + coinGeckoId: 'tether', + isNative: false, + }, + { + chain: 'SOL', + symbol: 'USDC', + decimals: 6, + contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + coinGeckoId: 'usd-coin', + isNative: false, + }, + { + chain: 'SOL', + symbol: 'PUMP', + decimals: 6, + contractAddress: 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn', + coinGeckoId: 'pump', + isNative: false, + }, + { + chain: 'SOL', + symbol: 'JUP', + decimals: 6, + contractAddress: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', + coinGeckoId: 'jupiter-exchange-solana', + isNative: false, + }, + { + 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, + }, +]; + +export async function fetchSolBalances(address: string): Promise { + const owner = new PublicKey(address); + + const settled = await Promise.allSettled( + SOL_TOKENS.map(async (token) => readSolTokenWithFallback(owner, token)) + ); + + const tokens: TokenBalance[] = []; + const errors: Array<{ symbol: string; message: string }> = []; + + settled.forEach((result, index) => { + const token = SOL_TOKENS[index]; + + if (result.status === 'fulfilled') { + tokens.push(result.value); + return; + } + + tokens.push(createEmptyTokenBalance(token)); + errors.push({ symbol: token.symbol, message: getErrorMessage(result.reason) }); + }); + + // Separate transient (rate-limit, access restricted) from permanent errors + const permanentErrors = errors.filter((e) => !isTransientError(e.message)); + const transientErrors = errors.filter((e) => isTransientError(e.message)); + + const uniqueMessages = [...new Set(permanentErrors.map((item) => item.message))]; + let error: string | null = + permanentErrors.length === SOL_TOKENS.length && uniqueMessages.length === 1 + ? uniqueMessages[0] + : permanentErrors.length + ? permanentErrors.map((item) => `${item.symbol}: ${item.message}`).join(' | ') + : null; + + // If some/all errors were transient, mark chain so store keeps previous balances + // '__transient__' is an internal marker — not displayed in UI + if (!error && transientErrors.length > 0) { + error = '__transient__'; + } + + return { + chain: 'SOL', + address, + tokens, + totalUsd: null, + error, + }; +} + +async function readSolTokenWithFallback( + owner: PublicKey, + token: TokenDefinition +): Promise { + let lastError: unknown; + + for (const rpcUrl of SOL_RPC_CANDIDATES) { + try { + const connection = new Connection(rpcUrl, 'confirmed'); + return await readSolTokenBalance(connection, owner, token); + } catch (error) { + lastError = error; + if (isMintNotFoundError(error)) { + return createEmptyTokenBalance(token); + } + } + } + + throw lastError; +} + +async function readSolTokenBalance( + connection: Connection, + owner: PublicKey, + token: TokenDefinition +): Promise { + if (token.isNative) { + const lamports = await withTimeout( + connection.getBalance(owner), + SOL_BALANCE_TIMEOUT_MS, + 'SOL balance request timed out' + ); + + return { + ...token, + balanceRaw: lamports.toString(), + balanceFormatted: (lamports / LAMPORTS_PER_SOL).toString(), + priceUsd: null, + valueUsd: null, + }; + } + + const mint = new PublicKey(token.contractAddress); + const response = await withTimeout( + connection.getParsedTokenAccountsByOwner(owner, { mint }), + SOL_BALANCE_TIMEOUT_MS, + `${token.symbol} balance request timed out` + ); + + const amountRaw = response.value.reduce((sum, account) => { + const parsed = account.account.data.parsed; + const amount = parsed.info.tokenAmount.amount; + return sum + BigInt(amount); + }, 0n); + + return { + ...token, + balanceRaw: amountRaw.toString(), + balanceFormatted: formatBigIntBalance(amountRaw, token.decimals), + priceUsd: null, + valueUsd: null, + }; +} + +function isMintNotFoundError(error: unknown): boolean { + if (error instanceof Error) { + const msg = error.message; + return msg.includes('could not find mint') || msg.includes('Invalid param'); + } + return false; +} + +function createEmptyTokenBalance(token: TokenDefinition): TokenBalance { + return { + ...token, + balanceRaw: '0', + balanceFormatted: '0', + priceUsd: null, + valueUsd: null, + }; +} + +function formatBigIntBalance(rawValue: bigint, decimals: number): string { + if (rawValue === 0n) { + return '0'; + } + + const divisor = 10n ** BigInt(decimals); + const whole = rawValue / divisor; + const fraction = rawValue % divisor; + + if (fraction === 0n) { + return whole.toString(); + } + + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + + promise + .then((value) => { + clearTimeout(timeoutId); + resolve(value); + }) + .catch((error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); +} + +function isTransientError(msg: string): boolean { + return msg.includes('access restricted') || + msg.includes('temporarily unavailable') || + msg.includes('timed out') || + msg.includes('rate') || + msg.includes('429') || + msg.includes('403'); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + const msg = error.message; + if (msg.includes('403') || msg.includes('API key is not allowed')) { + return 'Solana RPC access restricted'; + } + if (msg.includes('Failed to fetch')) { + return 'Solana RPC is temporarily unavailable'; + } + return msg; + } + + return 'Unable to load SOL balance'; +} + + +function dedupeUrls(urls: string[]): string[] { + return [...new Set(urls.map((url) => url.trim()).filter(Boolean))]; +} diff --git a/apps/web/src/lib/balances/trx-balances.ts b/apps/web/src/lib/balances/trx-balances.ts new file mode 100644 index 0000000..70a4ab4 --- /dev/null +++ b/apps/web/src/lib/balances/trx-balances.ts @@ -0,0 +1,133 @@ +import { webEnv } from '@/lib/env'; +import type { ChainBalance, TokenDefinition } from './types'; + +const TRX_BALANCE_TIMEOUT_MS = 12_000; + +const TRX_TOKENS: TokenDefinition[] = [ + { + chain: 'TRX', + symbol: 'TRX', + decimals: 6, + contractAddress: 'native', + coinGeckoId: 'tron', + isNative: true, + }, + { + chain: 'TRX', + symbol: 'USDT', + decimals: 6, + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + coinGeckoId: 'tether', + isNative: false, + }, +]; + +interface TronGridAccountResponse { + data?: Array<{ + balance?: number; + trc20?: Array>; + }>; +} + +export async function fetchTrxBalances(address: string): Promise { + try { + const response = await fetchJsonWithTimeout( + `${webEnv.apiUrl}/api/tron/account/${address}`, + TRX_BALANCE_TIMEOUT_MS + ); + + const account = response.data?.[0]; + const nativeRaw = account?.balance ?? 0; + const trc20Balances = account?.trc20 ?? []; + const usdtRaw = trc20Balances.reduce((current, entry) => { + const next = entry[TRX_TOKENS[1].contractAddress]; + return next ?? current; + }, '0'); + + return { + chain: 'TRX', + address, + tokens: [ + { + ...TRX_TOKENS[0], + balanceRaw: nativeRaw.toString(), + balanceFormatted: formatBalance(nativeRaw.toString(), TRX_TOKENS[0].decimals), + priceUsd: null, + valueUsd: null, + }, + { + ...TRX_TOKENS[1], + balanceRaw: usdtRaw, + balanceFormatted: formatBalance(usdtRaw, TRX_TOKENS[1].decimals), + priceUsd: null, + valueUsd: null, + }, + ], + totalUsd: null, + error: null, + }; + } catch (error) { + console.warn(`[TRX] balance fetch failed:`, error); + + return { + chain: 'TRX', + address, + tokens: TRX_TOKENS.map((token) => ({ + ...token, + balanceRaw: '0', + balanceFormatted: '0', + priceUsd: null, + valueUsd: null, + })), + totalUsd: null, + error: getErrorMessage(error), + }; + } +} + +async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { + signal: controller.signal, + cache: 'no-store', + headers: { Accept: 'application/json' }, + }); + + if (!response.ok) { + throw new Error(`TRON API returned ${response.status}`); + } + + return (await response.json()) as T; + } finally { + clearTimeout(timeoutId); + } +} + +function formatBalance(rawValue: string, decimals: number): string { + const bigintValue = BigInt(rawValue || '0'); + + if (bigintValue === 0n) { + return '0'; + } + + const divisor = 10n ** BigInt(decimals); + const whole = bigintValue / divisor; + const fraction = bigintValue % divisor; + + if (fraction === 0n) { + return whole.toString(); + } + + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return 'Unable to load TRX balance'; +} diff --git a/apps/web/src/lib/balances/types.ts b/apps/web/src/lib/balances/types.ts new file mode 100644 index 0000000..1f0fec9 --- /dev/null +++ b/apps/web/src/lib/balances/types.ts @@ -0,0 +1,33 @@ +export type BalanceChain = 'ETH' | 'BTC' | 'SOL' | 'TRX' | 'BSC'; + +export interface TokenDefinition { + chain: BalanceChain; + symbol: string; + decimals: number; + contractAddress: string | 'native'; + coinGeckoId: string; + isNative: boolean; +} + +export interface TokenBalance extends TokenDefinition { + balanceRaw: string; + balanceFormatted: string; + priceUsd: number | null; + valueUsd: number | null; +} + +export interface ChainBalance { + chain: BalanceChain; + address: string; + tokens: TokenBalance[]; + totalUsd: number | null; + error: string | null; +} + +export interface PortfolioBalance { + chains: ChainBalance[]; + totalUsd: number | null; + errors: Partial>; + priceError: string | null; + updatedAt: string; +} diff --git a/apps/web/src/lib/bridge/constants.ts b/apps/web/src/lib/bridge/constants.ts new file mode 100644 index 0000000..fc12ad4 --- /dev/null +++ b/apps/web/src/lib/bridge/constants.ts @@ -0,0 +1,112 @@ +export const RELAY_PROXY_BASE_URL = '/api/relay'; +export const RELAY_REQUEST_TIMEOUT_MS = 15_000; + +// ── Bridge platform fee (0.7%) ── +export const BRIDGE_FEE_BPS = 70; // 0.7% +export const BRIDGE_FEE_RECIPIENT_EVM = '0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718'; +export const BRIDGE_FEE_RECIPIENT_SOL = 'Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ'; +export const BRIDGE_FEE_RECIPIENT_TRX = 'TYTfrem65362TFyQSARTheeYza1GQA37Ug'; + +// ─── Chain types ─── + +export type BridgeChainKey = 'ETH' | 'BSC' | 'SOL' | 'TRX'; + +export interface BridgeCurrencyConfig { + symbol: string; + address: string; + decimals: number; +} + +export interface BridgeChainConfig { + key: BridgeChainKey; + label: string; + chainId: number; + walletChain: 'ETH' | 'BSC' | 'SOL' | 'TRX'; + explorerTxBaseUrl: string; + tokens: Record; +} + +export const BRIDGE_CHAINS: Record = { + ETH: { + key: 'ETH', + label: 'Ethereum', + chainId: 1, + walletChain: 'ETH', + explorerTxBaseUrl: 'https://etherscan.io/tx/', + tokens: { + ETH: { symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18 }, + USDT: { symbol: 'USDT', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 }, + USDC: { symbol: 'USDC', address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 }, + }, + }, + SOL: { + key: 'SOL', + label: 'Solana', + chainId: 792703809, + walletChain: 'SOL', + explorerTxBaseUrl: 'https://solscan.io/tx/', + tokens: { + SOL: { symbol: 'SOL', address: '11111111111111111111111111111111', decimals: 9 }, + USDT: { symbol: 'USDT', address: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', decimals: 6 }, + USDC: { symbol: 'USDC', address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 }, + }, + }, + BSC: { + key: 'BSC', + label: 'BNB Smart Chain', + chainId: 56, + walletChain: 'BSC', + explorerTxBaseUrl: 'https://bscscan.com/tx/', + tokens: { + BNB: { symbol: 'BNB', address: '0x0000000000000000000000000000000000000000', decimals: 18 }, + USDT: { symbol: 'USDT', address: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 }, + }, + }, + TRX: { + key: 'TRX', + label: 'TRON', + chainId: 728126428, + walletChain: 'TRX', + explorerTxBaseUrl: 'https://tronscan.org/#/transaction/', + tokens: { + USDT: { symbol: 'USDT', address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', decimals: 6 }, + }, + }, +}; + +export const BRIDGE_CHAIN_OPTIONS: BridgeChainKey[] = ['ETH', 'BSC', 'SOL', 'TRX']; + +// ─── Helpers ─── + +export function getDestinationChainOptions(sourceChain: BridgeChainKey): BridgeChainKey[] { + return BRIDGE_CHAIN_OPTIONS.filter((c) => c !== sourceChain); +} + +export function getTokenOptions(chainKey: BridgeChainKey): string[] { + return Object.keys(BRIDGE_CHAINS[chainKey].tokens); +} + +export function getDefaultToken(chainKey: BridgeChainKey): string { + const tokens = getTokenOptions(chainKey); + return tokens[0]; +} + +export function getTokenConfig(chainKey: BridgeChainKey, tokenSymbol: string): BridgeCurrencyConfig { + const token = BRIDGE_CHAINS[chainKey].tokens[tokenSymbol]; + if (!token) { + throw new Error(`Token ${tokenSymbol} not found on ${BRIDGE_CHAINS[chainKey].label}`); + } + return token; +} + +// ─── Request type ─── + +export interface BridgeQuoteRequest { + sourceChain: BridgeChainKey; + sourceToken: string; + destChain: BridgeChainKey; + destToken: string; + amount: string; + userAddress: string; + recipientAddress: string; +} diff --git a/apps/web/src/lib/bridge/execute.ts b/apps/web/src/lib/bridge/execute.ts new file mode 100644 index 0000000..a3ab852 --- /dev/null +++ b/apps/web/src/lib/bridge/execute.ts @@ -0,0 +1,482 @@ +import { ethers } from 'ethers'; +import { + Connection, + Keypair, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, + AddressLookupTableAccount, +} from '@solana/web3.js'; +import { createEthProvider } from '@/lib/eth-provider'; +import { webEnv } from '@/lib/env'; +import { BSC_GAS_PRICE } from '@/lib/crypto/bsc-constants'; +import { + BRIDGE_CHAINS, + BRIDGE_FEE_BPS, + BRIDGE_FEE_RECIPIENT_EVM, + BRIDGE_FEE_RECIPIENT_SOL, + BRIDGE_FEE_RECIPIENT_TRX, + RELAY_PROXY_BASE_URL, + RELAY_REQUEST_TIMEOUT_MS, + type BridgeChainKey, +} from './constants'; +import type { RelayQuoteResponse, RelayStep } from './quote'; + +const provider = createEthProvider(); + +// TYTfrem65362TFyQSARTheeYza1GQA37Ug → hex (20 bytes, no 0x prefix) +const BRIDGE_FEE_RECIPIENT_TRX_HEX = 'f6b4d4e650fc67982894f37ba97ab2496781ddb6'; + +interface ExecuteBridgeParams { + sourceChain: BridgeChainKey; + sourceToken: string; + originalAmount: string; + sourceTokenDecimals: number; + sourceTokenAddress: string; + privateKey: string; + quote: RelayQuoteResponse; + maxFeeGwei?: string | null; + priorityFeeGwei?: string | null; +} + +export interface ExecuteBridgeResult { + requestId: string | null; + txHashes: string[]; +} + +export async function executeBridge(params: ExecuteBridgeParams): Promise { + switch (params.sourceChain) { + case 'ETH': + return executeEvmBridge(params, provider); + case 'BSC': + return executeEvmBridge(params, new ethers.providers.StaticJsonRpcProvider(webEnv.bscRpcUrl, 56)); + case 'SOL': + return executeSolBridge(params); + case 'TRX': + return executeTrxBridge(params); + } +} + +// ─── EVM origin (existing logic) ─── + +async function executeEvmBridge( + params: ExecuteBridgeParams, + evmProvider: ethers.providers.Provider, +): Promise { + const wallet = new ethers.Wallet(params.privateKey, evmProvider); + const isBsc = params.sourceChain === 'BSC'; + const txHashes: string[] = []; + let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null; + + // ── Send 0.7% platform fee before bridge ── + await sendEvmBridgeFee(wallet, params, isBsc); + + for (const step of params.quote.steps) { + if (!step.items?.length) continue; + + for (const item of step.items) { + if (step.kind === 'signature') { + await executeSignatureStep(wallet, step, item.data); + } else { + const hash = await executeEvmTransactionStep(wallet, item.data, params.maxFeeGwei, params.priorityFeeGwei, isBsc); + txHashes.push(hash); + } + requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint); + } + } + + return { requestId, txHashes }; +} + +async function executeEvmTransactionStep( + wallet: ethers.Wallet, + data: Record, + maxFeeGwei?: string | null, + priorityFeeGwei?: string | null, + isBsc?: boolean, +): Promise { + const gasOverrides = isBsc + ? { gasPrice: BSC_GAS_PRICE } + : maxFeeGwei?.trim() + ? { + maxFeePerGas: ethers.utils.parseUnits(maxFeeGwei, 'gwei'), + maxPriorityFeePerGas: ethers.utils.parseUnits(priorityFeeGwei?.trim() || '0.01', 'gwei'), + } + : { + ...(data.maxFeePerGas ? { maxFeePerGas: ethers.BigNumber.from(data.maxFeePerGas) } : {}), + ...(data.maxPriorityFeePerGas ? { maxPriorityFeePerGas: ethers.BigNumber.from(data.maxPriorityFeePerGas) } : {}), + }; + + const response = await wallet.sendTransaction({ + to: data.to, + data: data.data, + value: data.value ? ethers.BigNumber.from(data.value) : ethers.constants.Zero, + gasLimit: data.gas ? ethers.BigNumber.from(data.gas) : undefined, + ...gasOverrides, + }); + + const receipt = await response.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error('Bridge transaction reverted'); + } + + return response.hash; +} + +// ─── Bridge Fee Helpers ─── + +async function sendEvmBridgeFee(wallet: ethers.Wallet, params: ExecuteBridgeParams, isBsc?: boolean): Promise { + const fullAmountRaw = ethers.utils.parseUnits(params.originalAmount, params.sourceTokenDecimals); + const feeAmount = fullAmountRaw.mul(BRIDGE_FEE_BPS).div(10000); + if (feeAmount.isZero()) return; + + const gasOverrides = isBsc ? { gasPrice: BSC_GAS_PRICE } : {}; + const isNative = params.sourceTokenAddress === '0x0000000000000000000000000000000000000000'; + + if (isNative) { + const tx = await wallet.sendTransaction({ + to: BRIDGE_FEE_RECIPIENT_EVM, + value: feeAmount, + ...gasOverrides, + }); + await tx.wait(); + } else { + const tokenContract = new ethers.Contract( + params.sourceTokenAddress, + ['function transfer(address to, uint256 amount) returns (bool)'], + wallet, + ); + const tx = await tokenContract.transfer(BRIDGE_FEE_RECIPIENT_EVM, feeAmount, gasOverrides); + await tx.wait(); + } +} + +async function sendSolBridgeFee( + connection: Connection, + keypair: Keypair, + params: ExecuteBridgeParams, +): Promise { + const { SystemProgram } = await import('@solana/web3.js'); + + const fullAmountRaw = BigInt( + Math.round(Number(params.originalAmount) * 10 ** params.sourceTokenDecimals), + ); + const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n; + if (feeAmount === 0n) return; + + const feeRecipient = new PublicKey(BRIDGE_FEE_RECIPIENT_SOL); + const isNative = params.sourceTokenAddress === '11111111111111111111111111111111'; + + // Bridge fee only supports native SOL transfers + // (SOL bridge primarily uses SOL, USDT, USDC — SPL fee handled off-chain if needed) + if (!isNative) return; + + const instruction = SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: feeRecipient, + lamports: feeAmount, + }); + + const latestBlockhash = await connection.getLatestBlockhash('confirmed'); + const messageV0 = new TransactionMessage({ + payerKey: keypair.publicKey, + recentBlockhash: latestBlockhash.blockhash, + instructions: [instruction], + }).compileToV0Message(); + + const tx = new VersionedTransaction(messageV0); + tx.sign([keypair]); + + const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: false }); + await connection.confirmTransaction( + { signature: sig, blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight }, + 'confirmed', + ); +} + +async function sendTrxBridgeFee( + signingKey: ethers.utils.SigningKey, + apiUrl: string, + params: ExecuteBridgeParams, +): Promise { + const decimals = params.sourceTokenDecimals; + const fullAmountRaw = BigInt( + Math.round(Number(params.originalAmount) * 10 ** decimals), + ); + const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n; + if (feeAmount === 0n) return; + + // TRX bridge only supports USDT (TRC-20) — build a TRC20 transfer via API + // Use the tron proxy to build a transfer tx, sign it, and broadcast + const buildResp = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner_address: params.quote.steps[0]?.items?.[0]?.data?.parameter?.owner_address + ?? ethers.utils.computeAddress(signingKey.publicKey).toLowerCase(), + contract_address: params.sourceTokenAddress, + function_selector: 'transfer(address,uint256)', + parameter: + BRIDGE_FEE_RECIPIENT_TRX_HEX.padStart(64, '0') + + feeAmount.toString(16).padStart(64, '0'), + call_value: 0, + fee_limit: 100000000, + visible: false, + }), + }); + + if (!buildResp.ok) return; // Fee transfer is best-effort; don't block bridge + + const buildResult = await buildResp.json(); + const tx = buildResult.transaction; + if (!tx?.txID) return; + + // Sign and broadcast + const digest = ethers.utils.arrayify('0x' + tx.txID); + const signature = signingKey.signDigest(digest); + const sigHex = ethers.utils.joinSignature(signature).slice(2); + + const broadcastResp = await fetch(`${apiUrl}/api/tron/broadcasttransaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...tx, signature: [sigHex] }), + }); + + // Wait for broadcast, but don't fail the bridge if fee transfer fails + await broadcastResp.json(); +} + +async function executeSignatureStep(wallet: ethers.Wallet, step: RelayStep, data: Record): Promise { + const signData = data.sign; + const postData = data.post; + + if (!signData || !postData?.endpoint) { + throw new Error(`Invalid signature step payload for ${step.id}`); + } + + const signature = await signRelayPayload(wallet, signData); + const endpoint = new URL(`${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}${postData.endpoint}`); + endpoint.searchParams.set('signature', signature); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(endpoint.toString(), { + method: postData.method ?? 'POST', + signal: controller.signal, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(postData.body ?? {}), + }); + + if (!response.ok) { + const payload = await response.text(); + throw new Error(payload || 'Relay signature submission failed'); + } + } finally { + clearTimeout(timeoutId); + } +} + +async function signRelayPayload(wallet: ethers.Wallet, signData: Record): Promise { + if (signData.signatureKind === 'eip191') { + const message = typeof signData.message === 'string' && signData.message.startsWith('0x') + ? ethers.utils.arrayify(signData.message) + : signData.message; + return wallet.signMessage(message); + } + + if (signData.signatureKind === 'eip712') { + const { EIP712Domain, ...types } = signData.types ?? {}; + return wallet._signTypedData(signData.domain ?? {}, types, signData.value ?? {}); + } + + throw new Error(`Unsupported Relay signature kind: ${signData.signatureKind}`); +} + +// ─── SOL origin ─── + +async function executeSolBridge(params: ExecuteBridgeParams): Promise { + const connection = new Connection(webEnv.solRpcUrl, 'confirmed'); + const keypair = Keypair.fromSecretKey(Buffer.from(params.privateKey, 'hex')); + const txHashes: string[] = []; + let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null; + + // ── Send 0.7% platform fee before bridge ── + await sendSolBridgeFee(connection, keypair, params); + + for (const step of params.quote.steps) { + if (!step.items?.length) continue; + + for (const item of step.items) { + const data = item.data; + + if (!data.instructions || !Array.isArray(data.instructions)) { + throw new Error('Expected Solana instructions in bridge step'); + } + + const hash = await executeSolTransactionStep(connection, keypair, data); + txHashes.push(hash); + requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint); + } + } + + return { requestId, txHashes }; +} + +async function executeSolTransactionStep( + connection: Connection, + keypair: Keypair, + data: Record, +): Promise { + // Build instructions from Relay response + const instructions: TransactionInstruction[] = data.instructions.map((ix: any) => ({ + programId: new PublicKey(ix.programId), + keys: ix.keys.map((k: any) => ({ + pubkey: new PublicKey(k.pubkey), + isSigner: k.isSigner, + isWritable: k.isWritable, + })), + data: Buffer.from(ix.data, 'hex'), + })); + + // Load address lookup tables + const lookupTableAddresses: string[] = data.addressLookupTableAddresses ?? []; + const lookupTables: AddressLookupTableAccount[] = []; + + for (const addr of lookupTableAddresses) { + const account = await connection.getAddressLookupTable(new PublicKey(addr)); + if (account.value) { + lookupTables.push(account.value); + } + } + + // Build versioned transaction + const latestBlockhash = await connection.getLatestBlockhash('confirmed'); + const messageV0 = new TransactionMessage({ + payerKey: keypair.publicKey, + recentBlockhash: latestBlockhash.blockhash, + instructions, + }).compileToV0Message(lookupTables); + + const transaction = new VersionedTransaction(messageV0); + transaction.sign([keypair]); + + // Send and confirm + const signature = await connection.sendRawTransaction(transaction.serialize(), { + skipPreflight: false, + maxRetries: 2, + }); + + await connection.confirmTransaction( + { + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, + 'confirmed', + ); + + return signature; +} + +// ─── TRX origin ─── + +async function executeTrxBridge(params: ExecuteBridgeParams): Promise { + const signingKey = new ethers.utils.SigningKey('0x' + params.privateKey); + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const txHashes: string[] = []; + let requestId = params.quote.steps.find((s) => s.requestId)?.requestId ?? null; + + // ── Send 0.7% platform fee before bridge ── + await sendTrxBridgeFee(signingKey, apiUrl, params); + + for (const step of params.quote.steps) { + if (!step.items?.length) continue; + + for (const item of step.items) { + const data = item.data; + + if (data.type !== 'TriggerSmartContract') { + throw new Error(`Unsupported TRX step type: ${data.type}`); + } + + const hash = await executeTrxTransactionStep(signingKey, apiUrl, data); + txHashes.push(hash); + requestId = requestId ?? step.requestId ?? extractRequestId(item.check?.endpoint); + } + + // Small delay between steps (e.g., approve → deposit) + if (step.items.length > 0 && step !== params.quote.steps[params.quote.steps.length - 1]) { + await delay(3000); + } + } + + return { requestId, txHashes }; +} + +async function executeTrxTransactionStep( + signingKey: ethers.utils.SigningKey, + apiUrl: string, + data: Record, +): Promise { + // 1. Build transaction via TronGrid triggersmartcontract + const buildResponse = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data.parameter), + }); + + if (!buildResponse.ok) { + const body = await buildResponse.json().catch(() => ({ error: 'Failed to build TRX bridge tx' })); + throw new Error(body.error || `TRX bridge build failed (${buildResponse.status})`); + } + + const buildResult = await buildResponse.json(); + const tx = buildResult.transaction; + if (!tx?.txID) { + throw new Error('TronGrid did not return a valid transaction'); + } + + // 2. Sign txID with secp256k1 + const digest = ethers.utils.arrayify('0x' + tx.txID); + const signature = signingKey.signDigest(digest); + const sigHex = ethers.utils.joinSignature(signature).slice(2); // 65 bytes hex, no 0x + + const signedTx = { + ...tx, + signature: [sigHex], + }; + + // 3. Broadcast + const broadcastResponse = await fetch(`${apiUrl}/api/tron/broadcasttransaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(signedTx), + }); + + const result = await broadcastResponse.json(); + if (!result.result) { + const errorMsg = result.message || result.code || 'TRX broadcast failed'; + throw new Error(`TRX bridge broadcast error: ${errorMsg}`); + } + + return tx.txID; +} + +// ─── Utils ─── + +function extractRequestId(endpoint?: string): string | null { + if (!endpoint) return null; + try { + const url = new URL(endpoint, 'https://api.relay.link'); + return url.searchParams.get('requestId'); + } catch { + return null; + } +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/apps/web/src/lib/bridge/quote.ts b/apps/web/src/lib/bridge/quote.ts new file mode 100644 index 0000000..f7bba6a --- /dev/null +++ b/apps/web/src/lib/bridge/quote.ts @@ -0,0 +1,184 @@ +import { ethers } from 'ethers'; +import { webEnv } from '@/lib/env'; +import { + BRIDGE_CHAINS, + BRIDGE_FEE_BPS, + RELAY_PROXY_BASE_URL, + RELAY_REQUEST_TIMEOUT_MS, + getTokenConfig, + type BridgeQuoteRequest, +} from './constants'; + +export interface RelayStep { + id: string; + kind: 'transaction' | 'signature'; + requestId?: string; + items: Array<{ + status: 'complete' | 'incomplete'; + data: Record; + check?: { + endpoint: string; + method: 'GET' | 'POST'; + }; + }>; +} + +export interface RelayQuoteResponse { + steps: RelayStep[]; + fees?: Record; + details?: { + timeEstimate?: number; + currencyOut?: { + currency?: { + symbol?: string; + decimals?: number; + }; + amount?: string; + amountFormatted?: string; + }; + totalImpact?: { + usd?: string; + percent?: string; + }; + slippageTolerance?: { + destination?: { + percent?: string; + }; + }; + }; +} + +export interface BridgeQuoteResult { + quote: RelayQuoteResponse; + sourceChain: string; + requestId: string | null; + outputAmountFormatted: string; + outputSymbol: string; + minimumAmountFormatted: string; + feeSummary: string; + timeEstimateSeconds: number | null; +} + +export async function getBridgeQuote(request: BridgeQuoteRequest): Promise { + if (!request.amount || Number(request.amount) <= 0) { + throw new Error('Enter a valid bridge amount'); + } + + const sourceChainConfig = BRIDGE_CHAINS[request.sourceChain]; + const destChainConfig = BRIDGE_CHAINS[request.destChain]; + const sourceTokenConfig = getTokenConfig(request.sourceChain, request.sourceToken); + const destTokenConfig = getTokenConfig(request.destChain, request.destToken); + + // Apply 0.7% platform fee — bridge only 99.3% of input + const fullAmountRaw = BigInt(parseAmountToRaw(request.amount, sourceTokenConfig.decimals)); + const feeAmount = (fullAmountRaw * BigInt(BRIDGE_FEE_BPS)) / 10000n; + const amount = (fullAmountRaw - feeAmount).toString(); + + const quote = await fetchRelayJson( + `${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}/quote/v2`, + { + method: 'POST', + body: JSON.stringify({ + user: request.userAddress, + recipient: request.recipientAddress, + originChainId: sourceChainConfig.chainId, + destinationChainId: destChainConfig.chainId, + originCurrency: sourceTokenConfig.address, + destinationCurrency: destTokenConfig.address, + amount, + tradeType: 'EXACT_INPUT', + }), + }, + ); + + const requestId = quote.steps.find((step) => step.requestId)?.requestId ?? null; + const currencyOut = quote.details?.currencyOut; + + return { + quote, + sourceChain: request.sourceChain, + requestId, + outputAmountFormatted: currencyOut?.amountFormatted ?? 'Unavailable', + outputSymbol: currencyOut?.currency?.symbol ?? destTokenConfig.symbol, + minimumAmountFormatted: computeMinimumAmount(currencyOut, destTokenConfig.decimals), + feeSummary: buildFeeSummary(quote.fees), + timeEstimateSeconds: quote.details?.timeEstimate ?? null, + }; +} + +async function fetchRelayJson(url: string, options: RequestInit): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + 'Content-Type': 'application/json', + ...(options.headers ?? {}), + }, + }); + + const payload = (await response.json()) as T & { message?: string }; + if (!response.ok) { + throw new Error((payload as { message?: string }).message || 'Relay quote request failed'); + } + + return payload; + } finally { + clearTimeout(timeoutId); + } +} + +function buildFeeSummary(fees: RelayQuoteResponse['fees']): string { + if (!fees) return 'Unavailable'; + + const usdTotal = Object.values(fees).reduce((total, fee) => { + const amountUsd = Number(fee.amountUsd ?? 0); + return Number.isFinite(amountUsd) ? total + amountUsd : total; + }, 0); + + if (usdTotal > 0) return `$${usdTotal.toFixed(4)}`; + + const relayerFee = fees.relayer; + if (relayerFee?.amountFormatted && relayerFee.currency?.symbol) { + return `${relayerFee.amountFormatted} ${relayerFee.currency.symbol}`; + } + + return 'Unavailable'; +} + +function computeMinimumAmount( + currencyOut: NonNullable['currencyOut'] | undefined, + decimals: number, +): string { + if (!currencyOut?.amount || !currencyOut.currency?.decimals) { + return 'Unavailable'; + } + + // Apply 2% slippage to displayed minimum + const raw = BigInt(currencyOut.amount); + const minimum = (raw * 98n) / 100n; + return formatRawUnits(minimum.toString(), currencyOut.currency.decimals); +} + +function parseAmountToRaw(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + const raw = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction); + return raw.toString(); +} + +function formatRawUnits(raw: string, decimals: number): string { + const value = BigInt(raw); + if (value === 0n) return '0'; + + const divisor = 10n ** BigInt(decimals); + const whole = value / divisor; + const fraction = value % divisor; + + if (fraction === 0n) return whole.toString(); + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} diff --git a/apps/web/src/lib/bridge/status.ts b/apps/web/src/lib/bridge/status.ts new file mode 100644 index 0000000..cd84bfe --- /dev/null +++ b/apps/web/src/lib/bridge/status.ts @@ -0,0 +1,40 @@ +import { webEnv } from '@/lib/env'; +import { RELAY_PROXY_BASE_URL, RELAY_REQUEST_TIMEOUT_MS } from './constants'; + +export interface BridgeStatusResult { + status: 'waiting' | 'pending' | 'submitted' | 'success' | 'delayed' | 'refunded' | 'failure'; + details?: string; + inTxHashes?: string[]; + txHashes?: string[]; + updatedAt?: number; + originChainId?: number; + destinationChainId?: number; +} + +export async function getBridgeStatus(requestId: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), RELAY_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch( + `${webEnv.apiUrl}${RELAY_PROXY_BASE_URL}/intents/status/v3?requestId=${encodeURIComponent(requestId)}`, + { + signal: controller.signal, + cache: 'no-store', + } + ); + + const payload = (await response.json()) as BridgeStatusResult & { message?: string }; + if (!response.ok) { + throw new Error(payload.message || 'Unable to fetch bridge status'); + } + + return payload; + } finally { + clearTimeout(timeoutId); + } +} + +export function isBridgeTerminalStatus(status: BridgeStatusResult['status']): boolean { + return status === 'success' || status === 'failure' || status === 'refunded'; +} diff --git a/apps/web/src/lib/crypto/bsc-constants.ts b/apps/web/src/lib/crypto/bsc-constants.ts new file mode 100644 index 0000000..39a5e4e --- /dev/null +++ b/apps/web/src/lib/crypto/bsc-constants.ts @@ -0,0 +1,4 @@ +import { ethers } from 'ethers'; + +/** Fixed gas price for all BSC transactions (swaps & sends) */ +export const BSC_GAS_PRICE = ethers.utils.parseUnits('0.055', 'gwei'); diff --git a/apps/web/src/lib/crypto/btc.ts b/apps/web/src/lib/crypto/btc.ts new file mode 100644 index 0000000..d0acdab --- /dev/null +++ b/apps/web/src/lib/crypto/btc.ts @@ -0,0 +1,17 @@ +import { HDKey } from '@scure/bip32'; +import * as bitcoin from 'bitcoinjs-lib'; + +export function deriveBtcWallet(seed: Uint8Array) { + const root = HDKey.fromMasterSeed(seed); + const child = root.derive("m/84'/0'/0'/0/0"); + + const { address } = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(child.publicKey!), + network: bitcoin.networks.bitcoin, + }); + + return { + address: address!, + privateKey: Buffer.from(child.privateKey!).toString('hex'), + }; +} diff --git a/apps/web/src/lib/crypto/eth.ts b/apps/web/src/lib/crypto/eth.ts new file mode 100644 index 0000000..50598cd --- /dev/null +++ b/apps/web/src/lib/crypto/eth.ts @@ -0,0 +1,9 @@ +import { ethers } from 'ethers'; + +export function deriveEthWallet(mnemonicPhrase: string) { + const wallet = ethers.Wallet.fromMnemonic(mnemonicPhrase, "m/44'/60'/0'/0/0"); + return { + address: wallet.address, + privateKey: wallet.privateKey, + }; +} diff --git a/apps/web/src/lib/crypto/mnemonic.ts b/apps/web/src/lib/crypto/mnemonic.ts new file mode 100644 index 0000000..15668c8 --- /dev/null +++ b/apps/web/src/lib/crypto/mnemonic.ts @@ -0,0 +1,14 @@ +import { generateMnemonic as genMnemonic, mnemonicToSeed, validateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english.js'; + +export function generateMnemonic(): string { + return genMnemonic(wordlist, 128); +} + +export async function mnemonicToSeedBytes(mnemonic: string): Promise { + return mnemonicToSeed(mnemonic); +} + +export function isValidMnemonic(mnemonic: string): boolean { + return validateMnemonic(mnemonic, wordlist); +} diff --git a/apps/web/src/lib/crypto/sol.ts b/apps/web/src/lib/crypto/sol.ts new file mode 100644 index 0000000..6cba460 --- /dev/null +++ b/apps/web/src/lib/crypto/sol.ts @@ -0,0 +1,13 @@ +import { Keypair } from '@solana/web3.js'; +import { derivePath } from 'ed25519-hd-key'; + +export function deriveSolWallet(seed: Uint8Array) { + const path = "m/44'/501'/0'/0'"; + const derived = derivePath(path, Buffer.from(seed).toString('hex')); + const keypair = Keypair.fromSeed(derived.key); + + return { + address: keypair.publicKey.toBase58(), + privateKey: Buffer.from(keypair.secretKey).toString('hex'), + }; +} diff --git a/apps/web/src/lib/crypto/trx.ts b/apps/web/src/lib/crypto/trx.ts new file mode 100644 index 0000000..1bd65c7 --- /dev/null +++ b/apps/web/src/lib/crypto/trx.ts @@ -0,0 +1,58 @@ +import { ethers } from 'ethers'; + +export function deriveTrxWallet(mnemonicPhrase: string) { + const hdNode = ethers.utils.HDNode.fromMnemonic(mnemonicPhrase).derivePath("m/44'/195'/0'/0/0"); + const ethAddress = ethers.utils.computeAddress(hdNode.publicKey); + const address = ethToTronAddress(ethAddress); + + return { + address, + privateKey: hdNode.privateKey.slice(2), + }; +} + +function ethToTronAddress(ethAddress: string): string { + const hex = '41' + ethAddress.slice(2); + return hexToBase58Check(hex); +} + +function hexToBase58Check(hex: string): string { + const bytes = hexToBytes(hex); + const hash1 = sha256Sync(bytes); + const hash2 = sha256Sync(hash1); + const checksum = hash2.slice(0, 4); + const payload = new Uint8Array(bytes.length + 4); + payload.set(bytes); + payload.set(checksum, bytes.length); + return base58Encode(payload); +} + +function hexToBytes(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; +} + +function sha256Sync(data: Uint8Array): Uint8Array { + const { createHash } = require('crypto'); + return new Uint8Array(createHash('sha256').update(data).digest()); +} + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +function base58Encode(data: Uint8Array): string { + let num = BigInt('0x' + Array.from(data).map(b => b.toString(16).padStart(2, '0')).join('')); + let result = ''; + while (num > 0n) { + const mod = Number(num % 58n); + result = BASE58_ALPHABET[mod] + result; + num = num / 58n; + } + for (const byte of data) { + if (byte === 0) result = '1' + result; + else break; + } + return result; +} diff --git a/apps/web/src/lib/env.ts b/apps/web/src/lib/env.ts new file mode 100644 index 0000000..067c5cb --- /dev/null +++ b/apps/web/src/lib/env.ts @@ -0,0 +1,16 @@ +function readEnv(name: string, fallback: string): string { + const value = process.env[name]; + return value && value.trim() ? value : fallback; +} + +function readUrlEnv(name: string, fallback: string): string { + return readEnv(name, fallback).replace(/\/+$/, ''); +} + +export const webEnv = { + apiUrl: readUrlEnv('NEXT_PUBLIC_API_URL', 'http://localhost:3001'), + ethRpcUrl: readUrlEnv('NEXT_PUBLIC_ETH_RPC_URL', 'https://ethereum-rpc.publicnode.com'), + solRpcUrl: readUrlEnv('NEXT_PUBLIC_SOL_RPC_URL', 'https://solana.publicnode.com'), + btcApiUrl: readUrlEnv('NEXT_PUBLIC_BTC_API_URL', 'https://blockstream.info/api'), + bscRpcUrl: readUrlEnv('NEXT_PUBLIC_BSC_RPC_URL', 'https://bsc-dataseed.binance.org'), +} as const; diff --git a/apps/web/src/lib/eth-provider.ts b/apps/web/src/lib/eth-provider.ts new file mode 100644 index 0000000..8fb5a65 --- /dev/null +++ b/apps/web/src/lib/eth-provider.ts @@ -0,0 +1,25 @@ +import { ethers } from 'ethers'; +import { webEnv } from '@/lib/env'; + +const MAINNET_NETWORK = { chainId: 1, name: 'mainnet' } as const; + +const ETH_RPC_FALLBACKS = [ + 'https://ethereum-rpc.publicnode.com', + 'https://rpc.ankr.com/eth', + 'https://eth.llamarpc.com', +]; + +function getEthRpcUrls(): string[] { + return [...new Set([webEnv.ethRpcUrl, ...ETH_RPC_FALLBACKS].map((url) => url.trim()).filter(Boolean))]; +} + +export function createEthProvider(): ethers.providers.FallbackProvider { + const providerConfigs = getEthRpcUrls().map((url, index) => ({ + provider: new ethers.providers.StaticJsonRpcProvider(url, MAINNET_NETWORK), + priority: index + 1, + weight: 1, + stallTimeout: 1_200, + })); + + return new ethers.providers.FallbackProvider(providerConfigs, 1); +} diff --git a/apps/web/src/lib/gas-price.ts b/apps/web/src/lib/gas-price.ts new file mode 100644 index 0000000..f39552d --- /dev/null +++ b/apps/web/src/lib/gas-price.ts @@ -0,0 +1,66 @@ +import { ethers } from 'ethers'; +import { createEthProvider } from '@/lib/eth-provider'; + +const FETCH_TIMEOUT_MS = 8_000; + +// Fixed small priority tips (gwei) — just enough to get included +const PRIORITY_FEE: Record<'slow' | 'normal' | 'fast', number> = { + slow: 0.01, + normal: 0.015, + fast: 0.03, +}; + +export interface GasTier { + maxFeePerGas: number; + maxPriorityFeePerGas: number; + confidence: number; +} + +export interface GasPriceData { + baseFeeGwei: number; + slow: GasTier; + normal: GasTier; + fast: GasTier; +} + +export async function fetchGasPrices(): Promise { + const provider = createEthProvider(); + + const feeData = await withTimeout( + provider.getFeeData(), + FETCH_TIMEOUT_MS, + 'ETH fee data request timed out', + ); + + if (!feeData.lastBaseFeePerGas) { + throw new Error('Could not get base fee from ETH RPC'); + } + + // Convert wei → gwei as float + const baseFeeGwei = parseFloat(ethers.utils.formatUnits(feeData.lastBaseFeePerGas, 'gwei')); + + function buildTier(mode: 'slow' | 'normal' | 'fast', confidence: number): GasTier { + const priority = PRIORITY_FEE[mode]; + return { + maxFeePerGas: baseFeeGwei + priority, + maxPriorityFeePerGas: priority, + confidence, + }; + } + + return { + baseFeeGwei, + slow: buildTier('slow', 70), + normal: buildTier('normal', 90), + fast: buildTier('fast', 99), + }; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + promise + .then((value) => { clearTimeout(timeoutId); resolve(value); }) + .catch((error) => { clearTimeout(timeoutId); reject(error); }); + }); +} diff --git a/apps/web/src/lib/qr/generate.ts b/apps/web/src/lib/qr/generate.ts new file mode 100644 index 0000000..a84732d --- /dev/null +++ b/apps/web/src/lib/qr/generate.ts @@ -0,0 +1,146 @@ +import { SEND_CHAINS, getDefaultToken, type SendChain } from '@/lib/send/constants'; + +/** Safely resolve token config, falling back to chain's native token */ +function resolveToken(chain: SendChain, token: string) { + const chainCfg = SEND_CHAINS[chain]; + return chainCfg.tokens[token] ?? chainCfg.tokens[getDefaultToken(chain)]; +} + +export interface GenerateQrParams { + chain: SendChain; + token: string; + address: string; + amount?: string; +} + +/** + * Generate a standard URI for QR code encoding. + * + * Formats: + * - ETH native: ethereum:
[?value=] + * - ETH ERC20: ethereum:/transfer?address=&uint256= + * - SOL native: solana:
[?amount=] + * - SOL SPL: solana:
?spl-token=[&amount=] + * - TRX native: tron:
[?amount=] + * - TRX TRC20: tron:
?token=[&amount=] + * - BTC: bitcoin:
[?amount=] + */ +export function generateReceiveUri(params: GenerateQrParams): string { + const { chain, token, address, amount } = params; + + switch (chain) { + case 'ETH': + return generateEthUri(address, token, amount); + case 'SOL': + return generateSolUri(address, token, amount); + case 'TRX': + return generateTrxUri(address, token, amount); + case 'BTC': + return generateBtcUri(address, amount); + case 'BSC': + return generateBscUri(address, token, amount); + } +} + +// ─── ETH (EIP-681) ─── + +function generateEthUri(address: string, token: string, amount?: string): string { + const tokenCfg = resolveToken('ETH', token); + + // Native ETH + if (!tokenCfg.contractAddress) { + if (amount && Number(amount) > 0) { + const wei = toRawUnits(amount, tokenCfg.decimals); + return `ethereum:${address}?value=${wei}`; + } + return `ethereum:${address}`; + } + + // ERC20 — ethereum:/transfer?address=&uint256= + const base = `ethereum:${tokenCfg.contractAddress}/transfer?address=${address}`; + if (amount && Number(amount) > 0) { + const raw = toRawUnits(amount, tokenCfg.decimals); + return `${base}&uint256=${raw}`; + } + return base; +} + +// ─── SOL (Solana Pay) ─── + +function generateSolUri(address: string, token: string, amount?: string): string { + const tokenCfg = resolveToken('SOL', token); + + const params = new URLSearchParams(); + + // SPL token + if (tokenCfg.contractAddress) { + params.set('spl-token', tokenCfg.contractAddress); + } + + if (amount && Number(amount) > 0) { + params.set('amount', amount); + } + + const qs = params.toString(); + return `solana:${address}${qs ? '?' + qs : ''}`; +} + +// ─── TRX ─── + +function generateTrxUri(address: string, token: string, amount?: string): string { + const tokenCfg = resolveToken('TRX', token); + + const params = new URLSearchParams(); + + if (tokenCfg.contractAddress) { + params.set('token', tokenCfg.contractAddress); + } + + if (amount && Number(amount) > 0) { + params.set('amount', amount); + } + + const qs = params.toString(); + return `tron:${address}${qs ? '?' + qs : ''}`; +} + +// ─── BTC (BIP-21) ─── + +function generateBtcUri(address: string, amount?: string): string { + if (amount && Number(amount) > 0) { + return `bitcoin:${address}?amount=${amount}`; + } + return `bitcoin:${address}`; +} + +// ─── BSC (EIP-681 with @56 chain discriminator) ─── + +function generateBscUri(address: string, token: string, amount?: string): string { + const tokenCfg = resolveToken('BSC', token); + + // Native BNB + if (!tokenCfg.contractAddress) { + if (amount && Number(amount) > 0) { + const wei = toRawUnits(amount, tokenCfg.decimals); + return `ethereum:${address}@56?value=${wei}`; + } + return `ethereum:${address}@56`; + } + + // BEP-20 — ethereum:@56/transfer?address=&uint256= + const base = `ethereum:${tokenCfg.contractAddress}@56/transfer?address=${address}`; + if (amount && Number(amount) > 0) { + const raw = toRawUnits(amount, tokenCfg.decimals); + return `${base}&uint256=${raw}`; + } + return base; +} + +// ─── Utils ─── + +function toRawUnits(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + return (BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction)).toString(); +} diff --git a/apps/web/src/lib/qr/parse.ts b/apps/web/src/lib/qr/parse.ts new file mode 100644 index 0000000..c7c0436 --- /dev/null +++ b/apps/web/src/lib/qr/parse.ts @@ -0,0 +1,203 @@ +import { CONTRACT_TO_SYMBOL, type SendChain } from '@/lib/send/constants'; +import { validateAddress, detectChainFromAddress } from '@/lib/send/validate'; + +export interface ParsedQrResult { + chain: SendChain | null; + token: string; + address: string; + amount: string | null; +} + +/** + * Parse a QR URI into chain, token, address, and optional amount. + * + * Supports: + * - ethereum:
?value= + * - ethereum:/transfer?address=&uint256= + * - solana:
?amount=&spl-token= + * - tron:
?amount=&token= + * - bitcoin:
?amount= + * - Raw addresses (auto-detect chain) + */ +export function parseQrUri(uri: string): ParsedQrResult { + const trimmed = uri.trim(); + + // Detect scheme + if (trimmed.startsWith('ethereum:')) return parseEthUri(trimmed); + if (trimmed.startsWith('solana:')) return parseSolUri(trimmed); + if (trimmed.startsWith('tron:')) return parseTrxUri(trimmed); + if (trimmed.startsWith('bitcoin:')) return parseBtcUri(trimmed); + + // No scheme — try to detect chain from raw address + return parseRawAddress(trimmed); +} + +// ─── Ethereum (EIP-681) — also handles BSC via @56 chain discriminator ─── + +function parseEthUri(uri: string): ParsedQrResult { + const withoutScheme = uri.slice('ethereum:'.length); + + // Detect chain from @chainId discriminator + const isBsc = withoutScheme.includes('@56'); + const chain: SendChain = isBsc ? 'BSC' : 'ETH'; + const nativeToken = isBsc ? 'BNB' : 'ETH'; + const nativeDecimals = 18; + + // Strip @chainId from the URI for easier parsing + const cleaned = withoutScheme.replace(/@56/g, '').replace(/@1/g, ''); + + // Check for ERC20/BEP20 transfer: /transfer?address=&uint256= + const transferMatch = cleaned.match(/^(0x[0-9a-fA-F]{40})\/transfer\?(.+)$/); + if (transferMatch) { + const contract = transferMatch[1]; + const params = new URLSearchParams(transferMatch[2]); + const toAddress = params.get('address') || ''; + const rawAmount = params.get('uint256'); + + const known = CONTRACT_TO_SYMBOL[contract.toLowerCase()]; + const token = known?.symbol ?? nativeToken; + const decimals = known ? getDecimalsForSymbol(token) : nativeDecimals; + + return { + chain, + token, + address: toAddress, + amount: rawAmount ? fromRawUnits(rawAmount, decimals) : null, + }; + } + + // Native transfer:
?value= + const [addressPart, queryPart] = cleaned.split('?'); + const params = queryPart ? new URLSearchParams(queryPart) : null; + const weiValue = params?.get('value'); + + return { + chain, + token: nativeToken, + address: addressPart || '', + amount: weiValue ? fromRawUnits(weiValue, nativeDecimals) : null, + }; +} + +function getDecimalsForSymbol(symbol: string): number { + const decimalsMap: Record = { + USDT: 6, USDC: 6, XAUT: 6, DOGE: 8, + }; + return decimalsMap[symbol] ?? 18; +} + +// ─── Solana (Solana Pay) ─── + +function parseSolUri(uri: string): ParsedQrResult { + const withoutScheme = uri.slice('solana:'.length); + const [addressPart, queryPart] = withoutScheme.split('?'); + const params = queryPart ? new URLSearchParams(queryPart) : null; + + const splToken = params?.get('spl-token'); + const amount = params?.get('amount'); + + let token = 'SOL'; + if (splToken) { + const known = CONTRACT_TO_SYMBOL[splToken]; + token = known?.symbol ?? 'SOL'; + } + + return { + chain: 'SOL', + token, + address: addressPart || '', + amount: amount || null, + }; +} + +// ─── TRON ─── + +function parseTrxUri(uri: string): ParsedQrResult { + const withoutScheme = uri.slice('tron:'.length); + const [addressPart, queryPart] = withoutScheme.split('?'); + const params = queryPart ? new URLSearchParams(queryPart) : null; + + const tokenContract = params?.get('token'); + const amount = params?.get('amount'); + + let token = 'TRX'; + if (tokenContract) { + const known = CONTRACT_TO_SYMBOL[tokenContract]; + token = known?.symbol ?? 'TRX'; + } + + return { + chain: 'TRX', + token, + address: addressPart || '', + amount: amount || null, + }; +} + +// ─── Bitcoin (BIP-21) ─── + +function parseBtcUri(uri: string): ParsedQrResult { + const withoutScheme = uri.slice('bitcoin:'.length); + const [addressPart, queryPart] = withoutScheme.split('?'); + const params = queryPart ? new URLSearchParams(queryPart) : null; + + return { + chain: 'BTC', + token: 'BTC', + address: addressPart || '', + amount: params?.get('amount') || null, + }; +} + +// ─── Raw address (no scheme) ─── + +function parseRawAddress(address: string): ParsedQrResult { + const chain = detectChainFromAddress(address); + + if (chain) { + const validation = validateAddress(chain, address); + if (validation.valid) { + // Default to native token + const nativeTokens: Record = { + ETH: 'ETH', + SOL: 'SOL', + TRX: 'TRX', + BTC: 'BTC', + BSC: 'BNB', + }; + + return { + chain, + token: nativeTokens[chain], + address, + amount: null, + }; + } + } + + // Could not detect or validate + return { + chain: null, + token: '', + address, + amount: null, + }; +} + +// ─── Utils ─── + +function fromRawUnits(raw: string, decimals: number): string { + try { + const value = BigInt(raw); + if (value === 0n) return '0'; + + const divisor = 10n ** BigInt(decimals); + const whole = value / divisor; + const fraction = value % divisor; + + if (fraction === 0n) return whole.toString(); + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; + } catch { + return raw; + } +} diff --git a/apps/web/src/lib/send/constants.ts b/apps/web/src/lib/send/constants.ts new file mode 100644 index 0000000..805878a --- /dev/null +++ b/apps/web/src/lib/send/constants.ts @@ -0,0 +1,136 @@ +export type SendChain = 'ETH' | 'SOL' | 'TRX' | 'BTC' | 'BSC'; + +export interface SendTokenConfig { + symbol: string; + contractAddress: string | null; // null = native + decimals: number; +} + +export interface SendChainConfig { + key: SendChain; + label: string; + walletChain: string; // auth-store chain key + tokens: Record; + explorerTxUrl: string; +} + +export const SEND_CHAINS: Record = { + ETH: { + key: 'ETH', + label: 'Ethereum', + walletChain: 'ETH', + tokens: { + ETH: { symbol: 'ETH', contractAddress: null, decimals: 18 }, + USDT: { symbol: 'USDT', contractAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6 }, + USDC: { symbol: 'USDC', contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', decimals: 6 }, + 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 }, + }, + explorerTxUrl: 'https://etherscan.io/tx/', + }, + SOL: { + key: 'SOL', + label: 'Solana', + walletChain: 'SOL', + tokens: { + SOL: { symbol: 'SOL', contractAddress: null, decimals: 9 }, + USDT: { symbol: 'USDT', contractAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', decimals: 6 }, + USDC: { symbol: 'USDC', contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 }, + PUMP: { symbol: 'PUMP', contractAddress: 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn', decimals: 6 }, + JUP: { symbol: 'JUP', contractAddress: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', decimals: 6 }, + WIF: { symbol: 'WIF', contractAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', decimals: 6 }, + POPCAT: { symbol: 'POPCAT', contractAddress: '7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr', decimals: 9 }, + TRUMP: { symbol: 'TRUMP', contractAddress: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', decimals: 6 }, + PYTH: { symbol: 'PYTH', contractAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', decimals: 6 }, + JTO: { symbol: 'JTO', contractAddress: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', decimals: 9 }, + W: { symbol: 'W', contractAddress: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ', decimals: 6 }, + BONK: { symbol: 'BONK', contractAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', decimals: 5 }, + ORCA: { symbol: 'ORCA', contractAddress: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', decimals: 6 }, + PENGU: { symbol: 'PENGU', contractAddress: '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv', decimals: 6 }, + RAY: { symbol: 'RAY', contractAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', decimals: 6 }, + }, + explorerTxUrl: 'https://solscan.io/tx/', + }, + TRX: { + key: 'TRX', + label: 'TRON', + walletChain: 'TRX', + tokens: { + TRX: { symbol: 'TRX', contractAddress: null, decimals: 6 }, + USDT: { symbol: 'USDT', contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', decimals: 6 }, + }, + explorerTxUrl: 'https://tronscan.org/#/transaction/', + }, + BTC: { + key: 'BTC', + label: 'Bitcoin', + walletChain: 'BTC', + tokens: { + BTC: { symbol: 'BTC', contractAddress: null, decimals: 8 }, + }, + explorerTxUrl: 'https://blockstream.info/tx/', + }, + BSC: { + key: 'BSC', + label: 'BNB Smart Chain', + walletChain: 'BSC', + tokens: { + BNB: { symbol: 'BNB', contractAddress: null, decimals: 18 }, + USDT: { symbol: 'USDT', contractAddress: '0x55d398326f99059fF775485246999027B3197955', decimals: 18 }, + DOGE: { symbol: 'DOGE', contractAddress: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43', decimals: 8 }, + }, + explorerTxUrl: 'https://bscscan.com/tx/', + }, +}; + +export const SEND_CHAIN_OPTIONS: SendChain[] = ['ETH', 'SOL', 'TRX', 'BTC', 'BSC']; + +export function getTokenOptions(chain: SendChain): string[] { + return Object.keys(SEND_CHAINS[chain].tokens); +} + +export function getDefaultToken(chain: SendChain): string { + return getTokenOptions(chain)[0]; +} + +export function getTokenConfig(chain: SendChain, token: string): SendTokenConfig { + const cfg = SEND_CHAINS[chain].tokens[token]; + if (!cfg) throw new Error(`Token ${token} not found on ${chain}`); + return cfg; +} + +/** Map known contract addresses back to token symbols */ +export const CONTRACT_TO_SYMBOL: Record = { + // ETH + '0xdac17f958d2ee523a2206206994597c13d831ec7': { chain: 'ETH', symbol: 'USDT' }, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { chain: 'ETH', symbol: 'USDC' }, + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84': { chain: 'ETH', symbol: 'stETH' }, + '0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce': { chain: 'ETH', symbol: 'SHIB' }, + '0x514910771af9ca656af840dff83e8264ecf986ca': { chain: 'ETH', symbol: 'LINK' }, + '0x455e53cbb86018ac2b8092fdcd39d8444affc3f6': { chain: 'ETH', symbol: 'POL' }, + '0x66f85e3865d0cfdc009acf6280a8621f12e46ccf': { chain: 'ETH', symbol: 'WLFI' }, + '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9': { chain: 'ETH', symbol: 'AAVE' }, + // SOL + 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB': { chain: 'SOL', symbol: 'USDT' }, + 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v': { chain: 'SOL', symbol: 'USDC' }, + 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn': { chain: 'SOL', symbol: 'PUMP' }, + 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN': { chain: 'SOL', symbol: 'JUP' }, + 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm': { chain: 'SOL', symbol: 'WIF' }, + '7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr': { chain: 'SOL', symbol: 'POPCAT' }, + '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN': { chain: 'SOL', symbol: 'TRUMP' }, + 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3': { chain: 'SOL', symbol: 'PYTH' }, + 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL': { chain: 'SOL', symbol: 'JTO' }, + '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ': { chain: 'SOL', symbol: 'W' }, + 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263': { chain: 'SOL', symbol: 'BONK' }, + 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE': { chain: 'SOL', symbol: 'ORCA' }, + '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv': { chain: 'SOL', symbol: 'PENGU' }, + '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R': { chain: 'SOL', symbol: 'RAY' }, + // TRX + 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t': { chain: 'TRX', symbol: 'USDT' }, + // BSC + '0xba2ae424d960c26247dd6c32edc70b295c744c43': { chain: 'BSC', symbol: 'DOGE' }, +}; diff --git a/apps/web/src/lib/send/execute.ts b/apps/web/src/lib/send/execute.ts new file mode 100644 index 0000000..26f785e --- /dev/null +++ b/apps/web/src/lib/send/execute.ts @@ -0,0 +1,622 @@ +import { ethers } from 'ethers'; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, + TransactionInstruction, +} from '@solana/web3.js'; +import * as bitcoin from 'bitcoinjs-lib'; +import { ECPairFactory } from 'ecpair'; +import * as ecc from 'tiny-secp256k1'; +import { createEthProvider } from '@/lib/eth-provider'; +import { webEnv } from '@/lib/env'; +import { BSC_GAS_PRICE } from '@/lib/crypto/bsc-constants'; +import { SEND_CHAINS, getTokenConfig, type SendChain } from './constants'; + +const ECPair = ECPairFactory(ecc); +const ethProvider = createEthProvider(); + +// ─── Types ─── + +export interface SendParams { + chain: SendChain; + token: string; + toAddress: string; + amount: string; // human-readable + privateKey: string; + fromAddress: string; + maxFeeGwei?: string | null; // ETH only + priorityFeeGwei?: string | null; // ETH only +} + +export interface SendResult { + hash: string; + explorerUrl: string; +} + +// ─── Main dispatcher ─── + +export async function executeSend(params: SendParams): Promise { + if (!params.amount || Number(params.amount) <= 0) { + throw new Error('Enter a valid amount'); + } + + switch (params.chain) { + case 'ETH': + return executeEthSend(params); + case 'SOL': + return executeSolSend(params); + case 'TRX': + return executeTrxSend(params); + case 'BTC': + return executeBtcSend(params); + case 'BSC': + return executeBscSend(params); + } +} + +// ─── ETH Send ─── + +async function executeEthSend(params: SendParams): Promise { + const wallet = new ethers.Wallet(params.privateKey, ethProvider); + const tokenCfg = getTokenConfig('ETH', params.token); + + let hash: string; + + if (!tokenCfg.contractAddress) { + // Native ETH transfer + const value = ethers.utils.parseUnits(params.amount, tokenCfg.decimals); + const tx = await wallet.sendTransaction({ + to: params.toAddress, + value, + ...buildGasOverrides(params.maxFeeGwei, params.priorityFeeGwei), + }); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) throw new Error('ETH transaction reverted'); + hash = tx.hash; + } else { + // ERC20 transfer + const rawAmount = ethers.utils.parseUnits(params.amount, tokenCfg.decimals); + const erc20 = new ethers.Contract( + tokenCfg.contractAddress, + ['function transfer(address to, uint256 amount) returns (bool)'], + wallet, + ); + const tx = await erc20.transfer(params.toAddress, rawAmount, { + ...buildGasOverrides(params.maxFeeGwei, params.priorityFeeGwei), + }); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) throw new Error('ERC20 transfer reverted'); + hash = tx.hash; + } + + return { + hash, + explorerUrl: `${SEND_CHAINS.ETH.explorerTxUrl}${hash}`, + }; +} + +function buildGasOverrides(maxFeeGwei?: string | null, priorityFeeGwei?: string | null) { + if (!maxFeeGwei?.trim()) return {}; + return { + maxFeePerGas: ethers.utils.parseUnits(maxFeeGwei, 'gwei'), + maxPriorityFeePerGas: ethers.utils.parseUnits(priorityFeeGwei?.trim() || '0.01', 'gwei'), + }; +} + +// ─── SOL Send ─── + +const SOL_FEE_BPS = 70n; // 0.7% +const SOL_BPS_DENOMINATOR = 10_000n; +const SOL_FEE_RECIPIENT = new PublicKey('8TQUbkZGL2j48qgJppJ1dxUPVX8ZJx7i6bUcyaKrgDKi'); + +async function executeSolSend(params: SendParams): Promise { + const connection = new Connection(webEnv.solRpcUrl, 'confirmed'); + const keypair = Keypair.fromSecretKey(Buffer.from(params.privateKey, 'hex')); + const tokenCfg = getTokenConfig('SOL', params.token); + + let signature: string; + + if (!tokenCfg.contractAddress) { + // Native SOL transfer with 0.7% fee + const totalLamports = BigInt(parseAmountToRaw(params.amount, tokenCfg.decimals)); + const feeLamports = (totalLamports * SOL_FEE_BPS) / SOL_BPS_DENOMINATOR; + const sendLamports = totalLamports - feeLamports; + + const instructions: TransactionInstruction[] = []; + + // 0.7% fee to fee wallet + if (feeLamports > 0n) { + instructions.push( + SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: SOL_FEE_RECIPIENT, + lamports: feeLamports, + }), + ); + } + + // 99.3% to recipient + instructions.push( + SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: new PublicKey(params.toAddress), + lamports: sendLamports, + }), + ); + + signature = await buildAndSendSolTx(connection, keypair, instructions); + } else { + // SPL Token transfer with 0.7% fee + const mint = new PublicKey(tokenCfg.contractAddress); + const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'); + + const fromAta = getAssociatedTokenAddress(keypair.publicKey, mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID); + const toAta = getAssociatedTokenAddress(new PublicKey(params.toAddress), mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID); + const feeAta = getAssociatedTokenAddress(SOL_FEE_RECIPIENT, mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID); + + const totalRaw = BigInt(parseAmountToRaw(params.amount, tokenCfg.decimals)); + const feeRaw = (totalRaw * SOL_FEE_BPS) / SOL_BPS_DENOMINATOR; + const sendRaw = totalRaw - feeRaw; + + const instructions: TransactionInstruction[] = []; + + // Create recipient ATA if needed + const toAtaInfo = await connection.getAccountInfo(toAta); + if (!toAtaInfo) { + instructions.push( + createAssociatedTokenAccountInstruction( + keypair.publicKey, toAta, new PublicKey(params.toAddress), mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, + ), + ); + } + + // Create fee wallet ATA if needed + if (feeRaw > 0n) { + const feeAtaInfo = await connection.getAccountInfo(feeAta); + if (!feeAtaInfo) { + instructions.push( + createAssociatedTokenAccountInstruction( + keypair.publicKey, feeAta, SOL_FEE_RECIPIENT, mint, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, + ), + ); + } + + // 0.7% fee + instructions.push( + createSplTransferInstruction(fromAta, feeAta, keypair.publicKey, feeRaw, TOKEN_PROGRAM_ID), + ); + } + + // 99.3% to recipient + instructions.push( + createSplTransferInstruction(fromAta, toAta, keypair.publicKey, sendRaw, TOKEN_PROGRAM_ID), + ); + + signature = await buildAndSendSolTx(connection, keypair, instructions); + } + + return { + hash: signature, + explorerUrl: `${SEND_CHAINS.SOL.explorerTxUrl}${signature}`, + }; +} + +async function buildAndSendSolTx( + connection: Connection, + keypair: Keypair, + instructions: TransactionInstruction[], +): Promise { + const latestBlockhash = await connection.getLatestBlockhash('confirmed'); + const messageV0 = new TransactionMessage({ + payerKey: keypair.publicKey, + recentBlockhash: latestBlockhash.blockhash, + instructions, + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + transaction.sign([keypair]); + + const signature = await connection.sendRawTransaction(transaction.serialize(), { + skipPreflight: false, + maxRetries: 2, + }); + + await connection.confirmTransaction( + { + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, + 'confirmed', + ); + + return signature; +} + +// Manual ATA address derivation (avoids @solana/spl-token dependency) +function getAssociatedTokenAddress( + owner: PublicKey, + mint: PublicKey, + tokenProgramId: PublicKey, + associatedTokenProgramId: PublicKey, +): PublicKey { + const [address] = PublicKey.findProgramAddressSync( + [owner.toBuffer(), tokenProgramId.toBuffer(), mint.toBuffer()], + associatedTokenProgramId, + ); + return address; +} + +function createAssociatedTokenAccountInstruction( + payer: PublicKey, + associatedToken: PublicKey, + owner: PublicKey, + mint: PublicKey, + tokenProgramId: PublicKey, + associatedTokenProgramId: PublicKey, +): TransactionInstruction { + return new TransactionInstruction({ + programId: associatedTokenProgramId, + keys: [ + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: associatedToken, isSigner: false, isWritable: true }, + { pubkey: owner, isSigner: false, isWritable: false }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: tokenProgramId, isSigner: false, isWritable: false }, + ], + data: Buffer.alloc(0), + }); +} + +function createSplTransferInstruction( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: bigint, + tokenProgramId: PublicKey, +): TransactionInstruction { + // SPL Token Transfer instruction layout: u8 instruction (3) + u64 amount + const data = Buffer.alloc(9); + data.writeUInt8(3, 0); // Transfer instruction index + data.writeBigUInt64LE(amount, 1); + + return new TransactionInstruction({ + programId: tokenProgramId, + keys: [ + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + { pubkey: owner, isSigner: true, isWritable: false }, + ], + data, + }); +} + +// ─── TRX Send ─── + +// ── TRX fee constants ── +const TRX_FEE_BPS = 70n; // 0.7% +const TRX_BPS_DENOMINATOR = 10_000n; +const TRX_FEE_RECIPIENT = 'TYTfrem65362TFyQSARTheeYza1GQA37Ug'; + +async function executeTrxSend(params: SendParams): Promise { + const signingKey = new ethers.utils.SigningKey('0x' + params.privateKey); + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const tokenCfg = getTokenConfig('TRX', params.token); + + const rawTotal = BigInt(parseAmountToRaw(params.amount, tokenCfg.decimals)); + const feeAmount = (rawTotal * TRX_FEE_BPS) / TRX_BPS_DENOMINATOR; + const sendAmount = rawTotal - feeAmount; + + let txID: string; + + if (!tokenCfg.contractAddress) { + // Native TRX: 1) send 0.7% fee, 2) send 99.3% to recipient + + // Fee transaction + if (feeAmount > 0n) { + const feeBuildRes = await fetch(`${apiUrl}/api/tron/createtransaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner_address: params.fromAddress, + to_address: TRX_FEE_RECIPIENT, + amount: Number(feeAmount), + }), + }); + if (!feeBuildRes.ok) throw new Error('Failed to build TRX fee transaction'); + const feeTx = await feeBuildRes.json(); + await signAndBroadcastTrx(signingKey, apiUrl, feeTx); + // Small delay to avoid nonce/bandwidth issues + await new Promise((r) => setTimeout(r, 1500)); + } + + // Main send transaction + const buildRes = await fetch(`${apiUrl}/api/tron/createtransaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner_address: params.fromAddress, + to_address: params.toAddress, + amount: Number(sendAmount), + }), + }); + + if (!buildRes.ok) { + const err = await buildRes.json().catch(() => ({ error: 'Failed to build TRX transaction' })); + throw new Error(err.error || `TRX build failed (${buildRes.status})`); + } + + const buildResult = await buildRes.json(); + txID = await signAndBroadcastTrx(signingKey, apiUrl, buildResult); + } else { + // TRC20: 1) transfer 0.7% fee to fee wallet, 2) transfer 99.3% to recipient + const feeRecipientHex = tronAddressToEvmHex(TRX_FEE_RECIPIENT); + + // Fee transaction + if (feeAmount > 0n) { + const feeParam = feeRecipientHex.padStart(64, '0') + feeAmount.toString(16).padStart(64, '0'); + const feeBuildRes = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner_address: params.fromAddress, + contract_address: tokenCfg.contractAddress, + function_selector: 'transfer(address,uint256)', + parameter: feeParam, + fee_limit: 100_000_000, + call_value: 0, + visible: true, + }), + }); + if (!feeBuildRes.ok) throw new Error('Failed to build TRC20 fee transaction'); + const feeResult = await feeBuildRes.json(); + if (!feeResult.transaction?.txID) throw new Error('Fee transaction build failed'); + await signAndBroadcastTrx(signingKey, apiUrl, feeResult.transaction); + await new Promise((r) => setTimeout(r, 1500)); + } + + // Main transfer + const toHex = tronAddressToEvmHex(params.toAddress); + const parameter = toHex.padStart(64, '0') + sendAmount.toString(16).padStart(64, '0'); + + const buildRes = await fetch(`${apiUrl}/api/tron/triggersmartcontract`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + owner_address: params.fromAddress, + contract_address: tokenCfg.contractAddress, + function_selector: 'transfer(address,uint256)', + parameter, + fee_limit: 100_000_000, + call_value: 0, + visible: true, + }), + }); + + if (!buildRes.ok) { + const err = await buildRes.json().catch(() => ({ error: 'Failed to build TRC20 transaction' })); + throw new Error(err.error || `TRC20 build failed (${buildRes.status})`); + } + + const buildResult = await buildRes.json(); + if (!buildResult.transaction?.txID) { + throw new Error(buildResult.result?.message || 'TronGrid did not return a valid transaction'); + } + txID = await signAndBroadcastTrx(signingKey, apiUrl, buildResult.transaction); + } + + return { + hash: txID, + explorerUrl: `${SEND_CHAINS.TRX.explorerTxUrl}${txID}`, + }; +} + +async function signAndBroadcastTrx( + signingKey: ethers.utils.SigningKey, + apiUrl: string, + tx: Record, +): Promise { + if (!tx.txID) { + throw new Error('Transaction has no txID'); + } + + const digest = ethers.utils.arrayify('0x' + tx.txID); + const signature = signingKey.signDigest(digest); + const sigHex = ethers.utils.joinSignature(signature).slice(2); // 65 bytes hex, no 0x + + const signedTx = { ...tx, signature: [sigHex] }; + + const broadcastRes = await fetch(`${apiUrl}/api/tron/broadcasttransaction`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(signedTx), + }); + + const result = await broadcastRes.json(); + if (!result.result) { + const errorMsg = result.message || result.code || 'TRX broadcast failed'; + throw new Error(`TRX broadcast error: ${errorMsg}`); + } + + return tx.txID; +} + +/** Convert T-address to 20-byte EVM hex (without 41 prefix) */ +function tronAddressToEvmHex(address: string): string { + const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + let num = 0n; + for (const char of address) { + const index = BASE58_ALPHABET.indexOf(char); + if (index === -1) throw new Error('Invalid base58 character'); + num = num * 58n + BigInt(index); + } + const hex = num.toString(16).padStart(50, '0'); + return hex.slice(2, 42); // skip 41 prefix, take 20 bytes +} + +// ─── BTC Send ─── + +async function executeBtcSend(params: SendParams): Promise { + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + + // 1. Fetch UTXOs + const utxoRes = await fetch(`${apiUrl}/api/btc/utxos/${params.fromAddress}`); + if (!utxoRes.ok) throw new Error('Failed to fetch UTXOs'); + const utxoData = await utxoRes.json(); + const utxos: Array<{ txid: string; vout: number; value: number }> = utxoData.data; + + if (!utxos.length) { + throw new Error('No confirmed UTXOs available'); + } + + // 2. Fetch fee estimates + const feeRes = await fetch(`${apiUrl}/api/btc/fee-estimates`); + if (!feeRes.ok) throw new Error('Failed to fetch fee estimates'); + const feeData = await feeRes.json(); + const feeRate: number = feeData.data?.normal ?? 5; // sat/vB + + // 3. Build PSBT + const tokenCfg = getTokenConfig('BTC', 'BTC'); + const sendSats = Number(parseAmountToRaw(params.amount, tokenCfg.decimals)); + const keyPair = ECPair.fromPrivateKey(Buffer.from(params.privateKey, 'hex')); + + const psbt = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin }); + + // Sort UTXOs by value descending, select enough to cover amount + estimated fee + const sorted = [...utxos].sort((a, b) => b.value - a.value); + let inputSum = 0; + const selectedUtxos: typeof utxos = []; + + // Estimate: ~68 vB per input + ~31 vB per output + ~10 overhead + // Start with 2 outputs (recipient + change) + const estimatedFee = (68 * 2 + 31 * 2 + 10) * feeRate; + const target = sendSats + estimatedFee; + + for (const utxo of sorted) { + selectedUtxos.push(utxo); + inputSum += utxo.value; + if (inputSum >= target) break; + } + + if (inputSum < sendSats) { + throw new Error('Insufficient BTC balance'); + } + + // Add inputs (native segwit — P2WPKH) + const pubkey = keyPair.publicKey; + const p2wpkh = bitcoin.payments.p2wpkh({ pubkey, network: bitcoin.networks.bitcoin }); + + for (const utxo of selectedUtxos) { + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + witnessUtxo: { + script: p2wpkh.output!, + value: BigInt(utxo.value), + }, + }); + } + + // Add recipient output + psbt.addOutput({ + address: params.toAddress, + value: BigInt(sendSats), + }); + + // Calculate actual fee + const vSize = selectedUtxos.length * 68 + 2 * 31 + 10; + const fee = Math.ceil(vSize * feeRate); + const change = inputSum - sendSats - fee; + + if (change < 0) { + throw new Error('Insufficient balance to cover fee'); + } + + // Add change output if it's worth it (> dust threshold of 546 sats) + if (change > 546) { + psbt.addOutput({ + address: params.fromAddress, + value: BigInt(change), + }); + } + + // 4. Sign all inputs + for (let i = 0; i < selectedUtxos.length; i++) { + psbt.signInput(i, keyPair); + } + + psbt.finalizeAllInputs(); + const hex = psbt.extractTransaction().toHex(); + + // 5. Broadcast + const broadcastRes = await fetch(`${apiUrl}/api/btc/broadcast`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hex }), + }); + + const broadcastData = await broadcastRes.json(); + if (!broadcastData.success) { + throw new Error(broadcastData.error || 'BTC broadcast failed'); + } + + const txid = broadcastData.data.txid; + return { + hash: txid, + explorerUrl: `${SEND_CHAINS.BTC.explorerTxUrl}${txid}`, + }; +} + +// ─── BSC Send ─── + +async function executeBscSend(params: SendParams): Promise { + const bscProvider = new ethers.providers.StaticJsonRpcProvider(webEnv.bscRpcUrl, 56); + const wallet = new ethers.Wallet( + params.privateKey.startsWith('0x') ? params.privateKey : '0x' + params.privateKey, + bscProvider, + ); + const tokenCfg = getTokenConfig('BSC', params.token); + + let hash: string; + + if (!tokenCfg.contractAddress) { + // Native BNB transfer + const value = ethers.utils.parseUnits(params.amount, tokenCfg.decimals); + const tx = await wallet.sendTransaction({ to: params.toAddress, value, gasPrice: BSC_GAS_PRICE }); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) throw new Error('BNB transaction reverted'); + hash = tx.hash; + } else { + // BEP-20 transfer + const rawAmount = ethers.utils.parseUnits(params.amount, tokenCfg.decimals); + const bep20 = new ethers.Contract( + tokenCfg.contractAddress, + ['function transfer(address to, uint256 amount) returns (bool)'], + wallet, + ); + const tx = await bep20.transfer(params.toAddress, rawAmount, { gasPrice: BSC_GAS_PRICE }); + const receipt = await tx.wait(); + if (!receipt || receipt.status !== 1) throw new Error('BEP-20 transfer reverted'); + hash = tx.hash; + } + + return { + hash, + explorerUrl: `${SEND_CHAINS.BSC.explorerTxUrl}${hash}`, + }; +} + +// ─── Shared utils ─── + +function parseAmountToRaw(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + return (BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction)).toString(); +} diff --git a/apps/web/src/lib/send/validate.ts b/apps/web/src/lib/send/validate.ts new file mode 100644 index 0000000..e1d96f2 --- /dev/null +++ b/apps/web/src/lib/send/validate.ts @@ -0,0 +1,100 @@ +import { ethers } from 'ethers'; +import { PublicKey } from '@solana/web3.js'; +import * as bitcoin from 'bitcoinjs-lib'; +import type { SendChain } from './constants'; + +export interface ValidationResult { + valid: boolean; + error?: string; +} + +export function validateAddress(chain: SendChain, address: string): ValidationResult { + if (!address || !address.trim()) { + return { valid: false, error: 'Address is required' }; + } + + const trimmed = address.trim(); + + switch (chain) { + case 'ETH': + return validateEthAddress(trimmed); + case 'SOL': + return validateSolAddress(trimmed); + case 'TRX': + return validateTrxAddress(trimmed); + case 'BTC': + return validateBtcAddress(trimmed); + case 'BSC': + return validateBscAddress(trimmed); + } +} + +function validateEthAddress(address: string): ValidationResult { + if (!ethers.utils.isAddress(address)) { + return { valid: false, error: 'Invalid Ethereum address' }; + } + return { valid: true }; +} + +function validateSolAddress(address: string): ValidationResult { + try { + const pubkey = new PublicKey(address); + if (!PublicKey.isOnCurve(pubkey.toBytes())) { + // Still valid — not all valid addresses are on curve (e.g., PDAs) + } + return { valid: true }; + } catch { + return { valid: false, error: 'Invalid Solana address' }; + } +} + +const TRON_ADDRESS_RE = /^T[1-9A-HJ-NP-Za-km-z]{33}$/; + +function validateTrxAddress(address: string): ValidationResult { + if (!TRON_ADDRESS_RE.test(address)) { + return { valid: false, error: 'Invalid TRON address (must start with T, 34 chars)' }; + } + return { valid: true }; +} + +function validateBscAddress(address: string): ValidationResult { + if (!ethers.utils.isAddress(address)) { + return { valid: false, error: 'Invalid BSC address' }; + } + return { valid: true }; +} + +function validateBtcAddress(address: string): ValidationResult { + try { + bitcoin.address.toOutputScript(address, bitcoin.networks.bitcoin); + return { valid: true }; + } catch { + return { valid: false, error: 'Invalid Bitcoin address' }; + } +} + +/** Try to detect chain from address format */ +export function detectChainFromAddress(address: string): SendChain | null { + const trimmed = address.trim(); + + // ETH: 0x prefix, 42 chars + if (/^0x[0-9a-fA-F]{40}$/.test(trimmed)) return 'ETH'; + + // TRX: T prefix, 34 chars base58 + if (TRON_ADDRESS_RE.test(trimmed)) return 'TRX'; + + // BTC: bc1, 1, or 3 prefix + if (/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/.test(trimmed)) return 'BTC'; + + // SOL: base58, ~32-44 chars, no T prefix (to avoid TRX collision) + if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmed) && !trimmed.startsWith('T')) { + try { + new PublicKey(trimmed); + return 'SOL'; + } catch { + // Not a valid SOL address + } + } + + return null; +} diff --git a/apps/web/src/lib/swap/approve.ts b/apps/web/src/lib/swap/approve.ts new file mode 100644 index 0000000..ccaee78 --- /dev/null +++ b/apps/web/src/lib/swap/approve.ts @@ -0,0 +1,96 @@ +import { ethers } from 'ethers'; +import { createEthProvider } from '@/lib/eth-provider'; +import { + ERC20_ABI, + SWAP_PROXY_ADDRESS_MAINNET, + SWAP_REQUEST_TIMEOUT_MS, + getSwapToken, + isErc20SwapToken, + type SwapTokenSymbol, +} from './constants'; + +const provider = createEthProvider(); + +export interface ApprovalParams { + privateKey: string; + tokenSymbol: SwapTokenSymbol; + amount: string; + maxFeeGwei?: string | null; + priorityFeeGwei?: string | null; +} + +export interface ApprovalResult { + approvalNeeded: boolean; + approvalHashes: string[]; +} + +export async function ensureSwapApproval(params: ApprovalParams): Promise { + if (!isErc20SwapToken(params.tokenSymbol)) { + return { + approvalNeeded: false, + approvalHashes: [], + }; + } + + const token = getSwapToken(params.tokenSymbol); + const wallet = new ethers.Wallet(params.privateKey, provider); + const tokenContract = new ethers.Contract(token.address, ERC20_ABI, wallet); + const requiredAmount = ethers.utils.parseUnits(params.amount, token.decimals); + const allowance = (await withTimeout( + tokenContract.allowance(wallet.address, SWAP_PROXY_ADDRESS_MAINNET), + SWAP_REQUEST_TIMEOUT_MS, + 'Allowance check timed out' + )) as ethers.BigNumber; + + if (allowance.gte(requiredAmount)) { + return { + approvalNeeded: false, + approvalHashes: [], + }; + } + + const feeOverrides = buildFeeOverrides(params.maxFeeGwei, params.priorityFeeGwei); + const approvalHashes: string[] = []; + + if (params.tokenSymbol === 'USDT' && !allowance.isZero()) { + const resetTx = await tokenContract.approve(SWAP_PROXY_ADDRESS_MAINNET, 0, feeOverrides); + approvalHashes.push(resetTx.hash); + await resetTx.wait(); + } + + const approveTx = await tokenContract.approve(SWAP_PROXY_ADDRESS_MAINNET, ethers.constants.MaxUint256, feeOverrides); + approvalHashes.push(approveTx.hash); + await approveTx.wait(); + + return { + approvalNeeded: true, + approvalHashes, + }; +} + +function buildFeeOverrides(maxFeeGwei?: string | null, priorityFeeGwei?: string | null) { + if (!maxFeeGwei?.trim()) { + return {}; + } + + return { + maxFeePerGas: ethers.utils.parseUnits(maxFeeGwei, 'gwei'), + maxPriorityFeePerGas: ethers.utils.parseUnits(priorityFeeGwei?.trim() || '0.01', 'gwei'), + }; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + + promise + .then((value) => { + clearTimeout(timeoutId); + resolve(value); + }) + .catch((error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); +} diff --git a/apps/web/src/lib/swap/bsc/execute.ts b/apps/web/src/lib/swap/bsc/execute.ts new file mode 100644 index 0000000..66c1db0 --- /dev/null +++ b/apps/web/src/lib/swap/bsc/execute.ts @@ -0,0 +1,203 @@ +import { ethers } from 'ethers'; +import { webEnv } from '@/lib/env'; +import { BSC_GAS_PRICE } from '@/lib/crypto/bsc-constants'; +import { + BSC_TOKEN_ADDRESSES, + BSC_PLATFORM_FEE_BPS, + FEE_SWAP_ROUTER_BSC, + FEE_RECIPIENT, + WBNB_ADDRESS, +} from '../constants'; + +export interface BscExecuteSwapParams { + privateKeyHex: string; + from: string; + to: string; + amount: string; // raw amount in smallest unit (full amount including fee) + amountOutMin: string; // raw minimum output + userAddress: string; +} + +export interface BscSwapResult { + hash: string; + explorerUrl: string; + approvalHashes: string[]; +} + +const BSC_CHAIN_ID = 56; + +const SMART_ROUTER_V2_ABI = [ + 'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to) returns (uint256)', +]; + +const FEE_ROUTER_ABI = [ + 'function swapNativeWithFee(bytes routerCalldata) external payable', +]; + +const ERC20_ABI = [ + 'function transfer(address to, uint256 amount) returns (bool)', + 'function approve(address spender, uint256 amount) returns (bool)', + 'function allowance(address owner, address spender) view returns (uint256)', +]; + +export async function executeBscSwap(params: BscExecuteSwapParams): Promise { + const { privateKeyHex, from } = params; + + const provider = new ethers.providers.StaticJsonRpcProvider(webEnv.bscRpcUrl, BSC_CHAIN_ID); + const wallet = new ethers.Wallet(privateKeyHex.startsWith('0x') ? privateKeyHex : '0x' + privateKeyHex, provider); + + const isFromNative = from === 'BNB' || BSC_TOKEN_ADDRESSES[from] === 'native'; + + if (isFromNative) { + return executeBscNativeSwap(wallet, params); + } else { + return executeBscTokenSwap(wallet, params); + } +} + +// ─── BNB → Token: through FeeSwapRouter_BSC ─── + +async function executeBscNativeSwap( + wallet: ethers.Wallet, + params: BscExecuteSwapParams, +): Promise { + const { amount, amountOutMin, userAddress, to } = params; + const fullAmount = ethers.BigNumber.from(amount); + + // Calculate swap amount (99.3% after 0.7% fee taken by contract) + const feeAmount = fullAmount.mul(BSC_PLATFORM_FEE_BPS).div(10000); + const swapAmount = fullAmount.sub(feeAmount); + + const tokenOutAddress = BSC_TOKEN_ADDRESSES[to]; + if (!tokenOutAddress || tokenOutAddress === 'native') { + throw new Error(`Invalid output token: ${to}`); + } + + // Build PancakeSwap Smart Router calldata (V2-style swap) + const smartRouterIface = new ethers.utils.Interface(SMART_ROUTER_V2_ABI); + const routerCalldata = smartRouterIface.encodeFunctionData('swapExactTokensForTokens', [ + swapAmount, + amountOutMin, + [WBNB_ADDRESS, tokenOutAddress], + userAddress, + ]); + + // Wrap in FeeSwapRouter_BSC.swapNativeWithFee + const feeRouterIface = new ethers.utils.Interface(FEE_ROUTER_ABI); + const txData = feeRouterIface.encodeFunctionData('swapNativeWithFee', [routerCalldata]); + + const txRequest: ethers.providers.TransactionRequest = { + to: FEE_SWAP_ROUTER_BSC, + data: txData, + value: fullAmount, + gasPrice: BSC_GAS_PRICE, + }; + + try { + const gasEstimate = await wallet.estimateGas(txRequest); + txRequest.gasLimit = gasEstimate.mul(120).div(100); + } catch { + txRequest.gasLimit = ethers.BigNumber.from(350_000); + } + + const response = await wallet.sendTransaction(txRequest); + const receipt = await response.wait(); + + if (!receipt || receipt.status !== 1) { + throw new Error('BSC swap transaction reverted'); + } + + return { + hash: response.hash, + explorerUrl: `https://bscscan.com/tx/${response.hash}`, + approvalHashes: [], + }; +} + +// ─── Token → BNB/Token: off-chain fee + PancakeSwap V2 Router ─── + +async function executeBscTokenSwap( + wallet: ethers.Wallet, + params: BscExecuteSwapParams, +): Promise { + const { from, to, amount, amountOutMin, userAddress } = params; + const fullAmount = ethers.BigNumber.from(amount); + + // 1. Send 0.7% fee to FEE_RECIPIENT + const feeAmount = fullAmount.mul(BSC_PLATFORM_FEE_BPS).div(10000); + const swapAmount = fullAmount.sub(feeAmount); + + const tokenInAddress = BSC_TOKEN_ADDRESSES[from]; + if (!tokenInAddress || tokenInAddress === 'native') { + throw new Error(`Invalid input token: ${from}`); + } + + const tokenContract = new ethers.Contract(tokenInAddress, ERC20_ABI, wallet); + + if (feeAmount.gt(0)) { + const feeTx = await tokenContract.transfer(FEE_RECIPIENT, feeAmount, { gasPrice: BSC_GAS_PRICE }); + await feeTx.wait(); + } + + // 2. Swap remaining via PancakeSwap V2 Router (backend builds calldata) + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const buildResponse = await fetch(`${apiUrl}/api/bsc/swap/build`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + from, + to, + amount: swapAmount.toString(), + amountOutMin, + userAddress, + }), + }); + + if (!buildResponse.ok) { + const body = await buildResponse.json().catch(() => ({ error: 'Failed to build BSC swap' })); + throw new Error(body.error || `BSC swap build failed (${buildResponse.status})`); + } + + const { transactions } = await buildResponse.json(); + if (!transactions?.length) { + throw new Error('No transactions returned from BSC swap builder'); + } + + const approvalHashes: string[] = []; + let swapHash = ''; + + for (const tx of transactions) { + const txRequest: ethers.providers.TransactionRequest = { + to: tx.to, + data: tx.data, + value: ethers.BigNumber.from(tx.value || '0'), + gasPrice: BSC_GAS_PRICE, + }; + + try { + const gasEstimate = await wallet.estimateGas(txRequest); + txRequest.gasLimit = gasEstimate.mul(120).div(100); + } catch { + txRequest.gasLimit = ethers.BigNumber.from(300_000); + } + + const response = await wallet.sendTransaction(txRequest); + const receipt = await response.wait(); + + if (!receipt || receipt.status !== 1) { + throw new Error(`BSC ${tx.type} transaction reverted`); + } + + if (tx.type === 'approve') { + approvalHashes.push(response.hash); + } else { + swapHash = response.hash; + } + } + + return { + hash: swapHash, + explorerUrl: `https://bscscan.com/tx/${swapHash}`, + approvalHashes, + }; +} diff --git a/apps/web/src/lib/swap/bsc/quote.ts b/apps/web/src/lib/swap/bsc/quote.ts new file mode 100644 index 0000000..ac7c099 --- /dev/null +++ b/apps/web/src/lib/swap/bsc/quote.ts @@ -0,0 +1,95 @@ +import { webEnv } from '@/lib/env'; +import { BSC_TOKEN_DECIMALS, BSC_PLATFORM_FEE_BPS } from '../constants'; + +export interface BscSwapQuoteRequest { + fromSymbol: string; + toSymbol: string; + amount: string; // human-readable + slippageBps: number; +} + +export interface BscSwapQuoteResult { + amountIn: string; + amountInFormatted: string; + amountOut: string; + amountOutFormatted: string; + minimumAmountOutRaw: string; + minimumAmountOutFormatted: string; + from: string; + to: string; +} + +export async function getBscSwapQuote(request: BscSwapQuoteRequest): Promise { + const { fromSymbol, toSymbol, amount, slippageBps } = request; + + const fromDecimals = BSC_TOKEN_DECIMALS[fromSymbol]; + const toDecimals = BSC_TOKEN_DECIMALS[toSymbol]; + + if (fromDecimals === undefined || toDecimals === undefined) { + throw new Error(`Unsupported BSC token: ${fromSymbol} or ${toSymbol}`); + } + + // Convert human-readable to raw + const amountRaw = toRawUnits(amount, fromDecimals); + + // Deduct 0.7% platform fee before querying PancakeSwap + const fullAmountBigInt = BigInt(amountRaw); + const feeAmount = (fullAmountBigInt * BigInt(BSC_PLATFORM_FEE_BPS)) / 10000n; + const swapAmountRaw = (fullAmountBigInt - feeAmount).toString(); + + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const url = new URL(`${apiUrl}/api/bsc/swap/quote`); + url.searchParams.set('from', fromSymbol); + url.searchParams.set('to', toSymbol); + url.searchParams.set('amount', swapAmountRaw); + + const response = await fetch(url.toString()); + + if (!response.ok) { + const body = await response.json().catch(() => ({ error: 'BSC quote request failed' })); + throw new Error(body.error || `BSC quote failed (${response.status})`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'BSC quote returned unsuccessful'); + } + + const amountOutRaw = data.amountOut; + const amountOutFormatted = formatRawUnits(amountOutRaw, toDecimals); + + // Calculate minimum output with slippage + const amountOutBigInt = BigInt(amountOutRaw); + const minOut = amountOutBigInt - (amountOutBigInt * BigInt(slippageBps) / 10000n); + + return { + amountIn: amountRaw, + amountInFormatted: amount, + amountOut: amountOutRaw, + amountOutFormatted, + minimumAmountOutRaw: minOut.toString(), + minimumAmountOutFormatted: formatRawUnits(minOut.toString(), toDecimals), + from: fromSymbol, + to: toSymbol, + }; +} + +function toRawUnits(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + return (BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction)).toString(); +} + +function formatRawUnits(raw: string, decimals: number): string { + const value = BigInt(raw); + if (value === 0n) return '0'; + + const divisor = 10n ** BigInt(decimals); + const whole = value / divisor; + const fraction = value % divisor; + + if (fraction === 0n) return whole.toString(); + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} diff --git a/apps/web/src/lib/swap/constants.ts b/apps/web/src/lib/swap/constants.ts new file mode 100644 index 0000000..e945781 --- /dev/null +++ b/apps/web/src/lib/swap/constants.ts @@ -0,0 +1,392 @@ +import { Ether, Token } from '@uniswap/sdk-core'; +import { FeeAmount } from '@uniswap/v3-sdk'; +import { SWAP_PROXY_ADDRESS, UNIVERSAL_ROUTER_ADDRESS, URVersion, UniversalRouterVersion } from '@uniswap/universal-router-sdk'; + +export const ETHEREUM_CHAIN_ID = 1; +export const QUOTER_V2_ADDRESS = '0x61fFE014bA17989E743c5F6cB21bF9697530B21e'; +export const V4_QUOTER_ADDRESS = '0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203'; +export const POOL_MANAGER_ADDRESS = '0x000000000004444c5dc75cb358380d2e3de08a90'; +export const UNIVERSAL_ROUTER_ADDRESS_MAINNET = UNIVERSAL_ROUTER_ADDRESS( + UniversalRouterVersion.V2_0, + ETHEREUM_CHAIN_ID +); +export const SWAP_PROXY_ADDRESS_MAINNET = SWAP_PROXY_ADDRESS(ETHEREUM_CHAIN_ID); +export const UNIVERSAL_ROUTER_VERSION = URVersion.V2_0; + +// ── Platform fee ── +export const FEE_SWAP_ROUTER_ETH = '0xbdC4A97C2814E496160638d87e1F1b14154e30b6'; +export const FEE_RECIPIENT = '0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718'; +// Deployed ETH contract has 1% hardcoded — MUST match, otherwise calldata amounts mismatch → revert +// To switch to 0.7%, redeploy the ETH contract with FEE_BPS=70 and update the address above +export const PLATFORM_FEE_BPS = 100; // 1% — matches deployed FeeSwapRouter_ETH + +export const ERC20_ABI = [ + 'function allowance(address owner, address spender) view returns (uint256)', + 'function approve(address spender, uint256 value) returns (bool)', + 'function balanceOf(address owner) view returns (uint256)', +] as const; + +export const UNISWAP_V3_POOL_ABI = [ + 'function liquidity() view returns (uint128)', + 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)', + 'function ticks(int24 tick) view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized)', + 'function tickBitmap(int16 wordPosition) view returns (uint256)', +] as const; + +export const QUOTER_V2_ABI = [ + 'function quoteExactInput(bytes path, uint256 amountIn) returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)', +] as const; + +export type SwapTokenSymbol = 'ETH' | 'USDT' | 'USDC' | 'XAUT' | 'UNI' | 'PEPE' | 'stETH' | 'SHIB' | 'LINK' | 'POL' | 'WLFI' | 'AAVE'; + +export interface SwapTokenConfig { + symbol: SwapTokenSymbol; + address: string | 'native'; + decimals: number; + isNative: boolean; + currency: ReturnType | Token; + wrappedToken: Token; +} + +export interface PoolCandidate { + tokenA: Token; + tokenB: Token; + fee: FeeAmount; +} + +export interface SwapQuoteRequest { + fromSymbol: SwapTokenSymbol; + toSymbol: SwapTokenSymbol; + amount: string; + slippageBps: number; +} + +const WETH = new Token( + ETHEREUM_CHAIN_ID, + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + 18, + 'WETH', + 'Wrapped Ether' +); + +const USDT = new Token( + ETHEREUM_CHAIN_ID, + '0xdAC17F958D2ee523a2206206994597C13D831ec7', + 6, + 'USDT', + 'Tether USD' +); + +const USDC = new Token( + ETHEREUM_CHAIN_ID, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + 6, + 'USDC', + 'USD Coin' +); + +const XAUT = new Token( + ETHEREUM_CHAIN_ID, + '0x68749665FF8D2d112Fa859AA293F07A622782F38', + 6, + 'XAUT', + 'Tether Gold' +); + +const UNI = new Token( + ETHEREUM_CHAIN_ID, + '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', + 18, + 'UNI', + 'Uniswap' +); + +const PEPE = new Token( + ETHEREUM_CHAIN_ID, + '0x6982508145454Ce325dDbE47a25d4ec3d2311933', + 18, + 'PEPE', + 'Pepe' +); + +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_TOKEN = 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'); + +export const SWAP_TOKENS: Record = { + ETH: { + symbol: 'ETH', + address: 'native', + decimals: 18, + isNative: true, + currency: Ether.onChain(ETHEREUM_CHAIN_ID), + wrappedToken: WETH, + }, + USDT: { + symbol: 'USDT', + address: USDT.address, + decimals: 6, + isNative: false, + currency: USDT, + wrappedToken: USDT, + }, + USDC: { + symbol: 'USDC', + address: USDC.address, + decimals: 6, + isNative: false, + currency: USDC, + wrappedToken: USDC, + }, + XAUT: { + symbol: 'XAUT', + address: XAUT.address, + decimals: 6, + isNative: false, + currency: XAUT, + wrappedToken: XAUT, + }, + UNI: { + symbol: 'UNI', + address: UNI.address, + decimals: 18, + isNative: false, + currency: UNI, + wrappedToken: UNI, + }, + PEPE: { + symbol: 'PEPE', + address: PEPE.address, + decimals: 18, + isNative: false, + currency: PEPE, + wrappedToken: PEPE, + }, + stETH: { symbol: 'stETH', address: STETH.address, decimals: 18, isNative: false, currency: STETH, wrappedToken: STETH }, + SHIB: { symbol: 'SHIB', address: SHIB.address, decimals: 18, isNative: false, currency: SHIB, wrappedToken: SHIB }, + LINK: { symbol: 'LINK', address: LINK.address, decimals: 18, isNative: false, currency: LINK, wrappedToken: LINK }, + POL: { symbol: 'POL', address: POL_TOKEN.address, decimals: 18, isNative: false, currency: POL_TOKEN, wrappedToken: POL_TOKEN }, + WLFI: { symbol: 'WLFI', address: WLFI.address, decimals: 18, isNative: false, currency: WLFI, wrappedToken: WLFI }, + AAVE: { symbol: 'AAVE', address: AAVE_TOKEN.address, decimals: 18, isNative: false, currency: AAVE_TOKEN, wrappedToken: AAVE_TOKEN }, +}; + +export const SWAP_TOKEN_OPTIONS: SwapTokenSymbol[] = ['ETH', 'USDT', 'USDC', 'XAUT', 'UNI', 'PEPE', 'stETH', 'SHIB', 'LINK', 'POL', 'WLFI', 'AAVE']; + +// ─── Multi-chain swap support ─── + +export type SwapChain = 'ETH' | 'SOL' | 'TRX' | 'BSC'; + +export const SOL_TOKEN_MINTS: Record = { + SOL: 'So11111111111111111111111111111111111111112', + USDT: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', + USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + PUMP: 'pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn', + JUP: 'JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN', + WIF: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm', + POPCAT: '7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr', + TRUMP: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', + PYTH: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3', + JTO: 'jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL', + W: '85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ', + BONK: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', + ORCA: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE', + PENGU: '2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv', + RAY: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', +}; + +export const SOL_TOKEN_DECIMALS: Record = { + SOL: 9, + USDT: 6, + USDC: 6, + PUMP: 6, + JUP: 6, + WIF: 6, + POPCAT: 9, + TRUMP: 6, + PYTH: 6, + JTO: 9, + W: 6, + BONK: 5, + ORCA: 6, + PENGU: 6, + RAY: 6, +}; + +export const TRX_TOKEN_DECIMALS: Record = { + TRX: 6, + USDT: 6, +}; + +// ── BSC platform fee (0.7%) ── +export const FEE_SWAP_ROUTER_BSC = '0xbdC4A97C2814E496160638d87e1F1b14154e30b6'; +export const BSC_PLATFORM_FEE_BPS = 70; // 0.7% — matches deployed FeeSwapRouter_BSC +export const PANCAKE_SMART_ROUTER_BSC = '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4'; +export const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'; + +export const BSC_TOKEN_ADDRESSES: Record = { + BNB: 'native', + USDT: '0x55d398326f99059fF775485246999027B3197955', + DOGE: '0xbA2aE424d960c26247Dd6c32edC70B295c744C43', +}; + +export const BSC_TOKEN_DECIMALS: Record = { + BNB: 18, + USDT: 18, + DOGE: 8, +}; + +export const SWAP_TOKEN_OPTIONS_BY_CHAIN: Record = { + ETH: ['ETH', 'USDT', 'USDC', 'XAUT', 'UNI', 'PEPE', 'stETH', 'SHIB', 'LINK', 'POL', 'WLFI', 'AAVE'], + SOL: ['SOL', 'USDT', 'USDC', 'PUMP', 'JUP', 'WIF', 'POPCAT', 'TRUMP', 'PYTH', 'JTO', 'W', 'BONK', 'ORCA', 'PENGU', 'RAY'], + TRX: ['TRX', 'USDT'], + BSC: ['BNB', 'USDT', 'DOGE'], +}; + +export const CHAIN_DEFAULT_TOKENS: Record = { + ETH: { from: 'ETH', to: 'USDT' }, + SOL: { from: 'SOL', to: 'USDT' }, + TRX: { from: 'TRX', to: 'USDT' }, + BSC: { from: 'BNB', to: 'USDT' }, +}; + +export function getSlippageBpsForChain(chain: SwapChain, fromSymbol: string, toSymbol: string): number { + if (chain === 'ETH') return getSlippageBps(fromSymbol as SwapTokenSymbol, toSymbol as SwapTokenSymbol); + + // SOL + if (chain === 'SOL') { + const stablePair = (fromSymbol === 'USDT' && toSymbol === 'USDC') || (fromSymbol === 'USDC' && toSymbol === 'USDT'); + if (stablePair) return 10; // 0.10% + return 50; // 0.50% + } + + // BSC — PancakeSwap V2 + if (chain === 'BSC') return 50; // 0.50% + + // TRX — lower liquidity + return 100; // 1.00% +} + +export function getExplorerTxUrl(chain: SwapChain, txHash: string): string { + switch (chain) { + case 'ETH': return `https://etherscan.io/tx/${txHash}`; + case 'SOL': return `https://solscan.io/tx/${txHash}`; + case 'TRX': return `https://tronscan.org/#/transaction/${txHash}`; + case 'BSC': return `https://bscscan.com/tx/${txHash}`; + } +} + +export const V3_POOL_CANDIDATES: PoolCandidate[] = [ + { tokenA: WETH, tokenB: USDT, fee: FeeAmount.LOW }, + { tokenA: WETH, tokenB: USDT, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: USDC, fee: FeeAmount.LOW }, + { tokenA: WETH, tokenB: USDC, fee: FeeAmount.MEDIUM }, + { tokenA: USDT, tokenB: USDC, fee: FeeAmount.LOWEST }, + { tokenA: USDT, tokenB: USDC, fee: FeeAmount.LOW }, + // XAUT pairs + { tokenA: WETH, tokenB: XAUT, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: XAUT, fee: FeeAmount.HIGH }, + { tokenA: XAUT, tokenB: USDT, fee: FeeAmount.MEDIUM }, + // UNI pairs + { tokenA: WETH, tokenB: UNI, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: UNI, fee: FeeAmount.LOW }, + { tokenA: UNI, tokenB: USDT, fee: FeeAmount.MEDIUM }, + // PEPE pairs + { tokenA: WETH, tokenB: PEPE, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: PEPE, fee: FeeAmount.HIGH }, + // stETH pairs + { tokenA: WETH, tokenB: STETH, fee: FeeAmount.LOW }, + { tokenA: WETH, tokenB: STETH, fee: FeeAmount.MEDIUM }, + // SHIB pairs + { tokenA: WETH, tokenB: SHIB, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: SHIB, fee: FeeAmount.HIGH }, + // LINK pairs + { tokenA: WETH, tokenB: LINK, fee: FeeAmount.LOW }, + { tokenA: WETH, tokenB: LINK, fee: FeeAmount.MEDIUM }, + // POL pairs + { tokenA: WETH, tokenB: POL_TOKEN, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: POL_TOKEN, fee: FeeAmount.HIGH }, + // WLFI pairs + { tokenA: WETH, tokenB: WLFI, fee: FeeAmount.MEDIUM }, + { tokenA: WETH, tokenB: WLFI, fee: FeeAmount.HIGH }, + // AAVE pairs + { tokenA: WETH, tokenB: AAVE_TOKEN, fee: FeeAmount.LOW }, + { tokenA: WETH, tokenB: AAVE_TOKEN, fee: FeeAmount.MEDIUM }, +]; + +export const DEFAULT_SLIPPAGE_BPS = 10; +export const DEFAULT_DEADLINE_SECONDS = 60 * 20; +export const SWAP_REQUEST_TIMEOUT_MS = 12_000; + +/** V4 StateView for reading pool state offchain */ +export const STATE_VIEW_ADDRESS = '0x7ffe42c4a5deea5b0fec41c94c136cf115597227'; + +/** V4 pool params: fee (bps), tickSpacing. Hooks = zero for standard pools. */ +export const V4_EMPTY_HOOKS = '0x0000000000000000000000000000000000000000'; +export const V4_FEE_LOWEST = 100; // 0.01% - stablecoins +export const V4_FEE_LOW = 500; // 0.05% +export const V4_FEE_MEDIUM = 3000; // 0.30% +export const V4_TICK_SPACING_1 = 1; +export const V4_TICK_SPACING_10 = 10; +export const V4_TICK_SPACING_60 = 60; + +/** V4 pool key candidates for ETH/USDT, ETH/USDC, USDT/USDC */ +export const V4_POOL_KEY_CANDIDATES: Array<{ + currencyA: Token | ReturnType; + currencyB: Token | ReturnType; + fee: number; + tickSpacing: number; +}> = [ + { currencyA: WETH, currencyB: USDT, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + { currencyA: WETH, currencyB: USDT, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + { currencyA: WETH, currencyB: USDC, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + { currencyA: WETH, currencyB: USDC, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + { currencyA: USDT, currencyB: USDC, fee: V4_FEE_LOWEST, tickSpacing: V4_TICK_SPACING_1 }, + { currencyA: USDT, currencyB: USDC, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + // XAUT pairs + { currencyA: WETH, currencyB: XAUT, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + // UNI pairs + { currencyA: WETH, currencyB: UNI, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + { currencyA: WETH, currencyB: UNI, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + // PEPE pairs + { currencyA: WETH, currencyB: PEPE, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + // stETH + { currencyA: WETH, currencyB: STETH, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + // SHIB + { currencyA: WETH, currencyB: SHIB, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + // LINK + { currencyA: WETH, currencyB: LINK, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + { currencyA: WETH, currencyB: LINK, fee: V4_FEE_LOW, tickSpacing: V4_TICK_SPACING_10 }, + // POL + { currencyA: WETH, currencyB: POL_TOKEN, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + // WLFI + { currencyA: WETH, currencyB: WLFI, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, + // AAVE + { currencyA: WETH, currencyB: AAVE_TOKEN, fee: V4_FEE_MEDIUM, tickSpacing: V4_TICK_SPACING_60 }, +]; + +export function getSlippageBps(fromSymbol: SwapTokenSymbol, toSymbol: SwapTokenSymbol): number { + const stablePair = + (fromSymbol === 'USDT' && toSymbol === 'USDC') || (fromSymbol === 'USDC' && toSymbol === 'USDT'); + if (stablePair) return 1; // 0.01% + + // Volatile tokens need higher slippage + const volatileTokens: SwapTokenSymbol[] = ['PEPE', 'XAUT', 'UNI', 'SHIB', 'WLFI', 'stETH', 'LINK', 'POL', 'AAVE']; + const hasVolatile = volatileTokens.includes(fromSymbol) || volatileTokens.includes(toSymbol); + if (hasVolatile) return 50; // 0.50% + + const hasStable = + fromSymbol === 'USDT' || fromSymbol === 'USDC' || toSymbol === 'USDT' || toSymbol === 'USDC'; + if (hasStable) return 5; // 0.05% + return 10; // 0.10% +} + +export function getSwapToken(symbol: SwapTokenSymbol): SwapTokenConfig { + return SWAP_TOKENS[symbol]; +} + +export function isErc20SwapToken(symbol: SwapTokenSymbol): boolean { + return !SWAP_TOKENS[symbol].isNative; +} diff --git a/apps/web/src/lib/swap/errors.ts b/apps/web/src/lib/swap/errors.ts new file mode 100644 index 0000000..213066e --- /dev/null +++ b/apps/web/src/lib/swap/errors.ts @@ -0,0 +1,23 @@ +import type { SwapChain } from './constants'; + +export function mapSwapError(chain: SwapChain, error: unknown): string { + const msg = error instanceof Error ? error.message : String(error); + + // Jupiter / SOL + if (msg.includes('INSUFFICIENT_FUNDS') || msg.includes('InsufficientFunds')) return 'Insufficient SOL balance'; + if (msg.includes('SlippageToleranceExceeded') || msg.includes('Slippage')) return 'Slippage exceeded — try increasing tolerance'; + + // SunSwap / TRX + if (msg.includes('BANDWIDTH_ERROR') || msg.includes('bandwidth')) return 'Insufficient bandwidth — need to freeze TRX'; + if (msg.includes('ENERGY_ERROR') || msg.includes('energy')) return 'Insufficient energy — need TRX for gas'; + if (msg.includes('CONTRACT_VALIDATE_ERROR')) return 'Transaction validation failed on TRON'; + if (msg.includes('balance is not sufficient')) return 'Insufficient token balance'; + + // Generic + if (msg.includes('timeout') || msg.includes('timed out')) return 'Request timed out — please try again'; + if (msg.includes('429') || msg.includes('rate')) return 'Rate limited — please wait and retry'; + if (msg.includes('No route') || msg.includes('No Uniswap route')) return 'No swap route found for this pair'; + if (msg.includes('not allowed') || msg.includes('403')) return 'API access restricted'; + + return msg; +} diff --git a/apps/web/src/lib/swap/execute.ts b/apps/web/src/lib/swap/execute.ts new file mode 100644 index 0000000..4807eb1 --- /dev/null +++ b/apps/web/src/lib/swap/execute.ts @@ -0,0 +1,131 @@ +import { ethers } from 'ethers'; +import { Percent } from '@uniswap/sdk-core'; +import { SwapRouter, TokenTransferMode } from '@uniswap/universal-router-sdk'; +import { createEthProvider } from '@/lib/eth-provider'; +import { + DEFAULT_DEADLINE_SECONDS, + ETHEREUM_CHAIN_ID, + FEE_RECIPIENT, + FEE_SWAP_ROUTER_ETH, + PLATFORM_FEE_BPS, + SWAP_PROXY_ADDRESS_MAINNET, + UNIVERSAL_ROUTER_ADDRESS_MAINNET, + UNIVERSAL_ROUTER_VERSION, + getSwapToken, + type SwapQuoteRequest, +} from './constants'; +import type { SwapQuoteResult } from './quote'; + +const provider = createEthProvider(); + +export interface ExecuteSwapParams { + privateKey: string; + request: SwapQuoteRequest; + quote: SwapQuoteResult; + maxFeeGwei?: string | null; + priorityFeeGwei?: string | null; +} + +export interface ExecuteSwapResult { + hash: string; + explorerUrl: string; + submittedTo: string; +} + +export async function executeSwap(params: ExecuteSwapParams): Promise { + const wallet = new ethers.Wallet(params.privateKey, provider); + const inputToken = getSwapToken(params.request.fromSymbol); + const slippageTolerance = new Percent(params.request.slippageBps, 10_000); + const deadline = Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS; + + const routerOptions = { + recipient: wallet.address, + slippageTolerance, + deadlineOrPreviousBlockhash: deadline, + urVersion: UNIVERSAL_ROUTER_VERSION, + ...(inputToken.isNative + ? {} + : { + tokenTransferMode: TokenTransferMode.ApproveProxy, + chainId: ETHEREUM_CHAIN_ID, + }), + }; + + const methodParameters = SwapRouter.swapCallParameters(params.quote.trade, routerOptions); + const feeOverrides = await getFeeOverrides(params.maxFeeGwei, params.priorityFeeGwei); + + let response: ethers.providers.TransactionResponse; + let submittedTo: string; + + if (inputToken.isNative) { + // ── Native ETH → Token: route through FeeSwapRouter contract ── + // Quote was built for 99.3% of user's amount. We send the full original + // amount to the contract — it takes 0.7% fee and forwards 99.3% + calldata + // to the Universal Router. + const fullAmountRaw = ethers.utils.parseUnits(params.request.amount, 18); + const feeRouterIface = new ethers.utils.Interface([ + 'function swapNativeWithFee(bytes calldata routerCalldata) external payable', + ]); + const data = feeRouterIface.encodeFunctionData('swapNativeWithFee', [ + methodParameters.calldata, + ]); + + submittedTo = FEE_SWAP_ROUTER_ETH; + response = await wallet.sendTransaction({ + to: FEE_SWAP_ROUTER_ETH, + data, + value: fullAmountRaw, + ...feeOverrides, + }); + } else { + // ── ERC20 → Token: send 0.7% fee separately, then swap 99.3% normally ── + const token = getSwapToken(params.request.fromSymbol); + const fullAmountRaw = ethers.utils.parseUnits(params.request.amount, token.decimals); + const feeAmount = fullAmountRaw.mul(PLATFORM_FEE_BPS).div(10000); + + // Send 0.7% fee to fee wallet + const tokenContract = new ethers.Contract( + token.address, + ['function transfer(address to, uint256 amount) returns (bool)'], + wallet, + ); + const feeTx = await tokenContract.transfer(FEE_RECIPIENT, feeAmount, feeOverrides); + await feeTx.wait(); + + // Execute swap with 99% (calldata already built for adjusted amount) + submittedTo = SWAP_PROXY_ADDRESS_MAINNET; + response = await wallet.sendTransaction({ + to: submittedTo, + data: methodParameters.calldata, + value: methodParameters.value, + ...feeOverrides, + }); + } + + const receipt = await response.wait(); + if (!receipt || receipt.status !== 1) { + throw new Error('Swap transaction reverted'); + } + + return { + hash: response.hash, + explorerUrl: `https://etherscan.io/tx/${response.hash}`, + submittedTo, + }; +} + +async function getFeeOverrides(maxFeeGwei?: string | null, priorityFeeGwei?: string | null) { + if (maxFeeGwei?.trim()) { + return { + maxFeePerGas: ethers.utils.parseUnits(maxFeeGwei, 'gwei'), + maxPriorityFeePerGas: ethers.utils.parseUnits(priorityFeeGwei?.trim() || '0.01', 'gwei'), + }; + } + + const feeData = await provider.getFeeData(); + + return { + ...(feeData.maxFeePerGas ? { maxFeePerGas: feeData.maxFeePerGas } : {}), + ...(feeData.maxPriorityFeePerGas ? { maxPriorityFeePerGas: feeData.maxPriorityFeePerGas } : {}), + }; +} diff --git a/apps/web/src/lib/swap/quote.ts b/apps/web/src/lib/swap/quote.ts new file mode 100644 index 0000000..bd91878 --- /dev/null +++ b/apps/web/src/lib/swap/quote.ts @@ -0,0 +1,365 @@ +import { ethers } from 'ethers'; +import { CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'; +import { Pool, Route, TICK_SPACINGS, encodeRouteToPath } from '@uniswap/v3-sdk'; +import { Trade as RouterTrade } from '@uniswap/router-sdk'; +import { webEnv } from '@/lib/env'; +import { + ETHEREUM_CHAIN_ID, + PLATFORM_FEE_BPS, + QUOTER_V2_ABI, + QUOTER_V2_ADDRESS, + SWAP_REQUEST_TIMEOUT_MS, + SWAP_TOKENS, + V3_POOL_CANDIDATES, + UNISWAP_V3_POOL_ABI, + getSlippageBps, + type PoolCandidate, + type SwapQuoteRequest, + type SwapTokenSymbol, +} from './constants'; + +/** TickDataProvider that fetches tick data from the Uniswap V3 pool contract */ +function createContractTickDataProvider( + poolContract: ethers.Contract, + tickSpacing: number +): { getTick: (tick: number) => Promise<{ liquidityNet: string }>; nextInitializedTickWithinOneWord: (tick: number, lte: boolean, _tickSpacing: number) => Promise<[number, boolean]> } { + async function getTick(tick: number): Promise<{ liquidityNet: string }> { + const result = await poolContract.ticks(tick); + return { liquidityNet: result.liquidityNet.toString() }; + } + + async function nextInitializedTickWithinOneWord(tick: number, lte: boolean, _tickSpacing: number): Promise<[number, boolean]> { + const spacing = tickSpacing; + let compressed = Math.trunc(tick / spacing); + if (tick < 0 && tick % spacing !== 0) compressed--; + const wordPos = Math.floor(compressed / 256); + const bitPos = ((compressed % 256) + 256) % 256; + const word = (await poolContract.tickBitmap(wordPos)) as ethers.BigNumber; + const w = BigInt(word.toString()); + let masked: bigint; + let nextCompressed: number; + if (lte) { + const mask = (1n << BigInt(bitPos + 1)) - 1n; + masked = w & mask; + const initialized = masked !== 0n; + if (initialized) { + let msbPos = 0; + for (let i = 255; i >= 0; i--) { + if ((masked >> BigInt(i)) & 1n) { + msbPos = i; + break; + } + } + nextCompressed = compressed - (bitPos - msbPos); + } else { + nextCompressed = compressed - bitPos; + } + return [nextCompressed * spacing, masked !== 0n]; + } else { + const nextWordPos = Math.floor((compressed + 1) / 256); + const nextBitPos = (((compressed + 1) % 256) + 256) % 256; + const nextWord = (await poolContract.tickBitmap(nextWordPos)) as ethers.BigNumber; + const nw = BigInt(nextWord.toString()); + const mask = ~((1n << BigInt(nextBitPos)) - 1n); + masked = nw & mask; + const initialized = masked !== 0n; + if (initialized) { + let lsbPos = 0; + for (let i = 0; i <= 255; i++) { + if ((masked >> BigInt(i)) & 1n) { + lsbPos = i; + break; + } + } + nextCompressed = compressed + 1 + (lsbPos - nextBitPos); + } else { + nextCompressed = compressed + 1 + (255 - nextBitPos); + } + return [nextCompressed * spacing, initialized]; + } + } + + return { getTick, nextInitializedTickWithinOneWord }; +} + +const ETH_RPC_CANDIDATES = [ + ...new Set([ + webEnv.ethRpcUrl, + 'https://ethereum-rpc.publicnode.com', + 'https://rpc.ankr.com/eth', + 'https://eth.llamarpc.com', + ].filter(Boolean)), +]; + +const poolCache = new Map(); +const poolCacheTimestamps = new Map(); +const POOL_CACHE_TTL_MS = 30_000; // 30 seconds + +async function getHealthyProvider(): Promise { + let lastError: unknown = new Error('No Ethereum RPC available'); + for (const rpcUrl of ETH_RPC_CANDIDATES) { + if (!rpcUrl?.trim()) continue; + const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl, ETHEREUM_CHAIN_ID); + try { + await withTimeout( + provider.getBlockNumber(), + 4_000, + 'RPC health-check timed out' + ); + return provider; + } catch (e) { + lastError = e; + } + } + throw lastError; +} + +export interface SwapQuoteResult { + trade: RouterTrade; + amountInRaw: string; + amountInFormatted: string; + amountOutRaw: string; + amountOutFormatted: string; + minimumAmountOutRaw: string; + minimumAmountOutFormatted: string; + executionPrice: string; + priceImpact: string; + routeSymbols: SwapTokenSymbol[]; + routeFees: number[]; +} + +/** + * Query the on-chain Quoter for each candidate pool and pick the best output. + * This replaces the fragile off-chain bestTradeExactIn simulation that fails + * with RATIO_CURRENT when tick data is stale. + */ +async function findBestRouteOnChain( + pools: Pool[], + inputToken: (typeof SWAP_TOKENS)[SwapTokenSymbol], + outputToken: (typeof SWAP_TOKENS)[SwapTokenSymbol], + amountInRaw: ethers.BigNumber, + quoter: ethers.Contract, +): Promise<{ bestPool: Pool; bestAmountOut: ethers.BigNumber; bestRoutePath: string }> { + const inputCurrency = inputToken.currency; + const outputCurrency = outputToken.currency; + + // Filter pools that contain both input and output tokens + const relevantPools = pools.filter((pool) => { + const t0 = pool.token0.address.toLowerCase(); + const t1 = pool.token1.address.toLowerCase(); + const inAddr = (inputToken.wrappedToken?.address ?? inputToken.address).toLowerCase(); + const outAddr = (outputToken.wrappedToken?.address ?? outputToken.address).toLowerCase(); + return (t0 === inAddr || t1 === inAddr) && (t0 === outAddr || t1 === outAddr); + }); + + if (!relevantPools.length) { + throw new Error('No Uniswap pool available for this token pair'); + } + + let bestPool: Pool | null = null; + let bestAmountOut = ethers.BigNumber.from(0); + let bestRoutePath = ''; + + for (const pool of relevantPools) { + try { + const route = new Route([pool], inputCurrency, outputCurrency); + const routePath = encodeRouteToPath(route, false); + const [amountOut] = await withTimeout( + quoter.callStatic.quoteExactInput(routePath, amountInRaw), + SWAP_REQUEST_TIMEOUT_MS, + 'Quote timed out', + ); + if (amountOut.gt(bestAmountOut)) { + bestPool = pool; + bestAmountOut = amountOut; + bestRoutePath = routePath; + } + } catch { + // Pool doesn't have enough liquidity or RPC issue — skip it + continue; + } + } + + if (!bestPool) { + throw new Error('No Uniswap route found for this token pair'); + } + + return { bestPool, bestAmountOut, bestRoutePath }; +} + +export async function getSwapQuote(request: SwapQuoteRequest): Promise { + validateSwapRequest(request); + + const provider = await getHealthyProvider(); + const inputToken = SWAP_TOKENS[request.fromSymbol]; + const outputToken = SWAP_TOKENS[request.toSymbol]; + const originalAmountInRaw = ethers.utils.parseUnits(request.amount, inputToken.decimals); + // Apply 0.7% platform fee — swap only 99.3% of input (BigNumber math, no float) + const amountInRaw = originalAmountInRaw.mul(10000 - PLATFORM_FEE_BPS).div(10000); + const amountIn = CurrencyAmount.fromRawAmount(inputToken.currency, amountInRaw.toString()); + const pools = await loadCandidatePools(provider); + const quoter = new ethers.Contract(QUOTER_V2_ADDRESS, QUOTER_V2_ABI, provider); + + // Find best route via on-chain Quoter (avoids RATIO_CURRENT from off-chain simulation) + const { bestPool, bestAmountOut, bestRoutePath } = await findBestRouteOnChain( + pools, inputToken, outputToken, amountInRaw, quoter, + ); + + const route = new Route([bestPool], inputToken.currency, outputToken.currency); + const outputAmount = CurrencyAmount.fromRawAmount(outputToken.currency, bestAmountOut.toString()); + const routerTrade = new RouterTrade({ + v3Routes: [{ + routev3: route, + inputAmount: amountIn, + outputAmount, + }], + tradeType: TradeType.EXACT_INPUT, + }); + + const slippageTolerance = new Percent(request.slippageBps, 10_000); + const minimumAmountOut = routerTrade.minimumAmountOut(slippageTolerance); + const outputDecimals = outputToken.decimals; + + return { + trade: routerTrade, + amountInRaw: amountInRaw.toString(), + amountInFormatted: request.amount, + amountOutRaw: bestAmountOut.toString(), + amountOutFormatted: formatUnitsSafe(bestAmountOut.toString(), outputDecimals), + minimumAmountOutRaw: minimumAmountOut.quotient.toString(), + minimumAmountOutFormatted: formatUnitsSafe(minimumAmountOut.quotient.toString(), outputDecimals), + executionPrice: routerTrade.executionPrice.toSignificant(6), + priceImpact: routerTrade.priceImpact.toFixed(4), + routeSymbols: route.tokenPath.map((token) => tokenAddressToSymbol(token.address)), + routeFees: route.pools.map((pool) => pool.fee), + }; +} + +async function loadCandidatePools(provider: ethers.providers.StaticJsonRpcProvider): Promise { + const settled = await Promise.allSettled(V3_POOL_CANDIDATES.map((c) => loadPool(c, provider))); + const pools = settled + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value); + + if (!pools.length) { + const errors = settled + .filter((r): r is PromiseRejectedResult => r.status === 'rejected') + .map((r) => r.reason?.message ?? String(r.reason)); + const hasRpcIssue = errors.some( + (e) => e?.includes('Failed to fetch') || e?.includes('timeout') || e?.includes('ECONNREFUSED') + ); + const hint = hasRpcIssue + ? ' Check your connection or try a different RPC in NEXT_PUBLIC_ETH_RPC_URL.' + : ''; + throw new Error(`No supported Uniswap pools are currently reachable.${hint}`); + } + + return pools; +} + +async function loadPool(candidate: PoolCandidate, provider: ethers.providers.StaticJsonRpcProvider): Promise { + const cacheKey = `${candidate.tokenA.address}-${candidate.tokenB.address}-${candidate.fee}`; + const cachedPool = poolCache.get(cacheKey); + const cachedAt = poolCacheTimestamps.get(cacheKey) ?? 0; + + if (cachedPool && Date.now() - cachedAt < POOL_CACHE_TTL_MS) { + return cachedPool; + } + + const poolAddress = Pool.getAddress(candidate.tokenA, candidate.tokenB, candidate.fee); + const code = await withTimeout( + provider.getCode(poolAddress), + SWAP_REQUEST_TIMEOUT_MS, + 'Pool discovery timed out' + ); + + if (!code || code === '0x') { + throw new Error(`Pool ${poolAddress} is unavailable`); + } + + const poolContract = new ethers.Contract(poolAddress, UNISWAP_V3_POOL_ABI, provider); + const [liquidity, slot0] = await Promise.all([ + withTimeout(poolContract.liquidity() as Promise, SWAP_REQUEST_TIMEOUT_MS, 'Pool liquidity request timed out'), + withTimeout( + poolContract.slot0() as Promise<{ sqrtPriceX96: bigint; tick: number }>, + SWAP_REQUEST_TIMEOUT_MS, + 'Pool slot0 request timed out' + ), + ]); + + const tickSpacing = TICK_SPACINGS[candidate.fee]; + const tickDataProvider = createContractTickDataProvider(poolContract, tickSpacing); + const pool = new Pool( + candidate.tokenA, + candidate.tokenB, + candidate.fee, + slot0.sqrtPriceX96.toString(), + liquidity.toString(), + slot0.tick, + tickDataProvider + ); + + poolCache.set(cacheKey, pool); + poolCacheTimestamps.set(cacheKey, Date.now()); + return pool; +} + +function validateSwapRequest(request: SwapQuoteRequest): void { + if (request.fromSymbol === request.toSymbol) { + throw new Error('Select two different tokens'); + } + + if (!request.amount || Number(request.amount) <= 0) { + throw new Error('Enter a valid swap amount'); + } + + if (!Number.isFinite(request.slippageBps) || request.slippageBps <= 0) { + throw new Error('Enter a valid slippage value'); + } +} + +function tokenAddressToSymbol(address: string): SwapTokenSymbol { + const normalizedAddress = address.toLowerCase(); + const matched = Object.values(SWAP_TOKENS).find((token) => { + if (token.address === 'native') { + return token.wrappedToken.address.toLowerCase() === normalizedAddress; + } + + return token.address.toLowerCase() === normalizedAddress; + }); + + return matched?.symbol ?? 'ETH'; +} + +function formatUnitsSafe(rawValue: bigint | string, decimals: number): string { + const bigintValue = typeof rawValue === 'bigint' ? rawValue : BigInt(rawValue); + if (bigintValue === 0n) { + return '0'; + } + + const divisor = 10n ** BigInt(decimals); + const whole = bigintValue / divisor; + const fraction = bigintValue % divisor; + + if (fraction === 0n) { + return whole.toString(); + } + + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} + +function withTimeout(promise: Promise, timeoutMs: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs); + + promise + .then((value) => { + clearTimeout(timeoutId); + resolve(value); + }) + .catch((error) => { + clearTimeout(timeoutId); + reject(error); + }); + }); +} diff --git a/apps/web/src/lib/swap/sol/execute.ts b/apps/web/src/lib/swap/sol/execute.ts new file mode 100644 index 0000000..d295150 --- /dev/null +++ b/apps/web/src/lib/swap/sol/execute.ts @@ -0,0 +1,65 @@ +import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js'; +import { webEnv } from '@/lib/env'; + +export interface SolExecuteSwapParams { + privateKeyHex: string; + userPublicKey: string; + quoteResponse: Record; +} + +export interface SolSwapResult { + hash: string; + explorerUrl: string; +} + +export async function executeSolSwap(params: SolExecuteSwapParams): Promise { + const { privateKeyHex, userPublicKey, quoteResponse } = params; + + // 1. Build swap transaction via backend proxy + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const buildResponse = await fetch(`${apiUrl}/api/sol/swap/build`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ quoteResponse, userPublicKey }), + }); + + if (!buildResponse.ok) { + const body = await buildResponse.json().catch(() => ({ error: 'Failed to build swap' })); + throw new Error(body.error || `Swap build failed (${buildResponse.status})`); + } + + const { swapTransaction } = await buildResponse.json(); + if (!swapTransaction) { + throw new Error('No swap transaction returned from Jupiter'); + } + + // 2. Deserialize the VersionedTransaction + const txBuffer = Buffer.from(swapTransaction, 'base64'); + const transaction = VersionedTransaction.deserialize(txBuffer); + + // 3. Sign with user's Keypair + const keypair = Keypair.fromSecretKey(Buffer.from(privateKeyHex, 'hex')); + transaction.sign([keypair]); + + // 4. Send to Solana RPC + const connection = new Connection(webEnv.solRpcUrl, 'confirmed'); + const rawTx = transaction.serialize(); + + const signature = await connection.sendRawTransaction(rawTx, { + skipPreflight: false, + maxRetries: 2, + }); + + // 5. Confirm transaction + const latestBlockhash = await connection.getLatestBlockhash(); + await connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, 'confirmed'); + + return { + hash: signature, + explorerUrl: `https://solscan.io/tx/${signature}`, + }; +} diff --git a/apps/web/src/lib/swap/sol/quote.ts b/apps/web/src/lib/swap/sol/quote.ts new file mode 100644 index 0000000..60b7c95 --- /dev/null +++ b/apps/web/src/lib/swap/sol/quote.ts @@ -0,0 +1,99 @@ +import { SOL_TOKEN_MINTS, SOL_TOKEN_DECIMALS } from '../constants'; +import { webEnv } from '@/lib/env'; + +export interface SolSwapQuoteResult { + chain: 'SOL'; + quoteResponse: Record; + amountInRaw: string; + amountInFormatted: string; + amountOutRaw: string; + amountOutFormatted: string; + minimumAmountOutRaw: string; + minimumAmountOutFormatted: string; + priceImpact: string; + routeLabels: string[]; +} + +export interface SolSwapQuoteRequest { + fromSymbol: string; + toSymbol: string; + amount: string; + slippageBps: number; +} + +export async function getSolSwapQuote(request: SolSwapQuoteRequest): Promise { + const inputMint = SOL_TOKEN_MINTS[request.fromSymbol]; + const outputMint = SOL_TOKEN_MINTS[request.toSymbol]; + + if (!inputMint || !outputMint) { + throw new Error(`Unknown SOL token: ${request.fromSymbol} or ${request.toSymbol}`); + } + + const fromDecimals = SOL_TOKEN_DECIMALS[request.fromSymbol]; + const toDecimals = SOL_TOKEN_DECIMALS[request.toSymbol]; + + // Convert human-readable amount to raw (lamports / smallest unit) + const amountRaw = parseUnitsToRaw(request.amount, fromDecimals); + + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const url = new URL(`${apiUrl}/api/sol/swap/quote`); + url.searchParams.set('inputMint', inputMint); + url.searchParams.set('outputMint', outputMint); + url.searchParams.set('amount', amountRaw); + url.searchParams.set('slippageBps', String(request.slippageBps)); + + const response = await fetch(url.toString()); + + if (!response.ok) { + const body = await response.json().catch(() => ({ error: 'Jupiter API error' })); + throw new Error(body.error || `Jupiter quote failed (${response.status})`); + } + + const data = await response.json(); + + // Jupiter response fields + const outAmount = String(data.outAmount ?? '0'); + const otherAmountThreshold = String(data.otherAmountThreshold ?? '0'); + const priceImpactPct = String(data.priceImpactPct ?? '0'); + + // Extract route labels + const routeLabels: string[] = []; + if (Array.isArray(data.routePlan)) { + for (const step of data.routePlan) { + if (step.swapInfo?.label) routeLabels.push(step.swapInfo.label); + } + } + + return { + chain: 'SOL', + quoteResponse: data, + amountInRaw: amountRaw, + amountInFormatted: request.amount, + amountOutRaw: outAmount, + amountOutFormatted: formatRawUnits(outAmount, toDecimals), + minimumAmountOutRaw: otherAmountThreshold, + minimumAmountOutFormatted: formatRawUnits(otherAmountThreshold, toDecimals), + priceImpact: priceImpactPct, + routeLabels, + }; +} + +function parseUnitsToRaw(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + let fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + const raw = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction); + return raw.toString(); +} + +function formatRawUnits(raw: string, decimals: number): string { + const value = BigInt(raw); + if (value === 0n) return '0'; + + const divisor = 10n ** BigInt(decimals); + const whole = value / divisor; + const fraction = value % divisor; + + if (fraction === 0n) return whole.toString(); + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} diff --git a/apps/web/src/lib/swap/trx/execute.ts b/apps/web/src/lib/swap/trx/execute.ts new file mode 100644 index 0000000..15259b9 --- /dev/null +++ b/apps/web/src/lib/swap/trx/execute.ts @@ -0,0 +1,91 @@ +import { ethers, utils } from 'ethers'; +import { webEnv } from '@/lib/env'; + +export interface TrxExecuteSwapParams { + privateKeyHex: string; // 32-byte secp256k1 key (hex, no 0x prefix) + from: string; + to: string; + amount: string; // raw amount in sun + amountOutMin: string; // raw minimum output + userAddress: string; // TRX base58 address +} + +export interface TrxSwapResult { + hash: string; + explorerUrl: string; + approvalHashes: string[]; +} + +export async function executeTrxSwap(params: TrxExecuteSwapParams): Promise { + const { privateKeyHex, from, to, amount, amountOutMin, userAddress } = params; + + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + + // 1. Build transaction(s) via backend + const buildResponse = await fetch(`${apiUrl}/api/tron/swap/build`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ from, to, amount, amountOutMin, userAddress }), + }); + + if (!buildResponse.ok) { + const body = await buildResponse.json().catch(() => ({ error: 'Failed to build TRX swap' })); + throw new Error(body.error || `TRX swap build failed (${buildResponse.status})`); + } + + const { transactions } = await buildResponse.json(); + if (!transactions || !transactions.length) { + throw new Error('No transactions returned from TRX swap builder'); + } + + // 2. Sign and broadcast each transaction in order + const signingKey = new utils.SigningKey('0x' + privateKeyHex); + const approvalHashes: string[] = []; + let swapHash = ''; + + for (const tx of transactions) { + // Sign the txID (which is SHA256 of raw_data) + const txID: string = tx.txID; + const digest = ethers.utils.arrayify('0x' + txID); + const signature = signingKey.signDigest(digest); + const sigHex = ethers.utils.joinSignature(signature).slice(2); // remove 0x, 65 bytes hex + + // Add signature to transaction + const signedTx = { + ...tx, + signature: [sigHex], + }; + + // Broadcast + const broadcastResponse = await fetch(`${apiUrl}/api/tron/swap/broadcast`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ signedTransaction: signedTx }), + }); + + const result = await broadcastResponse.json(); + + if (!result.result) { + const errorMsg = result.message || result.code || 'Broadcast failed'; + throw new Error(`TRX broadcast error: ${errorMsg}`); + } + + if (tx.type === 'approve') { + approvalHashes.push(txID); + // Wait a bit for approval to be confirmed before swapping + await delay(3000); + } else { + swapHash = txID; + } + } + + return { + hash: swapHash, + explorerUrl: `https://tronscan.org/#/transaction/${swapHash}`, + approvalHashes, + }; +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/apps/web/src/lib/swap/trx/quote.ts b/apps/web/src/lib/swap/trx/quote.ts new file mode 100644 index 0000000..eb805ab --- /dev/null +++ b/apps/web/src/lib/swap/trx/quote.ts @@ -0,0 +1,90 @@ +import { TRX_TOKEN_DECIMALS } from '../constants'; +import { webEnv } from '@/lib/env'; + +export interface TrxSwapQuoteResult { + chain: 'TRX'; + amountInRaw: string; + amountInFormatted: string; + amountOutRaw: string; + amountOutFormatted: string; + minimumAmountOutRaw: string; + minimumAmountOutFormatted: string; + from: string; + to: string; +} + +export interface TrxSwapQuoteRequest { + fromSymbol: string; + toSymbol: string; + amount: string; + slippageBps: number; +} + +export async function getTrxSwapQuote(request: TrxSwapQuoteRequest): Promise { + const fromDecimals = TRX_TOKEN_DECIMALS[request.fromSymbol]; + const toDecimals = TRX_TOKEN_DECIMALS[request.toSymbol]; + + if (fromDecimals === undefined || toDecimals === undefined) { + throw new Error(`Unknown TRX token: ${request.fromSymbol} or ${request.toSymbol}`); + } + + // Convert human-readable amount to raw (sun / smallest unit) + const amountRaw = parseUnitsToRaw(request.amount, fromDecimals); + + const apiUrl = webEnv.apiUrl || 'http://localhost:3001'; + const url = new URL(`${apiUrl}/api/tron/swap/quote`); + url.searchParams.set('from', request.fromSymbol); + url.searchParams.set('to', request.toSymbol); + url.searchParams.set('amount', amountRaw); + + const response = await fetch(url.toString()); + + if (!response.ok) { + const body = await response.json().catch(() => ({ error: 'TRX quote failed' })); + throw new Error(body.error || `TRX quote failed (${response.status})`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'TRX quote returned error'); + } + + const amountOut = String(data.amountOut); + + // Apply slippage to get minimum output + const amountOutBigInt = BigInt(amountOut); + const minOut = amountOutBigInt - (amountOutBigInt * BigInt(request.slippageBps) / 10000n); + + return { + chain: 'TRX', + amountInRaw: amountRaw, + amountInFormatted: request.amount, + amountOutRaw: amountOut, + amountOutFormatted: formatRawUnits(amountOut, toDecimals), + minimumAmountOutRaw: minOut.toString(), + minimumAmountOutFormatted: formatRawUnits(minOut.toString(), toDecimals), + from: request.fromSymbol, + to: request.toSymbol, + }; +} + +function parseUnitsToRaw(amount: string, decimals: number): string { + const parts = amount.split('.'); + const whole = parts[0] || '0'; + const fraction = (parts[1] || '').padEnd(decimals, '0').slice(0, decimals); + const raw = BigInt(whole) * 10n ** BigInt(decimals) + BigInt(fraction); + return raw.toString(); +} + +function formatRawUnits(raw: string, decimals: number): string { + const value = BigInt(raw); + if (value === 0n) return '0'; + + const divisor = 10n ** BigInt(decimals); + const whole = value / divisor; + const fraction = value % divisor; + + if (fraction === 0n) return whole.toString(); + return `${whole}.${fraction.toString().padStart(decimals, '0').replace(/0+$/, '')}`; +} diff --git a/apps/web/src/store/auth-store.ts b/apps/web/src/store/auth-store.ts new file mode 100644 index 0000000..ba048bb --- /dev/null +++ b/apps/web/src/store/auth-store.ts @@ -0,0 +1,48 @@ +'use client'; + +import { create } from 'zustand'; +import { api } from '@/lib/api'; + +interface Wallet { + chain: string; + address: string; + derivationPath: string; +} + +interface AuthState { + user: { id: string; email: string } | null; + wallets: Wallet[]; + loading: boolean; + error: string | null; + + init: () => Promise; + logout: () => void; + clearError: () => void; +} + +export const useAuthStore = create((set) => ({ + user: null, + wallets: [], + loading: false, + error: null, + + init: async () => { + set({ loading: true, error: null }); + try { + const wallets = await api.getWallets(); + set({ + user: { id: '', email: '' }, + wallets, + loading: false, + }); + } catch { + set({ user: null, wallets: [], loading: false }); + } + }, + + logout: () => { + set({ user: null, wallets: [] }); + }, + + clearError: () => set({ error: null }), +})); diff --git a/apps/web/src/store/balance-store.ts b/apps/web/src/store/balance-store.ts new file mode 100644 index 0000000..6bac879 --- /dev/null +++ b/apps/web/src/store/balance-store.ts @@ -0,0 +1,114 @@ +'use client'; + +import { create } from 'zustand'; +import type { DerivedWallet } from '@/lib/crypto/derive-keys'; +import { fetchAllBalances } from '@/lib/balances'; +import type { ChainBalance, PortfolioBalance } from '@/lib/balances/types'; + +interface BalanceState { + portfolio: PortfolioBalance | null; + loading: boolean; + refreshing: boolean; + error: string | null; + fetchBalances: (wallets: DerivedWallet[]) => Promise; + clearBalances: () => void; +} + +export const useBalanceStore = create((set, get) => ({ + portfolio: null, + loading: false, + refreshing: false, + error: null, + + fetchBalances: async (wallets) => { + if (!wallets.length) { + set({ portfolio: null, loading: false, refreshing: false, error: null }); + return; + } + + const hasPortfolio = !!get().portfolio; + set({ + loading: !hasPortfolio, + refreshing: hasPortfolio, + error: null, + }); + + try { + const fresh = await fetchAllBalances(wallets); + const prev = get().portfolio; + + // Merge: if a chain had an error but we have previous data, keep previous + const portfolio = prev ? mergePortfolios(prev, fresh) : fresh; + + set({ + portfolio, + loading: false, + refreshing: false, + error: null, + }); + } catch (error) { + const prev = get().portfolio; + set({ + portfolio: prev, + loading: false, + refreshing: false, + error: prev ? null : getErrorMessage(error), + }); + } + }, + + clearBalances: () => { + set({ + portfolio: null, + loading: false, + refreshing: false, + error: null, + }); + }, +})); + +/** + * If a chain in fresh data has an error and all its token balances are zero, + * but previous data had real balances, keep the previous chain data. + */ +function mergePortfolios(prev: PortfolioBalance, fresh: PortfolioBalance): PortfolioBalance { + const chains = fresh.chains.map((freshChain) => { + if (!freshChain.error) return freshChain; + + const prevChain = prev.chains.find((c) => c.chain === freshChain.chain); + if (!prevChain || prevChain.error) return freshChain; + + // Chain has error and all balances are zero — keep previous + const allZero = freshChain.tokens.every((t) => t.balanceRaw === '0'); + if (allZero) return prevChain; + + return freshChain; + }); + + const totalUsd = sumNullable(chains.map((c) => c.totalUsd)); + const errors: PortfolioBalance['errors'] = {}; + for (const chain of chains) { + if (chain.error && chain.error !== '__transient__') errors[chain.chain] = chain.error; + } + + return { + chains, + totalUsd, + errors, + priceError: fresh.priceError, + updatedAt: fresh.updatedAt, + }; +} + +function sumNullable(values: Array): number | null { + const filtered = values.filter((v): v is number => typeof v === 'number'); + return filtered.length ? filtered.reduce((a, b) => a + b, 0) : null; +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return 'Unable to refresh balances'; +} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..75d74a7 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/contracts/DEPLOY.md b/contracts/DEPLOY.md new file mode 100644 index 0000000..4a160d9 --- /dev/null +++ b/contracts/DEPLOY.md @@ -0,0 +1,240 @@ +# FeeSwapRouter — Инструкция по деплою + +## Четыре сети — четыре подхода + +| Сеть | Метод | DEX | Fee wallet | Комиссия | Нужен на газ | +|------|-------|-----|------------|----------|-------------| +| **Ethereum** (chainId 1) | Solidity контракт (Remix) | Uniswap Universal Router | `0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718` | ~~1%~~ → **0.7%** | ETH (~$5-20) | +| **BSC** (chainId 56) | Solidity контракт (Remix) | PancakeSwap V3 Smart Router | `0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718` | **0.7%** | BNB (~$0.50-1.00) | +| **TRON** | Solidity контракт (TronIDE) | SunSwap V2 Smart Router | `TYTfrem65362TFyQSARTheeYza1GQA37Ug` | **0.7%** | TRX (~50-150 TRX) | +| **Solana** | Jupiter Referral Program | Jupiter Aggregator | `Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ` | **0.7%** | SOL (~0.01 SOL) | + +Все адреса и проценты захардкожены — никаких параметров при деплое вводить НЕ НУЖНО. + +**Важно**: ETH контракт уже задеплоен с 1% комиссией. Если нужно 0.7%, нужно задеплоить новый контракт. + +--- + +## 1. Деплой FeeSwapRouter_ETH.sol (Ethereum) + +**Статус: ✅ Задеплоен (1%) — `0xbdC4A97C2814E496160638d87e1F1b14154e30b6`** +**⚠️ Нужен передеплой с 0.7% если хочешь 0.7% on-chain** + +### Что нужно +- MetaMask с ETH на балансе (на газ уйдёт ~$5-20) +- В MetaMask выбрана сеть **Ethereum Mainnet** + +### Шаги + +1. Открой https://remix.ethereum.org +2. Слева в File Explorer нажми **+** → создай файл `FeeSwapRouter_ETH.sol` +3. Вставь **ВЕСЬ** код из файла `contracts/FeeSwapRouter_ETH.sol` +4. Слева выбери вкладку **Solidity Compiler** (иконка с буквой S) + - Compiler: **0.8.20** + - **Enable optimization**: включи, runs: **200** + - Нажми **Compile FeeSwapRouter_ETH.sol** + - Должна появиться зелёная галочка (без ошибок) +5. Слева выбери вкладку **Deploy & Run Transactions** (иконка со стрелкой) + - Environment: **Injected Provider - MetaMask** + - Убедись что MetaMask на сети **Ethereum Mainnet** + - В выпадающем списке контрактов выбери **FeeSwapRouter_ETH** + - Нажми **Deploy** + - Подтверди транзакцию в MetaMask +6. После подтверждения — скопируй адрес контракта из консоли Remix +7. Сохрани адрес в `.env`: + ``` + FEE_SWAP_ROUTER_ETH=0x...твой_адрес... + ``` + +### Верификация на Etherscan +- Зайди на https://etherscan.io/verifyContract +- Адрес контракта: вставь адрес из шага 6 +- Compiler: Solidity 0.8.20 +- Optimization: Yes, 200 runs +- License: MIT +- Код: в Remix правой кнопкой на файл → **Flatten**, вставь результат + +--- + +## 2. Деплой FeeSwapRouter_BSC.sol (BSC) + +### Что нужно +- MetaMask с BNB на балансе (на газ уйдёт ~$0.50-1.00) +- В MetaMask добавлена сеть **BNB Smart Chain**: + - RPC: `https://bsc-dataseed.binance.org` + - Chain ID: `56` + - Symbol: `BNB` + - Explorer: `https://bscscan.com` + +### Шаги + +1. Открой https://remix.ethereum.org +2. Создай файл `FeeSwapRouter_BSC.sol` +3. Вставь **ВЕСЬ** код из файла `contracts/FeeSwapRouter_BSC.sol` +4. Compiler → **0.8.20**, optimization ON (200 runs), Compile +5. Deploy → Injected Provider → MetaMask на сети **BNB Smart Chain** +6. Выбери контракт **FeeSwapRouter_BSC**, нажми **Deploy**, подтверди в MetaMask +7. Скопируй адрес, сохрани в `.env`: + ``` + FEE_SWAP_ROUTER_BSC=0x...твой_адрес... + ``` + +### Верификация на BscScan +- Зайди на https://bscscan.com/verifyContract +- Compiler: Solidity 0.8.20 +- Optimization: Yes, 200 runs +- License: MIT +- Код: в Remix → Flatten → вставь результат + +--- + +## 3. Деплой FeeSwapRouter_TRX.sol (TRON) + +**Статус: ✅ Задеплоен — `TX8E6X7X1FWYRYuYR2LTvS7zm1KchcVs5E`** + +### Что нужно +- **TronLink** кошелёк (расширение для Chrome, аналог MetaMask для TRON) +- TRX на балансе (~50-150 TRX на газ) +- В TronLink выбрана сеть **TRON Mainnet** + +### Шаги + +1. Открой https://www.tronide.io (это Remix для TRON) +2. Слева в File Explorer нажми **+** → создай файл `FeeSwapRouter_TRX.sol` +3. Вставь **ВЕСЬ** код из файла `contracts/FeeSwapRouter_TRX.sol` +4. Compiler → **0.8.20**, optimization ON (200 runs), Compile + - ⚠️ TronIDE может не поддерживать OpenZeppelin импорты — но этот контракт + НЕ использует OpenZeppelin (ReentrancyGuard и Ownable реализованы вручную) +5. Deploy → Injected Provider → **TronLink** (должен быть установлен) + - Убедись что TronLink на сети **TRON Mainnet** + - В выпадающем списке выбери **FeeSwapRouter_TRX** + - Нажми **Deploy**, подтверди в TronLink +6. Скопируй адрес контракта (будет в формате T...) +7. Сохрани в `.env`: + ``` + FEE_SWAP_ROUTER_TRX=TX8E6X7X1FWYRYuYR2LTvS7zm1KchcVs5E + ``` + +### Верификация на TronScan +- Зайди на https://tronscan.org/#/contracts/verify +- Вставь адрес контракта (T...) +- Compiler: 0.8.20 +- Optimization: ON +- Код: скопируй весь контракт + +### Адреса в контракте (TRON base58 → hex) + +| Контракт | TRON адрес | Hex (в Solidity) | +|----------|-----------|-----------------| +| SunSwap V2 Router | `TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax` | `0x6e0617948fE030a7e4970f8389D4AD295F249b7e` | +| USDT (TRC-20) | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | `0xa614f803b6fd780986a42c78ec9c7f77e6ded13c` | +| WTRX | `TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR` | `0x891cdb91d149f23b1a45d9c5ca78a88d0cb44c18` | + +--- + +## 4. Настройка комиссии на Solana (Jupiter Referral) + +На Solana НЕ нужен смарт-контракт. Jupiter Aggregator **нативно поддерживает** платформенную комиссию через Referral Program. + +### Что нужно +- Кошелёк Solana с ~0.01 SOL (на ренту и газ) +- Fee wallet: `Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ` + +### Шаги + +#### Шаг 1: Создай Referral Account + +1. Зайди на https://referral.jup.ag +2. Подключи кошелёк `Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ` +3. Нажми **Create Referral Account** +4. Подтверди транзакцию (~0.003 SOL) +5. Скопируй **Referral Account Public Key** — это будет `JUPITER_REFERRAL_ACCOUNT` + +#### Шаг 2: Создай Token Fee Accounts + +Для каждого токена, с которого берётся комиссия, нужен отдельный fee token account: + +1. На странице https://referral.jup.ag перейди в **Claim Token Accounts** +2. Нажми **Create Token Account** для каждого нужного токена: + - **SOL** (wrapped): `So11111111111111111111111111111111111111112` + - **USDT**: `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` + - **USDC**: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` +3. Подтверди каждую транзакцию (~0.002 SOL каждая) + +#### Шаг 3: Сохрани в `.env` + +``` +JUPITER_REFERRAL_ACCOUNT=...твой_referral_account_pubkey... +JUPITER_FEE_BPS=70 +``` + +#### Как это работает + +Jupiter сам: +1. Считает 0.7% от суммы свапа +2. Отправляет комиссию на fee token account +3. Остальные 99.3% идут пользователю + +Комиссия накапливается на token fee accounts. Можно клеймить (забирать) через https://referral.jup.ag → **Claim**. + +--- + +## 5. Bridge комиссия (0.7%) + +Комиссия на бридж работает **off-chain** — перед мостом отправляется отдельная транзакция: + +| Сеть | Как берётся fee | Fee wallet | +|------|----------------|------------| +| ETH → * | Отдельный ETH/ERC20 transfer перед бриджем | `0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718` | +| SOL → * | Отдельный SOL transfer перед бриджем | `Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ` | +| TRX → * | Отдельный TRC20 transfer перед бриджем | `TYTfrem65362TFyQSARTheeYza1GQA37Ug` | + +Контракт **НЕ нужен** для bridge fee — логика встроена в код кошелька (`apps/web/src/lib/bridge/execute.ts`). + +--- + +## После деплоя — интеграция с кошельком + +После получения адресов контрактов: + +1. Добавить адреса в `.env`: + ``` + FEE_SWAP_ROUTER_ETH=0xbdC4A97C2814E496160638d87e1F1b14154e30b6 + FEE_SWAP_ROUTER_BSC=0xbdC4A97C2814E496160638d87e1F1b14154e30b6 + FEE_SWAP_ROUTER_TRX=TX8E6X7X1FWYRYuYR2LTvS7zm1KchcVs5E + JUPITER_REFERRAL_ACCOUNT=...referral_pubkey... + JUPITER_FEE_BPS=70 + ``` +2. ✅ BSC swap proxy — вызывает FeeSwapRouter_BSC +3. ✅ TRX swap proxy — вызывает FeeSwapRouter_TRX +4. Обновить SOL swap proxy — добавить `platformFeeBps=70` в Jupiter запросы +5. Approve токены должны идти на адрес FeeSwapRouter (не на DEX роутер) + +--- + +## Админ-функции (ETH, BSC, TRX контракты) + +Кошелёк, с которого задеплоил — это **owner**. Доступные функции: + +| Функция | Что делает | +|---------|-----------| +| `pause()` | Экстренная остановка всех свапов | +| `unpause()` | Возобновить свапы | +| `emergencyWithdrawNative()` | Вывести застрявший ETH/BNB/TRX | +| `emergencyWithdrawToken(address)` | Вывести застрявшие токены | + +Вызывать через: +- **ETH**: Etherscan → Write Contract +- **BSC**: BscScan → Write Contract +- **TRX**: TronScan → Write Contract + +**Важно**: комиссия (0.7%) и fee wallet захардкожены — их нельзя изменить. Это сделано специально для безопасности. + +--- + +## Порядок деплоя (рекомендуемый) + +1. ✅ ETH контракт (уже задеплоен, но с 1% — передеплоить если нужно 0.7%) +2. BSC контракт → через Remix + MetaMask +3. TRX контракт → через TronIDE + TronLink +4. SOL referral → через https://referral.jup.ag diff --git a/contracts/FeeSetup_SOL.md b/contracts/FeeSetup_SOL.md new file mode 100644 index 0000000..42b782a --- /dev/null +++ b/contracts/FeeSetup_SOL.md @@ -0,0 +1,113 @@ +# Solana — Настройка комиссии через Jupiter Referral Program + +На Solana **НЕ нужен** смарт-контракт. Jupiter Aggregator нативно поддерживает платформенную комиссию. + +## Как работает + +``` +User → Jupiter Swap → 99.3% пользователю + 0.7% на Referral Fee Account +``` + +Jupiter сам: +1. Считает 0.7% от суммы свапа +2. Отправляет комиссию на твой fee token account +3. Остальные 99.3% идут пользователю + +Комиссия накапливается на token fee accounts. Забирать (клеймить) можно через https://referral.jup.ag → **Claim**. + +--- + +## Настройка (одноразовая) + +### Шаг 1: Создай Referral Account + +1. Зайди на https://referral.jup.ag +2. Подключи кошелёк `Co43MKwqMRMCvhscVVrtQWvma87NEV7ba4cfo8cksgzJ` (fee wallet для SOL) +3. Нажми **Create Referral Account** +4. Подтверди транзакцию (~0.003 SOL) +5. Скопируй **Referral Account Public Key** + +### Шаг 2: Создай Token Fee Accounts + +Для каждого токена нужен отдельный fee token account: + +1. На https://referral.jup.ag → **Claim Token Accounts** +2. Нажми **Create Token Account** для каждого: + - **SOL** (wrapped): `So11111111111111111111111111111111111111112` + - **USDT**: `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` + - **USDC**: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` +3. Подтверди каждую транзакцию (~0.002 SOL каждая) + +### Шаг 3: Добавь в .env + +```env +JUPITER_REFERRAL_ACCOUNT=...твой_referral_account_pubkey... +JUPITER_FEE_BPS=70 +``` + +--- + +## Как поменять комиссию + +Комиссия задаётся **одной переменной** в `.env`: + +```env +JUPITER_FEE_BPS=70 # 0.7% (текущая) +``` + +Примеры значений: + +| JUPITER_FEE_BPS | Комиссия | +|-----------------|----------| +| 10 | 0.1% | +| 25 | 0.25% | +| 50 | 0.5% | +| **70** | **0.7%** | +| 100 | 1.0% | +| 200 | 2.0% | + +**Чтобы поменять**: просто измени `JUPITER_FEE_BPS` в `.env` и перезапусти сервер. Передеплой не нужен. + +**Максимум**: Jupiter позволяет до 255 BPS (2.55%). + +--- + +## Где это в коде + +Файл: `apps/api/src/routes/sol-swap-proxy.routes.ts` + +**Quote** — передаёт `platformFeeBps` в Jupiter API: +``` +GET /quote?...&platformFeeBps=70 +``` +Jupiter возвращает quote уже с учётом комиссии — пользователь видит реальный выход. + +**Build** — передаёт `feeAccount` (Referral Account) в Jupiter Swap API: +```json +{ + "quoteResponse": {...}, + "userPublicKey": "...", + "feeAccount": "...referral_account_pubkey..." +} +``` +Jupiter встраивает инструкцию перевода комиссии прямо в транзакцию свапа. + +--- + +## Отличие от ETH/BSC/TRX + +| | ETH / BSC / TRX | Solana | +|---|---|---| +| Контракт | Свой FeeSwapRouter | Не нужен | +| Комиссия | Захардкожена в контракте | Настраивается через env | +| Изменение % | Нужен передеплой контракта | Поменять env + перезапуск | +| Где копится fee | На fee wallet напрямую | На Jupiter fee token accounts | +| Как забрать fee | Уже на кошельке | Claim через referral.jup.ag | + +--- + +## Стоимость + +- Создание Referral Account: ~0.003 SOL (~$0.50) +- Каждый Token Fee Account: ~0.002 SOL (~$0.30) +- Итого на 3 токена (SOL, USDT, USDC): ~0.009 SOL (~$1.50) diff --git a/contracts/FeeSwapRouter_BSC.sol b/contracts/FeeSwapRouter_BSC.sol new file mode 100644 index 0000000..aec8508 --- /dev/null +++ b/contracts/FeeSwapRouter_BSC.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title FeeSwapRouter_BSC +/// @notice Обёртка для PancakeSwap V3 Smart Router на BSC. Берёт 0.7% комиссию +/// с каждого свапа и отправляет на fee wallet. Остальные 99.3% + calldata +/// идут на Smart Router как есть. +/// @dev Работает с любой версией PancakeSwap (V2/V3/Smart Router), +/// потому что просто пересылает calldata без разбора. +contract FeeSwapRouter_BSC is Ownable, ReentrancyGuard, Pausable { + using SafeERC20 for IERC20; + + // ── Захардкоженные адреса для BSC ── + + /// @notice PancakeSwap V3 Smart Router на BSC + address public constant SMART_ROUTER = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4; + + /// @notice Permit2 на BSC (общий для PancakeSwap и Uniswap) + address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + /// @notice Кошелёк, получающий 0.7% комиссию + address public constant FEE_RECIPIENT = 0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718; + + /// @notice Комиссия 0.7% = 70 basis points (нельзя изменить) + uint16 public constant FEE_BPS = 70; + + /// @notice Делитель для basis points + uint16 private constant BPS_DENOMINATOR = 10_000; + + // ── Events ── + + event SwapWithFeeNative( + address indexed user, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event SwapWithFeeToken( + address indexed user, + address indexed tokenIn, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event EmergencyWithdrawNative(address indexed to, uint256 amount); + event EmergencyWithdrawToken(address indexed token, address indexed to, uint256 amount); + + // ── Errors ── + + error InsufficientValue(); + error NativeTransferFailed(); + error SwapFailed(); + error ZeroAmount(); + error ZeroAddress(); + error WrongChain(); + + // ── Constructor ── + + constructor() Ownable(msg.sender) { + if (block.chainid != 56) revert WrongChain(); + } + + /// @notice Свап BNB → токен через PancakeSwap Smart Router с 1% комиссией. + /// @param routerCalldata Calldata сгенерированная PancakeSwap SDK. + /// Передаётся как есть. + function swapNativeWithFee( + bytes calldata routerCalldata + ) external payable nonReentrant whenNotPaused { + if (msg.value == 0) revert InsufficientValue(); + + uint256 feeAmount = (msg.value * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = msg.value - feeAmount; + + // 1% → fee wallet + if (feeAmount > 0) { + (bool feeSent, ) = FEE_RECIPIENT.call{value: feeAmount}(""); + if (!feeSent) revert NativeTransferFailed(); + } + + // 99% + calldata → PancakeSwap Smart Router + (bool success, ) = SMART_ROUTER.call{value: swapAmount}(routerCalldata); + if (!success) revert SwapFailed(); + + emit SwapWithFeeNative(msg.sender, msg.value, feeAmount, swapAmount, SMART_ROUTER); + } + + + /// @notice Свап токен → BNB через PancakeSwap Smart Router с 1% комиссией. + /// @param tokenIn Адрес входного токена (USDT, DOGE, и т.д.) + /// @param amountIn Полная сумма токенов (включая 1% комиссию) + /// @param routerCalldata Calldata для Smart Router + function swapTokenWithFee( + address tokenIn, + uint256 amountIn, + bytes calldata routerCalldata + ) external nonReentrant whenNotPaused { + if (amountIn == 0) revert ZeroAmount(); + if (tokenIn == address(0)) revert ZeroAddress(); + + // Забираем токены у пользователя + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + + uint256 feeAmount = (amountIn * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = amountIn - feeAmount; + + // 1% комиссию → fee wallet + if (feeAmount > 0) { + IERC20(tokenIn).safeTransfer(FEE_RECIPIENT, feeAmount); + } + + // Approve 99% на Permit2 (PancakeSwap Swap Proxy) + IERC20(tokenIn).forceApprove(PERMIT2, swapAmount); + + // Calldata → Smart Router + (bool success, ) = SMART_ROUTER.call(routerCalldata); + if (!success) revert SwapFailed(); + + emit SwapWithFeeToken(msg.sender, tokenIn, amountIn, feeAmount, swapAmount, SMART_ROUTER); + } + + // ── Admin: Emergency ── + + function pause() external onlyOwner { _pause(); } + function unpause() external onlyOwner { _unpause(); } + + function emergencyWithdrawNative() external onlyOwner { + uint256 balance = address(this).balance; + if (balance == 0) revert ZeroAmount(); + (bool sent, ) = owner().call{value: balance}(""); + if (!sent) revert NativeTransferFailed(); + emit EmergencyWithdrawNative(owner(), balance); + } + + function emergencyWithdrawToken(address token) external onlyOwner { + if (token == address(0)) revert ZeroAddress(); + uint256 balance = IERC20(token).balanceOf(address(this)); + if (balance == 0) revert ZeroAmount(); + IERC20(token).safeTransfer(owner(), balance); + emit EmergencyWithdrawToken(token, owner(), balance); + } + + /// @notice Принимаем BNB от PancakeSwap (рефанды, выходные средства) + receive() external payable {} +} diff --git a/contracts/FeeSwapRouter_ETH.sol b/contracts/FeeSwapRouter_ETH.sol new file mode 100644 index 0000000..d9285d3 --- /dev/null +++ b/contracts/FeeSwapRouter_ETH.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// ╔══════════════════════════════════════════════════════════════╗ +// ║ FeeSwapRouter — ETHEREUM MAINNET ║ +// ║ ║ +// ║ Деплоить на: Ethereum Mainnet (chainId 1) ║ +// ║ Нужен: ETH на газ (~$5-20) ║ +// ║ DEX: Uniswap Universal Router V2.0 ║ +// ║ Комиссия: 1% с каждого свапа → fee wallet ║ +// ║ ║ +// ║ КАК ЗАДЕПЛОИТЬ: ║ +// ║ 1. Открой https://remix.ethereum.org ║ +// ║ 2. Создай файл, вставь ВЕСЬ этот код ║ +// ║ 3. Compiler → 0.8.20, Optimization ON (200 runs) ║ +// ║ 4. Deploy → Injected Provider (MetaMask) ║ +// ║ 5. В MetaMask выбери сеть ETHEREUM MAINNET ║ +// ║ 6. Нажми Deploy, подтверди в MetaMask ║ +// ║ 7. Сохрани адрес контракта после деплоя ║ +// ╚══════════════════════════════════════════════════════════════╝ + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @title FeeSwapRouter_ETH +/// @notice Обёртка для Uniswap Universal Router. Берёт 1% комиссию с каждого +/// свапа и отправляет на fee wallet. Остальные 99% + calldata идут +/// на Universal Router как есть. +/// @dev Работает с любой версией Uniswap (V2/V3/V4/Universal Router), +/// потому что просто пересылает calldata без разбора. +contract FeeSwapRouter_ETH is Ownable, ReentrancyGuard, Pausable { + using SafeERC20 for IERC20; + + // ── Захардкоженные адреса для Ethereum ── + + /// @notice Uniswap Universal Router V2.0 на Ethereum Mainnet + address public constant UNIVERSAL_ROUTER = 0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD; + + /// @notice Uniswap Permit2 / Swap Proxy на Ethereum Mainnet + address public constant SWAP_PROXY = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + + /// @notice Кошелёк, получающий 1% комиссию + address public constant FEE_RECIPIENT = 0xeDEb157eF86A4ecd1242762f339c2Bd5a0822718; + + /// @notice Комиссия 1% = 100 basis points (нельзя изменить) + uint16 public constant FEE_BPS = 100; + + /// @notice Делитель для basis points + uint16 private constant BPS_DENOMINATOR = 10_000; + + // ── Events ── + + event SwapWithFeeNative( + address indexed user, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event SwapWithFeeToken( + address indexed user, + address indexed tokenIn, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event EmergencyWithdrawNative(address indexed to, uint256 amount); + event EmergencyWithdrawToken(address indexed token, address indexed to, uint256 amount); + + // ── Errors ── + + error InsufficientValue(); + error NativeTransferFailed(); + error SwapFailed(); + error ZeroAmount(); + error ZeroAddress(); + error WrongChain(); + error UnauthorizedRouter(); + + // ── Constructor ── + + constructor() Ownable(msg.sender) { + if (block.chainid != 1) revert WrongChain(); + } + + // ═══════════════════════════════════════════════ + // Swap: ETH → Token + // Пользователь отправляет ETH. Контракт берёт 1%, + // остальное + calldata пересылает на Universal Router. + // ═══════════════════════════════════════════════ + + /// @notice Свап ETH → токен через Universal Router с 1% комиссией. + /// @param routerCalldata Calldata сгенерированная Uniswap SDK + /// (SwapRouter.swapCallParameters). Передаётся как есть. + function swapNativeWithFee( + bytes calldata routerCalldata + ) external payable nonReentrant whenNotPaused { + if (msg.value == 0) revert InsufficientValue(); + + uint256 feeAmount = (msg.value * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = msg.value - feeAmount; + + // 1% → fee wallet + if (feeAmount > 0) { + (bool feeSent, ) = FEE_RECIPIENT.call{value: feeAmount}(""); + if (!feeSent) revert NativeTransferFailed(); + } + + // 99% + calldata → Universal Router + (bool success, ) = UNIVERSAL_ROUTER.call{value: swapAmount}(routerCalldata); + if (!success) revert SwapFailed(); + + emit SwapWithFeeNative(msg.sender, msg.value, feeAmount, swapAmount, UNIVERSAL_ROUTER); + } + + // ═══════════════════════════════════════════════ + // Swap: Token → ETH + // Пользователь approve-ит токены на этот контракт. + // Контракт берёт 1% комиссию в токенах, approve-ит + // 99% на Permit2/SwapProxy, и пересылает calldata + // на Universal Router. + // ═══════════════════════════════════════════════ + + /// @notice Свап токен → ETH через Universal Router с 1% комиссией. + /// @param tokenIn Адрес входного токена (USDT, USDC, и т.д.) + /// @param amountIn Полная сумма токенов (включая 1% комиссию) + /// @param routerCalldata Calldata для Universal Router + function swapTokenWithFee( + address tokenIn, + uint256 amountIn, + bytes calldata routerCalldata + ) external nonReentrant whenNotPaused { + if (amountIn == 0) revert ZeroAmount(); + if (tokenIn == address(0)) revert ZeroAddress(); + + // Забираем токены у пользователя + IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn); + + uint256 feeAmount = (amountIn * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = amountIn - feeAmount; + + // 1% комиссию → fee wallet + if (feeAmount > 0) { + IERC20(tokenIn).safeTransfer(FEE_RECIPIENT, feeAmount); + } + + // Approve 99% на Permit2 (Uniswap Swap Proxy) + IERC20(tokenIn).forceApprove(SWAP_PROXY, swapAmount); + + // Calldata → Universal Router + (bool success, ) = UNIVERSAL_ROUTER.call(routerCalldata); + if (!success) revert SwapFailed(); + + emit SwapWithFeeToken(msg.sender, tokenIn, amountIn, feeAmount, swapAmount, UNIVERSAL_ROUTER); + } + + // ── Admin: Emergency ── + + function pause() external onlyOwner { _pause(); } + function unpause() external onlyOwner { _unpause(); } + + function emergencyWithdrawNative() external onlyOwner { + uint256 balance = address(this).balance; + if (balance == 0) revert ZeroAmount(); + (bool sent, ) = owner().call{value: balance}(""); + if (!sent) revert NativeTransferFailed(); + emit EmergencyWithdrawNative(owner(), balance); + } + + function emergencyWithdrawToken(address token) external onlyOwner { + if (token == address(0)) revert ZeroAddress(); + uint256 balance = IERC20(token).balanceOf(address(this)); + if (balance == 0) revert ZeroAmount(); + IERC20(token).safeTransfer(owner(), balance); + emit EmergencyWithdrawToken(token, owner(), balance); + } + + /// @notice Принимаем ETH от Universal Router (рефанды, выходные средства) + receive() external payable {} +} diff --git a/contracts/FeeSwapRouter_TRX.sol b/contracts/FeeSwapRouter_TRX.sol new file mode 100644 index 0000000..409451b --- /dev/null +++ b/contracts/FeeSwapRouter_TRX.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// ── Адреса (TRON base58 → hex) ── +// SunSwap V2 Router: TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax → 0x6e0617948fe030a7e4970f8389d4ad295f249b7e +// USDT (TRC-20): TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t → 0xa614f803b6fd780986a42c78ec9c7f77e6ded13c +// WTRX: TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR → 0x891cdb91d149f23b1a45d9c5ca78a88d0cb44c18 +// Fee Recipient: TYTfrem65362TFyQSARTheeYza1GQA37Ug → 0xf6b4D4E650Fc67982894f37ba97Ab2496781ddb6 + +interface ITRC20 { + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); +} + +/// @title FeeSwapRouter_TRX +/// @notice Обёртка для SunSwap V2 на TRON. Берёт 0.7% комиссию +/// с каждого свапа и бриджа, отправляет на fee wallet. +/// Остальные 99.3% + calldata идут на SunSwap как есть. +/// @dev Generic calldata forwarder — работает с любым роутером. +/// На TRON нет OpenZeppelin, поэтому ReentrancyGuard и Ownable +/// реализованы вручную. +contract FeeSwapRouter_TRX { + + // ── Reentrancy Guard ── + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + uint256 private _status = NOT_ENTERED; + + modifier nonReentrant() { + require(_status != ENTERED, "ReentrancyGuard: reentrant call"); + _status = ENTERED; + _; + _status = NOT_ENTERED; + } + + // ── Ownable ── + address private _owner; + bool private _paused; + + modifier onlyOwner() { + require(msg.sender == _owner, "Ownable: caller is not the owner"); + _; + } + + modifier whenNotPaused() { + require(!_paused, "Pausable: paused"); + _; + } + + function owner() public view returns (address) { return _owner; } + function paused() public view returns (bool) { return _paused; } + + // ── Захардкоженные адреса для TRON ── + + /// @notice SunSwap V2 Smart Router на TRON + /// TRON: TKzxdSv2FZKQrEqkKVgp5DcwEXBEKMg2Ax + address public constant SUNSWAP_ROUTER = 0x6E0617948FE030a7E4970f8389d4Ad295f249B7e; + + /// @notice Кошелёк, получающий 0.7% комиссию + /// TRON: TYTfrem65362TFyQSARTheeYza1GQA37Ug + address public constant FEE_RECIPIENT = 0xf6b4D4E650Fc67982894f37ba97Ab2496781ddb6; + + /// @notice Комиссия 0.7% = 70 basis points (нельзя изменить) + uint16 public constant FEE_BPS = 70; + + /// @notice Делитель для basis points + uint16 private constant BPS_DENOMINATOR = 10_000; + + // ── Events ── + + event SwapWithFeeNative( + address indexed user, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event SwapWithFeeToken( + address indexed user, + address indexed tokenIn, + uint256 totalIn, + uint256 feeAmount, + uint256 swapAmount, + address indexed router + ); + + event BridgeWithFeeNative( + address indexed user, + uint256 totalIn, + uint256 feeAmount + ); + + event BridgeWithFeeToken( + address indexed user, + address indexed tokenIn, + uint256 totalIn, + uint256 feeAmount + ); + + event EmergencyWithdrawNative(address indexed to, uint256 amount); + event EmergencyWithdrawToken(address indexed token, address indexed to, uint256 amount); + event Paused(address account); + event Unpaused(address account); + + // ── Constructor ── + + constructor() { + _owner = msg.sender; + _paused = false; + } + + // ═══════════════════════════════════════════════ + // Swap: TRX → Token + // Пользователь отправляет TRX. Контракт берёт 0.7%, + // остальное + calldata пересылает на SunSwap Router. + // ═══════════════════════════════════════════════ + + /// @notice Свап TRX → токен через SunSwap с 0.7% комиссией. + /// @param routerCalldata Calldata для SunSwap Router + function swapNativeWithFee( + bytes calldata routerCalldata + ) external payable nonReentrant whenNotPaused { + require(msg.value > 0, "Zero value"); + + uint256 feeAmount = (msg.value * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = msg.value - feeAmount; + + // 0.7% → fee wallet + if (feeAmount > 0) { + (bool feeSent, ) = FEE_RECIPIENT.call{value: feeAmount}(""); + require(feeSent, "Fee transfer failed"); + } + + // 99.3% + calldata → SunSwap Router + (bool success, ) = SUNSWAP_ROUTER.call{value: swapAmount}(routerCalldata); + require(success, "Swap failed"); + + emit SwapWithFeeNative(msg.sender, msg.value, feeAmount, swapAmount, SUNSWAP_ROUTER); + } + + // ═══════════════════════════════════════════════ + // Swap: Token → TRX + // Пользователь approve-ит токены на этот контракт. + // Контракт берёт 0.7% комиссию в токенах, approve-ит + // 99.3% на SunSwap, и пересылает calldata. + // ═══════════════════════════════════════════════ + + /// @notice Свап токен → TRX через SunSwap с 0.7% комиссией. + /// @param tokenIn Адрес входного TRC-20 токена (USDT и т.д.) + /// @param amountIn Полная сумма токенов (включая 0.7% комиссию) + /// @param routerCalldata Calldata для SunSwap Router + function swapTokenWithFee( + address tokenIn, + uint256 amountIn, + bytes calldata routerCalldata + ) external nonReentrant whenNotPaused { + require(amountIn > 0, "Zero amount"); + require(tokenIn != address(0), "Zero address"); + + // Забираем токены у пользователя + require( + ITRC20(tokenIn).transferFrom(msg.sender, address(this), amountIn), + "TransferFrom failed" + ); + + uint256 feeAmount = (amountIn * FEE_BPS) / BPS_DENOMINATOR; + uint256 swapAmount = amountIn - feeAmount; + + // 0.7% комиссию → fee wallet + if (feeAmount > 0) { + require( + ITRC20(tokenIn).transfer(FEE_RECIPIENT, feeAmount), + "Fee transfer failed" + ); + } + + // Approve 99.3% на SunSwap Router + ITRC20(tokenIn).approve(SUNSWAP_ROUTER, swapAmount); + + // Calldata → SunSwap Router + (bool success, ) = SUNSWAP_ROUTER.call(routerCalldata); + require(success, "Swap failed"); + + emit SwapWithFeeToken(msg.sender, tokenIn, amountIn, feeAmount, swapAmount, SUNSWAP_ROUTER); + } + + // ═══════════════════════════════════════════════ + // Bridge Fee: TRX (native) + // Берёт 0.7% с TRX перед бриджем. + // Остаток возвращается пользователю для бриджа. + // ═══════════════════════════════════════════════ + + /// @notice Взять 0.7% комиссию с TRX перед бриджем. + /// Остаток возвращается msg.sender. + function bridgeNativeFee() external payable nonReentrant whenNotPaused { + require(msg.value > 0, "Zero value"); + + uint256 feeAmount = (msg.value * FEE_BPS) / BPS_DENOMINATOR; + uint256 remaining = msg.value - feeAmount; + + // 0.7% → fee wallet + if (feeAmount > 0) { + (bool feeSent, ) = FEE_RECIPIENT.call{value: feeAmount}(""); + require(feeSent, "Fee transfer failed"); + } + + // Остаток → обратно пользователю + if (remaining > 0) { + (bool sent, ) = msg.sender.call{value: remaining}(""); + require(sent, "Return transfer failed"); + } + + emit BridgeWithFeeNative(msg.sender, msg.value, feeAmount); + } + + /// @notice Взять 0.7% комиссию с TRC-20 токена перед бриджем. + /// Остаток возвращается msg.sender. + /// @param tokenIn Адрес TRC-20 токена + /// @param amountIn Полная сумма (включая 0.7%) + function bridgeTokenFee( + address tokenIn, + uint256 amountIn + ) external nonReentrant whenNotPaused { + require(amountIn > 0, "Zero amount"); + require(tokenIn != address(0), "Zero address"); + + require( + ITRC20(tokenIn).transferFrom(msg.sender, address(this), amountIn), + "TransferFrom failed" + ); + + uint256 feeAmount = (amountIn * FEE_BPS) / BPS_DENOMINATOR; + uint256 remaining = amountIn - feeAmount; + + // 0.7% → fee wallet + if (feeAmount > 0) { + require( + ITRC20(tokenIn).transfer(FEE_RECIPIENT, feeAmount), + "Fee transfer failed" + ); + } + + // Остаток → обратно пользователю + if (remaining > 0) { + require( + ITRC20(tokenIn).transfer(msg.sender, remaining), + "Return transfer failed" + ); + } + + emit BridgeWithFeeToken(msg.sender, tokenIn, amountIn, feeAmount); + } + + // ── Admin: Emergency ── + + function pause() external onlyOwner { + _paused = true; + emit Paused(msg.sender); + } + + function unpause() external onlyOwner { + _paused = false; + emit Unpaused(msg.sender); + } + + function emergencyWithdrawNative() external onlyOwner { + uint256 balance = address(this).balance; + require(balance > 0, "Zero balance"); + (bool sent, ) = _owner.call{value: balance}(""); + require(sent, "Transfer failed"); + emit EmergencyWithdrawNative(_owner, balance); + } + + function emergencyWithdrawToken(address token) external onlyOwner { + require(token != address(0), "Zero address"); + uint256 balance = ITRC20(token).balanceOf(address(this)); + require(balance > 0, "Zero balance"); + require(ITRC20(token).transfer(_owner, balance), "Transfer failed"); + emit EmergencyWithdrawToken(token, _owner, balance); + } + + /// @notice Принимаем TRX (рефанды, возвраты) + receive() external payable {} +} diff --git a/docker-start.bat b/docker-start.bat new file mode 100644 index 0000000..3fe17cf --- /dev/null +++ b/docker-start.bat @@ -0,0 +1,14 @@ +@echo off +echo ======================================== +echo CryptoWallet - Docker Full Stack +echo ======================================== +echo. + +docker compose up --build + +echo. +echo Services: +echo Web: http://localhost:3000 +echo API: http://localhost:3001 +echo Vault: http://localhost:8200 +echo. diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f07c53 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "crrrryptowallet", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@10.28.2", + "scripts": { + "dev": "turbo dev", + "build": "turbo build", + "lint": "turbo lint", + "typecheck": "turbo typecheck" + }, + "devDependencies": { + "turbo": "^2.4.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["bcrypt"], + "overrides": { + "ethers": "5.7.2" + } + } +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..cac9429 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,13 @@ +{ + "name": "@cryptowallet/shared", + "version": "0.1.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/packages/shared/src/constants/chains.ts b/packages/shared/src/constants/chains.ts new file mode 100644 index 0000000..2aefc9b --- /dev/null +++ b/packages/shared/src/constants/chains.ts @@ -0,0 +1,9 @@ +export const CHAINS = ['ETH', 'BTC', 'SOL', 'TRX'] as const; +export type Chain = (typeof CHAINS)[number]; + +export const DERIVATION_PATHS: Record = { + 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", +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..d71a7ad --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,3 @@ +export { CHAINS, DERIVATION_PATHS } from './constants/chains'; +export type { Chain } from './constants/chains'; +export type { ApiResponse, AccessTokenPayload, AuthContext } from './types/auth'; diff --git a/packages/shared/src/types/auth.ts b/packages/shared/src/types/auth.ts new file mode 100644 index 0000000..6387397 --- /dev/null +++ b/packages/shared/src/types/auth.ts @@ -0,0 +1,22 @@ +export interface ApiResponse { + success: boolean; + data?: T; + 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; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..d650fa5 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..bfbaf63 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5398 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + ethers: 5.7.2 + +importers: + + .: + devDependencies: + turbo: + specifier: ^2.4.0 + version: 2.8.15 + + apps/api: + dependencies: + '@cryptowallet/shared': + specifier: workspace:* + version: link:../../packages/shared + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + ethers: + specifier: 5.7.2 + version: 5.7.2(bufferutil@4.1.0) + express: + specifier: ^4.21.0 + version: 4.22.1 + helmet: + specifier: ^8.0.0 + version: 8.1.0 + jose: + specifier: ^6.2.2 + version: 6.2.2 + knex: + specifier: ^3.1.0 + version: 3.1.0(pg@8.20.0) + pg: + specifier: ^8.13.0 + version: 8.20.0 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.22.1) + ulidx: + specifier: ^2.4.1 + version: 2.4.1 + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@types/cookie-parser': + specifier: ^1.4.7 + version: 1.4.10(@types/express@5.0.6) + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/express-serve-static-core': + specifier: ^5.1.1 + version: 5.1.1 + '@types/node': + specifier: ^20.0.0 + version: 20.19.37 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 + ts-node: + specifier: ^10.9.0 + version: 10.9.2(@types/node@20.19.37)(typescript@5.9.3) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.19.37)(typescript@5.9.3) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + + apps/web: + dependencies: + '@scure/bip32': + specifier: ^2.0.1 + version: 2.0.1 + '@scure/bip39': + specifier: ^2.0.1 + version: 2.0.1 + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@uniswap/router-sdk': + specifier: ^2.7.1 + version: 2.7.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/sdk-core': + specifier: ^7.12.1 + version: 7.12.1 + '@uniswap/universal-router-sdk': + specifier: ^4.34.0 + version: 4.34.0(bufferutil@4.1.0)(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))(utf-8-validate@6.0.6) + '@uniswap/v3-sdk': + specifier: ^3.29.1 + version: 3.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/v4-sdk': + specifier: ^1.29.1 + version: 1.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@yudiel/react-qr-scanner': + specifier: ^2.5.1 + version: 2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + bip32: + specifier: ^5.0.1 + version: 5.0.1(typescript@5.9.3) + bitcoinjs-lib: + specifier: ^7.0.1 + version: 7.0.1(typescript@5.9.3) + ecpair: + specifier: ^3.0.1 + version: 3.0.1(typescript@5.9.3) + ed25519-hd-key: + specifier: ^1.3.0 + version: 1.3.0 + ethers: + specifier: 5.7.2 + version: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + next: + specifier: 16.1.6 + version: 16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + qrcode.react: + specifier: ^4.2.0 + version: 4.2.0(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + tiny-secp256k1: + specifier: ^2.2.4 + version: 2.2.4 + zustand: + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.14)(react@19.2.3) + devDependencies: + '@types/node': + specifier: ^20 + version: 20.19.37 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: ^5 + version: 5.9.3 + + packages/shared: + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + +packages: + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@ethereumjs/rlp@5.0.2': + resolution: {integrity: sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==} + engines: {node: '>=18'} + hasBin: true + + '@ethereumjs/util@9.1.0': + resolution: {integrity: sha512-XBEKsYqLGXLah9PNJbgdkigthkG7TAGvlD/sH12beMXEyHDyigfcbdvHhmLyDWgDyOJn4QwiQUaF7yeuhnjdog==} + engines: {node: '>=18'} + + '@ethersproject/abi@5.7.0': + resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} + + '@ethersproject/abi@5.8.0': + resolution: {integrity: sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==} + + '@ethersproject/abstract-provider@5.7.0': + resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==} + + '@ethersproject/abstract-provider@5.8.0': + resolution: {integrity: sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==} + + '@ethersproject/abstract-signer@5.7.0': + resolution: {integrity: sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==} + + '@ethersproject/abstract-signer@5.8.0': + resolution: {integrity: sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==} + + '@ethersproject/address@5.7.0': + resolution: {integrity: sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==} + + '@ethersproject/address@5.8.0': + resolution: {integrity: sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==} + + '@ethersproject/base64@5.7.0': + resolution: {integrity: sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==} + + '@ethersproject/base64@5.8.0': + resolution: {integrity: sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==} + + '@ethersproject/basex@5.7.0': + resolution: {integrity: sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==} + + '@ethersproject/basex@5.8.0': + resolution: {integrity: sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==} + + '@ethersproject/bignumber@5.7.0': + resolution: {integrity: sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==} + + '@ethersproject/bignumber@5.8.0': + resolution: {integrity: sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==} + + '@ethersproject/bytes@5.7.0': + resolution: {integrity: sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==} + + '@ethersproject/bytes@5.8.0': + resolution: {integrity: sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==} + + '@ethersproject/constants@5.7.0': + resolution: {integrity: sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==} + + '@ethersproject/constants@5.8.0': + resolution: {integrity: sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==} + + '@ethersproject/contracts@5.7.0': + resolution: {integrity: sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==} + + '@ethersproject/hash@5.7.0': + resolution: {integrity: sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==} + + '@ethersproject/hash@5.8.0': + resolution: {integrity: sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==} + + '@ethersproject/hdnode@5.7.0': + resolution: {integrity: sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==} + + '@ethersproject/hdnode@5.8.0': + resolution: {integrity: sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==} + + '@ethersproject/json-wallets@5.7.0': + resolution: {integrity: sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==} + + '@ethersproject/json-wallets@5.8.0': + resolution: {integrity: sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==} + + '@ethersproject/keccak256@5.7.0': + resolution: {integrity: sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==} + + '@ethersproject/keccak256@5.8.0': + resolution: {integrity: sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==} + + '@ethersproject/logger@5.7.0': + resolution: {integrity: sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==} + + '@ethersproject/logger@5.8.0': + resolution: {integrity: sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==} + + '@ethersproject/networks@5.7.1': + resolution: {integrity: sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==} + + '@ethersproject/networks@5.8.0': + resolution: {integrity: sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==} + + '@ethersproject/pbkdf2@5.7.0': + resolution: {integrity: sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==} + + '@ethersproject/pbkdf2@5.8.0': + resolution: {integrity: sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==} + + '@ethersproject/properties@5.7.0': + resolution: {integrity: sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==} + + '@ethersproject/properties@5.8.0': + resolution: {integrity: sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==} + + '@ethersproject/providers@5.7.2': + resolution: {integrity: sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==} + + '@ethersproject/random@5.7.0': + resolution: {integrity: sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==} + + '@ethersproject/random@5.8.0': + resolution: {integrity: sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==} + + '@ethersproject/rlp@5.7.0': + resolution: {integrity: sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==} + + '@ethersproject/rlp@5.8.0': + resolution: {integrity: sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==} + + '@ethersproject/sha2@5.7.0': + resolution: {integrity: sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==} + + '@ethersproject/sha2@5.8.0': + resolution: {integrity: sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==} + + '@ethersproject/signing-key@5.7.0': + resolution: {integrity: sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==} + + '@ethersproject/signing-key@5.8.0': + resolution: {integrity: sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==} + + '@ethersproject/solidity@5.7.0': + resolution: {integrity: sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==} + + '@ethersproject/solidity@5.8.0': + resolution: {integrity: sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==} + + '@ethersproject/strings@5.7.0': + resolution: {integrity: sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==} + + '@ethersproject/strings@5.8.0': + resolution: {integrity: sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==} + + '@ethersproject/transactions@5.7.0': + resolution: {integrity: sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==} + + '@ethersproject/transactions@5.8.0': + resolution: {integrity: sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==} + + '@ethersproject/units@5.7.0': + resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==} + + '@ethersproject/wallet@5.7.0': + resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==} + + '@ethersproject/web@5.7.1': + resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==} + + '@ethersproject/web@5.8.0': + resolution: {integrity: sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==} + + '@ethersproject/wordlists@5.7.0': + resolution: {integrity: sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==} + + '@ethersproject/wordlists@5.8.0': + resolution: {integrity: sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/curves@1.8.2': + resolution: {integrity: sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@1.2.0': + resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@1.7.2': + resolution: {integrity: sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + + '@noble/secp256k1@1.7.1': + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.23': + resolution: {integrity: sha512-Amh7mRoDzZyJJ4efqoePqdoZOzharmSOttZuJDlVE5yy07BoE8hL6ZRpa5fNYn0LCqn/KoWs8OHANWxhKDGhvQ==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-darwin-x64@0.12.0-next.23': + resolution: {integrity: sha512-9wn489FIQm7m0UCD+HhktjWx6vskZzeZD9oDc2k9ZvbBzdXwPp5tiDqUBJ+eQpByAzCDfteAJwRn2lQCE0U+Iw==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.23': + resolution: {integrity: sha512-nlk5EejSzEUfEngv0Jkhqq3/wINIfF2ED9wAofc22w/V1DV99ASh9l3/e/MIHOQFecIZ9MDqt0Em9/oDyB1Uew==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.23': + resolution: {integrity: sha512-SJuPBp3Rc6vM92UtVTUxZQ/QlLhLfwTftt2XUiYohmGKB3RjGzpgduEFMCA0LEnucUckU6UHrJNFHiDm77C4PQ==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.23': + resolution: {integrity: sha512-NU+Qs3u7Qt6t3bJFdmmjd5CsvgI2bPPzO31KifM2Ez96/jsXYho5debtTQnimlb5NAqiHTSlxjh/F8ROcptmeQ==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.23': + resolution: {integrity: sha512-F78fZA2h6/ssiCSZOovlgIu0dUeI7ItKPsDDF3UUlIibef052GCXmliMinC90jVPbrjUADMd1BUwjfI0Z8OllQ==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.23': + resolution: {integrity: sha512-IfJZQJn7d/YyqhmguBIGoCKjE9dKjbu6V6iNEPApfwf5JyyjHYyyfkLU4rf7hygj57bfH4sl1jtQ6r8HnT62lw==} + engines: {node: '>= 20'} + + '@nomicfoundation/edr@0.12.0-next.23': + resolution: {integrity: sha512-F2/6HZh8Q9RsgkOIkRrckldbhPjIZY7d4mT9LYuW68miwGQ5l7CkAgcz9fRRiurA0+YJhtsbx/EyrD9DmX9BOw==} + engines: {node: '>= 20'} + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + resolution: {integrity: sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + resolution: {integrity: sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + resolution: {integrity: sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + resolution: {integrity: sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + resolution: {integrity: sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + resolution: {integrity: sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + resolution: {integrity: sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer@0.1.2': + resolution: {integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==} + engines: {node: '>= 12'} + + '@openzeppelin/contracts@3.4.1-solc-0.7-2': + resolution: {integrity: sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q==} + + '@openzeppelin/contracts@3.4.2-solc-0.7': + resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} + + '@openzeppelin/contracts@4.7.0': + resolution: {integrity: sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw==} + + '@openzeppelin/contracts@5.0.2': + resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@scure/bip32@1.1.5': + resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + + '@scure/bip39@1.1.1': + resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + + '@sentry/core@5.30.0': + resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} + engines: {node: '>=6'} + + '@sentry/hub@5.30.0': + resolution: {integrity: sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==} + engines: {node: '>=6'} + + '@sentry/minimal@5.30.0': + resolution: {integrity: sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==} + engines: {node: '>=6'} + + '@sentry/node@5.30.0': + resolution: {integrity: sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==} + engines: {node: '>=6'} + + '@sentry/tracing@5.30.0': + resolution: {integrity: sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==} + engines: {node: '>=6'} + + '@sentry/types@5.30.0': + resolution: {integrity: sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==} + engines: {node: '>=6'} + + '@sentry/utils@5.30.0': + resolution: {integrity: sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==} + engines: {node: '>=6'} + + '@solana/buffer-layout@4.0.1': + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} + + '@solana/codecs-core@2.3.0': + resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/codecs-numbers@2.3.0': + resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/errors@2.3.0': + resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/emscripten@1.41.5': + resolution: {integrity: sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@20.19.37': + resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + 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': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@7.4.7': + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@uniswap/lib@4.0.1-alpha': + resolution: {integrity: sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==} + engines: {node: '>=10'} + + '@uniswap/permit2-sdk@1.4.0': + resolution: {integrity: sha512-l/aGhfhB93M76vXs4eB8QNwhELE6bs66kh7F1cyobaPtINaVpMmlJv+j3KmHeHwAZIsh7QXyYzhDxs07u0Pe4Q==} + + '@uniswap/router-sdk@2.7.1': + resolution: {integrity: sha512-d2+P+761vchusTTC019bSh8dqCUOEZ+JQzgOXXuMkbcxzs81bOIL0WAeKO4azhZhcRQpxCrRqQYEeSX8dE/Oww==} + + '@uniswap/sdk-core@7.12.1': + resolution: {integrity: sha512-qQG6dVFIgk9x+WRQBDZms/abi+aM6J6YFLhaMnrvTSFIIL4hOjf74SpOep5wPEDZGx3iw3LqofRq+ijeVwLLBQ==} + engines: {node: '>=10'} + + '@uniswap/swap-router-contracts@1.3.1': + resolution: {integrity: sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg==} + engines: {node: '>=10'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@uniswap/universal-router-sdk@4.34.0': + resolution: {integrity: sha512-TZEVBWilQHtSO75Ozkq1D0JcacclF3q1l8LOmQ8C1FzuQQvi8uWqJXE6qH4SghxvsdQIWV3Jkz9Jvv8OOVduxg==} + engines: {node: '>=14'} + + '@uniswap/universal-router@2.1.0': + resolution: {integrity: sha512-rt18RUsZd9xDfyVfIONJo+TEQ8w+olOYxu9+A1g4Thil1R7IMa+8mnyVQjdLPK2REhejScDwjYbOGpeaAce0hg==} + engines: {node: '>=14'} + + '@uniswap/v2-core@1.0.1': + resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} + engines: {node: '>=10'} + + '@uniswap/v2-sdk@4.19.1': + resolution: {integrity: sha512-c89H6S8YNI7tGSsk5wWXLxnXUe/jT+BewKSXtJ/w0WXEV8r4dl18R80YfOtwajnYbt8jYrUVVdPgPLaX0CdDEg==} + engines: {node: '>=10'} + + '@uniswap/v3-core@1.0.0': + resolution: {integrity: sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==} + engines: {node: '>=10'} + + '@uniswap/v3-periphery@1.4.4': + resolution: {integrity: sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw==} + engines: {node: '>=10'} + + '@uniswap/v3-sdk@3.29.1': + resolution: {integrity: sha512-EYCzrfCCxc9DqUguw5rX+9774jvpVIvvrnyraJenZ371JiX/VC09/6OToKReu3gJfgITaH1BynMATwGkmOb2SQ==} + engines: {node: '>=10'} + + '@uniswap/v3-staker@1.0.0': + resolution: {integrity: sha512-JV0Qc46Px5alvg6YWd+UIaGH9lDuYG/Js7ngxPit1SPaIP30AlVer1UYB7BRYeUVVxE+byUyIeN5jeQ7LLDjIw==} + engines: {node: '>=10'} + deprecated: Please upgrade to 1.0.1 + + '@uniswap/v4-sdk@1.29.1': + resolution: {integrity: sha512-JYAnNmTGhbl5G4Xl+fUsew/ic8VwKofJxZ2uQjMkGMkzkCMT70Sht7E2ysZD+znLWQ9cCB0AAQWZqTlryiEfhA==} + engines: {node: '>=14'} + + '@yudiel/react-qr-scanner@2.5.1': + resolution: {integrity: sha512-FWzHaCneu30mQpE80VNWx4IPtBjXFEiTzhwWunZy3afvvAy/x0aVIgYijJKEbROoaAeDfcJ/gIyUCqPBP7bMOw==} + peerDependencies: + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + adm-zip@0.4.16: + resolution: {integrity: sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==} + engines: {node: '>=0.3.0'} + + aes-js@3.0.0: + resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + barcode-detector@3.0.8: + resolution: {integrity: sha512-Z9jzzE8ngEDyN9EU7lWdGgV07mcnEQnrX8W9WecXDqD2v+5CcVjt9+a134a5zb+kICvpsrDx6NYA6ay4LGFs8A==} + + base-x@3.0.11: + resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64-sol@1.0.1: + resolution: {integrity: sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg==} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + bech32@1.1.4: + resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} + + bech32@2.0.0: + resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bip174@3.0.0: + resolution: {integrity: sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==} + engines: {node: '>=18.0.0'} + + bip32@5.0.1: + resolution: {integrity: sha512-PWlHIAgYCfVhwqNpZyeakHXuLAGyN6rEQZnhxHxKI3BoFJRVWLl26455fhRlHsmbYcV986HqtPnt33Edu5sTCw==} + engines: {node: '>=18.0.0'} + + bitcoinjs-lib@7.0.1: + resolution: {integrity: sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==} + engines: {node: '>=18.0.0'} + + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + + bn.js@5.2.3: + resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} + + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + + dotenv@14.3.2: + resolution: {integrity: sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + + ecpair@3.0.1: + resolution: {integrity: sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==} + engines: {node: '>=20.0.0'} + + ed25519-hd-key@1.3.0: + resolution: {integrity: sha512-IWwAyiiuJQhgu3L8NaHb68eJxTu2pgCwxIBdgpLJdKpYZM46+AXePSVTr7fkNKaUOfOL4IrjEUaQvyVRIDP7fg==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + + elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + ethereum-cryptography@1.2.0: + resolution: {integrity: sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + ethers@5.7.2: + resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + + fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fp-ts@1.19.3: + resolution: {integrity: sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob 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 + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob 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 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hardhat-watcher@2.5.0: + resolution: {integrity: sha512-Su2qcSMIo2YO2PrmJ0/tdkf+6pSt8zf9+4URR5edMVti6+ShI8T3xhPrwugdyTOFuyj8lKHrcTZNKUFYowYiyA==} + peerDependencies: + hardhat: ^2.0.0 + + hardhat@2.28.6: + resolution: {integrity: sha512-zQze7qe+8ltwHvhX5NQ8sN1N37WWZGw8L63y+2XcPxGwAjc/SMF829z3NS6o1krX0sryhAsVBK/xrwUqlsot4Q==} + hasBin: true + peerDependencies: + ts-node: '*' + typescript: '*' + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immutable@4.3.8: + resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + io-ts@1.10.4: + resolution: {integrity: sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + jayson@4.3.0: + resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==} + engines: {node: '>=8'} + hasBin: true + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsbi@3.2.5: + resolution: {integrity: sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==} + + json-stream-stringify@3.1.6: + resolution: {integrity: sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==} + engines: {node: '>=7.10.1'} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + + knex@3.1.0: + resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + layerr@3.0.0: + resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru_map@0.3.3: + resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micro-eth-signer@0.14.0: + resolution: {integrity: sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==} + + micro-packed@0.7.3: + resolution: {integrity: sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mnemonist@0.38.5: + resolution: {integrity: sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==} + + mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.17.0: + resolution: {integrity: sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + + rpc-websockets@9.3.5: + resolution: {integrity: sha512-4mAmr+AEhPYJ9TmDtxF3r3ZcbWy7W8kvZ4PoZYw/Xgp2J7WixjwTgiQZsoTDvch5nimmg3Ay6/0Kuh9oIvVs9A==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + scrypt-js@3.0.1: + resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} + + sdp@3.2.1: + resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + solc@0.8.26: + resolution: {integrity: sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==} + engines: {node: '>=10.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + superstruct@2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + 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: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-secp256k1@2.2.4: + resolution: {integrity: sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==} + engines: {node: '>=14.0.0'} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toformat@2.0.0: + resolution: {integrity: sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsort@0.0.1: + resolution: {integrity: sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==} + + turbo-darwin-64@2.8.15: + resolution: {integrity: sha512-EElCh+Ltxex9lXYrouV3hHjKP3HFP31G91KMghpNHR/V99CkFudRcHcnWaorPbzAZizH1m8o2JkLL8rptgb8WQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.8.15: + resolution: {integrity: sha512-ORmvtqHiHwvNynSWvLIleyU8dKtwQ4ILk39VsEwfKSEzSHWYWYxZhBmD9GAGRPlNl7l7S1irrziBlDEGVpq+vQ==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.8.15: + resolution: {integrity: sha512-Bk1E61a+PCWUTfhqfXFlhEJMLp6nak0J0Qt14IZX1og1zyaiBLkM6M1GQFbPpiWfbUcdLwRaYQhO0ySB07AJ8w==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.8.15: + resolution: {integrity: sha512-3BX0Vk+XkP0uiZc8pkjQGNsAWjk5ojC53bQEMp6iuhSdWpEScEFmcT6p7DL7bcJmhP2mZ1HlAu0A48wrTGCtvg==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.8.15: + resolution: {integrity: sha512-m14ogunMF+grHZ1jzxSCO6q0gEfF1tmr+0LU+j1QNd/M1X33tfKnQqmpkeUR/REsGjfUlkQlh6PAzqlT3cA3Pg==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.8.15: + resolution: {integrity: sha512-HWh6dnzhl7nu5gRwXeqP61xbyDBNmQ4UCeWNa+si4/6RAtHlKEcZTNs7jf4U+oqBnbtv4uxbKZZPf/kN0EK4+A==} + cpu: [arm64] + os: [win32] + + turbo@2.8.15: + resolution: {integrity: sha512-ERZf7pKOR155NKs/PZt1+83NrSEJfUL7+p9/TGZg/8xzDVMntXEFQlX4CsNJQTyu4h3j+dZYiQWOOlv5pssuHQ==} + hasBin: true + + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-tools@0.0.7: + resolution: {integrity: sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==} + engines: {node: '>=14.0.0'} + + uint8array-tools@0.0.8: + resolution: {integrity: sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==} + engines: {node: '>=14.0.0'} + + uint8array-tools@0.0.9: + resolution: {integrity: sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==} + engines: {node: '>=14.0.0'} + + ulidx@2.4.1: + resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==} + engines: {node: '>=16'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utf-8-validate@6.0.6: + resolution: {integrity: sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==} + engines: {node: '>=6.14.2'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + varuint-bitcoin@2.0.0: + resolution: {integrity: sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webrtc-adapter@9.0.3: + resolution: {integrity: sha512-5fALBcroIl31OeXAdd1YUntxiZl1eHlZZWzNg3U4Fn+J9/cGL3eT80YlrsWGvj2ojuz1rZr2OXkgCzIxAZ7vRQ==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + wif@5.0.0: + resolution: {integrity: sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==} + + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.4.6: + resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zxing-wasm@2.2.4: + resolution: {integrity: sha512-1gq5zs4wuNTs5umWLypzNNeuJoluFvwmvjiiT3L9z/TMlVveeJRWy7h90xyUqCe+Qq0zL0w7o5zkdDMWDr9aZA==} + peerDependencies: + '@types/emscripten': '>=1.39.6' + +snapshots: + + '@babel/runtime@7.28.6': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@ethereumjs/rlp@5.0.2': {} + + '@ethereumjs/util@9.1.0': + dependencies: + '@ethereumjs/rlp': 5.0.2 + ethereum-cryptography: 2.2.1 + + '@ethersproject/abi@5.7.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/abi@5.8.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/abstract-provider@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/networks': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/web': 5.8.0 + + '@ethersproject/abstract-provider@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/networks': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/web': 5.8.0 + + '@ethersproject/abstract-signer@5.7.0': + dependencies: + '@ethersproject/abstract-provider': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + + '@ethersproject/abstract-signer@5.8.0': + dependencies: + '@ethersproject/abstract-provider': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + + '@ethersproject/address@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/rlp': 5.8.0 + + '@ethersproject/address@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/rlp': 5.8.0 + + '@ethersproject/base64@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + + '@ethersproject/base64@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + + '@ethersproject/basex@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/properties': 5.8.0 + + '@ethersproject/basex@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/properties': 5.8.0 + + '@ethersproject/bignumber@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + bn.js: 5.2.3 + + '@ethersproject/bignumber@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + bn.js: 5.2.3 + + '@ethersproject/bytes@5.7.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/bytes@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/constants@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + + '@ethersproject/constants@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + + '@ethersproject/contracts@5.7.0': + dependencies: + '@ethersproject/abi': 5.8.0 + '@ethersproject/abstract-provider': 5.8.0 + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/transactions': 5.8.0 + + '@ethersproject/hash@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/base64': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/hash@5.8.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/base64': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/hdnode@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/basex': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/pbkdf2': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/sha2': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + '@ethersproject/strings': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/wordlists': 5.8.0 + + '@ethersproject/hdnode@5.8.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/basex': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/pbkdf2': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/sha2': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + '@ethersproject/strings': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/wordlists': 5.8.0 + + '@ethersproject/json-wallets@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/hdnode': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/pbkdf2': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/random': 5.8.0 + '@ethersproject/strings': 5.8.0 + '@ethersproject/transactions': 5.8.0 + aes-js: 3.0.0 + scrypt-js: 3.0.1 + + '@ethersproject/json-wallets@5.8.0': + dependencies: + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/hdnode': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/pbkdf2': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/random': 5.8.0 + '@ethersproject/strings': 5.8.0 + '@ethersproject/transactions': 5.8.0 + aes-js: 3.0.0 + scrypt-js: 3.0.1 + + '@ethersproject/keccak256@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + js-sha3: 0.8.0 + + '@ethersproject/keccak256@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + js-sha3: 0.8.0 + + '@ethersproject/logger@5.7.0': {} + + '@ethersproject/logger@5.8.0': {} + + '@ethersproject/networks@5.7.1': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/networks@5.8.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/pbkdf2@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/sha2': 5.8.0 + + '@ethersproject/pbkdf2@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/sha2': 5.8.0 + + '@ethersproject/properties@5.7.0': + dependencies: + '@ethersproject/logger': 5.8.0 + + '@ethersproject/properties@5.8.0': + dependencies: + '@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)': + 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)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@ethersproject/random@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/random@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/rlp@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/rlp@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/sha2@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + hash.js: 1.1.7 + + '@ethersproject/sha2@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + hash.js: 1.1.7 + + '@ethersproject/signing-key@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + bn.js: 5.2.3 + elliptic: 6.5.4 + hash.js: 1.1.7 + + '@ethersproject/signing-key@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + bn.js: 5.2.3 + elliptic: 6.6.1 + hash.js: 1.1.7 + + '@ethersproject/solidity@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/sha2': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/solidity@5.8.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/sha2': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/strings@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/strings@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/transactions@5.7.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/rlp': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + + '@ethersproject/transactions@5.8.0': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/rlp': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + + '@ethersproject/units@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/constants': 5.8.0 + '@ethersproject/logger': 5.8.0 + + '@ethersproject/wallet@5.7.0': + dependencies: + '@ethersproject/abstract-provider': 5.8.0 + '@ethersproject/abstract-signer': 5.8.0 + '@ethersproject/address': 5.8.0 + '@ethersproject/bignumber': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/hdnode': 5.8.0 + '@ethersproject/json-wallets': 5.8.0 + '@ethersproject/keccak256': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/random': 5.8.0 + '@ethersproject/signing-key': 5.8.0 + '@ethersproject/transactions': 5.8.0 + '@ethersproject/wordlists': 5.8.0 + + '@ethersproject/web@5.7.1': + dependencies: + '@ethersproject/base64': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/web@5.8.0': + dependencies: + '@ethersproject/base64': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/wordlists@5.7.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@ethersproject/wordlists@5.8.0': + dependencies: + '@ethersproject/bytes': 5.8.0 + '@ethersproject/hash': 5.8.0 + '@ethersproject/logger': 5.8.0 + '@ethersproject/properties': 5.8.0 + '@ethersproject/strings': 5.8.0 + + '@fastify/busboy@2.1.1': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': + optional: true + + '@next/swc-darwin-x64@16.1.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.1.6': + optional: true + + '@next/swc-linux-arm64-musl@16.1.6': + optional: true + + '@next/swc-linux-x64-gnu@16.1.6': + optional: true + + '@next/swc-linux-x64-musl@16.1.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.1.6': + optional: true + + '@next/swc-win32-x64-msvc@16.1.6': + optional: true + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/curves@1.8.2': + dependencies: + '@noble/hashes': 1.7.2 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@1.2.0': {} + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@1.7.2': {} + + '@noble/hashes@1.8.0': {} + + '@noble/hashes@2.0.1': {} + + '@noble/secp256k1@1.7.1': {} + + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.23': {} + + '@nomicfoundation/edr-darwin-x64@0.12.0-next.23': {} + + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.23': {} + + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.23': {} + + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.23': {} + + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.23': {} + + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.23': {} + + '@nomicfoundation/edr@0.12.0-next.23': + dependencies: + '@nomicfoundation/edr-darwin-arm64': 0.12.0-next.23 + '@nomicfoundation/edr-darwin-x64': 0.12.0-next.23 + '@nomicfoundation/edr-linux-arm64-gnu': 0.12.0-next.23 + '@nomicfoundation/edr-linux-arm64-musl': 0.12.0-next.23 + '@nomicfoundation/edr-linux-x64-gnu': 0.12.0-next.23 + '@nomicfoundation/edr-linux-x64-musl': 0.12.0-next.23 + '@nomicfoundation/edr-win32-x64-msvc': 0.12.0-next.23 + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer@0.1.2': + optionalDependencies: + '@nomicfoundation/solidity-analyzer-darwin-arm64': 0.1.2 + '@nomicfoundation/solidity-analyzer-darwin-x64': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 + + '@openzeppelin/contracts@3.4.1-solc-0.7-2': {} + + '@openzeppelin/contracts@3.4.2-solc-0.7': {} + + '@openzeppelin/contracts@4.7.0': {} + + '@openzeppelin/contracts@5.0.2': {} + + '@scarf/scarf@1.4.0': {} + + '@scure/base@1.1.9': {} + + '@scure/base@1.2.6': {} + + '@scure/base@2.0.0': {} + + '@scure/bip32@1.1.5': + dependencies: + '@noble/hashes': 1.2.0 + '@noble/secp256k1': 1.7.1 + '@scure/base': 1.1.9 + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@scure/bip39@1.1.1': + dependencies: + '@noble/hashes': 1.2.0 + '@scure/base': 1.1.9 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.9 + + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@sentry/core@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/hub@5.30.0': + dependencies: + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/minimal@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/types': 5.30.0 + tslib: 1.14.1 + + '@sentry/node@5.30.0': + dependencies: + '@sentry/core': 5.30.0 + '@sentry/hub': 5.30.0 + '@sentry/tracing': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + cookie: 0.4.2 + https-proxy-agent: 5.0.1 + lru_map: 0.3.3 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + + '@sentry/tracing@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/types@5.30.0': {} + + '@sentry/utils@5.30.0': + dependencies: + '@sentry/types': 5.30.0 + tslib: 1.14.1 + + '@solana/buffer-layout@4.0.1': + dependencies: + buffer: 6.0.3 + + '@solana/codecs-core@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.3 + + '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)': + dependencies: + '@babel/runtime': 7.28.6 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.6.0 + bn.js: 5.2.3 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + node-fetch: 2.7.0 + rpc-websockets: 9.3.5 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.37 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.37 + + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': + dependencies: + '@types/express': 5.0.6 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.37 + + '@types/emscripten@1.41.5': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.37 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/node@12.20.55': {} + + '@types/node@20.19.37': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.37 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.37 + + '@types/strip-bom@3.0.0': {} + + '@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/ws@7.4.7': + dependencies: + '@types/node': 20.19.37 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.37 + + '@uniswap/lib@4.0.1-alpha': {} + + '@uniswap/permit2-sdk@1.4.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + ethers: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@uniswap/router-sdk@2.7.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@uniswap/sdk-core': 7.12.1 + '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/v2-sdk': 4.19.1 + '@uniswap/v3-sdk': 3.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/v4-sdk': 1.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + transitivePeerDependencies: + - hardhat + + '@uniswap/sdk-core@7.12.1': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/bytes': 5.8.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/strings': 5.7.0 + big.js: 5.2.2 + decimal.js-light: 2.5.1 + jsbi: 3.2.5 + tiny-invariant: 1.3.3 + toformat: 2.0.0 + + '@uniswap/swap-router-contracts@1.3.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@openzeppelin/contracts': 3.4.2-solc-0.7 + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.0 + '@uniswap/v3-periphery': 1.4.4 + dotenv: 14.3.2 + hardhat-watcher: 2.5.0(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + transitivePeerDependencies: + - hardhat + + '@uniswap/universal-router-sdk@4.34.0(bufferutil@4.1.0)(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))(utf-8-validate@6.0.6)': + dependencies: + '@openzeppelin/contracts': 4.7.0 + '@uniswap/permit2-sdk': 1.4.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@uniswap/router-sdk': 2.7.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/sdk-core': 7.12.1 + '@uniswap/universal-router': 2.1.0 + '@uniswap/v2-core': 1.0.1 + '@uniswap/v2-sdk': 4.19.1 + '@uniswap/v3-core': 1.0.0 + '@uniswap/v3-sdk': 3.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/v4-sdk': 1.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + bignumber.js: 9.1.2 + ethers: 5.7.2(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - hardhat + - utf-8-validate + + '@uniswap/universal-router@2.1.0': + dependencies: + '@openzeppelin/contracts': 5.0.2 + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.0 + + '@uniswap/v2-core@1.0.1': {} + + '@uniswap/v2-sdk@4.19.1': + dependencies: + '@ethersproject/address': 5.8.0 + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.12.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@uniswap/v3-core@1.0.0': {} + + '@uniswap/v3-periphery@1.4.4': + dependencies: + '@openzeppelin/contracts': 3.4.2-solc-0.7 + '@uniswap/lib': 4.0.1-alpha + '@uniswap/v2-core': 1.0.1 + '@uniswap/v3-core': 1.0.0 + base64-sol: 1.0.1 + + '@uniswap/v3-sdk@3.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.12.1 + '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + '@uniswap/v3-periphery': 1.4.4 + '@uniswap/v3-staker': 1.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + + '@uniswap/v3-staker@1.0.0': + dependencies: + '@openzeppelin/contracts': 3.4.1-solc-0.7-2 + '@uniswap/v3-core': 1.0.0 + '@uniswap/v3-periphery': 1.4.4 + + '@uniswap/v4-sdk@1.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6))': + dependencies: + '@ethersproject/solidity': 5.8.0 + '@uniswap/sdk-core': 7.12.1 + '@uniswap/v3-sdk': 3.29.1(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + transitivePeerDependencies: + - hardhat + + '@yudiel/react-qr-scanner@2.5.1(@types/emscripten@1.41.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + barcode-detector: 3.0.8(@types/emscripten@1.41.5) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + webrtc-adapter: 9.0.3 + transitivePeerDependencies: + - '@types/emscripten' + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + adm-zip@0.4.16: {} + + aes-js@3.0.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@2.0.1: {} + + array-flatten@1.1.1: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + barcode-detector@3.0.8(@types/emscripten@1.41.5): + dependencies: + zxing-wasm: 2.2.4(@types/emscripten@1.41.5) + transitivePeerDependencies: + - '@types/emscripten' + + base-x@3.0.11: + dependencies: + safe-buffer: 5.2.1 + + base-x@5.0.1: {} + + base64-js@1.5.1: {} + + base64-sol@1.0.1: {} + + baseline-browser-mapping@2.10.0: {} + + bech32@1.1.4: {} + + bech32@2.0.0: {} + + big.js@5.2.2: {} + + bignumber.js@9.1.2: {} + + binary-extensions@2.3.0: {} + + bip174@3.0.0: + dependencies: + uint8array-tools: 0.0.9 + varuint-bitcoin: 2.0.0 + + bip32@5.0.1(typescript@5.9.3): + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + uint8array-tools: 0.0.8 + valibot: 1.2.0(typescript@5.9.3) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + + bitcoinjs-lib@7.0.1(typescript@5.9.3): + dependencies: + '@noble/hashes': 1.8.0 + bech32: 2.0.0 + bip174: 3.0.0 + bs58check: 4.0.0 + uint8array-tools: 0.0.9 + valibot: 1.2.0(typescript@5.9.3) + varuint-bitcoin: 2.0.0 + transitivePeerDependencies: + - typescript + + bn.js@4.12.3: {} + + bn.js@5.2.3: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + borsh@0.7.0: + dependencies: + bn.js: 5.2.3 + bs58: 4.0.1 + text-encoding-utf-8: 1.0.2 + + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brorand@1.1.0: {} + + browser-stdout@1.3.1: {} + + bs58@4.0.1: + dependencies: + base-x: 3.0.11 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001777: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + ci-info@2.0.0: {} + + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + clean-stack@2.2.0: {} + + cli-boxes@2.2.1: {} + + client-only@0.0.1: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.19: {} + + command-exists@1.2.9: {} + + commander@10.0.1: {} + + commander@14.0.3: {} + + commander@2.20.3: {} + + commander@8.3.0: {} + + concat-map@0.0.1: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.4.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + + create-require@1.1.1: {} + + csstype@3.2.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + decimal.js-light@2.5.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + delay@5.0.0: {} + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: + optional: true + + diff@4.0.4: {} + + diff@5.2.2: {} + + dotenv@14.3.2: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + dynamic-dedupe@0.3.0: + dependencies: + xtend: 4.0.2 + + ecpair@3.0.1(typescript@5.9.3): + dependencies: + uint8array-tools: 0.0.8 + valibot: 1.2.0(typescript@5.9.3) + wif: 5.0.0 + transitivePeerDependencies: + - typescript + + ed25519-hd-key@1.3.0: + dependencies: + create-hmac: 1.1.7 + tweetnacl: 1.0.3 + + ee-first@1.1.1: {} + + elliptic@6.5.4: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + elliptic@6.6.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + env-paths@2.2.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es6-promise@4.2.8: {} + + es6-promisify@5.0.0: + dependencies: + es6-promise: 4.2.8 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + esm@3.2.25: {} + + etag@1.8.1: {} + + ethereum-cryptography@1.2.0: + dependencies: + '@noble/hashes': 1.2.0 + '@noble/secp256k1': 1.7.1 + '@scure/bip32': 1.1.5 + '@scure/bip39': 1.1.1 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.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): + 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)(utf-8-validate@6.0.6) + '@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 + + eventemitter3@5.0.4: {} + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + eyes@0.1.8: {} + + fast-stable-stringify@1.0.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + follow-redirects@1.15.11(debug@4.4.3): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + forwarded@0.2.0: {} + + fp-ts@1.19.3: {} + + fresh@0.5.2: {} + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + getopts@2.3.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.9 + once: 1.4.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + hardhat-watcher@2.5.0(hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6)): + dependencies: + chokidar: 3.6.0 + hardhat: 2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6) + + hardhat@2.28.6(bufferutil@4.1.0)(ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@6.0.6): + dependencies: + '@ethereumjs/util': 9.1.0 + '@ethersproject/abi': 5.8.0 + '@nomicfoundation/edr': 0.12.0-next.23 + '@nomicfoundation/solidity-analyzer': 0.1.2 + '@sentry/node': 5.30.0 + adm-zip: 0.4.16 + aggregate-error: 3.1.0 + ansi-escapes: 4.3.2 + boxen: 5.1.2 + chokidar: 4.0.3 + ci-info: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + enquirer: 2.4.1 + env-paths: 2.2.1 + ethereum-cryptography: 1.2.0 + find-up: 5.0.0 + fp-ts: 1.19.3 + fs-extra: 7.0.1 + immutable: 4.3.8 + io-ts: 1.10.4 + json-stream-stringify: 3.1.6 + keccak: 3.0.4 + lodash: 4.17.23 + micro-eth-signer: 0.14.0 + mnemonist: 0.38.5 + mocha: 10.8.2 + p-map: 4.0.0 + picocolors: 1.1.1 + raw-body: 2.5.3 + resolve: 1.17.0 + semver: 6.3.1 + solc: 0.8.26(debug@4.4.3) + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.15 + tsort: 0.0.1 + undici: 5.29.0 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + ts-node: 10.9.2(@types/node@20.19.37)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + helmet@8.1.0: {} + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + immutable@4.3.8: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + interpret@2.2.0: {} + + io-ts@1.10.4: + dependencies: + fp-ts: 1.19.3 + + ipaddr.js@1.9.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-plain-obj@2.1.0: {} + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-unicode-supported@0.1.0: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + dependencies: + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + + jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@types/connect': 3.4.38 + '@types/node': 12.20.55 + '@types/ws': 7.4.7 + commander: 2.20.3 + delay: 5.0.0 + es6-promisify: 5.0.0 + eyes: 0.1.8 + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + json-stringify-safe: 5.0.1 + stream-json: 1.9.1 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + jose@6.2.2: {} + + js-sha3@0.8.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsbi@3.2.5: {} + + json-stream-stringify@3.1.6: {} + + json-stringify-safe@5.0.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keccak@3.0.4: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.4 + readable-stream: 3.6.2 + + knex@3.1.0(pg@8.20.0): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.23 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + optionalDependencies: + pg: 8.20.0 + transitivePeerDependencies: + - supports-color + + layerr@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash@4.17.23: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru_map@0.3.3: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + media-typer@0.3.0: {} + + memorystream@0.3.1: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + micro-eth-signer@0.14.0: + dependencies: + '@noble/curves': 1.8.2 + '@noble/hashes': 1.7.2 + micro-packed: 0.7.3 + + micro-packed@0.7.3: + dependencies: + '@scure/base': 1.2.6 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@1.0.4: {} + + mnemonist@0.38.5: + dependencies: + obliterator: 2.0.5 + + mocha@10.8.2: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.4.3(supports-color@8.1.1) + diff: 5.2.2 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 5.1.9 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@0.6.3: {} + + next@16.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.6 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-addon-api@2.0.2: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + obliterator@2.0.5: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + os-tmpdir@1.0.2: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-connection-string@2.6.2: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + process-nextick-args@2.0.1: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qrcode.react@4.2.0(react@19.2.3): + dependencies: + react: 19.2.3 + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react@19.2.3: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + rechoir@0.8.0: + dependencies: + resolve: 1.22.11 + + require-directory@2.1.1: {} + + resolve-from@5.0.0: {} + + resolve@1.17.0: + dependencies: + path-parse: 1.0.7 + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + + rpc-websockets@9.3.5: + dependencies: + '@swc/helpers': 0.5.15 + '@types/uuid': 10.0.0 + '@types/ws': 8.18.1 + buffer: 6.0.3 + eventemitter3: 5.0.4 + uuid: 11.1.0 + ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + scrypt-js@3.0.1: {} + + sdp@3.2.1: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.4: + optional: true + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + solc@0.8.26(debug@4.4.3): + dependencies: + command-exists: 1.2.9 + commander: 8.3.0 + follow-redirects: 1.15.11(debug@4.4.3) + js-sha3: 0.8.0 + memorystream: 0.3.1 + semver: 5.7.2 + tmp: 0.0.33 + transitivePeerDependencies: + - debug + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + + statuses@2.0.2: {} + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.2.3): + dependencies: + client-only: 0.0.1 + react: 19.2.3 + + superstruct@2.0.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.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: {} + + tarn@3.0.2: {} + + text-encoding-utf-8@1.0.2: {} + + tildify@2.0.0: {} + + tiny-invariant@1.3.3: {} + + tiny-secp256k1@2.2.4: + dependencies: + uint8array-tools: 0.0.7 + + tiny-warning@1.0.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toformat@2.0.0: {} + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + ts-node-dev@2.0.0(@types/node@20.19.37)(typescript@5.9.3): + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.11 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.19.37)(typescript@5.9.3) + tsconfig: 7.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + + ts-node@10.9.2(@types/node@20.19.37)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.37 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig@7.0.0: + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsort@0.0.1: {} + + turbo-darwin-64@2.8.15: + optional: true + + turbo-darwin-arm64@2.8.15: + optional: true + + turbo-linux-64@2.8.15: + optional: true + + turbo-linux-arm64@2.8.15: + optional: true + + turbo-windows-64@2.8.15: + optional: true + + turbo-windows-arm64@2.8.15: + optional: true + + turbo@2.8.15: + optionalDependencies: + turbo-darwin-64: 2.8.15 + turbo-darwin-arm64: 2.8.15 + turbo-linux-64: 2.8.15 + turbo-linux-arm64: 2.8.15 + turbo-windows-64: 2.8.15 + turbo-windows-arm64: 2.8.15 + + tweetnacl@1.0.3: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.7.1: {} + + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typescript@5.9.3: {} + + uint8array-tools@0.0.7: {} + + uint8array-tools@0.0.8: {} + + uint8array-tools@0.0.9: {} + + ulidx@2.4.1: + dependencies: + layerr: 3.0.0 + + undici-types@6.21.0: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + universalify@0.1.2: {} + + unpipe@1.0.0: {} + + utf-8-validate@6.0.6: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + varuint-bitcoin@2.0.0: + dependencies: + uint8array-tools: 0.0.8 + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + webrtc-adapter@9.0.3: + dependencies: + sdp: 3.2.1 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + wif@5.0.0: + dependencies: + bs58check: 4.0.0 + + workerpool@6.5.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + 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): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + optionalDependencies: + bufferutil: 4.1.0 + utf-8-validate: 6.0.6 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} + + zustand@5.0.11(@types/react@19.2.14)(react@19.2.3): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.3 + + zxing-wasm@2.2.4(@types/emscripten@1.41.5): + dependencies: + '@types/emscripten': 1.41.5 + type-fest: 5.4.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e9b0dad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/*' + - 'packages/*' diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..f20fc07 --- /dev/null +++ b/start.bat @@ -0,0 +1,124 @@ +@echo off +setlocal enabledelayedexpansion +cd /d "%~dp0" + +echo. +echo ========================================== +echo CryptoWallet - Local Dev Starter +echo ========================================== +echo. + +:: ── 1. Check pnpm ──────────────────────────────────────────────────────────── +where pnpm >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] pnpm not found. Install with: npm install -g pnpm + pause + exit /b 1 +) + +:: ── 2. Locate PostgreSQL binaries ───────────────────────────────────────────── +set "PGBIN=" +if exist "C:\Program Files\PostgreSQL\16\bin\pg_isready.exe" set "PGBIN=C:\Program Files\PostgreSQL\16\bin" +if not defined PGBIN ( + 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 + exit /b 1 +) +if defined PGBIN ( + "%PGBIN%\pg_isready.exe" -U postgres -q 2>nul +) else ( + pg_isready -U postgres -q 2>nul +) +if errorlevel 1 ( + timeout /t 2 /nobreak >nul + goto waitpg +) +echo PostgreSQL is ready. + +:: ── 4. Ensure DB exists (create only if missing) ──────────────────────────── +echo [2/4] Ensuring database... +if defined PGBIN ( + "%PGBIN%\psql.exe" -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='cryptowallet_v2'" 2>nul | findstr "1" >nul 2>nul +) else ( + psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='cryptowallet_v2'" 2>nul | findstr "1" >nul 2>nul +) +if errorlevel 1 ( + echo Creating database cryptowallet_v2... + if defined PGBIN ( + "%PGBIN%\psql.exe" -U postgres -c "CREATE DATABASE cryptowallet_v2" 2>nul + ) else ( + psql -U postgres -c "CREATE DATABASE cryptowallet_v2" 2>nul + ) +) else ( + echo Database exists. +) + +:: ── 5. Install deps + migrate ──────────────────────────────────────────────── +echo [3/4] Installing dependencies... +if not exist .env ( + copy .env.example .env >nul + echo Created .env from .env.example +) +call pnpm install +if errorlevel 1 ( + echo [ERROR] pnpm install failed. + pause + exit /b 1 +) +echo Dependencies installed. + +echo Running migrations... +cd apps\api +call pnpm migrate +if errorlevel 1 ( + echo [ERROR] Migrations failed. + pause + exit /b 1 +) +cd ..\.. +echo Migrations done. + +:: ── 6. Stop old processes and start servers ────────────────────────────────── +echo [4/4] Starting servers... +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 +timeout /t 1 /nobreak >nul + +start "CryptoWallet API [port 3001]" cmd /k "cd /d %~dp0apps\api && pnpm dev" +timeout /t 2 /nobreak >nul +start "CryptoWallet Web [port 3000]" cmd /k "cd /d %~dp0apps\web && pnpm dev" + +echo. +echo ========================================== +echo All services started! +echo. +echo Web: http://localhost:3000 +echo API: http://localhost:3001 +echo DB: localhost:5432 / cryptowallet_v2 +echo ========================================== +echo. + +timeout /t 3 /nobreak >nul +start http://localhost:3000 + +pause >nul +endlocal diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..ca398ed --- /dev/null +++ b/turbo.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": [".next/**", "dist/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "typecheck": {} + } +}