# 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 USDT contract can also be pulled from Vault via the optional `ethereum` secret; `.env` remains as a fallback for those fields and runtime limits. ## 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= 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 | Optional `dev-secrets/ethereum`: | key | value | |------------------------|--------------------------| | rpc_url | Ethereum RPC URL | | hot_wallet_private_key | company wallet private key | | usdt_contract_address | USDT contract address | ## HTTP A single endpoint is exposed for liveness/readiness probes: - `GET /health` — RPC block number, hot-wallet ETH/USDT balances, Postgres ping and RabbitMQ connection state. Returns 503 if any dependency is degraded. ## 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.