|
| 1 | +# Plan: HLR #455 — Align action status with reference SDK; fail loudly on broadcast misconfiguration |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +`bsv-wallet`'s `create_action` and `internalize_action` collapse distinct states into `'completed'`, leading consumers to believe transactions are on-chain when they may not be. This caused phantom-payment bugs in x402-rack (#148, #158) and x402-doom (#196). |
| 6 | + |
| 7 | +The TS reference SDK (wallet-toolbox) distinguishes `'nosend'`, `'unsent'`, `'sending'`, `'unmined'`, `'unproven'`, `'completed'`, `'failed'`. `'completed'` means a merkle proof is on file. Ruby currently uses `'completed'` for "broadcast succeeded" (wrong) and "no broadcaster, nothing on chain" (also wrong, silently). |
| 8 | + |
| 9 | +## Current status-setting sites |
| 10 | + |
| 11 | +| Site | Current | Fix | |
| 12 | +|------|---------|-----| |
| 13 | +| `auto_fund_and_create` `no_send: true` (wallet_client.rb:814) | `'nosend'` | ✓ keep | |
| 14 | +| `auto_fund_and_create` default (wallet_client.rb:834) | `'pending'` (pre-queue) | ✓ keep | |
| 15 | +| `finalize_action` (wallet_client.rb:1441-1448) | `'nosend'` or `'pending'` | ✓ keep | |
| 16 | +| `InlineQueue#broadcast_and_promote` success (inline_queue.rb:100) | `'completed'` | → `'unproven'` | |
| 17 | +| `InlineQueue#broadcast_and_promote` failure (inline_queue.rb:89) | `'failed'` | ✓ keep | |
| 18 | +| `InlineQueue#promote_without_broadcast` default (inline_queue.rb:127) | `'completed'` silently | → **raise `WalletError`** (no broadcaster + sync broadcast requested) | |
| 19 | +| `InlineQueue#promote_without_broadcast` delayed (inline_queue.rb:127) | `'unproven'` | → `'unproven'` (sync fallback path per #380) | |
| 20 | +| `SolidQueueAdapter#process_job` success (solid_queue_adapter.rb:~292) | `'completed'` | → `'unproven'` | |
| 21 | +| `SolidQueueAdapter#process_job` failure | `'failed'` | ✓ keep | |
| 22 | +| `promote_no_send` success (wallet_client.rb:1416) | `'unproven'` | ✓ keep | |
| 23 | +| `internalize_action` via `store_action` (wallet_client.rb:306) | `'completed'` unconditional | → `'completed'` if BEEF has merkle proof for subject tx, else `'unproven'` | |
| 24 | +| `store_action` default (wallet_client.rb:1484) | `'completed'` | ✓ keep (tests only use default) | |
| 25 | + |
| 26 | +## Tasks |
| 27 | + |
| 28 | +### Task 1: Add validation in `create_action` |
| 29 | + |
| 30 | +**Modify** `gem/bsv-wallet/lib/bsv/wallet_interface/wallet_client.rb`. |
| 31 | + |
| 32 | +Add a new private method `validate_broadcast_configuration!(args)` called at the top of `create_action` (after `validate_create_action!`): |
| 33 | + |
| 34 | +```ruby |
| 35 | +def validate_broadcast_configuration!(args) |
| 36 | + no_send = args.dig(:options, :no_send) |
| 37 | + return if no_send |
| 38 | + return if @broadcaster |
| 39 | + |
| 40 | + raise WalletError, |
| 41 | + 'create_action requires a broadcaster for on-chain broadcast. ' \ |
| 42 | + 'Pass broadcaster: BSV::Network::ARC.default to WalletClient.new, ' \ |
| 43 | + 'or options: { no_send: true } to build a transaction without broadcasting.' |
| 44 | +end |
| 45 | +``` |
| 46 | + |
| 47 | +This fails loudly at the call site — not after state has been written to storage. |
| 48 | + |
| 49 | +### Task 2: Fix `InlineQueue` success status |
| 50 | + |
| 51 | +**Modify** `gem/bsv-wallet/lib/bsv/wallet_interface/inline_queue.rb`. |
| 52 | + |
| 53 | +- `broadcast_and_promote` success: change `promote(..., txid)` default from `'completed'` to `'unproven'`. Pass explicit `status: 'unproven'`. |
| 54 | +- `promote_without_broadcast`: remove the silent-`'completed'` branch. This method is now reached only via the `accept_delayed_broadcast: true` + no-broadcaster fallback (per #380 design). Status stays `'unproven'`. If somehow called without `accept_delayed_broadcast`, raise `WalletError` — a defensive guard since Task 1 should catch this upstream. |
| 55 | +- Update docstrings: `'completed'` → `'unproven'` (on-chain but unproven); `'completed'` only after proof monitoring promotes it (future work). |
| 56 | + |
| 57 | +### Task 3: Fix `SolidQueueAdapter` success status |
| 58 | + |
| 59 | +**Modify** `gem/bsv-wallet-postgres/lib/bsv/wallet_postgres/solid_queue_adapter.rb`. |
| 60 | + |
| 61 | +- `process_job` success path: `promote(..., txid)` should use `status: 'unproven'` instead of `'completed'`. |
| 62 | +- Update docstring. |
| 63 | + |
| 64 | +### Task 4: Fix `internalize_action` to check merkle proof |
| 65 | + |
| 66 | +**Modify** `gem/bsv-wallet/lib/bsv/wallet_interface/wallet_client.rb` `internalize_action` (line ~267) and/or `store_proofs_from_beef` / `extract_subject_transaction` / `find_by_subject_txid`. |
| 67 | + |
| 68 | +- Determine whether the BEEF contains a merkle proof (BUMP) for the subject transaction. |
| 69 | +- If yes → status = `'completed'` |
| 70 | +- If no → status = `'unproven'` |
| 71 | +- Pass explicit status to `store_action` |
| 72 | + |
| 73 | +Mirrors TS `storage/methods/internalizeAction.ts:301`: |
| 74 | +```typescript |
| 75 | +const status: TransactionStatus = provenTx ? 'completed' : 'unproven' |
| 76 | +``` |
| 77 | + |
| 78 | +### Task 5: Update specs |
| 79 | + |
| 80 | +**Modify** existing specs to reflect the new status values. Files identified: |
| 81 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/wallet_client_spec.rb` (2 sites) |
| 82 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/wallet_client_auto_fund_spec.rb` (4 sites) |
| 83 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/inline_queue_spec.rb` (5 sites) |
| 84 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/broadcast_rollback_spec.rb` |
| 85 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/auto_funding_spec.rb` |
| 86 | +- `gem/bsv-wallet/spec/bsv/wallet_interface/wire/serializer_spec.rb` |
| 87 | +- `gem/bsv-wallet-postgres/spec/bsv/wallet_postgres/solid_queue_adapter_spec.rb` |
| 88 | + |
| 89 | +Strategy: global search-and-replace of `'completed'` → `'unproven'` in test expectations where the asserted state is post-broadcast, then review each hit manually to catch exceptions (e.g. `internalize_action` tests with proven BEEFs should stay `'completed'`). |
| 90 | + |
| 91 | +**Add** new specs: |
| 92 | +- `create_action` raises `WalletError` when no broadcaster AND `no_send: false` (default) |
| 93 | +- `create_action` succeeds with `no_send: true` and no broadcaster → status `'nosend'` |
| 94 | +- `create_action` with broadcaster → status `'unproven'` on success |
| 95 | +- `internalize_action` with proven BEEF → `'completed'` |
| 96 | +- `internalize_action` with unproven BEEF → `'unproven'` |
| 97 | +- `SolidQueueAdapter` worker success → status `'unproven'` |
| 98 | + |
| 99 | +### Task 6: Documentation |
| 100 | + |
| 101 | +- **CHANGELOG.md** for `bsv-wallet`: document as breaking change for 0.8.0 with migration notes |
| 102 | +- **gem/bsv-wallet/README.md**: update status meanings table |
| 103 | +- **docs/gems/wallet.md**: add "Status values" section |
| 104 | +- **CLAUDE.md**: no changes needed (general development guidance is already correct) |
| 105 | + |
| 106 | +### Task 7: Downstream coordination |
| 107 | + |
| 108 | +- **bsv-attest**: verify `Attest.publish` callers don't assert `'completed'` — confirmed no assertions, no change needed. |
| 109 | +- **bsv-wallet-postgres**: no schema change (status is TEXT). Bump dependency floor to match new wallet version. |
| 110 | +- **x402-rack, x402-doom**: will need guidance — `'unproven'` is the new "success" state for fresh broadcasts. Document this in the bsv-wallet 0.8.0 release notes. |
| 111 | + |
| 112 | +## Critical files |
| 113 | + |
| 114 | +| File | Action | |
| 115 | +|------|--------| |
| 116 | +| `gem/bsv-wallet/lib/bsv/wallet_interface/wallet_client.rb` | Modify — add `validate_broadcast_configuration!`, fix internalize status logic | |
| 117 | +| `gem/bsv-wallet/lib/bsv/wallet_interface/inline_queue.rb` | Modify — success status + remove silent fallback | |
| 118 | +| `gem/bsv-wallet-postgres/lib/bsv/wallet_postgres/solid_queue_adapter.rb` | Modify — success status | |
| 119 | +| `gem/bsv-wallet/CHANGELOG.md` | Update for breaking change | |
| 120 | +| `gem/bsv-wallet/README.md` | Update status table | |
| 121 | +| `docs/gems/wallet.md` | Add status values section | |
| 122 | + |
| 123 | +## Reference files (read-only) |
| 124 | + |
| 125 | +| File | Why | |
| 126 | +|------|-----| |
| 127 | +| `wallet-toolbox/src/storage/methods/processAction.ts` | Status state machine reference | |
| 128 | +| `wallet-toolbox/src/storage/methods/internalizeAction.ts:301` | Proven vs unproven decision | |
| 129 | +| `wallet-toolbox/src/signer/methods/processAction.ts:147-155` | SendWithResult status mapping | |
| 130 | + |
| 131 | +## Verification |
| 132 | + |
| 133 | +```bash |
| 134 | +cd /opt/ruby/bsv-ruby-sdk |
| 135 | +bundle exec rake spec:wallet # wallet specs |
| 136 | +bundle exec rake spec:wallet_postgres # postgres specs (SolidQueueAdapter) |
| 137 | +bundle exec rake # full suite |
| 138 | +bundle exec rubocop # lint |
| 139 | +``` |
| 140 | + |
| 141 | +Manual verification: |
| 142 | +1. Construct `WalletClient.new(key)` (no broadcaster) and call `create_action` with default options → expect `WalletError` |
| 143 | +2. Same + `options: { no_send: true }` → expect success with status `'nosend'` |
| 144 | +3. Construct `WalletClient.new(key, broadcaster: ARC.default)` and call `create_action` → expect status `'unproven'` on success |
| 145 | +4. Internalize a BEEF with merkle proof → action status `'completed'` |
| 146 | +5. Internalize a BEEF without merkle proof → action status `'unproven'` |
| 147 | + |
| 148 | +## Sequencing |
| 149 | + |
| 150 | +``` |
| 151 | +Task 1 (create_action guard) ──→ Task 5 (specs update) |
| 152 | +Task 2 (InlineQueue) ──→ Task 5 |
| 153 | +Task 3 (SolidQueueAdapter) ──→ Task 5 |
| 154 | +Task 4 (internalize_action) ──→ Task 5 |
| 155 | +Task 6 (docs) ── independent |
| 156 | +Task 7 (downstream floor) ── last, requires wallet version bump |
| 157 | +``` |
| 158 | + |
| 159 | +Tasks 1-4 can be done in parallel (touch different code paths). Task 5 must wait for all of them. Tasks 6-7 are independent bookkeeping. |
0 commit comments