docs: sync from backend 22ae0bd — scanner balance watches

This commit is contained in:
Siavash Sameni
2026-06-03 21:23:50 +04:00
parent 4b1d8ea36d
commit 9dcdb420fc
9 changed files with 866 additions and 13 deletions

View File

@@ -9,7 +9,7 @@ created: 2026-05-30
AMN Pay Scanner is a standalone Go microservice that watches on-chain payment events and notifies the backend via webhook when a payment is confirmed. It replaces the Request Network integration with an in-house polling scanner that supports EVM chains, Tron, and TON.
> [!info]
> Repo: `scanner/` within the escrow monorepo. Binary: `scanner`. Written in Go 1.25. SQLite (WAL mode) for state. No external dependencies beyond the chain APIs.
> Repo: `scanner/` within the escrow monorepo. Binary: `scanner`. Written in Go 1.25. SQLite (WAL mode) for state. No external dependencies beyond the chain APIs. Scanner `0.1.8` also supports direct-address EVM ERC-20 balance reads and balance watches for non-smart-contract payment rails.
---
@@ -21,6 +21,8 @@ AMN Pay Scanner is a standalone Go microservice that watches on-chain payment ev
- Deliver a signed webhook to the backend callback URL when confirmed
- Retry failed webhook deliveries
- Expire stale pending intents on a configurable TTL
- Read an EVM ERC-20 balance on demand for a backend-supplied address/token
- Watch an EVM address/token pair for balance changes, decay polling cadence over time, and stop after backend cancellation or 7-day expiry
---
@@ -41,10 +43,13 @@ AMN Pay Scanner is a standalone Go microservice that watches on-chain payment ev
│ │ └── TonChainWorker ton_chain.go (TON) │
│ ├── HTTP routes api.go / main.go │
│ ├── intent TTL expiry main.go + intent.go │
── webhook retry loop main.go + webhook.go │
── webhook retry loop main.go + webhook.go │
│ └── BalanceWatchScheduler balance_watch.go │
│ │
│ reference.go — payment reference / topic hash math │
│ webhook.go — delivery, HMAC signing, retry │
│ balance.go — EVM ERC-20 balanceOf(address) reads │
│ balance_watch.go — balance_watches state + webhooks │
└─────────────────────────────────────────────────────────┘
```
@@ -140,7 +145,32 @@ pending ──(tx seen)──► confirming ──(enough blocks)──► confi
---
## 8. Payment reference math (EVM)
## 8. Direct balance watch model
Direct-address payments are a separate rail from scanner intents. They do not require `ERC20FeeProxy`, `paymentReference`, or a smart-contract event. The backend gives the scanner a public address, token, callback URL, and HMAC secret.
Two backend usage modes are supported:
- **Synchronous check mode**: backend calls `POST /balances/check` when an address is allocated to the buyer and again when the buyer clicks "I paid". The backend compares the current base-unit balance to its stored baseline and target amount.
- **Watch mode**: backend calls `POST /balance-watches`; the scanner stores a row in `balance_watches`, reads the initial balance, and checks the same address/token periodically. If the balance changes, scanner sends a signed `balance_changed` webhook. Backend stops the watch with `DELETE /balance-watches/{watchId}` once the payment is accepted, cancelled, or otherwise resolved.
Cadence is age-based:
| Watch age | Next check interval |
|---|---|
| 024 h | 5 min |
| 2448 h | 10 min |
| 4872 h | 20 min |
| 72 h7 d | 40 min |
| 7 d+ | status becomes `expired` |
The scanner only advances `current_balance` after a changed-balance webhook is delivered successfully. If the backend is down or returns non-2xx, the same change is retried on the next scheduled due check.
Current implementation scope: EVM ERC-20 `balanceOf(address)` via JSON-RPC `eth_call`. Tron/TON balance reads are future scope.
---
## 9. Payment reference math (EVM)
```
paymentReference = last8Bytes(keccak256(lower(intentId + salt + destination)))
@@ -151,9 +181,9 @@ The ERC20FeeProxy indexes `paymentReference` so `Topics[1]` in the log is `topic
---
## 9. Database schema (SQLite WAL)
## 10. Database schema (SQLite WAL)
Two tables:
Three main tables:
**`intents`** — one row per payment intent
@@ -188,12 +218,33 @@ Unique index on `(tx_hash, log_index)` prevents duplicate intent confirmation.
| `chain_id` | PK |
| `last_scanned_block` | block number (EVM), ms timestamp (Tron), unix seconds (TON) |
**`balance_watches`** — one row per direct-address balance watch
| Column | Type | Notes |
|---|---|---|
| `watch_id` | TEXT PK | caller-supplied or scanner-generated idempotency key |
| `chain_id` / `chain_type` | INTEGER / TEXT | currently EVM only for direct balance reads |
| `token_address` / `token_symbol` | TEXT | ERC-20 contract and optional registry symbol |
| `decimals` | INTEGER | registry decimals for display/metadata |
| `address` | TEXT | watched holder address |
| `baseline_balance` | TEXT | base-unit balance at backend baseline |
| `current_balance` | TEXT | last delivered/current scanner balance |
| `status` | TEXT | `watching` / `stopped` / `expired` |
| `callback_url` / `callback_secret` | TEXT | signed webhook destination |
| `last_checked_at` / `next_check_at` | DATETIME | scheduler state |
| `change_count` / `last_notified_at` | INTEGER / DATETIME | notification audit fields |
| `expires_at` | DATETIME | hard stop after 7 days |
| `created_at` / `updated_at` | DATETIME | UTC timestamps |
Indexes: `(status, next_check_at)` for due scans and `(chain_id, status)` for status reporting.
---
## 10. Security model
## 11. Security model
- All non-health endpoints require `Authorization: Bearer <SCANNER_API_KEY>` (constant-time compare).
- If `SCANNER_API_KEY` is unset the server logs a warning and allows all requests — intended for local dev only.
- Webhooks are signed with HMAC-SHA256: `X-AMN-Signature: hex(hmac(body, callbackSecret))`.
- The `callbackSecret` is stored in the DB but excluded from all JSON responses (`json:"-"` tag).
- Request bodies are limited to 64 KB.
- Balance-watch callbacks use the same HMAC header plus `X-AMN-Delivery-ID=<watchId>` and `X-AMN-Event-Type=balance_changed`.