# Class BSV::Wallet::Engine::Action <a id="class-BSV-Wallet-Engine-Action"></a>

**Inherits:** `Object`

Business object wrapping a single action's lifecycle.

Per-call construction — cheap, discarded on completion. Wraps a
<code>Models::Action</code> row hash (+Store#action_to_hash+ output). Adds
row-level lifecycle actions (build_deferred!, build_with_caller_inputs!,
build_via_funding!, sign_and_save!, complete_internal!, apply_caller_spends!,
abort!) and BRC-100 helpers (build_input_specs, build_output_specs).

Orchestration lives in +Engine#build_action+ / <code>#sign_action</code> (#402
Stage 2). <code>Action.create</code> is now a thin row-creation helper; the
lifecycle steps are instance methods so Engine can drive them sequentially
without reach-backs.

## Attributes
### `engine` [R] <a id="attribute-i-engine"></a> <a id="engine-instance_method"></a>
---- Instance -------------------------------------------------------

### `id` [R] <a id="attribute-i-id"></a> <a id="id-instance_method"></a>
---- Instance -------------------------------------------------------

### `row` [R] <a id="attribute-i-row"></a> <a id="row-instance_method"></a>
---- Instance -------------------------------------------------------

## Public Class Methods
### `attach_labels(engine:, action_id:, labels:)` <a id="method-c-attach_labels"></a> <a id="attach_labels-class_method"></a>
Attach labels to an action. No-op for nil/empty labels.

### `build_input_specs(inputs)` <a id="method-c-build_input_specs"></a> <a id="build_input_specs-class_method"></a>
Translate caller input specs into Store input specs.

### `build_output_specs(outputs, vout_mapping = nil)` <a id="method-c-build_output_specs"></a> <a id="build_output_specs-class_method"></a>
Translate caller outputs into Store output specs. Used by both the synchronous
internal path ({Store#complete_internal_action}) and the send-path sign-time
persistence ({Store#sign_action}).

Every output spec must carry <code>:spendable_intent</code> — +'spendable'+
(wallet-owned, joins the UTXO set on promotion) or +'none'+ (outbound; no
spendable row written). Inferring intent from the presence of
`derivation_prefix` was the anti-pattern HLR #467 /
<code>docs/reference/intent-and-outcomes.md</code> removed; the decision-maker
states it explicitly. A missing intent is a caller-side bug.

### `create(engine:, description:, intent:, input_beef: = nil, labels: = nil)` <a id="method-c-create"></a> <a id="create-class_method"></a>
Row-creation helper. Returns an `Action` instance wrapping a newly-created
(empty) `actions` row. The orchestrator (+Engine#build_action+) drives the
lifecycle from here via the instance methods.

### `find(engine:, reference:)` <a id="method-c-find"></a> <a id="find-class_method"></a>
Canonical lookup by BRC-100 reference. Returns an Action wrapping the row, or
`nil` when no action matches.

### `find_by_id(engine:, id:)` <a id="method-c-find_by_id"></a> <a id="find_by_id-class_method"></a>
Lookup by wallet-local action id (the integer primary key, not a BRC-100
reference). Same shape as <code>.find</code> — a public factory for callers
that hold an id rather than a reference.

### `list(engine:, labels:, label_query_mode: = :any, include_labels: = false, include_inputs: = false, include_input_source_locking_scripts: = false, include_input_unlocking_scripts: = false, include_outputs: = false, include_output_locking_scripts: = false, limit: = 10, offset: = 0)` <a id="method-c-list"></a> <a id="list-class_method"></a>
Wallet-vocab action query — returns +{ total:, actions: }+, matching the shape
+Store#query_actions+ produces and the other <code>list_*</code> collection
primitives use. BRC100 re-keys <code>:total</code> →
<code>:total_actions</code> at the wrap layer. Called from
+Engine#list_actions+; <code>originator:</code> and
<code>seek_permission:</code> stop at the wrap layer per ADR-026 decision 7
and are not accepted here.

### `lock_caller_inputs!(engine:, action_id:, inputs:)` <a id="method-c-lock_caller_inputs-21"></a> <a id="lock_caller_inputs!-class_method"></a>
Lock the caller-supplied inputs against the empty action row. Used by the
deferred and `skip_change` (zero-input or pure OP_RETURN) paths — neither runs
the funding loop, so they bypass `FundingStrategy` and use the atomic Store
primitive directly. A short-count from <code>store.lock_inputs</code> surfaces
as `InsufficientFundsError`, matching the wallet-selected path's behaviour on
caller-supplied contention (fail-fast, no retry).
- **@raise** [BSV::Wallet::InsufficientFundsError]

## Public Instance Methods
### `abort!()` <a id="method-i-abort-21"></a> <a id="abort!-instance_method"></a>
Abort an in-flight action. Releases any locked inputs and marks the action
aborted in the Store.
- **@return** [Hash] +{ aborted: true }+

### `apply_caller_spends!(spends:)` <a id="method-i-apply_caller_spends-21"></a> <a id="apply_caller_spends!-instance_method"></a>
Complete the deferred-signing flow's signing step: deserialise the unsigned
tx, apply caller spends, sign remaining wallet-owned P2PKH inputs, persist
signed raw_tx + wtxid, save proof. Returns the signed result so the
orchestrator can build BEEF + dispatch.
- **@return** [Hash] +{ wtxid:, raw_tx: }+

### `build_deferred!(inputs:, outputs:, lock_time:, version:, randomize:, intent:)` <a id="method-i-build_deferred-21"></a> <a id="build_deferred!-instance_method"></a>
Deferred path: lock caller inputs, build unsigned tx, stage, save proof,
return wallet-vocab signable handle.

`intent` flows from the orchestrator (always <code>:none</code>,
<code>:delayed</code>, or <code>:inline</code>). <code>:none</code> + nil
outputs both suppress the staged pending-outputs persistence — they get
written on `sign_action` when the caller comes back with unlocking scripts.
- **@return** [Hash] +{ signable: { atomic_beef:, reference: } }+ —
wallet vocab; BRC100 wraps to +{ signable_transaction: { tx:, reference: } }+.

### `build_via_funding!(outputs:, caller_inputs:, lock_time:, version:, randomize:, change_count:, change_basket: = nil)` <a id="method-i-build_via_funding-21"></a> <a id="build_via_funding!-instance_method"></a>
Wallet-funded path: hand off to `FundingStrategy` which owns input acquisition
(initial + top-up) and the build collaborator's fixpoint loop. Caller-supplied
inputs (if any) pin part of the input set; the strategy tops up from the
wallet's pool.

On `InsufficientFundsError` the caller (Engine) aborts the empty action row —
that decision belongs at the orchestrator level so the headroom recheck after
the fact has somewhere to roll back to.
- **@return** [Hash] +{ wtxid:, raw_tx:, vout_mapping:, change_outputs:, total_input_satoshis: }+

### `build_with_caller_inputs!(inputs:, outputs:, lock_time:, version:, randomize:)` <a id="method-i-build_with_caller_inputs-21"></a> <a id="build_with_caller_inputs!-instance_method"></a>
Synchronous + skip-change path: caller-supplied inputs only, build + sign in
one TxBuilder pass (no funding loop, no change). Used by the OP_RETURN-only
and explicit-caller-inputs cases.
- **@return** [Hash] +{ wtxid:, raw_tx:, vout_mapping:, change_outputs: [] }+

### `complete_internal!(built:, outputs:)` <a id="method-i-complete_internal-21"></a> <a id="complete_internal!-instance_method"></a>
Internal-path atomic completion: sign + proof + Phase-4 promotion in one Store
call so a crash can't strand a signed-but-unpromoted action (#327) or
promoted-but-unspendable change (#328). No broadcast to wait for — the whole
completion commits at once.

`sign_outputs` is empty by design here: the internal path short-circuits
pending-output staging because the outputs jump straight to promoted (the
canonical row in `outputs`). Caller outputs land in `promote_outputs` only —
duplicating them into `sign_outputs` would violate
`outputs_action_id_vout_key`.

### `initialize(engine:, row:)` <a id="method-i-initialize"></a> <a id="initialize-instance_method"></a>
The one canonical <code>row:</code> shape is a +Store#action_to_hash+ hash —a
plain Hash keyed by <code>:id</code>, <code>:wtxid</code>,
<code>:reference</code>, <code>:status</code>, … . Every construction site
already hands one over: <code>.create</code> via
<code>store.create_action</code>, <code>.find</code>/<code>.find_by_id</code>
via <code>store.find_action</code>. The guard fails fast at construction
rather than at the first +@`row`+ read.
- **@return** [Action] a new instance of Action

### `query_change_outpoints()` <a id="method-i-query_change_outpoints"></a> <a id="query_change_outpoints-instance_method"></a>
Outpoints (<code>dtxid.vout</code>) of this action's change outputs. Public
because +Engine#build_action+ invokes it on the freshly-built instance to
populate the no_send return's <code>change_outpoints:</code>; operates on the
instance's own +@id+.

### `sign_and_save!(built:, outputs:)` <a id="method-i-sign_and_save-21"></a> <a id="sign_and_save!-instance_method"></a>
Send-path sign-time persistence: +Store#sign_action+ writes the signed raw_tx
+ wtxid + pending outputs + change outputs in one atomic transition, then
<code>#save_proof</code> stages the raw_tx in the proof store so the broadcast
worker can ship EF.
