import { Bytes } from '@scure/btc-signer/utils.js'; import { Transaction as Transaction$1, NETWORK } from '@scure/btc-signer'; import { TxOpts } from '@scure/btc-signer/transaction.js'; import { TransactionInput, TransactionOutput, PSBTOutput } from '@scure/btc-signer/psbt.js'; /** * Transaction is a wrapper around the @scure/btc-signer Transaction class. * It adds the Arkade protocol specific options to the transaction. */ declare class Transaction extends Transaction$1 { static ARK_TX_OPTS: TxOpts; constructor(opts?: TxOpts); /** * Decode a PSBT. PSBTs carrying a legacy `PSBT_OUT_TAP_TREE` (see * {@link repairLegacyOutputTapTrees}) are repaired and decoded again * instead of being rejected. */ static fromPSBT(psbt_: Bytes, opts?: TxOpts): Transaction; static fromRaw(raw: Bytes, opts?: TxOpts): Transaction; } /** * Reject a PSBT that declares a sighash type outside `allowedSighashTypes` on * any input, before it reaches a signer. An input carrying no explicit type is * left alone: the signer treats a taproot input as {@link SigHash.DEFAULT}. */ declare function assertAllowedSighashTypes(tx: Transaction$1, allowedSighashTypes?: number[]): void; /** * MuSig2 nonce pair containing public and secret values. * Public nonces are two compressed points (33 bytes each). * Secret nonces are the corresponding private scalars plus pubkey. */ type Nonces = { pubNonce: Uint8Array; secNonce: Uint8Array; }; declare class PartialSig { s: Uint8Array; R: Uint8Array; constructor(s: Uint8Array, R: Uint8Array); /** * Encodes the partial signature into bytes * Returns a 32-byte array containing just the s value */ encode(): Uint8Array; /** * Decodes a partial signature from bytes * @param bytes - 32-byte array containing s value */ static decode(bytes: Uint8Array): PartialSig; } /** * TxTreeNode is a node of the tree. * It contains the transaction id, the transaction and the children. * any TxTree can be serialized as a list of TxTreeNode. */ type TxTreeNode = { txid: string; tx: string; children: Record; }; /** * TxTree is a graph of bitcoin transactions. * It is used to represent batch tree created during settlement session */ declare class TxTree { readonly root: Transaction; readonly children: Map; constructor(root: Transaction, children?: Map); static create(chunks: TxTreeNode[]): TxTree; nbOfNodes(): number; validate(): void; leaves(): Transaction[]; get txid(): string; find(txid: string): TxTree | null; update(txid: string, fn: (tx: Transaction) => void): void; iterator(): Generator; } type Musig2PublicNonce = Pick; type TreeNonces = Map; type TreePartialSigs = Map; interface SignerSession { getPublicKey(): Promise; init(tree: TxTree, scriptRoot: Uint8Array, rootInputAmount: bigint): Promise; getNonces(): Promise; aggregatedNonces(txid: string, noncesByPubkey: TreeNonces): Promise<{ hasAllNonces: boolean; }>; sign(): Promise; } /** A signing request that pairs a descriptor with a transaction. */ interface DescriptorSigningRequest { /** Descriptor identifying which key to sign with */ descriptor: string; /** Transaction to sign */ tx: Transaction; /** Specific input indexes to sign (signs all if omitted) */ inputIndexes?: number[]; } /** * Provider interface for descriptor-based signing. * * Implementations include: * - {@link StaticDescriptorProvider}: wraps a legacy {@link Identity} with a single key. * - {@link HDDescriptorProvider}: rotates through HD-derived descriptors. * * The provider has no read accessor for "current" — it is a pure descriptor * allocator. "What addresses am I currently bound to?" is a question the * contract repository answers, not the provider. * * Providers that want to participate in HD receive rotation can also * implement the wallet-side `ReceiveRotatorFactory` interface (see * `src/wallet/walletReceiveRotator.ts`). That extension is opt-in — the * core `DescriptorProvider` contract intentionally stays free of * wallet-specific concerns so HSM-backed and other minimal providers * don't have to know about the receive-rotation lifecycle. */ interface DescriptorProvider { /** * Allocate a new signing descriptor. For HD providers each call advances * the internal index and returns a fresh descriptor; for single-key * providers each call returns the same descriptor. */ getNextSigningDescriptor(): Promise; /** Checks if a descriptor belongs to this provider. */ isOurs(descriptor: string): boolean; /** Signs transactions, each with its own descriptor-derived key. */ signWithDescriptor(requests: DescriptorSigningRequest[]): Promise; /** Signs a message using the key derived from the descriptor. */ signMessageWithDescriptor(descriptor: string, message: Uint8Array, type?: "schnorr" | "ecdsa"): Promise; } interface Identity extends ReadonlyIdentity { /** Returns a signer session used for musig2 tree signing flows. */ signerSession(): SignerSession; /** Sign an arbitrary message using the requested signature type. */ signMessage(message: Uint8Array, signatureType: "schnorr" | "ecdsa"): Promise; /** * Sign the provided transaction inputs. * * @param tx - Transaction to sign * @param inputIndexes - Optional input indexes to sign. When omitted, the implementation should sign every signable input. */ sign(tx: Transaction, inputIndexes?: number[]): Promise; } interface ReadonlyIdentity { /** Returns the x-only public key used by Taproot scripts. */ xOnlyPublicKey(): Promise; /** Returns the compressed public key for this identity. */ compressedPublicKey(): Promise; } /** A single PSBT signing request within a batch. */ interface SignRequest { tx: Transaction; inputIndexes?: number[]; } /** * Identity that supports signing multiple PSBTs in a single wallet interaction. * Browser wallet providers that support batch signing (e.g. Xverse, UniSat, OKX) * should implement this interface to reduce the number of confirmation popups * from N+1 to 1 during Arkade send transactions. * * Contract: * - Implementations MUST return exactly one `Transaction` per request, in the * same order as the input array. The SDK validates this at runtime and will * throw if the lengths do not match. * - Implementations MUST preserve any partial signatures already present on the * input PSBTs and only ADD their own — never drop, replace, or normalize away * foreign signatures. The pending-tx recovery path * (`Wallet.finalizePendingTxs`) hands `signMultiple` checkpoint PSBTs that * already carry the server's `tapScriptSig` and relies on that server * signature surviving alongside the freshly added user signature. A provider * that discards the pre-existing server sig produces checkpoints that fail * server-side finalization, stranding the transaction in the pending state. */ interface BatchSignableIdentity extends Identity { /** * Sign multiple transactions in a single wallet interaction. * * Must preserve pre-existing partial signatures on each input PSBT (see the * interface-level contract) and return one signed `Transaction` per request, * in request order. * * @param requests - Transactions and optional input indexes to sign * @returns Signed transactions in the same order as the input requests */ signMultiple(requests: SignRequest[]): Promise; } /** Type guard for identities that support batch signing. */ declare function isBatchSignable(identity: Identity): identity is BatchSignableIdentity; /** * Whether `value` is a complete {@link Identity} rather than the read-only * half of one. * * All four members are checked because all four are load-bearing — * `signerSession` in particular, which an interactive refund needs and which * a watch-only identity lacks. A partial identity satisfies a pubkey check * happily and then fails as a `TypeError` deep inside a signing path, where * callers read it as retryable. * * One shared guard: two copies drift, and the copy that is not updated when * `Identity` gains a member is the one that lets a watch-only identity * through. */ declare function isSigningIdentity(value: unknown): value is Identity; /** * Where the SDK gets its `EventSource`. * * Every server-sent-events stream this SDK opens — settlement events, the tx * feed, the indexer script subscription — needs one, and until now each site * reached for the global. That works in browsers and, via * {@link ExpoArkProvider}/{@link ExpoIndexerProvider}, in React Native. It does * NOT work in Node, which exposes `EventSource` only behind * `--experimental-eventsource` (24.x) — so a CLI, a server-side wallet or a * background worker got a `ReferenceError` out of the stream and everything * event-driven degraded to whatever polling happened to be running. * * Resolution order, per call: an explicit per-provider factory, then whatever * {@link configureEventSource} was given, then the global. When none answers, * {@link resolveEventSource} throws {@link EventSourceUnavailableError} rather * than a bare `ReferenceError`, so callers can tell "this environment has no * SSE" from "the connection dropped" — a distinction that matters, because the * first is not worth reconnecting for and the second is. */ /** * The slice of `EventSource` this SDK actually uses. * * Structural rather than the DOM type on purpose: Node's `eventsource` package * and the React Native polyfills are not DOM `EventSource`s, and demanding one * made every consumer launder a perfectly capable object through * `as unknown as EventSource` — a cast that hides real mismatches as readily as * it waves through this one. */ interface EventSourceLike { addEventListener(type: "message" | "error", listener: (event: MessageEvent) => void): void; removeEventListener(type: "message" | "error", listener: (event: MessageEvent) => void): void; close(): void; } /** Opens an SSE connection to `url`. `new EventSource(url)`, as a value. */ type EventSourceFactory = (url: string) => EventSourceLike; /** Options shared by every provider that opens an SSE stream. */ interface EventSourceCapable { /** * Where this provider gets its `EventSource`, overriding * {@link configureEventSource} and the global. Use it when one process * needs different transports per connection; otherwise configure once. */ eventSource?: EventSourceFactory; } /** * No `EventSource` could be resolved, so no SSE stream can be opened. * * A property of the environment, not of the connection: retrying cannot fix it, * which is why {@link ContractWatcher} reports it once and stops reconnecting * instead of looping on it. */ declare class EventSourceUnavailableError extends Error { constructor(); } /** * Type guard for {@link EventSourceUnavailableError}. * * Falls back to the `name` because a custom error crossing the service-worker * `postMessage` boundary arrives as a plain `Error` — the same reason * `ProviderUnavailableError` keeps its state in the message. */ declare function isEventSourceUnavailableError(error: unknown): error is Error; /** * Set the `EventSource` every provider uses by default. Call once at startup, * before opening a wallet; pass `undefined` to go back to the global. * * This exists because the provider-level option cannot reach providers the SDK * builds for you — `Wallet.create({ arkServerUrl })` constructs both itself. * * @example * ```typescript * import { EventSource } from "eventsource"; * configureEventSource((url) => new EventSource(url)); * ``` */ declare function configureEventSource(factory?: EventSourceFactory): void; /** What {@link configureEventSource} last set, if anything. */ declare function getConfiguredEventSource(): EventSourceFactory | undefined; /** * Resolve the factory to open a stream with, or throw * {@link EventSourceUnavailableError}. * * The global is read on every call rather than captured at module load: a test * (or a polyfill) that assigns `globalThis.EventSource` after import must still * be seen. */ declare function resolveEventSource(override?: EventSourceFactory): EventSourceFactory; type PaginationOptions = { pageIndex?: number; pageSize?: number; }; /** * Page size {@link RestIndexerProvider.getVtxos} uses when the caller named no * pagination of its own and it pages the query out itself. */ declare const DEFAULT_VTXO_PAGE_SIZE = 500; declare enum IndexerTxType { INDEXER_TX_TYPE_UNSPECIFIED = 0, INDEXER_TX_TYPE_RECEIVED = 1, INDEXER_TX_TYPE_SENT = 2 } declare enum ChainTxType { UNSPECIFIED = "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED", COMMITMENT = "INDEXER_CHAINED_TX_TYPE_COMMITMENT", ARK = "INDEXER_CHAINED_TX_TYPE_ARK", TREE = "INDEXER_CHAINED_TX_TYPE_TREE", CHECKPOINT = "INDEXER_CHAINED_TX_TYPE_CHECKPOINT" } interface PageResponse { /** Current page index **/ current: number; /** Next page index **/ next: number; /** Total pages given the page-size used in the query **/ total: number; } interface BatchInfo { totalOutputAmount: string; totalOutputVtxos: number; expiresAt: string; swept: boolean; } interface ChainTx { txid: string; expiresAt: string; type: ChainTxType; /** IDs of the transactions in the chain used as input of the current transaction. */ spends: string[]; } interface CommitmentTx { startedAt: string; endedAt: string; batches: { [key: string]: BatchInfo; }; totalInputAmount: string; totalInputVtxos: number; totalOutputAmount: string; totalOutputVtxos: number; } interface Tx { txid: string; children: Record; } interface TxHistoryRecord { commitmentTxid?: string; virtualTxid?: string; type: IndexerTxType; amount: string; createdAt: string; isSettled: boolean; settledBy: string; } interface VtxoAsset { assetId: string; amount: string; } interface Vtxo { outpoint: Outpoint; createdAt: string; expiresAt: string | null; amount: string; script: string; isPreconfirmed: boolean; isSwept: boolean; isUnrolled: boolean; isSpent: boolean; spentBy: string | null; commitmentTxids: string[]; settledBy?: string; arkTxid?: string; assets?: VtxoAsset[]; } interface VtxoChain { chain: ChainTx[]; page?: PageResponse; } interface SubscriptionResponse { txid?: string; scripts: string[]; newVtxos: VirtualCoin[]; spentVtxos: VirtualCoin[]; sweptVtxos: VirtualCoin[]; tx?: string; checkpointTxs?: Record; } interface SubscriptionHeartbeat { type: "heartbeat"; } interface SubscriptionEvent extends SubscriptionResponse { type: "event"; } /** * Filters accepted by `IndexerProvider.getVtxos`. * * @remarks * Exactly one of `scripts` or `outpoints` must be supplied. * * @see IndexerProvider.getVtxos */ type GetVtxosOptions = PaginationOptions & { /** Only return spendable virtual outputs. */ spendableOnly?: boolean; /** Only return spent virtual outputs. */ spentOnly?: boolean; /** Only return recoverable virtual outputs. */ recoverableOnly?: boolean; /** Only return pending/preconfirmed virtual outputs. */ pendingOnly?: boolean; /** Only return renewable virtual outputs (the union of the spendable and recoverable sets). */ renewableOnly?: boolean; /** Only return virtual outputs created after this timestamp. */ after?: number; /** Only return virtual outputs created before this timestamp. */ before?: number; } & ({ /** Scripts to search for matching virtual outputs. */ scripts: string[]; outpoints?: never; } | { /** Explicit outpoints to fetch. */ outpoints: Outpoint[]; scripts?: never; }); interface IndexerProvider { /** * Fetch the virtual output tree for a batch outpoint. * * @param batchOutpoint - Batch outpoint whose tree should be fetched * @param opts - Optional pagination settings * @returns virtual output tree nodes and optional pagination state */ getVtxoTree(batchOutpoint: Outpoint, opts?: PaginationOptions): Promise<{ vtxoTree: Tx[]; page?: PageResponse; }>; /** * Fetch the leaf outpoints for a batch virtual output tree. * * @param batchOutpoint - Batch outpoint whose leaf outpoints should be fetched * @param opts - Optional pagination settings * @returns Leaf outpoints and optional pagination state */ getVtxoTreeLeaves(batchOutpoint: Outpoint, opts?: PaginationOptions): Promise<{ leaves: Outpoint[]; page?: PageResponse; }>; /** * Fetch sweep transactions that spent a batch. * * @param batchOutpoint - Batch outpoint to inspect * @returns Sweep transaction ids */ getBatchSweepTransactions(batchOutpoint: Outpoint): Promise<{ sweptBy: string[]; }>; /** * Fetch a commitment transaction by txid. * * @param txid - Commitment transaction id * @returns Commitment transaction details */ getCommitmentTx(txid: string): Promise; /** * Fetch connector transactions for a commitment transaction. * * @param txid - Commitment transaction id * @param opts - Optional pagination settings * @returns Connector transactions and optional pagination state */ getCommitmentTxConnectors(txid: string, opts?: PaginationOptions): Promise<{ connectors: Tx[]; page?: PageResponse; }>; /** * Fetch forfeit transaction ids for a commitment transaction. * * @param txid - Commitment transaction id * @param opts - Optional pagination settings * @returns Forfeit transaction ids and optional pagination state */ getCommitmentTxForfeitTxs(txid: string, opts?: PaginationOptions): Promise<{ txids: string[]; page?: PageResponse; }>; /** * Open a streamed subscription for a previously created subscription id. * * @param subscriptionId - Subscription identifier returned by `subscribeForScripts` * @param abortSignal - Signal used to terminate the stream * @returns Async iterator of subscription responses */ getSubscription(subscriptionId: string, abortSignal: AbortSignal): AsyncIterableIterator; /** * Fetch raw virtual transactions by txid. * * @param txids - Virtual transaction ids to fetch * @param opts - Optional pagination settings * @returns Raw virtual transactions and optional pagination state */ getVirtualTxs(txids: string[], opts?: PaginationOptions): Promise<{ txs: string[]; page?: PageResponse; }>; /** * Fetch the ancestry chain for a virtual output. * * @param vtxoOutpoint - Virtual output outpoint to inspect * @param opts - Optional pagination settings * @returns Chain data and optional pagination state */ getVtxoChain(vtxoOutpoint: Outpoint, opts?: PaginationOptions): Promise; /** * Fetch virtual outputs by script set or outpoints. * * @param opts - Virtual output filters and pagination settings * @returns Virtual outputs and optional pagination state */ getVtxos(opts?: GetVtxosOptions): Promise<{ vtxos: VirtualCoin[]; page?: PageResponse; }>; /** * Fetch metadata for a specific asset id. * * @param assetId - Asset identifier * @returns Asset details */ getAssetDetails(assetId: string): Promise; /** * Create or extend a subscription for a set of scripts. * * @param scripts - Scripts to monitor * @param subscriptionId - Existing subscription id to extend * @returns Subscription id */ subscribeForScripts(scripts: string[], subscriptionId?: string): Promise; /** * Remove some or all scripts from an existing subscription. * * @param subscriptionId - Subscription identifier to update * @param scripts - Scripts to remove, or omit to remove all */ unsubscribeForScripts(subscriptionId: string, scripts?: string[]): Promise; } /** * REST-based indexer provider implementation. * @see https://buf.build/arkade-os/arkd/docs/main:ark.v1#ark.v1.IndexerService * @example * ```typescript * const provider = new RestIndexerProvider('https://arkade.computer'); * const commitmentTx = await provider.getCommitmentTx("6686af8f3be3517880821f62e6c3d749b9d6713736a1d8e229a55daa659446b2"); * ``` */ declare class RestIndexerProvider implements IndexerProvider { serverUrl: string; /** Overrides {@link configureEventSource} for this provider's subscription. */ protected readonly eventSource?: EventSourceFactory; /** @see fetchVtxosJson */ private readonly inFlightVtxoReads; constructor(serverUrl?: string, options?: EventSourceCapable); getVtxoTree(batchOutpoint: Outpoint, opts?: PaginationOptions): Promise<{ vtxoTree: Tx[]; page?: PageResponse; }>; getVtxoTreeLeaves(batchOutpoint: Outpoint, opts?: PaginationOptions): Promise<{ leaves: Outpoint[]; page?: PageResponse; }>; getBatchSweepTransactions(batchOutpoint: Outpoint): Promise<{ sweptBy: string[]; }>; getCommitmentTx(txid: string): Promise; getCommitmentTxConnectors(txid: string, opts?: PaginationOptions): Promise<{ connectors: Tx[]; page?: PageResponse; }>; getCommitmentTxForfeitTxs(txid: string, opts?: PaginationOptions): Promise<{ txids: string[]; page?: PageResponse; }>; getSubscription(subscriptionId: string, abortSignal: AbortSignal): AsyncIterableIterator; getVirtualTxs(txids: string[], opts?: PaginationOptions): Promise<{ txs: string[]; page?: PageResponse; }>; getVtxoChain(vtxoOutpoint: Outpoint, opts?: PaginationOptions): Promise; getVtxos(opts?: GetVtxosOptions): Promise<{ vtxos: VirtualCoin[]; page?: PageResponse; }>; /** * Fetch a single page of vtxos. Callers paging to exhaustion get no * deliberate inter-page backoff here: `indexerFetch` already enforces * politeness through the rate gate, so don't add a redundant sleep. */ private fetchVtxosPage; /** * The wire read behind {@link fetchVtxosPage}, with an identical read * already in flight served from that one request instead of repeated. * * Two callers can want the same page at the same instant without either * being redundant, so there is no single call site to remove: a send that * leaves change makes the indexer emit `vtxo_spent` and `vtxo_received` for * the wallet's own contract milliseconds apart, and `handleContractEvent` * delta-syncs that contract on both arms. */ private fetchVtxosJson; getAssetDetails(assetId: string): Promise; subscribeForScripts(scripts: string[], subscriptionId?: string): Promise; unsubscribeForScripts(subscriptionId: string, scripts?: string[]): Promise; } /** * FeeAmount is a wrapper around a number that represents a fee amount in satoshis floating point. * @param value - The fee amount in floating point. * @example * const fee = new FeeAmount(1.23456789); * console.log(fee.value); // 1.23456789 * console.log(fee.satoshis); // 2 */ declare class FeeAmount { readonly value: number; static ZERO: FeeAmount; constructor(value: number); /** Returns the fee amount rounded up to whole satoshis. */ get satoshis(): number; /** Add two fee amounts together. */ add(other: FeeAmount): FeeAmount; } interface IntentFeeConfig { offchainInput?: string; onchainInput?: string; offchainOutput?: string; onchainOutput?: string; } type VtxoType = "recoverable" | "vtxo" | "note"; interface OffchainInput { amount: bigint; expiry?: Date; birth?: Date; type: VtxoType; weight: number; } interface OnchainInput { amount: bigint; } interface FeeOutput { amount: bigint; script: string; } /** * Off-chain source of previous transactions: ark, tree and checkpoint txs, as * base64 PSBTs keyed by txid. Structurally satisfied by `IndexerProvider`. */ interface PrevTxSource { getVirtualTxs(txids: string[]): Promise<{ txs: string[]; }>; } /** * On-chain source of previous transactions — boarding and commitment txs, which * `getVirtualTxs` does not serve. Structurally satisfied by `OnchainProvider`. */ interface RawTxSource { getRawTransaction(txid: string): Promise; } /** * A previous transaction could not be resolved, so the emulator would reject the * submission with `missing prevout tx for input N`. Distinguishes "no source is * wired up" from "the source did not have it" — the remedies differ. */ declare class PrevTxUnavailableError extends Error { readonly txids: string[]; constructor(message: string, txids?: string[]); } /** * Resolve raw wire-format bytes for each txid, batched and de-duplicated. * * Emulator v0.0.7+ requires the raw tx behind every input of a submitted ark tx * or intent proof (`PrevArkTxField`) and of an onchain tx (`PrevoutTxField`). * The value is a serialized `wire.MsgTx`, *not* a PSBT — `Transaction.toBytes()` * with the default `(withScriptSig=false, withWitness=false)` is exactly the * preimage the emulator hashes to check the tx against the outpoint. * * Off-chain txs are fetched in one batched call; anything the indexer does not * serve (a commitment or boarding tx) falls through to `onchain` when supplied. */ declare function resolvePrevTxs(txids: string[], source: PrevTxSource, onchain?: RawTxSource): Promise>; /** * Attach `PrevArkTxField` to every input of `tx` that does not already carry * one, resolving the bytes for `txids[i]` — the coin input `i` ultimately * spends. * * Inputs already carrying the field are skipped rather than overwritten: the * emulator rejects an input with more than one field of the same type, so a * caller-supplied value (`Utxo.sourceTx`) must win outright. Nothing is fetched * when every input is already covered. */ declare function attachPrevArkTxs(tx: Transaction, txids: string[], source: PrevTxSource, onchain?: RawTxSource): Promise; /** * Attach `PrevoutTxField` to every input of an onchain PSBT, reading each * input's own outpoint — unlike an ark tx, an onchain tx spends its prevout * directly, so the txid is in the PSBT already. */ declare function attachPrevoutTxs(tx: Transaction, source: RawTxSource): Promise; /** * A coin an intent proof spends, carrying the raw previous tx the emulator needs * to resolve its prevout pkScript. */ type WithPrevTx = T & { prevTx?: Uint8Array; }; /** * Fill in `prevTx` on every coin an intent proof will spend, in one batched * lookup. The proof's synthetic input 0 needs nothing — the emulator synthesises * a one-output tx for it — so this covers exactly the coins, which land on proof * inputs `1..n`. */ declare function withPrevTxs(coins: T[], source: PrevTxSource, onchain?: RawTxSource): Promise; /** * A coin an intent proof proves ownership of. `prevTx` carries the raw wire * bytes of the transaction that created it — required by emulator v0.0.7+ on * every real input of a submitted proof. {@link Intent.create} is synchronous, * so the bytes are supplied rather than fetched; `withPrevTxs` fills them in. */ type IntentCoin = WithPrevTx; /** * Intent proof implementation for Bitcoin message signing. * * Intent proof defines a standard for signing Bitcoin messages as well as proving * ownership of outputs. * * This namespace provides utilities for creating and validating Intent proof. * * It is greatly inspired by BIP322. * @see https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki * * @example * ```typescript * // Create a Intent proof * const proof = Intent.create( * "Hello Bitcoin!", * [input], * [output] * ); * * // Sign the proof * const signedProof = await identity.sign(proof); * */ declare namespace Intent { type Proof = Transaction; /** * Creates a new Intent proof unsigned transaction. * * This function constructs a special transaction that can be signed to prove * ownership of onchain and virtual outputs. The proof includes the message to be * signed and the inputs/outputs that demonstrate ownership. * * @param message - The Intent message to be signed, either raw string of Message object * @param ins - Array of transaction inputs to prove ownership of * @param outputs - Optional array of transaction outputs * @returns An unsigned Intent proof transaction */ function create(message: string | Message, ins: (TransactionInput | IntentCoin)[], outputs?: TransactionOutput[]): Proof; /** * Compute the fee paid by an intent proof transaction. * * @param proof - Intent proof transaction * @returns The fee in satoshis */ function fee(proof: Proof): number; type RegisterMessage = { type: "register"; onchain_output_indexes: number[]; valid_at: number; expire_at: number; cosigners_public_keys: string[]; }; type DeleteMessage = { type: "delete"; expire_at: number; }; type GetPendingTxMessage = { type: "get-pending-tx"; expire_at: number; }; type Message = RegisterMessage | DeleteMessage | GetPendingTxMessage; /** * Serialize an intent message to the canonical JSON string used for signing. * * @param message - Intent message payload * @returns Canonical string form of the message */ function encodeMessage(message: Message): string; } /** * Thrown by {@link RestArkProvider} when arkd rejects a request with * `DIGEST_MISMATCH` — the client's cached server info was stale (e.g. an * operator signer rotation). By the time this surfaces the provider has already * refreshed its info and fired `onServerInfoChanged`; the caller should rebuild * the request under the fresh server info and retry. Mirrors NArk's * `DigestMismatchException` (dotnet-sdk #131): the SDK never silently retries. */ declare class DigestMismatchError extends Error { constructor(message: string); } /** Output requested during settlement or transaction submission. */ type Output = { /** Destination address, either onchain or Arkade (offchain). */ address: string; /** Amount to send in satoshis. */ amount: bigint; }; declare enum SettlementEventType { BatchStarted = "batch_started", BatchFinalization = "batch_finalization", BatchFinalized = "batch_finalized", BatchFailed = "batch_failed", TreeSigningStarted = "tree_signing_started", TreeNonces = "tree_nonces", TreeTx = "tree_tx", TreeSignature = "tree_signature", StreamStarted = "stream_started" } type BatchFinalizationEvent = { type: SettlementEventType.BatchFinalization; id: string; commitmentTx: string; }; type BatchFinalizedEvent = { type: SettlementEventType.BatchFinalized; id: string; commitmentTxid: string; }; type BatchFailedEvent = { type: SettlementEventType.BatchFailed; id: string; reason: string; }; type TreeSigningStartedEvent = { type: SettlementEventType.TreeSigningStarted; id: string; cosignersPublicKeys: string[]; unsignedCommitmentTx: string; }; type TreeNoncesEvent = { type: SettlementEventType.TreeNonces; id: string; topic: string[]; txid: string; /** Musig2 public nonces keyed by cosigner public key. */ nonces: TreeNonces; }; type BatchStartedEvent = { type: SettlementEventType.BatchStarted; id: string; intentIdHashes: string[]; batchExpiry: bigint; }; type TreeTxEvent = { type: SettlementEventType.TreeTx; id: string; topic: string[]; batchIndex: number; chunk: TxTreeNode; }; type TreeSignatureEvent = { type: SettlementEventType.TreeSignature; id: string; topic: string[]; batchIndex: number; txid: string; signature: string; }; type StreamStartedEvent = { type: SettlementEventType.StreamStarted; id: string; }; type SettlementEvent = BatchFinalizationEvent | BatchFinalizedEvent | BatchFailedEvent | TreeSigningStartedEvent | TreeNoncesEvent | BatchStartedEvent | TreeTxEvent | TreeSignatureEvent | StreamStartedEvent; interface ScheduledSession { duration: bigint; fees: FeeInfo; nextEndTime: bigint; nextStartTime: bigint; period: bigint; } interface FeeInfo { intentFee: IntentFeeConfig; txFeeRate: string; } interface PendingTx { arkTxid: string; finalArkTx: string; signedCheckpointTxs: string[]; } interface DeprecatedSigner { /** * Unix timestamp (seconds) after which the server no longer accepts this * signer's VTXOs as cooperative-migration inputs. arkd advertises this as a * non-nullable field, so it is always present: `0n` is the sentinel for "no * cutoff advertised" — the deprecated signer is due for migration * immediately ({@link classifyContractSigner} maps `0n` to `DUE_NOW`). A * positive value is a real deadline (`MIGRATABLE` until it passes, then * `EXPIRED`). */ cutoffDate: bigint; pubkey: string; } type ServiceStatus = Record; interface ArkInfo { boardingExitDelay: bigint; checkpointTapscript: string; deprecatedSigners: DeprecatedSigner[]; digest: string; dust: bigint; fees: FeeInfo; forfeitAddress: string; forfeitPubkey: string; network: string; scheduledSession?: ScheduledSession; serviceStatus: ServiceStatus; sessionDuration: bigint; signerPubkey: string; unilateralExitDelay: bigint; /** * Sweep delay the operator advertises for the shared batch output. * `undefined` when not advertised — never coerce to `0n`, which would * satisfy any floor comparison. */ vtxoTreeExpiry?: bigint; /** * Weight budget an ark transaction must stay under; the server answers * `TX_TOO_LARGE` past it. * * `undefined` when not advertised — a server that omits the field and one * that sends `0` both mean "no limit stated". Never `0n`, which would read * as a server that accepts no transaction at all. */ maxTxWeight?: bigint; /** * Maximum OP_RETURN outputs the server accepts per transaction. * * `undefined` when not advertised — a server that omits the field and one * that sends `0` both mean "no limit stated". Never `0n`, which would read * as a server that forbids them outright. */ maxOpReturnOutputs?: bigint; /** * Maximum boarding input amount. * * @remarks * `-1` means unlimited, while `0` disables boarding. */ utxoMaxAmount: bigint; utxoMinAmount: bigint; version: string; /** * Maximum virtual output amount. * * @remarks * `-1` means unlimited. */ vtxoMaxAmount: bigint; vtxoMinAmount: bigint; } /** Signed intent payload sent to the Arkade server. */ interface SignedIntent { /** Base64-encoded signed proof transaction. */ proof: string; /** Intent message payload associated with the proof. */ message: T; } /** Transaction notification emitted by the Arkade server stream. */ interface TxNotification { /** Transaction id. */ txid: string; /** Raw transaction payload. */ tx: string; /** Virtual outputs spent by the transaction. */ spentVtxos: Vtxo[]; /** Virtual outputs made spendable by the transaction. */ spendableVtxos: Vtxo[]; /** Optional checkpoint transactions associated with the notification. */ checkpointTxs?: Record; } /** * A `sweepTx` notification. Only a sweep carries swept outpoints, so only this * arm declares them: reading `sweptVtxos` off another arm is a type error, and * a sweep handler never has to narrow the field it came for. */ interface SweepTxNotification extends TxNotification { /** * Outpoints the transaction swept — outpoints rather than virtual outputs, * because a swept output is gone: the server has nothing left to describe * beyond where it was. Empty when the sweep touched none of them. */ sweptVtxos: Outpoint[]; } /** * One frame of {@link ArkProvider.getTransactionsStream}. Exactly one arm is * set; an unrecognized frame is dropped rather than yielded. */ interface TxNotificationEvent { commitmentTx?: TxNotification; arkTx?: TxNotification; sweepTx?: SweepTxNotification; } interface ArkProvider { /** Fetch Arkade server configuration and fee settings. */ getInfo(): Promise; /** Submit a signed Arkade transaction and its checkpoint transactions. */ submitTx(signedArkTx: string, checkpointTxs: string[]): Promise<{ arkTxid: string; finalArkTx: string; signedCheckpointTxs: string[]; }>; /** Finalize a previously submitted Arkade transaction. */ finalizeTx(arkTxid: string, finalCheckpointTxs: string[]): Promise; /** Register a signed intent with the Arkade server. */ registerIntent(intent: SignedIntent): Promise; /** Delete a previously registered intent. */ deleteIntent(intent: SignedIntent): Promise; /** Confirm an already registered intent id. */ confirmRegistration(intentId: string): Promise; /** Submit musig2 tree nonces for a batch signing session. */ submitTreeNonces(batchId: string, pubkey: string, nonces: TreeNonces): Promise; /** Submit musig2 partial signatures for a batch signing session. */ submitTreeSignatures(batchId: string, pubkey: string, signatures: TreePartialSigs): Promise; /** Submit signed forfeit transactions for cooperative settlement. */ submitSignedForfeitTxs(signedForfeitTxs: string[], signedCommitmentTx?: string): Promise; /** Open the settlement event stream for the given topics. */ getEventStream(signal: AbortSignal, topics: string[]): AsyncIterableIterator; /** Stream transaction notifications emitted by the Arkade server. */ getTransactionsStream(signal: AbortSignal): AsyncIterableIterator; /** Fetch pending transactions for a signed get-pending-tx intent. */ getPendingTxs(intent: SignedIntent): Promise; } /** * REST-based Arkade provider implementation. * * @see https://buf.build/arkade-os/arkd/docs/main:ark.v1#ark.v1.ArkService * @example * ```typescript * const provider = new RestArkProvider('https://arkade.computer'); * const info = await provider.getInfo(); * ``` */ declare class RestArkProvider implements ArkProvider { serverUrl: string; /** Overrides {@link configureEventSource} for this provider's streams. */ protected readonly eventSource?: EventSourceFactory; constructor(serverUrl?: string, options?: EventSourceCapable); /** * Last server-info digest seen from {@link getInfo}. Sent as `X-Digest` * so arkd can reject stale client configuration. */ private _digest; private _hasServerInfo; private _suppressNextGetInfoChangeEmit; private _serverInfoListeners; /** * Subscribe to server-info changes. Fired after a stale-info * `DIGEST_MISMATCH` refresh or when {@link getInfo} observes a changed digest. * Returns an unsubscribe function. */ onServerInfoChanged(listener: (info: ArkInfo) => void): () => void; private emitServerInfoChanged; /** * `fetch` wrapper for arkd requests that participates in server-info digest * negotiation. Sends the cached `X-Digest`; when arkd rejects a request with * `DIGEST_MISMATCH`, refreshes {@link getInfo} (updating the digest), fires * {@link onServerInfoChanged}, and THROWS {@link DigestMismatchError} — it * never silently retries, since the in-flight request was built against the * now-stale config. Dormant until arkd returns the error — then it is the * instant, event-driven signer-rotation trigger. {@link getInfo} itself never * routes through here: it is the refresh path and must not be digest-gated. */ private authedFetch; getInfo(): Promise; submitTx(signedArkTx: string, checkpointTxs: string[]): Promise<{ arkTxid: string; finalArkTx: string; signedCheckpointTxs: string[]; }>; finalizeTx(arkTxid: string, finalCheckpointTxs: string[]): Promise; registerIntent(intent: SignedIntent): Promise; deleteIntent(intent: SignedIntent): Promise; confirmRegistration(intentId: string): Promise; submitTreeNonces(batchId: string, pubkey: string, nonces: TreeNonces): Promise; submitTreeSignatures(batchId: string, pubkey: string, signatures: TreePartialSigs): Promise; submitSignedForfeitTxs(signedForfeitTxs: string[], signedCommitmentTx?: string): Promise; getEventStream(signal: AbortSignal, topics: string[]): AsyncIterableIterator; getTransactionsStream(signal: AbortSignal): AsyncIterableIterator; getPendingTxs(intent: SignedIntent): Promise; protected parseSettlementEvent(data: ProtoTypes.GetEventStreamResponse): SettlementEvent | null; protected parseTransactionNotification(data: ProtoTypes.GetTransactionsStreamResponse): TxNotificationEvent | null; } declare namespace ProtoTypes { interface BatchStartedEvent { id: string; intentIdHashes: string[]; batchExpiry: number; } interface BatchFailed { id: string; reason: string; } export interface BatchFinalizationEvent { id: string; commitmentTx: string; } interface BatchFinalizedEvent { id: string; commitmentTxid: string; } interface TreeSigningStartedEvent { id: string; cosignersPubkeys: string[]; unsignedCommitmentTx: string; } interface TreeNoncesAggregatedEvent { id: string; treeNonces: Record; } interface TreeNoncesEvent { id: string; topic: string[]; txid: string; nonces: Record; } interface TreeTxEvent { id: string; topic: string[]; batchIndex: number; txid: string; tx: string; children: Record; } interface TreeSignatureEvent { id: string; topic: string[]; batchIndex: number; txid: string; signature: string; } interface StreamStartedEvent { id: string; } interface Heartbeat { } export interface VtxoData { outpoint: { txid: string; vout: number; }; amount: string; script: string; createdAt: string; expiresAt: string | null; commitmentTxids: string[]; isPreconfirmed: boolean; isSwept: boolean; isUnrolled: boolean; isSpent: boolean; spentBy: string; settledBy?: string; arkTxid?: string; } export interface GetEventStreamResponse { batchStarted?: BatchStartedEvent; batchFailed?: BatchFailed; batchFinalization?: BatchFinalizationEvent; batchFinalized?: BatchFinalizedEvent; treeSigningStarted?: TreeSigningStartedEvent; treeNoncesAggregated?: TreeNoncesAggregatedEvent; treeNonces?: TreeNoncesEvent; treeTx?: TreeTxEvent; treeSignature?: TreeSignatureEvent; streamStarted?: StreamStartedEvent; heartbeat?: Heartbeat; } export interface TxNotificationData { txid: string; tx: string; spentVtxos: VtxoData[]; spendableVtxos: VtxoData[]; checkpointTxs?: Record; sweptVtxos?: { txid: string; vout: number; }[]; } export interface GetTransactionsStreamResponse { commitmentTx?: TxNotificationData; arkTx?: TxNotificationData; sweepTx?: TxNotificationData; heartbeat?: Heartbeat; } export interface EventData { batchStarted?: BatchStartedEvent; batchFailed?: BatchFailed; batchFinalization?: BatchFinalizationEvent; batchFinalized?: BatchFinalizedEvent; treeSigningStarted?: TreeSigningStartedEvent; treeNoncesAggregated?: TreeNoncesAggregatedEvent; treeTx?: TreeTxEvent; treeSignature?: TreeSignatureEvent; } export interface TransactionData { commitmentTx?: { txid: string; tx: string; spentVtxos: VtxoData[]; spendableVtxos: VtxoData[]; checkpointTxs?: Record; }; arkTx?: { txid: string; tx: string; spentVtxos: VtxoData[]; spendableVtxos: VtxoData[]; checkpointTxs?: Record; }; } export { }; } /** * RelativeTimelock lets to create timelocked with CHECKSEQUENCEVERIFY script. * * @example * ```typescript * const timelock = { value: 144n, type: "blocks" }; // 1 day in blocks * const timelock = { value: 512n, type: "seconds" }; // 8 minutes in seconds * ``` */ type RelativeTimelock = { value: bigint; type: "seconds" | "blocks"; }; declare enum TapscriptType { Multisig = "multisig", CSVMultisig = "csv-multisig", ConditionCSVMultisig = "condition-csv-multisig", ConditionMultisig = "condition-multisig", CLTVMultisig = "cltv-multisig" } /** * ArkTapscript is the base element of vtxo scripts. * It is used to encode and decode the different types of vtxo scripts. */ interface ArkTapscript { type: T; params: Params; script: Uint8Array; } /** * decodeTapscript is a function that decodes an Arkade tapscript from a raw script. * * @throws {Error} if the script is not a valid Arkade tapscript * @example * ```typescript * const arkTapscript = decodeTapscript(new Uint8Array(32)); * console.log("type:", arkTapscript.type); * ``` */ declare function decodeTapscript(script: Uint8Array): ArkTapscript; /** * Implements a multi-signature tapscript. * * CHECKSIGVERIFY CHECKSIG * * @example * ```typescript * const multisigTapscript = MultisigTapscript.encode({ pubkeys: [new Uint8Array(32), new Uint8Array(32)] }); * ``` */ declare namespace MultisigTapscript { type Type = ArkTapscript; enum MultisigType { CHECKSIG = 0, CHECKSIGADD = 1 } type Params = { pubkeys: Bytes[]; type?: MultisigType; }; /** Encode a plain multisig tapscript. */ function encode(params: Params): Type; /** Decode a plain multisig tapscript from raw script bytes. */ function decode(script: Uint8Array): Type; /** Return true when the tapscript is a plain multisig tapscript. */ function is(tapscript: ArkTapscript): tapscript is Type; } /** * Implements a relative timelock script that requires all specified pubkeys to sign * after the relative timelock has expired. The timelock can be specified in blocks or seconds. * * This is the standard exit closure and it is also used for the sweep closure in vtxo trees. * * CHECKSEQUENCEVERIFY DROP CHECKSIG * * @example * ```typescript * const csvMultisigTapscript = CSVMultisigTapscript.encode({ timelock: { type: "blocks", value: 144 }, pubkeys: [new Uint8Array(32), new Uint8Array(32)] }); * ``` */ declare namespace CSVMultisigTapscript { type Type = ArkTapscript; type Params = { timelock: RelativeTimelock; } & MultisigTapscript.Params; /** Encode a CSV multisig tapscript. */ function encode(params: Params): Type; /** Decode a CSV multisig tapscript from raw script bytes. */ function decode(script: Uint8Array): Type; /** Return true when the tapscript is a CSV multisig tapscript. */ function is(tapscript: ArkTapscript): tapscript is Type; function isScriptValid(script: Uint8Array): true | Error; } /** * Combines a condition script with an exit closure. The resulting script requires * the condition to be met, followed by the standard exit closure requirements * (timelock and signatures). * * VERIFY CHECKSEQUENCEVERIFY DROP CHECKSIGVERIFY CHECKSIG * * @example * ```typescript * const conditionCSVMultisigTapscript = ConditionCSVMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] }); * ``` */ declare namespace ConditionCSVMultisigTapscript { type Type = ArkTapscript; type Params = { conditionScript: Bytes; } & CSVMultisigTapscript.Params; /** Encode a condition + CSV multisig tapscript. */ function encode(params: Params): Type; /** Decode a condition + CSV multisig tapscript from raw script bytes. */ function decode(script: Uint8Array): Type; /** Return true when the tapscript is a condition + CSV multisig tapscript. */ function is(tapscript: ArkTapscript): tapscript is Type; function isScriptValid(script: Uint8Array): true | Error; } /** * Combines a condition script with a forfeit closure. The resulting script requires * the condition to be met, followed by the standard forfeit closure requirements * (multi-signature). * * VERIFY CHECKSIGVERIFY CHECKSIG * * @example * ```typescript * const conditionMultisigTapscript = ConditionMultisigTapscript.encode({ conditionScript: new Uint8Array(32), pubkeys: [new Uint8Array(32), new Uint8Array(32)] }); * ``` */ declare namespace ConditionMultisigTapscript { type Type = ArkTapscript; type Params = { conditionScript: Bytes; } & MultisigTapscript.Params; /** Encode a condition + multisig tapscript. */ function encode(params: Params): Type; /** Decode a condition + multisig tapscript from raw script bytes. */ function decode(script: Uint8Array): Type; /** Return true when the tapscript is a condition + multisig tapscript. */ function is(tapscript: ArkTapscript): tapscript is Type; function isScriptValid(script: Uint8Array): true | Error; } /** * Implements an absolute timelock (CLTV) script combined with a forfeit closure. * The script requires waiting until a specific block height/timestamp before the * forfeit closure conditions can be met. * * CHECKLOCKTIMEVERIFY DROP CHECKSIGVERIFY CHECKSIG * * @example * ```typescript * const cltvMultisigTapscript = CLTVMultisigTapscript.encode({ absoluteTimelock: 144, pubkeys: [new Uint8Array(32), new Uint8Array(32)] }); * ``` */ declare namespace CLTVMultisigTapscript { type Type = ArkTapscript; type Params = { absoluteTimelock: bigint; } & MultisigTapscript.Params; /** Encode a CLTV multisig tapscript. */ function encode(params: Params): Type; /** Decode a CLTV multisig tapscript from raw script bytes. */ function decode(script: Uint8Array): Type; /** Return true when the tapscript is a CLTV multisig tapscript. */ function is(tapscript: ArkTapscript): tapscript is Type; function isScriptValid(script: Uint8Array): true | Error; } /** * ArkAddress allows creating and decoding bech32m-encoded Arkade addresses. * * An Arkade address is composed of: * - a human readable prefix (hrp) * - a version byte (1 byte) * - a server public key (32 bytes) * - a vtxo taproot public key (32 bytes) * * @remarks * This is an Arkade-specific address format. * It is distinct from the Taproot onchain address returned by `VtxoScript.onchainAddress`. * * @see VtxoScript * * @example * ```typescript * const address = new ArkAddress( * new Uint8Array(32), // server public key * new Uint8Array(32), // vtxo taproot public key * "ark" * ); * * const encoded = address.encode(); * console.log("address: ", encoded); * * const decoded = ArkAddress.decode(encoded); * ``` */ declare class ArkAddress { readonly serverPubKey: Bytes; readonly vtxoTaprootKey: Bytes; readonly hrp: string; readonly version: number; /** * Create an Arkade address from its server public key, Taproot output key, and prefix. * * @param serverPubKey - 32-byte Arkade server public key * @param vtxoTaprootKey - 32-byte Taproot output key (a.k.a. tweaked public key) * @param hrp - Bech32 human-readable prefix * @param version - Address version byte * @defaultValue `version = 0` * @throws Error if either public key is not 32 bytes long */ constructor(serverPubKey: Bytes, vtxoTaprootKey: Bytes, hrp?: string, version?: number); /** * Decode an Arkade address from its bech32m string form. * * @param address - Bech32m-encoded Arkade address * @returns Decoded Arkade address * @throws Error if the address is malformed or has an invalid payload length * @see encode */ static decode(address: string): ArkAddress; /** * Encode the address to its bech32m string form. * * @returns Bech32m-encoded Arkade address * @see decode */ encode(): string; /** ScriptPubKey used to send non-dust funds to the address. */ get pkScript(): Bytes; /** ScriptPubKey used to send sub-dust funds to the address. */ get subdustPkScript(): Bytes; } type TapLeafScript = [ { version: number; internalKey: Bytes; merklePath: Bytes[]; }, Bytes ]; declare const TapTreeCoder: (typeof PSBTOutput.tapTree)[2]; declare function scriptFromTapLeafScript(leaf: TapLeafScript): Bytes; /** * VtxoScript is a script that contains a list of tapleaf scripts. * It is used to create virtual output scripts. * * @see ArkAddress * * @example * ```typescript * const vtxoScript = new VtxoScript([new Uint8Array(32), new Uint8Array(32)]); * ``` */ declare class VtxoScript { readonly scripts: Bytes[]; readonly leaves: TapLeafScript[]; readonly tweakedPublicKey: Bytes; readonly pkScript: Bytes; /** * Decode a virtual output script from an encoded TapTree. * * Leaves are normally read in construction order and their depths * ignored: that covers the {@link encode} form (every depth 1) and the * placeholder depths other writers use, such as arkd's. The exception is * the BIP-371 form {@link toBIP371TapTree} writes, whose leaves are in the * tree's DFS order: for some leaf counts (6, 7, 10, ...) that differs from * construction order. A tree whose depths are exactly the shape * `assembleBtcdTaprootTree` builds is that form, so its leaves are mapped * back to construction order first. No construction-order writer produces * that depth sequence when the two orders differ, so this never reorders * a tree that decoded correctly before. * * Every leaf must carry the base tapscript version: the tree is rebuilt * with {@link TAP_LEAF_VERSION} leaves, so any other version would * silently derive a different taproot key. * * @param tapTree - Encoded TapTree bytes * @returns Decoded virtual output script * @throws Error if the TapTree cannot be decoded into a valid script set * or a leaf carries a version other than {@link TAP_LEAF_VERSION} * @see encode */ static decode(tapTree: Bytes): VtxoScript; /** * Create a virtual output script from its tapleaf scripts. * * The Taproot script tree is assembled using btcd's algorithm * (`txscript.AssembleTaprootScriptTree`) so the derived taproot output * key agrees with arkd for any leaf count. `@scure/btc-signer`'s * default `taprootListToTree` is a Huffman builder that only agrees * with arkd for power-of-2 leaf counts. * * @param scripts - Raw tapscript bytes for each leaf * @throws Error if the provided leaves cannot produce a valid Taproot tree */ constructor(scripts: Bytes[]); /** * Encode the virtual output script to a TapTree byte representation. * * @returns Encoded TapTree bytes * @see decode */ encode(): Bytes; /** * Build the Arkade address corresponding to this virtual output script. * * @param prefix - Bech32 human-readable prefix * @param serverPubKey - 32-byte Arkade server public key * @returns Arkade address for this script * @see ArkAddress */ address(prefix: string | undefined, serverPubKey: Bytes): ArkAddress; /** * Build the Taproot onchain address corresponding to this virtual output script. * * @param network - Bitcoin network descriptor * @returns Taproot onchain address * @see address */ onchainAddress(network?: typeof NETWORK): string; /** * Look up a tapleaf script by its hex-encoded tapscript body. * * @param scriptHex - Hex-encoded tapscript body without the leaf version byte * @returns Matching tapleaf script * @throws Error if no matching leaf exists */ findLeaf(scriptHex: string): TapLeafScript; /** * Return all unilateral exit paths embedded in the virtual output script. * * @returns CSV-based exit paths found in the leaves * @see getSequence */ exitPaths(): Array; } type EncodedVtxoScript = { tapTree: Bytes; }; /** * Extract the timelock value encoded in a timelocked tapleaf, if any. * * The return value is unit-ambiguous: for a CSV leaf it is a BIP-68 * nSequence (relative timelock); for a CLTV leaf it is an absolute * nLockTime. Callers must know which leaf shape they are inspecting to * interpret the number correctly, and must not copy a CSV result into * `Transaction.lockTime` (or vice versa). * * @param tapLeafScript - Tapleaf script to inspect * @returns The encoded timelock value, or `undefined` when neither a CSV * nor CLTV path is present * @see VtxoScript.exitPaths */ declare function getSequence(tapLeafScript: TapLeafScript): number | undefined; type NetworkName = "bitcoin" | "testnet" | "signet" | "mutinynet" | "regtest"; interface Network { hrp: string; bech32: string; pubKeyHash: number; scriptHash: number; wif: number; /** * Canonical name this network was resolved from, when known. * * `bech32` cannot separate the tb-family — testnet, signet and mutinynet * share every field above — so anything that has to tell them apart (a * per-network timelock floor, for one) has no other handle. Optional * because a hand-built `Network` carries no name: consumers must read a * missing one as "unknown" and fall back to their strictest branch rather * than guess at one. */ name?: NetworkName; } declare const getNetwork: (network: NetworkName) => Network; declare const networks: { bitcoin: Network; testnet: Network; signet: Network; mutinynet: Network; regtest: Network; }; /** Covenant co-signer ("emulator") for the mainnet Arkade deployment. */ declare const BITCOIN_EMULATOR_PUBKEY: "0239c196415da47b26456a101daaa12ba9e445bfe153197f1e2b750bf40e52092e"; /** Covenant co-signer ("emulator") for the hosted mutinynet Arkade deployment. */ declare const MUTINYNET_EMULATOR_PUBKEY: "03f823b9b2febc81f4af967e77aed2f541cbd3397c6d8f5a72e32eb7b471af889a"; /** Covenant co-signer ("emulator") shipped with the `arkade-regtest` stack. */ declare const REGTEST_EMULATOR_PUBKEY: "02999413c46fa10ada5cbc4bcc79a1d09160c2ba3cfc812705d7a13e5e545fb2a9"; /** * The pinned co-signer key for `network`, as 33-byte compressed lowercase hex. * * @throws if the network carries no name, or names one with no deployed * emulator. Fail closed on both counts: the return value ends up in a * covenant leaf that decides who can move the funds, so there is no * defensible fallback. Guessing a neighbour's key (the tb-family share every * other field on {@link Network}) would build a leaf co-signed by a service * that will never sign for it — funds locked to a key nobody holds — and * returning an empty string just moves the same failure somewhere harder to * read. A hand-assembled `Network` carries no `name` and lands here too, * matching how the per-network timelock floors treat an unnamed network. * * Deliberately NOT falling back to whatever key the emulator reports about * itself: that is the self-report this pin exists to stop trusting, and doing * it only on unpinned networks would make the guarantee depend on * `network.name` with no signal to the caller that it had lapsed. The * unnamed case is the sharp one — a hand-built `Network` can carry * bitcoin-equivalent parameters, so a silent fallback would drop to * "trust the endpoint" on exactly the input that looks most like mainnet. * Callers who mean it pass {@link resolveEmulatorPubkey}'s override, which * the thrown message names. */ declare function defaultEmulatorPubkey(network: Network): string; /** * Resolve the co-signer key for `network`, letting a caller substitute its own. * * The override is the escape hatch for three situations, and the first is the * one that matters operationally: * * 1. **A network rotated its emulator key and this SDK has not shipped the new * constant yet.** Since `Arkade.connect` no longer asks the service which * key it signs with, a rotation is invisible here until the constant is * updated — covenants keep building against the retired key and fail only * when a claim is attempted. Passing the new key restores service without * waiting for a release, so a rotation is a config change rather than an * outage. * 2. A private or self-hosted emulator, including on a network with no pinned * key at all (signet, testnet, a hand-built `Network`). * 3. Tests and local stacks. * * Supplying it means co-signing with a different service: every covenant built * from the returned key can be completed by whoever holds it and by no one * else, so it is a statement that you trust that operator in place of the * network's. Prefer {@link defaultEmulatorPubkey} unless that is deliberate. * * A malformed override throws rather than being passed through — a typo here * would otherwise surface as an unspendable contract long after the fact. */ declare function resolveEmulatorPubkey(network: Network, override?: string): string; /** * The default base URLs for esplora API providers. * * Mainnet, mutinynet, and signet point at Ark Labs–operated * mempool deployments (mempool.space-compatible esplora API). * Testnet falls back to the public mempool.space deployment * because Ark doesn't host it. Regtest assumes a local arkade-regtest * stack exposing mempool's esplora API on the standard port. */ declare const ESPLORA_URL: Record; type ExplorerTransaction = { txid: string; /** * Inputs as returned by Esplora's `/address/:addr/txs`, each carrying the * outpoint it spends (`txid:vout`). Optional: not every provider populates * it (the electrum provider omits inputs), so consumers that correlate * spenders must tolerate its absence. Used to recover a boarding output's * spending (commitment) tx when `/outspends` omits the spender txid. */ vin?: { txid: string; vout: number; }[]; vout: { scriptpubkey_address: string; value: string; }[]; status: { confirmed: boolean; block_time: number; /** Set by Esplora; the electrum provider omits it. */ block_height?: number; }; }; interface OnchainProvider { /** * Fetch spendable onchain outputs for an address. * * @param address - Bitcoin address to query * @returns Spendable onchain outputs for the address * @see Coin */ getCoins(address: string): Promise; /** * Fetch the current fastest fee rate estimate. * * @returns Fee rate in sats/vB, if available * @remarks * Implementations may return `undefined` when the backing service does not expose * a usable fee estimate. */ getFeeRate(): Promise; /** * Broadcast a single transaction or a 1P1C package. * * @param txs - One or more raw transaction hex strings * @returns Broadcast transaction id * @throws Error if the broadcast request fails or the package shape is invalid */ broadcastTransaction(...txs: string[]): Promise; /** * Fetch outspend information for every output in a transaction. * * @param txid - Transaction id to inspect * @returns Per-output spend status information. `txid` (the spender) may be * absent even when `spent` is true: some Esplora deployments * (e.g. mempool.arkade.sh) omit it from `/outspends`. * @see getTxStatus */ getTxOutspends(txid: string): Promise<{ spent: boolean; txid?: string; }[]>; /** * Fetch transactions associated with an address. * * @param address - Bitcoin address to query * @returns Transactions involving the address * @see ExplorerTransaction */ getTransactions(address: string): Promise; /** * Fetch the raw wire-format bytes of a transaction. * * @param txid - Transaction id to fetch * @returns Serialized transaction bytes * @throws Error if the transaction is unknown to the backend * @remarks * Needed to carry a boarding or commitment tx as a PSBT prevout field — * those have no off-chain source, so the indexer cannot serve them. */ getRawTransaction(txid: string): Promise; /** * Fetch confirmation status for a transaction. * * @param txid - Transaction id to inspect * @returns Confirmation status and block metadata when confirmed * @see getTxOutspends */ getTxStatus(txid: string): Promise<{ confirmed: false; } | { confirmed: true; blockTime: number; blockHeight: number; }>; /** * Fetch the current chain tip. * * @returns Current chain height, median-time-past, and block hash * @remarks * `time` is the tip's **median-time-past**, not the header's own `nTime`. * BIP-113 evaluates seconds-typed `OP_CHECKLOCKTIMEVERIFY` / * `OP_CHECKSEQUENCEVERIFY` against MTP, which lags the tip by roughly an * hour; the header time may sit two hours ahead of it. An implementation * returning the header time calls a timelock mature before the network * does — surfacing as a broadcast the node rejects, not as a bug here. */ getChainTip(): Promise<{ height: number; time: number; hash: string; }>; /** * Watch a set of addresses and invoke the callback when transactions are observed. * * @param addresses - Addresses to monitor * @param eventCallback - Callback invoked when matching transactions are seen * @returns Stop function that cancels the watch * @remarks * Implementations may use websockets, server-sent events, polling, or a hybrid strategy. * @see getTransactions */ watchAddresses(addresses: string[], eventCallback: (txs: ExplorerTransaction[]) => void): Promise<() => void>; } /** * Implementation of the onchain provider interface for esplora REST API. * * @see https://mempool.space/docs/api/rest * @example * ```typescript * const provider = new EsploraProvider("https://mempool.space/api"); * const outputs = await provider.getCoins("bcrt1q679zsd45msawvr7782r0twvmukns3drlstjt77"); * ``` */ declare class EsploraProvider implements OnchainProvider { private baseUrl; readonly pollingInterval: number; readonly forcePolling: boolean; /** * Live {@link watchAddresses} subscriptions, keyed by their address set. * * Concurrent watchers over the same addresses share one transport, so a * caller that opens watchers in a loop — or leaks them by abandoning * {@link waitForIncomingFunds} — costs one entry in a `Set` rather than * another WebSocket plus another full-history polling loop. Entries are * dropped once their last subscriber stops. */ private readonly addressWatches; constructor(baseUrl?: string, opts?: { /** Polling interval in milliseconds. */ pollingInterval?: number; /** Force polling even when websocket transport is available. */ forcePolling?: boolean; }); getCoins(address: string): Promise; getFeeRate(): Promise; broadcastTransaction(...txs: string[]): Promise; getTxOutspends(txid: string): Promise<{ spent: boolean; txid?: string; }[]>; getTransactions(address: string): Promise; /** * Activity counts for an address, without its transactions — orders of * magnitude smaller than `/address/{addr}/txs`, which is what makes it * usable as a change probe for the polling fallback. */ private getAddressStats; getRawTransaction(txid: string): Promise; getTxStatus(txid: string): Promise<{ confirmed: false; } | { confirmed: true; blockTime: number; blockHeight: number; }>; /** * Watch a set of addresses over the explorer's WebSocket, degrading to HTTP * polling whenever the socket is unavailable and returning to the socket as * soon as it can be re-established. * * Concurrent calls covering the same address set share one transport. The * returned function releases **this** subscription only; the transport is * torn down when the last subscriber releases it. Calling it more than once * is a no-op. * * @param addresses - Addresses to monitor; order is not significant * @param callback - Invoked with transactions seen after the watch started * @returns A function releasing this subscription * @remarks * The HTTP fallback fetches full address history per address per cycle, * which is dramatically more expensive than the socket — so callers should * release watches they no longer need rather than relying on sharing. * @see {@link waitForIncomingFunds} for the cancellation-aware wallet-level helper */ watchAddresses(addresses: string[], callback: (txs: ExplorerTransaction[]) => void): Promise<() => void>; /** * Bring up one shared address watch: a WebSocket subscription where the * explorer supports it, degrading to HTTP polling when it doesn't. * * @param addresses - Addresses this watch covers * @param onTeardown - Invoked when the watch retires, to drop the registry entry */ private createAddressWatch; getChainTip(): Promise<{ height: number; time: number; hash: string; }>; private broadcastPackage; private broadcastTx; } /** * The current moment, as the expiry predicates need it. * * `height` is optional: offline-first paths have no chain tip at hand. When it is absent, * height-based expiry cannot be evaluated and reads as not expired. */ type TimeHeight = { timestamp: Date; height?: number; }; /** * A {@link VirtualCoin} that has passed through {@link normalizeVtxo}: every fact the capability * predicates read is present. * * Internal signatures take this rather than `VirtualCoin` so the compiler rejects un-normalized * input — on the public shape these facts are optional, and `undefined` is falsy, so a legacy coin * would silently read as "not swept", "not spent", and drop out of the wrong bucket. * * It is deliberately a *subtype* of `VirtualCoin`, so normalized coins are returned to consumers * directly and no egress projection exists. */ type NormalizedVirtualCoin = Omit & { isSwept: boolean; isPreconfirmed: boolean; isSpent: boolean; spentBy: string; commitmentTxIds: string[]; }; type NormalizedExtendedVirtualCoin = ExtendedVirtualCoin & NormalizedVirtualCoin; /** * Whether a virtual output has been consumed and can never be spent again. * * @remarks * Unions all three spend facts rather than trusting any one of them. The wire contract permits * `isSpent: true` with an empty `spentBy` (settlement inputs needing no forfeit are written that * way), so a `spentBy || settledBy` definition would classify a spent VTXO as spendable — inflating * balance and selecting it for a send that must fail. * * `isUnrolled` is deliberately **not** part of this union: it says where the output lives, not that * it was consumed. Mirrors NArk's `ArkVtxo.IsSpent()`. The location axis is {@link canSweepOnchain}, * which the two capability predicates below subtract instead. */ declare function isVtxoSpent(vtxo: VirtualCoin): boolean; /** @deprecated Use {@link isVtxoSpent}. */ declare const hasTerminalSpend: typeof isVtxoSpent; /** * Whether a virtual output's batch expiry has passed. Pure expiry — swept is a separate fact, ORed * in explicitly by {@link canSpendOffchain} / {@link canRecoverOnchain}. * * @remarks * Not named `isExpired`: the deprecated {@link isExpired} also returns `true` for a swept VTXO, and * two same-named predicates with different truth conditions is how a call site gets silently * rewired. * * Height-based expiry is only evaluated when `now.height` is supplied. */ declare function isPastExpiry(vtxo: VirtualCoin, now: TimeHeight): boolean; /** Whether a virtual output can be spent in an offchain transaction. The send/coin-selection test. */ declare function canSpendOffchain(vtxo: VirtualCoin, now: TimeHeight): boolean; /** * Whether a virtual output must be recovered into a fresh batch rather than spent offchain. The * recovery/renewal test and the `recoverable` balance bucket. */ declare function canRecoverOnchain(vtxo: VirtualCoin, now: TimeHeight): boolean; /** * Whether a virtual output's exit already happened: the output lives onchain and `completeUnroll` * is the only remedy. The third and last capability, on the location axis rather than the spend * one — together the three partition the live set, since this claims every unrolled coin and the * other two refuse them. * * @remarks * The ts-sdk analogue of NArk's `OnchainSweepService` filter, minus the expiry clause: the relevant * maturity here is the exit tx's CSV timelock, which `prepareUnrollTransaction` checks against the * chain tip, not the batch expiry. */ declare function canSweepOnchain(vtxo: VirtualCoin): boolean; /** * Narrow a settle input to a virtual output. * * @remarks * Keyed on `script`: it is required on `VirtualCoin` (so legacy and canonical shapes both have it) * and absent from `ExtendedCoin`, which the optional canonical facts cannot claim. * * The `typeof` guard is load-bearing — `settle` accepts arknote strings, and `in` throws a * `TypeError` on a primitive rather than returning false. */ declare function isVirtualCoin(input: T): input is T & VirtualCoin; /** * Return whether a virtual output is still spendable. * * @param vtxo - virtual output to inspect * @returns `true` when the virtual output has not been consumed * * @deprecated Ambiguous: `true` for swept, expired or unrolled virtual outputs, none of which can * in fact be spent offchain. Use {@link canSpendOffchain}. */ declare function isSpendable(vtxo: VirtualCoin): boolean; /** * Return whether a virtual output is recoverable. * * @param vtxo - virtual output to inspect * @returns `true` when the virtual output is swept but not yet consumed * * @deprecated Swept-only: ignores virtual outputs that are past expiry but not yet swept, which are * equally recoverable, and claims unrolled ones, which are not recoverable at all. Use * {@link canRecoverOnchain}. */ declare function isRecoverable(vtxo: VirtualCoin): boolean; /** * Return whether a virtual output should be treated as expired. * * @param vtxo - virtual output to inspect * @returns `true` when the virtual output is swept or its wall-clock batch expiry has passed * * @deprecated Conflates swept with expired, and cannot evaluate height-based expiry — being * synchronous, it has no source for the current chain tip, so it ignores `expiresAtHeight` exactly * as it always has. For the recovery decision use {@link canRecoverOnchain}; to reproduce this * truth condition use `v.isSwept || isPastExpiry(v, now)`. */ declare function isExpired(vtxo: VirtualCoin): boolean; /** * Machine-readable classification of a contract's server signer relative to a * fresh {@link ArkInfo} snapshot. Drives both the migration selection (Section * 3) and the user-facing cutoff reporting (Section 6) without persisting any * stale-key metadata: staleness is always derived at read time from the * contract's `params.serverPubKey` plus the server's advertised signer set. * * - `CURRENT`: the contract was minted under the server's active signer; no * migration needed. * - `MIGRATABLE`: the contract's signer is advertised as deprecated and its * cutoff has not passed — cooperative migration is still possible. * - `DUE_NOW`: the contract's signer is advertised as deprecated with no cutoff * date (arkd advertises this as a `0n` cutoff), so migration should start * immediately. * - `EXPIRED`: the contract's signer is deprecated and its cutoff has passed — * cooperative migration is closed. These funds are NOT stranded: the VTXO * keeps its own batch expiry, the server sweeps that batch at expiry, and the * swept VTXO then recovers into the active signer through the normal recovery * settle (no deprecated key, no forfeit, no on-chain exit). Unilateral exit * remains an opt-in escape hatch, not a requirement (Section 6 / post-cutoff). * - `UNKNOWN_SIGNER`: the contract's signer is neither the active signer nor an * advertised deprecated signer. The SDK does not attempt to migrate these. */ type SignerStatus = "CURRENT" | "MIGRATABLE" | "DUE_NOW" | "EXPIRED" | "UNKNOWN_SIGNER"; /** * Result of classifying a single contract's server signer against the current * {@link ArkInfo} signer set. */ interface SignerClassification { status: SignerStatus; /** The contract's server signer, normalized to x-only (32-byte) hex. */ signerPubKey: string; /** * Absolute cutoff as a Unix timestamp in seconds, present only when the * server advertised one for this deprecated signer. */ cutoffDate?: bigint; /** * Derived seconds until the advertised cutoff (`cutoffDate - now`), present * only for `MIGRATABLE`/`EXPIRED` (i.e. an advertised cutoff exists). * Negative once the cutoff has passed. */ secondsUntilCutoff?: number; } /** * The server's signer set, pre-normalized to x-only hex for cheap repeated * lookups. Built once per migration/reporting pass from a fresh * {@link ArkInfo} snapshot via {@link signerSetFromInfo}. */ interface SignerSet { /** Active signer, x-only (32-byte) hex. */ active: string; /** * Deprecated signers keyed by x-only hex, mapped to their cutoff. The cutoff * is always a bigint (arkd advertises it non-nullable); `0n` means "no cutoff * advertised" (→ `DUE_NOW`). * * Read-only: the wallet hands out its live `_deprecatedSigners` here rather * than a copy, and that map drives coin selection and the pendingRecovery * balance bucket. A holder must not write through it. */ deprecated: ReadonlyMap; } /** * Normalize a server signer pubkey hex to the x-only (32-byte) form contract * scripts and `params.serverPubKey` use. A 33-byte compressed key drops its * parity prefix; a 32-byte key is canonicalized to lowercase. The active * signer and the deprecated signers (which arkd may advertise compressed) * compare equal to the x-only `params.serverPubKey` persisted on contracts. */ declare function toXOnlySignerHex(pubkeyHex: string): string; /** * Build the {@link SignerSet} from a server-info snapshot. Deprecated signers * with an empty pubkey are skipped. */ declare function signerSetFromInfo(info: ArkInfo): SignerSet; /** * Classify a contract's server signer against a pre-built {@link SignerSet}. * * @param contractServerPubKeyHex - the contract's `params.serverPubKey` * @param signerSet - the server's signer set * @param nowSeconds - current Unix time in seconds (compared against the * advertised cutoff). Defaults to `Math.floor(Date.now() / 1000)`. */ declare function classifyAgainstSignerSet(contractServerPubKeyHex: string, signerSet: SignerSet, nowSeconds?: number): SignerClassification; /** * Convenience wrapper that builds the signer set from {@link ArkInfo} and * classifies a single contract signer. Prefer {@link classifyAgainstSignerSet} * with a shared signer set when classifying many contracts in one pass. */ declare function classifyContractSigner(contractServerPubKeyHex: string, info: ArkInfo, nowSeconds?: number): SignerClassification; /** * Whether a signer status admits cooperative migration (i.e. a `settle()` * intent should be built for VTXOs under this signer). True for `MIGRATABLE` * and `DUE_NOW`; false for `CURRENT`, `EXPIRED`, and `UNKNOWN_SIGNER`. */ declare function isCooperativelyMigratable(status: SignerStatus): boolean; /** * Outpoints (`txid:vout`) of VTXOs that are NOT cooperatively spendable because * their contract's signer is past its cutoff (`EXPIRED`) and the VTXO has not * yet been swept by the server. Such funds are unspendable until they recover * (the server sweeps the batch at expiry, then the swept output re-settles under * the active signer), so `getBalance` buckets them under `pendingRecovery` and * coin selection skips them — otherwise a send would pick a VTXO the operator * will not co-sign and fail at submit. * * Pure + offline: classification uses a cached {@link SignerSet}, never a fresh * GetInfo. `MIGRATABLE` / `DUE_NOW` (still cooperatively spendable), already- * swept (recoverable), `CURRENT`, and `UNKNOWN_SIGNER` rows are all left alone. */ declare function selectPendingRecoveryOutpoints(contractsWithVtxos: ReadonlyArray<{ contract: { params: { serverPubKey?: string; }; }; vtxos: ReadonlyArray; }>, signerSet: SignerSet, nowSeconds?: number): Set; /** * Configuration options for automatic virtual output renewal * * @see DEFAULT_RENEWAL_CONFIG * @deprecated Leave `renewalConfig` undefined and use `settlementConfig` instead. * @see SettlementConfig */ interface RenewalConfig { /** * Enable automatic renewal monitoring * * @defaultValue `false` * @deprecated Explicitly set `settlementConfig` to `false` to disable VTXO renewal. */ enabled?: boolean; /** * Threshold in milliseconds to use as threshold for renewal * E.g., 86_400_000 means renew when 24 hours until expiry remains * * @defaultValue `259_200_000` (3 days). * @deprecated Use `SettlementConfig.vtxoThreshold` (in seconds) instead. */ thresholdMs?: number; } /** * Configuration for automatic settlement and renewal. * * Controls two behaviors: * 1. **VTXO renewal**: Automatically renew virtual outputs that are close to expiry * 2. **Boarding UTXO sweep**: Sweep expired boarding inputs back to a fresh boarding address * via the unilateral exit path (onchain self-spend to restart the timelock) * * Enabled by default when no config is provided. * Pass `false` to explicitly disable all settlement behavior. * * @remarks * VTXO renewal and boarding UTXO sweep are both coordinated by `VtxoManager`, which periodically * inspects wallet virtual outputs and boarding inputs and decides whether action is needed. * * @see DEFAULT_SETTLEMENT_CONFIG * * @example * ```typescript * // Default behavior: virtual output renewal at 3 days, boarding sweep enabled, polling every minute * const wallet = await Wallet.create({ * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), * arkProvider: new RestArkProvider(), * }); * * // Custom expiry threshold of 24 hours * const wallet = await Wallet.create({ * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), * arkProvider: new RestArkProvider(), * settlementConfig: { * vtxoThreshold: 60 * 60 * 24, // 24 hours in seconds * }, * }); * * // Explicitly disable * const wallet = await Wallet.create({ * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), * arkProvider: new RestArkProvider(), * settlementConfig: false, * }); * ``` */ interface SettlementConfig { /** * Seconds before virtual output expiry to trigger renewal. * * @defaultValue `259_200` (3 days) */ vtxoThreshold?: number; /** * Sweep expired boarding inputs back to a fresh boarding address * via the unilateral exit path (onchain self-spend to restart the timelock). * * When enabled, expired boarding inputs are batched into a single onchain * transaction with multiple inputs and one output. * * A dust check ensures the sweep is only performed when the output * after fees is above dust. * * @defaultValue `true` */ boardingUtxoSweep?: boolean; /** * Polling interval in milliseconds for checking boarding inputs. * The poll loop auto-settles new boarding inputs into Arkade and * sweeps expired ones (when boardingUtxoSweep is enabled). * * @defaultValue `60_000` (1 minute) */ pollIntervalMs?: number; /** * Automatically migrate VTXOs minted under a now-deprecated server signer * back to the wallet's active-signer address before their cutoff window * closes (planned arkd key rotation). * * When enabled, each poll cycle cooperatively migrates stale-signer VTXOs * via the normal `settle()` path, applying a mid-session server-signer * rotation first when the wallet's own snapshot signer has been deprecated. * The explicit {@link IVtxoManager.migrateDeprecatedSignerVtxos} method * remains available for manual migration regardless of this flag. * * Setting `settlementConfig: false` disables all background settlement, * including migration. Set this field to `false` to keep renewal/sweep but * skip automatic deprecated-signer migration specifically. * * @defaultValue `true` */ deprecatedSignerMigration?: boolean; } /** * Check if a virtual output is expiring soon based on threshold * * Always `false` for an unilaterally exited output: "expiring soon" is a renewal * signal, and no batch can renew an output that already lives onchain. * * @param vtxo - The virtual output to check * @param thresholdMs - Threshold in milliseconds from now * @returns true if virtual output expires within threshold, false otherwise */ declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, thresholdMs: number): boolean; /** * Optional arguments for {@link IVtxoManager.renewVtxos}. */ interface RenewVtxosOptions { /** * Override the renewal threshold for this call only, in seconds. * * When provided, takes precedence over `SettlementConfig.vtxoThreshold` * and the default (3 days). Useful for renewing only VTXOs that are * more urgently expiring than the globally configured threshold. */ thresholdSeconds?: number; } /** * Optional arguments for {@link IVtxoManager.migrateDeprecatedSignerVtxos}. */ interface MigrateDeprecatedSignerOptions { /** Callback to receive settlement events during the migration intent. */ eventCallback?: (event: SettlementEvent) => void; } /** * A single VTXO referenced in a {@link DeprecatedSignerMigrationReport}. */ interface MigrationVtxoRef { txid: string; vout: number; value: number; /** The deprecated signer the VTXO was minted under (x-only hex). */ signerPubKey: string; /** Absolute cutoff (Unix seconds) when the server advertised one. */ cutoffDate?: bigint; } /** * Machine-readable status for a single deprecated signer the wallet holds * funds under (Section 6). Derived at read time from contract params plus a * fresh {@link ArkInfo} snapshot — never persisted. */ interface DeprecatedSignerReport { /** Deprecated signer key (x-only hex). */ signerPubKey: string; /** One of `migratable` | `dueNow` | `expired` | `unknownSigner`. */ status: SignerStatus; /** Absolute cutoff (Unix seconds), present only when advertised. */ cutoffDate?: bigint; /** Derived seconds until cutoff; negative once passed. */ secondsUntilCutoff?: number; /** Number of spendable VTXOs the wallet holds under this signer. */ vtxoCount: number; /** Total value of those VTXOs in satoshis. */ totalValue: number; /** * Number of spendable boarding UTXOs the wallet holds under this signer * (Section 7). Counts every confirmed boarding coin, including those whose * own CSV exit window has elapsed (they leave via the unilateral sweep). */ boardingCount: number; /** Total value of those boarding UTXOs in satoshis (Section 7). */ boardingValue: number; /** * Expired-signer VTXOs already swept and queued for recovery to the active * signer (the recover-on-sweep default — see {@link SignerStatus} `EXPIRED`). * Non-zero only on `EXPIRED` rows; these drain on the next recovery pass * (Section 6 / post-cutoff). */ recoverableCount: number; recoverableValue: number; /** * Expired-signer VTXOs not yet swept; awaiting the server batch sweep before * they become recoverable. Non-zero only on `EXPIRED` rows — nothing for the * user to do but wait (Section 6 / post-cutoff). */ awaitingSweepCount: number; awaitingSweepValue: number; /** * Soonest batch expiry (ms since epoch) among the awaiting-sweep VTXOs, as a * recovery ETA hint. Present only when an `EXPIRED` row has awaiting-sweep * VTXOs that carry a batch expiry (Section 6 / post-cutoff). */ nextSweepEta?: number; } /** * Why a single migration leg (VTXO send or boarding settle) submitted nothing. * `oversized-only` means every migratable input in that leg individually * exceeds the server's per-output ceiling (`vtxoMaxAmount`) — see * {@link MigrationLegReport.oversized}. */ type MigrationLegSkipReason = "below-dust" | "oversized-only" | "not-spendable-only"; /** * Why the whole pass submitted nothing, before either leg was built. * `no-deprecated-vtxos` means BOTH migratable sets (VTXO and boarding) were * empty; `unknown-wallet-signer` means the wallet's own snapshot signer is * neither active nor advertised deprecated, so the pass refuses to rotate. */ type MigrationGlobalSkipReason = "no-deprecated-vtxos" | "unknown-wallet-signer"; /** * Outcome of one migration leg. The VTXO leg migrates through the Ark send path * ({@link Wallet.sendSelectedVtxosToSelf}); the boarding leg keeps its * settle-backed migration (boarding coins are on-chain inputs with no send * path). Each leg owns its full sizing pipeline (oversized filtering, count + * amount caps, its own dust floor) and reports independently — a failure or skip * in one leg never suppresses the other. */ interface MigrationLegReport { /** VTXO leg: Ark transaction id from send. Boarding leg: settle commitment txid. */ txid?: string; /** Inputs submitted and accepted in this leg's transaction; empty on error/skip. */ migrated: MigrationVtxoRef[]; /** Why this leg submitted nothing (every candidate below dust or oversized). */ skipped?: MigrationLegSkipReason; /** * Migratable inputs deferred to a later pass by this leg's own caps (the * input count {@link MAX_VTXOS_PER_SETTLEMENT} or the per-output amount * ceiling `vtxoMaxAmount`). Present and non-zero only when a cap bound and * the leg actually submitted; makes the truncation visible. */ deferred?: number; /** * Inputs whose value alone exceeds the per-output ceiling (`vtxoMaxAmount`): * a single ≤-ceiling output can never hold them, so they never migrate * cooperatively and require a unilateral exit. Present only when non-empty; * absent when the server advertises no ceiling (`vtxoMaxAmount < 0`). */ oversized?: MigrationVtxoRef[]; /** * Inputs the leg's submit path would have rejected — for the VTXO leg, no longer * cooperatively spendable at this pass's chain tip (past batch expiry, since swept and spent * inputs are already excluded upstream) or carrying no batch expiry at all. * * Partitioned out rather than submitted, because the send path validates the batch as a * whole: one rejected input would abort the entire leg and strand every other migratable * VTXO until the next pass. Present only when non-empty. */ notSpendableOffchain?: MigrationVtxoRef[]; /** Error message when this leg's submission failed; the other leg still runs. */ error?: string; } /** * Result of a {@link IVtxoManager.migrateDeprecatedSignerVtxos} pass, split into * two symmetric legs: VTXOs migrate through the send path, boarding UTXOs keep a * separate settle-backed migration. They are never combined into one intent. */ interface DeprecatedSignerMigrationReport { /** * Whether this pass moved the wallet's receive state onto the active * signer — directly, or through the server-info refresh it opens with. * `false` when the wallet was already there, which includes a rotation an * earlier refresh elsewhere in the session already applied. */ rotated: boolean; /** Global skip; when set, neither leg is present. */ skipped?: MigrationGlobalSkipReason; /** Send leg. Present iff ≥1 cooperatively-migratable VTXO existed this pass. */ vtxos?: MigrationLegReport; /** Settle leg. Present iff ≥1 cooperatively-migratable boarding UTXO existed this pass. */ boarding?: MigrationLegReport; /** * Cutoff-expired inputs of both kinds (a classification outcome, not a leg * outcome). Skipped because their signer cutoff has passed: cooperative * migration is closed for them. They are NOT pushed to a unilateral exit — * each keeps its own batch expiry, the server sweeps it at expiry, and the * recovery path then re-mints it under the active signer. The per-signer * sweep/recovery lifecycle is surfaced on {@link signers} * ({@link DeprecatedSignerReport.recoverableCount} / * {@link DeprecatedSignerReport.awaitingSweepCount}). */ expired: MigrationVtxoRef[]; /** Per-deprecated-signer status snapshot (Section 6). */ signers: DeprecatedSignerReport[]; } /** * VtxoManager is a unified class for managing virtual output lifecycle operations including * recovery of swept/expired virtual outputs and renewal to prevent expiration. * * Key Features: * - **Recovery**: Reclaim swept or expired virtual outputs back to the wallet * - **Renewal**: Refresh virtual output expiration time before they expire * - **Smart subdust handling**: Automatically includes subdust virtual outputs when economically viable * - **Expiry monitoring**: Check for virtual outputs that are expiring soon * * Virtual outputs become recoverable when: * - The Arkade server sweeps them (`isSwept`) and they remain spendable * - They are preconfirmed subdust (to consolidate small amounts without locking liquidity on settled virtual outputs) * * @example * ```typescript * const wallet = await Wallet.create({ * identity, * arkProvider: new RestArkProvider(), * settlementConfig: { * // Seconds before virtual output expiry to trigger renewal * vtxoThreshold: 259_200, // 3 days * // Whether to sweep expired boarding inputs back to a fresh boarding address * boardingUtxoSweep: true, * // Polling interval in milliseconds for checking boarding inputs * pollIntervalMs: 60_000 // 1 minute * }, * }); * const manager = await wallet.getVtxoManager(); * * // Check recoverable balance * const balance = await manager.getRecoverableBalance(); * if (balance.recoverable > 0n) { * console.log(`Can recover ${balance.recoverable} sats`); * const txid = await manager.recoverVtxos(); * } * * // Check for expiring virtual outputs * const expiring = await manager.getExpiringVtxos(); * if (expiring.length > 0) { * console.log(`${expiring.length} virtual outputs expiring soon`); * const txid = await manager.renewVtxos(); * } * ``` */ interface IVtxoManager { recoverVtxos(eventCallback?: (event: SettlementEvent) => void): Promise; getRecoverableBalance(): Promise<{ recoverable: bigint; subdust: bigint; includesSubdust: boolean; vtxoCount: number; }>; getExpiringVtxos(thresholdMs?: number): Promise; renewVtxos(eventCallback?: (event: SettlementEvent) => void, options?: RenewVtxosOptions): Promise; getExpiredBoardingUtxos(): Promise; sweepExpiredBoardingUtxos(): Promise; /** * Cooperatively migrate VTXOs minted under a now-deprecated server signer * to the wallet's active-signer address (planned arkd key rotation). * * Applies a mid-session server-signer rotation first when the wallet's own * snapshot signer has been deprecated, so the migration output commits to * the active signer. Selects spendable VTXOs under deprecated-signer * contracts, prioritizing those closest to their cutoff, and settles them * back to the (rotated) Ark address. VTXOs whose cutoff has already passed * are reported as `expired` rather than migrated. * * Available regardless of the `deprecatedSignerMigration` config flag (that * flag only gates the automatic poll-loop pass). * * As a side effect, refreshes the wallet's cached deprecated-signer set from * fresh server info, so a subsequent `settle()` correctly excludes EXPIRED * deprecated-signer inputs. To refresh that filter without migrating, call * `wallet.refreshDeprecatedSigners(await wallet.arkProvider.getInfo())` instead. * * @returns A report of what was migrated, skipped, expired, or failed. */ migrateDeprecatedSignerVtxos(options?: MigrateDeprecatedSignerOptions): Promise; /** * Machine-readable status of every deprecated server signer the wallet * currently holds funds under, without performing any migration. Lets * consumers surface cutoff warnings on their own schedule. */ getDeprecatedSignerStatus(): Promise; dispose(): Promise; } declare class VtxoManager implements AsyncDisposable, IVtxoManager { readonly wallet: IWallet; /** @deprecated Use settlementConfig instead */ readonly renewalConfig?: RenewalConfig | undefined; readonly settlementConfig: SettlementConfig | false; private readonly contractEventsSubscriptionReady; private disposePromise?; private pollTimeoutId?; private knownBoardingUtxos; private sweptBoardingUtxos; private pollInProgress; private pollDone?; private disposed; private consecutivePollFailures; private startupPollTimeoutId?; private static readonly MAX_BACKOFF_MS; private renewalInProgress; private lastRenewalTimestamp; private static readonly RENEWAL_COOLDOWN_MS; private lastPeriodicSettleTimestamp; private consecutivePeriodicSettleFailures; private static readonly PERIODIC_SETTLE_COOLDOWN_MS; private static readonly PERIODIC_SETTLE_MAX_BACKOFF_MS; private lastVtxoSpentRefreshTimestamp; private vtxoSpentRefreshPromise?; private static readonly VTXO_SPENT_REFRESH_COOLDOWN_MS; private lastMigrationTimestamp; private consecutiveMigrationFailures; private static readonly MIGRATION_COOLDOWN_MS; private static readonly MIGRATION_MAX_BACKOFF_MS; constructor(wallet: IWallet, /** @deprecated Use settlementConfig instead */ renewalConfig?: RenewalConfig | undefined, settlementConfig?: SettlementConfig | false); /** * Outpoints recovery must not name yet, with the reason each was refused. * Empty when the manager offers no opinion — an embedder's may not implement * the predicate, and every contract type but VHTLC never answers. * * Reaches only a lockup whose `sender` is this wallet's own key: role * resolution matches that key against the contract's params, so a * descriptor-derived sender (every RFQ lockup) is not matched and such a row * still fails its batch at submit. * * The key stays a thunk so an ordinary recovery never touches the identity, * and the full coins are passed rather than bare outpoints because only they * carry the confirmation a relative timelock is measured from. */ private unspendableNow; /** * Recover swept/expired virtual outputs by settling them back to the wallet's Arkade address. * * This method: * 1. Fetches all virtual outputs (including recoverable ones) * 2. Filters for swept but still spendable virtual outputs and preconfirmed subdust * 3. Includes subdust virtual outputs if the total value >= dust threshold * 4. Settles everything back to the wallet's Arkade address * * Note: Settled virtual outputs with long expiry are NOT recovered to avoid locking liquidity unnecessarily. * Only preconfirmed subdust is recovered to consolidate small amounts. * * Inputs whose contract refuses a spend right now — an immature VHTLC refund * path — are skipped rather than failing the batch that holds them, and * {@link getRecoverableBalance} skips the same set. See * {@link unspendableNow} for which rows that reaches. * * @param eventCallback - Optional callback to receive settlement events * @returns Settlement transaction ID * @throws Error if no recoverable virtual outputs found * * @example * ```typescript * const manager = await wallet.getVtxoManager(); * * // Simple recovery * const txid = await manager.recoverVtxos(); * * // With event callback * const txid = await manager.recoverVtxos((event) => { * console.log('Settlement event:', event.type); * }); * ``` */ recoverVtxos(eventCallback?: (event: SettlementEvent) => void): Promise; /** * Get information about recoverable balance without executing recovery. * * Useful for displaying to users before they decide to recover funds. * * Both amounts are net of the operator's intent fees, so `recoverable` is * what {@link recoverVtxos} would actually hand back rather than the gross * sum of the inputs. `subdust` is net of the per-input fees only — the * output fee is charged on the batch as a whole and is not attributed per * coin — so it reports what the subdust coins are worth once each has paid * its own way, and is not strictly a slice of `recoverable`. It can exceed * `recoverable` where a flat output fee meets a wholly subdust wallet. * * The batch size caps are not applied here: they defer the overflow to the * next cycle rather than reducing what is recoverable. * * Inputs a contract refuses right now are excluded, so this and * {@link recoverVtxos} answer over the same set. `Balance.recoverable` still * counts them: that field reports what the wallet owns, this one what a * batch would hand back today. * * @returns Object containing recoverable amounts and subdust information * * @example * ```typescript * const manager = await wallet.getVtxoManager(); * const balance = await manager.getRecoverableBalance(); * * if (balance.recoverable > 0n) { * console.log(`You can recover ${balance.recoverable} sats`); * if (balance.includesSubdust) { * console.log(`This includes ${balance.subdust} sats from subdust virtual outputs`); * } * } * ``` */ getRecoverableBalance(): Promise<{ recoverable: bigint; subdust: bigint; includesSubdust: boolean; vtxoCount: number; }>; /** * Get virtual outputs that are expiring soon based on renewal configuration * * @param thresholdMs - Optional override for threshold in milliseconds * @returns Array of expiring virtual outputs, empty array if renewal is disabled or no virtual outputs expiring * * @example * ```typescript * const wallet = await Wallet.create({ * identity, * arkProvider: new RestArkProvider(), * settlementConfig: { * vtxoThreshold: 86_400 // 24 hours * }, * }); * const manager = await wallet.getVtxoManager(); * const expiringVtxos = await manager.getExpiringVtxos(); * if (expiringVtxos.length > 0) { * console.log(`${expiringVtxos.length} virtual outputs expiring soon`); * } * ``` */ getExpiringVtxos(thresholdMs?: number): Promise; /** * {@link getExpiringVtxos}, against a caller-supplied chain tip. * * The settle paths select, re-select after pre-flight, and sort by expiry within one pass; * each of those judges expiry, so they share one tip rather than fetching (and possibly * disagreeing on) one apiece. Fetches its own when `now` is omitted. */ private selectExpiringVtxos; /** * Renew expiring virtual outputs by settling them back to the wallet's address * * This method collects all expiring spendable virtual outputs (including recoverable ones) and settles * them back to the wallet, effectively refreshing their expiration time. This is the * primary way to prevent virtual outputs from expiring. * * @param eventCallback - Optional callback for settlement events * @param options - Optional per-call overrides; see {@link RenewVtxosOptions} * @returns Settlement transaction ID * @throws Error if no virtual outputs available to renew * @throws Error if total amount is below dust threshold * * @example * ```typescript * const manager = await wallet.getVtxoManager(); * * // Simple renewal * const txid = await manager.renewVtxos(); * * // With event callback * const txid = await manager.renewVtxos((event) => { * console.log('Settlement event:', event.type); * }); * * // Renew only VTXOs that expire within 6 hours * const txid = await manager.renewVtxos(undefined, { thresholdSeconds: 6 * 60 * 60 }); * ``` */ renewVtxos(eventCallback?: (event: SettlementEvent) => void, options?: RenewVtxosOptions): Promise; /** * Get boarding inputs whose timelock has expired. * * These inputs can no longer be onboarded cooperatively via `settle()` and * must be swept back to a fresh boarding address using the unilateral exit path. * * @returns Array of expired boarding inputs * * @example * ```typescript * const manager = await wallet.getVtxoManager(); * const expired = await manager.getExpiredBoardingUtxos(); * if (expired.length > 0) { * console.log(`${expired.length} expired boarding inputs to sweep`); * } * ``` */ getExpiredBoardingUtxos(prefetchedUtxos?: ExtendedCoin[]): Promise; /** * Sweep expired boarding inputs back to a fresh boarding address via * the unilateral exit path (onchain self-spend). * * This builds a raw onchain transaction that: * - Uses all expired boarding inputs as inputs (spent via the CSV exit script path) * - Has a single output to the wallet's boarding address (restarts the timelock) * - Batches multiple expired boarding inputs into one transaction * - Skips the sweep if the output after fees would be below dust * * No Arkade server involvement is needed — this is a pure onchain transaction. * * @returns The broadcast transaction ID * @throws Error if no expired boarding inputs are found * @throws Error if output after fees is below dust (not economical to sweep) * @throws Error if boarding input sweep is not enabled in settlementConfig * * @example * ```typescript * const wallet = await Wallet.create({ * identity, * arkProvider: new RestArkProvider(), * settlementConfig: { * boardingUtxoSweep: true, * }, * }); * const manager = await wallet.getVtxoManager(); * * try { * const txid = await manager.sweepExpiredBoardingUtxos(); * console.log('Swept expired boarding inputs:', txid); * } catch (e) { * console.log('No sweep needed or not economical'); * } * ``` */ sweepExpiredBoardingUtxos(prefetchedUtxos?: ExtendedCoin[]): Promise; /** * Cooperatively migrate VTXOs minted under a now-deprecated server signer * to the wallet's active-signer address. See {@link IVtxoManager}. */ migrateDeprecatedSignerVtxos(options?: MigrateDeprecatedSignerOptions): Promise; /** * Machine-readable status of every deprecated server signer the wallet * currently holds funds under (Section 6), without migrating. Covers both * VTXO and boarding holdings (Section 7), merged per signer. * * @remarks This is no longer a pure repository/info read: surfacing boarding * holdings fans out per boarding address (`getCoins` round trips) and * refreshes the UTXO cache via `saveUtxos`. */ getDeprecatedSignerStatus(): Promise; /** * Core migration routine shared by the manual API and the automatic poll * pass. Fetches a fresh {@link ArkInfo}, applies a mid-session signer * rotation when the wallet's own snapshot signer has been deprecated, * selects spendable VTXOs under deprecated-signer contracts (cutoff-first), * and settles them to the active-signer Ark address. */ private migrateCore; /** * Size and submit one migration leg. Filters inputs whose value alone * exceeds the per-output ceiling (`vtxoMaxAmount`; `< 0` means no limit) — * those can never form a ≤-ceiling output and must exit unilaterally — then * caps the rest (highest-value first; bounded by {@link MAX_VTXOS_PER_SETTLEMENT} * AND a gross total within `vtxoMaxAmount`), applies the protocol dust floor, * and submits the capped batch through `submit`. A throw from `submit` lands * in `error`; the caller's other leg still runs. * * Migration is mandatory and fee-exempt: every selected input moves at its * full value, so the gross total IS the aggregated output amount (kept under * the server ceiling by the cap). The dust floor guards the degenerate cases * where every input was oversized or the whole holding sums below dust. */ private runMigrationLeg; /** * Enumerate the wallet's `default`/`delegate` contracts, classify each * against the fresh signer set, and split their spendable VTXOs into * cooperatively-migratable and cutoff-expired sets while building the * per-signer status report. Current-signer contracts are skipped; swept * (recoverable) VTXOs are excluded from the settle sets — those follow the * recovery path — but are still counted on EXPIRED report rows * (`recoverableCount`) so post-cutoff funds in flight stay visible. */ private classifyDeprecatedSignerContracts; /** * Boarding sibling of {@link classifyDeprecatedSignerContracts} (Section 7): * fan out over the wallet's boarding addresses (current + historical), group * the on-chain UTXOs per address, classify each address's signer against the * fresh signer set, and split the confirmed boarding coins into cooperatively- * migratable and cutoff-expired sets while building the per-signer report. * * Discovery sees the active signer plus EVERY deprecated key (EXPIRED * included), so expired-signer boarding is still reported; migration * eligibility is gated afterwards by {@link isCooperativelyMigratable} and a * per-row boarding-output CSV check — never by the fetch. Current-signer * coins are classified `CURRENT` and ignored; foreign-ASP rows are excluded * because their keys are not in the signer set. */ private classifyDeprecatedSignerBoarding; /** * Automatic migration pass invoked from the poll loop. Self-contained: * respects an exponential cooldown and logs failures rather than throwing, * so a persistently failing migration backs off instead of re-submitting * an identical intent every cycle. */ private runMigrationPass; /** Asserts migration capability and returns the typed wallet. */ private requireMigrationCapableWallet; /** * If the wallet's own construction-time signer snapshot has been deprecated, * re-derive its receive/boarding state under the active signer so any output * built afterwards commits to the active key. No-op when the snapshot is * already current. Returns whether a rotation was applied. Treats an * unknown-signer snapshot as "do not rotate" (caller decides). * * Shared by the migration pass (where the wallet's own snapshot is the thing * being migrated) and the recovery/renewal/periodic-settle paths (via * {@link rotateForRecoverableInputs}), so a swept old-signer VTXO recovered * after cutoff re-mints under the active signer rather than re-committing to * the deprecated key (Section 6 / post-cutoff). `rotateServerSigner` is * idempotent and serializes itself against HD receive rotation, so repeated * calls across passes are safe. */ private ensureReceiveOnActiveSigner; /** * Rotation guard for the recovery-bearing settle paths (recover / renew / * periodic settle). Pins the wallet's receive snapshot to the active signer * before they build their output, but ONLY when this pass actually carries * an input minted under a deprecated signer — so a routine current-signer * settle on a long-lived pre-rotation instance does not eagerly rotate. * * Cheap in the common case: a watch-only/proxy wallet (not migration-capable) * and a current/unknown wallet snapshot both short-circuit before the * contract round-trip, so the only instance that pays for the input scan is * the long-lived deprecated-snapshot one that genuinely needs rotating. * * Runs OUTSIDE any `renewalInProgress` window the caller sets, and * `rotateServerSigner` does not depend on that flag, so it cannot deadlock * against the receive rotator. Returns whether a rotation was applied. */ private rotateForRecoverableInputs; /** * Whether any of the given input outpoints belongs to a contract whose * server signer classifies as non-`CURRENT` against the fresh signer set — * i.e. a deprecated-signer (incl. EXPIRED) input. Maps outpoints to their * owning contract via the ContractManager so it works on the typed * {@link ExtendedVirtualCoin}/{@link ExtendedCoin} inputs the recovery paths * carry (which don't expose `contractScript`). */ private anyInputUnderDeprecatedSigner; /** Asserts sweep capability and returns the typed wallet. */ private getSweepWallet; /** Decodes the boarding tapscript exit path to extract the CSV timelock. */ private getBoardingTimelock; /** Returns the TapLeafScript for the boarding tapscript's exit (CSV) path. */ private getBoardingExitLeaf; /** Returns the onchain provider for fee estimation and broadcasting. */ private getOnchainProvider; /** Returns the Ark provider for intent fee and server info lookups. */ private getArkProvider; /** * Read-only access to the ark provider for fetching server limits. Unlike * {@link getArkProvider}, this does not require full boarding-sweep * capability — recovery and renewal only need it to read `vtxoMaxAmount`. * Returns undefined when no provider is wired, which callers treat as * "no limit". */ private getInfoProvider; /** Returns the Bitcoin network configuration from the wallet. */ private getNetwork; private initializeSubscription; /** * VTXO_ALREADY_SPENT means the server's authoritative view of VTXO state * is ahead of ours — cross-instance race, pre-lock snapshot drift, or an * SSE gap left stale data in the local cache. Silent-swallowing * guarantees the same error on the next cycle because nothing * reconciles the cache. * * The cursor-derived delta sync filters by `created_at`, so a VTXO that * was created before the cursor but spent recently can never be * reconciled by `refreshVtxos()`. Use `refreshOutpoints` for surgical * recovery: query the indexer for the specific stale outpoint and * upsert its authoritative state into the wallet repository. * * Throttled because the same VTXO can fire repeatedly before the * upsert observably propagates through the renewal selector. */ private maybeRefreshAfterVtxoSpent; /** * Extract the offending VTXO outpoint from a `VTXO_ALREADY_SPENT` error, * if the server attached one in `metadata.vtxo_outpoint`. Returns * `undefined` when the error isn't a parsed ArkError, isn't this code, * or doesn't carry the metadata. */ private extractSpentOutpoint; /** * Reconcile the chosen VTXOs with the indexer's authoritative state * before submitting a settle intent. Pulls the canonical record for * each candidate outpoint via {@link IContractManager.refreshOutpoints} * (which upserts the result into the wallet repository), then * re-selects through the standard expiring-vtxo filter so anything * the refresh flagged as spent is dropped. * * Best-effort: a failed refresh just falls back to the original * candidates and lets the post-submit `VTXO_ALREADY_SPENT` recovery * handle whatever slipped through. */ private revalidateBeforeSettle; /** Computes the next poll delay, applying exponential backoff on failures. */ private getNextPollDelay; /** * Starts a polling loop that: * 1. Auto-settles new boarding inputs into Arkade * 2. Sweeps expired boarding inputs (when boardingUtxoSweep is enabled) * * Uses setTimeout chaining (not setInterval) so a slow/blocked poll * cannot stack up and the next delay can incorporate backoff. */ private startBoardingUtxoPoll; private schedulePoll; private pollBoardingUtxos; /** * Auto-settle new (unexpired) boarding inputs AND near-expiry VTXOs into * Arkade in a single intent. Skips boarding UTXOs that are already expired * (those are handled by sweep) and those already in-flight (tracked in * knownBoardingUtxos). If the event-driven renewal path is currently * running, VTXOs are omitted from this cycle to avoid double-spending. * * Failure bookkeeping: after every settle *attempt*, lastPeriodicSettleTimestamp * is armed and consecutive failures are counted so the next attempt is * blocked by an exponentially growing cooldown (capped). This stops a * persistently failing input from producing identical RegisterIntent + * DeleteIntent retries on every 60s poll. */ private runPeriodicSettle; dispose(): Promise; [Symbol.asyncDispose](): Promise; } interface WalletState { /** Arbitrary stored wallet settings. */ settings?: Record; /** * High-water mark for VTXO indexer syncs, in milliseconds. * * Reused the legacy `lastSyncTime` column name to avoid an * `ALTER TABLE` migration; the value is interpreted as the new * "max indexer `updatedAt`" cursor only after `settings.vtxoCursorMigrated` * is set, so pre-existing values written by the buggy pre-PR sync * are ignored and force a one-shot re-bootstrap on upgrade. */ lastSyncTime?: number; } interface VtxoRepositoryKey { /** Authoritative ownership key. */ script: string; /** Legacy storage bucket. Required by all current backends; throw if absent. */ address?: string; } interface WalletRepository extends AsyncDisposable { readonly version: 1; /** * Clear all data from storage. */ clear(): Promise; /** Fetch stored virtual outputs for an address. */ getVtxos(address: string): Promise; /** Save virtual outputs for an address. */ saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise; /** Delete stored virtual outputs for an address. */ deleteVtxos(address: string): Promise; /** * Fetch stored virtual outputs for a script. * Unspent-only reads use {@link getVtxosForScripts} when present, so it can skip spent rows. * @optional SDK backends implement this; custom backends fall back to Tier 1. */ getVtxosForScript?(script: string): Promise; /** Fetch a script set without one read per contract; `unspentOnly` omits spent rows. */ getVtxosForScripts?(scripts: string[], options?: { unspentOnly?: boolean; }): Promise; /** * Save virtual outputs for a script. * @optional SDK backends implement this; custom backends fall back to Tier 1. */ saveVtxosForScript?(key: VtxoRepositoryKey, vtxos: ExtendedVirtualCoin[]): Promise; /** * Delete stored virtual outputs for a script. * @optional SDK backends implement this; custom backends fall back to Tier 1. */ deleteVtxosForScript?(script: string): Promise; /** Fetch stored boarding inputs for an address. */ getUtxos(address: string): Promise; /** Save boarding inputs for an address. */ saveUtxos(address: string, utxos: ExtendedCoin[]): Promise; /** Delete stored boarding inputs for an address. */ deleteUtxos(address: string): Promise; /** Fetch stored transaction history for an address. */ getTransactionHistory(address: string): Promise; /** Save transaction history for an address. */ saveTransactions(address: string, txs: ArkTransaction[]): Promise; /** Delete stored transaction history for an address. */ deleteTransactions(address: string): Promise; /** Fetch stored wallet state. */ getWalletState(): Promise; /** Save wallet state. */ saveWalletState(state: WalletState): Promise; } /** * Filter options for querying contracts. */ interface ContractFilter { /** Filter by script(s) */ script?: string | string[]; /** Filter by state(s) */ state?: ContractState | ContractState[]; /** Filter by contract type(s) */ type?: string | string[]; /** * Filter by watch state(s). Rows written before the field existed * have no stored value and match `"watched"`. * @see ContractWatchState */ watch?: ContractWatchState | ContractWatchState[]; } interface ContractRepository extends AsyncDisposable { /** * 2 — {@link Contract.watch}. An implementation must persist and * round-trip it, and treat a row without one as `"watched"`. */ readonly version: 2; /** * Clear all data from storage. */ clear(): Promise; /** * Get contracts with optional filter. * Returns all contracts if no filter provided. */ getContracts(filter?: ContractFilter): Promise; /** * Save or update a contract. */ saveContract(contract: Contract): Promise; /** * Delete a contract by script. */ deleteContract(script: string): Promise; } type ArkIntentState = "waiting_to_submit" | "waiting_for_batch" | "batch_in_progress" | "batch_failed" | "batch_succeeded" | "cancelled"; declare const INTENT_TERMINAL_STATES: ReadonlySet; declare function isTerminalIntentState(s: ArkIntentState): boolean; interface ArkIntent { /** Intent proof tx id. Primary key. */ intentTxId: string; /** * Server-assigned batch intent id. Unique when present: saving a *different* * intentTxId that reuses a live intentId is rejected (NArk parity). Absent * until the intent is registered with arkd. */ intentId?: string; state: ArkIntentState; validFrom?: number; validUntil?: number; createdAt: number; updatedAt: number; /** hex PSBT. */ registerProof: string; /** canonical JSON signed by the register proof. */ registerProofMessage: string; /** hex PSBT. */ deleteProof: string; deleteProofMessage: string; batchId?: string; commitmentTransactionId?: string; cancellationReason?: string; partialForfeits: string[]; signerDescriptor?: string; /** Input VTXOs locked by this intent. */ intentVtxos: Outpoint[]; } interface IntentFilter { intentTxIds?: string[]; intentIds?: string[]; /** Intents whose `intentVtxos` intersect any of these outpoints. */ containingInputs?: Outpoint[]; states?: ArkIntentState[]; /** Intents valid at this ms-epoch instant (null bounds = open). */ validAt?: number; /** Substring match over intentId, batchId, commitmentTransactionId. */ searchText?: string; skip?: number; take?: number; } interface IntentRepository extends AsyncDisposable { readonly version: 1; clear(): Promise; /** Upsert by `intentTxId`; implementation sets `updatedAt = Date.now()`. */ saveIntent(intent: ArkIntent): Promise; getIntents(filter?: IntentFilter): Promise; /** * Outpoints held by NON-terminal intents — `waiting_to_submit`, * `waiting_for_batch`, and `batch_in_progress` — to exclude from spendable * balance. Terminal states never lock. * * Deliberate divergence from NArk EF storage, whose `GetLockedVtxoOutpoints` * locks only `WaitingToSubmit`/`WaitingForBatch`: the TS wallet balance is * offline-first and reads this set as its single source of coin locking, so * it must also hold inputs already committed to an in-progress batch. NArk * excludes `BatchInProgress` here but protects those inputs through its * active-intent services instead. */ getLockedVtxoOutpoints(): Promise; } /** * Type of a tx inside a VTXO's chain (commitment → leaf), as reported by * arkd's chain indexer. Values pinned verbatim from NArk `ChainedTxType.cs`. */ declare enum ChainedTxType { Unspecified = 0, Commitment = 1, Ark = 2, Tree = 3, Checkpoint = 4 } /** A single virtual transaction in the VTXO tree. */ interface VirtualTx { /** Transaction id (hex). Primary key. */ txid: string; /** Base64-encoded PSBT body; null when only metadata has been cached. */ psbt: string | null; /** Operator pre-signature expiry, ms epoch; null if not applicable. */ expiresAt: number | null; /** Tx type as reported by arkd's chain indexer. */ type: ChainedTxType; } /** * Links a VTXO to one virtual tx in its exit branch, ordered. * `position` 0 = closest to commitment (tree root); higher = closer to leaf. */ interface VtxoBranch { vtxoTxid: string; vtxoVout: number; virtualTxid: string; position: number; } interface VirtualTxRepository extends AsyncDisposable { readonly version: 1; clear(): Promise; /** Upsert: non-null incoming fields overwrite; null (or, for `type`, * `Unspecified`) is treated as absent and preserves the stored value. */ upsertVirtualTxs(txs: VirtualTx[]): Promise; getVirtualTx(txid: string): Promise; /** Replace the entire stored branch for a VTXO. */ setBranch(vtxo: Outpoint, branch: VtxoBranch[]): Promise; /** Resolved virtual txs in the VTXO's branch, ordered by position asc. */ getBranch(vtxo: Outpoint): Promise; hasBranch(vtxo: Outpoint): Promise; /** Drop branch rows for a spent VTXO, then delete now-orphaned VirtualTx rows. */ pruneForSpentVtxo(vtxo: Outpoint): Promise; } /** One transaction's participation in one logical action. */ interface GroupMembership { /** * Stable id of the action; txs sharing it group together. Third-party * resolvers should namespace it (`"vendor:thing"`) to avoid colliding with * other resolvers' groups. SDK built-ins use namespaced ids such as `boarding:`. * A membership with an empty groupId is dropped. */ groupId: string; /** Human label for the action, e.g. "Dice game". */ label?: string; /** App category for icon/filtering, e.g. "game". */ kind?: string; /** * Outcome of the action this membership belongs to, e.g. "failed", * "refunded" — an opaque machine token for the app to map to its own * copy or icon, not display text. Merged first-writer-wins across * resolvers sharing a `groupId`, like `label` and `kind`. */ outcome?: string; /** * Free-form row data. Same-group metadata is shallow-merged with * earlier-resolver keys winning. */ metadata?: Record; /** * This tx's unsigned sat contribution to this group. Defaults to the tx's * full amount; the builder applies direction. Use it to split a batched tx * across groups. Same-key receive rows paired with a sent row are treated * as change and excluded from `Activity.amount`. A membership with a * non-finite amount (NaN/Infinity) is dropped. */ amount?: number; } /** A pluggable resolver keyed by `id`. */ interface ActivityResolver { /** * Registry key — override or remove by it. Namespace it (`"vendor:games"`) * so independent libraries don't clobber each other; `use()` overwrites * silently on a duplicate id. */ id: string; /** * Load correlation data before `resolve` runs. If it rejects, this resolver * contributes no memberships. */ prepare?(): Promise; /** Pure and synchronous. The groups this tx belongs to, or undefined to leave it plain. */ resolve(tx: ArkTransaction): GroupMembership[] | undefined; } /** The non-id, non-amount part of a {@link GroupMembership}. */ interface ActivityIntent { /** Human label for the action, e.g. "Dice game". */ label?: string; /** App category for icon/filtering, e.g. "game". */ kind?: string; /** * Resolver-declared outcome for the group, e.g. "failed", "refunded" — an * opaque machine token for the app to map to its own copy or icon, not * display text. */ outcome?: string; /** Free-form row data, shallow-merged across the group's resolvers (first-writer-wins). */ metadata?: Record; } /** One logical activity. */ interface Activity { /** The groupId, or the natural tx key for untagged rows. */ id: string; /** Merged intent for the group, if any resolver tagged it. */ intent?: ActivityIntent; /** Member txs, oldest-first. */ txs: ArkTransaction[]; /** Signed net sats: positive received, negative sent; same-key change rows are excluded. */ amount: number; /** Earliest member createdAt (ms since epoch). */ createdAt: number; /** True once every member tx is settled. */ settled: boolean; } /** Resolver registry keyed by id. */ declare class ActivityRegistry { private readonly resolvers; /** Add a resolver, or override an existing one with the same id (kept in place). */ use(resolver: ActivityResolver): void; /** Remove a resolver (built-in or custom) by id. */ remove(id: string): void; /** The registered resolver ids, in registration order. */ list(): string[]; /** All registered resolvers, in registration (priority) order. */ all(): ActivityResolver[]; } /** Built-in resolver: labels on-chain boarding (deposit) transactions. */ declare function boardingResolver(): ActivityResolver; /** Built-in resolver: labels collaborative exits (VTXOs forfeited to chain in a batch). */ declare function collabExitResolver(): ActivityResolver; /** * Built-in resolver: labels the genesis transaction of a minted asset — "Asset * mint" on the issuer's sent tx, "Asset receive" when the fresh supply arrives * in the genesis tx itself. An asset id encodes its genesis txid; reissues and * transfers carry the asset under a different `arkTxid`, so they are left plain. * * `metadata.amount` is the decimal string of the asset's `bigint` amount, kept * as a string so large supplies survive JSON round-trips without truncation. * Recover the value with `BigInt(metadata.amount as string)` — using it * directly in arithmetic coerces (`"10" + 1` is `"101"`, not `11`) and loses * precision past `Number.MAX_SAFE_INTEGER`. */ declare function assetMintResolver(): ActivityResolver; /** * A registry pre-populated with the SDK's built-in resolvers: `boarding`, * `collab-exit`, and `asset-mint`. */ declare function createDefaultActivityRegistry(): ActivityRegistry; /** * A source of unilateral-exit chain data for a set of VTXOs. Sources are tried * in order by the resolver; a source returns `null` (chain) or omits keys (psbts) * for data it cannot supply — a "miss" — and the resolver falls through. */ interface ExitDataSource { readonly name: string; /** Full ancestry chain for a vtxo (all pages merged), or null on a miss. */ getVtxoChain(vtxo: Outpoint): Promise; /** Base64 PSBTs this source has, keyed by (unsigned) txid. Absent key = miss. */ getVirtualTxs(txids: string[]): Promise>; } /** Reads exit chain data through an ordered chain of sources. */ interface ExitChainResolver { getVtxoChain(vtxo: Outpoint): Promise; getVirtualTxs(txids: string[]): Promise; } /** * Assemble the standard exit-data resolver: local repo (if configured) → any * extra sources (e.g. a future provider) → indexer. Read-through persists to the * repository. With no repository this is exactly the indexer path (a no-op seam). */ declare function createExitChainResolver(params: { indexer: IndexerProvider; repository?: VirtualTxRepository; extraSources?: ExitDataSource[]; }): ExitChainResolver; type ExitCaptureMode = "lite" | "full"; /** * Delegate identity and fee information returned by `getDelegateInfo`. */ interface DelegateInfo { /** Delegate public key. */ pubkey: string; /** Delegate fee amount or expression returned by the delegate. */ fee: string; /** Address for delegate fee collection. Sourced from `delegatorAddress` in Fulmine response, for now. */ delegateAddress: string; /** @deprecated alias for @see DelegateInfo.delegateAddress */ delegatorAddress?: string; } /** * Optional delegate behavior flags. */ interface DelegateOptions { /** * Instruct the delegate not to replace an existing delegation * (meaning a signed register intent and its forfeit transactions) * that already includes at least one virtual output from this request. * * @defaultValue `false` */ rejectReplace?: boolean; } /** * Provider interface for remote delegation service. */ interface DelegateProvider { /** * Request delegation for a signed register intent and its forfeit transactions. * * @param intent - Signed register intent to delegate * @param forfeitTxs - Forfeit transactions associated with the delegation request * @param options - Optional delegate behavior flags */ delegate(intent: SignedIntent, forfeitTxs: string[], options?: DelegateOptions): Promise; /** * Fetch delegate metadata such as pubkey, fee, and delegate address. * * @returns Delegate identity and fee information */ getDelegateInfo(): Promise; } /** @deprecated alias for @see DelegateProvider */ type DelegatorProvider = DelegateProvider; /** * REST-based delegate provider implementation. * @example * ```typescript * const provider = new RestDelegateProvider('https://delegate.example.com'); * const info = await provider.getDelegateInfo(); * await provider.delegate(intent, forfeitTxs); * ``` */ declare class RestDelegateProvider implements DelegateProvider { url: string; /** * Create a REST delegate provider targeting the given base URL. * * @param url - Base URL of the remote delegation service. */ constructor(url: string); /** * Submit a delegation request to the remote delegation service. * * @param intent - Signed register intent to delegate * @param forfeitTxs - Forfeit transactions associated with the delegation request * @param options - Optional delegate behavior flags * @throws Error if the remote service rejects the request */ delegate(intent: SignedIntent, forfeitTxs: string[], options?: DelegateOptions): Promise; /** * Fetch delegate metadata exposed by the remote delegation service. * * @returns Delegate identity and fee information * @throws Error if the remote service returns invalid data */ getDelegateInfo(): Promise; } /** @deprecated alias for @see RestDelegateProvider */ declare const RestDelegatorProvider: typeof RestDelegateProvider; type RestDelegatorProvider = RestDelegateProvider; interface IDelegateManager { /** * Delegate virtual outputs to the remote delegation service. * * Vtxos that are not locked to a delegate-type contract (no tap leaf * matches the delegate's pubkey) are filtered out silently, since they * cannot be co-signed by the delegate. * * @param vtxos - Virtual outputs to delegate * @param destination - Arkade address that should receive renewed funds * @param delegateAt - Optional timestamp to force a specific delegation time * @returns Successfully delegated and failed outpoint groups */ delegate(vtxos: ContractVtxo[], destination: string, delegateAt?: Date): Promise<{ delegated: Outpoint[]; failed: { outpoints: Outpoint[]; error: unknown; }[]; }>; /** Fetch delegate metadata such as pubkey, fee, and delegate address. */ getDelegateInfo(): Promise; } /** @deprecated alias for @see IDelegateManager */ type IDelegatorManager = IDelegateManager; declare class DelegateManagerImpl implements IDelegateManager { readonly delegateProvider: DelegateProvider; readonly arkInfoProvider: Pick; readonly identity: Identity; /** Create a delegate manager from the configured provider, Arkade info source, and wallet identity. */ constructor(delegateProvider: DelegateProvider, arkInfoProvider: Pick, identity: Identity); getDelegateInfo(): Promise; delegate(vtxos: ContractVtxo[], destination: string, delegateAt?: Date): Promise<{ delegated: Outpoint[]; failed: { outpoints: Outpoint[]; error: unknown; }[]; }>; } /** @deprecated alias for @see DelegateManagerImpl */ declare const DelegatorManagerImpl: typeof DelegateManagerImpl; type DelegatorManagerImpl = DelegateManagerImpl; /** * Tapscript fields derived purely from a contract's params. They are identical * for every VTXO locked to the same contract, so they can be memoized per * contract (see {@link ContractTapscriptCache}). */ type ContractTapscripts = Pick; /** * Cache of per-contract tapscript data, keyed by `contract.script`. Building * the taproot tree via `handler.createScript(contract.params)` is the dominant * cost when annotating large VTXO sets; passing a shared cache across an * annotation batch rebuilds it once per distinct contract instead of once per * VTXO (see #521). */ type ContractTapscriptCache = Map; declare function getRandomId(): string; /** * What a recipient Arkade address must match. An address failing either check * belongs to another network or operator, so this wallet's operator cannot * create the VTXO where the recipient's wallet expects it. */ type RecipientAddressContext = { hrp: string; signerSet: SignerSet; }; /** * An input for {@link IContractManager.assertSpendableNow}. * * The outpoint and script are what identify the owning contract. A full * {@link VirtualCoin} is accepted and preferred: a relative (CSV) timelock is * measured from this coin's own confirmation, so `status.block_height` / * `status.block_time` are the only way to answer one. Pass the coin where you * have it; the bare shape still answers every absolute (CLTV) question. */ type AssertSpendableInput = { txid: string; vout: number; script: string; }; type RefreshVtxosOptions = { /** * Narrow the refresh to these scripts. A subset query, so the * cursor is not advanced: contracts outside the list may have data * we'd skip. */ scripts?: string[]; /** * Time window overriding the cursor-derived one. The cursor never * advances on a windowed query because the window may skip data * outside its bounds. */ after?: number; /** @see after */ before?: number; /** * When true and `scripts` is not set, refresh every contract in * the repository rather than the watcher's watched set — which * differs only for rows the watcher never registered, since * retirement doesn't narrow that set * (see {@link ContractWatcher.getWatchedContracts}). * * Because this is a *superset* of the watched set, the cursor * invariant still holds and the cursor advances normally (unless * `after` / `before` is also supplied). * * @defaultValue `false` */ includeInactive?: boolean; }; /** * A single `Discoverable` handler's discovery failure, captured during a * {@link IContractManager.scanContracts} run instead of aborting the loop. * * TODO(next major): rename `index` → `fromIndex` so the pair reads * `fromIndex`/`toIndex`. It stays `index` here only to keep this exported * shape backward-compatible. */ interface HandlerError { handler: string; /** The failed index, or the first index of a failed `discoverRange` window. */ index: number; /** Inclusive end of a failed `discoverRange` window; absent for a single index. */ toIndex?: number; error: unknown; } /** * Outcome of a {@link IContractManager.scanContracts} run. */ interface ScanResult { /** @deprecated Alias of {@link ScanResult.highestConfirmedUsedIndex}. */ lastIndexUsed: number; /** * Highest HD index at which any handler confirmed a contract (`-1` if none), * including hits past {@link ScanResult.truncatedAt}. Safe to record * unconditionally: the HD watermark it feeds is a monotonic max over a scan * that always restarts at 0, so it cannot skip an index — while withholding * it risks re-issuing a funded index as a fresh receive address. */ highestConfirmedUsedIndex: number; /** * First index a handler failed at, making it *indeterminate* — neither a hit * nor a confirmed miss. The scan stops there, so indices `>= truncatedAt` are * unverified and the caller must retry (scanning is idempotent). `undefined` * when the scan closed a genuine gap. */ truncatedAt?: number; /** Per-handler discovery failures. Non-empty implies `truncatedAt` is set. */ handlerErrors: HandlerError[]; } /** * Options for {@link IContractManager.scanContracts}. */ interface ScanContractsOptions { /** Default 20. A non-positive / non-integer value throws. */ gapLimit?: number; /** * Number of HD indices probed per window (default * {@link DEFAULT_SCAN_BATCH}). The gap loop stays gap-limit bounded and * the discovered set is identical regardless of batch size; the window is * also the unit a batching handler ({@link Discoverable.discoverRange}) * collapses into one request, so it doubles as the batch width. A * non-positive / non-integer value throws. Ignored when `hd` is false (the * static pass probes only index 0). */ batchSize?: number; /** HD mode → unbounded gap loop guided by the gap counter; false → probe only index 0 (single static pass). */ hd: boolean; /** * Materialize the descriptor at an HD index. Pure derivation; a throw * here is structural/fatal and propagates out of `scanContracts`. */ materialize: (index: number) => string; /** Read-only context injected into every `discoverAt` call. */ deps: DiscoveryDeps; } /** * Freshness of the ContractManager's provider-backed sync. `degraded` means the * most recent sync (boot, best-effort read, or contract hydration) hit a * retryable indexer/operator failure and the manager is serving repository * state; it returns to `online` on the next successful sync. This only * describes sync freshness — never wallet data itself. */ type ContractSyncState = { mode: "online"; lastSyncedAt?: number; } | { mode: "degraded"; reason: string; lastSyncedAt?: number; }; interface IContractManager extends Disposable { /** * Create and register a new contract. * * Implementations may validate that: * - A handler exists for `params.type` * - `params.script` matches the script derived from `params.params` * * The contract script is used as the unique identifier. */ createContract(params: CreateContractParams): Promise; /** * {@link createContract} for a set, one indexer round trip instead of N. * Optional so adding it breaks no implementer of this public interface * (`serviceWorker/wallet.ts` proxies each method over wire): fall back. */ createContracts?(paramsList: CreateContractParams[]): Promise; /** * List contracts with optional filters. * * @example * ```typescript * const vhtlcs = await manager.getContracts({ type: "vhtlc" }); * const active = await manager.getContracts({ state: "active" }); * ``` */ getContracts(filter?: GetContractsFilter): Promise; /** * List contracts and their current virtual outputs. * * If no filter is provided, returns all contracts with their virtual outputs. * `unspentOnly` omits spent VTXOs from the repository result; it * does not narrow the provider sync or change the full-history default. */ getContractsWithVtxos(filter?: GetContractsFilter, pageSize?: number, options?: { maxSyncAgeMs?: number; unspentOnly?: boolean; requireSynced?: boolean; }): Promise; /** * Latest provider-sync health (online vs. degraded to repository data). * See {@link ContractSyncState}. */ getSyncState(): ContractSyncState; /** * 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. */ annotateVtxos(vtxos: VirtualCoin[], tapscripts?: ContractTapscriptCache): Promise; /** * 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. */ assertAnnotatable(vtxos: readonly { txid: string; vout: number; script: string; }[]): Promise; /** * Throw when one of `vtxos` belongs to a contract that provably cannot be * spent right now, asking each owning handler's * {@link ContractHandler.assertSpendableNow}. * * The complement of {@link 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. */ assertSpendableNow?(vtxos: readonly AssertSpendableInput[], walletPubKey?: () => Promise): Promise; /** * 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 {@link 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 {@link assertSpendableNow} is. */ unspendableNowReasons?(vtxos: readonly AssertSpendableInput[], walletPubKey?: () => Promise): Promise>; /** * Update mutable contract fields. * * `script` and `createdAt` are immutable. */ updateContract(script: string, updates: Partial>): Promise; /** * Convenience helper to update only the contract state. Note * `inactive` governs receive-address selection and does not stop * watching; see {@link ContractState} and * {@link setContractWatchState}. */ setContractState(script: string, state: ContractState): Promise; /** * Convenience helper to update only the contract's watch state. * * `retained` is how an owner says "this script is done": it leaves * the subscription and the sweep, while the row — and so history, * annotation and restore — is untouched. `awaiting-funds` asks for * coverage only until the script is funded, after which the manager * demotes it to `retained` itself. * * @see ContractWatchState */ setContractWatchState(script: string, watch: ContractWatchState): Promise; /** * Delete a contract by script, dropping both the row and the watch. * Destructive: the row is what keeps the contract's VTXOs * annotatable and its transactions readable in history, so to stop * watching a finished contract use * {@link setContractWatchState}(`"retained"`) instead. */ deleteContract(script: string): Promise; /** * Get all currently spendable paths for a contract. * * Returns an empty array if the contract or its handler cannot be found. */ getSpendablePaths(options: GetSpendablePathsOptions): Promise; /** * Get all possible spending paths for a contract. * * Returns an empty array if the contract or its handler cannot be found. */ getAllSpendingPaths(options: GetAllSpendingPathsOptions): Promise; /** * Subscribe to contract events. * * @returns Unsubscribe function */ onContractEvent(callback: ContractEventCallback): () => void; /** * Force a virtual output refresh from the indexer. * * Without options, refreshes all contracts from scratch. * With options, narrows the refresh to specific scripts and/or a time window. */ refreshVtxos(opts?: RefreshVtxosOptions): Promise; /** * 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. */ refreshOutpoints(outpoints: Outpoint[]): Promise; /** * Rebuild the HD look-ahead watch window around the current allocation * watermark. No-op when the manager was configured without `lookAhead`. * * Call after anything that moves the watermark (restore, boarding * allocation, receive rotation, server-signer rotation). Concurrent calls * coalesce into a single drain. */ refillLookAhead(): Promise; /** * Allocate the next signing descriptor through the manager-owned HD * watermark path. Returns `undefined` when look-ahead/allocation is not * configured. */ getNextSigningDescriptor(): Promise; /** * Advance the HD signing descriptor watermark to `index` and refill the * watched look-ahead band. No-op when look-ahead/allocation is not configured. */ advanceSigningDescriptorWatermark(index: number): Promise; /** * Explicit, gap-limit contract discovery used by `wallet.restore()`. * * Walks HD indices from 0, asking every registered `Discoverable` * handler whether it owns a contract anchored at that index, and * registers each find via the idempotent {@link createContract}. A hit * at index `i` (by any handler, including an injected swap handler) * resets the gap counter, so swap discovery keeps the HD window open. * * Error contract (safety-critical — see spec §4): * - A handler's discovery rejecting is **collected** into `handlerErrors` * and makes its index *indeterminate*: it never advances the gap * counter, and the scan **stops verifying** there rather than closing a * window it never observed close. A batched * {@link Discoverable.discoverRange} failure makes its whole requested * range indeterminate. It still never throws. * - A fatal operational error — `materialize()` throwing, or * `createContract` rejecting — **propagates** out of `scanContracts` * (it invalidates the gap-window signal, so a silent truncation * would risk hiding user funds). * * @param opts See {@link ScanContractsOptions}. * @returns See {@link ScanResult}. The caller surfaces `truncatedAt` / * `handlerErrors` *after* the inline VTXO pull. */ scanContracts(opts: ScanContractsOptions): Promise; /** * Report VTXO activity at `script` without registering it as a contract, * and without the wallet owning it. Activity arrives as * {@link ContractEvent} `vtxo_received` / `vtxo_spent` without the * `contract` — narrow with {@link isContractVtxoEvent}; * nothing is persisted and the outputs never enter the wallet's balance, * renewal or recovery paths. * * At-least-once, and re-announces on restart — the registration is * in-memory, so there is no baseline to diff a restart against. * Deduplicate by outpoint, and tolerate a `vtxo_spent` with no * preceding `vtxo_received` — an output can be created and spent * inside one gap in the stream. Re-registering a watched script is a * no-op, so a caller may re-derive its whole set on a timer. * * Reports **spendable** outputs: a preconfirmed one counts, so a fresh * funding is not missed, but one already recoverable or swept is not * reported. * * Takes one script or a set. A set costs one subscription update and one * indexer read however many scripts it carries, which is the difference * between registering and re-registering cheaply and paying N round trips * — what a service watching an address per payment does on every restart. * * Optional so adding it does not break an embedder with its own * `IContractManager`; both shipped implementations provide it for real. */ watchScript?(script: string | string[], options?: { label?: string; }): Promise; /** Stop watching a script registered via {@link watchScript}. Takes one or * a set, and rebuilds the subscription once either way. */ unwatchScript?(script: string | string[]): Promise; /** * Every script registered via {@link watchScript}. Async for the same * reason {@link isWatching} is: a service worker answers over the bus. */ getWatchedScripts?(): Promise; /** * Whether the underlying watcher is currently active. */ isWatching(): Promise; /** * Release resources (stop watching, clear listeners). */ dispose(): void; } /** * Options for getting spendable paths. */ type GetSpendablePathsOptions = { /** The contract script */ contractScript: string; /** The specific virtual output being evaluated */ vtxo: VirtualCoin; /** Whether collaborative spending is available (default: true) */ collaborative?: boolean; /** Wallet's public key (hex) to determine role */ walletPubKey?: string; }; /** * Options for getting all possible spending paths. */ type GetAllSpendingPathsOptions = { /** The contract script */ contractScript: string; /** Whether collaborative spending is available (default: true) */ collaborative?: boolean; /** Wallet's public key (hex) to determine role */ walletPubKey?: string; }; /** * Configuration for the ContractManager. */ interface ContractManagerConfig { /** The indexer provider */ indexerProvider: IndexerProvider; /** The contract repository for persistence */ contractRepository: ContractRepository; /** The wallet repository for virtual output storage (single source of truth) */ walletRepository: WalletRepository; /** * Optional intent store. When present, the online sync path reconciles * persisted non-terminal settlement intents against authoritative indexer * state (crash recovery) on boot and reconnect — see * {@link reconcileIntents}. Absent ⇒ no-op. */ intentRepository?: IntentRepository; /** * Optional exit-data capture hook. Fired best-effort after VTXOs are * persisted so a configured virtualTxRepository can store each one's * unilateral-exit branch. Absent ⇒ no-op. */ onVtxosPersisted?: (contract: Contract, vtxos: ExtendedVirtualCoin[]) => Promise; /** * Optional exit-data prune hook. Fired best-effort with the spent outpoints * on `vtxo_spent` so a configured virtualTxRepository can drop their branch. * Absent ⇒ no-op. */ onVtxosSpent?: (vtxos: Outpoint[]) => Promise; /** Watcher configuration */ watcherConfig?: Partial; /** * Enables the HD look-ahead watch window. Absent ⇒ feature off (static / * non-HD wallets, third-party embedders). See * {@link ContractManager.refillLookAhead}. */ lookAhead?: LookAheadConfig; /** * Current chain tip height, for the `blockHeight` a {@link PathContext} * carries. Absent, or resolving `undefined`, leaves `blockHeight` unset. * * `isCltvSatisfied` answers `false` outright for a height-typed locktime * when `blockHeight` is missing, so every such path was reported * unspendable however mature it was. Nothing populated this before, which * made that the only behaviour available. Seconds-typed locktimes read * `currentTime` and are unaffected either way. * * Block-typed CSV is not fixed by this. `isCsvSpendable` also needs the * VTXO's confirmation height, and `status.block_height` is never populated * for a virtual coin, so it stays `false` regardless of the tip. * * Both fields matter. `height` answers height-typed timelocks; `time` (the * tip's timestamp, in SECONDS) is what seconds-typed ones should be judged * against, because the machine's clock is an estimate of chain time and a * drifting one reads the boundary wrong. * * Resolve `undefined` rather than rejecting when the tip cannot be read: * the callers treat it as "unknown", which is the pre-existing behaviour, * and a path query is not worth failing over a provider hiccup. */ chainTip?: () => Promise<{ height: number; time: number; } | undefined>; /** * How stale {@link ContractManager.getContractsWithVtxos} may let its * opportunistic sync be before repeating it. `0` (default) repeats it every * call. A send syncs, selects, then submits, so its coins are already one * submit latency stale; this widens that bound by this value, and is * therefore a budget rather than a free saving. */ vtxoSyncMaxAgeMs?: number; } /** * Wallet-injected surface backing the HD look-ahead window. Kept as a * callback bundle so the contracts layer never learns what an HD descriptor * is (mirrors {@link ScanContractsOptions.materialize}). */ interface LookAheadConfig { /** Per-side band bound: the window spans `[max(0, w - size), w + size]`. */ size: number; /** Current allocation watermark (`lastIndexUsed ?? -1`). */ currentWatermark(): Promise; /** Allocate the next signing descriptor, advancing the watermark. */ allocate?(): Promise; /** Advance the allocation watermark to a confirmed/restored index. */ advanceWatermark?(index: number): Promise; /** Signing descriptor at an HD index. Pure derivation. */ materialize(index: number): string; /** * Key and timelock axes an externally issued receive script could be * anchored to. Read once per refill, so a band rebuilt after * `rotateServerSigner` fans the new signer set. */ candidateDeps(): CandidateDeps; /** * Fired after a speculative entry at `index` is promoted to a real row. * @deprecated Use `advanceWatermark`; kept for external LookAheadConfig users. */ onPromoted?(index: number): Promise; } /** * Parameters for creating a new contract. */ type CreateContractParams = Omit & { /** Initial state (defaults to "active") */ state?: ContractState; }; /** * 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). * * @example * ```typescript * 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(); * ``` */ declare class ContractManager implements IContractManager { private config; private watcher; /** Per-sync tapscript caches start empty; this outlives them. Consumers only ever see clones. */ private readonly tapscriptMemo; private initialized; private eventCallbacks; private stopWatcherFn?; /** `undefined` while online; the failure reason once a sync degrades. */ private syncDegradedReason?; /** Epoch-ms of the last successful provider sync, if any. */ private lastSyncedAt?; private syncedAtByScript; /** Last chain tip read, with the epoch-ms it was read at. @see currentChainTip */ private chainTipCache?; /** In-flight chain tip read, so concurrent cache misses share one. */ private chainTipInflight?; /** Speculative look-ahead scripts, keyed by script. @see LookAheadEntry */ private lookAheadEntries; /** In-flight look-ahead drain, if any. @see scheduleLookAheadDrain */ private lookAheadDrain?; /** A refill was requested while a drain was running. */ private lookAheadDirty; /** A fire-and-forget drain failed, so the band is behind the watermark and * owes a retry. @see requestLookAheadDrain */ private lookAheadRefillOwed; /** Set by {@link dispose}, cleared by a re-`initialize`. A drain is a * fire-and-forget async loop that outlives the synchronous `dispose()`, * so it re-checks this at every await boundary instead of running on * against a torn-down watcher. */ private disposed; private constructor(); /** * Static 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. * * @param config ContractManagerConfig */ static create(config: ContractManagerConfig): Promise; /** * Latest provider-sync health. See {@link ContractSyncState}. Degradation is * recorded by {@link initialize}, {@link getContractsWithVtxos}, and * {@link createContract}; it flips back to `online` on the next successful * sync. Purely a freshness signal — not a source of truth for wallet data. */ getSyncState(): ContractSyncState; /** @see ContractManagerConfig.vtxoSyncMaxAgeMs — for factory-built managers. */ setVtxoSyncMaxAge(maxAgeMs: number): void; private markSyncOnline; /** * Why a sync that just succeeded still has not covered everything it should * have. Both reasons outlive the sync that failed to cover them, so neither * is discharged by the next one succeeding — which is the point: a wallet * that reported itself current while the look-ahead band sat behind the * watermark, or while a contract could not be annotated, would be claiming * coverage it never had. `getSyncState()` is the channel apps already watch. */ private syncGapReason; /** Contracts a sync could not annotate, as `script → reason`. */ private annotationFailures; private annotationDegradedReason; /** * Fold one batch's verdict in: what it annotated clears, what it could not * sets. Merged rather than replaced because a batch can cover a subset of * the wallet's contracts (a single-contract fetch, the pending-only * reconcile), and those must not erase what a wider sync found. A row * repaired by an upgrade clears itself on the next batch that includes it. */ private recordAnnotationFailures; private markSyncDegraded; private initialize; /** * Delta-sync the full watched set and reconcile the pending frontier. * * Shared recovery path used on initial boot and after a subscription * reconnect. `syncContracts({})` scopes to the current watched set * (see {@link ContractWatcher.getWatchedContracts}), uses the * cursor-derived delta window, and advances the cursor on success. * `reconcilePendingFrontier` catches not-yet-finalized virtual * outputs that could sit outside any delta window. */ private reconcileWatched; /** * Crash-recovery for persisted settlement intents: reconcile any * non-terminal intent left behind by a mid-settle crash against * authoritative indexer state (see {@link reconcileIntents}). Runs on the * online sync path only — boot and subscription reconnect — never from a * wallet read API. Best-effort: intent recovery is not a sync invariant, so * a failure is logged and sync continues. No-op without an intent store. */ private reconcileStaleIntents; /** @see IContractManager.refillLookAhead */ refillLookAhead(): Promise; /** @see IContractManager.getNextSigningDescriptor */ getNextSigningDescriptor(): Promise; /** @see IContractManager.advanceSigningDescriptorWatermark */ advanceSigningDescriptorWatermark(index: number): Promise; /** * Serialized drain of the look-ahead band: concurrent callers join the * active drain and mark it dirty, an idle call starts a new one. Promotion * can uncover more funded indices, and boot / SSE / rotate / reconnect can * all request a refill at once, so the loop coalesces them instead of * recursing. */ private scheduleLookAheadDrain; /** * Request a drain without awaiting it. Used from inside a sync (promotion), * and after an allocation the drain must not be able to fail — the * watermark already moved, and a retry would burn another index. * * A failure here is not terminal: it leaves the watch band behind the * watermark, so funded indices inside it would go unregistered and the * balance would under-report for the rest of the session. Record the debt * so the next contract event retries it. @see handleContractEvent */ private requestLookAheadDrain; /** * Rebuild the speculative watch band around the allocation watermark. * * NArk needs no analogue because it *is* the address issuer: it persists a * contract row at derivation time, so its watched set is a superset of * every address it ever advertised. This SDK, when a third party issues * addresses from the shared seed, cannot observe those allocations at all — * the window is how a non-issuer compensates. * * Entries are registered with the watcher but NOT persisted: they become * repository rows only once funded (see {@link promoteLookAheadHits}). * * Each index contributes every {@link Discoverable.candidatesAt} candidate — * the same set `restore()`'s gap scan probes, since the wallet cannot know * which variant a third-party issuer handed out. */ private ensureLookAhead; /** * One-time full-history sync for speculative entries the manager has not * successfully caught up on yet. * * Every normal sync uses the cursor-derived delta window and SSE only * delivers events after a script is subscribed, so a script first * registered after its funding time would otherwise be skipped forever * (upgrade migration, or a band that slid over an already-funded index). * The entries are passed as in-memory contracts because `refreshVtxos` * resolves scripts through the repository, where they deliberately do not * exist. Targeted + explicitly windowed, so the global cursor stays put. */ private runLookAheadCatchUp; private advanceLookAheadWatermark; /** * Promote every look-ahead entry funded by `vtxos` into a real repository * row, returning the persisted rows keyed by script. * * MUST run on each raw indexer fetch before `annotateVtxos`: * `extendVirtualCoinForContract` throws when a VTXO's script has no * contract row, so a funded window entry has to be persisted before its * VTXOs are annotated and saved. Callers also swap the returned rows into * the local contract maps they built pre-promotion, so `saveVtxosForContract` * and `onVtxosPersisted` never see the synthetic watcher object. */ private promoteLookAheadHits; /** * Create and register a new contract. * * @param params - Contract parameters * @returns The created contract */ createContract(params: CreateContractParams): Promise; /** * `createContract`'s step order, fetch batched: persist all, hydrate once, * then watch. Hydrating first keeps it equivalent — the watcher seeds from * the repository, so an unhydrated row reads as all-new. A part-way failure * still watches what it wrote; a retry reports those rows as existing. */ createContracts(paramsList: CreateContractParams[]): Promise; /** * Lightweight variant of {@link createContract} for batch discovery * paths (currently: {@link scanContracts}). Validates, dedupes, persists, * and registers the watcher — but skips the per-contract * `fetchContractVxosFromIndexer` round-trip. The caller is responsible * for hydrating VTXOs afterwards via a bulk `refreshVtxos(...)` so a * scan that finds N contracts costs one batched indexer call instead * of N + 1. Error semantics are identical to `createContract`: * validation / type-mismatch / persistence failures propagate. */ private persistAndWatchContract; /** * Shared validate + check-existing + persist core for * {@link createContract} and {@link persistAndWatchContract}. Returns * the resolved contract and whether *this* call wrote it — callers * that need to attach hydration / watcher work do so only when * `persisted` is `true`. */ private upsertContract; private upsertContractRow; /** * Explicit, gap-limit contract discovery (see {@link IContractManager.scanContracts}). * * Each hit is routed through {@link persistAndWatchContract} — the same * dedupe + watcher-register path as {@link 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 {@link 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 {@link ContractWatcher.withCoalescedSubscription}). */ scanContracts(opts: ScanContractsOptions): Promise; private runScan; /** * Get contracts with optional filters. * * @param filter - Optional filter criteria * @returns Filtered contracts TODO: filter spent/unspent * * @example * ```typescript * // Get all VHTLC contracts * const vhtlcs = await manager.getContracts({ type: 'vhtlc' }); * * // Get all active contracts * const active = await manager.getContracts({ state: 'active' }); * ``` */ getContracts(filter?: GetContractsFilter): Promise; getContractsWithVtxos(filter?: GetContractsFilter, pageSize?: number, options?: { maxSyncAgeMs?: number; unspentOnly?: boolean; requireSynced?: boolean; }): Promise; annotateVtxos(vtxos: VirtualCoin[], tapscripts?: ContractTapscriptCache): Promise; /** @inheritdoc */ assertAnnotatable(vtxos: readonly { txid: string; vout: number; script: string; }[]): Promise; /** @inheritdoc */ assertSpendableNow(vtxos: readonly AssertSpendableInput[], walletPubKey?: () => Promise): Promise; /** @inheritdoc */ unspendableNowReasons(vtxos: readonly AssertSpendableInput[], walletPubKey?: () => Promise): Promise>; private buildContractsDbFilter; /** * Update a contract. * Nested fields like `params` and `metadata` are replaced with the provided values. * If you need to preserve existing fields, merge them manually. * * @param script - Contract script * @param updates - Fields to update */ updateContract(script: string, updates: Partial>): Promise; /** * Update a contract's params. * This method preserves existing params by merging the provided values. * * @param script - Contract script * @param updates - The new values to merge with existing params */ updateContractParams(script: string, updates: Contract["params"]): Promise; /** * Set a contract's state. Retiring (`inactive`) keeps it watched; * see {@link ContractState}. To stop watching while keeping the row, * use {@link setContractWatchState}. */ setContractState(script: string, state: ContractState): Promise; /** @see IContractManager.setContractWatchState */ setContractWatchState(script: string, watch: ContractWatchState): Promise; /** * Delete a contract, dropping the row along with the watch. To stop * watching a finished contract without losing its history, use * {@link setContractWatchState}(`"retained"`). * * @param script - Contract script */ deleteContract(script: string): Promise; /** * Chain tip height for a {@link PathContext}, or `undefined` when there is * no source configured or it cannot be read. * * Cached for {@link CHAIN_TIP_TTL_MS} so a caller resolving paths for many * contracts does not pay a provider round trip each time. Blocks arrive * ~10 minutes apart, so a cache this short can only ever be one block * stale, and a stale-low height is the conservative direction: a path is * reported unspendable slightly longer than it truly is, never spendable * before it is. */ private currentChainTip; /** * One chain tip read, bounded by {@link CHAIN_TIP_TIMEOUT_MS}. Never * rejects: an unreadable tip is "unknown", which is what the callers did * before a tip existed at all, and a path query is not worth failing over * a provider hiccup. */ private readChainTip; /** * Get currently spendable paths for a contract. * * @param options - Options for getting spendable paths */ getSpendablePaths(options: GetSpendablePathsOptions): Promise; /** * 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. * * @param options - Options for getting spending paths */ getAllSpendingPaths(options: GetAllSpendingPathsOptions): Promise; /** * Register a callback for contract events. * * The manager automatically watches after `initialize()`. This method * allows registering callbacks to receive events. * * @param callback - Event callback * @returns Unsubscribe function to remove this callback * * @example * ```typescript * const unsubscribe = manager.onContractEvent((event) => { * console.log(`${event.type} on ${event.contractScript}`); * }); * * // Later: stop receiving events * unsubscribe(); * ``` */ onContractEvent(callback: ContractEventCallback): () => 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 * {@link RefreshVtxosOptions}. */ refreshVtxos(opts?: RefreshVtxosOptions): Promise; refreshOutpoints(outpoints: Outpoint[]): Promise; /** * Check if currently watching. */ isWatching(): Promise; /** @see IContractManager.watchScript */ watchScript(script: string | string[], options?: { label?: string; }): Promise; /** @see IContractManager.unwatchScript */ unwatchScript(script: string | string[]): Promise; /** @see IContractManager.getWatchedScripts */ getWatchedScripts(): Promise; /** * Emit an event to all registered callbacks. */ private emitEvent; /** * Handle events from the watcher. */ private handleContractEvent; private getVtxosForContracts; /** * Sync virtual outputs for the given contracts against the indexer. * * When `options.contracts` is omitted the sync covers the full * watched set ({@link ContractWatcher.getWatchedContracts}) and the * global cursor is advanced on success. Passing an explicit subset * leaves the cursor alone so a narrow poll can't hide data that * other contracts still need to pick up. */ private syncContracts; private syncedWithin; /** * Demote every `awaiting-funds` contract in `contracts` that has been * funded — the automatic half of {@link ContractWatchState}. * * Runs after the sync has persisted, so the funding VTXO is saved * while the contract is still watched, and reads the repository * rather than this sync's delta: funds that landed while the app was * closed are outside every later window, and a contract asked to * watch until it is funded must still stop once it is. * * Best-effort. A demotion that fails costs coverage that is merely * no longer needed, and must not fail the sync that carried it. */ private demoteFundedAwaitingContracts; /** * Fetch all pending (unfinalized) virtual outputs and upsert them into the * repository. This catches virtual outputs whose state changed outside the delta * window (e.g. a spend that hasn't settled yet). */ private reconcilePendingFrontier; private fetchContractVxosFromIndexer; private fetchContractVtxosBulk; /** * 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. */ dispose(): void; /** * Symbol.dispose implementation for using with `using` keyword. * @example * ```typescript * { * using manager = await wallet.getContractManager(); * // ... use manager * } // automatically disposed * ``` */ [Symbol.dispose](): void; } /** * Contract lifecycle state. Neither state affects coverage: the watcher * subscribes and sweeps a contract according to {@link Contract.watch} * alone, because a retired receive address can still be paid. `inactive` * only demotes a contract out of receive-address selection; it does * **not** unsubscribe it. To stop watching but keep the row, set * {@link ContractWatchState} to `retained`; to drop both, use * {@link IContractManager.deleteContract}. */ type ContractState = "active" | "inactive"; /** * Whether a contract is covered by background monitoring — the * subscription and the failsafe/indexer sweep * ({@link ContractWatcher.getWatchedContracts}). * * Orthogonal to {@link ContractState}, which governs receive-address * selection only. A contract can be the wallet's display address and * watched, or terminal and retained; the two questions never answer each * other. * * NArk calls this `ContractActivityState` (`Active` / `Inactive` / * `AwaitingFundsBeforeDeactivate`). The concept is the same; the names * differ because TS already spends `active`/`inactive` on * {@link ContractState}, and a row reading `state: "active", * activityState: "inactive"` would be unreadable. */ type ContractWatchState = /** Subscribed and polled. */ "watched" /** * Watched until the first VTXO lands at the script, then * automatically demoted to `retained` by the contract manager. * For one-shot destinations — a refund address, a swap lockup — * that only need coverage until they are funded. */ | "awaiting-funds" /** * Kept for history, restore and classification, but absent from * every background channel. The row still resolves in * `getContracts`, still annotates its VTXOs, and still feeds * transaction history; nothing subscribes or polls it. */ | "retained"; /** * Represents a contract that can receive and manage virtual outputs. * * A contract is defined by its type and parameters, which together * determine the VtxoScript (spending paths). The wallet's default * receiving address is itself a contract of type "default". * * External services (Boltz swaps, atomic swaps, etc.) create additional * contracts with their own types and parameters. * * @example * ```typescript * const vhtlcContract: Contract = { * type: "vhtlc", * params: { * sender: "ab12...", * receiver: "cd34...", * server: "ef56...", * hash: "1234...", * refundLocktime: "800000", * // ... timelocks * }, * script: "5120...", * address: "ark1q...", * state: "active", * createdAt: 1704067200000, * }; * ``` */ interface Contract { /** Human-readable label for display purposes. */ label?: string; /** * Contract type identifier. * Built-in types: "default", "vhtlc" * Custom types can be registered via ContractHandler. */ type: string; /** * Type-specific parameters for constructing the VtxoScript. * All values are serialized as strings (hex for bytes, string for bigint). * The ContractHandler for this type knows how to interpret these. */ params: Record; /** The pkScript hex, used as the unique identifier and primary key for contracts. */ script: string; /** Address derived from the contract script. */ address: string; /** Current state of the contract. */ state: ContractState; /** * Background-monitoring scope. Absent means `watched`. * @see ContractWatchState */ watch?: ContractWatchState; /** Unix timestamp in milliseconds when this contract was created. */ createdAt: number; /** * Optional metadata for external integrations. */ metadata?: Record; } /** * A virtual output that has been associated with a specific contract. */ type ContractVtxo = VirtualCoin & Partial & { extraWitness?: Bytes[]; contractScript: string; }; /** * A {@link ContractVtxo} with all taproot annotation fields required. * * Mirrors the {@link ExtendedVirtualCoin} / {@link VirtualCoin} split: * - {@link ContractVtxo} carries `TapLeaves` and `EncodedVtxoScript` as * `Partial<>` because VTXOs fetched raw from the indexer do not yet have * taproot data. * - `ExtendedContractVtxo` narrows those fields to required, guaranteeing * that `annotateVtxos` has run and the taproot leaves are present. * * Use this type (instead of {@link ContractVtxo}) wherever the compiler * should enforce that annotation has happened — e.g. `saveVtxos` and * forfeit transaction construction. */ type ExtendedContractVtxo = NormalizedExtendedVirtualCoin & { contractScript: string; }; /** * Result of path selection, including the tapleaf to use and any extra witness data. */ interface PathSelection { /** Tapleaf script to use for spending. */ leaf: TapLeafScript; /** Additional witness elements, for example a preimage for HTLC-like paths. */ extraWitness?: Bytes[]; /** * nSequence for the spending input, BIP-68 encoded when the leaf * uses CSV. Decode with `sequenceToTimelock`; do NOT use as an * absolute `Transaction.lockTime`. */ sequence?: number; } /** * Context for path selection decisions. */ interface PathContext { /** Whether collaborative spending is available through server cooperation. */ collaborative: boolean; /** Current time in milliseconds. */ currentTime: number; /** Current block height, when known. */ blockHeight?: number; /** * Wallet's descriptor for signing. * Format: tr(pubkey) for static keys, tr([fingerprint/path']xpub/0/{index}) for HD. * Used by handlers to determine wallet's role in multi-party contracts. */ walletDescriptor?: string; /** * Wallet's public key (x-only, 32 bytes hex). * @deprecated Use walletDescriptor instead. */ walletPubKey?: string; /** * Explicit role override for multi-party contracts such as VHTLC. * If not provided, the handler may derive the role by matching * {@link walletDescriptor} (preferred) — or {@link walletPubKey} as a * fallback — against the contract's sender/receiver params. */ role?: string; /** * Chain tip timestamp in SECONDS, when known. * * Timelocks mature against chain time, not the machine's clock, so any * seconds-typed comparison should prefer this and fall back to * {@link currentTime} only when it is absent. The two differ by more than * pedantry: the server matures absolute locktimes against median-time-past, * which trails wall clock, and a host whose clock drifts turns a local * decision into a wrong one in whichever direction it drifted. */ chainTime?: number; /** The specific virtual output being evaluated. */ vtxo?: VirtualCoin; } /** * Handler for a specific contract type. * * Each contract type (`default`, `vhtlc`, etc.) has a handler that knows how to: * 1. Create the VtxoScript from parameters * 2. Serialize/deserialize parameters for storage * 3. Select the appropriate spending path based on context * * @example * ```typescript * const vhtlcHandler: ContractHandler = { * type: "vhtlc", * createScript(params) { * return new VHTLC.Script(this.deserializeParams(params)); * }, * selectPath(script, contract, context) { * const vhtlc = script as VHTLC.Script; * const preimage = contract.data?.preimage; * if (context.collaborative && preimage) { * return { leaf: vhtlc.claim(), extraWitness: [hex.decode(preimage)] }; * } * // ... other paths * }, * // ... * }; * ``` */ interface ContractHandler

, S extends VtxoScript = VtxoScript> { /** Contract type managed by this handler. */ readonly type: string; /** * Create the VtxoScript from serialized parameters. * * @param params - Serialized contract parameters * @returns Contract script instance */ createScript(params: Record): S; /** * Serialize typed parameters to string key-value pairs. * * @param params - Typed contract parameters * @returns Serialized key-value representation */ serializeParams(params: P): Record; /** * Deserialize string key-value pairs to typed parameters. */ deserializeParams(params: Record): P; /** * Select the preferred spending path based on contract state and context. * Returns the best available path (e.g., collaborative over unilateral). * * @returns PathSelection if a viable path exists, null otherwise */ selectPath(script: S, contract: Contract, context: PathContext): PathSelection | null; /** * Get all possible spending paths for the current context. * Returns empty array if no paths are available. * * Useful for showing users which spending options exist regardless of * current spendability. */ getAllSpendingPaths(script: S, contract: Contract, context: PathContext): PathSelection[]; /** * Get all currently spendable paths. * Returns empty array if no paths are available. */ getSpendablePaths(script: S, contract: Contract, context: PathContext): PathSelection[]; /** * Whether this contract's VTXOs may be picked by *generic* wallet spending — * send, settle, renewal, asset operations, offboard, `available` balance. * Explicit-input APIs (`settle({ inputs })`, `sendBitcoin({ selectedVtxos })`, * …) stay open regardless: naming an outpoint is the intent this gate protects. * * Pure, synchronous and offline — it runs inside the service worker, so no * chain tip, no network, no live plugin object. Absent or `false` ⇒ NOT * spendable: a type core cannot reason about must not leak by omission. * * No `script` parameter: deriving it costs a taproot tree per contract on a * read path (#521) and no shipped handler needs it. A handler that does can * call its own `createScript(contract.params)`. */ isGenericallySpendable?(contract: Contract): boolean; /** * Refuse a spend this contract definitively cannot make right now, with a * reason the caller can act on. Called before anything is signed or * submitted, for inputs the caller named explicitly. * * This is the counterpart to {@link isGenericallySpendable}, not a * duplicate of it. That gate keeps escrow out of GENERIC selection and * deliberately leaves explicit-input APIs open, because naming an outpoint * is the intent it protects. Naming one too early is still a mistake * though, and without this it is a mistake the server reports — as a * protocol-level rejection, after the round trip, in terms that do not name * the timelock that was not yet mature. * * **Throw only on a definite no.** Absent, silent, or unsure all mean "no * opinion" and the spend proceeds. A handler must not refuse merely because * it found no path: `getSpendablePaths` legitimately returns empty for * spendable contracts — `arkade`'s skips every covenant leaf, so a program * spendable only through its emulator-signed leaf reports nothing — and an * unreadable timelock (height-typed with no chain tip) is unknown, not * immature. @see cltvMaturity, which keeps those apart. * * Returning a promise is allowed so a handler needing I/O is not forced to * throw synchronously — callers await the result. Prefer synchronous where * possible: this runs on the path between a caller's decision to spend and * the spend itself. * * @throws Error when the contract provably cannot be spent at `context` */ assertSpendableNow?(script: S, contract: Contract, context: PathContext): void | Promise; } /** * What a {@link Discoverable.discoverAt} call returns — exactly the * shape `ContractManager.createContract` accepts (script-keyed, * idempotent on re-register). */ interface DiscoveredContract { type: string; params: Record; script: string; address: string; metadata?: Record; label?: string; } /** * Read-only context the scanner injects into every `discoverAt` call. * The boltz/swap handler does NOT receive its Boltz client here — it * closes over its own client at registration time. */ interface DiscoveryDeps { indexerProvider: IndexerProvider; onchainProvider: OnchainProvider; /** * Ark-address network data. The `{ hrp }` shape is all the L2 * (`default`/`delegate`) discovery path needs to render an Ark address. */ network: { hrp: string; }; /** * Full Bitcoin network descriptor for on-chain (P2TR) address * rendering. Required by the boarding discovery probe, which derives an * on-chain Taproot address via {@link VtxoScript.onchainAddress} — the * `{ hrp }`-only {@link DiscoveryDeps.network} lacks the `bech32` data * that needs. Absent only when no boarding discovery is plumbed (e.g. * the scanner unit harness), in which case boarding `discoverAt` no-ops. */ onchainNetwork?: Network; /** * The server's **current** signer key (x-only, 32 bytes), taken from a * fresh server-info snapshot at restore time. L2 (`default`/`delegate`) * discovery probes this key first. */ serverPubKey: Uint8Array; /** * The server's **deprecated** signer keys (x-only, 32 bytes) from the same * snapshot. A VTXO minted under a now-rotated signer is anchored to a * different script; L2 discovery scans these keys alongside * {@link DiscoveryDeps.serverPubKey} so signer rotation does not strand * funds. Empty/absent when the server advertises no deprecated signers. * Boarding discovery does not consult this set (current UTXO set only). */ deprecatedSignerPubKeys?: Uint8Array[]; /** Relative timelocks the wallet treats as its baseline matrix. */ csvTimelocks: RelativeTimelock[]; /** * Boarding-exit CSV timelock. Distinct from {@link DiscoveryDeps.csvTimelocks} * (the unilateral-exit matrix): boarding scripts source their CSV from the * server's boarding-exit delay. Present only when boarding discovery is * plumbed; when absent, boarding `discoverAt` no-ops (so the scanner unit * harness, which never sets it, is unaffected). */ boardingTimelock?: RelativeTimelock; /** Present only for delegate wallets. */ delegatePubKey?: Uint8Array; } /** * The I/O-free subset of {@link DiscoveryDeps}: the axes a receive script can * be anchored to. `DiscoveryDeps` satisfies it structurally, so one * {@link Discoverable.candidatesAt} serves the scan and the look-ahead band. */ type CandidateDeps = Pick & Pick, "deprecatedSignerPubKeys" | "delegatePubKey">; /** * Optional capability a {@link ContractHandler} implements to participate * in `wallet.restore()`'s gap-limit scan. The scanner owns the index * loop and the gap counter; the handler answers "do I own a contract * anchored to the pubkey/descriptor at this index?" — checked against * the indexer / explorer / (for swaps) the handler's own source. The * handler MAY batch/cache internally across calls. */ interface Discoverable { discoverAt(index: number, descriptor: string, deps: DiscoveryDeps): Promise; /** * Optional: answer for a whole scan window in one batched round-trip. * The scanner prefers it over per-index `discoverAt` calls when present, * which is what keeps a 10-index window to 1-2 indexer requests instead * of one per index. Handlers whose source is inherently per-address (e.g. * boarding, on Esplora) implement only `discoverAt`. * * **All-or-nothing per call.** Either resolve with a map covering *every* * requested index (empty array = confirmed miss), or reject — a handler * whose inner chunk fails partway must discard the partial results and * reject, because a missing index would otherwise read as "no funds here" * and let restore close its gap window on a failed request. The scanner * enforces this rather than trusting it: an incomplete map is treated as a * rejection, making the whole requested range indeterminate (hits present * in it are still persisted) and truncating the scan at the range's first * index. Indices that were not requested are ignored. */ discoverRange?(entries: readonly { index: number; descriptor: string; }[], deps: DiscoveryDeps): Promise>; /** * Optional: every unverified contract this handler could own at one HD * index — pure derivation, no I/O, no metadata. `discoverRange` probes * these scripts and the look-ahead band subscribes to them, so the two * cannot cover different sets. * * `boarding` omits it: its probe is an on-chain address lookup, and that * keeps it out of the band (deposits are watched on their own channel). */ candidatesAt?(index: number, descriptor: string, deps: CandidateDeps): DiscoveredContract[]; } /** Duck-typed guard for the pure candidate surface. @see Discoverable.candidatesAt */ declare function hasCandidates(handler: ContractHandler | undefined): handler is ContractHandler & Required>; /** Duck-typed guard (mirrors `hasReceiveRotatorFactory`). */ declare function isDiscoverable(handler: ContractHandler | undefined): handler is ContractHandler & Discoverable; /** * The per-contract tapscript annotation stamped onto every VTXO locked to a * contract (see `extendVirtualCoinForContract`): the leaf used to co-sign * forfeits, the leaf committed in intent proofs, and the encoded taproot tree. */ interface DerivedContractTapscripts { forfeitTapLeafScript: TapLeafScript; intentTapLeafScript: TapLeafScript; tapTree: Bytes; } /** * Optional capability a {@link ContractHandler} implements to provide the * forfeit/intent tapscripts for VTXO annotation. Handlers whose script shape * doesn't expose the legacy `forfeit()` method (e.g. program-compiled arkade * contracts, where the right leaf depends on the program) implement this so * the annotation pipeline stays type-agnostic. * Must be pure in `(contract.type, contract.script, contract.params)`: `ContractManager` memoizes the result for its lifetime. */ interface TapscriptDeriving { deriveTapscripts(script: S, contract: Contract): DerivedContractTapscripts; } /** A script the watcher reports on without the wallet owning it. */ interface WatchedScript { script: string; /** Free-form tag echoed back by `getWatchedScripts`; never sent anywhere. */ label?: string; } /** * Event emitted when contract-related changes occur. * * A watch-only script reports the same `vtxo_received` / `vtxo_spent` types * but carries no `contract`, and cannot: annotation needs one. That absence is * the ownership boundary, and the compiler enforces it — reading * `event.contract` unnarrowed by {@link isContractVtxoEvent} fails. */ type ContractEvent = { type: "vtxo_received"; contractScript: string; vtxos: ContractVtxo[]; contract: Contract; timestamp: number; } | { type: "vtxo_spent"; contractScript: string; vtxos: ContractVtxo[]; contract: Contract; timestamp: number; } | { type: "vtxo_received"; contractScript: string; vtxos: VirtualCoin[]; timestamp: number; } | { type: "vtxo_spent"; contractScript: string; vtxos: VirtualCoin[]; timestamp: number; } | { type: "connection_reset"; timestamp: number; }; type ContractVtxoEvent = Extract; /** * Gate every wallet-side effect on this: a watch-only event has no contract. * @example `if (!isContractVtxoEvent(event)) return;` inside `onContractEvent`, * before reading `event.contract` — which does not compile without it. */ declare function isContractVtxoEvent(event: ContractEvent): event is ContractVtxoEvent; /** * Callback for contract events. */ type ContractEventCallback = (event: ContractEvent) => void; /** * Options for retrieving contracts from the Contract Manager. * Currently an alias of the repository's filter type but can be extended in the future. */ type GetContractsFilter = ContractFilter; /** * Contract with its virtual outputs included. */ type ContractWithVtxos = { contract: Contract; vtxos: ExtendedContractVtxo[]; }; /** * Summary of a contract's balance. */ interface ContractBalance { /** Total balance (settled + pending) in satoshis */ total: number; /** Spendable balance in satoshis */ spendable: number; /** Number of virtual outputs in this contract */ vtxoCount: number; } /** * Configuration for the ContractWatcher. * * @see ContractWatcher * * @example * ```typescript * const watcher = new ContractWatcher({ * indexerProvider, * walletRepository, * }) * ``` */ interface ContractWatcherConfig { /** Indexer provider used for subscriptions and queries. */ indexerProvider: IndexerProvider; /** Wallet repository used to store virtual output state between watcher updates. */ walletRepository: WalletRepository; /** * Interval for failsafe polling (ms). * Polls even when subscription is active to catch missed events. * * @defaultValue `60_000` (1 minute) */ failsafePollIntervalMs?: number; /** * Initial reconnection delay (ms). * Uses exponential backoff on repeated failures. * * @defaultValue `1_000` (1 second) */ reconnectDelayMs?: number; /** * Maximum reconnection delay (ms). * * @defaultValue `30_000` (30 seconds) */ maxReconnectDelayMs?: number; /** * Maximum reconnection attempts before giving up. * Set to 0 for unlimited attempts. * * @defaultValue `0` (unlimited) */ maxReconnectAttempts?: number; } /** * Connection state for the watcher. */ type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting"; /** * Watches multiple contracts for virtual output state changes with resilient connection handling. * * Features: * - Automatic reconnection with exponential backoff * - Failsafe polling to catch missed events * - Polls immediately after (re)connection to sync state * - Graceful handling of subscription failures * * @example * ```typescript * const watcher = new ContractWatcher({ * indexerProvider: wallet.indexerProvider, * }); * * // Add the wallet's default contract * await watcher.addContract(defaultContract); * * // Add additional contracts (swaps, etc.) * await watcher.addContract(swapContract); * * // Start watching for events * const stop = await watcher.startWatching((event) => { * console.log(`${event.type} on contract ${event.contractScript}`); * }); * * // Later: stop watching * stop(); * ``` */ declare class ContractWatcher { private config; private contracts; /** In-memory and process-lifetime; never persisted. */ private watchedScripts; private subscriptionId?; private abortController?; private isWatching; private eventCallback?; private connectionState; private reconnectAttempts; private reconnectTimeoutId?; private failsafePollIntervalId?; /** See {@link withCoalescedSubscription}. */ private subscriptionBatchDepth; private subscriptionUpdateDeferred; /** See {@link reportEventSourceUnavailable} — said once, not per attempt. */ private eventSourceReported; /** * Create a contract watcher with the given providers and polling settings. * * @param config - Contract watcher configuration * @see ContractWatcherConfig */ constructor(config: ContractWatcherConfig); /** * Add a contract to be watched. * * Once watching, every contract is subscribed and polled whatever * its {@link ContractState} — a `retained` one is held for reads * only, and never enters a background channel. * * @see getWatchedContracts */ addContract(contract: Contract): Promise; /** * Pre-populate `lastKnownVtxos` from the wallet repository. * * Runs on add (and can be re-run after reconnect) so polling always * compares the indexer's view against what is already persisted, * emitting only genuine deltas. */ private seedLastKnownVtxos; /** * Update an existing contract. */ updateContract(contract: Contract): Promise; /** * Remove a contract from watching. */ removeContract(contractScript: string): Promise; /** * Get all in-memory contracts. */ getAllContracts(): Contract[]; /** * Every registered contract except the `retained` ones, retired * (`inactive`) receive addresses included. * * Feeds both the subscription and the indexer sweep scope, so * narrowing it drops a contract from every background channel at * once. `state` may never narrow it: an Ark receive address can be * paid again after the wallet has rotated past it, and a payment * that lands outside every background channel is invisible until * some foreground read happens to sweep it. Retirement therefore * governs receive-address selection, not coverage. * * {@link ContractWatchState} is the one thing that does narrow it, * and only when an owner has explicitly said the script is done — * a settled swap lockup, a funded one-shot destination. The row * stays in {@link getAllContracts} so reads, annotation and history * are unaffected. */ getWatchedContracts(): Contract[]; /** * Every script the subscription must carry. Wider than * {@link getWatchedContracts}, which stays the sync scope. */ private getSubscribedScripts; /** @see IContractManager.watchScript for the delivery and filter contract. */ addWatchedScript(script: string | string[], options?: { label?: string; }): Promise; /** Stop watching a script added by {@link addWatchedScript}. */ removeWatchedScript(script: string | string[]): Promise; /** Every script registered via {@link addWatchedScript}. */ getWatchedScripts(): WatchedScript[]; /** * Get virtual outputs for contracts, grouped by contract script. * @see WalletRepository for `repo` */ private getContractVtxos; /** * Start watching for virtual output events across all watched contracts. */ startWatching(callback: ContractEventCallback): Promise<() => void>; /** * Stop watching for events. */ stopWatching(): Promise; /** * Check if currently watching. */ isCurrentlyWatching(): boolean; /** * Get current connection state. */ getConnectionState(): ConnectionState; /** * Force a poll of all watched contracts. * Useful for manual refresh or after app resume. */ forcePoll(): Promise; /** * Connect to the subscription. * * @param skipUpdate - Skip the leading `updateSubscription` call when * the caller has already established `subscriptionId`. */ private connect; /** * Handle "this environment has no `EventSource`": say so once, loudly and * actionably, and answer whether the caller should skip reconnecting. * * Reconnecting is pointless here — a missing global is not a dropped * connection, and the default backoff (unlimited attempts, capped at 5s) * would otherwise retry it forever, logging each failure and firing a * `connection_reset` every few seconds for the life of the wallet. Not even * one goes out: subscribers read that event as "the stream dropped, resync * and expect it back", and here it never opened and never will. * Failsafe polling keeps running, so the watcher stays correct and merely * slower; what it loses is push latency. */ private reportEventSourceUnavailable; /** * Schedule a reconnection attempt. */ private scheduleReconnect; /** * Start the failsafe polling interval. */ private startFailsafePolling; private pollAllContracts; /** * Poll watch-only scripts against the indexer and emit the delta. * * Separate from {@link pollContracts} because that one diffs the wallet * repository, which is permanently empty for a script this wallet does not * own — reusing it would report every live output as spent on every tick. * Not a second poll loop: same timer and callers, different source. */ private pollWatchedScripts; /** * Poll specific contracts and emit events for changes. */ private pollContracts; /** * Run `fn` with subscription updates coalesced into a single * `subscribeForScripts` on the way out. * * {@link addContract} re-subscribes eagerly (the watcher may already be * running), and every subscribe posts the *whole* accumulated script list — * so a restore scan that discovers N contracts sends N growing POSTs, * quadratic in script-slots. Inside this scope those updates are only * marked dirty, flushed once on the way out (success and error path alike). * * A contract added inside the scope is therefore not streaming until the * flush. Nothing in the watcher closes that window — the failsafe poll * replays repository state and cannot see VTXOs no one has fetched yet. The * one caller, `scanContracts`, is covered because `Wallet.restore` follows * it with a bulk `refreshVtxos`. A new caller must provide its own * equivalent catch-up, or keep the scope short enough not to need one. */ withCoalescedSubscription(fn: () => Promise): Promise; private tryUpdateSubscription; /** * Update the subscription with scripts that should be watched. * * @see getSubscribedScripts */ private updateSubscription; /** * Main listening loop for subscription events. */ private listenLoop; /** * Handle a subscription update. * * Normalization boundary: `getSubscription` is part of the public `IndexerProvider` interface, * so a consumer implementation may yield legacy-shaped VTXOs. Normalizing on ingest also fixes * the shape of the payloads emitted to external event consumers. */ private handleSubscriptionUpdate; /** * Process virtual outputs from subscription and route each VTXO to the * single contract that actually locks it via `vtxo.script`. If the script * doesn't match any watched contract, skip the VTXO rather than fan it * out to every matching contract — fan-out produced phantom state in * non-owning contracts that then never reconciled. */ private processSubscriptionVtxos; /** As {@link emitVtxoEvent}, less the `contract` and the annotation. */ private emitWatchedScriptEvent; /** * Emit a virtual output event for a contract. */ private emitVtxoEvent; } /** * Wallet receive-address strategy. * * - `'auto'` *(default)*: **short-term** — currently identical to * `'static'`. The `'auto'` name is reserved for a future change that * will re-enable identity-probing once HD rotation has matured in * the field. Until then, opt into HD explicitly via `'hd'` or a * {@link DescriptorProvider}. * *(See `TODO(hd-maturation)` in * `src/wallet/walletReceiveRotator.ts:resolveDescriptorProvider` for * the flip-back criteria.)* * - `'static'`: never rotate. The wallet uses one receive address derived * from `identity.xOnlyPublicKey()`. * - `'hd'`: must rotate, using the built-in HD provider derived from the * identity. Throws at `Wallet.create` if the identity isn't HD-capable * or its descriptor isn't rangeable — no silent fallback. * - A {@link DescriptorProvider} instance: rotate via the supplied * provider on every incoming VTXO. The wallet does not probe the * identity; the caller is responsible for ensuring the identity can * sign for whatever pubkey the provider returns. Errors thrown by the * provider propagate — there is no silent fallback for an explicit * provider. */ type WalletMode = "auto" | "static" | "hd" | DescriptorProvider; /** * Address flavours {@link Wallet.getNewAddresses} can mint. Both derive from * the same HD index within one call — `default` is the offchain Arkade * receive script, `boarding` the onchain deposit script. */ type NewAddressType = "default" | "boarding"; /** Options for {@link Wallet.getNewAddresses}. */ interface GetNewAddressesOptions { /** * Flavours to mint, in the order they are returned. * * @defaultValue `["default"]` */ types?: readonly NewAddressType[]; /** * Require a genuinely fresh index. A wallet with no HD stream to advance * (`walletMode: 'static'` / `'auto'`, or a provider that declines to * allocate) throws {@link WalletCannotAllocateAddressError} rather than * silently handing back the address it already gave you — which, for a * caller issuing one address per counterparty, surfaces only as two * people paying the same script. * * @defaultValue `false` */ forceNew?: boolean; } /** One minted address and the contract row backing it. */ interface NewAddress { /** * The address to hand out: the onchain address for `boarding`, the Arkade * address for `default`. * * Not always `contract.address`. A boarding row persists the *ark* * encoding of its script, so reading `contract.address` on a boarding * entry yields an address no onchain sender can pay. */ address: string; /** * The descriptor this address was derived from — hand it to * `signerForDescriptor` to recover the key later. Every entry from a * single call carries the same one, because they share an index. * * Also present on `contract.metadata.signingDescriptor`, but surfaced here * typed: `Contract.metadata` is `Record`, so reading it * there costs the caller an `as string` on the one field they are most * likely to persist beside an invoice. */ signingDescriptor: string; /** * The persisted, watched contract row — script, type, state and metadata. */ contract: Contract; } /** * Base configuration options shared by all wallet types. * * Supports URL-based and provider-based configuration. * * @deprecated URL-based configuration starts from `arkServerUrl` and can optionally override * derived service URLs such as `indexerUrl` and `esploraUrl`. * * Provider-based configuration supplies concrete provider instances directly, * including the ArkProvider, IndexerProvider, OnchainProvider, and DelegateProvider. * * The wallet will use provided URLs to create default providers if custom provider * instances are not supplied. If optional parameters are not provided, the wallet * will fetch configuration from the Arkade server. * * @remarks * URL-based and provider-based configuration can be mixed, but provider instances * always take precedence over URLs for the corresponding service. * * @see WalletConfig * @see ReadonlyWalletConfig * @see StorageConfig */ interface BaseWalletConfig { /** * Base URL of the Arkade server. * * @deprecated Pass an explicit `arkProvider` instance instead. URL-based * configuration will be removed in a future major version. */ arkServerUrl?: string; /** * Optional override for the indexer URL. * * @deprecated Pass an explicit `indexerProvider` instance instead. */ indexerUrl?: string; /** * Optional override for the Esplora API URL. * * @deprecated Pass an explicit `onchainProvider` instance instead. */ esploraUrl?: string; /** Optional Arkade server public key used to construct and validate Arkade addresses. */ arkServerPublicKey?: string; /** Relative timelock applied to boarding scripts. */ boardingTimelock?: RelativeTimelock; /** Relative timelock applied to unilateral exit paths. */ exitTimelock?: RelativeTimelock; /** * Minimum accepted `BatchStartedEvent.batchExpiry`, as wall-clock seconds. * Defaults per network — see `defaultBatchExpiryPolicy`. Lowering it below * the default relaxes a fund-safety bound; intended for local testing. */ minBatchExpirySeconds?: bigint; /** * Minimum accepted checkpoint exit delay decoded from `ArkInfo.checkpointTapscript`, * as wall-clock seconds. Defaults per network — see * `defaultCheckpointExitDelayPolicy`, which already carries the value the * hosted signet and mutinynet Arkade Services advertise, so neither needs * this set. Lowering it below the default relaxes a fund-safety bound; * intended for local testing. */ minCheckpointExitDelaySeconds?: bigint; /** * Repository-backed storage configuration overrides. * Defaults to IndexedDB if unset. */ storage?: StorageConfig; /** Optional Arkade provider instance. */ arkProvider?: ArkProvider; /** Optional indexer provider instance. */ indexerProvider?: IndexerProvider; /** Optional onchain provider instance. */ onchainProvider?: OnchainProvider; /** Optional delegation service instance. */ delegateProvider?: DelegateProvider; /** @deprecated alias for @see BaseWalletConfig.delegateProvider */ delegatorProvider?: DelegateProvider; } /** * Configuration options for readonly wallet initialization. * * Use this config when you only need to query wallet state (balance, addresses, transactions) * without the ability to send transactions. This is useful for: * - Watch-only wallets * - Monitoring addresses * - Safe sharing of wallet state without private key exposure * * @see BaseWalletConfig * @see IReadonlyWallet * * @example * ```typescript * // Provider-based configuration (e.g., for Expo/React Native) * const wallet = await ReadonlyWallet.create({ * identity: ReadonlySingleKey.fromPublicKey(pubkey), * arkProvider: new ExpoArkProvider(), * indexerProvider: new ExpoIndexerProvider(), * onchainProvider: new EsploraProvider() * }); * ``` */ interface ReadonlyWalletConfig extends BaseWalletConfig { /** Readonly identity used to derive wallet addresses. */ identity: ReadonlyIdentity; /** * Configuration for the ContractManager's watcher. * Controls reconnection behavior and failsafe polling. * * @see ContractWatcherConfig */ watcherConfig?: Partial>; } /** * Configuration options for full wallet initialization. * * This config provides full wallet capabilities including sending transactions, * settling virtual outputs, and all readonly operations. * * @see ReadonlyWalletConfig * @see IWallet * * @example * ```typescript * // Provider-based configuration * const wallet = await Wallet.create({ * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), * arkProvider: new ExpoArkProvider(), * indexerProvider: new ExpoIndexerProvider(), * onchainProvider: new EsploraProvider() * }); * * // With settlement configuration * const wallet = await Wallet.create({ * identity: MnemonicIdentity.fromMnemonic('abandon abandon...'), * arkProvider: new RestArkProvider(), * settlementConfig: { * vtxoThreshold: 60 * 60 * 24, // 24 hours in seconds * boardingUtxoSweep: true, * }, * }); * ``` */ interface WalletConfig extends ReadonlyWalletConfig { /** Signing identity used to authorize transactions. */ identity: Identity; /** * Legacy renewal configuration. * * @remarks * This field is still accepted for backwards compatibility, but `settlementConfig` * is the source of truth for new code. * * @deprecated Use `settlementConfig` instead. */ renewalConfig?: RenewalConfig; /** * Configuration for automatic settlement and renewal. * `false` = explicitly disabled, `undefined` or `{}` = enabled with defaults. * * @defaultValue `undefined` (enabled with defaults) * @see SettlementConfig */ settlementConfig?: SettlementConfig | false; /** * Receive-address strategy. Pass `'static'`, `'hd'`, or a * {@link DescriptorProvider} instance to drive rotation; omit (or * pass `'auto'`) for the built-in auto-detect behaviour. See * {@link WalletMode}. * * @defaultValue `'auto'` */ walletMode?: WalletMode; /** * Per-side width of the HD look-ahead watch window: the wallet watches * missing offchain receive scripts across `[watermark - N, watermark + N]` * so funds paid to an address issued by an external party (a merchant * backend sharing the seed) arrive without an explicit `restore()`. * * Only meaningful for HD wallets (`walletMode: 'hd'` or an HD * {@link DescriptorProvider}); ignored otherwise. Raise it when the issuer * is expected to hand out more than `N` consecutive addresses without any * of them being paid. Must be a positive integer. * * @defaultValue `20` */ lookAheadWindow?: number; } /** * Repository implementations used to store wallet and contract state. * * @see BaseWalletConfig * @see WalletRepository * @see ContractRepository */ type StorageConfig = { /** Wallet-state repository implementation. */ walletRepository: WalletRepository; /** Contract-state repository implementation. */ contractRepository: ContractRepository; /** * Optional intent-lifecycle repository. Opt-in: when present, the wallet * persists settlement intents and excludes intent-locked VTXOs from * spendable balance. Absent ⇒ those code paths are no-ops. */ intentRepository?: IntentRepository; /** * **Experimental / inert.** Optional virtual-tx (exit-branch) repository. * Today it is only a best-effort raw-PSBT cache that unilateral exit * ({@link Unroll}) reads and writes when a caller passes it to * `Unroll.Session.create`. Normal wallet/contract sync does NOT populate, * maintain, or prune it, and {@link ContractManager} is never given it — * branch/full-mode persistence is out of scope for this release. Treat this * option as experimental until those paths land. Absent ⇒ no-op. */ virtualTxRepository?: VirtualTxRepository; /** * Optional exit-data capture settings (only in effect when * `virtualTxRepository` is set). `mode` "lite" (default) stores structure * only; "full" stores PSBTs so a unilateral exit needs no Ark indexer. * `minExitWorthSats` (default 1000) skips dust. `sources` are extra * `ExitDataSource`s (e.g. a wallet-provider) tried before the indexer for * both capture and exit reads. */ exitDataCapture?: { mode?: ExitCaptureMode; minExitWorthSats?: number; sources?: ExitDataSource[]; }; }; /** * Provider class constructor interface for dependency injection. * Ensures provider classes follow the consistent constructor pattern. */ interface ProviderClass { /** * Create a provider instance for the given server URL. * * @param serverUrl - Base server URL used by the provider */ new (serverUrl: string): T; } /** * Balance summary returned by `IWallet.getBalance`. * * @see IWallet.getBalance * * @example * ```typescript * const balance = await wallet.getBalance() * console.log(balance.available, balance.boarding.total) * ``` */ interface WalletBalance { /** Boarding funds */ boarding: { /** Confirmed funds ready to swap for virtual outputs. */ confirmed: number; /** Pending funds awaiting confirmation on mainnet */ unconfirmed: number; /** Combined boarding balance (`confirmed` + `unconfirmed`) */ total: number; }; /** Settled (finalized) balance the wallet owns, including gated and intent-locked funds. */ settled: number; /** Preconfirmed (unfinalized) balance the wallet owns, on the same owned rule as {@link settled}. */ preconfirmed: number; /** * Immediately spendable offchain balance — what generic selection would * pick, so nothing counted here can be refused by `send`: * `settled + preconfirmed - gated - intentLocked`. * A dust carrier is reserved if any assets are held on outputs comprising this balance. */ available: number; /** * Spendable-but-for-the-gate funds: VTXOs under a contract the * generic-spending gate refuses — a VHTLC lockup, an unmarked `arkade` * program, or a type whose handler this runtime never registered. Counted in * `settled`/`preconfirmed` and `total`, never in `available`. * * Tested before {@link intentLocked}: the gate is a durable property of the * contract while an intent lock clears on its own, so a VTXO that is both is * reported here — it does not become available when the batch settles. * * Covers this bucket only: {@link recoverable} has the same owned-versus- * obtainable split under a different predicate and is not counted here. * * Subtract this from `settled + preconfirmed`, never from `total`. `total` * also carries {@link boarding}, {@link recoverable}, {@link pendingRecovery} * and {@link unrolled}, which are still the user's funds — netting a bucket * out of it drops them from the figure with no signal. */ gated: number; /** * Funds committed to an in-flight (non-terminal) intent, and not already * counted in {@link gated}. Unlike `gated`, these return to `available` when * the intent reaches a terminal state. * * Reported as zero where the wallet cannot answer the question — no intent * repository, or a repository read that fails — so this under-reports into * `available` rather than misattributing. */ intentLocked: number; /** * Recoverable balance from subdust or expired (swept) virtual outputs — * recoverable in principle, so a lockup whose contract refuses a spend right * now is still counted and `total` does not lose it. * `VtxoManager.getRecoverableBalance()` answers the narrower question of * what a batch would hand back today, and excludes it. */ recoverable: number; /** * Funds under a now-deprecated signer past its cutoff (EXPIRED) that have not * yet been swept by the server. NOT spendable until they recover, so excluded * from `available`/`settled`/`preconfirmed` and from coin selection — but * still the wallet's funds, so counted in `total`. */ pendingRecovery: number; /** * Funds whose unilateral exit already happened — the output is onchain * behind its CSV timelock, so `Unroll.completeUnroll` is the only thing * that moves it. Excluded from `available`/`settled`/`preconfirmed`, from * `recoverable` (no batch can lift an onchain output), and from coin * selection — but still the wallet's funds, so counted in `total`. */ unrolled: number; /** * Total balance across offchain, recoverable, pending-recovery, unrolled, * and boarding funds. * * One known main-thread-only wedge: while a spend is in flight — `send`, * `sendBitcoin` or `settle` — its VTXO inputs are withheld from every bucket, * including this one. Boarding inputs are not: only virtual coins enter the * set. That state lives on the `Wallet` instance driving the spend, so a * service-worker client reading the same repository still counts them until * the spend settles. Both sides converge when it does. */ total: number; /** Asset balance entries (`assetId` & `amount`) the wallet owns. */ assets: Asset[]; /** * The subset of {@link assets} generic spending will accept, i.e. the asset * analogue of {@link available}. `assets - availableAssets` is what is held * but not selectable, for the {@link gated} and {@link intentLocked} causes * plus recovery and {@link unrolled} — assets have no per-cause split of * their own. */ availableAssets: Asset[]; } /** * Parameters accepted by `OnchainWallet.send`. * * @remarks * This shape was also used by the deprecated `Wallet.sendBitcoin` method. * New wallet sends should use `Recipient` via `IWallet.send`. * * @see Recipient */ interface SendBitcoinParams { /** Destination address. */ address: string; /** Amount to send in satoshis. */ amount: number; /** Optional fee rate override in sats/vB. */ feeRate?: number; /** * Optional memo associated with the transaction. * @deprecated Does not appear to have ever been used. */ memo?: string; /** * Optional explicit virtual output selection used by `Wallet.sendBitcoin`. * Ungated, like `settle({ inputs })`: whatever is named here is spent, even * if generic selection would skip it. * * @see IReadonlyWallet.getSpendableVtxos */ selectedVtxos?: ExtendedVirtualCoin[]; } /** * Asset amount paired with an asset id. * * @see AssetDetails */ interface Asset { /** Asset identifier. */ assetId: string; /** * Asset amount in base units. Typed as `bigint` because asset * supplies routinely exceed `Number.MAX_SAFE_INTEGER` (2^53 - 1) * and silently truncating in arithmetic would corrupt balances. */ amount: bigint; } /** * Recipient accepted by `IWallet.send`. * * @see IWallet.send */ interface Recipient { address: string; /** * BTC amount in satoshis. * * @defaultValue Dust amount (`330`). */ amount?: number; /** Assets to send to the same recipient (`assetId` & `amount`) */ assets?: Asset[]; extensions?: Array<{ type: number; payload: Uint8Array; }>; /** * The recipient contract's tapleaf set (`VtxoScript.encode` form), published * on this output's `PSBT_OUT_TAP_TREE` so its spending paths are recoverable * from the transaction alone — an address commits only to the output key. * * Refused unless it derives the recipient address's taproot key, and it must * come from `VtxoScript.encode()`: leaf depths are ignored on read and the * tree is rebuilt in arkd's canonical shape, so a tree from another encoder * is refused even where it commits to the same address. */ tapTree?: Bytes; } /** Object form of `IWallet.send`'s arguments; the variadic form has no slot for options. */ interface SendParams { /** One or more recipients — the variadic arguments of the other form. */ recipients: [Recipient, ...Recipient[]]; /** * Spend exactly these virtual outputs instead of letting the wallet choose. * Taken as given, like `settle({ inputs })`: nothing is added, so a shortfall * is an error rather than a top-up. Use when a contract must be funded from * coins outliving its timelock, which generic selection does not know about. * * @see IReadonlyWallet.getVtxos */ selectedVtxos?: ExtendedVirtualCoin[]; } /** * Known asset metadata fields. * * @remarks * Additional metadata keys are allowed through `AssetMetadata`. * * @see AssetMetadata */ type KnownMetadata = Partial<{ /** Asset name, e.g. "Tether USD" */ name: string; /** Asset symbol, e.g. "USDT" */ ticker: string; /** * Amount of decimal places to adjust the `amount` for * (e.g. `1_000_000` adjusted for `6` decimals = `1`) */ decimals: number; /** Image source that can be passed to an `` attribute. */ icon: string; }>; /** * Asset metadata including known fields and arbitrary extension keys. * * @see KnownMetadata */ type AssetMetadata = KnownMetadata & Record; /** * Asset details returned by `IAssetManager.getAssetDetails`. * * @see IAssetManager.getAssetDetails * @see AssetMetadata */ type AssetDetails = { /** Asset identifier. */ assetId: string; /** * Total issued supply in base units. Typed as `bigint` for the * same reason as {@link Asset.amount} — supplies often exceed * `Number.MAX_SAFE_INTEGER`. */ supply: bigint; /** Optional immutable metadata associated with the asset. */ metadata?: AssetMetadata; /** Optional control asset id required for future reissuance. */ controlAssetId?: string; }; /** * Parameters accepted by `IAssetManager.issue`. * * @see IAssetManager.issue * @see IssuanceResult */ interface IssuanceParams { /** Initial amount of asset to issue */ amount: bigint; /** Optional control asset ID that can be used for future reissuance */ controlAssetId?: string; /** Immutable asset metadata including `ticker`, `decimals`, `icon` */ metadata?: AssetMetadata; } /** * Result returned by `IAssetManager.issue`. * * @see IAssetManager.issue * @see IssuanceParams */ interface IssuanceResult { /** Arkade transaction ID where the asset was issued */ arkTxId: string; /** Permanent asset ID, made up of above `arkTxId` and zero-based asset group index */ assetId: string; } /** * Parameters accepted by `IAssetManager.reissue`. * * @see IAssetManager.reissue */ interface ReissuanceParams { /** Existing asset ID, made up of genesis (Arkade) transaction ID and zero-based asset group index */ assetId: string; /** Amount of asset to issue */ amount: bigint; } /** * Parameters accepted by `IAssetManager.burn`. * * @see IAssetManager.burn */ interface BurnParams { /** Existing asset ID, made up of genesis (Arkade) transaction ID and zero-based asset group index */ assetId: string; /** Amount of asset to burn */ amount: bigint; } /** * Explicit inputs and outputs accepted by `IWallet.settle`. * * @remarks * Inputs can include both offchain virtual outputs and onchain boarding inputs. * * @see IWallet.settle * @see Output */ interface SettleParams { /** * Offchain virtual outputs and/or onchain boarding inputs to settle. * * @remarks * Arknotes are settled by passing the `ArkNote` itself (it is an `ExtendedCoin`), not its * string form — `ArkNote.fromString(note)`. */ inputs: ExtendedCoin[]; /** Optional onchain outputs to create (i.e., exit to). */ outputs: Output[]; } /** * Onchain output status */ interface Status { /** Whether the output is confirmed */ confirmed: boolean; /** * Whether the output exists as a finalized batch leaf. * In the current mapping this is `true` for settled and swept virtual outputs, * and `false` for preconfirmed virtual outputs. * * @remarks * `isLeaf` is currently derived from `!isPreconfirmed` in the indexer mapping. * It is used primarily by transaction history classification to distinguish * finalized batch outputs from preconfirmed offchain outputs. */ isLeaf?: boolean; /** Block height where the output was confirmed, when known. */ block_height?: number; /** Block hash where the output was confirmed, when known. */ block_hash?: string; /** Block time where the output was confirmed, when known. */ block_time?: number; } /** * Virtual output status. * * @deprecated Use the canonical facts on {@link VirtualCoin} — `isSwept`, `isPreconfirmed`, * `isSpent`, `expiresAt`, `expiresAtHeight`, `commitmentTxIds`, `spentBy`, `settledBy` — and the * capability predicates {@link canSpendOffchain}, {@link canRecoverOnchain}, * {@link isVtxoSpent}, {@link isPastExpiry}. `state` collapses independent facts into one * lossy label; this object is retained only as a backward-compatible projection. */ interface VirtualStatus { /** * Extended output status. * * - `preconfirmed`: not yet finalized in a batch * - `settled`: finalized in a batch * - `swept`: expired/swept and recoverable in a new batch * - `spent`: destroyed by a later transaction * * @deprecated Lossy: the states are not orthogonal and collapse with precedence * `spent` > `swept` > `preconfirmed` > `settled`, so a spent VTXO that was also swept reports * only `spent`. Read `isSpent`/`isSwept`/`isPreconfirmed` instead, or a capability predicate. */ state: "preconfirmed" | "settled" | "swept" | "spent"; /** * Which batch commitment transaction(s) this virtual output depends on. * * @deprecated Use {@link VirtualCoin.commitmentTxIds}. */ commitmentTxIds?: string[]; /** * The earliest point at which this virtual output stops being safely preconfirmed, * in milliseconds. * * @deprecated Unit-ambiguous: the server returns a single scalar that is either unix seconds or * a block height, and both land here multiplied by 1000. Use {@link VirtualCoin.expiresAt} and * {@link VirtualCoin.expiresAtHeight}, which disambiguate the two. */ batchExpiry?: number; } /** Onchain output location data. */ interface Outpoint { /** Transaction ID where the output was created */ txid: string; /** Transaction output index for this output */ vout: number; } /** * Onchain output data. * * @see Outpoint */ interface Coin extends Outpoint { /** Value of the output in satoshis */ value: number; /** Onchain output status */ status: Status; } /** * Virtual output data. * * @remarks * The canonical facts (`isSwept`, `isPreconfirmed`, `isSpent`, `expiresAt`, `expiresAtHeight`, * `commitmentTxIds`) are optional because `VirtualCoin` is also a *construction* type: custom * {@link IndexerProvider} and {@link WalletRepository} implementations may hand back coins without * them. The SDK normalizes every incoming coin, so coins it returns always carry the facts that are * determinable; do not read these fields off a coin the SDK has not returned to you — use * {@link canSpendOffchain} / {@link canRecoverOnchain} / {@link isVtxoSpent} / * {@link isPastExpiry}, which normalize defensively. * * @see Coin */ interface VirtualCoin extends Coin { /** Creation time of the virtual output. */ createdAt: Date; /** The scriptPubKey (hex) locking this virtual output, as returned by the indexer. */ script: string; /** Whether this virtual output has been broadcasted onchain via an unroll (unilateral exit). */ isUnrolled: boolean; /** * Whether this virtual output is already spent (boolean helper for `spentBy`). * This is not set to true if the virtual output is unrolled or swept, only when it's spent offchain. */ isSpent?: boolean; /** Whether the server has swept the batch this virtual output belongs to. */ isSwept?: boolean; /** Whether this virtual output is not yet finalized in a batch. */ isPreconfirmed?: boolean; /** ID of the onchain commitment transaction that settled this output, if applicable. */ settledBy?: string; /** * ID of the offchain checkpoint transaction that spent this output. * * @remarks * The empty string means "not spent by anything" — test truthiness, never presence. */ spentBy?: string; /** ID of the offchain Arkade transaction that spent the above checkpoint output, if applicable. */ arkTxId?: string; /** Batch commitment transaction(s) this virtual output depends on. */ commitmentTxIds?: string[]; /** * Wall-clock batch expiry, when the server expressed expiry as a timestamp. * * @remarks * Mutually exclusive with `expiresAtHeight`; both are absent when there is no expiry. */ expiresAt?: Date; /** * Block-height batch expiry, when the server expressed expiry as a height (regtest-like * deployments). Evaluating it needs a chain tip — see {@link isPastExpiry}. */ expiresAtHeight?: number; /** * Virtual output status. * * @deprecated See {@link VirtualStatus}. */ virtualStatus: VirtualStatus; /** Assets carried by this virtual output, if any. */ assets?: Asset[]; } /** Wallet transaction direction. */ declare enum TxType { TxSent = "SENT", TxReceived = "RECEIVED" } /** * Composite key used to correlate a wallet transaction across layers. * * @see ArkTransaction */ interface TxKey { /** Boarding transaction id, when applicable. */ boardingTxid: string; /** Batch commitment transaction id, when applicable. */ commitmentTxid: string; /** Arkade transaction id, when applicable. */ arkTxid: string; } /** * The categories the history builder itself assigns. * * Four of them name the mechanism that moved the coin. `"gated"` names the * counterparty instead: an offchain row facing a contract row of this wallet's * that generic spending is closed on — a swap covenant, a lockup — which is the * same money {@link WalletBalance.gated} reports. History reads such a contract * as an external party, so the movement is a real send or receive rather than * change, and the tag is what lets a consumer tell "into my own escrow" from * "to a stranger" instead of the movement arriving unattributed. */ type BuiltinTxTag = "offchain" | "boarding" | "exit" | "batch" | "gated"; /** * The category the history builder assigns to a transaction. The `(string & {})` * arm keeps the union open — apps and resolvers can introduce their own * categories without a breaking change — while preserving editor autocomplete * for the built-in ones. */ type TxTag = BuiltinTxTag | (string & {}); /** * Wallet transaction history entry. * * @see TxKey * @see TxType */ interface ArkTransaction { /** Composite key referencing the related transaction ids. */ key: TxKey; /** Transaction direction. */ type: TxType; /** Net transaction amount in satoshis. */ amount: number; /** Whether the transaction is finalized. */ settled: boolean; /** Creation timestamp in milliseconds since epoch. */ createdAt: number; /** Assets sent or received by this transaction, if any. */ assets?: Asset[]; /** * The {@link TxTag} category assigned by the history builder. Always set on * transactions returned by the wallet's `getTransactionHistory()`; optional * only because a hand-built `ArkTransaction` may omit it. */ tag?: TxTag; } /** * Tapleaves required to spend or settle a wallet output. * * @see ExtendedCoin * @see ExtendedVirtualCoin */ type TapLeaves = { /** Tapleaf script used for the forfeit path. */ forfeitTapLeafScript: TapLeafScript; /** Tapleaf script used for the intent path. */ intentTapLeafScript: TapLeafScript; }; /** * Onchain output data enriched with tapscript and witness data. * * @see Coin * @see TapLeaves */ type ExtendedCoin = TapLeaves & EncodedVtxoScript & Coin & { extraWitness?: Bytes[]; }; /** * Virtual output data enriched with tapscript and witness data. * * @see VirtualCoin * @see TapLeaves */ type ExtendedVirtualCoin = TapLeaves & EncodedVtxoScript & VirtualCoin & { extraWitness?: Bytes[]; }; /** * Return whether a virtual output is below the dust threshold. * * @param vtxo - virtual output to inspect * @param dust - dust threshold in satoshis * @returns `true` when the virtual output value is below `dust` * * @see isRecoverable */ declare function isSubdust(vtxo: { value: number; } | bigint, dust: bigint): boolean; /** * Filtering options for `IWallet.getVtxos`. * * @see IWallet.getVtxos */ type GetVtxosFilter = { /** Include swept but still unspent virtual outputs. */ withRecoverable?: boolean; /** * Include virtual outputs that have been unrolled onchain. * * Authoritative on the *location* axis and only that: an exited output is * returned whatever else is true of it, spent ones included. So unlike * {@link withRecoverable}, this flag does not narrow to a capability — * test {@link canSweepOnchain} before acting on what comes back. * `Unroll.prepareUnrollTransaction`, the flag's main consumer, does. */ withUnrolled?: boolean; }; type GetSpendableVtxosFilter = GetVtxosFilter & { /** Exclude contracts retained for history from this spendable read. */ watchedOnly?: boolean; /** Query only contracts whose handler permits generic spending. */ genericallySpendableOnly?: boolean; /** Maximum age of a successful sync reused by this read, in milliseconds. Default: 0. */ maxSyncAgeMs?: number; /** Reject repository fallback when the selected contracts could not be synced. */ requireSynced?: boolean; }; /** * Readonly asset manager interface for asset operations that do not require wallet identity. * * @see IAssetManager */ interface IReadonlyAssetManager { /** * Fetch metadata and supply data for an asset. * * @param assetId - Asset identifier * @returns Asset details * @see AssetDetails */ getAssetDetails(assetId: string): Promise; } /** * Asset manager interface for asset operations that require wallet identity. * * @see IReadonlyAssetManager */ interface IAssetManager extends IReadonlyAssetManager { /** * Issue a new asset. * * @param params - Asset issuance parameters * @returns Asset issuance result * @see IssuanceParams * @see IssuanceResult */ issue(params: IssuanceParams): Promise; /** * Reissue an existing asset. * * @param params - Asset reissuance parameters * @returns Arkade transaction id * @see ReissuanceParams */ reissue(params: ReissuanceParams): Promise; /** * Burn an existing asset. * * @param params - Asset burn parameters * @returns Arkade transaction id * @see BurnParams */ burn(params: BurnParams): Promise; } /** * Core wallet interface for Bitcoin transactions with Arkade protocol support. * * This interface defines the contract that all wallet implementations must follow. * It provides methods for address management, balance checking, virtual output * operations, and transaction management including sending, settling, and unrolling. * * @see IReadonlyWallet */ interface IWallet extends IReadonlyWallet { /** * Signing identity associated with the wallet. * * A real signer, not a `ReadonlyIdentity` that structurally fits: contract * corridors need all four members — `sign`, `signMessage`, `signerSession` * and `xOnlyPublicKey` — and `signerSession` is the one a watch-only * identity lacks. `isSigningIdentity` is the check; a wallet that fails it * is refused as `WalletCannotSignError` before anything is funded, rather * than at the push that discovers there is no signer. */ identity: Identity; /** * Send bitcoin to a single Arkade address. * * @param params - Destination, amount, fee rate override, etc * @returns Arkade transaction id * @deprecated Use `send` * @see send * @see Recipient */ sendBitcoin(params: SendBitcoinParams): Promise; /** * Settle boarding inputs and/or preconfirmed virtual outputs into settled virtual outputs. * * @param params - Optional explicit settlement inputs and outputs * @param eventCallback - Optional callback that receives settlement events * @returns Arkade transaction id * @see SettleParams */ settle(params?: SettleParams, eventCallback?: (event: SettlementEvent) => void): Promise; /** * Send bitcoin and/or assets to one or more Arkade recipients, passed * either as variadic `Recipient`s or as a single `SendParams` object — * the latter also carries the inputs to spend. * * @param args - Recipients, or a `SendParams` object * @returns Arkade transaction id * @see SendParams * @example * ```typescript * await wallet.send({ address: 'ark1q...', amount: 1000 }) * * // choosing the inputs as well as the outputs * await wallet.send({ * recipients: [{ address: 'ark1q...', amount: 1000 }], * selectedVtxos: mine, * }) * ``` */ send(...args: [SendParams] | [Recipient, ...Recipient[]]): Promise; /** Asset manager bound to this wallet instance. */ assetManager: IAssetManager; /** @returns Delegation manager, when configured. */ getDelegateManager(): Promise; /** @deprecated alias for @see IWallet.getDelegateManager */ getDelegatorManager(): Promise; } /** * Readonly wallet interface for Bitcoin transactions with Arkade protocol support. * * This interface defines the contract that all wallet implementations must follow. * It provides methods for address management, balance checking, virtual output * operations, and transaction management including sending, settling, and unrolling. * * @see IWallet */ interface IReadonlyWallet { /** Readonly identity associated with the wallet. */ identity: ReadonlyIdentity; /** @returns Arkade address used for offchain funds. */ getAddress(): Promise; /** @returns Onchain boarding address used to move funds into Arkade. */ getBoardingAddress(): Promise; /** @returns The wallet's combined onchain and offchain balance. */ getBalance(): Promise; /** * Get virtual outputs tracked by the wallet. * * @param filter - Optional filtering flags * @returns virtual outputs with tapscript and witness data, normalized: every canonical fact * the capability predicates read is populated, whatever the underlying repository stored * @see GetVtxosFilter */ getVtxos(filter?: GetVtxosFilter): Promise; /** * The subset of {@link getVtxos} that generic spending may select: the same * filter, minus contracts the generic-spending gate closes, minus funds * awaiting recovery under a past-cutoff signer, minus outpoints locked by an * in-flight intent. Every implicit coin selection in the SDK reads this; * `getVtxos` stays the raw reporting/recovery read. * * Both exclusion sets are derived from one contract snapshot, so they cannot * disagree about which VTXOs exist. * * @param filter - Same coin flags and defaults as {@link getVtxos}, with opt-in contract scopes * @see GetSpendableVtxosFilter */ getSpendableVtxos(filter?: GetSpendableVtxosFilter): Promise; /** @returns Onchain boarding inputs tracked by the wallet. */ getBoardingUtxos(): Promise; /** @returns Wallet transaction history derived from boarding and Arkade activity. */ getTransactionHistory(): Promise; /** Resolvers that group/label {@link getActivityHistory} rows. */ readonly activity: ActivityRegistry; /** @returns Wallet history grouped into logical activities with signed net amounts. */ getActivityHistory(): Promise; /** * Get the contract manager associated with this wallet. * This is useful for querying contract state and watching for contract events. * * @returns Contract manager instance */ getContractManager(): Promise; /** Readonly asset manager bound to this wallet instance. */ assetManager: IReadonlyAssetManager; /** * Wipe all locally persisted wallet data (VTXOs, UTXOs, history, sync * cursor, contracts). */ clear(): Promise; } export { type BatchFinalizedEvent as $, type ArkTransaction as A, type ContractFilter as B, type ContractRepository as C, type IntentRepository as D, type ExtendedCoin as E, type ArkIntent as F, type GetVtxosFilter as G, type IntentFilter as H, type IWallet as I, type VirtualTxRepository as J, type VirtualTx as K, type VtxoBranch as L, ChainedTxType as M, type NormalizedExtendedVirtualCoin as N, type Outpoint as O, type Network as P, CSVMultisigTapscript as Q, type Recipient as R, type SendBitcoinParams as S, type TxNotificationEvent as T, type BatchStartedEvent as U, VtxoScript as V, type WalletRepository as W, type TreeSigningStartedEvent as X, TxTree as Y, type TreeNoncesEvent as Z, type BatchFinalizationEvent as _, type Identity as a, type ChainTx as a$, type BatchFailedEvent as a0, type TreeTxEvent as a1, type TreeSignatureEvent as a2, type DescriptorProvider as a3, type IReadonlyWallet as a4, type ReadonlyIdentity as a5, type OnchainProvider as a6, type DelegateProvider as a7, type ReadonlyWalletConfig as a8, type ExitCaptureMode as a9, type Asset as aA, type FeeInfo as aB, type CreateContractParams as aC, type GetContractsFilter as aD, type GetSpendablePathsOptions as aE, type GetAllSpendingPathsOptions as aF, type IssuanceParams as aG, type ReissuanceParams as aH, type BurnParams as aI, type RenewVtxosOptions as aJ, type WatchedScript as aK, type PathSelection as aL, type ContractEvent as aM, type AssetDetails as aN, type IssuanceResult as aO, type DelegateInfo as aP, type MigrationGlobalSkipReason as aQ, type MigrationLegSkipReason as aR, type SignerStatus as aS, type StorageConfig as aT, type IVtxoManager as aU, type ExplorerTransaction as aV, type TreeNonces as aW, type TreePartialSigs as aX, type PendingTx as aY, type EncodedVtxoScript as aZ, type Status as a_, type ExitDataSource as aa, type IReadonlyAssetManager as ab, type ContractSyncState as ac, type RecipientAddressContext as ad, type NetworkName as ae, type ArkInfo as af, ArkAddress as ag, type ContractWithVtxos as ah, selectPendingRecoveryOutpoints as ai, type Coin as aj, ContractManager as ak, type ContractManagerConfig as al, type SettlementConfig as am, VtxoManager as an, type SignerSession as ao, Transaction as ap, type SignedIntent as aq, Intent as ar, type TimeHeight as as, type DescriptorSigningRequest as at, type IntentFeeConfig as au, type OffchainInput as av, FeeAmount as aw, type OnchainInput as ax, type FeeOutput as ay, type ContractWatcherConfig as az, type WalletConfig as b, REGTEST_EMULATOR_PUBKEY as b$, type PathContext as b0, type ActivityIntent as b1, type ActivityResolver as b2, type ArkIntentState as b3, type ArkTapscript as b4, type AssetMetadata as b5, BITCOIN_EMULATOR_PUBKEY as b6, type BaseWalletConfig as b7, type BatchInfo as b8, type BatchSignableIdentity as b9, type EventSourceCapable as bA, type EventSourceFactory as bB, type EventSourceLike as bC, EventSourceUnavailableError as bD, type ExitChainResolver as bE, type ExtendedContractVtxo as bF, type GroupMembership as bG, type HandlerError as bH, type IDelegatorManager as bI, INTENT_TERMINAL_STATES as bJ, IndexerTxType as bK, type IntentCoin as bL, type KnownMetadata as bM, MUTINYNET_EMULATOR_PUBKEY as bN, type MigrateDeprecatedSignerOptions as bO, type MigrationLegReport as bP, type MigrationVtxoRef as bQ, MultisigTapscript as bR, type NewAddressType as bS, type Nonces as bT, type Output as bU, type PageResponse as bV, type PaginationOptions as bW, PartialSig as bX, type PrevTxSource as bY, PrevTxUnavailableError as bZ, type ProviderClass as b_, CLTVMultisigTapscript as ba, type CandidateDeps as bb, ChainTxType as bc, type CommitmentTx as bd, ConditionCSVMultisigTapscript as be, ConditionMultisigTapscript as bf, type ContractBalance as bg, type ContractEventCallback as bh, type ContractHandler as bi, type ContractState as bj, type ContractVtxo as bk, type ContractVtxoEvent as bl, ContractWatcher as bm, DEFAULT_VTXO_PAGE_SIZE as bn, DelegateManagerImpl as bo, type DelegateOptions as bp, DelegatorManagerImpl as bq, type DelegatorProvider as br, type DeprecatedSignerMigrationReport as bs, type DeprecatedSignerReport as bt, DigestMismatchError as bu, type Discoverable as bv, type DiscoveredContract as bw, type DiscoveryDeps as bx, ESPLORA_URL as by, EsploraProvider as bz, type WalletBalance as c, isVirtualCoin as c$, type RawTxSource as c0, RestDelegateProvider as c1, RestDelegatorProvider as c2, type ScanContractsOptions as c3, type ScanResult as c4, type ScheduledSession as c5, SettlementEventType as c6, type SignRequest as c7, type SignerClassification as c8, type SignerSet as c9, canSweepOnchain as cA, classifyAgainstSignerSet as cB, classifyContractSigner as cC, collabExitResolver as cD, configureEventSource as cE, createDefaultActivityRegistry as cF, createExitChainResolver as cG, decodeTapscript as cH, defaultEmulatorPubkey as cI, getConfiguredEventSource as cJ, getNetwork as cK, getRandomId as cL, getSequence as cM, hasCandidates as cN, hasTerminalSpend as cO, isBatchSignable as cP, isContractVtxoEvent as cQ, isCooperativelyMigratable as cR, isDiscoverable as cS, isEventSourceUnavailableError as cT, isExpired as cU, isPastExpiry as cV, isRecoverable as cW, isSigningIdentity as cX, isSpendable as cY, isSubdust as cZ, isTerminalIntentState as c_, type SubscriptionEvent as ca, type SubscriptionHeartbeat as cb, type SweepTxNotification as cc, type TapLeaves as cd, TapTreeCoder as ce, TapscriptType as cf, type Tx as cg, type TxHistoryRecord as ch, type TxKey as ci, type TxNotification as cj, type TxTag as ck, type TxTreeNode as cl, TxType as cm, type VirtualStatus as cn, type Vtxo as co, type VtxoChain as cp, type VtxoType as cq, type WalletMode as cr, type WithPrevTx as cs, assertAllowedSighashTypes as ct, assetMintResolver as cu, attachPrevArkTxs as cv, attachPrevoutTxs as cw, boardingResolver as cx, canRecoverOnchain as cy, canSpendOffchain as cz, type GetSpendableVtxosFilter as d, isVtxoExpiringSoon as d0, isVtxoSpent as d1, networks as d2, resolveEmulatorPubkey as d3, resolveEventSource as d4, resolvePrevTxs as d5, scriptFromTapLeafScript as d6, signerSetFromInfo as d7, toXOnlySignerHex as d8, withPrevTxs as d9, type TapscriptDeriving as da, ActivityRegistry as e, type Activity as f, type IContractManager as g, type GetNewAddressesOptions as h, type NewAddress as i, type IDelegateManager as j, type SettleParams as k, type SettlementEvent as l, type SendParams as m, type IAssetManager as n, RestArkProvider as o, RestIndexerProvider as p, type SubscriptionResponse as q, type ArkProvider as r, type IndexerProvider as s, type RelativeTimelock as t, type TapLeafScript as u, type VirtualCoin as v, type Contract as w, type ExtendedVirtualCoin as x, type VtxoRepositoryKey as y, type WalletState as z };