@arkade-os/sdk Documentation - v0.5.0-rc.11
    Preparing search index...

    Class ContractManager

    Central manager for contract lifecycle and operations.

    Responsibilities:

    • Create and persist contracts
    • Query stored contracts (optionally with their virtual outputs)
    • Provide spendable path selection for a contract
    • Emit contract-related events (virtual output received/spent, connection reset)

    Notes:

    • Implementations typically start watching automatically during initialization (so onContractEvent() is just for subscribing).
    const manager = await ContractManager.create({
    indexerProvider: wallet.indexerProvider,
    contractRepository: wallet.contractRepository,
    });

    // Create a new VHTLC contract
    const contract = await manager.createContract({
    label: "Lightning Receive",
    type: "vhtlc",
    params: { sender: "ark1q...", receiver: "ark1q...", ... },
    script: "5120...",
    address: "ark1q...",
    });

    // Start watching for events
    const unsubscribe = manager.onContractEvent((event) => {
    console.log(`${event.type} on ${event.contractScript}`);
    });

    // Query contracts together with their current virtual outputs
    const contractsWithVtxos = await manager.getContractsWithVtxos();

    // Get balance across all contracts
    const balances = contractsWithVtxos.flatMap(({vtxos}) => vtxos).reduce((acc, vtxo) => acc + vtxo.value, 0)

    // Later: unsubscribe from events
    unsubscribe();

    // Clean up
    manager.dispose();

    Implements

    Index

    Methods

    • Symbol.dispose implementation for using with using keyword.

      Returns void

      {
      using manager = await wallet.getContractManager();
      // ... use manager
      } // automatically disposed
    • 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.

      Parameters

      • vtxos: VirtualCoin[]
      • Optionaltapscripts: ContractTapscriptCache

      Returns Promise<NormalizedExtendedVirtualCoin[]>

    • Throw unless every one of vtxos still has an annotatable contract.

      Spending does not re-derive tapscripts — it uses the ones stored on the coin — so a contract that stopped being annotatable (handler no longer registered, or params its handler now rejects) still builds and submits a transaction fine, and only fails afterwards, in the bookkeeping that re-annotates the inputs. Calling this before submitting turns that into a refusal to spend, naming the contract, rather than a broadcast whose local state could not be recorded.

      Parameters

      • vtxos: readonly { script: string; txid: string; vout: number }[]

      Returns Promise<void>

    • Throw when one of vtxos belongs to a contract that provably cannot be spent right now, asking each owning handler's ContractHandler.assertSpendableNow.

      The complement of isContractGenericallySpendable, which keeps escrow out of generic selection and leaves explicit-input APIs open on purpose. This does not close that door — it makes walking through it too early report itself locally, naming the timelock, instead of coming back as a protocol-level rejection after the round trip.

      Handlers answer only where they are certain, so contracts with no opinion (which is all of them but VHTLC today) pass through untouched and cost nothing — not even a chain-tip read.

      Optional so that adding it is not a breaking change for an embedder with its own IContractManager. An implementation that omits it simply offers no opinion, which is the same answer every non-VHTLC contract gives.

      Parameters

      • vtxos: readonly AssertSpendableInput[]
      • OptionalwalletDescriptor: () => Promise<string | undefined>

      Returns Promise<void>

    • Get every currently valid spending path for a contract.

      No blockHeight: this enumerates paths "regardless of current spendability", so no handler evaluates a timelock here and the tip would be fetched only to be discarded — leaving a purely local answer waiting on the network for nothing.

      Parameters

      • options: GetAllSpendingPathsOptions

        Options for getting spending paths

      Returns Promise<PathSelection[]>

    • Get contracts with optional filters.

      Parameters

      • Optionalfilter: ContractFilter

        Optional filter criteria

      Returns Promise<Contract[]>

      Filtered contracts TODO: filter spent/unspent

      // Get all VHTLC contracts
      const vhtlcs = await manager.getContracts({ type: 'vhtlc' });

      // Get all active contracts
      const active = await manager.getContracts({ state: 'active' });
    • 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.

      Parameters

      Returns Promise<void>

    • 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.

      Parameters

      • Optionalopts: RefreshVtxosOptions

      Returns Promise<void>

    • Explicit, 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.
      • A discovery rejection is collected into 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).
      • A handler exposing Discoverable.discoverRange is asked for the whole window in ONE call instead of one per index — the batching that keeps a large restore from bursting into an operator's rate limiter. Its failures are therefore range-wide: every index in the window goes indeterminate and truncation lands on the window's first index. Its answer must cover every requested index; an incomplete map is treated as a rejection (see Discoverable.discoverRange).
      • Handlers are probed concurrently (independent network reads); their hits are persisted sequentially in discoverables order to preserve the first-wins collision tie-break.
      • Indices are probed 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.
      • The whole scan runs inside one coalesced subscription scope, so N discovered contracts cost ONE subscribeForScripts instead of N growing ones (see ContractWatcher.withCoalescedSubscription).

      Parameters

      Returns Promise<ScanResult>

    • Which of vtxos their owning handler refuses right now, keyed by outpoint (txid:vout) and valued with the handler's own explanation.

      The predicate form of assertSpendableNow, for callers that must DROP a refused input rather than fail the batch holding it. Keyed per outpoint, not per script: a relative (CSV) timelock is measured from each coin's own confirmation, so two coins on one contract can disagree.

      Optional for the same reason assertSpendableNow is.

      Parameters

      • vtxos: readonly AssertSpendableInput[]
      • OptionalwalletDescriptor: () => Promise<string | undefined>

      Returns Promise<Map<string, string>>

    • Update a contract's params. This method preserves existing params by merging the provided values.

      Parameters

      • script: string

        Contract script

      • updates: Record<string, string>

        The new values to merge with existing params

      Returns Promise<Contract>