Skip to content
135 changes: 135 additions & 0 deletions docs/ai-design/2026-07-10-qr-deposit-funding/dev-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Development Plan — QR/Receive-Deposit Identity Funding (IDN-014 restore)

Phase 1c. For `developer-bilby`. Restores the "fund by receiving a deposit"
method on Register-Identity (`add_new_identity_screen`) and Top-Up
(`top_up_identity_screen`), routed through the **existing** `FundWithWallet`
→ `AssetLockFunding::FromWalletBalance` path. Test IDs reference
`test-cases.md` (TC-QRFUND-01..17).

## Scope guardrails (read first)

- **NO upstream `platform-wallet` change. NO new `BackendTask`/`WalletTask`
variant. NO new `TaskError` variant.** The new funding method reuses the
exact dispatch `UseWalletBalance` already uses:
`register_identity.rs:56` / `top_up_identity.rs:59`
`FundWithWallet(..)` → `FromWalletBalance { amount_duffs, account_index: 0 }`.
- The receive-address QR reuses the existing
`WalletTask::GenerateReceiveAddress { seed_hash }` →
`BackendTaskSuccessResult::GeneratedReceiveAddress { seed_hash, address }`
(`backend_task/wallet/generate_receive_address.rs`) — the SPV-watched pool
address. Same task `create_asset_lock_screen` already consumes.
- Reference implementation to mirror for deposit detection:
`create_asset_lock_screen.rs:679-697` — **single-address equality** against
`self.funding_address`, NOT `known_addresses` membership (TC-QRFUND-06/07).
Note the REG screen's *other* detection point at `mod.rs:1268-1290`
(`WaitingForAssetLock` arm) uses `known_addresses` — that is asset-lock-tx
surfacing, a different concern; do not copy it for deposit detection.

## Task 1 — shared pure helper + enum in `funding_common.rs`

Single file, unit-tested first (TDD). Covers the logic both screens share so
detection is testable without a live egui screen (TC-QRFUND-14, Marvin pt 3).

1. **`FundingMethod::ReceiveDeposit` variant** (l.19). `Display` (l.31) and
`top_up_label` (l.47) are exhaustive `match` with no wildcard, so the
compiler forces a label in both — supply jargon-free copy for each
(TC-QRFUND-03). Suggested: Display `"Receive a new deposit"`; top_up_label
identical (no "new identity" assumption — §6 parity). Extend the existing
exhaustive tests `display_is_jargon_free_for_every_variant` and
`top_up_label_differs_only_for_asset_lock` to include it (TC-QRFUND-02/03/16).
2. **`fn deposit_matches(funding_address: Option<&Address>, outputs:
&[(OutPoint, TxOut, Address)]) -> u64`** — pure; sums `TxOut.value` of
outputs whose `Address == funding_address` (equality, not membership);
returns `0` when address is `None` or no match. This is the guard both
screens call from `display_task_result`. Unit tests: match ≥ minimum
(TC-QRFUND-04), different address → 0 (TC-QRFUND-06), empty/None → 0.
3. **`fn reset_to_choose() -> (FundingMethod, WalletFundedScreenStep)`** or reuse
`default_funding_state` for the never-trap back-out target
(`NoSelection`/`ChooseFundingMethod`), asserting `funding_address` clearing is
the caller's job (TC-QRFUND-10). Keep it a pure returning-helper; the field
write stays in the screen.

Running-total display (TC-QRFUND-05) reads wallet spendable in the UI (needs a
wallet read, not pure) — `deposit_matches` supplies the per-event threshold
decision; the cumulative figure shown comes from the wallet snapshot. Document
this split in a one-line comment.

## Task 2 — wire both screens (one Bilby pass)

Both screens are near-twins bound to the same helper; splitting them would
double the shared-helper churn. **Do both in one pass.** Order within the pass:
helper first (Task 1), then REG, then TOPUP (TOPUP already partially references
`ReceivedAvailableUTXOTransaction` at `mod.rs:512`).

Per screen:

1. **Chooser option** — add a `selectable_value` for `ReceiveDeposit` beside the
existing three (REG `render_funding_method` ~l.560-611; TOPUP ~l.298-337). On
select, set step to `WaitingOnFunds` (not `ReadyToCreate`) and dispatch
`WalletTask::GenerateReceiveAddress { seed_hash }` to populate
`funding_address`.
2. **`GeneratedReceiveAddress` handling** — in `display_task_result`, on that
result for the selected wallet's `seed_hash`, store `funding_address`
(mirror `create_asset_lock_screen.rs:665-674`). REG's `funding_address` field
must be added if absent; TOPUP already has it (`mod.rs:54`).
3. **Revive `WaitingOnFunds` arm** (REG `mod.rs:1265` empty; TOPUP equivalent):
on `CoreItem::ReceivedAvailableUTXOTransaction(_, outputs)`, call
`deposit_matches`; if cumulative spendable ≥ minimum → set `FundsReceived`.
Guard fires ONLY when `step == WaitingOnFunds` (TC-QRFUND-07).
4. **Revive `FundsReceived` arm** — pre-fill the amount input via
`max_amount_after_fee_reserve(spendable_duffs, estimated_fee)` (already tested
pure fn; TC-QRFUND-08); leave editable, clamped by existing `AmountInput`
(TC-QRFUND-09). On confirm, dispatch the **existing**
`FundWithWallet(amount_duffs, identity_index[, top_up_index])` and set
`WaitingForAssetLock` — identical to the `UseWalletBalance` confirm arm
(REG l.988-995; TOPUP l.390-403). From here the flow is already implemented
(`WaitingForAssetLock` → `WaitingForPlatformAcceptance` → `Success`),
TC-QRFUND-01.
5. **QR render** in the `WaitingOnFunds` view — `generate_qr_code_image(pay_uri)`
(exists, l.254) with a `dash:<address>?amount=` URI, show address text +
minimum-amount hint + running total (TC-QRFUND-05), and
`request_repaint_after(1s)` (no timeout — TC-QRFUND-15).
6. **Never-trap affordance** — a "Choose a different funding method" button
present in BOTH `WaitingOnFunds` and `FundsReceived`, resetting to
`ChooseFundingMethod`/`NoSelection` and clearing `funding_address`
(TC-QRFUND-10). No error banner on back-out.
7. **Failure reset** — reuse existing reset points (REG l.1222; TOPUP l.484):
`WaitingForAssetLock`/`WaitingForPlatformAcceptance` failure → `ReadyToCreate`
(TC-QRFUND-11/12). No new code path — the `FundWithWallet` error already
surfaces via `AppState`.

## Error handling — no new variant (TC-QRFUND-11)

`ReceiveDeposit` funds land in the wallet balance first, then route through
`FundWithWallet` → `FromWalletBalance`. Any build/broadcast failure leaves the
deposit in the wallet — the "funds are safe" condition is structural. The
existing typed `FundWithWallet` failure surface (`error.rs`, e.g. the
payment-preparation / wallet-service variants) already covers it. **Soft
option only:** if reassurance wording ("your deposit is safe in your wallet")
is wanted, refine the *existing* wallet-funding failure variant's `#[error(..)]`
copy — do not add a variant. Flag to coordinator; not required for green tests.

## Task 3 — `docs/user-stories.md` IDN-014

Flip `[Removed — upstream-only funding]` → `[Implemented]`. Replacement (Diziet's
draft, refined): *"As an everyday user, I can fund a new identity or a top-up by
receiving a Dash deposit to an address the tool shows me as a QR code, so I can
pay from any wallet or exchange without first moving funds into this tool.
Acceptance: choosing 'Receive a new deposit' shows a scannable address; once
enough arrives the amount pre-fills and I confirm to create/top-up; I can switch
funding methods at any time; a failure leaves my deposit safe in the wallet."*
Non-code doc edit — fold into the same PR, its own commit.

## Ordering / dependencies

Task 1 → Task 2 (Task 2 depends on the helper + variant). Task 3 independent.
**One Bilby invocation** covers Tasks 1+2+3: ~150-250 lines across
`funding_common.rs` + two screens + the doc, tightly coupled through the shared
helper. Splitting screens across passes would fork the helper contract — do not.

## Test traceability

Unit (helper + `display_task_result`): 01-08, 11, 12, 14, 15, 16, 17.
kittest (widget/nav/lock-gate): 09, 10, 13, 17. Manual testnet (coordinator GUI):
05 accumulation, 12 asset-lock reuse, 14 funds-safe-on-chain. Extract
`deposit_matches`/reset helpers so 04/06/07/14 are unit-reachable without egui.
165 changes: 165 additions & 0 deletions docs/ai-design/2026-07-10-qr-deposit-funding/test-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Test Case Specification — QR/Receive-Deposit Identity Funding (IDN-014 restore)

Phase 1b. Restores the removed "fund by receiving a deposit" method on the
Register-Identity (`add_new_identity_screen`) and Top-Up (`top_up_identity_screen`)
screens, routed through the existing `FundWithWallet` → `AssetLockFunding::FromWalletBalance`
backend path. Both screens are near-twins; cases apply to BOTH unless a
**[REG]**/**[TOPUP]** divergence is called out.

## Verified ground truth (corrections to the Phase-1a brief)

- **Detection is single-address equality, not `known_addresses`.** The reference
(`create_asset_lock_screen.rs:685`) advances only when a received output's
address `== self.funding_address`. Tests must encode equality against the ONE
shown address, not membership in the wallet's address set.
- `funding_address: Option<Address>` already exists as a **per-screen** field in
both screens (not in `funding_common.rs`).
- Deposit-state transitions are driven through `display_task_result()`; the
existing kittest harness only renders `AppState` and cannot inject task
results, so detection/state-machine cases are **unit-level** (call
`display_task_result` with a synthesized `BackendTaskSuccessResult`), NOT
egui_kittest. New pure helpers (see TC-QRFUND-14) should be extracted so the
guard is testable without a live screen.
- No timeout exists (only `request_repaint_after(1s)`); "never errors while
waiting" is structural, assert absence of error banner.
- `step`/`funding_address` init to `ChooseFundingMethod`/`None` on construction →
waiting state does **not** survive reload.

## 1. State machine

**TC-QRFUND-01** — Happy path progression. Choose "Receive a new deposit" →
`WaitingOnFunds`; inject a `ReceivedAvailableUTXOTransaction` paying the shown
address ≥ minimum → `FundsReceived`; confirm amount → dispatch `FundWithWallet`,
step = `WaitingForAssetLock`; inject asset-lock result → `WaitingForPlatformAcceptance`;
inject acceptance → `Success`. Expected: exact ordered transitions, no skipped
state. Trace: journey 1–6. *Automatable (unit).*

**TC-QRFUND-02** — Dead-arm reactivation is non-regressive. With the four
already-live methods, `UseUnusedAssetLock`/`UseWalletBalance`/`UsePlatformAddress`
and `NoSelection` still reach their existing steps unchanged after the new
variant is added. Expected: existing transition table intact. Trace: journey 1.
*Automatable (unit — extend existing `funding_common` tests).*

**TC-QRFUND-03** — New `FundingMethod` variant forces copy decisions. The
existing exhaustive tests `display_is_jargon_free_for_every_variant` and
`top_up_label_differs_only_for_asset_lock` must be extended to include the new
variant. Expected: variant has a jargon-free `Display` label AND a `top_up_label`;
compile-time exhaustiveness prevents a silent `Debug` fallback. Trace: journey 1;
§8. *Automatable (unit).*

## 2. Deposit detection

**TC-QRFUND-04** — Single deposit ≥ minimum advances. `ReceivedAvailableUTXOTransaction`
with one output to the shown address, amount ≥ minimum → `FundsReceived`. Trace:
journey 2,4. *Automatable (unit).*

**TC-QRFUND-05** — Multiple partial deposits accumulate. Two sub-minimum deposits
to the shown address: after the first, step stays `WaitingOnFunds` and the running
total reflects deposit 1; after the second (cumulative ≥ minimum) → `FundsReceived`.
Expected: running total = sum; advance only on crossing minimum. Trace: journey 4.
*Automatable (unit) — but confirm cumulative balance source (SPV spendable) is
readable without live sync; the accumulation display itself needs **manual**
verification with real testnet deposits.*

**TC-QRFUND-06** — Deposit to a DIFFERENT address must NOT advance.
`ReceivedAvailableUTXOTransaction` whose output address ≠ `funding_address` →
step remains `WaitingOnFunds`, no total change. Trace: journey 2 (guard).
*Automatable (unit).* Encodes the single-address-equality correction.

**TC-QRFUND-07** — Guard scoped to active method/state. Inject a matching
`ReceivedAvailableUTXOTransaction` while the screen is in `ReadyToCreate` (a
different funding method selected) or `ChooseFundingMethod`. Expected: NO
spurious advance to `FundsReceived` — the match arm fires only when
step == `WaitingOnFunds`. Trace: journey 2 (guard). *Automatable (unit).*

## 3. Amount pre-fill

**TC-QRFUND-08** — Fee-reserve cap on pre-fill. On `FundsReceived`, the amount
field pre-fills `max_amount_after_fee_reserve(spendable_duffs, estimated_fee)`.
Expected: equals received-spendable-minus-fee; saturates to 0 when fee exceeds
balance. Trace: journey 5. *Automatable (unit — pure fn already tested; add a
test asserting the FundsReceived path calls it with received balance).*

**TC-QRFUND-09** — User may edit but not exceed received balance. Editing the
field is allowed; a value above spendable is rejected/clamped by the existing
`AmountInput` validation. Expected: confirm disabled / value clamped when
over-max. Trace: journey 5. *Automatable (kittest for the widget; unit for the
clamp rule).*

## 4. Never-trap

**TC-QRFUND-10** — "Choose a different funding method" reachable from every
waiting sub-state. From `WaitingOnFunds` and `FundsReceived`, the affordance is
present and returns to `ChooseFundingMethod` / `NoSelection`, clearing
`funding_address`. Expected: no dead end; no error banner on back-out. Trace:
journey 3. *Automatable (kittest — assert control exists & click resets step;
also unit for the reset helper).*

## 5. Failure / edge paths

**TC-QRFUND-11** — Build/broadcast failure resets safely. From `WaitingForAssetLock`,
inject a `FundWithWallet` failure `TaskError`. Expected: step resets to
`ReadyToCreate`; banner is a **typed** `TaskError` (not a string literal) stating
funds are safe in the wallet; [TOPUP] reuses existing 481–484 reset, [REG] the
1222 reset. Trace: journey 7. *Automatable (unit).*

**TC-QRFUND-12** — Platform rejection recoverable. From `WaitingForPlatformAcceptance`,
inject rejection. Expected: reset to `ReadyToCreate`, banner points user to the
existing `UseUnusedAssetLock` recovery path; funds not lost. Trace: journey 7.
*Automatable (unit) + **manual** confirm the asset lock is actually reusable on
testnet.*

**TC-QRFUND-13** — Wallet-locked gate unchanged. With a locked wallet, the
lock/secret gate fires BEFORE any Create dispatch, regardless of the new method.
Expected: identical gate behavior to existing methods. Trace: journey 7.
*Automatable (kittest — secret-prompt path) — see `tests/kittest/secret_prompt.rs`.*

**TC-QRFUND-14** — Reload while `WaitingOnFunds` resets gracefully. Reconstruct
the screen (simulating restart). Expected: step = `ChooseFundingMethod`,
`funding_address = None`, no crash; any deposit already sent remains in wallet
balance and is later usable via `UseWalletBalance`/`UseUnusedAssetLock`. This is
the SAME persistence model as the existing `WaitingFor*` states. Trace: journey
7 / §5. *Automatable (unit for the reset; **manual** for the on-chain-funds-safe
claim).*

**TC-QRFUND-15** — No error while idle-waiting. Remain in `WaitingOnFunds` with no
deposit across many frames. Expected: no error/timeout banner ever set; only
repaint scheduling. Trace: journey 3. *Automatable (unit — assert no banner).*

## 6. Register vs Top-Up parity — divergences to encode

- Labels: [REG] uses `Display`, [TOPUP] uses `top_up_label()` — new variant must
supply BOTH (TC-QRFUND-03).
- [TOPUP] operates on an existing identity (has current balance/context); [REG]
has none. Minimum-amount hint copy may differ; detection + state machine are
identical. Flag any wording that assumes "new identity" in the shared copy.
- Otherwise detection, cap, and never-trap logic must be identical; a shared
helper (extracted per TC-QRFUND-14) keeps them so.

## 7. Regression guard (existing methods — minimal re-run)

**TC-QRFUND-16** — Re-run existing `funding_common` unit suite (all
`default_funding_state`, `funding_method_after_switch`, label, and
`max_amount_after_fee_reserve` tests) unchanged-green. **TC-QRFUND-17** — Smoke:
each of `UseUnusedAssetLock`, `UseWalletBalance`, `UsePlatformAddress` still
selects → `ReadyToCreate` and dispatches its existing backend variant. *Automatable
(unit + existing kittest render smoke).*

## 8. i18n/UX string spot-check (flag only — do not rewrite)

Diziet's draft copy uses named placeholders (`{minimum_amount}`, `{received_amount}`)
and complete sentences — **passes** the CLAUDE.md convention on inspection. One
flag for the dev-plan stage: "Received {received_amount} so far. Waiting for at
least {minimum_amount}." is two sentences sharing a unit — fine, but ensure
`{received_amount}`/`{minimum_amount}` are pre-formatted amount strings (with unit),
not bare numbers, so no fragment concatenation leaks in. *Manual/review-time.*

## Automation summary

- **Unit (`display_task_result` + pure helpers):** 01–08, 11, 12, 14, 15, 16, 17
— the core state-machine and detection coverage. Requires extracting the
`WaitingOnFunds` guard into a testable helper.
- **kittest (widget/nav):** 09, 10, 13, 17 (render smoke).
- **Manual live-testnet only (coordinator's GUI pass):** real QR scan + deposit
arrival (05 accumulation, 12 asset-lock reuse, 14 funds-safe-on-chain). These
cannot be verified without funding a real address.
Loading
Loading