Stamp raw virtual outputs with the correct per-contract tapscripts (forfeit, intent, tap tree).
Resolves each vtxo's script to its owning contract via the contract
repository and attaches the matching tapscripts. Throws when any vtxo
references a script with no registered contract — callers are expected
to register the contract before asking for annotation. This is the
single shared path that replaces scattered extendVirtualCoin* calls
in wallet/handler code, and keeps the wallet from silently stamping the
default tapscript onto a non-default vtxo.
Create and register a new contract.
Contract parameters
The created contract
Delete a contract. Also removes it from the watcher — the only way to stop watching a contract (retiring it via setContractState does not).
Contract script
Dispose of the ContractManager and release all resources.
Stops the watcher, clears callbacks, and marks the manager as uninitialized.
Implements the disposable pattern for cleanup.
Get every currently valid spending path for a contract.
Options for getting spending paths
Get contracts with optional filters.
Optionalfilter: ContractFilterOptional filter criteria
Filtered contracts TODO: filter spent/unspent
List contracts and their current virtual outputs.
If no filter is provided, returns all contracts with their virtual outputs.
Optionalfilter: ContractFilterOptionalpageSize: numberGet currently spendable paths for a contract.
Options for getting spendable paths
Latest provider-sync health. See ContractSyncState. Degradation is
recorded by initialize, getContractsWithVtxos, and
createContract; it flips back to online on the next successful
sync. Purely a freshness signal — not a source of truth for wallet data.
Check if currently watching.
Register a callback for contract events.
The manager automatically watches after initialize(). This method
allows registering callbacks to receive events.
Event callback
Unsubscribe function to remove this callback
Reconcile specific outpoints with the indexer's authoritative state and upsert the result into the wallet repository.
The cursor-derived delta sync filters by created_at, so a VTXO that
was created before the cursor but spent recently won't surface in a
standard refreshVtxos() call. This method is the surgical recovery
path for that case: when something hands us a stale outpoint (e.g. the
server returns VTXO_ALREADY_SPENT with a vtxo_outpoint in its
error metadata), call this to pull the latest state and unblock the
caller — no full re-scan, no cursor change.
Outpoints not owned by any tracked contract are silently dropped.
Force refresh virtual outputs from the indexer.
Without options, re-fetches the watcher's watched set and advances the global cursor. Each option narrows or widens that scope and may hold the cursor back — see RefreshVtxosOptions.
Optionalopts: RefreshVtxosOptionsExplicit, gap-limit contract discovery (see IContractManager.scanContracts).
Each hit is routed through persistAndWatchContract — the same
dedupe + watcher-register path as createContract minus the
per-contract indexer round-trip. The caller (Wallet.restore) follows
up with a single bulk refreshVtxos({ includeInactive: true }), so a
scan that finds N contracts costs one batched indexer call instead of
N + 1.
Safety-critical invariants (spec §2.C / §4):
opts.materialize(i) throwing is structural/fatal: it is NOT
wrapped — it propagates and aborts the scan.handlerErrors and makes its
index indeterminate: it does NOT advance the gap counter, and the
scan stops verifying there, reporting truncatedAt. Only an index
every handler answered for can be a confirmed miss.persistAndWatchContract rejecting is operational/fatal and
propagates (only the handler calls are guarded).Discoverable.discoverRange).discoverables order to preserve
the first-wins collision tie-break.batchSize at a time, but each window is CAPPED to
gapLimit - unused indices — the most a serial scan could still reach
before the gap window is guaranteed to close. So every index probed in
a window is one a one-index-at-a-time scan would also reach: nothing is
over-scanned, nothing is discarded, and materialize/discovery are
invoked on exactly the same index set. The window's hits are still
processed strictly in ascending index order, so the discovered set,
persisted rows, highestConfirmedUsedIndex, and handlerErrors are
byte-for-byte identical to the serial path — only the wall-clock
differs. Truncation is the one exception: a window's concurrent probes
can surface hits above the failed index that a serial scan would never
have reached, so a truncated batched scan discovers a superset — never
a subset — of the serial one.subscribeForScripts instead of N
growing ones (see ContractWatcher.withCoalescedSubscription).Set a contract's state. Retiring (inactive) keeps it watched;
see ContractState. To stop watching, use
deleteContract.
Update a contract's params. This method preserves existing params by merging the provided values.
Contract script
The new values to merge with existing params
StaticcreateStatic factory method for creating a new ContractManager. Initialize the manager by loading persisted contracts and starting to watch.
After initialization, the manager automatically watches every persisted
contract. Use onContractEvent() to register event callbacks.
ContractManagerConfig
Central manager for contract lifecycle and operations.
Responsibilities:
Notes:
onContractEvent()is just for subscribing).Example