Skip to content

[#455] Task 1: create_action broadcast-configuration guard + BroadcastQueue#broadcast_enabled? #456

@sgbett

Description

@sgbett

Part of #455.

Summary

Add validate_broadcast_configuration! to WalletClient and introduce BroadcastQueue#broadcast_enabled? so the new guard works with queue-embedded broadcasters (the documented SolidQueue pattern).

When create_action is called with synchronous broadcast semantics (the default — accept_delayed_broadcast: false, no no_send, no send_with) AND no broadcaster is available anywhere in the wiring (neither directly on WalletClient nor inside @broadcast_queue), raise BSV::Wallet::WalletError before any storage writes occur.

This closes the silent-failure path that caused the x402-rack phantom-payment incident (#148).

Design decisions (resolved)

  1. Delegate broadcast availability to the queue. Add broadcast_enabled? to the BroadcastQueue module (default false). Override in InlineQueue and SolidQueueAdapter to return !!@broadcaster. WalletClient#broadcast_enabled? (currently lines ~98-100) delegates to @broadcast_queue.broadcast_enabled?. validate_broadcast_configuration! uses the queue's check, not @broadcaster directly.

    • Why: WalletClient.new(..., broadcast_queue: SolidQueueAdapter.new(..., broadcaster: arc)) passes broadcaster: nil to WalletClient while the queue has a real broadcaster. This is the documented SolidQueue integration pattern (gem/bsv-wallet-postgres/README.md:44-48). Delegating to the queue keeps this pattern working.
  2. Also validate at sign_action. Run validate_broadcast_configuration!(merged_args) at the top of sign_action (after its own validation), not only at create_action.

    • Why: sign_action merges its own options over the create-time args. A caller can omit no_send at create time (passes validation), receive a signable tx, then flip no_send: false at sign time with no broadcaster configured. Validating only at create_action entry misses this flip case. Same fail-loud principle applies: refuse before storage is written.

Scope

  • gem/bsv-wallet/lib/bsv/wallet_interface/wallet_client.rb
    • create_action (line ~122)
    • sign_action (line ~183)
    • broadcast_enabled? (line ~98)
    • New private method validate_broadcast_configuration!(args)
  • gem/bsv-wallet/lib/bsv/wallet_interface/broadcast_queue.rb — add broadcast_enabled? (default false)
  • gem/bsv-wallet/lib/bsv/wallet_interface/inline_queue.rb — override broadcast_enabled?!!@broadcaster
  • gem/bsv-wallet-postgres/lib/bsv/wallet_postgres/solid_queue_adapter.rb — override broadcast_enabled?!!@broadcaster

Error message

WalletError: create_action requires a broadcaster for on-chain broadcast.
Pass broadcaster: BSV::Network::ARC.default to WalletClient.new,
or options: { no_send: true } to build a transaction without broadcasting.

(Same message from sign_action entry; consistent wording aids diagnosis.)

Acceptance criteria

  • WalletClient.new(key, storage: store).create_action(valid_args) raises WalletError with the documented message.
  • WalletClient.new(key, storage: store).create_action(valid_args.merge(options: { no_send: true })) succeeds (no raise).
  • WalletClient.new(key, storage: store, broadcaster: arc).create_action(valid_args) does not raise.
  • WalletClient.new(key, storage: store, broadcast_queue: solid_queue_with_broadcaster).create_action(valid_args) does not raise.
  • sign_action with no_send: true merged in at sign time does not raise (even if no broadcaster).
  • sign_action flipping to no_send: false at sign time with no broadcaster raises WalletError.
  • Validation fires BEFORE store_transaction, store_action, lock_utxos — no storage side effects on misconfig (verified via storage.find_actions returning empty after rescue).
  • send_with-only path continues to raise at line ~135 with its specific message (unchanged).
  • BroadcastQueue#broadcast_enabled? defaults to false; subclass overrides work.
  • WalletClient#broadcast_enabled? delegates to the queue.

Edge cases

  • args[:options] absent entirely (nil, not {}) — dig(:options, :no_send) handles cleanly.
  • accept_delayed_broadcast: true with no broadcaster AND no_send: false — still raises. Delayed broadcast without a broadcaster was the silent-fallback case; per HLR that is now blocked.
  • send_with: [...] with no broadcaster — the existing raise at line ~135 still fires. Validation passes first if no_send: true is set; otherwise the new validation raises before reaching line 135. Either error is acceptable.
  • broadcaster: nil explicitly on WalletClient.new but queue has a broadcaster — must pass.

Test scenarios

  • raises WalletError when no broadcaster and no no_send option
  • raises WalletError when no broadcaster and accept_delayed_broadcast: true and no no_send
  • does not raise when no_send: true with no broadcaster
  • does not raise when broadcaster present on WalletClient
  • does not raise when broadcast_queue has embedded broadcaster but WalletClient @broadcaster is nil
  • raises before any storage writes occur (verify storage is untouched after rescue)
  • sign_action: raises when flipping no_send false with no broadcaster
  • sign_action: does not raise when no_send true remains true through sign
  • BroadcastQueue.new.broadcast_enabled? returns false
  • InlineQueue.new(...).broadcast_enabled? reflects @broadcaster presence
  • SolidQueueAdapter.new(...).broadcast_enabled? reflects @broadcaster presence

Files modified

  • gem/bsv-wallet/lib/bsv/wallet_interface/wallet_client.rb
  • gem/bsv-wallet/lib/bsv/wallet_interface/broadcast_queue.rb
  • gem/bsv-wallet/lib/bsv/wallet_interface/inline_queue.rb
  • gem/bsv-wallet-postgres/lib/bsv/wallet_postgres/solid_queue_adapter.rb
  • Corresponding specs (Task 5 handles the spec edits; this task adds the new guard-specific specs)

Sequencing

Land first. Task 2 overrides broadcast_enabled? on InlineQueue; this task introduces the interface.

Metadata

Metadata

Assignees

No one assigned

    Labels

    gem:walletbsv-wallet companion gemlayer:walletBSV::Wallet moduletaskImplementation task

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions