From f250b992882711171eacbd3b97a1db540ed3e230 Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:00:21 +0300 Subject: [PATCH] Initial commit: USDT transfer worker (RabbitMQ + Postgres + Vault) --- .dockerignore | 7 + .env.example | 17 + .gitignore | 4 + Dockerfile | 18 + README.md | 139 ++ docker-compose.yml | 52 + package-lock.json | 1972 +++++++++++++++++++++++++ package.json | 29 + src/config.ts | 56 + src/db/PostgresClient.ts | 102 ++ src/domain/amount.ts | 41 + src/domain/types.ts | 3 + src/ethereum/EthereumGateway.ts | 7 + src/ethereum/EthersEthereumGateway.ts | 89 ++ src/http/app.ts | 49 + src/index.ts | 91 ++ src/logger.ts | 20 + src/queue/AmqpClient.ts | 147 ++ src/queue/messageSchema.ts | 37 + src/secrets/VaultClient.ts | 144 ++ src/services/TransferOrchestrator.ts | 201 +++ tests/amount.test.ts | 24 + tests/gasErrorMessage.test.ts | 19 + tsconfig.json | 15 + 24 files changed, 3283 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/db/PostgresClient.ts create mode 100644 src/domain/amount.ts create mode 100644 src/domain/types.ts create mode 100644 src/ethereum/EthereumGateway.ts create mode 100644 src/ethereum/EthersEthereumGateway.ts create mode 100644 src/http/app.ts create mode 100644 src/index.ts create mode 100644 src/logger.ts create mode 100644 src/queue/AmqpClient.ts create mode 100644 src/queue/messageSchema.ts create mode 100644 src/secrets/VaultClient.ts create mode 100644 src/services/TransferOrchestrator.ts create mode 100644 tests/amount.test.ts create mode 100644 tests/gasErrorMessage.test.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e37db74 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.env +.git +.gitignore +*.log +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..631378f --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +PORT=3000 +ETH_RPC_URL=https://ethereum-rpc.publicnode.com +HOT_WALLET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY +USDT_CONTRACT_ADDRESS=0xdAC17F958D2ee523a2206206994597C13D831ec7 + +VAULT_MOUNT_POINT=dev-secrets +VAULT_ADDR=https://corp.vault.elcsa.ru +VAULT_ROLE_ID= +VAULT_SECRET_ID= +LOG_LEVEL=INFO + +REQUIRED_CONFIRMATIONS=12 +MISMATCH_TIMEOUT_MS=300000 +POLL_INTERVAL_MS=15000 +MAX_TRANSFER_USDT=1000 +MIN_ETH_BALANCE_WEI=1 +WEBHOOK_TIMEOUT_MS=10000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa0926a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..78a4e76 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src ./src +COPY tests ./tests +RUN npm run build + +FROM node:20-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev && npm cache clean --force +COPY --from=build /app/dist ./dist +USER node +EXPOSE 3000 +CMD ["node", "dist/src/index.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7d3d10 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# USDT Transfer Worker + +Production worker that consumes `crypto.transfer.requested` from RabbitMQ, sends an exact-amount ERC20 USDT transfer on Ethereum mainnet from a hot wallet, verifies the recipient's balance increased, and publishes `crypto.transfer.completed` on success. Status of every order is written into the `payments` table in Postgres. + +## Architecture + +``` +RabbitMQ (crypto.transfer.requested) + │ + ▼ + AmqpConsumer ──▶ TransferOrchestrator + │ + ├─ Postgres (orders, wallets, payments) + ├─ Ethereum RPC (USDT balanceOf, transfer) + └─ AmqpPublisher (crypto.transfer.completed) +``` + +Postgres credentials and RabbitMQ credentials are pulled from HashiCorp Vault (KV v2, AppRole auth). The hot-wallet private key, ETH RPC URL and limits stay in `.env`. + +## Message contract + +Inbound — queue `crypto.transfer.requested`: + +```json +{ + "order_id": "01K...", + "user_id": "01K...", + "trace_id": "01K...", + "message_id": "01K..." +} +``` + +All four IDs are ULIDs (26 chars, Crockford base32). + +Outbound — queue `crypto.transfer.completed` (only on success): + +```json +{ + "user_id": "01K...", + "order_id": "01K...", + "trace_id": "01K...", + "message_id": "01K..." +} +``` + +`message_id` in the outbound payload is freshly generated; the other three are echoed from the request. + +## Processing flow + +1. Read `payments` row by `order_id`. If `usdt_delivered` already — skip. If terminal failure (`web3_hash_error` / `web3_balance_problem`) — also skip. +2. `SELECT usdt_amount FROM orders WHERE id = $order_id`. +3. `SELECT address FROM wallets WHERE user_id = $user_id AND chain = 'ETH' LIMIT 1`. +4. Pre-checks: hot-wallet ETH and USDT balances, `MAX_TRANSFER_USDT` cap. +5. Capture pre-balance of the recipient (`usdt.balanceOf`). +6. Broadcast `usdt.transfer(recipient, amount)`. If RPC fails — `payments.status = web3_hash_error` and stop. +7. Persist tx hash into `payments.web3_transaction_hash` immediately. +8. Balance loop: 3 attempts × 60 s. On the first attempt where `post − pre ≥ amount`, mark `payments.status = usdt_delivered`, set `paid_at`, publish to `crypto.transfer.completed`. +9. If all 3 attempts fail to observe the increase: `payments.status = web3_balance_problem` (the hash is still kept). + +Failure paths only update Postgres — the spec asks for queue publishing on success only. + +## Environment + +Required `.env` (see `.env.example`): + +``` +PORT=3000 +ETH_RPC_URL=https://ethereum-rpc.publicnode.com +HOT_WALLET_PRIVATE_KEY=0x... +USDT_CONTRACT_ADDRESS=0xdAC17F958D2ee523a2206206994597C13D831ec7 + +VAULT_MOUNT_POINT=dev-secrets +VAULT_ADDR=https://corp.vault.elcsa.ru +VAULT_ROLE_ID=... +VAULT_SECRET_ID=... +LOG_LEVEL=INFO + +REQUIRED_CONFIRMATIONS=12 +MISMATCH_TIMEOUT_MS=300000 +POLL_INTERVAL_MS=15000 +MAX_TRANSFER_USDT=1000 +MIN_ETH_BALANCE_WEI=1 +WEBHOOK_TIMEOUT_MS=10000 +``` + +`REQUIRED_CONFIRMATIONS`, `MISMATCH_TIMEOUT_MS`, `POLL_INTERVAL_MS`, `WEBHOOK_TIMEOUT_MS` are kept in the `.env` for compatibility with infra tooling but are **not consumed** by the worker — the verification cadence is fixed at 3 attempts × 60 s. + +## Vault layout (`dev-secrets/` mount, KV v2) + +`dev-secrets/database`: + +| key | value | +|----------|------------------------| +| host | postgres host | +| port | postgres port | +| name | database name | +| user | username | +| password | password | + +`dev-secrets/rabbitmq`: + +| key | value | +|----------|---------------| +| host | rabbit host | +| port | amqp port | +| vhost | virtual host | +| user | username | +| password | password | + +## HTTP + +A single endpoint is exposed for liveness/readiness probes: + +- `GET /health` — RPC block number + hot-wallet ETH/USDT balances. Returns 503 on RPC errors. + +## Local development + +```bash +npm install +cp .env.example .env # fill VAULT_ROLE_ID / VAULT_SECRET_ID, etc. +npm run build +npm start +``` + +With Docker: + +```bash +docker compose up --build +``` + +`docker-compose.yml` brings up Postgres and RabbitMQ alongside the app. Vault stays external — point `VAULT_ADDR` at your real instance. + +## Tests + +```bash +npm test +``` + +Covers amount parsing and gas error humanization. Integration testing (Postgres + RabbitMQ + RPC) is performed in the deploy environment. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dbc7ac4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres_user + POSTGRES_PASSWORD: postgres_password + POSTGRES_DB: app + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres_user -d app"] + interval: 5s + timeout: 5s + retries: 10 + + rabbitmq: + image: rabbitmq:3-management-alpine + environment: + RABBITMQ_DEFAULT_USER: rabbit + RABBITMQ_DEFAULT_PASS: rabbit + RABBITMQ_DEFAULT_VHOST: / + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbit_data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 5s + retries: 10 + + app: + build: + context: . + dockerfile: Dockerfile + env_file: + - .env + ports: + - "3000:3000" + depends_on: + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + restart: unless-stopped + +volumes: + pg_data: + rabbit_data: diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a6e3432 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1972 @@ +{ + "name": "usdt-transfer-confirmation-service", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "usdt-transfer-confirmation-service", + "version": "0.1.0", + "dependencies": { + "amqplib": "^0.10.4", + "dotenv": "^16.4.7", + "ethers": "^6.13.5", + "express": "^4.21.2", + "pg": "^8.13.1", + "pino": "^9.5.0", + "ulid": "^2.3.0" + }, + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/express": "^5.0.0", + "@types/node": "^22.10.5", + "@types/pg": "^8.11.10", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "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.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "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" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "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.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ulid": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", + "integrity": "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==", + "license": "MIT", + "bin": { + "ulid": "bin/cli.js" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "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 + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..88b1d9d --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "usdt-transfer-confirmation-service", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/src/index.js", + "test": "npm run build && node --test \"dist/tests/**/*.test.js\"" + }, + "dependencies": { + "amqplib": "^0.10.4", + "dotenv": "^16.4.7", + "ethers": "^6.13.5", + "express": "^4.21.2", + "pg": "^8.13.1", + "pino": "^9.5.0", + "ulid": "^2.3.0" + }, + "devDependencies": { + "@types/amqplib": "^0.10.5", + "@types/express": "^5.0.0", + "@types/node": "^22.10.5", + "@types/pg": "^8.11.10", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..7a3c29c --- /dev/null +++ b/src/config.ts @@ -0,0 +1,56 @@ +import { parseUsdtAmount } from "./domain/amount.js"; + +export const MAINNET_USDT_CONTRACT = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + +export interface AppConfig { + port: number; + ethRpcUrl: string; + hotWalletPrivateKey: string; + usdtContractAddress: string; + maxTransferAmountUnits: bigint; + minEthBalanceWei: bigint; + balanceCheckAttempts: number; + balanceCheckIntervalMs: number; + vaultAddr: string; + vaultMountPoint: string; + vaultRoleId: string; + vaultSecretId: string; + logLevel: string; +} + +export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { + return { + port: readInteger(env.PORT, 3000), + ethRpcUrl: readRequired(env.ETH_RPC_URL, "ETH_RPC_URL"), + hotWalletPrivateKey: readRequired(env.HOT_WALLET_PRIVATE_KEY, "HOT_WALLET_PRIVATE_KEY"), + usdtContractAddress: env.USDT_CONTRACT_ADDRESS ?? MAINNET_USDT_CONTRACT, + maxTransferAmountUnits: parseUsdtAmount(env.MAX_TRANSFER_USDT ?? "1000"), + minEthBalanceWei: BigInt(env.MIN_ETH_BALANCE_WEI ?? "1"), + balanceCheckAttempts: 3, + balanceCheckIntervalMs: 60_000, + vaultAddr: readRequired(env.VAULT_ADDR, "VAULT_ADDR"), + vaultMountPoint: readRequired(env.VAULT_MOUNT_POINT, "VAULT_MOUNT_POINT"), + vaultRoleId: readRequired(env.VAULT_ROLE_ID, "VAULT_ROLE_ID"), + vaultSecretId: readRequired(env.VAULT_SECRET_ID, "VAULT_SECRET_ID"), + logLevel: env.LOG_LEVEL ?? "INFO" + }; +} + +function readRequired(value: string | undefined, name: string): string { + if (!value) { + throw new Error(`${name} is required`); + } + return value; +} + +function readInteger(value: string | undefined, defaultValue: number): number { + if (!value) { + return defaultValue; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error(`Expected positive integer, got ${value}`); + } + return parsed; +} diff --git a/src/db/PostgresClient.ts b/src/db/PostgresClient.ts new file mode 100644 index 0000000..a26b2c3 --- /dev/null +++ b/src/db/PostgresClient.ts @@ -0,0 +1,102 @@ +import pg from "pg"; +import type { DatabaseSecret } from "../secrets/VaultClient.js"; + +const { Pool } = pg; + +export type PaymentStatus = + | "pending" + | "usdt_delivered" + | "web3_hash_error" + | "web3_balance_problem" + | string; + +export interface PaymentRecord { + status: PaymentStatus; + web3_transaction_hash: string | null; +} + +export class PostgresClient { + private readonly pool: pg.Pool; + + constructor(secret: DatabaseSecret) { + this.pool = new Pool({ + host: secret.host, + port: secret.port, + database: secret.name, + user: secret.user, + password: secret.password, + max: 5, + idleTimeoutMillis: 30_000, + connectionTimeoutMillis: 10_000 + }); + } + + async ping(): Promise { + await this.pool.query("SELECT 1"); + } + + async getOrderUsdtAmount(orderId: string): Promise { + const result = await this.pool.query<{ usdt_amount: string }>( + "SELECT usdt_amount::text AS usdt_amount FROM orders WHERE id = $1", + [orderId] + ); + return result.rows[0]?.usdt_amount ?? null; + } + + async getUserEthWalletAddress(userId: string): Promise { + const result = await this.pool.query<{ address: string }>( + "SELECT address FROM wallets WHERE user_id = $1 AND chain = 'ETH' LIMIT 1", + [userId] + ); + return result.rows[0]?.address ?? null; + } + + async getPaymentByOrderId(orderId: string): Promise { + const result = await this.pool.query( + "SELECT status, web3_transaction_hash FROM payments WHERE order_id = $1", + [orderId] + ); + return result.rows[0] ?? null; + } + + async setPaymentTxHash(orderId: string, txHash: string): Promise { + await this.pool.query( + "UPDATE payments SET web3_transaction_hash = $2, updated_at = now() WHERE order_id = $1", + [orderId, txHash] + ); + } + + async markPaymentDelivered(orderId: string, txHash: string): Promise { + await this.pool.query( + `UPDATE payments + SET status = 'usdt_delivered', + web3_transaction_hash = $2, + paid_at = now(), + updated_at = now() + WHERE order_id = $1`, + [orderId, txHash] + ); + } + + async markPaymentHashError(orderId: string): Promise { + await this.pool.query( + "UPDATE payments SET status = 'web3_hash_error', updated_at = now() WHERE order_id = $1", + [orderId] + ); + } + + async markPaymentBalanceProblem(orderId: string, txHash: string): Promise { + await this.pool.query( + `UPDATE payments + SET status = 'web3_balance_problem', + web3_transaction_hash = $2, + updated_at = now() + WHERE order_id = $1`, + [orderId, txHash] + ); + } + + async close(): Promise { + await this.pool.end(); + } +} diff --git a/src/domain/amount.ts b/src/domain/amount.ts new file mode 100644 index 0000000..e1d51f0 --- /dev/null +++ b/src/domain/amount.ts @@ -0,0 +1,41 @@ +export const USDT_DECIMALS = 6; +export const USDT_SCALE = 1_000_000n; + +export function parseUsdtAmount(input: string): bigint { + const value = input.trim(); + + if (!/^\d+(\.\d+)?$/.test(value)) { + throw new Error("Amount must be a valid decimal string"); + } + + const [wholePart, fractionalPart = ""] = value.split("."); + if (fractionalPart.length > USDT_DECIMALS) { + throw new Error("Amount must have at most 6 decimals"); + } + + const wholeUnits = BigInt(wholePart) * USDT_SCALE; + const fractionalUnits = BigInt((fractionalPart || "0").padEnd(USDT_DECIMALS, "0")); + const units = wholeUnits + fractionalUnits; + + if (units <= 0n) { + throw new Error("Amount must be greater than zero"); + } + + return units; +} + +export function formatUsdtAmount(units: bigint): string { + const whole = units / USDT_SCALE; + const fractional = units % USDT_SCALE; + + if (fractional === 0n) { + return whole.toString(); + } + + const trimmedFractional = fractional + .toString() + .padStart(USDT_DECIMALS, "0") + .replace(/0+$/, ""); + + return `${whole}.${trimmedFractional}`; +} diff --git a/src/domain/types.ts b/src/domain/types.ts new file mode 100644 index 0000000..bd35523 --- /dev/null +++ b/src/domain/types.ts @@ -0,0 +1,3 @@ +export const PAYMENT_STATUS_USDT_DELIVERED = "usdt_delivered"; +export const PAYMENT_STATUS_HASH_ERROR = "web3_hash_error"; +export const PAYMENT_STATUS_BALANCE_PROBLEM = "web3_balance_problem"; diff --git a/src/ethereum/EthereumGateway.ts b/src/ethereum/EthereumGateway.ts new file mode 100644 index 0000000..66ee507 --- /dev/null +++ b/src/ethereum/EthereumGateway.ts @@ -0,0 +1,7 @@ +export interface EthereumGateway { + getHotWalletAddress(): string; + getCurrentBlockNumber(): Promise; + getEthBalance(address?: string): Promise; + getUsdtBalance(address: string): Promise; + sendUsdtTransfer(to: string, amountUnits: bigint): Promise<{ hash: string }>; +} diff --git a/src/ethereum/EthersEthereumGateway.ts b/src/ethereum/EthersEthereumGateway.ts new file mode 100644 index 0000000..1ede591 --- /dev/null +++ b/src/ethereum/EthersEthereumGateway.ts @@ -0,0 +1,89 @@ +import { Contract, JsonRpcProvider, Wallet, getAddress } from "ethers"; +import type { EthereumGateway } from "./EthereumGateway.js"; + +const USDT_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function transfer(address to, uint256 value) returns (bool)" +]; + +interface EthersEthereumGatewayOptions { + rpcUrl: string; + privateKey: string; + usdtContractAddress: string; +} + +export class EthersEthereumGateway implements EthereumGateway { + private readonly provider: JsonRpcProvider; + private readonly wallet: Wallet; + private readonly usdt: Contract; + + constructor(options: EthersEthereumGatewayOptions) { + this.provider = new JsonRpcProvider(options.rpcUrl); + this.wallet = new Wallet(options.privateKey, this.provider); + const usdtAddress = getAddress(options.usdtContractAddress); + this.usdt = new Contract(usdtAddress, USDT_ABI, this.wallet); + } + + getHotWalletAddress(): string { + return this.wallet.address; + } + + async getCurrentBlockNumber(): Promise { + return this.provider.getBlockNumber(); + } + + async getEthBalance(address = this.wallet.address): Promise { + return this.provider.getBalance(address); + } + + async getUsdtBalance(address: string): Promise { + return BigInt(await this.usdt.balanceOf(getAddress(address))); + } + + async sendUsdtTransfer(to: string, amountUnits: bigint): Promise<{ hash: string }> { + try { + const tx = await this.usdt.transfer(getAddress(to), amountUnits); + return { hash: tx.hash }; + } catch (error) { + throw new Error(humanizeTransferError(error)); + } + } +} + +export function humanizeTransferError(error: unknown): string { + const fallback = error instanceof Error ? error.message : String(error); + const providerMessage = extractProviderMessage(error); + + if (providerMessage.includes("max fee per gas less than block base fee")) { + const maxFeeMatch = providerMessage.match(/maxFeePerGas:\s*(\d+)/i); + const baseFeeMatch = providerMessage.match(/baseFee:\s*(\d+)/i); + const configured = maxFeeMatch ? formatGwei(BigInt(maxFeeMatch[1])) : "configured"; + const required = baseFeeMatch ? formatGwei(BigInt(baseFeeMatch[1])) : "current"; + + return `Transfer fee ${configured} gwei is below current base fee ${required} gwei.`; + } + + return fallback; +} + +function extractProviderMessage(error: unknown): string { + if (error && typeof error === "object") { + const candidate = error as { + info?: { + error?: { + message?: string; + }; + }; + }; + + return candidate.info?.error?.message ?? ""; + } + + return ""; +} + +function formatGwei(valueWei: bigint): string { + const whole = valueWei / 1_000_000_000n; + const fraction = (valueWei % 1_000_000_000n).toString().padStart(9, "0").replace(/0+$/, ""); + return fraction ? `${whole}.${fraction}` : whole.toString(); +} diff --git a/src/http/app.ts b/src/http/app.ts new file mode 100644 index 0000000..35286f7 --- /dev/null +++ b/src/http/app.ts @@ -0,0 +1,49 @@ +import express, { type NextFunction, type Request, type Response } from "express"; +import type { Logger } from "pino"; +import { formatUsdtAmount } from "../domain/amount.js"; +import type { EthereumGateway } from "../ethereum/EthereumGateway.js"; + +interface AppOptions { + ethereum: EthereumGateway; + logger: Logger; +} + +export function createApp(options: AppOptions) { + const app = express(); + const { ethereum, logger } = options; + + app.get("/health", async (_req, res) => { + try { + const hotWalletAddress = ethereum.getHotWalletAddress(); + const [blockNumber, ethBalanceWei, usdtBalanceUnits] = await Promise.all([ + ethereum.getCurrentBlockNumber(), + ethereum.getEthBalance(hotWalletAddress), + ethereum.getUsdtBalance(hotWalletAddress) + ]); + + res.json({ + status: "ok", + blockNumber, + hotWalletAddress, + hotWalletEthBalanceWei: ethBalanceWei.toString(), + hotWalletUsdtBalanceUnits: usdtBalanceUnits.toString(), + hotWalletUsdtBalance: formatUsdtAmount(usdtBalanceUnits) + }); + } catch (error) { + logger.error({ err: error }, "/health degraded"); + res.status(503).json({ + status: "degraded", + error: error instanceof Error ? error.message : String(error) + }); + } + }); + + app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => { + logger.error({ err: error }, "unhandled http error"); + res.status(500).json({ + error: error instanceof Error ? error.message : String(error) + }); + }); + + return app; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..13ff47a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,91 @@ +import "dotenv/config"; +import type { Server } from "node:http"; +import { loadConfig } from "./config.js"; +import { PostgresClient } from "./db/PostgresClient.js"; +import { EthersEthereumGateway } from "./ethereum/EthersEthereumGateway.js"; +import { createApp } from "./http/app.js"; +import { createLogger } from "./logger.js"; +import { AmqpClient } from "./queue/AmqpClient.js"; +import { VaultClient } from "./secrets/VaultClient.js"; +import { TransferOrchestrator } from "./services/TransferOrchestrator.js"; + +async function main(): Promise { + const config = loadConfig(); + const logger = createLogger(config.logLevel); + logger.info({ event: "bootstrap.start" }, "starting usdt-transfer service"); + + const vault = new VaultClient({ + addr: config.vaultAddr, + mountPoint: config.vaultMountPoint, + roleId: config.vaultRoleId, + secretId: config.vaultSecretId + }); + const secrets = await vault.bootstrap(); + logger.info({ event: "bootstrap.vault_loaded" }, "vault secrets loaded"); + + const db = new PostgresClient(secrets.database); + await db.ping(); + logger.info({ event: "bootstrap.pg_ready" }, "postgres pool ready"); + + const ethereum = new EthersEthereumGateway({ + rpcUrl: config.ethRpcUrl, + privateKey: config.hotWalletPrivateKey, + usdtContractAddress: config.usdtContractAddress + }); + + let triggerShutdown: (reason: string) => void = () => {}; + + const amqp = new AmqpClient({ + secret: secrets.rabbitmq, + logger, + onConnectionLost: () => triggerShutdown("amqp_connection_lost") + }); + await amqp.connect(); + + const orchestrator = new TransferOrchestrator(db, ethereum, amqp, { + maxTransferAmountUnits: config.maxTransferAmountUnits, + minEthBalanceWei: config.minEthBalanceWei, + balanceCheckAttempts: config.balanceCheckAttempts, + balanceCheckIntervalMs: config.balanceCheckIntervalMs + }); + + await amqp.startConsumer((message, log) => orchestrator.handle(message, log)); + + const app = createApp({ ethereum, logger }); + const server: Server = await new Promise((resolve) => { + const s = app.listen(config.port, () => resolve(s)); + }); + logger.info({ event: "bootstrap.http_listening", port: config.port }, "http listening"); + + let shuttingDown = false; + triggerShutdown = (reason: string) => { + if (shuttingDown) { + return; + } + shuttingDown = true; + logger.warn({ event: "shutdown.start", reason }, "shutting down"); + + void (async () => { + try { + await new Promise((resolve) => server.close(() => resolve())); + await amqp.close(); + await db.close(); + logger.info({ event: "shutdown.done" }, "shutdown complete"); + process.exit(reason === "amqp_connection_lost" ? 1 : 0); + } catch (error) { + logger.error({ event: "shutdown.error", err: error }, "shutdown error"); + process.exit(1); + } + })(); + }; + + process.on("SIGINT", () => triggerShutdown("SIGINT")); + process.on("SIGTERM", () => triggerShutdown("SIGTERM")); +} + +main().catch((error) => { + // Logger may not be ready yet; fall back to stderr. + // eslint-disable-next-line no-console + console.error("fatal bootstrap error:", error); + process.exit(1); +}); diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..a5c56ad --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,20 @@ +import { pino, type Logger } from "pino"; + +const VALID_LEVELS = new Set(["trace", "debug", "info", "warn", "error", "fatal", "silent"]); + +function resolveLevel(input: string | undefined): string { + if (!input) { + return "info"; + } + + const normalized = input.toLowerCase(); + return VALID_LEVELS.has(normalized) ? normalized : "info"; +} + +export function createLogger(level: string | undefined): Logger { + return pino({ + level: resolveLevel(level), + base: { service: "usdt-transfer" }, + timestamp: pino.stdTimeFunctions.isoTime + }); +} diff --git a/src/queue/AmqpClient.ts b/src/queue/AmqpClient.ts new file mode 100644 index 0000000..8bc172b --- /dev/null +++ b/src/queue/AmqpClient.ts @@ -0,0 +1,147 @@ +import amqplib, { type ChannelModel, type Channel, type ConsumeMessage } from "amqplib"; +import type { Logger } from "pino"; +import type { RabbitMqSecret } from "../secrets/VaultClient.js"; +import type { CryptoTransferCompleted, CryptoTransferRequest } from "./messageSchema.js"; +import { parseCryptoTransferRequest } from "./messageSchema.js"; + +export const REQUEST_QUEUE = "crypto.transfer.requested"; +export const COMPLETED_QUEUE = "crypto.transfer.completed"; + +export type TransferRequestHandler = ( + message: CryptoTransferRequest, + log: Logger +) => Promise; + +export interface AmqpClientOptions { + secret: RabbitMqSecret; + logger: Logger; + onConnectionLost: () => void; +} + +export class AmqpClient { + private readonly logger: Logger; + private readonly secret: RabbitMqSecret; + private readonly onConnectionLost: () => void; + private connection?: ChannelModel; + private channel?: Channel; + + constructor(options: AmqpClientOptions) { + this.secret = options.secret; + this.logger = options.logger.child({ component: "amqp" }); + this.onConnectionLost = options.onConnectionLost; + } + + async connect(): Promise { + this.connection = await amqplib.connect({ + protocol: "amqp", + hostname: this.secret.host, + port: this.secret.port, + username: this.secret.user, + password: this.secret.password, + vhost: this.secret.vhost + }); + + this.connection.on("error", (error) => { + this.logger.error({ err: error }, "amqp connection error"); + }); + this.connection.on("close", () => { + this.logger.error("amqp connection closed"); + this.onConnectionLost(); + }); + + this.channel = await this.connection.createChannel(); + await this.channel.prefetch(1); + await this.channel.assertQueue(REQUEST_QUEUE, { durable: true }); + await this.channel.assertQueue(COMPLETED_QUEUE, { durable: true }); + + this.logger.info( + { host: this.secret.host, port: this.secret.port, vhost: this.secret.vhost }, + "amqp connected" + ); + } + + async startConsumer(handler: TransferRequestHandler): Promise { + if (!this.channel) { + throw new Error("AmqpClient not connected"); + } + + await this.channel.consume(REQUEST_QUEUE, (msg) => { + if (!msg) { + return; + } + void this.dispatch(msg, handler); + }); + + this.logger.info({ queue: REQUEST_QUEUE }, "amqp consumer started"); + } + + publishCompleted(payload: CryptoTransferCompleted): boolean { + if (!this.channel) { + throw new Error("AmqpClient not connected"); + } + + return this.channel.sendToQueue( + COMPLETED_QUEUE, + Buffer.from(JSON.stringify(payload)), + { contentType: "application/json", persistent: true } + ); + } + + async close(): Promise { + try { + await this.channel?.close(); + } catch { + /* ignore */ + } + try { + await this.connection?.close(); + } catch { + /* ignore */ + } + } + + private async dispatch(msg: ConsumeMessage, handler: TransferRequestHandler): Promise { + if (!this.channel) { + return; + } + const channel = this.channel; + + let parsed: CryptoTransferRequest; + try { + const body = JSON.parse(msg.content.toString("utf8")) as unknown; + parsed = parseCryptoTransferRequest(body); + } catch (error) { + this.logger.error( + { err: error, raw: safeRaw(msg) }, + "rejected malformed message" + ); + channel.ack(msg); + return; + } + + const log = this.logger.child({ + trace_id: parsed.trace_id, + order_id: parsed.order_id, + user_id: parsed.user_id, + message_id: parsed.message_id + }); + + log.info({ event: "transfer.requested.received" }, "received from queue"); + + try { + await handler(parsed, log); + channel.ack(msg); + } catch (error) { + log.error({ err: error }, "infrastructure error during message handling, requeue=false"); + channel.nack(msg, false, false); + } + } +} + +function safeRaw(msg: ConsumeMessage): string { + try { + return msg.content.toString("utf8").slice(0, 512); + } catch { + return ""; + } +} diff --git a/src/queue/messageSchema.ts b/src/queue/messageSchema.ts new file mode 100644 index 0000000..8e14b90 --- /dev/null +++ b/src/queue/messageSchema.ts @@ -0,0 +1,37 @@ +export interface CryptoTransferRequest { + order_id: string; + user_id: string; + trace_id: string; + message_id: string; +} + +export interface CryptoTransferCompleted { + user_id: string; + order_id: string; + trace_id: string; + message_id: string; +} + +const ULID_REGEX = /^[0-9A-HJKMNP-TV-Z]{26}$/; + +export function parseCryptoTransferRequest(raw: unknown): CryptoTransferRequest { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new Error("message body must be a JSON object"); + } + + const candidate = raw as Record; + return { + order_id: requireUlid(candidate, "order_id"), + user_id: requireUlid(candidate, "user_id"), + trace_id: requireUlid(candidate, "trace_id"), + message_id: requireUlid(candidate, "message_id") + }; +} + +function requireUlid(source: Record, key: string): string { + const value = source[key]; + if (typeof value !== "string" || !ULID_REGEX.test(value)) { + throw new Error(`field '${key}' must be a ULID string`); + } + return value; +} diff --git a/src/secrets/VaultClient.ts b/src/secrets/VaultClient.ts new file mode 100644 index 0000000..08df37f --- /dev/null +++ b/src/secrets/VaultClient.ts @@ -0,0 +1,144 @@ +export interface VaultClientOptions { + addr: string; + mountPoint: string; + roleId: string; + secretId: string; + requestTimeoutMs?: number; +} + +export interface DatabaseSecret { + host: string; + port: number; + name: string; + user: string; + password: string; +} + +export interface RabbitMqSecret { + host: string; + port: number; + vhost: string; + user: string; + password: string; +} + +export interface BootstrappedSecrets { + database: DatabaseSecret; + rabbitmq: RabbitMqSecret; +} + +export class VaultClient { + private readonly baseUrl: string; + private readonly mountPoint: string; + private readonly roleId: string; + private readonly secretId: string; + private readonly timeoutMs: number; + private token?: string; + + constructor(options: VaultClientOptions) { + this.baseUrl = options.addr.replace(/\/+$/, ""); + this.mountPoint = options.mountPoint; + this.roleId = options.roleId; + this.secretId = options.secretId; + this.timeoutMs = options.requestTimeoutMs ?? 10_000; + } + + async bootstrap(): Promise { + await this.login(); + const [database, rabbitmq] = await Promise.all([ + this.readSecret("database"), + this.readSecret("rabbitmq") + ]); + + return { + database: parseDatabaseSecret(database), + rabbitmq: parseRabbitMqSecret(rabbitmq) + }; + } + + private async login(): Promise { + const response = await this.fetch(`${this.baseUrl}/v1/auth/approle/login`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ role_id: this.roleId, secret_id: this.secretId }) + }); + + const json = (await response.json()) as { auth?: { client_token?: string } }; + const token = json.auth?.client_token; + if (!token) { + throw new Error("Vault AppRole login did not return a client_token"); + } + this.token = token; + } + + private async readSecret(path: string): Promise> { + if (!this.token) { + throw new Error("Vault token is missing; call bootstrap() first"); + } + + const url = `${this.baseUrl}/v1/${this.mountPoint}/data/${path}`; + const response = await this.fetch(url, { + method: "GET", + headers: { "X-Vault-Token": this.token } + }); + + const json = (await response.json()) as { data?: { data?: Record } }; + const data = json.data?.data; + if (!data || typeof data !== "object") { + throw new Error(`Vault secret ${this.mountPoint}/${path} is empty or malformed`); + } + return data; + } + + private async fetch(url: string, init: RequestInit): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const response = await fetch(url, { ...init, signal: controller.signal }); + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`Vault request to ${url} failed with HTTP ${response.status}: ${text}`); + } + return response; + } finally { + clearTimeout(timeout); + } + } +} + +function parseDatabaseSecret(raw: Record): DatabaseSecret { + return { + host: requireString(raw, "host", "database"), + port: requirePort(raw, "port", "database"), + name: requireString(raw, "name", "database"), + user: requireString(raw, "user", "database"), + password: requireString(raw, "password", "database") + }; +} + +function parseRabbitMqSecret(raw: Record): RabbitMqSecret { + return { + host: requireString(raw, "host", "rabbitmq"), + port: requirePort(raw, "port", "rabbitmq"), + vhost: requireString(raw, "vhost", "rabbitmq"), + user: requireString(raw, "user", "rabbitmq"), + password: requireString(raw, "password", "rabbitmq") + }; +} + +function requireString(raw: Record, key: string, secretName: string): string { + const value = raw[key]; + if (typeof value !== "string" || !value) { + throw new Error(`Vault secret '${secretName}' is missing string field '${key}'`); + } + return value; +} + +function requirePort(raw: Record, key: string, secretName: string): number { + const value = raw[key]; + const numeric = typeof value === "number" ? value : Number(value); + if (!Number.isFinite(numeric) || numeric <= 0 || numeric > 65535) { + throw new Error(`Vault secret '${secretName}' has invalid port '${String(value)}'`); + } + return Math.trunc(numeric); +} diff --git a/src/services/TransferOrchestrator.ts b/src/services/TransferOrchestrator.ts new file mode 100644 index 0000000..d395e5e --- /dev/null +++ b/src/services/TransferOrchestrator.ts @@ -0,0 +1,201 @@ +import { isAddress } from "ethers"; +import type { Logger } from "pino"; +import { ulid } from "ulid"; +import { parseUsdtAmount } from "../domain/amount.js"; +import type { PostgresClient } from "../db/PostgresClient.js"; +import type { EthereumGateway } from "../ethereum/EthereumGateway.js"; +import type { AmqpClient } from "../queue/AmqpClient.js"; +import type { CryptoTransferRequest } from "../queue/messageSchema.js"; + +export interface TransferOrchestratorOptions { + maxTransferAmountUnits: bigint; + minEthBalanceWei: bigint; + balanceCheckAttempts: number; + balanceCheckIntervalMs: number; + sleep?: (ms: number) => Promise; +} + +export class TransferOrchestrator { + private readonly sleep: (ms: number) => Promise; + + constructor( + private readonly db: PostgresClient, + private readonly ethereum: EthereumGateway, + private readonly amqp: AmqpClient, + private readonly options: TransferOrchestratorOptions + ) { + this.sleep = options.sleep ?? defaultSleep; + } + + async handle(message: CryptoTransferRequest, log: Logger): Promise { + const existing = await this.db.getPaymentByOrderId(message.order_id); + if (!existing) { + log.error({ event: "payment.missing" }, "no payment row for order_id, ack"); + return; + } + + if (existing.status === "usdt_delivered") { + log.info({ event: "payment.already_delivered" }, "skip — already delivered"); + return; + } + + if (existing.status === "web3_hash_error" || existing.status === "web3_balance_problem") { + log.warn( + { event: "payment.terminal_state", status: existing.status }, + "skip — payment already in terminal failure state" + ); + return; + } + + const usdtAmountString = await this.db.getOrderUsdtAmount(message.order_id); + if (!usdtAmountString) { + log.error({ event: "order.not_found" }, "order missing for order_id"); + await this.db.markPaymentHashError(message.order_id); + return; + } + + let amountUnits: bigint; + try { + amountUnits = parseUsdtAmount(usdtAmountString); + } catch (error) { + log.error( + { event: "order.invalid_amount", err: error, usdt_amount: usdtAmountString }, + "could not parse usdt_amount" + ); + await this.db.markPaymentHashError(message.order_id); + return; + } + + if (amountUnits > this.options.maxTransferAmountUnits) { + log.error( + { event: "transfer.amount_exceeds_limit", amount_units: amountUnits.toString() }, + "usdt_amount exceeds MAX_TRANSFER_USDT" + ); + await this.db.markPaymentHashError(message.order_id); + return; + } + + const recipient = await this.db.getUserEthWalletAddress(message.user_id); + if (!recipient || !isAddress(recipient)) { + log.error({ event: "wallet.not_found_or_invalid", recipient }, "no valid ETH wallet for user"); + await this.db.markPaymentHashError(message.order_id); + return; + } + + try { + await this.assertHotWalletReady(amountUnits); + } catch (error) { + log.error({ event: "hot_wallet.not_ready", err: error }, "hot wallet pre-check failed"); + await this.db.markPaymentHashError(message.order_id); + return; + } + + let preBalance: bigint; + try { + preBalance = await this.ethereum.getUsdtBalance(recipient); + } catch (error) { + log.error({ event: "rpc.pre_balance_failed", err: error }, "failed to read recipient pre-balance"); + await this.db.markPaymentHashError(message.order_id); + return; + } + + log.info( + { event: "transfer.pre_balance", pre_balance_units: preBalance.toString(), amount_units: amountUnits.toString() }, + "captured pre-balance" + ); + + let txHash: string; + try { + const tx = await this.ethereum.sendUsdtTransfer(recipient, amountUnits); + txHash = tx.hash; + } catch (error) { + log.error({ event: "transfer.broadcast_failed", err: error }, "broadcast failed"); + await this.db.markPaymentHashError(message.order_id); + return; + } + + log.info({ event: "transfer.broadcasted", tx_hash: txHash }, "broadcast OK"); + await this.db.setPaymentTxHash(message.order_id, txHash); + + const delivered = await this.pollForBalance(recipient, preBalance, amountUnits, log); + if (!delivered) { + log.error({ event: "transfer.balance_problem", tx_hash: txHash }, "balance did not increase after 3 checks"); + await this.db.markPaymentBalanceProblem(message.order_id, txHash); + return; + } + + await this.db.markPaymentDelivered(message.order_id, txHash); + log.info({ event: "transfer.delivered", tx_hash: txHash }, "marked usdt_delivered"); + + const completedMessageId = ulid(); + this.amqp.publishCompleted({ + user_id: message.user_id, + order_id: message.order_id, + trace_id: message.trace_id, + message_id: completedMessageId + }); + log.info( + { event: "transfer.completed.published", message_id: completedMessageId }, + "published crypto.transfer.completed" + ); + } + + private async assertHotWalletReady(amountUnits: bigint): Promise { + const hotWalletAddress = this.ethereum.getHotWalletAddress(); + const [ethBalanceWei, usdtBalanceUnits] = await Promise.all([ + this.ethereum.getEthBalance(hotWalletAddress), + this.ethereum.getUsdtBalance(hotWalletAddress) + ]); + + if (ethBalanceWei < this.options.minEthBalanceWei) { + throw new Error("Hot wallet ETH balance is below gas threshold"); + } + if (usdtBalanceUnits < amountUnits) { + throw new Error("Hot wallet USDT balance is insufficient"); + } + } + + private async pollForBalance( + recipient: string, + preBalance: bigint, + amountUnits: bigint, + log: Logger + ): Promise { + for (let attempt = 1; attempt <= this.options.balanceCheckAttempts; attempt += 1) { + await this.sleep(this.options.balanceCheckIntervalMs); + + let postBalance: bigint; + try { + postBalance = await this.ethereum.getUsdtBalance(recipient); + } catch (error) { + log.warn( + { event: "balance_check.rpc_error", attempt, err: error }, + "balance check RPC error" + ); + continue; + } + + const delta = postBalance - preBalance; + log.info( + { + event: "balance_check.result", + attempt, + attempts_total: this.options.balanceCheckAttempts, + post_balance_units: postBalance.toString(), + delta_units: delta.toString() + }, + "balance check" + ); + + if (delta >= amountUnits) { + return true; + } + } + + return false; + } +} + +function defaultSleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/tests/amount.test.ts b/tests/amount.test.ts new file mode 100644 index 0000000..b0a778d --- /dev/null +++ b/tests/amount.test.ts @@ -0,0 +1,24 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { formatUsdtAmount, parseUsdtAmount } from "../src/domain/amount.js"; + +void describe("USDT amount parsing", () => { + void it("parses whole and fractional USDT with 6 decimals", () => { + assert.equal(parseUsdtAmount("1"), 1_000_000n); + assert.equal(parseUsdtAmount("1.234567"), 1_234_567n); + assert.equal(parseUsdtAmount("0.000001"), 1n); + }); + + void it("rejects zero, negative, malformed, and over-precise values", () => { + assert.throws(() => parseUsdtAmount("0"), /greater than zero/); + assert.throws(() => parseUsdtAmount("-1"), /valid decimal/); + assert.throws(() => parseUsdtAmount("1.0000001"), /at most 6 decimals/); + assert.throws(() => parseUsdtAmount("1e3"), /valid decimal/); + }); + + void it("formats raw token units back to a trimmed USDT string", () => { + assert.equal(formatUsdtAmount(1_234_567n), "1.234567"); + assert.equal(formatUsdtAmount(1_000_000n), "1"); + assert.equal(formatUsdtAmount(10n), "0.00001"); + }); +}); diff --git a/tests/gasErrorMessage.test.ts b/tests/gasErrorMessage.test.ts new file mode 100644 index 0000000..227fadb --- /dev/null +++ b/tests/gasErrorMessage.test.ts @@ -0,0 +1,19 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { humanizeTransferError } from "../src/ethereum/EthersEthereumGateway.js"; + +void describe("humanizeTransferError", () => { + void it("translates low gas price into a readable error", () => { + const message = humanizeTransferError({ + message: "missing revert data", + info: { + error: { + message: + "failed with 3080732 gas: max fee per gas less than block base fee: address 0xabc, maxFeePerGas: 960000000, baseFee: 1575278655" + } + } + }); + + assert.match(message, /Transfer fee 0.96 gwei is below current base fee 1.575278655 gwei/); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6ea676f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +}