# Module BSV::Wallet::Interface::Store <a id="module-BSV-Wallet-Interface-Store"></a>

Persistence interface for wallet state.

Methods mirror the schema's phase model — the action lifecycle is create (lock
inputs) → sign (attach wtxid) → promote (write outputs). Status is derived
from structural state, never stored directly.

All methods receive and return plain hashes/arrays — no ORM objects leak
through the interface boundary.

## Public Instance Methods
### `abort_action(action_id:)` <a id="method-i-abort_action"></a> <a id="abort_action-instance_method"></a>
Abort an action. CASCADE deletes inputs, releasing locked UTXOs. Only valid
for unsigned actions (wtxid IS NULL).
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]

### `broadcast_provider_for(wtxid:)` <a id="method-i-broadcast_provider_for"></a> <a id="broadcast_provider_for-instance_method"></a>
Read broadcast affinity: the provider name for a given wtxid.

Resolves the wtxid to its action then reads <code>broadcasts.provider</code>.
Returns `nil` when no action matches, no broadcasts row exists, or the column
is NULL (no affinity yet).
- **@param** `wtxid` [String] 32-byte binary wire-order wtxid
- **@raise** [NotImplementedError]
- **@return** [String, nil] provider name, or +nil+ if no affinity recorded

### `broadcast_status(action_id:)` <a id="method-i-broadcast_status"></a> <a id="broadcast_status-instance_method"></a>
Query broadcast status for an action.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Hash, nil] broadcast data or nil if no broadcast exists

### `child_actions_of(action_id:)` <a id="method-i-child_actions_of"></a> <a id="child_actions_of-instance_method"></a>
Return action_ids of every action whose inputs spend an output of `action_id`.
The forward-walk for the reject_action cascade.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>]

### `complete_internal_action(action_id:, wtxid:, raw_tx:, sign_outputs:, change_outputs:, promote_outputs:)` <a id="method-i-complete_internal_action"></a> <a id="complete_internal_action-instance_method"></a>
Phases 2–4 (internal/no_send) in one atomic transition: sign, save proof,
promote outputs, and make change spendable. Closes the crash-windows a
separate-call sequence leaves (#327 sign→promote, #328 promote→change). Used
by the synchronous internal completion paths (no broadcast to wait for).
- **@param** `action_id` [Integer]
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@param** `raw_tx` [String] signed raw transaction binary
- **@param** `sign_outputs` [Array<Hash>] outputs for the sign step (empty on
the internal path — real outputs are promoted, not staged)
- **@param** `change_outputs` [Array<Hash>] change outputs for the sign step
- **@param** `promote_outputs` [Array<Hash>] output specs to promote
- **@raise** [NotImplementedError]

### `create_action(action:, inputs: = [])` <a id="method-i-create_action"></a> <a id="create_action-instance_method"></a>
Phase 1: Create an action and lock inputs atomically.

Inserts an action row and input rows in one transaction. Input locking uses
INSERT ON CONFLICT — concurrent callers competing for the same outputs are
resolved by the database.
- **@param** `action` [Hash] :description, :broadcast (:delayed/:inline/:none),
:input_beef
- **@param** `inputs` [Array<Hash>] each: :output_id, :vin, :nsequence, :description
- **@raise** [InsufficientFundsError] if not enough inputs could be locked
- **@return** [Hash] the created action with :id, :reference

### `delete_certificate(type:, serial_number:, certifier:)` <a id="method-i-delete_certificate"></a> <a id="delete_certificate-instance_method"></a>
Soft-delete a certificate.
- **@raise** [NotImplementedError]

### `descendant_action_ids_of(action_ids:, max_depth: = 100)` <a id="method-i-descendant_action_ids_of"></a> <a id="descendant_action_ids_of-instance_method"></a>
Structural descent walk (HLR #516 Sub 6.2). Returns the set of
<code>actions.id</code> reachable from any `action_ids` seed by following the
+inputs.output_id → outputs.id → outputs.action_id+ edge transitively. Uses a
recursive CTE (+Sequel::Dataset#with_recursive+) so the walk executes in one
DB round-trip on both Postgres and SQLite. The seed IDs are included in the
returned set.

Coarse-clear semantics: the walk descends every structural descendant
regardless of whether that row's SPV walk went through the seed anchor.
Inferring the answer requires replaying +Tx#verify+, which defeats the cache.
Clearing more than needed forces a cheap re-verify on next reference; clearing
less than needed opens a silent double-spend acceptance window — the asymmetry
favours coarse.

Depth cap `max_depth` bounds the walk. 100 is the natural coinbase maturity
ceiling — anything past that is definitionally beyond any re-org's
coinbase-invalidation reach and terminating early is correct. The cap also
serves as the cycle guard.
- **@param** `action_ids` [Array<Integer>] seed +actions.id+ values
- **@param** `max_depth` [Integer] recursion depth cap (default 100)
- **@raise** [NotImplementedError]
- **@return** [Set<Integer>] seeds + all transitive descendants

### `find_action(id: = nil, wtxid: = nil, reference: = nil)` <a id="method-i-find_action"></a> <a id="find_action-instance_method"></a>
Find an action by id, wtxid, or reference.
- **@raise** [NotImplementedError]
- **@return** [Hash, nil]

### `find_block(height:)` <a id="method-i-find_block"></a> <a id="find_block-instance_method"></a>
Find a block header by height.
- **@param** `height` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Hash, nil] { height:, merkle_root:, block_hash: } or nil

### `find_or_create_basket(name:)` <a id="method-i-find_or_create_basket"></a> <a id="find_or_create_basket-instance_method"></a>
Find or create a basket by name. Returns the basket ID.
- **@raise** [NotImplementedError]

### `find_or_create_labels(names:)` <a id="method-i-find_or_create_labels"></a> <a id="find_or_create_labels-instance_method"></a>
Find or create labels by name. Returns an array of label IDs.
- **@raise** [NotImplementedError]

### `find_or_create_tags(names:)` <a id="method-i-find_or_create_tags"></a> <a id="find_or_create_tags-instance_method"></a>
Find or create tags by name. Returns an array of tag IDs.
- **@raise** [NotImplementedError]

### `find_output(id:)` <a id="method-i-find_output"></a> <a id="find_output-instance_method"></a>
Find an output by id.
- **@param** `id` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Hash, nil] output data including :id, :action_id, :satoshis, :vout, etc.

### `find_proof(wtxid:)` <a id="method-i-find_proof"></a> <a id="find_proof-instance_method"></a>
Retrieve a proof by wtxid.
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@raise** [NotImplementedError]
- **@return** [Hash, nil] proof data, or nil if not stored

### `find_spendable(satoshis:, basket: = nil, exclude: = [])` <a id="method-i-find_spendable"></a> <a id="find_spendable-instance_method"></a>
Find spendable outputs totalling at least the required satoshis. This is the
database-level query — the UTXOPool wraps this with a selection strategy for
higher tiers.
- **@param** `satoshis` [Integer] minimum total value needed
- **@param** `basket` [String, nil] optional basket filter
- **@param** `exclude` [Array<Integer>] output IDs to skip (e.g. from a failed lock attempt)
- **@raise** [NotImplementedError]
- **@return** [Array<Hash>] candidates: :id, :satoshis, :vout, :locking_script, :action_id

### `get_setting(key:)` <a id="method-i-get_setting"></a> <a id="get_setting-instance_method"></a>
Retrieve a setting value by key.
- **@raise** [NotImplementedError]
- **@return** [String, nil]

### `header_at(height:)` <a id="method-i-header_at"></a> <a id="header_at-instance_method"></a>
The raw 80-byte header at `height`, or `nil` when the height holds no row or
only a trusted-service (header-NULL) row. A non-nil return is the "this height
is locally PoW-validated" signal (#335).
- **@param** `height` [Integer]
- **@raise** [NotImplementedError]
- **@return** [String, nil] the 80 raw wire bytes, or nil

### `increment_broadcast_retry(action_id:)` <a id="method-i-increment_broadcast_retry"></a> <a id="increment_broadcast_retry-instance_method"></a>
Increment broadcasts.retry_count for an action. Called from the resolution
loop when reject_action raises CannotRejectInternalActionError -- the row
stays alive for the next polling cycle but the counter surfaces stuck rows for
dashboards.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]

### `invalidate_stale_anchors!(heights_to_roots:)` <a id="method-i-invalidate_stale_anchors-21"></a> <a id="invalidate_stale_anchors!-instance_method"></a>
Anchor-liveness invalidation writer (HLR #516 Sub 6.1). Clears
`verified_at`/`verified_via`/`verifier_version` on any verified `tx_proofs`
row whose block sits at a supplied height but whose persisted `merkle_path`
folds to a root differing from the tracker's current view. Pure writer — no
chain_tracker dependency; <code>Engine::AnchorLivenessCache</code> resolves
the map.

A `nil` value in `heights_to_roots` marks the height as "unknown to the
tracker" (transient outage, sync gap) and is treated as a no-op — the trust
set must not decay under a network blip. Only a genuine root disagreement
invalidates.
- **@param** `heights_to_roots` [Hash{Integer => String, nil}] wire-order 32-byte binary current roots keyed by block height
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>] +actions.id+ of every action whose
proof was invalidated (Sub 6.2 walks the descendants)

### `invalidate_verification(action_ids:)` <a id="method-i-invalidate_verification"></a> <a id="invalidate_verification-instance_method"></a>
Shared row-clearing primitive for verification-cache invalidation (HLR #516
Sub 6.2). Clears `verified_at`, `verified_via` and `verifier_version` together
on every `tx_proofs` row whose backing action_id sits in `action_ids` AND
whose `verified_via` is currently non-NULL.

The +verified_via IS NOT NULL+ predicate is the security- specialist DoS
defence: an attacker can grow the descent set unboundedly by chaining
synthetic rows without verification marks; those rows lie on the descent WALK
but the UPDATE never touches them. The write cost stays bounded by "rows
carrying trust", never by "rows plausibly reachable".

Clearing all three columns in one UPDATE satisfies the
`verification_state_coherent` CHECK — a partial clear (+verified_at NULL+ with
`verified_via` still set) trips it and rolls the whole UPDATE back.
- **@param** `action_ids` [Array<Integer>, Set<Integer>]
- **@raise** [NotImplementedError]
- **@return** [Integer] number of rows whose verification state was
cleared

### `label_action(action_id:, label_ids:)` <a id="method-i-label_action"></a> <a id="label_action-instance_method"></a>
Attach labels to an action.
- **@raise** [NotImplementedError]

### `link_proof(action_id:, tx_proof_id:)` <a id="method-i-link_proof"></a> <a id="link_proof-instance_method"></a>
Link a merkle proof to an action (marks it as completed).
- **@param** `action_id` [Integer]
- **@param** `tx_proof_id` [Integer]
- **@raise** [NotImplementedError]

### `load_sse_cursor(token:)` <a id="method-i-load_sse_cursor"></a> <a id="load_sse_cursor-instance_method"></a>
Load the high-water Last-Event-ID for an Arcade SSE callback token.

Returns `nil` for an unknown token, signalling the listener to connect without
a <code>Last-Event-ID</code> header (fresh stream).
- **@param** `token` [String] Arcade callbackToken value
- **@raise** [NotImplementedError]
- **@return** [Integer, nil] last successfully bus-pushed event id
(nanosecond timestamp per Arcade SSE), or +nil+ if never seen

### `lock_inputs(action_id:, inputs:)` <a id="method-i-lock_inputs"></a> <a id="lock_inputs-instance_method"></a>
Phase 1 (top-up): Append additional input rows to an existing action.

Locks each output by inserting an inputs row with ON CONFLICT DO NOTHING on
`output_id`. All-or-nothing: if any input fails to lock (another action
already owns the output), the whole batch is rolled back and 0 is returned.
Mirrors {#create_action}'s Phase 1 locking semantics so the engine's funding
loop can request additional UTXOs after the initial lock without
re-implementing the contention path.

An empty `inputs` array is a no-op and returns 0.
- **@param** `action_id` [Integer] target action (must exist)
- **@param** `inputs` [Array<Hash>] each: :output_id, :vin, :nsequence, :description
- **@raise** [NotImplementedError]
- **@return** [Integer] number of input rows locked (size of +inputs+ on
success, 0 on rollback or empty input)

### `mark_broadcast_attempted(action_id:)` <a id="method-i-mark_broadcast_attempted"></a> <a id="mark_broadcast_attempted-instance_method"></a>
Mark a broadcast row as attempted (stamp `broadcast_at`).

Idempotent: only stamps rows where +broadcast_at IS NULL+. A row that already
has a stamp keeps the original timestamp -- this lets the push and poll
discovery loops race safely on the same row.

Called by +Engine::Broadcast#submit+ in a committed transaction immediately
before the network call. A mid-POST crash therefore leaves the row with
+broadcast_at IS NOT NULL+ and +tx_status IS NULL+, which the poll loop
subsequently recovers via +GET /tx/{txid}+.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]

### `mark_transmission_acked(action_id:, counterparty:, wtxids: = [], acked_at: = Time.now)` <a id="method-i-mark_transmission_acked"></a> <a id="mark_transmission_acked-instance_method"></a>
Mark a transmission acknowledged by the peer and record the wtxids the BEEF
carried (two-phase, HLR #385: known-set writes live here, not in
record_transmission). Idempotent on re-ack —`acked_at` overwrites, the txid
batch is INSERT ON CONFLICT DO NOTHING. Delivery status is derived from
`acked_at` presence; there is no status column (principle of state).
- **@param** `action_id` [Integer]
- **@param** `counterparty` [String] BRC-43 identity pubkey (66-char hex)
- **@param** `wtxids` [Array<String>] wire-order binary wtxids carried by the BEEF
- **@param** `acked_at` [Time] ack timestamp
- **@raise** [NotImplementedError]
- **@return** [Integer, nil] transmission id, or +nil+ if no matching row

### `mark_verified(wtxid:, via:, at_time: = nil)` <a id="method-i-mark_verified"></a> <a id="mark_verified-instance_method"></a>
Record a successful verification of the given wtxid. Idempotent under repeated
calls with the same `via`; upgrades permitted per the monotonicity rule (see
below).

Single atomic UPDATE with a monotonic predicate so an older writer cannot
clobber a newer `verifier_version` stamp. Silent no-op when the row does not
exist (the tx has not been persisted via `save_proof` yet). Callers verifying
a subject/ancestor should ensure the proof rows are persisted first.
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@param** `via` [String] one of the +VERIFIED_VIA_*+ constants on
+Store::Models::TxProof+
- **@param** `at_time` [Time, nil] verification timestamp; defaults to now
- **@raise** [NotImplementedError]
- **@return** [Integer] number of rows updated (0 if no matching proof,
0 also if the row's existing +verifier_version+ is higher — no
clobber)

### `mark_verified_batch(wtxids:, via:, at_time: = nil)` <a id="method-i-mark_verified_batch"></a> <a id="mark_verified_batch-instance_method"></a>
Batch form of <code>#mark_verified</code> — a single set-based UPDATE for the
whole ingress ancestry. Empty input is a cheap no-op (no DB round-trip).
Chunks internally at 10k to stay under Postgres's bind-parameter limit.

All rows in the batch get the same `via` / `at_time` (typical use:
BeefImporter marks every walked wtxid as +'spv'+ in one call).
- **@param** `wtxids` [Array<String>] 32-byte binary wtxids
- **@param** `via` [String] see +#mark_verified+
- **@param** `at_time` [Time, nil] see +#mark_verified+
- **@raise** [NotImplementedError]
- **@return** [Integer] number of rows updated across all chunks

### `max_block_height()` <a id="method-i-max_block_height"></a> <a id="max_block_height-instance_method"></a>
Return the maximum block height stored, or nil if no blocks.
- **@raise** [NotImplementedError]
- **@return** [Integer, nil]

### `max_verifier_version_seen()` <a id="method-i-max_verifier_version_seen"></a> <a id="max_verifier_version_seen-instance_method"></a>
Highest `verifier_version` observed in `tx_proofs`. Used at boot to refuse a
downgraded binary — a wallet with rows stamped at version N must not run under
code that only supports version < N. Returns `nil` when no rows have been
marked verified.
- **@raise** [NotImplementedError]
- **@return** [Integer, nil]

### `pending_proofs(limit: = 100)` <a id="method-i-pending_proofs"></a> <a id="pending_proofs-instance_method"></a>
Query actions needing proof acquisition.

Returns signed actions (wtxid set) that have no proof yet (tx_proof_id nil),
excluding internal actions (broadcast_intent: 'none') which receive proofs via
other channels.
- **@param** `limit` [Integer] maximum records to return
- **@raise** [NotImplementedError]
- **@return** [Array<Hash>] action hashes (same shape as find_action)

### `pending_resolutions(limit: = 100)` <a id="method-i-pending_resolutions"></a> <a id="pending_resolutions-instance_method"></a>
Query broadcasts the resolution loop should poll to terminal.

Returns broadcasts that have been attempted (+broadcast_at IS NOT NULL+) and
whose `tx_status` is not in the terminal set (or is still NULL, which signals
an in-flight or crash-recovery row). Under the post-T2/T3 invariant,
`broadcast_at` is stamped pre-POST in the same committed transaction, so this
query is purely binary: any attempted row not yet at a terminal status is the
daemon's responsibility to poll regardless of age. No staleness predicate.
- **@param** `limit` [Integer] maximum records to return
- **@raise** [NotImplementedError]
- **@return** [Array<Hash>] broadcast data hashes

### `pending_submissions(limit: = 100)` <a id="method-i-pending_submissions"></a> <a id="pending_submissions-instance_method"></a>
Query broadcasts queued for an initial ARC submission.

Returns broadcasts that have never been attempted (+broadcast_at IS NULL+).
Single-table scan, no join to actions. Under the #184 invariant a broadcasts
row implies a signed action, so no time math or staleness predicate is needed
-- a row in this state is by definition the daemon's responsibility to push.
- **@param** `limit` [Integer] maximum records to return
- **@raise** [NotImplementedError]
- **@return** [Array<Hash>] broadcast data hashes

### `promote_action(action_id:, outputs:)` <a id="method-i-promote_action"></a> <a id="promote_action-instance_method"></a>
Internal-path Phase 4: Write an action's outputs as canonical.

Records the per-action `promotions` row (intent='none', #307), then inserts
output rows, spendable entries for wallet-owned outputs, basket memberships,
output details, and tags in one transaction. Used by paths where the action's
broadcast intent is +'none'+ — incoming actions, root UTXO imports, wbikd —so
outputs join the canonical UTXO set immediately.
- **@param** `action_id` [Integer]
- **@param** `outputs` [Array<Hash>] each: :satoshis, :vout, :locking_script,
:derivation_prefix, :derivation_suffix, :sender_identity_key,
:basket, :tags, :description, :custom_instructions, :change
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>] output IDs in the same order as outputs

### `promote_action_outputs(action_id:, authorising_status:)` <a id="method-i-promote_action_outputs"></a> <a id="promote_action_outputs-instance_method"></a>
Send-path Phase 4: Promote an action by recording its promotions row.

Inserts the per-action `promotions` row (the canonical-state fact, #307) with
the given `authorising_status`, then creates spendable rows for wallet-owned
outputs (caller outputs with derivation parameters, root outputs, or change
outputs). The promotions row's composite FK to +broadcasts(action_id,
tx_status)+ means it can only be recorded while the broadcast holds that
(non-rejected) status. Idempotent — a second call finds the promotions row
present and no-ops.

Called when ARC accepts a broadcast (inline or via the daemon).
- **@param** `action_id` [Integer]
- **@param** `authorising_status` [String] the broadcast tx_status authorising
the promotion (the broadcasts row's current, non-rejected status)
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>] IDs of outputs made spendable (empty when
already promoted — idempotent guard)

### `promote_change_to_spendable(action_id:)` <a id="method-i-promote_change_to_spendable"></a> <a id="promote_change_to_spendable-instance_method"></a>
Promote change outputs to spendable for an action.

Creates spendable rows for change outputs that don't already have one. Called
after broadcast acceptance or in the no_send path.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]

### `proof_exists?(wtxid:)` <a id="method-i-proof_exists-3F"></a> <a id="proof_exists?-instance_method"></a>
Check whether a proof exists for a transaction.
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@raise** [NotImplementedError]
- **@return** [Boolean]

### `query_actions(labels:, label_query_mode: = :any, limit: = 10, offset: = 0, include_labels: = false, include_inputs: = false, include_input_locking_scripts: = false, include_input_unlocking_scripts: = false, include_outputs: = false, include_output_locking_scripts: = false)` <a id="method-i-query_actions"></a> <a id="query_actions-instance_method"></a>
Query actions by labels with pagination.
- **@raise** [NotImplementedError]
- **@return** [Hash] :total, :actions

### `query_certificates(certifiers:, types:, limit: = 10, offset: = 0)` <a id="method-i-query_certificates"></a> <a id="query_certificates-instance_method"></a>
Query certificates by certifiers and types.
- **@raise** [NotImplementedError]
- **@return** [Hash] :total, :certificates

### `query_change_output_vouts(action_id:)` <a id="method-i-query_change_output_vouts"></a> <a id="query_change_output_vouts-instance_method"></a>
Return vout positions of change outputs for an action.

Queries outputs joined to output_details where change is true. Used by
Engine#query_change_outpoints for the no_send_change response.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>] vout positions

### `query_outputs(basket:, tags: = nil, tag_query_mode: = :any, limit: = 10, offset: = 0, include_locking_scripts: = false, include_custom_instructions: = false, include_tags: = false, include_labels: = false)` <a id="method-i-query_outputs"></a> <a id="query_outputs-instance_method"></a>
Query spendable outputs in a basket with optional tag filtering.

Each output hash includes :id, :action_id, :satoshis, :vout, :spendable.
- **@raise** [NotImplementedError]
- **@return** [Hash] :total, :outputs

### `reap_action(action_id:)` <a id="method-i-reap_action"></a> <a id="reap_action-instance_method"></a>
Reclaim one abandoned action: tear it down and delete it in a single
transaction, cascading inputs to release locked UTXOs. Re-validates
reapability under a row lock; returns false if no longer reapable.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Boolean] true if reclaimed, false if no longer reapable

### `record_block_header(height:, merkle_root:, block_hash: = nil, header: = nil)` <a id="method-i-record_block_header"></a> <a id="record_block_header-instance_method"></a>
Upsert a block header record.

Accepts merkle_root and block_hash as either 32-byte binary or 64-char hex
strings. Already-binary values (Encoding::BINARY) are stored as-is; hex
strings are packed to binary.

When `header` (the raw 80-byte PoW-validated header, #335) is given, the write
is append-or-reject: a validated row is never downgraded or overwritten, and a
competing header at an already-validated height raises
{CompetingBlockHeaderError}. When `header` is nil (the trusted-service path)
only merkle_root / block_hash are upserted and any existing validated row is
left untouched.
- **@param** `height` [Integer] block height
- **@param** `merkle_root` [String] 32-byte binary or 64-char hex string
- **@param** `block_hash` [String, nil] 32-byte binary or 64-char hex string
- **@param** `header` [String, nil] raw 80-byte validated header (wire order)
- **@raise** [NotImplementedError]

### `record_broadcast_provider(wtxid:, provider:)` <a id="method-i-record_broadcast_provider"></a> <a id="record_broadcast_provider-instance_method"></a>
Persist broadcast affinity: the provider that handled this wtxid.

Looks up the action by wtxid, then sets <code>broadcasts.provider</code> on
the matching broadcast row. Used by <code>BSV::Network::Broadcaster</code> to
record which provider accepted the broadcast so a fresh broadcaster (e.g.
after daemon restart) can re-route status queries to the same provider.

Idempotent: re-recording the same +(wtxid, provider)+ is a no-op. A subsequent
call with a different provider overwrites the column (last-broadcaster wins).
No-op when no matching action or broadcast row exists (race with action
deletion).
- **@param** `wtxid` [String] 32-byte binary wire-order wtxid
- **@param** `provider` [String] provider name (e.g. +"GorillaPool"+)
- **@raise** [NotImplementedError]
- **@return** [Integer] number of rows updated (0 or 1)

### `record_broadcast_result(action_id:, tx_status:, arc_status: = nil, block_hash: = nil, block_height: = nil, merkle_path: = nil, extra_info: = nil, competing_txs: = nil)` <a id="method-i-record_broadcast_result"></a> <a id="record_broadcast_result-instance_method"></a>
Record a broadcast result from ARC or a callback event.

Find-or-creates a Broadcast record for the action, then updates status fields
atomically. Handles hex-to-binary decoding for block_hash and merkle_path, and
database-specific coercion for competing_txs.
- **@param** `action_id` [Integer]
- **@param** `tx_status` [String] e.g. 'SEEN_ON_NETWORK', 'MINED'
- **@param** `arc_status` [Integer, nil] HTTP status from ARC
- **@param** `block_hash` [String, nil] hex or binary block hash
- **@param** `block_height` [Integer, nil]
- **@param** `merkle_path` [String, nil] hex or binary merkle path
- **@param** `extra_info` [String, nil]
- **@param** `competing_txs` [Array<String>, nil]
- **@raise** [NotImplementedError]
- **@return** [Hash] updated broadcast data

### `record_transmission(action_id:, counterparty:)` <a id="method-i-record_transmission"></a> <a id="record_transmission-instance_method"></a>
Record a transmission of `action_id`'s BEEF to `counterparty`. Idempotent and
concurrency-safe at grain (action, peer): a re-transmit refreshes `updated_at`
and returns the same id. This does NOT populate `transmission_txids` — the
known-set is written only on ack (#mark_transmission_acked), two-phase, so a
failed delivery never trims the peer's next BEEF below verifiability.
- **@param** `action_id` [Integer]
- **@param** `counterparty` [String] BRC-43 identity pubkey (66-char hex)
- **@raise** [NotImplementedError]
- **@return** [Integer] transmission id

### `reject_action(action_id:)` <a id="method-i-reject_action"></a> <a id="reject_action-instance_method"></a>
Reject a broadcast action whose terminal outcome was REJECTED. Unwinds
speculatively-promoted outputs and cascades forward through any child action
that consumed this action's outputs. Single outer transaction; partial-cascade
failures roll back the entire walk.

Distinct from abort_action -- BRC-100 abortAction targets actions under
construction (pre-broadcast cancel, refuses on promoted outputs).
reject_action is for the post-broadcast rejection path where promotion was an
optimistic bet now contradicted by the network.

Raises <code>BSV::Wallet::CannotRejectInternalActionError</code> if the target
or any cascade descendant has broadcast_intent='none'. Internal-path actions
are not the domain of this method.

Raises <code>BSV::Wallet::CannotRejectAcceptedActionError</code> if the target
or any cascade descendant has a broadcast row whose tx_status is in
<code>BSV::Wallet::ArcStatus::ACCEPTED</code>. The network considers the tx
accepted; unwind would corrupt the wallet's view rather than recover it.
Operator investigation is the right response.
- **@param** `action_id` [Integer]
- **@raise** [NotImplementedError]

### `relinquish_output(output_id:)` <a id="method-i-relinquish_output"></a> <a id="relinquish_output-instance_method"></a>
Remove an output from the UTXO set and its basket. The output row stays in the
immutable log.
- **@param** `output_id` [Integer]
- **@raise** [NotImplementedError]

### `resolve_inputs_for_signing(action_id:)` <a id="method-i-resolve_inputs_for_signing"></a> <a id="resolve_inputs_for_signing-instance_method"></a>
Resolve the full context for each locked input of an action.

Joins inputs → outputs → actions (the action that **created** the output, not
the current action) to gather the source outpoint, satoshis, locking script,
and derivation parameters needed for transaction construction and signing.
- **@param** `action_id` [Integer]
- **@raise** [RuntimeError] if any source action has a nil wtxid
- **@return** [Array<Hash>] ordered by vin, each:
:vin, :sequence, :source_wtxid (32-byte binary, wire byte order),
:source_vout, :source_satoshis, :source_locking_script (binary),
:derivation_prefix, :derivation_suffix, :sender_identity_key

### `sanity_sweep_verified_anchors!(chain_tracker:, sample_size: = 100)` <a id="method-i-sanity_sweep_verified_anchors-21"></a> <a id="sanity_sweep_verified_anchors!-instance_method"></a>
Boot-time cache sanity sweep (HLR #516 Sub 6.3, failsafe-for- failsafe).
Samples up to `sample_size` `tx_proofs` rows that carry an +'spv'+ mark AND
have a `merkle_path` populated, then compares each row's computed root against
the `chain_tracker`'s current view. On divergence: log via
<code>BSV.logger.warn</code>. Does NOT invalidate — that's the per-verify-walk
path's job.

Env-gated by +BSV_WALLET_VERIFY_BOOT_SWEEP=1+; unset is a no-op (return 0).
Non-fatal on DB errors, tracker outages, and empty samples.
- **@param** `chain_tracker` [#known_roots_for_heights] tracker with
the batched root-lookup interface
- **@param** `sample_size` [Integer] upper bound on rows sampled
- **@raise** [NotImplementedError]
- **@return** [Integer] number of divergences observed (0 if disabled
or nothing to check)

### `save_certificate(certificate)` <a id="method-i-save_certificate"></a> <a id="save_certificate-instance_method"></a>
Persist a certificate with its fields.
- **@raise** [NotImplementedError]

### `save_proof(wtxid:, proof:)` <a id="method-i-save_proof"></a> <a id="save_proof-instance_method"></a>
Store a merkle proof for a transaction.

Upserts — if a proof already exists for this wtxid, updates it. Wraps the
multi-table write (TxProof + Block) in a single transaction.
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@param** `proof` [Hash] :height, :block_index, :merkle_path (binary),
:raw_tx (binary), :block_hash (binary), :merkle_root (binary)
- **@raise** [NotImplementedError]
- **@return** [Integer] the tx_proof ID

### `save_sse_cursor(token:, last_event_id:)` <a id="method-i-save_sse_cursor"></a> <a id="save_sse_cursor-instance_method"></a>
Persist the high-water Last-Event-ID for an Arcade SSE token.

Upsert keyed on `token`: a second save for the same token overwrites the
previous `last_event_id` rather than raising a PK violation. Used by the SSE
listener after each event has been handed off to the in-proc bus -- the cursor
reflects what has been pushed, so a reconnect resumes from the next frame.
- **@param** `token` [String] Arcade callbackToken value
- **@param** `last_event_id` [Integer] SSE id of the most recently
pushed event (nanosecond timestamp)
- **@raise** [NotImplementedError]

### `set_setting(key:, value:)` <a id="method-i-set_setting"></a> <a id="set_setting-instance_method"></a>
Set a setting value (upsert).
- **@raise** [NotImplementedError]

### `sign_action(action_id:, wtxid:, raw_tx:, outputs: = [], change_outputs: = [])` <a id="method-i-sign_action"></a> <a id="sign_action-instance_method"></a>
Phase 2: Attach wtxid and signed raw transaction to an action, atomically
queueing the broadcast.

Updates the action with `wtxid` and `raw_tx`, and (when
<code>actions.broadcast_intent</code> is not +'none'+) inserts the
corresponding `broadcasts` row in the same database transaction. The row
begins life with +broadcast_at IS NULL+ (queued, not yet attempted).

When `outputs` or `change_outputs` are present, writes the corresponding
output rows (with no promotions row yet) and their association rows
(output_details, output_baskets, output_tags) in the same transaction. No
spendable rows — promotion to the canonical UTXO set happens at Phase 4 via
{#promote_action_outputs} on broadcast acceptance. This ensures signing
failure produces zero orphan rows in the UTXO set.

Used by the real-signing paths (non-deferred `createAction` and BRC-100
`signAction`). The deferred `createAction` path calls {#stage_action} instead,
which does not touch `broadcasts`.
- **@param** `action_id` [Integer]
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@param** `raw_tx` [String] binary-encoded signed transaction
- **@param** `outputs` [Array<Hash>] optional caller-declared outputs to write
atomically with no promotions row yet. Each: :satoshis, :vout,
:locking_script, :spendable_intent (required — 'spendable' or
'none', stated explicitly per HLR #467 /
+docs/reference/intent-and-outcomes.md+; inferring from
derivation presence is no longer accepted),
:derivation_prefix, :derivation_suffix, :sender_identity_key,
:basket, :tags, :description, :custom_instructions
- **@param** `change_outputs` [Array<Hash>] optional change outputs to write
atomically with no promotions row yet. Each: :satoshis, :vout,
:locking_script, :derivation_prefix, :derivation_suffix,
:sender_identity_key, :basket (optional — when present, an
+output_baskets+ row is written routing this change output
into the named basket per HLR #436; absent leaves the change
unbasketed in the wallet's pool).
- **@raise** [NotImplementedError]

### `stage_action(action_id:, wtxid:, raw_tx:, outputs: = [])` <a id="method-i-stage_action"></a> <a id="stage_action-instance_method"></a>
Phase 2 (deferred): Attach placeholder signing artifacts and the caller's
declared outputs to an action.

Updates the action with `wtxid` and `raw_tx` but does NOT create a
`broadcasts` row. Used by the deferred `createAction` path where `raw_tx`
carries placeholder unlocking scripts; the broadcast row must wait for the
real {#sign_action} call (via BRC-100 `signAction`) to avoid pushing an
unsigned transaction to ARC.

Outputs are written with no promotions row yet (no spendable rows yet) so the
BRC-100 `signAction` — which does not receive the `outputs` array again —
finds the caller's metadata already persisted. Phase 4 promotion happens later
via {#promote_action_outputs} on broadcast acceptance.
- **@param** `action_id` [Integer]
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@param** `raw_tx` [String] binary-encoded transaction with placeholder
unlocking scripts
- **@param** `outputs` [Array<Hash>] caller's declared outputs to persist
with no promotions row yet. Each: :satoshis, :vout, :locking_script,
:spendable_intent (required — 'spendable' or 'none', stated
explicitly per HLR #467 /
+docs/reference/intent-and-outcomes.md+; inferring from
derivation presence is no longer accepted),
:derivation_prefix, :derivation_suffix,
:sender_identity_key, :basket, :tags, :description,
:custom_instructions
- **@raise** [NotImplementedError]

### `stale_action_ids(threshold:, limit:)` <a id="method-i-stale_action_ids"></a> <a id="stale_action_ids-instance_method"></a>
Discovery: IDs of abandoned actions ready to reclaim (past threshold, not
internal, not promoted). Pushed to Engine::Reaper by the Scheduler.
- **@param** `threshold` [Integer] age in seconds
- **@param** `limit` [Integer] max IDs per discovery pass
- **@raise** [NotImplementedError]
- **@return** [Array<Integer>] stale action IDs

### `sweepable_state()` <a id="method-i-sweepable_state"></a> <a id="sweepable_state-instance_method"></a>
Snapshot of "would dropping this DB destroy on-chain-anchored state?" Consult
before any destructive operation (DB drop, blank-slate reset, spec-setup
recreation, future +bsv-wallet destroy+ CLI). <code>clean?</code> is true ⇔
zero spendable derived outputs whose action has been signed and broadcast. HLR
#448.
- **@raise** [NotImplementedError]
- **@return** [BSV::Wallet::Store::SweepableState]

### `transmission_known_wtxids(counterparty:)` <a id="method-i-transmission_known_wtxids"></a> <a id="transmission_known_wtxids-instance_method"></a>
The wtxids `counterparty` has acknowledged across every transmission to them —
the BeefParty known set for trimming the next BEEF.
- **@param** `counterparty` [String] BRC-43 identity pubkey (66-char hex)
- **@raise** [NotImplementedError]
- **@return** [Array<String>] wire-order binary wtxids (deduplicated)

### `validated_tip(from_height:)` <a id="method-i-validated_tip"></a> <a id="validated_tip-instance_method"></a>
Highest height of the contiguous run of validated (header-present) rows
starting at `from_height` (the checkpoint) — the <code>:spv_headers</code>
validated tip (#335). Structural and gap-stopping: a header-island above a gap
is not the tip.
- **@param** `from_height` [Integer]
- **@raise** [NotImplementedError]
- **@return** [Integer, nil] the validated tip, or nil if unseeded

### `verification_state(wtxid:)` <a id="method-i-verification_state"></a> <a id="verification_state-instance_method"></a>
Look up the verification record for a wtxid. Returns `nil` when the tx has no
proof row OR the proof row has unpopulated verification state
(three-column-coherent CHECK ensures either all populated or all NULL — no
mixed reads).
- **@param** `wtxid` [String] 32-byte binary wtxid (wire byte order)
- **@raise** [NotImplementedError]
- **@return** [Hash, nil] +{ verified_at:, verified_via:, verifier_version: }+
or +nil+

### `verified_wtxids(wtxids:, version_at_least:, via_in:)` <a id="method-i-verified_wtxids"></a> <a id="verified_wtxids-instance_method"></a>
Batch trust-set query for the read path (BeefImporter Sub 5). Returns the
subset of `wtxids` whose stored `verified_via` is in the caller-supplied
`via_in` set AND whose `verifier_version` is at least `version_at_least`.
Empty input short-circuits (no DB round-trip). Chunked internally.

Callers pass +via_in: TxProof::VERIFIED_VIA_TRUSTED+ (currently +['spv']+ only
— `broadcast_ack` excluded pending its liveness mechanism, `self_built`
excluded on trust grounds) to build the <code>verified:</code> Set for
+Tx#verify+'s kwarg (bsv-ruby-sdk #904).
- **@param** `wtxids` [Array<String>] 32-byte binary wtxids
- **@param** `version_at_least` [Integer]
- **@param** `via_in` [Array<String>] +VERIFIED_VIA_*+ values to accept
- **@raise** [NotImplementedError]
- **@return** [Set<String>] subset of +wtxids+ that pass the gate
