docs: sync from backend 34f542e — Task #7 B unit tests + C protocol + PRD updates

This commit is contained in:
Siavash Sameni
2026-05-28 19:18:53 +04:00
parent 7868d94340
commit 2308db8074
3 changed files with 135 additions and 7 deletions

View File

@@ -93,8 +93,126 @@ Five follow-ups scoped for kimi to pick up independently. Full spec in `PRD - Wa
- `DERIVED_DESTINATION_MIN_SWEEP_AMOUNT=0` - `DERIVED_DESTINATION_MIN_SWEEP_AMOUNT=0`
- `DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000` - `DERIVED_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 execution on dev (see §Live multi-seller probe below) |
### B — Unit tests (backend@34f542e, 2.6.45)
**`__tests__/derived-destinations.test.ts`** (26 tests)
- `validateXpub` rejects xpriv, tprv, garbage, empty, null/undefined
- `deriveAddressAtIndex` is deterministic; different indices → different addresses; checksummed; rejects negative/non-integer
- `getDestinationFor` idempotency: same `(buyer, sellerOffer, chainId)` returns same row, counter increments exactly once
- `getDestinationFor` E11000 race fallback: simulates concurrent insert, verify second caller re-reads racer's row
- `getDestinationFor` non-E11000 errors are re-thrown
- `recordSweep` `$inc` accumulation: run twice, assert `totalSwept` equals sum (regression lock-in for item E)
- `recordSweep` handles string and negative amounts
- `resolveExpectedRecipientForPayment` prefers `metadata.derivedDestination`, falls back to `blockchain.receiver`
- `listDerivedDestinations` pagination
- `verifyDerivedDestinationConfig` ok / missing xpub / invalid xpub
**`__tests__/sweep-service.test.ts`** (18 tests)
- `getSweepSigner` returns `build-only` by default, `hot-key` when configured
- `queryTokenBalance` parses bigint, returns 0n for empty balance, null on RPC failure, null for unsupported chain+token
- `sweepDerivedDestinations` skips below-threshold balances, dry-run returns amount without broadcasting, build-only signer returns error without updating record, handles balance query failure, respects `destinationIds` filter, 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.find` during orphan cleanup is scoped to `provider: '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_XPUB` configured
- Dev frontend running ≥ 2.6.44
- Two seller accounts on `dev.amn.gg` with wallet addresses set
- Buyer account with a Rabby/Metamask wallet holding ≥ 0.02 testnet BSC USDC
**Steps:**
1. **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 `shareableLink` values
2. **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
3. **Select crypto payment and proceed**:
- Choose "پرداخت با Request Network"
- Click the multi-seller button (should show `۲ فروشنده`)
- Browser navigates to `/checkout/request-network/multi?session=<id>`
4. **Pay Seller A** (first checkout page):
- Connect wallet
- Approve 0.01 USDC for RN proxy
- Call `transferFromWithReferenceAndFee`
- Wait for "پرداخت تأیید شد ✓"
- Click "ادامه به پرداخت بعدی"
5. **Pay Seller B** (second checkout page):
- Approve 0.01 USDC (or reuse allowance if proxy unchanged)
- Call `transferFromWithReferenceAndFee`
- Wait for confirmation
- Click "پایان"
6. **Capture evidence**:
- **Derived addresses:** Check `GET /api/payment/derived-destinations?buyerId=<buyerId>` — expect 2 rows with different `address` and `derivationIndex`
- **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 `TransferWithReferenceAndFee` events show different `recipient` addresses
- **Webhooks:** Check backend logs for `payment-update` socket emissions or `POST /api/payment/request-network/webhook` handling for both payments
- **Payment records:** Query Mongo for the two `Payment` docs — both should have `status: 'completed'` and different `metadata.derivedDestination.address`
- **PurchaseRequests:** Verify `convertTemplatesToRequests` created 2 `PurchaseRequest` docs with `status: 'payment'`
7. **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:** **Remaining in task #7:**
1. Cart-aware buyer UX on the in-house checkout (sequential multi-seller approval flow with clear progress UI). - Item C: execute the live probe protocol above and fill the execution record table.
2. Unit tests for `derivedDestinations.ts` (idempotency, race handling) and `sweepService.ts`. - After C passes: flip PRD §1 acceptance criteria #1 and #2 from ⏳ → ✅ and mark Task #7 done in Taskmaster.
3. Live probe on dev: confirm RN accepts divergent `destinationId` across consecutive `POST /v2/secure-payments` calls from the same client.
4. Optional: auto-start sweep cron on backend boot via `app.ts` (currently manual via admin endpoint).

View File

@@ -11,6 +11,16 @@ entries on top. Maintained by agents per the rule in `../AGENTS.md`.
--- ---
### 2026-05-28 — backend@34f542e — Task #7 B: unit tests for derived-destinations + sweep-service + orphan-cleanup regression
**Commits:** backend `34f542e` (2.6.44 → 2.6.45)
**Touched:** `__tests__/derived-destinations.test.ts` (26 tests), `__tests__/sweep-service.test.ts` (18 tests), `__tests__/request-template-orphan-cleanup.test.ts` (2 tests)
**Why:** PRD item B — regression lock-in test suite for Task #7. Covers: `getDestinationFor` idempotency, E11000 race fallback, `validateXpub` rejection of xpriv/tprv/garbage, `deriveAddressAtIndex` determinism, `recordSweep` `$inc` accumulation (regression lock-in for item E), and orphan-payment cleanup provider filtering (regression lock-in for Gap 2 fix in 2.6.44).
**Verification:** All 46 tests green (`npx jest derived-destinations.test.ts sweep-service.test.ts request-template-orphan-cleanup.test.ts`).
**Linked docs updated:** [[08 - Operations/Handoff - Request Network In-House Checkout - 2026-05-28]]
---
### 2026-05-28 — backend@1889169, frontend@c44ed64 — Task #7 A verification fix: multi-checkout conversion + orphan-payment guard ### 2026-05-28 — backend@1889169, frontend@c44ed64 — Task #7 A verification fix: multi-checkout conversion + orphan-payment guard
**Commits:** backend `1889169` (2.6.43 → 2.6.44), frontend `c44ed64` (2.6.43 → 2.6.44) **Commits:** backend `1889169` (2.6.43 → 2.6.44), frontend `c44ed64` (2.6.43 → 2.6.44)

View File

@@ -76,7 +76,7 @@ These were open questions in the original draft; the shipped implementation lock
| # | What | Where | Status | Notes | | # | What | Where | Status | Notes |
|---|------|-------|--------|-------| |---|------|-------|--------|-------|
| A | **Cart-aware buyer UX** on the in-house checkout page. | `frontend/src/sections/payment/checkout/rn-in-house-checkout-view.tsx` (and `provider-payment.tsx` for the entry flow). | ✅ Done | Entry walks each `sellerOfferId` in the cart, creates N intents sequentially, stashes them in `sessionStorage`, and the checkout view iterates with per-Payment progress and an "N of M sellers" header. Mid-cart abandonment leaves already-paid Payments settled and the rest in `pending`. | | A | **Cart-aware buyer UX** on the in-house checkout page. | `frontend/src/sections/payment/checkout/rn-in-house-checkout-view.tsx` (and `provider-payment.tsx` for the entry flow). | ✅ Done | Entry walks each `sellerOfferId` in the cart, creates N intents sequentially, stashes them in `sessionStorage`, and the checkout view iterates with per-Payment progress and an "N of M sellers" header. Mid-cart abandonment leaves already-paid Payments settled and the rest in `pending`. |
| B | **Unit tests** for the new modules. | `backend/__tests__/derived-destinations.test.ts` + `backend/__tests__/sweep-service.test.ts`. | ⏳ Pending | Minimum: `getDestinationFor` idempotency, E11000 race fallback, xpub rejection of xpriv/tprv, `deriveAddressAtIndex` determinism, `recordSweep` accumulation (now fixed in E — lock the fix in with a test). | | B | **Unit tests** for the new modules. | `backend/__tests__/derived-destinations.test.ts` + `backend/__tests__/sweep-service.test.ts` + `backend/__tests__/request-template-orphan-cleanup.test.ts`. | ✅ Done | 46 tests: `getDestinationFor` idempotency, E11000 race fallback, xpub rejection of xpriv/tprv, `deriveAddressAtIndex` determinism, `recordSweep` accumulation (regression lock-in for E), orphan-cleanup provider filtering (regression lock-in for Gap 2 fix). |
| C | **Live divergent-destination probe** on dev. | Manual test, no code. | ⏳ Pending | Run two paid intents on the in-house page to two different `sellerOfferId`s (so two different derived addresses), confirm both `TransferWithReferenceAndFee` events fire, both webhooks land, and both Payments transition to `completed`. Record the tx hashes in the handoff doc. | | C | **Live divergent-destination probe** on dev. | Manual test, no code. | ⏳ Pending | Run two paid intents on the in-house page to two different `sellerOfferId`s (so two different derived addresses), confirm both `TransferWithReferenceAndFee` events fire, both webhooks land, and both Payments transition to `completed`. Record the tx hashes in the handoff doc. |
| D | **Auto-start the sweep cron on boot**. | `backend/src/app.ts` after the route mount, behind `DERIVED_DESTINATION_SWEEP_AUTOSTART=true`. | ✅ Done | Cron now starts on boot when the env flag is set; admin endpoint still available for manual control. | | D | **Auto-start the sweep cron on boot**. | `backend/src/app.ts` after the route mount, behind `DERIVED_DESTINATION_SWEEP_AUTOSTART=true`. | ✅ Done | Cron now starts on boot when the env flag is set; admin endpoint still available for manual control. |
| E | **Fix `recordSweep` accumulation**. | `backend/src/services/payment/wallets/derivedDestinations.ts`. | ✅ Done | Switched from `$setOnInsert: { totalSwept }` to `$inc: { totalSwept }` so accumulation advances on every sweep. | | E | **Fix `recordSweep` accumulation**. | `backend/src/services/payment/wallets/derivedDestinations.ts`. | ✅ Done | Switched from `$setOnInsert: { totalSwept }` to `$inc: { totalSwept }` so accumulation advances on every sweep. |
@@ -88,8 +88,8 @@ These were open questions in the original draft; the shipped implementation lock
- Hardware-wallet-signed sweeps (covered in Task #11 — task #7 ships the `build-only` plumbing that Task #11 plugs into). - Hardware-wallet-signed sweeps (covered in Task #11 — task #7 ships the `build-only` plumbing that Task #11 plugs into).
### Acceptance criteria ### Acceptance criteria
1. ✅ Two payments from the same buyer to two different sellers land on two different addresses on-chain. (Backend logic shipped; live verification pending item C.) 1. ✅ Two payments from the same buyer to two different sellers land on two different addresses on-chain. (Backend logic shipped; frontend multi-checkout UX shipped; live verification pending item C.)
2. RN's webhook fires correctly for both, regardless of the destination divergence. (Pending C.) 2. RN's webhook fires correctly for both, regardless of the destination divergence. (Backend integration + multi-checkout UX shipped; end-to-end verification pending item C.)
3. 🟡 Sweep runs idempotently — re-running it on an already-swept address advances `totalSwept` correctly. (`$inc` fix shipped in E; lock-in test pending B.) 3. 🟡 Sweep runs idempotently — re-running it on an already-swept address advances `totalSwept` correctly. (`$inc` fix shipped in E; lock-in test pending B.)
4. ✅ Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status. 4. ✅ Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status.
5. ✅ Master seed never leaves the KMS/secret store. Backend reads derivation paths from xpub only; production signing path is `build-only` (Task #11 Trezor). Dev hot-key is documented as dev-only. 5. ✅ Master seed never leaves the KMS/secret store. Backend reads derivation paths from xpub only; production signing path is `build-only` (Task #11 Trezor). Dev hot-key is documented as dev-only.