audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs: - Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md - 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer). - Scanner docs from scratch (was zero): architecture, data model, API ref, payment flow, operations runbook + repo README. - Doc-sync updates across API reference, data models, flows, design system. - Secret Rotation Runbook (08 - Operations) for the exposed credentials. - Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js. Issues remain status:open intentionally — the code fixes are uncommitted-then-committed working-tree changes per repo and aren't "resolved" until merged/deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
179
04 - Flows/Payment Flow - Scanner.md
Normal file
179
04 - Flows/Payment Flow - Scanner.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: Payment Flow - Scanner (In-House)
|
||||
tags: [flow, scanner, payment]
|
||||
created: 2026-05-30
|
||||
---
|
||||
|
||||
# Payment Flow — AMN Pay Scanner (In-House)
|
||||
|
||||
End-to-end payment flow using the in-house AMN Pay Scanner, replacing the Request Network integration. The scanner is a separate microservice; the backend talks to it over an internal HTTP API.
|
||||
|
||||
See also: [Scanner Architecture](../01%20-%20Architecture/Scanner%20Architecture.md), [Scanner API](../03%20-%20API%20Reference/Scanner%20API.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. High-level sequence
|
||||
|
||||
```
|
||||
Buyer Backend Scanner Chain
|
||||
│ │ │ │
|
||||
│ initiate payment │ │ │
|
||||
│────────────────────►│ │ │
|
||||
│ │ POST /intents │ │
|
||||
│ │───────────────────►│ │
|
||||
│ │ 200 checkoutBlock │ │
|
||||
│ │◄───────────────────│ │
|
||||
│ checkoutBlock │ │ │
|
||||
│◄────────────────────│ │ │
|
||||
│ │ │ │
|
||||
│ sign + submit tx ──────────────────────────────────────►│
|
||||
│ │ │ (polling) │
|
||||
│ │ │◄────────────────│
|
||||
│ │ │ log matched │
|
||||
│ │ │ confirmations… │
|
||||
│ │◄───────────────────│ │
|
||||
│ │ POST callbackUrl │ │
|
||||
│ │ (webhook) │ │
|
||||
│ │ │ │
|
||||
│ payment confirmed │ │ │
|
||||
│◄────────────────────│ │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Step-by-step
|
||||
|
||||
### Step 1 — Backend creates an intent
|
||||
|
||||
When the buyer chooses a payment method (e.g. USDT on BSC), the backend calls:
|
||||
|
||||
```
|
||||
POST http://scanner:8080/intents
|
||||
Authorization: Bearer <SCANNER_API_KEY>
|
||||
|
||||
{
|
||||
"intentId": "<payment._id>",
|
||||
"chainId": 56,
|
||||
"tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
|
||||
"destination": "0xSellerWalletAddress",
|
||||
"amount": "10000000000000000000",
|
||||
"callbackUrl": "https://api.amn.gg/api/payment/scanner-callback",
|
||||
"callbackSecret": "<per-intent HMAC secret stored in payment doc>",
|
||||
"confirmations": 12
|
||||
}
|
||||
```
|
||||
|
||||
The scanner responds with a `checkoutBlock` that the backend passes to the frontend.
|
||||
|
||||
### Step 2 — Frontend shows checkout
|
||||
|
||||
The `checkoutBlock` contains everything the frontend needs to build the `ERC20FeeProxy.transferWithReferenceAndFee` calldata:
|
||||
|
||||
| Field | Used for |
|
||||
|---|---|
|
||||
| `proxyAddress` | contract to call |
|
||||
| `tokenAddress` | ERC20 token |
|
||||
| `destination` | `_to` param |
|
||||
| `paymentReference` | `_paymentReference` param (8-byte reference) |
|
||||
| `amountWei` | `_amount` param |
|
||||
| `feeAmount` | `_feeAmount` param (always `"0"` currently) |
|
||||
| `feeAddress` | `_feeAddress` param (always dead address) |
|
||||
|
||||
For Tron/TON the buyer sends a plain TRC20/Jetton transfer to `destination`; there is no proxy contract.
|
||||
|
||||
### Step 3 — Buyer submits transaction
|
||||
|
||||
The buyer signs and broadcasts the transaction using their wallet. The scanner independently monitors the chain and does not require the transaction hash.
|
||||
|
||||
### Step 4 — Scanner detects and confirms
|
||||
|
||||
**EVM path:**
|
||||
1. `eth_getLogs` returns a `TransferWithReferenceAndFee` log matching `topicRef`
|
||||
2. `validateLogMatchesIntent` verifies token address, destination, and amount
|
||||
3. Intent moves to `confirming`; scanner waits for N blocks
|
||||
4. Once `confirmationsRequired` blocks have been built on top, intent moves to `confirmed`
|
||||
|
||||
**Tron path:**
|
||||
1. TronGrid `Transfer` event matches `destination` (EVM-hex normalized)
|
||||
2. Amount validated ≥ intent amount
|
||||
3. Intent goes directly to `confirmed` (TronGrid returns only confirmed txs)
|
||||
|
||||
**TON path:**
|
||||
1. TonCenter Jetton transfer matches `destination` (exact base64url) and `jetton_master_address`
|
||||
2. Amount validated ≥ intent amount
|
||||
3. Intent goes directly to `confirmed`
|
||||
|
||||
### Step 5 — Webhook delivery
|
||||
|
||||
The scanner POSTs to `callbackUrl` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"intentId": "...",
|
||||
"paymentReference": "0x...",
|
||||
"txHash": "0x...",
|
||||
"blockNumber": 39000010,
|
||||
"amount": "10000000000000000000",
|
||||
"token": "0x55d...",
|
||||
"chainId": 56,
|
||||
"status": "confirmed"
|
||||
}
|
||||
```
|
||||
|
||||
Header `X-AMN-Signature` = `HMAC-SHA256(body, callbackSecret)`.
|
||||
|
||||
The backend verifies the signature, matches the intentId to a Payment record, and marks it paid.
|
||||
|
||||
### Step 6 — Backend acknowledges
|
||||
|
||||
Backend returns a 2xx response. Scanner records `webhook_delivered_at` and the intent lifecycle ends.
|
||||
|
||||
---
|
||||
|
||||
## 3. Failure paths
|
||||
|
||||
### Webhook delivery failure
|
||||
|
||||
If the backend returns non-2xx or is unreachable, the scanner retries:
|
||||
|
||||
```
|
||||
attempt 1: after 5 s
|
||||
attempt 2: after 30 s
|
||||
attempt 3: after 2 min
|
||||
attempt 4: after 10 min
|
||||
attempt 5: after 1 h
|
||||
→ status = webhook_failed
|
||||
```
|
||||
|
||||
`webhook_failed` intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and on `POST /admin/webhooks/retry`.
|
||||
|
||||
On startup the scanner reconciles any `confirmed` intents with `webhook_delivered_at IS NULL` (crash recovery).
|
||||
|
||||
### Intent expiry
|
||||
|
||||
Intents in `pending` or `confirming` status older than `INTENT_TTL_HOURS` (default 24 h) are moved to `expired` by a background ticker running every hour.
|
||||
|
||||
`confirming` intents can get stuck if a transaction is deep-reorganised and never re-included; the TTL frees the destination address for reuse.
|
||||
|
||||
### Amount underpayment
|
||||
|
||||
Transfers where the on-chain amount is less than `intent.Amount` are silently skipped. The intent remains `pending` until the TTL.
|
||||
|
||||
### Wrong token or destination
|
||||
|
||||
The EVM log decoder validates all three fields (token, destination, amount). Mismatches are logged as `REJECT` and skipped. The intent remains `pending`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Key differences from Request Network integration
|
||||
|
||||
| Dimension | Request Network | AMN Pay Scanner |
|
||||
|---|---|---|
|
||||
| Dependency | RN SDK + API | None (direct RPC) |
|
||||
| Payment reference | RN-generated | Internal HMAC derivation |
|
||||
| EVM matching | By reference hash (RN) | By Topics[1] / topicRef (indexed) |
|
||||
| Tron | Not supported | TRC20 Transfer events via TronGrid |
|
||||
| TON | Not supported | Jetton transfers via TonCenter v3 |
|
||||
| Confirmations | RN handled | Per-chain configurable |
|
||||
| Webhook | RN webhook → backend adapter | Scanner → backend directly |
|
||||
| State store | External (RN cloud) | Internal SQLite |
|
||||
Reference in New Issue
Block a user