docs: sync from backend 22ae0bd — scanner balance watches
This commit is contained in:
@@ -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 |
|
||||
|---|---|
|
||||
| 0–24 h | 5 min |
|
||||
| 24–48 h | 10 min |
|
||||
| 48–72 h | 20 min |
|
||||
| 72 h–7 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`.
|
||||
|
||||
Reference in New Issue
Block a user