Files
web3-payment/README.md

148 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.