- Add Handoff - RN Multichain Probe - 2026-05-28.md - Update Handoff - Request Network In-House Checkout with Task #8 status - Update Activity Log with backend@ae17b18, frontend@0ebb2f1 - Update PRD §2 acceptance criteria for Task #8 - Update Payment API.md with /api/admin/rn/networks endpoints
18 KiB
Handoff: Request Network In-House Checkout — 2026-05-28
Status: fully end-to-end working on dev.amn.gg as of 2.6.38 backend / 2.6.41 frontend. A 0.01 USDC payment (tx 0x494c77a2…) flowed: page render → wallet connect (Rabby/injected) → approve → transferFromWithReferenceAndFee → RN webhook → backend marks completed → page flips to "پرداخت تأیید شد ✓" → continue → /dashboard/payment/<id>.
What's live
- Backend 2.6.38 —
/api/payment/request-network/intentsreturns aninHouseCheckoutblock (destination, tokenAddress, decimals, chainId, proxyAddress, paymentReference 8-byte hex, feeAmount, feeAddress, amountWei).GET /api/payment/request-network/:paymentId/checkoutrehydrates the block for an existing Payment record (lazy-enriches legacy records that pre-date 2.6.34 by calling RN'sGET /v2/request/:id). PublicGET /api/versionfor the version badge.PaymentCoordinator.updatePurchaseRequestStatusguards bothtemplate-checkout-andtemplate-tc-prefixes (plus regex fallback for any non-ObjectId) — earlier thetemplate-tc-blindspot crashed webhook processing on template-checkout payments and stranded escrow. - Frontend 2.6.41 —
/checkout/request-network/[paymentId]page with wagmi state machine: connect → switch-chain → check-allowance → approve → pay → wait-for-webhook. Destination + payment-reference + approve-tx + pay-tx hashes are copyable and click through to BscScan. Once a pay tx is in flight the page no longer reverts to "approve" even though the proxy call consumed the allowance. A 10-secondGET /api/payment/:idpoll runs as a fallback when the socket missespayment-update. Success-state continue button handles synthetic purchaseRequestId prefixes (template-checkout-,template-tc-) by routing to/dashboard/payment/<id>instead of the 404-prone/dashboard/request/<syntheticId>. WagmiProvider is now rendered unconditionally + the checkout page also self-wraps in its own WagmiProvider for defensive isolation.
Verify which versions are running by hovering the version chip at bottom-left of any page on dev.amn.gg, or curl https://dev.amn.gg/api/version.
Where things stand
A real 0.01 USDC payment ran clean through the in-house path on 2026-05-28. Webhook delivery is durable enough for dev usage; durability for prod is Phase 5 (Cloudflare Worker ingress, not started). Five follow-up tasks were scoped immediately after — see PRD - Wallet, Multichain, Confirmations, AML, Trezor.md and Taskmaster #7..#11.
Known issues / open work
- TypeScript-error CI false-success: pipelines #40 and #41 reported ✅ green in Woodpecker while
yarn buildwas actually failing at the TS step and no image was pushed. Memory entry:woodpecker_silent_build_fail.md. Always verifydev-<version>exists ingit.manko.yogabefore trusting CI green. The wagmichainIdfield requiresas anybecause of its literal-union type — keep that pattern when adding new wagmi calls. - Existing/legacy Payment records (created before backend 2.6.34) don't have RN's request details cached. The GET endpoint lazy-enriches them via
GET /v2/request/:requestIdon first visit, then persists. If RN's API is down at that moment, falls back to the hosted-page link. - Mongo access is denied to the auto-mode classifier on dev — debugging payment records currently requires either the user's mongo creds or relying on the 409
debugblock surfaced through the frontend. - Wagmi provider isolation (2.6.39): The checkout page wraps itself in its own
WagmiProvider. The rootWeb3Provideralso rendersWagmiProviderunconditionally as of 2.6.38. The doubling is intentional defensiveness — if one provider has an issue, the other still serves the checkout flow. Can be simplified later if both prove stable. - PRD Phase 5 — Cloudflare Worker durable webhook ingress — not started. Taskmaster
3.13. Current dev relies ondev.amn.ggbeing up at the moment RN's webhook fires. For prod, RN webhooks need to land in a durable Cloudflare Worker that buffers + replays into the backend.
Files changed (recent)
Backend (/Users/manwe/CascadeProjects/escrow/backend):
src/services/payment/requestNetwork/contract.ts— spreads full RN response intorawsrc/services/payment/requestNetwork/inHouseCheckout.ts— block builder, readspaymentReferencefromrnRaw.requestDetails.paymentReferencesrc/services/payment/requestNetwork/merchantReference.ts,tokens.ts,proxyAddresses.ts,paymentReference.ts— helperssrc/services/payment/requestNetwork/requestNetworkPayInService.ts— callsGET /v2/requestafter intent creationsrc/services/payment/requestNetwork/requestNetworkRoutes.ts—GET /:paymentId/checkout+ lazy enrichment + debug responsesrc/services/payment/requestNetwork/networkClient.ts— already hadgetRequestStatussrc/app.ts—GET /api/version, exempt from rate limit__tests__/rn-in-house-checkout.test.ts— 12 unit tests, all green
Frontend (/Users/manwe/CascadeProjects/escrow/frontend):
src/web3/contracts/rn-fee-proxy.ts— RN proxy + ERC20 ABIssrc/web3/context/wagmi-provider.tsx— removed the mount-gate that causedWagmiProviderNotFoundErrorsrc/web3/components/provider-payment.tsx—router.pushto in-house page + sessionStorage stashsrc/sections/payment/checkout/types.ts+rn-in-house-checkout-view.tsx— state machine, local WagmiProvider wrapsrc/app/checkout/request-network/[paymentId]/page.tsx— app router entrysrc/components/version-logger.tsx— version chip + tooltip showing backend version
Memory entries added
MEMORY.mdindex updated with:arcane_dev_stack.md(env/project IDs, two-step deploy note)woodpecker_silent_build_fail.md(CI green ≠ image pushed)- and existing
rn_webhook_event_field.md,backend_rate_limits.md,telegram_notify_no_parse_mode.md,devEscrow_nginx_after_redeploy.md,parallel_agents_on_escrow.md
Open PRD questions still to decide
From PRD - Request Network In-House Checkout.md §10:
- Proxy address universality across chains (currently BSC + Arb confirmed; Task #8 will probe Polygon/ETH/Base)
- API pricing for hosted-UI-less usage (need RN account-mgmt question)
- Approval UX — exact-amount vs MAX_UINT256 (current: exact-amount)
- Cancel / timeout semantics for abandoned intents
- Telemetry events for in-house vs hosted A/B
Follow-up tasks (Taskmaster + PRD)
Five follow-ups scoped for kimi to pick up independently. Full spec in PRD - Wallet, Multichain, Confirmations, AML, Trezor.md. Quick index:
| # | Task | Priority | Status |
|---|---|---|---|
| 7 | Per-(buyer, sellerOffer) ephemeral RN destination wallets | high | 🟡 In progress — backend + admin UI + cart-aware UX + tests shipped in 2.6.45/2.6.46; live RN-accepts-divergent-destination probe remains manual |
| 8 | Multichain RN proxy registry + USDC/USDT support | high | 🟡 In progress — backend registry + probe script + frontend admin page + USDT-mainnet reset shipped in 2.6.46; BSC USDT paid probe + Base proxy address hunt remain |
| 9 | Per-chain confirmation thresholds + admin UI | medium | ⏳ Not started |
| 10 | Optional AML screening on incoming payments (seller-paid) | medium | ⏳ Not started |
| 11 | Trezor signing for admin actions (release/refund/sweep) | high | ⏳ Not started |
Task #7 — what landed in 2.6.42
Backend (backend/src/services/payment/wallets/ + plumbing)
DerivedDestinationmodel:(buyerId, sellerOfferId, chainId)→ address, derivation path, status, sweep history.derivedDestinations.ts: xpub-driven HD address derivation, atomic counter-based index allocation, idempotentgetDestinationFor, race-safe upsert. Backend holdsDERIVED_DESTINATION_XPUBonly — master seed lives in KMS / Trezor (Task #11).sweepService.ts: pluggable signer abstraction (build-onlydefault;hot-keyfor dev), ERC-20 balance queries, sweep orchestration, interval-based cron.derivedDestinationRoutes.ts: admin-only REST endpoints (list, sweep-all, sweep-one, config health, cron start/stop/status). Mounted at/api/payment/derived-destinations.requestNetworkPayInService.tsnow callsgetDestinationFor(buyer, sellerOffer, chainId), builds the per-payment merchant reference viabuildMerchantReference, persistsmetadata.derivedDestination, and passes the override to RN.inHouseCheckout.tsaccepts adestinationOverride; the on-chainpaymentReferencecompute-fallback now uses the actual destination (previously readparsed.recipient— hidden bug because RN's response provides the ref directly, but the fallback was broken for derived destinations).TransactionSafetyProvider.resolveExpectedRecipientchecksmetadata.derivedDestination.addressfirst, then legacy fallback.
Frontend (admin only)
/dashboard/admin/derived-destinationspage (table view, filters by status/chain/address, pagination, sweep-all, cron start/stop).- Per-row UI: address with copy + BscScan link, status chip, derivation path, balance, sweep count, last sweep tx link.
Env additions (see backend/.env.example):
DERIVED_DESTINATION_XPUB— required for address derivation.DERIVED_DESTINATION_XPRIV— only whenDERIVED_DESTINATION_SWEEP_SIGNER=hot-key(dev shortcut).DERIVED_DESTINATION_BASE_PATH=m/44'/60'/0'DERIVED_DESTINATION_CHAIN_ID=56DERIVED_DESTINATION_SWEEP_SIGNER=build-onlyDERIVED_DESTINATION_MIN_SWEEP_AMOUNT=0DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000
Task #7 — completion status
| Item | Status | Notes |
|---|---|---|
| Backend model + HD derivation | ✅ | DerivedDestination, derivedDestinations.ts, counter-based index |
| Sweep service + cron | ✅ | sweepService.ts, build-only/hot-key signers, auto-start in app.ts |
| Admin API + UI | ✅ | /api/payment/derived-destinations/*, admin dashboard page |
| RN intent integration | ✅ | requestNetworkPayInService.ts passes per-seller destination |
| Transaction Safety Provider | ✅ | resolveExpectedRecipient checks metadata.derivedDestination first |
| A — Cart-aware buyer UX | ✅ | MultiSellerProviderPayment + RnMultiCheckoutView + template checkout wiring |
| D — Auto-start sweep cron | ✅ | app.ts boots cron when DERIVED_DESTINATION_SWEEP_AUTOSTART=true |
E — recordSweep accumulation fix |
✅ | $inc: { totalSwept } instead of $setOnInsert |
| F — API docs | ✅ | Derived-destination endpoints added to Payment API.md |
| B — Unit tests | ✅ | 46 tests across 3 files (see below) |
| C — Live divergent-destination probe | 🔄 | Protocol prepared; requires manual browser + wallet execution (see §Live multi-seller probe below). Dev is running 2.6.46 with all backend/frontend code deployed. |
Task #8 — completion status
| Item | Status | Notes |
|---|---|---|
| Probe script | ✅ | scripts/probe-rn-chains.ts — 4/5 chains verified (BSC, Arbitrum, Ethereum, Polygon). Base proxy not deployed at canonical address. |
| Token registry JSON | ✅ | tokens.json — USDC + USDT on 4 verified chains with correct decimals |
| Chain registry JSON | ✅ | supportedChains.json — 4 verified chains + Base in _unverified |
| Admin route | ✅ | GET /api/admin/rn/networks, POST /api/admin/rn/networks/reload |
| Frontend admin page | ✅ | /dashboard/admin/networks renders registry with reload button |
unsupported_chain reason |
✅ | buildInHouseCheckoutBlock returns unsupported_chain:<id> |
| Frontend wagmi multichain | ✅ | Added arbitrum + base to wagmi config with RPC transports |
| Per-chain explorers | ✅ | Checkout view uses correct explorer per chainId |
| USDT-mainnet approve reset | ✅ | Implemented but unverified — see note in Handoff - RN Multichain Probe - 2026-05-28 |
| BSC USDT paid probe | 🔄 | Pending manual execution — needs real wallet + test BSC USDT |
| Base proxy hunt | 🔄 | Canonical address has no code on Base. Need RN official docs for actual address. |
B — Unit tests (backend@34f542e, 2.6.45)
__tests__/derived-destinations.test.ts (26 tests)
validateXpubrejects xpriv, tprv, garbage, empty, null/undefinedderiveAddressAtIndexis deterministic; different indices → different addresses; checksummed; rejects negative/non-integergetDestinationForidempotency: same(buyer, sellerOffer, chainId)returns same row, counter increments exactly oncegetDestinationForE11000 race fallback: simulates concurrent insert, verify second caller re-reads racer's rowgetDestinationFornon-E11000 errors are re-thrownrecordSweep$incaccumulation: run twice, asserttotalSweptequals sum (regression lock-in for item E)recordSweephandles string and negative amountsresolveExpectedRecipientForPaymentprefersmetadata.derivedDestination, falls back toblockchain.receiverlistDerivedDestinationspaginationverifyDerivedDestinationConfigok / missing xpub / invalid xpub
__tests__/sweep-service.test.ts (18 tests)
getSweepSignerreturnsbuild-onlyby default,hot-keywhen configuredqueryTokenBalanceparses bigint, returns 0n for empty balance, null on RPC failure, null for unsupported chain+tokensweepDerivedDestinationsskips below-threshold balances, dry-run returns amount without broadcasting, build-only signer returns error without updating record, handles balance query failure, respectsdestinationIdsfilter, throws when master wallet missing- Cron lifecycle: start/stop/idempotent/zero-interval
__tests__/request-template-orphan-cleanup.test.ts (2 tests) — non-negotiable regression lock-in for Gap 2 fix
- Asserts
Payment.findduring orphan cleanup is scoped toprovider: 'shkeeper' - Asserts a pending
provider: 'request.network'Payment is NOT deleted when a shkeeper orphan exists for the same buyer - Asserts request.network orphan is untouched even when no shkeeper orphan is present
C — Live multi-seller probe protocol
Goal: Prove RN accepts divergent destinationId across consecutive POST /v2/secure-payments from the same buyer session, and that the multi-checkout cart UX creates two Payments landing on two different derived addresses.
Prerequisites:
- Dev backend running ≥ 2.6.45 with
DERIVED_DESTINATION_XPUBconfigured - Dev frontend running ≥ 2.6.44
- Two seller accounts on
dev.amn.ggwith wallet addresses set - Buyer account with a Rabby/Metamask wallet holding ≥ 0.02 testnet BSC USDC
Steps:
-
Create two template offers (one from each seller):
- Seller A:
https://dev.amn.gg/dashboard/shops/templates/new→ fill title, price 0.01 USDC, publish - Seller B: same, price 0.01 USDC, publish
- Capture both
shareableLinkvalues
- Seller A:
-
As buyer, add both to cart:
- Visit Seller A's shareable link → Add to cart
- Visit Seller B's shareable link → Add to cart
- Go to
/dashboard/shops/checkout/?step=2 - Confirm cart shows 2 items from 2 different sellers
-
Select crypto payment and proceed:
- Choose "پرداخت با Request Network"
- Click the multi-seller button (should show
۲ فروشنده) - Browser navigates to
/checkout/request-network/multi?session=<id>
-
Pay Seller A (first checkout page):
- Connect wallet
- Approve 0.01 USDC for RN proxy
- Call
transferFromWithReferenceAndFee - Wait for "پرداخت تأیید شد ✓"
- Click "ادامه به پرداخت بعدی"
-
Pay Seller B (second checkout page):
- Approve 0.01 USDC (or reuse allowance if proxy unchanged)
- Call
transferFromWithReferenceAndFee - Wait for confirmation
- Click "پایان"
-
Capture evidence:
- Derived addresses: Check
GET /api/payment/derived-destinations?buyerId=<buyerId>— expect 2 rows with differentaddressandderivationIndex - Tx hashes: Copy both approve and both pay tx hashes from the UI; verify on
https://testnet.bscscan.com(or mainnet BscScan if dev uses mainnet) - Transfer events: On BscScan, verify both
TransferWithReferenceAndFeeevents show differentrecipientaddresses - Webhooks: Check backend logs for
payment-updatesocket emissions orPOST /api/payment/request-network/webhookhandling for both payments - Payment records: Query Mongo for the two
Paymentdocs — both should havestatus: 'completed'and differentmetadata.derivedDestination.address - PurchaseRequests: Verify
convertTemplatesToRequestscreated 2PurchaseRequestdocs withstatus: 'payment'
- Derived addresses: Check
-
Document: Paste the full evidence block below under "Live multi-seller probe — execution record".
Live multi-seller probe — execution record
To be filled after manual execution of the protocol above.
Field Value Date Buyer ID Seller A ID / Offer Seller B ID / Offer Derived address A Derived address B Approve tx A Pay tx A Approve tx B Pay tx B Payment ID A Payment ID B PurchaseRequest ID A PurchaseRequest ID B RN webhook fired for both? Both Payments completed? Escrow funded for both?
Remaining in task #7:
- Item C: execute the live probe protocol above and fill the execution record table.
- After C passes: flip PRD §1 acceptance criteria #1 and #2 from ⏳ → ✅ and mark Task #7 done in Taskmaster.