In-house Request Network checkout went fully end-to-end on dev today. A real 0.01 USDC payment flowed through wallet connect -> approve -> ERC20FeeProxy.transferFromWithReferenceAndFee -> RN webhook -> TransactionSafetyProvider -> Payment.status=completed -> page success state. Tx 0x494c77a29161b5100d8e0b1ac675f1822955d0bb3633ecdbfafb886f84f2f320. Docs: - New PRD: Wallet, Multichain, Confirmations, AML, Trezor (5 follow-ups, each sized for an independent contributor) - Updated PRD: Request Network In-House Checkout (phases 0..3 done, phase 4 partial, phases 5-6 not started) - Updated handoff: deployed versions, what is working end-to-end, follow-up tasks index Taskmaster: 5 new top-level tasks (#7..#11) covering ephemeral destination wallets, multichain proxy registry + USDC/USDT, runtime confirmation thresholds, optional seller-paid AML screening, and Trezor signing for admin actions. Tasks are scoped fine-grained so each is independent enough for kimi to pick up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
281 lines
20 KiB
Markdown
281 lines
20 KiB
Markdown
# PRD: Request Network In-House Checkout
|
||
|
||
**Status:** Draft — updated after 2026-05-28 dev webhook probe
|
||
**Date:** 2026-05-27
|
||
**Related:** `01 - Architecture/Request Network Integration Constraints.md` §1; `PRD - Request Network Migration and Funds Management.md`
|
||
**Owner:** Backend payments + Frontend
|
||
|
||
---
|
||
|
||
## 1. Problem
|
||
|
||
Buyers paying through Request Network (RN) are redirected from Amanat to RN's hosted page at `pay.request.network/?token=…`. That page has three concrete problems:
|
||
|
||
1. **Rabby is not supported.** A meaningful portion of our user base pays from Rabby. The hosted page does not detect it (no EIP-6963 enumeration, hard-coded MetaMask/WalletConnect/Coinbase). For those users, checkout fails before a transaction is even attempted.
|
||
2. **The page is brand-divergent.** Buyers leave the Amanat domain mid-flow, which is a trust and conversion cost we can avoid.
|
||
3. **The page relies on RN's choice of infrastructure.** Observed in our own probe: their UI wraps the payment in Safe smart accounts + ERC-4337 + a Pimlico paymaster, hitting public BSC RPCs that already rate-limited us once ("RPC endpoint returned too many errors"). We have no control to fix that.
|
||
|
||
The core RN protocol is sound. The hosted UI on top of it is the problem.
|
||
|
||
### 2026-05-28 reality update
|
||
|
||
The first dev BSC probe did not fail because RN stayed silent. It failed because Amanat dropped the provider callback:
|
||
|
||
- Test transaction: `0x3a23febd9abd43d7e0851c1ea86c4ceaf08c11098852cb0425fa074e9c88350b`.
|
||
- On-chain result: successful BSC USDC transfer to Amanat's configured destination wallet.
|
||
- RN webhook result: RN called `POST /api/payment/request-network/webhook` on `dev.amn.gg` four times from `34.34.233.192`.
|
||
- Backend result: nginx/backend returned `404` because the handler did not correlate all Request Network identifiers stored on the `Payment` record.
|
||
- Frontend result: the callback page stayed on the processing state and later hit `429` from 3-second polling.
|
||
|
||
The pre-implementation gate therefore changes from "prove RN calls us" to "deploy and smoke-test the webhook correlation repair, callback polling repair, and transaction-safety gate before another paid probe".
|
||
|
||
## 2. Goal
|
||
|
||
Replace the redirect to RN's hosted page with an Amanat-rendered checkout that:
|
||
|
||
- Connects any EVM wallet (Rabby, MetaMask, OKX, Trust, WalletConnect — anything wagmi's `injected()` connector enumerates).
|
||
- Builds and submits the exact same on-chain calls RN's hosted page makes, so RN's existing webhook fires unchanged.
|
||
- Stays on `dev.amn.gg` (or prod equivalent) for the entire flow.
|
||
- Falls back gracefully to the hosted page for buyers who explicitly request it.
|
||
|
||
Settlement, webhook handling, and the backend `Payment` lifecycle do not change. Only the buyer-facing checkout surface changes.
|
||
|
||
Webhook delivery alone is not a sufficient trust signal. Amanat must run a **Transaction Safety Provider** gate before marking escrow funded. The initial provider checks: transaction hash present, configured confirmation depth, transfer recipient/token/amount match on-chain evidence, and future AML/sanctions provider approval.
|
||
|
||
## 3. Non-goals
|
||
|
||
- Per-seller multi-chain configuration (covered in `Request Network Integration Constraints.md` §2 — separate PRD).
|
||
- Per-`(buyer, merchant)` ephemeral wallets / wallet abstraction layer (§3 — separate PRD).
|
||
- Replacing RN entirely with our own chain watcher (§4 — depends on this PRD's outcome).
|
||
- Gasless / sponsored transactions for the buyer. Buyer pays own gas via their EOA. We can revisit paymaster integration in a follow-up.
|
||
|
||
## 4. Background: what RN's protocol actually requires
|
||
|
||
Cold-inspected from a real `$12` BSC payment on RN's hosted page:
|
||
|
||
| Item | Value |
|
||
|---|---|
|
||
| Proxy contract (BSC, and same address on most EVMs via deterministic CREATE2) | `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` |
|
||
| Function | `transferFromWithReferenceAndFee(address token, address to, uint256 amount, bytes reference, uint256 feeAmount, address feeAddress)` |
|
||
| Selector | `0xc219a14d` |
|
||
| Emitted event | `TransferWithReferenceAndFee(token, to, amount, reference, feeAmount, feeAddress)` |
|
||
| `paymentReference` derivation | `last8Bytes(keccak256(lowercase(requestId + salt + destinationAddress)))` |
|
||
| Fee config seen on RN's UI | `feeAmount = 0`, `feeAddress = 0x…dEaD` |
|
||
|
||
RN's webhook listens for `TransferWithReferenceAndFee`. If we emit it ourselves with the correct `paymentReference`, the webhook fires. The hosted UI is a UI-only convenience — not a settlement requirement.
|
||
|
||
## 5. Proposed flow (buyer perspective)
|
||
|
||
1. Buyer clicks "Pay with Request Network" on the request page.
|
||
2. Frontend calls existing `POST /payment/request-network/intents` (unchanged endpoint, expanded response — see §6).
|
||
3. Frontend navigates to new in-house page: `/checkout/request-network/:paymentId`.
|
||
4. Page shows: amount, token, chain, recipient (truncated, copyable), countdown timer.
|
||
5. "Connect wallet" — wagmi `injected()` + `metaMask()` (already configured). Rabby works automatically via EIP-6963.
|
||
6. Once connected, page checks the buyer's wallet is on the correct chain. If not, prompts `switchChain` (wagmi).
|
||
7. Page also checks current allowance. If `allowance(buyer, RN_PROXY) >= amount`, skip approve.
|
||
8. Buyer signs **transaction 1: `USDC.approve(RN_PROXY, amount)`**. UI shows "Approving…" → "Approved".
|
||
9. Buyer signs **transaction 2: `RN_PROXY.transferFromWithReferenceAndFee(token, dest, amount, ref, 0, 0x…dEaD)`**. UI shows "Submitting payment…" → "Confirmed on-chain".
|
||
10. Page waits for backend `payment-confirmed` socket event (already emitted on RN webhook → `Payment.status='paid'`). Shows success state.
|
||
11. Redirect to existing payment-success route.
|
||
|
||
Escape hatches surfaced as secondary links on the page:
|
||
|
||
- **"Continue on Request Network's hosted page"** → falls through to `securePaymentUrl` (unchanged behavior).
|
||
- **"Copy payment details"** → exposes destination, amount, token, chain, paymentReference for manual entry into any wallet.
|
||
|
||
## 6. Backend changes
|
||
|
||
### 6.1 Intent response payload
|
||
|
||
Expand `PayInIntentResult` for RN. Today it returns `paymentUrl`, `paymentId`, `providerPaymentId`, `amount`, `config.networks`, `config.allowedTokens`, `raw`. Add:
|
||
|
||
```ts
|
||
inHouseCheckout: {
|
||
destination: '0x05E280d7f3cA954f37afA8B1E4d2a51D167c573e',
|
||
tokenAddress: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
|
||
tokenSymbol: 'USDC',
|
||
decimals: 18, // BSC USDC quirk; varies per token+chain
|
||
chainId: 56,
|
||
proxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9',
|
||
paymentReference: '0xa826e248f5de5463', // 8-byte hex, computed from RN's requestId + salt
|
||
feeAmount: '0',
|
||
feeAddress: '0x000000000000000000000000000000000000dEaD',
|
||
amountWei: '12000000000000000000', // pre-multiplied by decimals, frontend doesn't compute
|
||
}
|
||
```
|
||
|
||
### 6.2 Sources for each field
|
||
|
||
| Field | Source |
|
||
|---|---|
|
||
| `destination`, `tokenAddress`, `chainId` | Parse `REQUEST_NETWORK_MERCHANT_REFERENCE` env (`address@eip155:chainId#ref:tokenAddr`). Today done once at startup; extract into a helper. |
|
||
| `decimals` | Token registry lookup. Hardcode the few we care about (USDC/USDT × BSC/ETH/Arb/Polygon) in a constant map; fall back to on-chain `decimals()` if missing. |
|
||
| `proxyAddress` | Constant per chain. Use canonical `0x0DfbEe14…0aC9` for all EVMs unless RN deploys a non-canonical address (open question — see §10). |
|
||
| `paymentReference` | Computed from RN's `/v2/secure-payments` response. `salt` is in their `raw` payload. Use `keccak256` + slice-to-8-bytes. |
|
||
| `feeAmount`, `feeAddress` | Constants matching what RN's hosted UI submits. Verify via cold inspection on each new chain we enable. |
|
||
| `amountWei` | `BigInt(amount) * 10n ** BigInt(decimals)`. Done backend-side to avoid frontend rounding bugs. |
|
||
|
||
### 6.3 Files touched
|
||
|
||
- `backend/src/services/payment/requestNetwork/contract.ts` — extend `PayInIntentResult` (or a new `inHouseCheckout` sub-object).
|
||
- `backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts` — populate the new fields.
|
||
- `backend/src/services/payment/requestNetwork/merchantReference.ts` (new) — parser for the `address@chain#ref:token` format. Currently this is implicit in env lookups; promote to a named helper.
|
||
- `backend/src/services/payment/requestNetwork/tokens.ts` (new) — `{ chainId, tokenAddr } → { symbol, decimals }` lookup.
|
||
- `backend/src/services/payment/requestNetwork/paymentReference.ts` (new) — `computePaymentReference(requestId, salt, destination)`.
|
||
- Unit tests for each new helper.
|
||
|
||
### 6.4 Webhook handler
|
||
|
||
The route remains `/api/payment/request-network/webhook`, but the handler must be hardened before more paid probes:
|
||
|
||
- Correlate against every RN identifier we persist: `providerPaymentId`, `metadata.requestNetworkRequestId`, `metadata.requestNetworkPaymentReference`, and nested `metadata.requestNetworkData` ids/references.
|
||
- Reject unsigned or test-header callbacks unless explicit test mode is enabled.
|
||
- Store raw webhook evidence on the `Payment` record for support/replay.
|
||
- Call the Transaction Safety Provider before marking `Payment.status='completed'` / `escrowState='funded'`.
|
||
- Return a successful pending response when safety is not yet satisfied, so the delivery is not lost but escrow is not credited.
|
||
|
||
## 7. Frontend changes
|
||
|
||
### 7.1 New page
|
||
|
||
`frontend/src/pages/checkout/request-network/[paymentId].tsx` (or whatever the router convention is).
|
||
|
||
State machine (one component, reducer or zustand):
|
||
|
||
```
|
||
idle
|
||
→ connecting (wallet not connected)
|
||
→ wrong-chain (connected, chain mismatch)
|
||
→ ready (correct chain, allowance unknown)
|
||
→ checking-allowance
|
||
→ needs-approve → approving → approve-confirming → ready-to-pay
|
||
→ ready-to-pay
|
||
→ paying → pay-confirming → confirmed
|
||
→ error (with retry)
|
||
→ expired (timer ran out)
|
||
```
|
||
|
||
### 7.2 Reusable parts from SHKeeper
|
||
|
||
`frontend/src/web3/components/manual-payment.tsx` already implements: address-with-QR, copy-to-clipboard, countdown timer, localStorage persistence per request, socket-driven status update. Reuse the layout chrome. The wallet-interaction half is new.
|
||
|
||
### 7.3 Wagmi calls
|
||
|
||
Two contracts. The ERC-20 ABI is already in viem/wagmi. RN proxy ABI is a single function — define it inline.
|
||
|
||
```ts
|
||
const RN_PROXY_ABI = [{
|
||
inputs: [
|
||
{ name: 'tokenAddress', type: 'address' },
|
||
{ name: 'to', type: 'address' },
|
||
{ name: 'amount', type: 'uint256' },
|
||
{ name: 'paymentReference', type: 'bytes' },
|
||
{ name: 'feeAmount', type: 'uint256' },
|
||
{ name: 'feeAddress', type: 'address' },
|
||
],
|
||
name: 'transferFromWithReferenceAndFee',
|
||
outputs: [], stateMutability: 'nonpayable', type: 'function',
|
||
}] as const;
|
||
```
|
||
|
||
Hooks used:
|
||
|
||
- `useAccount`, `useChainId`, `useSwitchChain`
|
||
- `useReadContract` for `allowance`
|
||
- `useWriteContract` + `useWaitForTransactionReceipt` for approve and proxy call
|
||
|
||
### 7.4 Routing change
|
||
|
||
Today, `createRequestNetworkIntent` consumers do `window.location = response.paymentUrl`. Replace with `router.push('/checkout/request-network/' + response.paymentId)`. Keep `response.paymentUrl` available so the in-house page can render the escape-hatch link.
|
||
|
||
### 7.5 Files touched
|
||
|
||
- `frontend/src/pages/checkout/request-network/[paymentId].tsx` — new page.
|
||
- `frontend/src/web3/components/rn-in-house-checkout.tsx` — new component holding the state machine.
|
||
- `frontend/src/web3/contracts/rn-fee-proxy.ts` — ABI + address-per-chain constants.
|
||
- `frontend/src/sections/request/components/buyer-steps/…` — change the existing RN button handler from `window.location` to `router.push`.
|
||
- Tests/Storybook for the new component.
|
||
|
||
## 8. Acceptance criteria
|
||
|
||
1. A buyer using **Rabby** can complete an RN payment end-to-end on `dev.amn.gg` without leaving the domain.
|
||
2. The two on-chain transactions emit a `TransferWithReferenceAndFee` event with the same `paymentReference` RN's hosted page would have produced.
|
||
3. RN's existing webhook fires on the event and the `Payment` doc transitions to `completed` / `escrowState='funded'` after safety approval.
|
||
4. If buyer connects on the wrong chain, they see a one-click "Switch network" CTA.
|
||
5. If buyer already has sufficient allowance, the approve step is skipped.
|
||
6. If the buyer abandons the page, returning to it restores their state from localStorage and resumes from the correct step.
|
||
7. "Continue on Request Network's hosted page" link is visible on the in-house page and works exactly like today's redirect.
|
||
8. Page handles tx failure (user reject, insufficient gas, RPC error) with a clear retry path that does not corrupt the `Payment` doc.
|
||
9. Existing non-RN payment flows (SHKeeper, others) are untouched.
|
||
|
||
## 9. Out of scope (explicit non-decisions)
|
||
|
||
- Multi-chain at checkout (BSC + Arb + ETH together) — needs the per-seller `acceptedChains` config; separate PRD.
|
||
- WalletConnect integration — currently disabled in `web3/config.ts` for SSR reasons; not regressed but not added either.
|
||
- Migration plan to remove RN entirely — kept as `§4` open option in the architecture doc.
|
||
- Mobile Mini App rendering of the checkout (different surface — handled in Task 5.4).
|
||
|
||
## 10. Open questions for review
|
||
|
||
These are the items we should discuss with the second developer before starting implementation.
|
||
|
||
1. **Proxy address universality.** Is `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` the RN ERC20FeeProxy address on **every** chain we plan to support (BSC, Arb, ETH mainnet, Polygon, Base)? We confirmed BSC + Arbitrum from RN's payments-subgraph repo. The other chains need explicit confirmation — preferably an on-chain probe per chain.
|
||
2. **API pricing for hosted-UI vs API-only.** RN's pricing model may assume hosted-UI usage. If we use them as a notification primitive only, are we still in their pricing terms? Worth a direct question to RN account management.
|
||
3. **Approval UX.** RN's hosted UI uses `approve(spender, MAX_UINT256)` for gas efficiency. Some Amanat users have voiced concern in past audits about unbounded approvals. Options:
|
||
- Mirror RN: `MAX_UINT256` (best UX, repeat payments don't need re-approve).
|
||
- Exact-amount: `approve(spender, amount)`. Slightly worse UX (every payment requires an approve), better security posture.
|
||
- Recommend: exact-amount by default, opt-in "remember this approval" for power users. Discuss.
|
||
4. **Tokens with non-standard ERC-20 behavior.** USDT on Ethereum mainnet famously requires `approve(0)` before changing a non-zero allowance. We need to handle this if we add USDT-ETH later. Not relevant on day one (BSC config is USDC), but worth flagging now.
|
||
5. **Cancel and timeout semantics.** Today the `Payment` doc transitions on the webhook. If the buyer signs the approve but bails on the proxy call, the doc sits in `pending` forever. Options:
|
||
- 30-minute TTL on the `pending` doc (existing partial unique index would let a re-attempt succeed).
|
||
- "Cancel intent" button on the in-house page that calls a new backend endpoint to mark the doc `cancelled`.
|
||
- Both. Discuss.
|
||
6. **Manual reconciliation.** If a buyer pays the correct amount to the correct destination but bypasses the proxy (e.g., raw `transfer`), no event fires and no webhook arrives. They'll think they paid but our system won't know. Today RN's hosted UI prevents this; our in-house UI inherits the same risk only if we add a "raw transfer" escape hatch — which we won't. But: should we proactively detect orphan transfers (chain watcher) and surface them in support? Out of scope for v1.
|
||
7. **Feature flag and rollout.** Ship behind a flag and A/B against the current redirect for one week of dev usage? Or hard-cut once acceptance criteria pass? Recommend A/B for at least one full release cycle.
|
||
8. **Telemetry.** What events do we instrument so we can prove the in-house page outperforms the redirect on conversion? Suggested: `rn_checkout_opened`, `rn_wallet_connected`, `rn_approve_signed`, `rn_pay_signed`, `rn_pay_confirmed`, `rn_pay_failed{reason}`, `rn_escape_to_hosted_clicked`.
|
||
|
||
## 11. Risks
|
||
|
||
- **Webhook handling failure.** RN delivered the 2026-05-28 dev webhook, but Amanat returned `404`. A deployed correlation fix and smoke test are now the load-bearing gate before another paid probe.
|
||
- **Webhook durability.** The main app is too unstable to be the only callback landing zone. Put a Cloudflare Worker in front of Request Network webhooks to durably store raw delivery evidence, forward to the backend, and replay after outages.
|
||
- **False-positive payment credit.** A signed provider event must not be enough to fund escrow. Transaction Safety Provider gates completion on on-chain evidence, confirmation depth, transfer matching, and future AML/sanctions checks.
|
||
- **Chain-specific divergences.** RN may have deployed non-canonical proxy contracts on some chains. We mitigate by hard-coding per-chain proxy addresses and treating "unknown chain → fall back to hosted page" as the safe default.
|
||
- **Wallet support drift.** EIP-6963 is mature but not universal. We rely on wagmi's `injected()` connector enumeration; if a wallet doesn't implement EIP-6963, it falls into the generic `window.ethereum` slot. Acceptable.
|
||
- **Buyer signs approve, then we change ABI before they sign the proxy call.** Not really a risk if both txs are in the same session, but worth a smoke test on a long-lived session.
|
||
|
||
## 12. Implementation phases
|
||
|
||
Roughly two weeks of checkout work, now gated on confirmation reliability repair.
|
||
|
||
| Phase | Work | Gate |
|
||
|---|---|---|
|
||
| 0 — Probe | Fire one real dev BSC payment and inspect nginx/backend logs | Completed: RN webhook reached nginx/backend; app returned `404` |
|
||
| 0A — Confirmation repair | Deploy correlation fix, callback polling fix, signed-webhook smoke test, and Transaction Safety Provider | Unsigned/test callbacks rejected unless explicitly enabled; real webhook can find the `Payment` |
|
||
| 1 — Backend | Expand intent response with `inHouseCheckout` fields; add helpers and tests | Existing RN smoke test still passes; new unit tests pass |
|
||
| 2 — Frontend skeleton | New page + state machine + wallet-connect; no on-chain calls yet, mocked confirmations | Local clickthrough on `dev.amn.gg` with Rabby connected |
|
||
| 3 — On-chain wiring | Real `approve` + `transferFromWithReferenceAndFee` calls; allowance check; chain-switch | One real end-to-end payment on dev BSC, webhook fires, `Payment.status='completed'`, safety approved |
|
||
| 4 — Hardening | Error handling, timer, persistence, telemetry, escape-hatch link | Acceptance criteria 1–9 demonstrably pass |
|
||
| 5 — Durable ingress | Cloudflare Worker receives RN webhooks, stores delivery records, forwards to backend, supports replay | Backend outage no longer loses webhook evidence |
|
||
| 6 — Rollout | Behind feature flag, A/B in dev for one cycle, then prod | Conversion telemetry; no regressions in non-RN flows |
|
||
|
||
## 13. Durable webhook ingress roadmap
|
||
|
||
Add a Cloudflare Worker as the public Request Network webhook target:
|
||
|
||
1. Receive the raw RN webhook body, headers, request id, delivery id, source IP, and timestamp.
|
||
2. Verify the RN signature at the edge if raw-body secret handling is straightforward; otherwise store first and let the backend perform canonical verification.
|
||
3. Write an immutable delivery record to durable storage. Candidate stack: Cloudflare Queues for handoff plus D1/R2/KV for indexed replay metadata and raw payload retention.
|
||
4. Forward to the primary backend and optionally a secondary endpoint.
|
||
5. Return `2xx` only after durable enqueue/store succeeds.
|
||
6. Provide operator replay by delivery id, payment reference, request id, or time window.
|
||
|
||
The Worker is only ingress/evidence buffering. The backend remains the trust boundary for signature verification, idempotency, Transaction Safety Provider checks, ledger updates, and marketplace state transitions.
|
||
|
||
## 14. References
|
||
|
||
- Cold-inspected transaction (real `$12` BSC payment on RN hosted page): in conversation history, dated 2026-05-27.
|
||
- RN ERC20FeeProxy spec: <https://github.com/RequestNetwork/requestNetwork/blob/master/packages/advanced-logic/specs/payment-network-erc20-fee-proxy-contract-0.1.0.md>
|
||
- Arbitrum proxy deployment: <https://github.com/RequestNetwork/payments-subgraph/blob/main/subgraph.arbitrum-one.yaml>
|
||
- Architecture constraints doc: `01 - Architecture/Request Network Integration Constraints.md`
|
||
- Previous RN work: `PRD - Request Network Migration and Funds Management.md`
|