diff --git a/01 - Architecture/Scanner Architecture.md b/01 - Architecture/Scanner Architecture.md index 2a28ba2..43417a8 100644 --- a/01 - Architecture/Scanner Architecture.md +++ b/01 - Architecture/Scanner Architecture.md @@ -2,31 +2,95 @@ title: Scanner Architecture tags: [architecture, scanner, payment] created: 2026-05-30 +updated: 2026-06-08 --- # Scanner Architecture -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. +AMN Pay Scanner is a standalone Go microservice that watches on-chain payment events and notifies the backend via signed 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. Scanner `0.1.8` also supports direct-address EVM ERC-20 balance reads and balance watches for non-smart-contract payment rails. +> 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. +> +> For operational how-it-works detail (API, webhook payloads, config vars, direct balance checks) see [[scanner]] in 10 - Services. --- ## 1. Responsibilities -- Accept payment **intents** from the backend (POST /intents) +- Accept payment **intents** from the backend (`POST /intents`) - Watch the relevant chain for matching on-chain transfers -- Track confirmation depth (EVM) or rely on finality from the chain API (Tron, TON) +- Track confirmation depth (EVM) or rely on API-reported finality (Tron, TON) - Deliver a signed webhook to the backend callback URL when confirmed -- Retry failed webhook deliveries +- Retry failed webhook deliveries with exponential back-off - 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 +- Read an EVM ERC-20 balance on demand (`POST /balances/check`) +- Watch an EVM address/token pair for balance changes with age-decayed polling cadence (`POST /balance-watches`) --- -## 2. Component map +## 2. Supported chains + +Chains are defined in `supported-chains.json`. A worker is spawned only for chains with `"verified": true` (or listed in `SCANNER_ENABLED_CHAINS`). + +| Chain | Chain ID | Type | Proxy address | Conf. threshold | Active by default | +|---|---|---|---|---|---| +| BNB Smart Chain | 56 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 200 blocks | Yes | +| Ethereum Mainnet | 1 | EVM | `0x370DE27fdb7D1Ff1e1BaA7D11c5820a324Cf623C` | 50 blocks | Yes | +| BSC Testnet | 97 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 5 blocks | Yes (testnet) | +| Arbitrum One | 42161 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 2400 blocks | No | +| Polygon | 137 | EVM | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` | 300 blocks | No | +| Base | 8453 | EVM | `0x1892196E80C4c17ea5100Da765Ab48c1fE2Fb814` | 300 blocks | No | +| Tron Mainnet | 728126428 | Tron | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` (USDT TRC20) | 200 (API-confirmed) | No | +| TON Mainnet | 1100 | TON | `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs` (USDT Jetton master) | 120 (API-finalized) | No | + +> [!note] Proxy address variations +> Ethereum mainnet uses a v0.1.0 proxy (`0x370DE...`). Base uses a non-canonical CREATE2 address (`0x189219...`). All other EVM chains use the canonical v0.2.0 address (`0x0DfbEe...`). The memory note [[RN proxy addresses per chain]] has background on why CREATE2 canonical-address claims should not be trusted without verification. + +To enable a disabled chain without a rebuild: set `SCANNER_ENABLED_CHAINS=56,1,42161` (overrides the JSON `verified` flags). + +--- + +## 3. Architecture decisions + +### Why a standalone Go service + +The scanner runs a tight polling loop that needs to hold open TCP connections to multiple RPC endpoints, manage per-chain checkpoints, and retry webhook delivery independently of backend restarts. A dedicated process with its own SQLite state is simpler and more reliable than embedding this into the Node.js backend. + +### Why SQLite + +Single-node deployment. WAL mode gives concurrent reads during writes. The state set is small (one row per intent, one checkpoint per chain). No operational overhead of a separate DB process inside the container. + +### Two payment rails + +The scanner supports two fundamentally different payment models: + +1. **Proxy-contract rail (EVM)**: funds flow through `ERC20FeeProxy`; the scanner matches by `paymentReference` embedded in the contract event. No unique destination address required; the reference is the discriminator. +2. **Direct-address rail (Tron, TON, and EVM balance-watch)**: each payment gets a unique HD-derived destination address. The scanner matches by `to` address and validates amount. This is the only model available on Tron and TON because no fee-proxy contract exists there. + +### Confirmation thresholds + +EVM confirmation depths are conservative to handle reorgs: + +- **BSC (200)**: BSC has had historical reorg incidents; 200 blocks (~10 min) provides a practical safety margin. +- **ETH (50)**: ~10 min at 12 s/block; Ethereum finality is probabilistic post-merge but 50 blocks is well past economic finality. +- **Arbitrum (2400)**: Arbitrum uses optimistic rollup; 2400 blocks (~54 min) covers the challenge window. +- **Polygon (300)**: polygon reorgs have occurred at depth >100; 300 blocks gives headroom. +- **Base (300)**: Base is an OP Stack chain; same rationale as Polygon. + +Tron and TON do not use block-depth confirmation — TronGrid and TonCenter only surface confirmed/finalized transactions, so status goes directly to `confirmed`. The scanner reports the chain's acceptance floor (200 / 120) in the webhook for backend use. + +### Reorg protection (EVM) + +The EVM worker re-scans `3 × confirmationThreshold` blocks before the checkpoint (clamped 20–500) on every tick. This `ReorgBuffer()` ensures that a log in a block that was reorganised off the canonical chain will be re-evaluated when the chain reorganises. The window is wide enough to cover any realistic reorg depth for the chains the scanner targets. + +### Startup reconciliation + +On startup, `confirmed` intents with `webhook_delivered_at IS NULL` created within the last 7 days have their webhook re-delivered. This recovers from a crash between `finalizeIntent` and `deliverWebhook` without requiring a manual retry trigger. + +--- + +## 4. Component map ``` ┌─────────────────────────────────────────────────────────┐ @@ -46,91 +110,38 @@ AMN Pay Scanner is a standalone Go microservice that watches on-chain payment ev │ ├── 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 │ +│ reference.go — payment reference / topic hash │ +│ webhook.go — delivery, HMAC signing, retry │ +│ balance.go — EVM ERC-20 balanceOf reads │ │ balance_watch.go — balance_watches state + webhooks │ └─────────────────────────────────────────────────────────┘ ``` +One worker goroutine is spawned per active chain. All three chain types implement a common `Worker` interface (`start()`, `stop()`, `getHead()`). Workers poll on `POLL_INTERVAL_SEC` (default 15 s). + --- -## 3. Chain worker model +## 5. Backend integration points -All three chain types implement the `Worker` interface: - -```go -type Worker interface { - start() - stop() - getHead(ctx context.Context) (int64, error) -} -``` - -One worker goroutine is spawned per chain marked `"verified": true` in `supported-chains.json`. Workers are selected by `chainType`: - -| chainType | Worker struct | API used | +| Direction | Endpoint | When | |---|---|---| -| `evm` (default) | `ChainWorker` | JSON-RPC 2.0 (`eth_getLogs`, `eth_blockNumber`) | -| `tron` | `TronChainWorker` | TronGrid REST (`/v1/contracts/{contract}/events`) | -| `ton` | `TonChainWorker` | TonCenter v3 REST (`/jetton/transfers`) | +| Backend → Scanner | `POST /intents` | New payment initiated; returns `checkoutBlock` with `paymentReference` and proxy address | +| Backend → Scanner | `GET /intents/{id}` | Poll intent status (optional; webhook is primary) | +| Scanner → Backend | `POST ` | Payment confirmed; signed with `X-AMN-Signature` HMAC-SHA256 | +| Backend → Scanner | `POST /balances/check` | Synchronous ERC-20 balance read (direct-address rail) | +| Backend → Scanner | `POST /balance-watches` | Start async balance watch (direct-address rail) | +| Scanner → Backend | `POST ` | Balance changed; `X-AMN-Event-Type: balance_changed` | +| Backend → Scanner | `DELETE /balance-watches/{id}` | Stop watch after payment accepted or cancelled | +| Backend → Scanner | `GET /scanner/status` | Chain lag + pending counts (ops/monitoring) | +| Backend → Scanner | `POST /admin/webhooks/retry` | Force re-delivery of `webhook_failed` intents | -Workers poll on `POLL_INTERVAL_SEC` (default 15 s). On first run, each worker starts scanning from the current chain head minus a small buffer (10 blocks for EVM, 24 h for Tron/TON). +All non-health endpoints require `Authorization: Bearer `. Webhooks are HMAC-SHA256 signed; backend must verify `X-AMN-Signature` before crediting any payment. + +The `amn.scanner` backend provider wires intent creation, webhook receipt, and balance-watch lifecycle. See memory note [[amn scanner pay-in wiring + env]] for the 6 required env vars and the dispatcher registration. --- -## 4. EVM scanning detail - -``` -for each tick: - head = eth_blockNumber - from = max(checkpoint − ReorgBuffer(), 0) - chunks = split [from..head] into 2000-block ranges - for each chunk: - logs = eth_getLogs(proxyAddress, EventTopic, from, to) - for each log: - topicRef = Topics[1] (keccak256 of paymentReference — pre-indexed) - intent = DB lookup by topicRef WHERE status='pending' - validate(log.Data, intent) ← token + destination + amount check - confirmIntentPending() ← status → 'confirming' - saveCheckpoint(to) - checkConfirmations(): - for each confirming intent: - confs = head - blockNumber + 1 - if confs >= required: finalizeIntent(capped at required) + deliverWebhook() -``` - -**Reorg protection**: `ReorgBuffer()` re-scans `3 × confirmationThreshold` blocks before the checkpoint (clamped 20–500). This catches any log that appeared in a block that was later reorganised off the canonical chain. - -**Event signature**: `TransferWithReferenceAndFee` keccak256 = `0xbc8b749850bdc0c5e4a49d50f87ec40dca2acdb70a84e7c6823ccdc7b3b3d4b3` - ---- - -## 5. Tron scanning detail - -TronGrid does not expose a fee-proxy contract. Each intent is assigned a unique HD-derived destination address. The scanner watches TRC20 `Transfer` events on the USDT contract and matches by `to` address. - -- Checkpoint: block timestamp in milliseconds (`last_scanned_block` column) -- TronGrid addresses arrive as `41xxxx` hex (21 bytes); normalized to `0x` (20 bytes EVM style) -- Tron transactions reported by TronGrid are already confirmed; status goes directly to `confirmed` (no multi-block wait) -- Pagination follows `meta.links.next` until empty - ---- - -## 6. TON scanning detail - -TON uses TonCenter v3. Per-intent polling: for each pending TON intent, a separate HTTP call fetches incoming Jetton transfers to that destination since the checkpoint. - -- Checkpoint: Unix timestamp in seconds -- TON addresses are base64url (`EQ…`/`UQ…`) — case-sensitive, never lowercased -- `proxyAddress` = USDT Jetton master address (`EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs`) -- TonCenter returns only finalized transactions; status goes directly to `confirmed` -- Lag is reported in seconds, not blocks -- Known scaling limitation: O(pending intents) API calls per scan cycle - ---- - -## 7. Intent lifecycle +## 6. Intent lifecycle ``` pending ──(tx seen)──► confirming ──(enough blocks)──► confirmed ──(webhook ok)──► [done] @@ -139,112 +150,28 @@ pending ──(tx seen)──► confirming ──(enough blocks)──► confi └───────────────────────┴──────────► expired webhook_failed ``` -- **Tron / TON** skip `confirming` and jump directly to `confirmed`. +- **Tron / TON** skip `confirming` and go directly to `confirmed` (API only surfaces finalized txns). - `webhook_failed` intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and on `POST /admin/webhooks/retry`. -- **Startup reconciliation**: on startup, `confirmed` intents with `webhook_delivered_at IS NULL` and created in the last 7 days have their webhook re-delivered. This recovers from a crash between `finalizeIntent` and `deliverWebhook`. +- Retry schedule on first delivery attempt: 5 s → 30 s → 2 min → 10 min → 1 h → `webhook_failed`. --- -## 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))) -topicRef (index) = keccak256(paymentReferenceBytes) -``` - -The ERC20FeeProxy indexes `paymentReference` so `Topics[1]` in the log is `topicRef`, not the raw reference. The DB stores `topic_ref` pre-computed per intent so the scan loop is a single indexed SQL lookup instead of O(n) hashing. - ---- - -## 10. Database schema (SQLite WAL) - -Three main tables: - -**`intents`** — one row per payment intent - -| Column | Type | Notes | -|---|---|---| -| `intent_id` | TEXT PK | caller-supplied UUID | -| `chain_id` | INTEGER | numeric chain ID | -| `chain_type` | TEXT | `evm` / `tron` / `ton` | -| `token_address` | TEXT | EVM/Tron: lowercase 0x hex; TON: base64url | -| `destination` | TEXT | receiving address | -| `amount` | TEXT | base-10 wei / token smallest unit | -| `payment_reference` | TEXT | 8-byte hex (EVM only) | -| `topic_ref` | TEXT | keccak256 of paymentReference (EVM index) | -| `status` | TEXT | `pending` / `confirming` / `confirmed` / `expired` / `webhook_failed` | -| `callback_url` | TEXT | backend webhook endpoint | -| `callback_secret` | TEXT | HMAC key (not returned in GET) | -| `confirmations_required` | INTEGER | from chain config or caller override, floored at the chain acceptance threshold | -| `tx_hash` | TEXT NULL | transaction hash once seen | -| `log_index` | INTEGER NULL | log position within tx (EVM) | -| `block_number` | INTEGER NULL | block / timestamp when seen | -| `confirmations` | INTEGER | current depth while confirming; capped at the accepted threshold after confirmation | -| `salt` | TEXT | 32-byte random hex for reference derivation | -| `webhook_delivered_at` | TEXT NULL | RFC3339 timestamp of successful delivery | -| `created_at` / `updated_at` | DATETIME | | - -Unique index on `(tx_hash, log_index)` prevents duplicate intent confirmation. - -**`checkpoints`** — one row per chain, tracks scan progress - -| Column | Notes | -|---|---| -| `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. - ---- - -## 11. Security model +## 7. Security model - All non-health endpoints require `Authorization: Bearer ` (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=` and `X-AMN-Event-Type=balance_changed`. +- Unset `SCANNER_API_KEY` logs a warning and allows all requests — local dev only. +- Webhooks signed with HMAC-SHA256: `X-AMN-Signature: hex(hmac(body, callbackSecret))`. +- `callbackSecret` stored in DB but excluded from all JSON responses (`json:"-"`). +- Request bodies limited to 64 KB. +- `SCANNER_CALLBACK_ALLOWED_HOSTS` env var restricts allowed webhook target hosts (SSRF guard). + +--- + +## 8. Known limitations and open items + +| Item | Detail | +|---|---| +| TON O(n) API calls | Per-intent polling — one TonCenter call per pending TON intent per scan cycle. Fine at low volume; needs batching for scale. | +| Direct balance reads: EVM only | `POST /balances/check` and `POST /balance-watches` only support EVM ERC-20 (`eth_call` balanceOf). Tron/TON balance reads are future scope. | +| Arbitrum / Polygon / Base / Tron / TON disabled | `verified: false` in `supported-chains.json`. Enable via `SCANNER_ENABLED_CHAINS` env var without a code change. | +| Ethereum proxy version | Chain 1 uses the v0.1.0 proxy (`0x370DE...`). A v0.2.0 proxy is also deployed but checkout still uses the v0.1.0 ABI. | diff --git a/04 - Flows/Telegram Mini App.md b/04 - Flows/Telegram Mini App.md index 07c9b99..1704c4c 100644 --- a/04 - Flows/Telegram Mini App.md +++ b/04 - Flows/Telegram Mini App.md @@ -1,20 +1,22 @@ --- title: Telegram Mini App Flow -tags: [flow, telegram, mini-app, auth, bilingual, RTL, shop, cart] +tags: [flow, telegram, mini-app, auth, bilingual, RTL, shop, cart, payment] related_models: ["[[User]]"] related_apis: ["POST /api/auth/telegram", "[[Auth API]]"] task: "5.4" --- -> **Last updated:** 2026-06-03 +> **Last updated:** 2026-06-08 > **Status:** IN PROGRESS — Task 5.4 (dependencies: 5.1 auth infra, 5.2 Telegram sign-in endpoint) -> **Frontend branch:** `integrate-main-into-development` · v2.8.94 +> **Frontend branch:** `integrate-main-into-development` · v2.8.94+ > **Entry point:** `src/sections/telegram/` · route `/telegram` # Telegram Mini App Flow End-to-end specification for the **Amaneh Telegram Mini App** — a fully self-contained marketplace shell surfaced inside Telegram's in-app browser via the WebApp SDK. Buyers and sellers can browse requests, create new escrow requests, shop seller templates, manage a cart, review offer state, follow payments, and message each other without leaving Telegram. +> **Two separate Mini Apps exist on this platform.** This document covers the **main marketplace Mini App** (`amn.gg/telegram`) built inside the primary Next.js frontend. For the AI-assisted request-creation Mini App, see [[amanat-assist]]. + --- ## 1. Architecture Overview @@ -26,10 +28,11 @@ Telegram Client ├─ useTelegramLiveContext ← SDK probe + polling ├─ useTelegramLanguage ← EN / FA detection ├─ useTelegramAutoSignIn ← silent JWT exchange - ├─ useTelegramMainButton ← native chrome sync + ├─ useTelegramMainButton ← native chrome sync (disabled) ├─ useTelegramBackButton ← native chrome sync ├─ useTelegramHaptic ← haptic wrapper ├─ useTelegramCart ← shared localStorage cart + ├─ useTelegramNotifications ← unread badge count │ ├─ [state: loading] → TelegramLoadingState ├─ [state: unsupported] → TelegramUnsupportedState @@ -38,17 +41,28 @@ Telegram Client ├─ TelegramHeader ├─ TelegramTabBar (Home / Shop / Requests / Chat / Account) │ + ├─ [drilldown] TelegramPaymentView ← highest priority + ├─ [drilldown] TelegramChatThreadView + ├─ [drilldown] TelegramRequestDetailView + ├─ [drilldown] TelegramTemplateDetailView + ├─ [drilldown] TelegramSellerShopView + │ + ├─ [overlay] TelegramPointsView + ├─ [overlay] TelegramSettingsView + ├─ [overlay] TelegramAddressesView + ├─ [overlay] TelegramCartView + ├─ [overlay] TelegramCheckoutView + ├─ [overlay] TelegramNotificationsView + ├─ [overlay] TelegramNewRequestView + │ ├─ TelegramHomeView ├─ TelegramShopView → TelegramSellerShopView ├─ TelegramRequestsView → TelegramRequestDetailView ├─ TelegramChatView → TelegramChatThreadView - ├─ TelegramAccountView - └─ [overlay] TelegramNewRequestView - └─ [overlay] TelegramNotificationsView - └─ [overlay] TelegramCartView + └─ TelegramAccountView ``` -The shell is a **single-page, no-router** design: all navigation (tabs, overlays, detail drilldowns) is pure React state in `TelegramMiniAppView`. `window.location.assign` is only used as a final escape hatch to the full web dashboard. +The shell is a **single-page, no-router** design: all navigation (tabs, overlays, detail drilldowns) is pure React state in `TelegramMiniAppView`. `window.location.assign` is used only as a final escape hatch to external URLs. `openTelegramExternalLink` is used for deep links into the web dashboard, which opens inside Telegram's WebView or an external browser depending on the Telegram client. --- @@ -66,7 +80,27 @@ The shell is a **single-page, no-router** design: all navigation (tabs, overlays --- -## 3. SDK Initialisation & Context Probe +## 3. amanat-assist vs Main Mini App + +Two distinct Telegram Mini Apps exist for this platform: + +| Property | Main Mini App (this doc) | amanat-assist | +|---|---|---| +| URL | `amn.gg/telegram` | `assist.amn.gg` | +| Bot | AmanehBot | AmanehBot (same) | +| Codebase | `frontend/` (Next.js, `src/sections/telegram/`) | `/amanat-assist` (React + Vite, separate repo) | +| Purpose | Full marketplace shell: browse, buy, sell, chat, manage account | Conversational LLM wizard to create one purchase request | +| LLM | None | Mistral → DeepSeek fallback (via `amanat-llm-proxy` on port 3001) | +| Backend access | Direct calls to `api.amn.gg` | Proxied through `amanat-llm-proxy` which holds the LLM API keys | +| Auth | Telegram `initData` → `POST /api/auth/telegram` | Same endpoint; also supports web redirect via `?access_token=` | +| Deep links between apps | Main Mini App has "New Request" overlay with an "Open Assist" CTA that navigates `window.location.href` to `assist.amn.gg?access_token=...` | Assist submits the finished request then the user returns to the main app | +| Status | In production | Live at `assist.amn.gg` v1.1.0 | + +**Hand-off from main app to assist:** `handleOpenAssist()` in `TelegramMiniAppView` constructs a URL to `https://assist.amn.gg` with `access_token`, `user_json`, `theme`, and `source=miniapp` query params. `window.location.href` is used (not `openLink`) to keep the navigation inside Telegram's WebView rather than opening Safari on iOS. + +--- + +## 4. SDK Initialisation & Context Probe **File:** `src/utils/telegram-webapp.ts` · `getTelegramContext()` @@ -92,7 +126,7 @@ The function assembles a `TelegramContext` object from: --- -## 4. Shell State Machine +## 5. Shell State Machine `getTelegramStatus(context, hasWebAccount)` returns one of three states: @@ -114,9 +148,9 @@ State transitions occur on: --- -## 5. Authentication Flow +## 6. Authentication Flow -### 5.1 Silent Auto Sign-In +### 6.1 Silent Auto Sign-In **Hook:** `useTelegramAutoSignIn` · **File:** `hooks/use-telegram-auto-sign-in.ts` @@ -127,7 +161,7 @@ On mount, if `context.isMiniApp && context.initData && !user`: 3. If the backend returns `isNewUser: true`, show `TelegramOnboardingSheet`. 4. A `useRef` deduplication guard (`attemptedInitDataRef`) prevents re-runs under React Strict Mode's double-effect behaviour. -### 5.2 Manual Sign-In (Unlinked State) +### 6.2 Manual Sign-In (Unlinked State) When `initData` is present but auto sign-in failed (or hasn't run yet), `TelegramUnlinkedState` renders: - **Continue with Telegram** — calls the same `signIn()` function from `useTelegramAutoSignIn`. @@ -136,47 +170,74 @@ When `initData` is present but auto sign-in failed (or hasn't run yet), `Telegra When `initData` is absent (accessed via a path that skips Telegram context), only the email/register buttons appear. -### 5.3 Backend Endpoint +### 6.3 Backend Endpoint `POST /api/auth/telegram` — expects `{ initData: string }`. Backend verifies the HMAC using the Telegram bot token, extracts `user` from the payload, upserts a `User` record (`telegramId`, `telegramVerified: true`), and issues a JWT + refresh token. Returns `{ token, refreshToken, isNewUser }`. +Registered at `authRoutes.ts` line 24: `router.post("/telegram", ctrl.telegramAuth.bind(ctrl))` — public route, no auth middleware. + +### 6.4 Session Linking (Telegram ↔ Amaneh Account) + +The `POST /api/auth/telegram` endpoint both creates and links accounts: + +- **New Telegram user, no existing Amanat account:** a new `User` is created with `telegramId` set; `isNewUser: true` is returned and the onboarding sheet is shown. +- **Existing Amanat account with the same `telegramId`:** the existing user is returned; session continues. +- **Existing Amanat account that has never used Telegram:** `telegramId` and `telegramVerified: true` are written onto the existing record (matched by Telegram user id). + +After the JWT is issued the standard `checkUserSession()` re-hydrates the React auth context. The Mini App shell reads `user.telegramVerified` and `user.isEmailVerified` from this context to render verification chips in the Account tab. + --- -## 6. Navigation Model +## 7. Navigation Model All navigation is in-shell React state — no Next.js router is involved. ``` -activeTab : 'home' | 'shop' | 'requests' | 'chat' | 'account' -overlayScreen : 'new-request' | 'notifications' | 'cart' | null -openConversationId : string | null -openRequestId : string | null -openSellerId : string | null +activeTab : 'home' | 'shop' | 'requests' | 'chat' | 'account' +overlayScreen : 'new-request' | 'notifications' | 'cart' | 'checkout' + | 'points' | 'settings' | 'addresses' | null +openConversationId : string | null +openRequestId : string | null +openPaymentRequestId : string | null ← payment drilldown (highest priority) +paymentCheckoutFlow : boolean ← true when reached from shop checkout +openSellerId : string | null +openTemplate : { template, seller } | null ``` **Priority rendering** (first match wins): -1. `openConversationId` → `TelegramChatThreadView` -2. `openRequestId` → `TelegramRequestDetailView` -3. `openSellerId` → `TelegramSellerShopView` -4. `overlayScreen === 'cart'` → `TelegramCartView` -5. `overlayScreen === 'notifications'` → `TelegramNotificationsView` -6. `overlayScreen === 'new-request'` → `TelegramNewRequestView` -7. `activeTab` → appropriate tab view +1. `openPaymentRequestId` → `TelegramPaymentView` ← new, highest priority +2. `openConversationId` → `TelegramChatThreadView` +3. `openRequestId` → `TelegramRequestDetailView` +4. `openTemplate` → `TelegramTemplateDetailView` ← new +5. `openSellerId` → `TelegramSellerShopView` +6. `overlayScreen === 'points'` → `TelegramPointsView` ← new +7. `overlayScreen === 'settings'` → `TelegramSettingsView` ← new +8. `overlayScreen === 'addresses'` → `TelegramAddressesView` ← new +9. `overlayScreen === 'cart'` → `TelegramCartView` +10. `overlayScreen === 'checkout'` → `TelegramCheckoutView` ← new (replaces web handoff) +11. `overlayScreen === 'notifications'` → `TelegramNotificationsView` +12. `overlayScreen === 'new-request'` → `TelegramNewRequestView` +13. `activeTab` → appropriate tab view -**Back button** (Telegram native `BackButton`) dismisses in reverse priority order: chat thread → request detail → seller shop → overlay → returns to `home` tab. +**Back button** (Telegram native `BackButton`) dismisses in reverse priority order: +- Payment drilldown → if `paymentCheckoutFlow`, steps back to cart; otherwise clears payment state. +- Chat thread → clears `openConversationId`. +- Request detail → clears `openRequestId`. +- Template detail → clears `openTemplate`. +- Seller shop → clears `openSellerId`. +- Overlay (`checkout` steps back to `cart`) → clears `overlayScreen`. +- Non-home tab → returns to `home`. `BackButton` visibility: shown whenever `state === 'linked'` and either an overlay/drilldown is active, or `activeTab !== 'home'`. -`MainButton` visibility: hidden while any overlay is open. When visible: -- **Linked** → "New Request" (opens `overlayScreen = 'new-request'`) -- **Unlinked** → "Sign In" (navigates to the JWT sign-in page) +`MainButton` visibility: **intentionally disabled** (`isReady: false`) — the native Telegram MainButton cannot use the project font and duplicates in-shell CTAs, so it is kept hidden. All primary actions live inside the shell UI itself. -Both chrome buttons are styled with the amaneh saffron palette (`color: #C2410C`, `text_color: #FFFFFF`) via `setParams` (WebApp SDK >= 6.1). +Both chrome buttons retain the amaneh saffron palette (`color: #C2410C`, `text_color: #FFFFFF`) via `setParams` (WebApp SDK >= 6.1) as a fallback should the MainButton ever be re-enabled. --- -## 7. Tab Structure +## 8. Tab Structure The shell has **five bottom tabs** rendered by `TelegramTabBar`: @@ -192,89 +253,115 @@ The shell has **five bottom tabs** rendered by `TelegramTabBar`: --- -## 8. Supported Flows +## 9. Supported Flows -### 8.1 Home Tab +### 9.1 Home Tab `TelegramHomeView` is the landing screen shown on first open. It contains: - **Welcome banner** (`TelegramWelcomeBanner`): escrow account summary, primary CTA. - **Quick-action cards** (`TelegramQuickActions`): shortcuts to Requests, Payments, Chat. - **Escrow state chips** (`TelegramEscrowStateChips`): legend of status values visible in the platform. +- **"New Request" CTA** → opens `overlayScreen = 'new-request'`. +- **"Open Assist" CTA** → calls `handleOpenAssist()` to navigate to `assist.amn.gg` in the same WebView (see section 3). -### 8.2 Shop Tab — Sellers List +### 9.2 Shop Tab — Sellers List **`TelegramShopView`** (`telegram-shop-view.tsx`): - Fetches all sellers via `useTelegramShops()` → SWR wrapping `getTemplateSellers()` → `GET /api/request-templates/sellers`. - Renders `TelegramShopRow` per seller: avatar, name, rating, template count, sales count. -- Shows a floating cart badge button in the header when `totalItems > 0`; tap opens `overlayScreen = 'cart'`. +- Shows a floating cart badge button (`TelegramCartFab`) in the header when `totalItems > 0`; tap opens `overlayScreen = 'cart'`. - Tap a seller row → sets `openSellerId` → navigates to `TelegramSellerShopView`. -### 8.3 Shop Tab — Seller Store +### 9.3 Shop Tab — Seller Store **`TelegramSellerShopView`** (`telegram-seller-shop-view.tsx`): - Fetches seller + active templates via `useTelegramSellerShop(sellerId)` → `GET /api/request-templates/sellers/:id`. - Dark header: seller avatar, name, rating, template count, description. - Each template card shows: image, title, 2-line description, budget range, usage count. - **Two actions per template:** - - **Add to cart / Remove from cart** — toggles item in `useTelegramCart` (localStorage, no API). Button is filled blue when not in cart, outline when added. - - **Order this template** — `` to `/dashboard/request/from-template?shareableLink=...`. Exits the Mini App to the web dashboard (single-template direct order, bypasses cart). + - **Add to cart / Remove from cart** — toggles item in `useTelegramCart` (localStorage, no API). + - **View template details** — sets `openTemplate` → navigates to `TelegramTemplateDetailView`. - Floating "Cart · N templates" sticky button at bottom when `totalItems > 0`; tap calls `onOpenCart()`. -### 8.4 Shopping Cart Overlay +### 9.4 Shop Tab — Template Detail + +**`TelegramTemplateDetailView`** (`telegram-template-detail-view.tsx`): +- Full-screen view of a single template. +- Shows full description, seller info, price, delivery info, usage/capacity counters. +- Add/remove cart action; direct "Order this template" link to `/dashboard/request/from-template?shareableLink=...` (exits to web dashboard). +- Back button returns to the seller store (`openTemplate` cleared, `openSellerId` retained). + +### 9.5 Shopping Cart Overlay **`TelegramCartView`** (`telegram-cart-view.tsx`): - Rendered as `overlayScreen = 'cart'`; dismissed by Telegram BackButton. - Lists each cart item: image, name, seller name, USDT price × quantity, +/− quantity controls, remove button. - Subtotal/total in USDT, locale-formatted (`fa-IR` for Persian, `en-US` for English); amounts always `dir="ltr"`. -- **"Continue to payment"** — plain `` link; exits Mini App to web checkout. +- **"Continue to payment"** → calls `onCheckout()` which sets `overlayScreen = 'checkout'` (in-shell checkout, replacing the previous web handoff). **Cart storage (`useTelegramCart`):** - -- Reads/writes `localStorage` key **`app-request-template-checkout`** — the same key the web `RequestTemplateCheckoutProvider` reads. This is the cart handoff mechanism: the cart built in Telegram IS the cart the web checkout page hydrates. +- Reads/writes `localStorage` key **`app-request-template-checkout`** — the same key the web `RequestTemplateCheckoutProvider` reads. This enables the web dashboard checkout to hydrate the same cart. - Dispatches a custom `tg-cart-changed` DOM event on every write; listens on both that event and the native `storage` event so all open tabs stay in sync. - Operations: `addTemplate(template, seller)`, `removeItem(itemId)`, `changeQuantity(itemId, qty)`, `isInCart(templateId)`. - No API calls — cart is purely client-side until checkout. -- Cart item model: `id`, `templateId`, `name`, `description`, `price` (from `template.budget.min`), `quantity`, `image`, `sellerId`, `sellerName`, `category`, `shareableLink`, `deliveryInfo`, `maxUsage`, `usageCount`, `remainingCapacity`. -### 8.5 Web Checkout Handoff +### 9.6 In-Shell Checkout Overlay -Destination: `/dashboard/shops/checkout` — `RequestTemplateCheckoutView` wrapped by `RequestTemplateCheckoutProvider`. +**`TelegramCheckoutView`** (`telegram-checkout-view.tsx`): +- Rendered as `overlayScreen = 'checkout'`; BackButton steps back to `overlayScreen = 'cart'`. +- A 3-step stepper running entirely inside the Mini App shell: + - **Step 0 (Cart review):** item list, quantities, totals, discount. + - **Step 1 (Address):** physical address or online delivery email. + - **Step 2 (Payment):** wallet-based payment execution. +- On successful order (`onPlaced(reqId)` callback): + - If a `reqId` is returned, sets `paymentCheckoutFlow = true` and `openPaymentRequestId = reqId` → immediately opens the payment view. + - If no `reqId`, switches `activeTab` to `'requests'`. +- Stock validation clamps or removes items exceeding `remainingCapacity` before payment. +- Integrates with `onManageAddresses()` to open the `addresses` overlay mid-flow. -The provider reads the shared `localStorage` key and hydrates the TMA cart. The checkout is a 3-step stepper: +### 9.7 Payment View (In-Shell) -| Step | Component | Description | -|---|---|---| -| 0 (Cart review) | `RequestTemplateCheckoutCart` | Item list, quantities, remove, totals, discount/shipping | -| 1 (Address) | `RequestTemplateCheckoutBillingAddress` | Physical address or online delivery email | -| 2 (Payment) | `RequestTemplateCheckoutPayment` | Wallet payment + socket confirmation | -| Complete | `RequestTemplateCheckoutOrderComplete` | Confirmation dialog, cart reset | +**`TelegramPaymentView`** (`telegram-payment-view.tsx`): +- Highest-priority drilldown (rendered before all other overlays). +- Loaded for a specific `requestId`. Used from two entry points: + - **Shop checkout flow** (`paymentCheckoutFlow = true`): after `TelegramCheckoutView` creates the requests. Shows a 3-step progress header (cart → address → payment). + - **Requests tab** (`paymentCheckoutFlow = false`): buyer taps "Pay" on an existing request. No progress header. +- Fetches request details via `useTelegramRequest`. +- Fetches offers via `useTelegramOffers`. +- Calls `getPaymentOptions()` → `GET /api/payment/options` and `createDirectBalanceIntent()` → `POST /api/payment/direct-balance`. +- Polls `checkDirectBalancePayment()` for confirmation. +- On successful payment: calls `onPaid()` → clears `openPaymentRequestId`, switches to `activeTab = 'requests'`. +- Back button: if `paymentCheckoutFlow`, steps back to `overlayScreen = 'cart'`; otherwise clears the payment state. -Payment execution calls `convertTemplatesToRequests()` to create escrow records, then awaits a `template-checkout-payment-confirmed` socket event. A guard checks `createdRequestIds` is non-empty before advancing (prevents stray global socket events from triggering premature completion). Stock validation clamps or removes items exceeding `remainingCapacity` before payment. - -### 8.6 Browse Requests (Requests Tab) +### 9.8 Browse Requests (Requests Tab) - `TelegramRequestsView` fetches the user's purchase requests via `useTelegramMyRequests` (GET `/api/requests`). - Displays a skeleton loader, then a scrollable list of `TelegramRequestRow` items. - Each row shows: title, status chip, budget, creation date. - Tap → sets `openRequestId` → renders `TelegramRequestDetailView`. -### 8.7 Request Detail with Stepper +### 9.9 Request Detail with Stepper and Offers - `TelegramRequestDetailView` fetches a single request via `useTelegramRequest`. - Renders `TelegramRequestStepper` — a visual timeline of the escrow status flow from `pending_payment` → `completed`. - `determineCurrentStepFromStatus` maps the current `status` to a step index. - Also renders: budget, description, creation date, category, urgency. +- **Offer review:** fetches offers via `useTelegramOffers`; renders offer cards with seller info, price, and accept/reject actions. +- **Pay action:** renders a "Pay" button when request is in a payable state → calls `onPay(id)` → sets `openPaymentRequestId`. +- **Web fallback:** "View full details" → `openTelegramExternalLink(context.webApp, path)`. +- **Chat seller:** taps the seller chat icon → calls `onChatSeller(sellerId)` → `createConversation` + sets `openConversationId`. +- Role-aware: `role` prop is `'seller'` or `'buyer'` based on `user.role`. - Dates formatted via `toLocaleDateString` with `fa-IR` locale for Persian. -### 8.8 Create New Request +### 9.10 Create New Request - `TelegramNewRequestView` is a full-screen overlay (not a routed page). - Form fields: title, description, category (fetched from `/api/categories`), budget min/max, urgency. +- Includes an **"Open Assist"** button that delegates to `handleOpenAssist()` for users who prefer the conversational LLM flow. - On submit: calls `createPurchaseRequest()` → POST `/api/purchase-requests`. - On success: closes overlay, switches `activeTab` to `'requests'`. -- `MainButton` is hidden while the overlay is open (submit lives in the form itself). -### 8.9 Chat Tab +### 9.11 Chat Tab - `TelegramChatView` shows the user's active conversations via `useTelegramConversations`. - Includes a Support row that calls `createSupportChat()` → `POST /api/chat/support`, then opens `TelegramChatThreadView` with the returned conversation ID. @@ -283,22 +370,21 @@ Payment execution calls `convertTemplatesToRequests()` to create escrow records, - Optimistic send: message appears immediately, confirmed/rolled back on API response. - Real-time updates via Socket.IO events; SWR is mutated on `new-notification` and `unread-count-update` events. -### 8.10 Account Tab +### 9.12 Account Tab **`TelegramAccountView`** (`telegram-account-view.tsx`): -The account tab has four sections. All user data is passed as props from the shell (loaded via `useAuthContext()` — no fetch on mount). - **Profile header:** - Avatar (from `user.profile.avatar`, falls back to initials), full name, Telegram `@username`, role chip (buyer / seller / admin / resolver / guard). - Verification chips: "Telegram Verified" (if `user.telegramVerified`) and "Email Verified" (if `user.isEmailVerified`). **Preferences section:** - Language toggle (FA / EN, in-shell via `TelegramLanguageToggle`). -- General Settings → `/dashboard/account` (web, labeled "Opens in the web dashboard"). -- Wallet → truncated address (`0x1234…abcd`) or "not connected" → `/dashboard/account/wallet` (web). +- **Settings** → opens `overlayScreen = 'settings'` (in-shell `TelegramSettingsView`). +- **Points** → opens `overlayScreen = 'points'` (in-shell `TelegramPointsView`). +- Wallet → truncated address (`0x1234…abcd`) or "not connected" → `/dashboard/account/wallet` (web via `openTelegramExternalLink`). - Notifications → opens `TelegramNotificationsView` overlay in-shell. -- Addresses → `/dashboard/account/address` (web). +- **Addresses** → opens `overlayScreen = 'addresses'` (in-shell `TelegramAddressesView`). - Passkey → `/dashboard/account/passkey` (web). **Help section:** @@ -308,16 +394,38 @@ The account tab has four sections. All user data is passed as props from the she **Session section:** - Sign Out → `TelegramBottomSheet` confirmation dialog → `authSignOut()` + `window.location.assign(paths.auth.jwt.signIn)`. -### 8.11 Notifications Overlay +### 9.13 Settings Overlay + +**`TelegramSettingsView`** (`telegram-settings-view.tsx`): +- Rendered as `overlayScreen = 'settings'`. +- Allows editing profile fields (name, bio) in-shell. +- On save: calls `onSaved()` which triggers `checkUserSession()` to refresh the auth context. + +### 9.14 Addresses Overlay + +**`TelegramAddressesView`** (`telegram-addresses-view.tsx`): +- Rendered as `overlayScreen = 'addresses'`. +- Fetches addresses via `use-telegram-addresses.ts`. +- Used both from the Account tab and as a mid-flow step from `TelegramCheckoutView`. + +### 9.15 Points Overlay + +**`TelegramPointsView`** (`telegram-points-view.tsx`): +- Rendered as `overlayScreen = 'points'`. +- Fetches user points via `use-telegram-points.ts`. +- Shows points balance and transaction history. + +### 9.16 Notifications Overlay - `TelegramNotificationsView` is rendered as `overlayScreen = 'notifications'`. - Fetches via `useTelegramNotifications` → `getNotifications(userId, 1, 50)` → `GET /api/notifications?userId=...&page=1&limit=50`. - Real-time updates: Socket.IO events `new-notification`, `unread-count-update` trigger SWR mutate. - "Mark all read" calls `markAllNotificationsAsRead(userId)` → `PATCH /api/notifications/mark-all-read`. +- Unread count is also surfaced in the `TelegramHeader` bell icon badge. --- -## 9. API Calls +## 10. API Calls | Action | Hook / call | Backend endpoint | |---|---|---| @@ -328,18 +436,26 @@ The account tab has four sections. All user data is passed as props from the she | My requests | `useTelegramMyRequests` | `GET /api/requests` | | Single request | `useTelegramRequest` | `GET /api/purchase-requests/:id` | | Create request | shell → `createPurchaseRequest()` | `POST /api/purchase-requests` | +| Offers for request | `useTelegramOffers` → `getOffers(requestId)` | `GET /api/marketplace/offers?requestId=...` | +| Payment options | `getPaymentOptions()` | `GET /api/payment/options` | +| Create payment intent | `createDirectBalanceIntent()` | `POST /api/payment/direct-balance` | +| Poll payment status | `checkDirectBalancePayment()` | `GET /api/payment/:id` | +| Update request status | `updateRequestStatus()` | `PATCH /api/marketplace/requests/:id/status` | | Conversations | `useTelegramConversations` | `GET /api/chat/conversations` | | Chat thread | `useTelegramChatThread` | `GET /api/chat/:id` + Socket.IO real-time | | Support chat | `createSupportChat()` | `POST /api/chat/support` | +| Direct conversation | `createConversation({ type: 'direct', participantIds })` | `POST /api/chat/conversations` | | Notifications | `useTelegramNotifications` | `GET /api/notifications?userId=...&page=1&limit=50` | | Mark all read | `markAllNotificationsAsRead(userId)` | `PATCH /api/notifications/mark-all-read` | | Auth sign-out | `authSignOut()` | JWT sign-out endpoint | +| Addresses | `use-telegram-addresses.ts` | `GET /api/user/addresses` | +| Points | `use-telegram-points.ts` | `GET /api/user/points` | -Cart operations (add/remove/quantity) are **pure localStorage** — no API calls until web checkout. +Cart operations (add/remove/quantity) are **pure localStorage** — no API calls until checkout. --- -## 10. Bilingual Support (EN / FA) +## 11. Bilingual Support (EN / FA) **Language detection priority** (`useTelegramLanguage`): @@ -378,7 +494,7 @@ All JSX uses `t.
.` — no inline strings in components. --- -## 11. Design System +## 12. Design System **File:** `src/sections/telegram/constants.ts` · `src/sections/telegram/telegram-shell-css.ts` @@ -391,7 +507,7 @@ The Mini App has a distinct visual identity (cream/saffron Persian palette) that | `cream50` | `#FBF6EB` | Page background | | `ink900` | `#1C1410` | Primary text | | `ink600` | `#6B5D4E` | Secondary text / labels | -| `saffron600` | `#C2410C` | Primary action, MainButton | +| `saffron600` | `#C2410C` | Primary action | | `saffron500` | `#D97757` | Hover states | | `pistachio700` | `#3D6B4F` | Success / released states | | `pomegranate700` | `#8E2424` | Error / disputed states | @@ -399,15 +515,15 @@ The Mini App has a distinct visual identity (cream/saffron Persian palette) that **Fonts:** `TG_FONTS` — Source Serif 4 (headings), IBM Plex Sans (body LTR), Vazirmatn (body RTL), IBM Plex Mono (amounts/addresses). -**CSS:** `buildTelegramShellCss()` injects a `