@arkade-os/swap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1747 @@
1
+ import { asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, RestArkProvider, VHTLC, Identity } from '@arkade-os/sdk';
2
+ import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, R as RfqStatus, b as RfqTransport, O as OnchainHtlc, C as ChainSource, c as ChainUtxo, d as OnchainHtlcPhase } from './rfq-CRgIOQ_y.cjs';
3
+ export { e as ARKADE_ASSET, f as ARKADE_BTC, g as AddressMismatch, h as AssetSwapFallbackSecrets, i as AssetSwapStatus, B as BTC_ASSET_ID, D as DerivedSwapSecrets, j as DeterministicSigner, H as HtlcUtxo, I as InMemoryAssetSwapRepository, k as InvoiceFacts, L as LIGHTNING_BTC, l as LIGHTNING_RECEIVE_PAIR, m as LIGHTNING_SEND_PAIR, n as MAX_MIN_CONFIRMATIONS, o as MIN_CLAIM_WINDOW_SECONDS, p as MIN_HEADROOM_SECONDS, q as ONCHAIN_BTC, r as ONCHAIN_CLAIM_MARGIN_SECONDS, s as ONCHAIN_DUST_SATS, t as ONCHAIN_ORDER_MARGIN_SECONDS, u as ONCHAIN_RECEIVE_PAIR, v as ONCHAIN_SECONDS_PER_BLOCK, w as ONCHAIN_SEND_PAIR, x as OnchainHtlcParams, y as OnchainNetwork, z as RFQ_PREIMAGE_TAG, E as RFQ_TERMINAL_STATES, F as RefundBlockedReason, G as RefundNotLocallyPossibleError, J as RelaySocket, K as RfqQuote, N as RfqRefusalReason, S as StoredSwapSecrets, P as SwapRefusal, Q as SwapSecrets, T as addAssetSwap, U as adoptSwapDescriptor, V as arkadeSwapRequest, W as assertFundable, X as assertReceivable, Y as awaitOnchainFill, Z as buildHtlcClaim, _ as buildHtlcRefund, $ as buildPreimageMessage, a0 as claimOnchainFill, a1 as classifyOnchainHtlc, a2 as deriveLightningReceive, a3 as deriveOnchainReceive, a4 as deriveOnchainSend, a5 as derivePreimage, a6 as deriveSwapSecrets, a7 as extractPreimage, a8 as getAssetSwaps, a9 as getAssetSwapsOrThrow, aa as httpTransport, ab as isDeterministicSigner, ac as lightningReceiveRequest, ad as lightningSendRequest, ae as lightningSendVtxoScript, af as newPreimage, ag as newRfqId, ah as offerTermsFromQuote, ai as onchainHtlcScript, aj as onchainReceiveRequest, ak as onchainSendRequest, al as paymentHashOf, am as preimageForRfqSecrets, an as randomSwapSecrets, ao as receiveVtxoScript, ap as relayTransport, aq as requestLightningReceive, ar as requestLightningSend, as as requestOnchainReceive, at as requestOnchainSend, au as rfqPair, av as rfqSecretsOfRecord, aw as rfqSecretsToRecord, ax as senderIdentityForRfqSecrets, ay as senderIdentityForSwapRecord, az as senderPubkeyForRfqSecrets, aA as unilateralClaimDelay, aB as unilateralRefundDelay, aC as unilateralRefundWithoutReceiverDelay, aD as updateAssetSwap, aE as updateAssetSwapBestEffort, aF as verifyLockupAddress, aG as verifyReceiveInvoice } from './rfq-CRgIOQ_y.cjs';
4
+ import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
5
+
6
+ /** The contracts — pure data, shared verbatim with any other implementation. */
7
+ declare const swapPrograms: Record<"wantAsset" | "wantBtc", ReturnType<typeof arkade.parseArtifact>>;
8
+ /** A full-fill offer. Exactly one field names an asset: `wantAsset` set = the
9
+ * fill must deliver that asset (the deposit may be BTC or another asset,
10
+ * identified by the funding vtxo itself); `offerAsset` set = the user
11
+ * deposits that asset and wants sats. */
12
+ interface Offer {
13
+ /** The scriptPubKey of the swap contract. */
14
+ swapPkScript: Uint8Array;
15
+ /** Amount the user wants (asset units, or sats when wanting BTC). */
16
+ wantAmount: bigint;
17
+ /** The asset the user wants. Omitted when wanting BTC. */
18
+ wantAsset?: asset.AssetId;
19
+ /** The asset the user deposits. Omitted when depositing BTC. */
20
+ offerAsset?: asset.AssetId;
21
+ /** Maker's taproot scriptPubKey (34 bytes) — where the fill must pay. */
22
+ makerPkScript: Uint8Array;
23
+ /** Maker's x-only key (32 bytes) — the cancel path's `user` signer. */
24
+ makerPublicKey: Uint8Array;
25
+ /** Covenant co-signer (emulator) x-only key (32 bytes). */
26
+ emulatorPubkey: Uint8Array;
27
+ }
28
+ /** Compile the offer's contract: program + args -> taproot tree. */
29
+ declare function offerVtxoScript(offer: Omit<Offer, "swapPkScript">, serverPubkey: Uint8Array): InstanceType<typeof arkade.ArkadeProgramScript>;
30
+ /** Extension packet type tag for Arkade Intents offers. */
31
+ declare const OFFER_PACKET_TYPE = 3;
32
+ /** Serialize an offer to TLV bytes (the packet payload). */
33
+ declare function encodeOffer(offer: Offer): Uint8Array;
34
+ /** Parse TLV bytes into an offer. Throws on malformed or unknown records. */
35
+ declare function decodeOffer(data: Uint8Array): Offer;
36
+ /**
37
+ * Build a new offer for `wallet` (the user). Fund `address` with the side
38
+ * you deposit, embedding the returned extension, and the solver does the rest:
39
+ *
40
+ * // BTC -> asset
41
+ * const o = await createOffer(wallet, ARK, EMULATOR_PUBKEY, { wantAmount: 1000n, wantAsset })
42
+ * await wallet.send({ address: o.address, amount: 1000, extensions: [o.extension] })
43
+ *
44
+ * // asset -> BTC (the sats are the VTXO carrier for the asset)
45
+ * const o = await createOffer(wallet, ARK, EMULATOR_PUBKEY, { wantAmount: 1000n, offerAsset })
46
+ * await wallet.send({ address: o.address, amount: 500,
47
+ * assets: [{ assetId, amount: 1000n }],
48
+ * extensions: [o.extension] })
49
+ *
50
+ * Broadcasts nothing, but does write locally: the covenant is registered with
51
+ * the wallet's contract manager before the address is returned, so the deposit
52
+ * is watched from the moment it lands and is marked as escrow (see
53
+ * {@link registerOfferContract}). Registration deliberately happens *before*
54
+ * funding rather than after: nothing is at stake yet, so a failure can throw
55
+ * and be retried, where the same failure after `wallet.send` would leave a
56
+ * funded deposit unwatched with no way to notice.
57
+ */
58
+ declare function createOffer(wallet: IWallet, arkServerUrl: string,
59
+ /** Covenant co-signer (emulator) x-only key — the SOLVER's deployment,
60
+ * not the user's. This library does NOT fetch or verify it: clients
61
+ * have no network path to the emulator, only the solver and covclaimd
62
+ * do. The caller must obtain this out-of-band, before calling this
63
+ * function, from the solver's signed registry/corridor card (its
64
+ * `emulator_pubkey`, added in arkade-os/solver-registry#18) or an
65
+ * equivalent source it
66
+ * independently trusts, and is responsible for having checked it
67
+ * against that trusted value itself. */
68
+ emulatorPubkey: Uint8Array, params: {
69
+ wantAmount: bigint;
70
+ wantAsset?: asset.AssetId;
71
+ offerAsset?: asset.AssetId;
72
+ }): Promise<{
73
+ /** The encoded offer, hex. **Persist this** — it is the only input
74
+ * `cancelOffer` needs to rebuild the covenant, and the restore scan reads
75
+ * the same bytes back off the funding tx into `AssetSwap.offerHex`. */
76
+ offerHex: string;
77
+ /** Ready for `wallet.send`'s `extensions` — the caller never handles the packet type. */
78
+ extension: {
79
+ type: number;
80
+ payload: Uint8Array;
81
+ };
82
+ /** The swap address to fund. Nothing exists on chain until the deposit
83
+ * lands here: `createOffer` is pure derivation and broadcasts nothing.
84
+ * Identical offers derive an identical address, so the funding txid — not
85
+ * the address — is what identifies one deposit. */
86
+ address: string;
87
+ /** The covenant's scriptPubKey: the key an indexer watches to spot the
88
+ * deposit and its later spend (`AssetSwap.swapPkScript`). */
89
+ swapPkScript: Uint8Array;
90
+ }>;
91
+ /**
92
+ * Cancel an offer: spend the swap VTXO back to the user. Returns the ark txid.
93
+ *
94
+ * This is the refund path — how a user takes back a deposit no solver filled.
95
+ * **Neither program carries a timelock**, so an unfilled deposit keeps its
96
+ * place at the swap address rather than expiring: no deadline to miss and no
97
+ * "expired" state to unwind, at the cost of the refund being something the
98
+ * user asks for rather than something a clock delivers.
99
+ *
100
+ * Both paths out of the covenant are deliberately asymmetric:
101
+ * - `fulfill` is signed by the **server alone**, but the covenant constrains
102
+ * it to pay output 0 to `makerWP` for at least `wantAmount` — a solver
103
+ * cannot take the deposit without delivering.
104
+ * - `cancel` is a **2-of-2 of the user and the server**, so cancelling is
105
+ * cooperative: the server co-signs. No solver signature is involved, so the
106
+ * refund never depends on the counterparty being reachable.
107
+ *
108
+ * Cancel therefore races a fill rather than pre-empting it. An offer the solver
109
+ * is filling in the same moment may be spent by `fulfill` first, in which case
110
+ * this throws "no spendable VTXO at the swap address" — which means the swap
111
+ * completed, not that anything failed. `restoreAssetSwaps` classifies the two
112
+ * spends apart afterwards by the leaf each took (see `classifySpend`).
113
+ *
114
+ * Marking the deposit as escrow (see {@link registerOfferContract}) does not
115
+ * close this path: the gate's subject is *implicit* coin selection, and cancel
116
+ * names its input outpoint explicitly. The user keeps the only spend route
117
+ * that was ever theirs to take.
118
+ *
119
+ * Identical offers derive the same address, so `fundingTxid` selects the exact
120
+ * deposit; without it the address must hold exactly one spendable VTXO — with
121
+ * several, cancel refuses to guess and throws.
122
+ * `swapAddress` (the funded address) pins the server key the covenant was
123
+ * built with, so cancel keeps working across a server signer rotation; without
124
+ * it a rotated key is detected and reported rather than reading as a missing
125
+ * VTXO.
126
+ *
127
+ * **When the matching swap record is present, this records its own outcome,
128
+ * and that is what makes the live watcher cheap.** `cancel` is a 2-of-2 of
129
+ * user and server, so a cancel can only be the user's own act: on a
130
+ * successful submit this *is* the authoritative answer, and writing it here
131
+ * means `watchOfferSwaps` has nothing left to decide for our own cancels — it
132
+ * sees a terminal record and leaves it alone. The status moves to `cancelling`
133
+ * first so a crash between submit and record leaves a marker rather than a
134
+ * swap that still looks pending.
135
+ *
136
+ * Passing a repository that does not contain the swap record is allowed: the
137
+ * cancel still submits and returns its txid, but no local status is written,
138
+ * so the watcher or restore scan must classify the spend later.
139
+ *
140
+ * When a local record exists, the txid is only knowable after `send()`
141
+ * returns, so a spend event that arrives in that window finds a `cancelling`
142
+ * record and classifies the spend by its covenant leaf instead — the same
143
+ * answer, one indexer read more.
144
+ */
145
+ declare function cancelOffer(wallet: IWallet, arkServerUrl: string, offerHex: string, opts: {
146
+ repository: AssetSwapRepository;
147
+ fundingTxid?: string;
148
+ swapAddress?: string;
149
+ }): Promise<string>;
150
+
151
+ /**
152
+ * Quoting: solver discovery and the pricing guardrails around a plan.
153
+ *
154
+ * The user side of a swap — the layer that works out what one would cost
155
+ * before `offer` commits to it. This is request-for-quote like every other
156
+ * Arkade Intents corridor; what is specific here is that the quote resolves
157
+ * client-side, from the market card the solver publishes (its price feed and
158
+ * its fee) rather than over a relay roundtrip. The card commits a solver to a
159
+ * price; only a fill commits it to this swap, so nothing here is signed and no
160
+ * inventory is reserved.
161
+ *
162
+ * Relay-negotiated quotes are where every corridor converges, this one
163
+ * included. What stays specific to intra-Arkade is the settlement covenant,
164
+ * not the negotiation. See the README's Roles section for why this package
165
+ * says user and solver rather than maker and taker.
166
+ */
167
+
168
+ /** Shared quote options so every quote path agrees.
169
+ * No safety margin on top of the market fee: pricing drift between quote
170
+ * and fill is the solver's risk to manage, not the user's to prepay. */
171
+ declare const QUOTE_OPTIONS: {
172
+ readonly safetyBps: 0;
173
+ };
174
+ /** Feed fetcher with a short per-URL TTL cache. A quote UI refetches the
175
+ * market's price feed on every debounced keystroke, and public feeds
176
+ * (CoinGecko) rate-limit that burst hard enough that big amounts reliably die
177
+ * as "Quote unavailable" mid-typing — one feed value per TTL window is fresh
178
+ * enough for a preview whose rate is re-checked at fill anyway.
179
+ * ponytail: no stale-serve when the fetch itself fails; add one if feeds
180
+ * flake beyond the TTL window (cap the staleness — the feed value becomes
181
+ * the covenant floor, so an old price must never price a real offer).
182
+ * Keyed on the request URL, so it assumes a market's feed URL is stable and
183
+ * amount-invariant (true today); a cache-busting nonce would silently make it
184
+ * a no-op — the flat-feedCalls swap test guards against that regressing. */
185
+ declare const makeCachedFeedFetch: (ttlMs?: number, fetchImpl?: typeof fetch) => typeof fetch;
186
+ interface DiscoverMarketsOptions {
187
+ network: Network;
188
+ /** The network's solver registry index URL; no registry means no markets. */
189
+ registryUrl: string | undefined;
190
+ /** Backs the 1-hour markets cache and its stale fallback. Omit for a
191
+ * one-shot discovery that always hits the registry. */
192
+ repository?: AssetSwapRepository;
193
+ /** Locally pinned solver cards to merge with the registry's markets. */
194
+ localCards?: LocalCardInput[];
195
+ /** Receives discovery warnings (stale index, skipped cards, …). */
196
+ logger?: (...args: unknown[]) => void;
197
+ /** Custom fetch (tests, mobile runtimes). Defaults to global fetch.
198
+ * ponytail: no request deadline here — a caller that needs one wraps its
199
+ * own fetchImpl with an AbortSignal; add one if a hung registry ever
200
+ * strands discovery in practice. */
201
+ fetchImpl?: typeof fetch;
202
+ /** `false` forces a refetch past a fresh cache (a user-triggered reload).
203
+ * It does not disable the stale-cache fallback: an unreachable registry
204
+ * still serves the last known markets rather than none. */
205
+ useCache?: boolean;
206
+ }
207
+ /**
208
+ * Markets from the network's solver registry; [] when none is configured.
209
+ * Registry content changes rarely, so results are cached for an hour and a
210
+ * stale cache backstops an unreachable registry (quotes stay live either way).
211
+ */
212
+ declare const discoverMarkets: (options: DiscoverMarketsOptions) => Promise<DiscoveredMarket[]>;
213
+ /** Best market for a from/to pair, in either orientation. `give` is the side
214
+ * the sender deposits; `wantSide` skips markets whose receive side is
215
+ * disabled (max = "0"). */
216
+ declare const findMarket: (markets: DiscoveredMarket[], fromId: string, toId: string) => {
217
+ market: DiscoveredMarket | null;
218
+ give: Side;
219
+ } | undefined;
220
+ type PlanError = "insufficient-balance" | "side-disabled" | "below-min" | "above-max" | "below-dust";
221
+ /** Validate a plan against the user's balance and the server dust limit. */
222
+ declare const validatePlan: (plan: OfferPlan, giveBalance: bigint, dust: bigint) => PlanError | undefined;
223
+
224
+ /** Browser backend over the SDK's shared IndexedDB manager — the same
225
+ * infrastructure the wallet already uses for its Boltz swap repository. */
226
+ declare class IndexedDbAssetSwapRepository implements AssetSwapRepository {
227
+ private readonly dbName;
228
+ readonly version: 1;
229
+ private dbPromise;
230
+ constructor(dbName?: string);
231
+ private ensureDb;
232
+ private readStore;
233
+ /** Every write in one place, so none of them can forget to await the
234
+ * commit. Requests need no individual await: a failed one aborts the
235
+ * transaction, which `txDone` reports. */
236
+ private write;
237
+ saveSwap(swap: AssetSwap): Promise<void>;
238
+ getAllSwaps(): Promise<AssetSwap[]>;
239
+ getScannedTxids(): Promise<Set<string>>;
240
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
241
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
242
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
243
+ /** All stores in one transaction: clearing swaps but keeping scanned txids
244
+ * would leave the restore scan permanently skipping those funding txs, so
245
+ * a partial clear must not be observable. */
246
+ clear(): Promise<void>;
247
+ [Symbol.asyncDispose](): Promise<void>;
248
+ }
249
+
250
+ /**
251
+ * The subset of a wallet transaction record the swap scan reads.
252
+ *
253
+ * No `assets`: a spend is classified from the covenant leaf it took, read off
254
+ * the spending transaction itself (see {@link classifySpend}). A wallet
255
+ * record's asset field is a net delta — an asset offer's cancel moves the asset
256
+ * out and back, netting to nothing — so it cannot answer the question.
257
+ */
258
+ interface Tx {
259
+ type: string;
260
+ /** The virtual (ark) txid; the funding tx's identity. */
261
+ redeemTxid: string;
262
+ boardingTxid?: string;
263
+ roundTxid?: string;
264
+ /** Unix seconds. */
265
+ createdAt?: number;
266
+ }
267
+ /** The indexer surface the restore scan needs — narrower than a full provider. */
268
+ type RestoreIndexer = Pick<RestIndexerProvider, "getVirtualTxs" | "getVtxos">;
269
+ /**
270
+ * What became of a deposit, as the spending transaction reports it.
271
+ *
272
+ * `indeterminate` is not a third outcome — it is the absence of one, and the
273
+ * caller decides whether to retry or accept a default.
274
+ */
275
+ type SpendKind = "cancelled" | "fulfilled" | "indeterminate";
276
+ /**
277
+ * Classify a spend by the covenant leaf it took.
278
+ *
279
+ * The covenant's whole vocabulary is two leaves: `cancel` returns the deposit
280
+ * to the user, `fulfill` is the solver paying for it. A submitted ark tx keeps
281
+ * each input's `tapLeafScript`, so the spend *states* which one it used — this
282
+ * reads an answer rather than inferring one.
283
+ *
284
+ * **Hand it the transaction that actually spends the deposit outpoint, which is
285
+ * the checkpoint, not the ark tx.** A spend is two linked transactions: the
286
+ * checkpoint (`vtxo.spentBy`) takes the deposit outpoint and carries the
287
+ * covenant leaf, and the ark tx (`vtxo.arkTxId`) spends the checkpoint's output
288
+ * — carrying the same leaf, but over an outpoint that is not the deposit's. A
289
+ * caller that offers only the ark tx gets `indeterminate` for every real spend,
290
+ * which is exactly what the first version of this function did.
291
+ * {@link classifyDepositSpend} takes both and picks whichever answers.
292
+ *
293
+ * What it replaces, and why: the previous test asked what the transaction
294
+ * moved. That works only while the deposit is invisible to the wallet. Once the
295
+ * covenant is a registered contract the deposit joins the wallet's own coins,
296
+ * every wallet-level asset figure becomes a *net* delta, and an asset offer's
297
+ * cancel — asset out of the covenant, same asset back to the user — nets to
298
+ * zero and reads exactly like its fill. Leaves do not have that failure mode,
299
+ * and they also survive batching: a solver filling several offers in one tx
300
+ * gives each input its own leaf.
301
+ *
302
+ * `serverPubkey` must be the key the covenant was *funded* against. If it has
303
+ * rotated since, the rebuilt script will not match the offer's own
304
+ * `swapPkScript` and this returns `indeterminate` rather than guessing —
305
+ * `cancelOffer` diagnoses the same mismatch the same way.
306
+ */
307
+ declare function classifySpend(offer: Offer, serverPubkey: Uint8Array, spendTx: Transaction, deposit: {
308
+ txid: string;
309
+ vout: number;
310
+ }): SpendKind;
311
+ /**
312
+ * The txids that may hold a deposit's spend, in the order worth trying: the
313
+ * checkpoint first, since it is the one carrying the deposit outpoint.
314
+ */
315
+ declare const spendTxidsOf: (vtxo: {
316
+ spentBy?: string;
317
+ arkTxId?: string;
318
+ }) => string[];
319
+ /**
320
+ * Classify a deposit's spend across both halves of it.
321
+ *
322
+ * A caller holds two txids for one spend — `spentBy` (the checkpoint, which
323
+ * takes the deposit outpoint) and `arkTxId` (the ark tx built on it) — and
324
+ * cannot tell from the outside which shape a given deployment produced: for a
325
+ * settlement they may be the same id. Try each and take the first definite
326
+ * answer, so the classification does not depend on that distinction.
327
+ */
328
+ declare function classifyDepositSpend(offer: Offer, serverPubkey: Uint8Array, spendTxs: Iterable<Transaction>, deposit: {
329
+ txid: string;
330
+ vout: number;
331
+ }): SpendKind;
332
+ /**
333
+ * Scan the given candidates for offer packets and rebuild the AssetSwap
334
+ * records the store lost. Returns the rebuilt swaps plus the txids that got
335
+ * an authoritative answer (fetched fine, vtxo lookup fine) — the caller
336
+ * persists those so they are never fetched again.
337
+ *
338
+ * ## A spent deposit is classified or left alone — never guessed
339
+ *
340
+ * Whether a spent deposit was filled or cancelled is read from the spending
341
+ * transaction's covenant leaf ({@link classifySpend}), fetched from the same
342
+ * indexer as everything else rather than from the `txs` you pass. A spend that
343
+ * cannot be classified — not fetchable yet, or gone by neither leaf — leaves
344
+ * the funding txid unanswered, so nothing is persisted and a later scan decides
345
+ * it. Nothing sticky is written on a guess.
346
+ *
347
+ * That is a deliberate reversal: this used to restore an unclassifiable spend
348
+ * as `fulfilled`, which a persisted record then made permanent, because the
349
+ * spending tx came from the caller's possibly-lagging history. It no longer
350
+ * does, so the `existingIds` escape hatch is no longer a correction mechanism
351
+ * for a wrong label — it is only a skip list.
352
+ *
353
+ * `serverPubkey` must be the server key the covenants were funded against; a
354
+ * key that has rotated since makes every affected swap unclassifiable rather
355
+ * than misclassified.
356
+ */
357
+ declare function restoreAssetSwaps(indexer: RestoreIndexer, txs: Tx[], existingIds: ReadonlySet<string>, opts: {
358
+ serverPubkey: Uint8Array;
359
+ scanned?: ReadonlySet<string>;
360
+ }): Promise<{
361
+ restored: AssetSwap[];
362
+ scannedTxids: string[];
363
+ }>;
364
+
365
+ /**
366
+ * The record change a classified spend implies, or `undefined` when it implies
367
+ * none — an already-resolved swap, or a spend nobody could classify.
368
+ *
369
+ * Pure, so a consumer with its own store can apply the same transition without
370
+ * taking the watcher, and so re-delivery of an event is a no-op rather than a
371
+ * rewrite.
372
+ */
373
+ declare function spendUpdate(swap: AssetSwap, spend: {
374
+ txid: string;
375
+ kind: SpendKind;
376
+ at?: number;
377
+ }): Partial<Omit<AssetSwap, "id">> | undefined;
378
+ /** A running watcher. `idle()` exists because the writes are async: shutdown
379
+ * and tests both need to know when in-flight updates have settled. */
380
+ interface OfferSwapWatcher {
381
+ stop(): void;
382
+ idle(): Promise<void>;
383
+ }
384
+ interface WatchOfferSwapsParams {
385
+ wallet: IWallet;
386
+ /** Same URL `createOffer`/`cancelOffer` take; used to read a spending tx
387
+ * when the exact classifier cannot answer. */
388
+ arkServerUrl: string;
389
+ repository: AssetSwapRepository;
390
+ /** Called after a change is persisted. A notification, not a store. */
391
+ onUpdate?: (swap: AssetSwap) => void;
392
+ }
393
+ /**
394
+ * Subscribe to the wallet's contract events and drive offer swap status.
395
+ *
396
+ * Registration ({@link createOffer}) is what makes this possible: only a
397
+ * registered covenant is watched, so only registered offers produce events.
398
+ * Offers funded before registration existed stay on the restore scan.
399
+ *
400
+ * This depends on the wallet's contract event transport. In Node, callers must
401
+ * provide an `EventSource` implementation or use a runtime where it is enabled;
402
+ * otherwise live updates do not arrive and restore remains the fallback.
403
+ */
404
+ declare function watchOfferSwaps({ wallet, arkServerUrl, repository, onUpdate, }: WatchOfferSwapsParams): Promise<OfferSwapWatcher>;
405
+
406
+ /**
407
+ * Coverage: whether an offer's script is in the wallet's watched set.
408
+ *
409
+ * Both edges live here because they are one decision seen from two sides.
410
+ * `createOffer` promotes a script the moment it hands the user an address to
411
+ * fund ({@link promoteOfferContract}); the watcher and the restore consumer
412
+ * retire it once nothing at that script holds funds any more
413
+ * ({@link retireSettledOfferContracts}). Identical offers derive one script, so
414
+ * those two can name the same row — and a demotion that lands after a promotion
415
+ * recreates exactly the failure the promotion exists to prevent: an address the
416
+ * user was told to fund, out of the subscription, the poll and every sync.
417
+ *
418
+ * Two things keep them apart:
419
+ *
420
+ * - **Per-script serialization.** `setContractWatchState` is a read-modify-write
421
+ * over the contract row, so promotion and demotion are ordered here rather
422
+ * than left to interleave inside the manager.
423
+ * - **The issuance mark.** A swap record is keyed by its funding txid, so an
424
+ * offer that has been created but not yet funded has no record at all — it is
425
+ * invisible to a liveness check over records, for as long as the user takes
426
+ * to send. {@link promoteOfferContract} therefore records the issuance
427
+ * itself, and a script with an address still waiting for its deposit is never
428
+ * retired.
429
+ *
430
+ * The mark is per process: a second wallet context (another tab, a service
431
+ * worker) issuing an offer while this one retires the same script is not
432
+ * covered, and neither is an address issued before a restart and funded after
433
+ * it. Both leave a funded-but-unwatched row that the restore scan still finds —
434
+ * the same backstop every other best-effort step here relies on.
435
+ */
436
+
437
+ /** The one contract-manager capability changing coverage needs. */
438
+ type OfferContractRetirer = Pick<IContractManager, "setContractWatchState">;
439
+ /**
440
+ * Retire every offer script in `swaps` that no live record still holds — the
441
+ * batch form of what the watcher does per spend event, for a consumer that
442
+ * applies {@link restoreAssetSwaps} results without running the watcher.
443
+ *
444
+ * Takes the caller's full record list: liveness is a property of all records at
445
+ * a script, so a partial list would retire a script another record still holds.
446
+ */
447
+ declare function retireSettledOfferContracts(manager: OfferContractRetirer, swaps: AssetSwap[]): Promise<void>;
448
+
449
+ interface SealedClaimPacket {
450
+ /** `ephPub(33) ‖ nonce(12) ‖ ciphertext`, base64 — wire-ready. This is
451
+ * the whole packet: the RFQ request's `claim_packet` field carries
452
+ * exactly this string. */
453
+ ciphertext: string;
454
+ }
455
+ interface ClaimPacketInput {
456
+ preimage: Uint8Array;
457
+ /** covclaimd's public key, 33-byte compressed (from its /v1 info). */
458
+ covclaimdPubkey: Uint8Array;
459
+ }
460
+ /**
461
+ * Seal a preimage to covclaimd. WebCrypto AES-GCM, hence async.
462
+ *
463
+ * The ephemeral key and nonce are generated here and CANNOT be supplied by a
464
+ * caller. That is the point of this signature: AES-GCM under a repeated
465
+ * (key, nonce) pair is a total break — forgery and plaintext recovery, not a
466
+ * degradation — so an optional `nonce` on a production export is a loaded gun
467
+ * whatever the doc comment says. Deterministic sealing lives in the test
468
+ * helper, where no consumer reaches it by accident.
469
+ */
470
+ declare function sealClaimPacket(input: ClaimPacketInput): Promise<SealedClaimPacket>;
471
+
472
+ /** True for the states after which the solver will report nothing further. */
473
+ declare const isRfqTerminal: (state: string) => boolean;
474
+ /**
475
+ * The terminal states that mean the swap is OVER and the lockup is already
476
+ * gone — the solver either claimed it (`settled`, revealing the preimage) or
477
+ * returned it (`refunded`). A trader seeing one of these has nothing left to
478
+ * do.
479
+ *
480
+ * Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
481
+ * and `stuck` are terminal for the NEGOTIATION but say nothing about whether
482
+ * the trader's sats are still sitting at the lockup. A trader that funded just
483
+ * as the quote expired, or whose solver wedged mid-payment, is exactly the
484
+ * trader who needs the refund most — so those states must not short-circuit
485
+ * it. {@link refundIfUnresolved} treats them as "keep going", and lets the
486
+ * on-chain VTXO lookup be the authority on whether anything is actually there.
487
+ */
488
+ declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
489
+ /**
490
+ * Poll a swap's status until it reaches a terminal state.
491
+ *
492
+ * Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
493
+ * an optional unix-seconds `deadline`, and a thrown error carrying a stable
494
+ * `reason` when that deadline passes.
495
+ *
496
+ * A `null` status (the solver has no record of this `rfq_id`) is treated as
497
+ * "not yet", not as an answer — a status route can legitimately 404 for a
498
+ * moment after a quote is issued. The deadline is what bounds that wait.
499
+ *
500
+ * Transport errors are NOT swallowed; a failing `status()` call rejects this
501
+ * function. Callers polling across a long refund window should expect to
502
+ * restart it after a network blip — nothing is lost by doing so, since the
503
+ * refund path this feeds is gated on an absolute timelock that does not
504
+ * expire.
505
+ */
506
+ declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
507
+ pollMs?: number;
508
+ deadline?: number;
509
+ }): Promise<RfqStatus>;
510
+ /** The Ark surface the refund push needs — narrower than a full provider, and
511
+ * satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
512
+ type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
513
+ /** The indexer surface the lockup lookup needs. */
514
+ type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
515
+ /** A still-refundable virtual output sitting at the swap lockup. */
516
+ interface LockupVtxo {
517
+ txid: string;
518
+ vout: number;
519
+ value: number;
520
+ /**
521
+ * The batch this output lived in expired and the operator swept it, so it
522
+ * is no longer a live leaf — it can be RECOVERED, but not spent offchain.
523
+ *
524
+ * It is still the trader's money and it is still visible, which is why
525
+ * {@link findLockupVtxos} returns it. What it is not is refundable by
526
+ * {@link pushRefundWithoutReceiver}: that builds an offchain Ark
527
+ * transaction, and the SDK's own predicates make the two states mutually
528
+ * exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
529
+ * is true (`wallet/vtxo.ts`), and the latter is documented as "must be
530
+ * recovered into a fresh batch rather than spent offchain". Holding the
531
+ * trader's `sender` key does not change that; a sweep removes the leaf from
532
+ * the live tree, not the signature from the trader.
533
+ *
534
+ * `packages/boltz-swap` splits on exactly this fact rather than working
535
+ * around it: `settleRefundWithoutReceiver` sends a live VTXO through an
536
+ * offchain tx and a recoverable one through `joinBatch` — "a swept
537
+ * (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
538
+ * by re-registering it into a batch".
539
+ *
540
+ * So the remedy is recovery (renewing the output into a fresh batch),
541
+ * after which the ordinary CLTV refund works again. This package does not
542
+ * build that round — see {@link pushRefundWithoutReceiver}, which refuses
543
+ * rather than submitting a spend that cannot succeed.
544
+ */
545
+ recoverable: boolean;
546
+ }
547
+ /**
548
+ * Thrown when a refund was asked for over outputs that have been swept.
549
+ *
550
+ * Carries the outpoints so a caller can act — recover exactly those, then
551
+ * retry — instead of reading a server rejection and guessing. `reason` follows
552
+ * the same convention as `awaitOnchainFill`'s `fill_timeout` and
553
+ * `claimOnchainFill`'s `claim_window_closed`.
554
+ *
555
+ * **The remedy already exists; this package does not reimplement it.** The SDK
556
+ * recovers swept outputs by re-registering them into a fresh batch, through
557
+ * `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
558
+ * reaches via its own `joinBatch`. It reads the wallet's registered-contract
559
+ * snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
560
+ * `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
561
+ * covers a swap lockup as soon as that lockup is registered as a contract —
562
+ * which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
563
+ * therefore not only a latency optimization; it is what turns a swept lockup
564
+ * from a dead end into something the ordinary wallet path can recover.
565
+ *
566
+ * Two caveats a caller must hold, neither enforceable from here:
567
+ *
568
+ * - **The wallet must hold the lockup's `sender` key**, because recovery
569
+ * settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
570
+ * these VTXOs with.
571
+ * - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
572
+ * recovery round including this VTXO earlier is rejected. `recoverVtxos`
573
+ * sweeps every recoverable output in ONE settlement and has no CLTV
574
+ * awareness, so recovering early can fail the whole batch rather than just
575
+ * this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
576
+ * recoverable → skipped".
577
+ */
578
+ declare class LockupNeedsRecoveryError extends Error {
579
+ readonly name = "LockupNeedsRecoveryError";
580
+ readonly reason = "needs_recovery";
581
+ /** `txid:vout` for each output that must be recovered first. */
582
+ readonly outpoints: string[];
583
+ /**
584
+ * The contract's `refundLocktime`. Recovering before this matures is the
585
+ * hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
586
+ * into one settlement with no CLTV awareness, so an early attempt can fail
587
+ * the whole batch — including unrelated outputs that were otherwise fine.
588
+ *
589
+ * Exposed as a value, not only inside the message, so a caller can encode
590
+ * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
591
+ * parsing prose. Seconds-based locktimes mature against the chain tip's
592
+ * timestamp rather than wall clock, so treat this as a floor to wait past,
593
+ * not an exact alarm.
594
+ */
595
+ readonly recoverableAfter: bigint;
596
+ constructor(outpoints: string[], recoverableAfter: bigint);
597
+ }
598
+ /**
599
+ * Every output still sitting at the lockup script — spendable AND
600
+ * swept-but-recoverable, each tagged with which it is.
601
+ *
602
+ * All of them, not the first: a trader may fund a lockup in more than one
603
+ * send, and refunding only `vtxos[0]` returns part of the money and strands
604
+ * the rest at a script whose other refund paths are all longer.
605
+ *
606
+ * BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
607
+ * blind at exactly the wrong moment. A lockup whose batch expiry passed is
608
+ * swept into the recoverable set, and this function exists to serve swaps that
609
+ * sat unresolved — which are precisely the ones most likely to have got there.
610
+ * Reading only the spendable set would report `nothing_to_refund` over money
611
+ * that is still sitting at the script, which is worse than an error: it looks
612
+ * like a resolved swap. `packages/boltz-swap` merges the same two queries for
613
+ * the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
614
+ *
615
+ * **Visible is not the same as refundable.** A `recoverable` output cannot be
616
+ * spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
617
+ * "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
618
+ * That function refuses the recoverable ones by name rather than submitting a
619
+ * spend the server must reject.
620
+ *
621
+ * This read — not the RFQ's reported state — is the authority on whether
622
+ * there is anything left at the lockup.
623
+ *
624
+ * **Not replaced by the contract manager's VTXO state, deliberately.** Once a
625
+ * lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
626
+ * these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
627
+ * `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
628
+ * counts: it serves the wallet REPOSITORY, which a degraded sync will happily
629
+ * hand back stale (`getSyncState()` reports `degraded` and returns cached rows
630
+ * rather than failing), and its height-based expiry test needs a chain tip this
631
+ * module does not have. The two queries below ask the indexer itself and need
632
+ * neither. Ask-the-indexer, don't-trust-local-state — the same posture
633
+ * {@link readLockupFate} takes, and for the same reason: this decides money.
634
+ */
635
+ declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
636
+ /**
637
+ * The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
638
+ * raw transactions those vtxos were spent by. Same narrow-seam style as
639
+ * {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
640
+ * {@link RestIndexerProvider}.
641
+ */
642
+ type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
643
+ /**
644
+ * What chain data says became of a swap lockup — the whole answer, with no
645
+ * solver involvement and nothing taken on the solver's word.
646
+ */
647
+ type LockupFate =
648
+ /** At least one output at the lockup is still unspent. Not over. */
649
+ {
650
+ fate: "open";
651
+ }
652
+ /** Spent by a witness carrying a preimage that HASHES to the quote's
653
+ * `payment_hash`. Only the claim leaf can reveal one, and the only
654
+ * legitimate way the solver obtains it is by completing its side. */
655
+ | {
656
+ fate: "claimed";
657
+ preimage: Uint8Array;
658
+ }
659
+ /** Fully spent, and nothing that spent it revealed a matching preimage —
660
+ * so the money went back to the trader. See {@link readLockupFate}. */
661
+ | {
662
+ fate: "returned";
663
+ }
664
+ /** Nothing was learned: no outputs visible, an output spent by nothing the
665
+ * indexer names, a spend it could not produce, or a blob that would not
666
+ * decode. Never an answer. */
667
+ | {
668
+ fate: "unknown";
669
+ };
670
+ /**
671
+ * Decide from chain data alone whether a swap lockup settled, came back, or is
672
+ * still live.
673
+ *
674
+ * **Why this is decidable without asking anyone.** The lockup's claim leaf can
675
+ * only be spent by revealing `P`, so a spend witness carrying a value that
676
+ * hashes to the quote's `payment_hash` is proof the claim leaf was used — and
677
+ * the only legitimate way the counterparty obtains `P` is by completing its
678
+ * side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
679
+ * covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
680
+ * and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
681
+ * `unilateralRefundWithoutReceiver` all require the trader's own signature. So
682
+ * "spent, but not by a hash-verified claim" means the money went back to the
683
+ * trader, and nothing here has to trust a counterparty to say so.
684
+ *
685
+ * **A matching witness SHAPE is not proof.** Only a candidate that hashes to
686
+ * `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
687
+ * else is just bytes, and is treated as a refund. Getting this wrong in the
688
+ * permissive direction would report "settled" for a swap that actually
689
+ * refunded, which is precisely the fact a trader is relying on.
690
+ *
691
+ * **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
692
+ * not visible yet), a `spentBy` the indexer cannot produce a transaction for,
693
+ * or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
694
+ * may legitimately return fewer transactions than were asked for, so the
695
+ * observed set is counted rather than assumed complete. The caller's correct
696
+ * response to `unknown` is the same as to `open`: keep watching, and let the
697
+ * refund timelock — which no outage can move — be what ends the wait.
698
+ *
699
+ * Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
700
+ * cached, the same posture {@link findLockupVtxos} already establishes.
701
+ */
702
+ declare function readLockupFate(indexer: LockupSpendIndexer, input: {
703
+ swapPkScript: Uint8Array;
704
+ /** `sha256(P)`, hex — the quote's `payment_hash`. */
705
+ paymentHash: string;
706
+ }): Promise<LockupFate>;
707
+ /**
708
+ * Build, sign, and push the `refundWithoutReceiver` spend: return every funded
709
+ * output at the lockup to the trader's refund address.
710
+ *
711
+ * The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
712
+ * VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
713
+ * emulator is not involved: it co-signs only the two covenant leaves
714
+ * (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
715
+ * own escape hatch has to go through it and this one does not. So unlike that
716
+ * push, this transaction is submitted SIGNED, and the only counterparty is the
717
+ * Arkade server doing what it does for any collaborative spend.
718
+ *
719
+ * One aggregate output, not one per input — again unlike the solver's covenant
720
+ * refund, which needs index-aligned outputs because its ArkadeScript inspects
721
+ * the output at the current input's index. This leaf carries no covenant, so a
722
+ * single output paying the whole balance is both valid and cheaper.
723
+ *
724
+ * `refundPkScript` defaults to the destination the contract itself commits to
725
+ * (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
726
+ * at quote time), so the ordinary call cannot send the refund somewhere the
727
+ * trader did not intend. It is overridable because this leaf, having no
728
+ * covenant, genuinely does permit any destination.
729
+ *
730
+ * **Consensus, not wall clock, decides when this is spendable.** A seconds
731
+ * locktime matures against median-time-past, which trails real time by roughly
732
+ * an hour, so a push issued the moment `refundLocktime` passes can be rejected
733
+ * until enough blocks land. That is expected, not a failure — see
734
+ * {@link refundIfUnresolved}, which retries.
735
+ *
736
+ * **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
737
+ * a swept output is no longer a live leaf: `canSpendOffchain` and
738
+ * `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
739
+ * input cannot be spent this way whatever key signs it (see
740
+ * {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
741
+ * transaction, a single swept output would take the live ones down with it —
742
+ * so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
743
+ * outpoints, rather than submitted and rejected. Filtering them out silently
744
+ * would be worse still: it would report success over money that never moved.
745
+ */
746
+ declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
747
+ script: InstanceType<typeof VHTLC.ScriptV2>;
748
+ /** The `sender` signer. Build it from the swap's `secrets` with
749
+ * `senderIdentityForRfqSecrets` — on an HD wallet that resolves from
750
+ * the seed, with no stored key bytes anywhere. */
751
+ sender: Identity;
752
+ vtxos: readonly LockupVtxo[];
753
+ /** Defaults to the contract's own committed refund destination. */
754
+ refundPkScript?: Uint8Array;
755
+ }): Promise<{
756
+ arkTxid: string;
757
+ amount: number;
758
+ }>;
759
+ /**
760
+ * How long past `refundLocktime` to keep retrying the push before giving up
761
+ * and surfacing the server's refusal.
762
+ *
763
+ * Two hours because the CLTV matures against median-time-past (BIP-113), which
764
+ * lags wall clock by about an hour, plus room for a slow block. This is the
765
+ * mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
766
+ * of the same margin.
767
+ */
768
+ declare const REFUND_MTP_LAG_SECONDS: number;
769
+ type RefundOutcome =
770
+ /** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
771
+ {
772
+ outcome: "resolved";
773
+ status: RfqStatus;
774
+ }
775
+ /** The trader took it back via `refundWithoutReceiver`. */
776
+ | {
777
+ outcome: "refunded";
778
+ arkTxid: string;
779
+ amount: number;
780
+ status: RfqStatus | null;
781
+ }
782
+ /** The refund window opened but the lockup holds nothing to return. */
783
+ | {
784
+ outcome: "nothing_to_refund";
785
+ status: RfqStatus | null;
786
+ }
787
+ /**
788
+ * The money is still at the lockup, but its batch was swept, so no offchain
789
+ * spend can take it back until it is recovered into a fresh batch. Returned
790
+ * rather than retried: unlike a median-time-past refusal, no amount of
791
+ * waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
792
+ * named outpoints, then call this again.
793
+ */
794
+ | {
795
+ outcome: "needs_recovery";
796
+ outpoints: string[];
797
+ vtxos: LockupVtxo[];
798
+ status: RfqStatus | null;
799
+ };
800
+ /**
801
+ * Ask first, then fall back: watch the swap for the solver to resolve it, and
802
+ * if `refundLocktime` matures without that happening, take the lockup back
803
+ * with `refundWithoutReceiver`.
804
+ *
805
+ * This is the whole trader-side failure story in one call. It polls `status()`
806
+ * — the only "asking" this protocol has (see the module doc) — and returns as
807
+ * soon as the solver reports `settled` or `refunded`. Otherwise, once the
808
+ * quote's `refund_locktime` passes, it looks up what is actually at the lockup
809
+ * and pushes the refund.
810
+ *
811
+ * Two behaviours worth knowing:
812
+ *
813
+ * - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
814
+ * `stuck` are terminal states, but a trader can be holding a funded lockup in
815
+ * every one of them, so they do not end the wait — only `settled`/`refunded`
816
+ * do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
817
+ * timelock.
818
+ * - **The first push after the deadline may legitimately fail.** Median-time-
819
+ * past trails wall clock, so the server can still consider the leaf locked
820
+ * for a while after `refundLocktime` passes in real time. Failures are
821
+ * retried at the poll interval until `attemptDeadline`, after which the last
822
+ * error is rethrown rather than swallowed.
823
+ * - **A swept lockup ends the wait instead of consuming it.** Once the batch
824
+ * is gone the CLTV refund is not "not yet" but "not this way", so it returns
825
+ * `needs_recovery` naming the outpoints rather than retrying until the
826
+ * deadline. Recover them and call again.
827
+ *
828
+ * Safe to call late, and safe to call again: a caller recovering from a crash
829
+ * well past the deadline skips straight to the push, and a lockup that is
830
+ * already empty comes back as `nothing_to_refund` instead of an error.
831
+ */
832
+ declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
833
+ rfqId: string;
834
+ script: InstanceType<typeof VHTLC.ScriptV2>;
835
+ /** @see pushRefundWithoutReceiver */
836
+ sender: Identity;
837
+ /** `refund_locktime` from the quote, unix seconds. */
838
+ refundLocktime: number;
839
+ /** Defaults to the contract's own committed refund destination. */
840
+ refundPkScript?: Uint8Array;
841
+ pollMs?: number;
842
+ /** Stop retrying the push at this unix time, rethrowing the last
843
+ * error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
844
+ attemptDeadline?: number;
845
+ /** Injected for tests; defaults to wall clock, in unix seconds. */
846
+ now?: () => number;
847
+ }): Promise<RefundOutcome>;
848
+
849
+ /** The Ark surface the claim push needs — the same seam the refund push uses. */
850
+ type ClaimArkProvider = RefundArkProvider;
851
+ /**
852
+ * The lockup is funded for less than the swap agreed.
853
+ *
854
+ * The attack this names: the solver funds the correctly derived script with
855
+ * dust. Deriving the script locally — what protects every other corridor —
856
+ * proves nothing here, because the script was never the lie. Claiming anyway
857
+ * publishes `P`, which is what lets the solver settle the payer's Lightning
858
+ * HTLC in full.
859
+ */
860
+ declare class LockupAmountMismatchError extends Error {
861
+ readonly name = "LockupAmountMismatchError";
862
+ readonly reason = "amount_mismatch";
863
+ readonly expectedAmount: number;
864
+ readonly lockedAmount: number;
865
+ constructor(expectedAmount: number, lockedAmount: number);
866
+ }
867
+ /**
868
+ * Build, sign, and push the collaborative claim of a receive-corridor lockup:
869
+ * move every funded output at the lockup to the trader's own destination,
870
+ * revealing `P` in the witness — which is also what settles the trader's side
871
+ * of the swap (the solver reads `P` off the public claim).
872
+ *
873
+ * The preimage is checked against the script's committed hash BEFORE anything
874
+ * is signed: a wrong value can never open the leaf, and catching it here
875
+ * beats learning it from the server's rejection.
876
+ *
877
+ * So is the funded VALUE, against `expectedAmount` — and for the same reason,
878
+ * only more sharply: disclosure happens at SUBMIT, since `P` rides the PSBT to
879
+ * the Ark server. A check that waits for the transaction to land has already
880
+ * leaked the secret.
881
+ *
882
+ * Swept outputs are refused, not attempted, exactly as in
883
+ * {@link pushRefundWithoutReceiver} — one aggregate transaction means a
884
+ * single non-live input would take the live ones down with it. That refusal
885
+ * comes first, which is what leaves the value gate a plain sum over live
886
+ * outputs.
887
+ *
888
+ * The server's countersignature is verified before finalizing, per input and
889
+ * against that input's own leaf. It does not protect `P` — that reached the
890
+ * server at submit — but it turns "reported claimed, nothing landed, the
891
+ * solver refunds hours later" into an immediate failure.
892
+ */
893
+ declare function pushClaim(ark: ClaimArkProvider, input: {
894
+ /** The receive-direction covenant (see `receiveVtxoScript`). */
895
+ script: InstanceType<typeof VHTLC.ScriptV2>;
896
+ /** The trader's `receiver` signer. Build it from the swap's `secrets`
897
+ * with `senderIdentityForRfqSecrets` — on an HD wallet that resolves
898
+ * from the seed, with no stored key bytes anywhere. */
899
+ receiver: Identity;
900
+ /** `P`, 32 bytes — the trader generated it at request time. */
901
+ preimage: Uint8Array;
902
+ vtxos: readonly LockupVtxo[];
903
+ /** Where the claimed sats land — the swap's payout address, decoded. */
904
+ destinationPkScript: Uint8Array;
905
+ /** What the lockup must carry: the quote's `to_amount`, captured at
906
+ * REQUEST time and persisted with the record. Required rather than
907
+ * optional, so the guard cannot be skipped by the records that need
908
+ * it most. */
909
+ expectedAmount: number;
910
+ /** Set when this lockup already carries a claim of ours: `P` is public
911
+ * by then, so the value gate protects nothing and would only strand
912
+ * the remainder. */
913
+ partiallyClaimed?: boolean;
914
+ }): Promise<{
915
+ arkTxid: string;
916
+ amount: number;
917
+ }>;
918
+ /**
919
+ * Wait for the solver-funded lockup to appear at the covenant script.
920
+ *
921
+ * Same conventions as `awaitRfqResolution`: a `pollMs` interval, an optional
922
+ * unix-seconds `deadline`, and a thrown error carrying a stable `reason` when
923
+ * the deadline passes.
924
+ */
925
+ declare function awaitLockupFunding(indexer: RefundIndexer, swapPkScript: Uint8Array, options?: {
926
+ pollMs?: number;
927
+ deadline?: number;
928
+ }): Promise<readonly LockupVtxo[]>;
929
+ /**
930
+ * The one-call composition: wait for the solver's funding, then push the
931
+ * claim. The polling deadline gates the WAIT only — once the lockup is
932
+ * funded, the claim itself is gated by nothing but the solver's own refund
933
+ * deadline (`refund_locktime` from the quote), which is the number the
934
+ * caller's deadline should be measured against.
935
+ *
936
+ * The wait returns on the first output seen, so a funding the indexer
937
+ * surfaces piecemeal reaches {@link pushClaim}'s value gate short and throws
938
+ * {@link LockupAmountMismatchError}. Nothing was signed, so retrying once the
939
+ * rest lands is safe — and that is also the answer to a genuinely underfunded
940
+ * lockup, which never gets past the gate at all.
941
+ */
942
+ declare function claimReceiveLockup(indexer: RefundIndexer, ark: ClaimArkProvider, input: Parameters<typeof pushClaim>[1] & {
943
+ /** The covenant's scriptPubKey, from the request flow's `swapPkScript`. */
944
+ swapPkScript: Uint8Array;
945
+ pollMs?: number;
946
+ deadline?: number;
947
+ }): Promise<{
948
+ arkTxid: string;
949
+ amount: number;
950
+ }>;
951
+
952
+ /** The contract type a swap lockup registers under. `@arkade-os/sdk`'s handler
953
+ * for `VHTLC.ScriptV2` — the covenant script this corridor builds. */
954
+ declare const SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
955
+ declare const SWAP_LOCKUP_CONTRACT_LABEL = "Arkade RFQ swap lockup";
956
+ declare const SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
957
+ /** The write seam registration needs, narrowed to the one method — the same
958
+ * injection style as `SwapContractRegistry`, and satisfied by a real
959
+ * `ContractManager` (`await wallet.getContractManager()`). */
960
+ type LockupContractWriter = Pick<IContractManager, "createContract">;
961
+ /**
962
+ * The lockup could not be written locally.
963
+ *
964
+ * Deliberately NOT a {@link SwapRefusal} or an {@link AddressMismatch}: those
965
+ * say "the quote is bad, never fund it", while this says "the quote is fine and
966
+ * your own store failed".
967
+ *
968
+ * The throw is the safe point: nothing is funded, and on the receive legs the
969
+ * invoice never left the function, so the abandoned quote is inert and simply
970
+ * retrying the request is the recovery. `script` travels beside `address` so a
971
+ * caller that still holds the swap — `RfqSwapManager`'s `ensureRegistered`, or
972
+ * one resuming from its own record — can retry `registerLockupContract` alone
973
+ * instead of re-quoting. It is NOT enough to resume a request that threw here:
974
+ * that caller never received the invoice or `secrets`.
975
+ */
976
+ declare class LockupRegistrationFailed extends Error {
977
+ /** The lockup address that was never registered — never fund it: nothing
978
+ * is watching it. */
979
+ readonly address: string;
980
+ /** The covenant the row would have been written from — the other half of
981
+ * `registerLockupContract`, so the write is retryable without a quote. */
982
+ readonly script: InstanceType<typeof VHTLC.ScriptV2>;
983
+ constructor(script: InstanceType<typeof VHTLC.ScriptV2>, address: string, cause: unknown);
984
+ }
985
+ /**
986
+ * Register a lockup covenant so its VTXOs are watched, annotatable and — via
987
+ * `vhtlc-v2`'s own handler, which is never generically spendable — kept out of
988
+ * ordinary coin selection.
989
+ *
990
+ * The row carries script-level facts only. Per-swap identity and key material
991
+ * stay in the swap record: rows are keyed by script and first-writer-wins, so
992
+ * anything per-swap written here is stale from the second swap onward.
993
+ *
994
+ * Takes the derived script rather than a script hex plus params, so the row
995
+ * cannot describe a script other than the one it is keyed by.
996
+ *
997
+ * Throws {@link LockupRegistrationFailed}, so a caller can tell a local
998
+ * storage problem from a reason to walk away from the quote.
999
+ */
1000
+ declare function registerLockupContract(contracts: LockupContractWriter, script: InstanceType<typeof VHTLC.ScriptV2>, address: string): Promise<void>;
1001
+
1002
+ /**
1003
+ * Where a monitored swap stands.
1004
+ *
1005
+ * `claimable` and `claimed` are the states of a swap the TRADER has something
1006
+ * to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
1007
+ * a receive. Only `lightning_send` has neither — there the solver claims the
1008
+ * lockup, and the trader's only move is the refund.
1009
+ */
1010
+ type RfqSwapState =
1011
+ /** Live; nothing actionable yet. On a receive leg this covers the whole
1012
+ * stretch before the solver funds anything. */
1013
+ "pending"
1014
+ /** There is something for the trader to take, and the window to take it is
1015
+ * open: the confirmed L1 fill on an onchain send, or a lockup funded for at
1016
+ * least `expectedAmount` on a receive. */
1017
+ | "claimable"
1018
+ /**
1019
+ * The trader's claim has been made — its L1 broadcast on an onchain send,
1020
+ * its Arkade submission on a receive.
1021
+ *
1022
+ * **On a receive this is a local belief and not a chain fact**, which is
1023
+ * why it is not terminal: `settled` is the chain's answer, and `refunded`
1024
+ * is still reachable from here if the claim never lands and the solver
1025
+ * takes the lockup back.
1026
+ */
1027
+ | "claimed"
1028
+ /**
1029
+ * This wallet will not act, and only the counterparty can change that.
1030
+ *
1031
+ * On a send leg: the Arkade refund cannot be pushed from here — no secrets
1032
+ * on the record, a descriptor from another seed, or nothing wired to act —
1033
+ * so the lockup comes back only if the counterparty claims it or the wallet
1034
+ * that can sign it is restored. On a receive leg: the trader holds no
1035
+ * refund at all, so this is a lockup that cannot be claimed — funded for
1036
+ * less than the swap agreed (publishing `P` for it is the whole attack
1037
+ * `LockupAmountMismatchError` exists to refuse), or one whose claim window
1038
+ * shut unclaimed. {@link RfqSwapCommon.blockedReason} says which.
1039
+ *
1040
+ * **Not terminal, and not a dead end.** The money is still at the lockup,
1041
+ * so the counterparty's move is still observable and still ends the swap;
1042
+ * and the refusal is re-checked every pass, so restoring the right wallet,
1043
+ * wiring the callbacks, or the solver topping the lockup up returns the
1044
+ * swap to `pending` and resumes the normal drive. For an onchain-send swap
1045
+ * it says nothing about the L1 half, which keeps being driven and claimed.
1046
+ */
1047
+ | "needs_counterparty"
1048
+ /**
1049
+ * Terminal: the lockup was spent by a hash-verified claim. Read off chain,
1050
+ * never reported.
1051
+ *
1052
+ * On a send leg that claim is the counterparty's, and it is proof the
1053
+ * counterparty completed its side. On a receive leg it is the TRADER's own
1054
+ * — matched by the hash and not by our txid, so a claim that lands without
1055
+ * us still counts (see {@link RfqSwapManager}).
1056
+ */
1057
+ | "settled"
1058
+ /**
1059
+ * Terminal: the lockup was spent by something other than a claim.
1060
+ *
1061
+ * On a send leg that is the money coming back, by the solver's hand or the
1062
+ * trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
1063
+ * money, every non-claim leaf is the solver's, and a swap that ends here
1064
+ * ended with the trader's incoming payment never arriving. It is also where
1065
+ * a receive swap ends when its window closes with nothing left to observe —
1066
+ * see {@link RfqSwapManager}.
1067
+ */
1068
+ | "refunded"
1069
+ /** Terminal: an action failed and its window closed. */
1070
+ | "failed";
1071
+ /** The states after which the manager stops monitoring a swap. Deliberately
1072
+ * without `needs_counterparty`: retiring on it would unwatch a funded lockup
1073
+ * whose claim is still the thing that ends the swap. */
1074
+ declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
1075
+ declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
1076
+ /**
1077
+ * What the manager needs to register a swap's lockup with the wallet, so the
1078
+ * indexer pushes its funding and its spend instead of being asked every few
1079
+ * seconds.
1080
+ *
1081
+ * Both fields are things the caller already holds. `script` is the very object
1082
+ * `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
1083
+ * it in hand; `address` is the request entrypoint's own return value. The
1084
+ * address is taken rather than re-derived on purpose — the row's address must be
1085
+ * the one that was actually funded, and a local re-derivation would silently use
1086
+ * the SDK's default network, which is the exact bug `registerOfferContract`
1087
+ * guards against.
1088
+ */
1089
+ interface RfqSwapLockup {
1090
+ /** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
1091
+ script: InstanceType<typeof VHTLC.ScriptV2>;
1092
+ /** The Arkade address that was funded. */
1093
+ address: string;
1094
+ }
1095
+ interface RfqSwapCommon {
1096
+ /** The negotiation id — this record's identity. */
1097
+ rfqId: string;
1098
+ state: RfqSwapState;
1099
+ /** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
1100
+ * request entrypoints. This is what the manager watches to decide the swap:
1101
+ * it is the only handle on the covenant whose spend witness says whether
1102
+ * the swap settled or came back. */
1103
+ lockupPkScript: Uint8Array;
1104
+ /** The covenant behind {@link lockupPkScript}, when the caller wants the
1105
+ * lockup registered with a contract manager. Optional: without it the
1106
+ * manager still watches the swap on its timer, it just cannot subscribe.
1107
+ * See {@link RfqSwapManagerDeps.contracts}. */
1108
+ lockup?: RfqSwapLockup;
1109
+ /** `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
1110
+ * be spent by revealing a value that hashes to this, which is what makes a
1111
+ * settlement provable rather than reported. For an onchain send this is
1112
+ * the SAME hash the L1 `htlc` carries: one `P` unlocks both legs. */
1113
+ paymentHash: string;
1114
+ /**
1115
+ * `refund_locktime` from the quote, unix seconds.
1116
+ *
1117
+ * Whose deadline it is inverts with the direction, and so does what to do
1118
+ * about it. On a send leg it is the TRADER's: the lockup is the trader's
1119
+ * money and this gates the refund that takes it back, so it is a moment to
1120
+ * act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
1121
+ * leaf at all, and this is the moment to have claimed BEFORE.
1122
+ */
1123
+ refundLocktime: number;
1124
+ createdAt: number;
1125
+ updatedAt: number;
1126
+ /** Set once the trader's own `refundWithoutReceiver` push landed. */
1127
+ refundArkTxid?: string;
1128
+ /** Why `state` is `failed`. */
1129
+ failure?: string;
1130
+ /** Why `state` is `needs_counterparty`. Distinct from {@link failure},
1131
+ * which means an action was attempted and did not work. */
1132
+ blockedReason?: string;
1133
+ }
1134
+ /** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
1135
+ * claims the lockup with the preimage it learns by paying the invoice — which
1136
+ * is exactly why that spend's witness is proof the payment landed. */
1137
+ interface LightningSendSwap extends RfqSwapCommon {
1138
+ kind: "lightning_send";
1139
+ }
1140
+ /** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
1141
+ interface OnchainSendSwap extends RfqSwapCommon {
1142
+ kind: "onchain_send";
1143
+ /** The locally derived HTLC from `requestOnchainSend` — the manager reads
1144
+ * `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
1145
+ htlc: OnchainHtlc;
1146
+ /** `profile.min_confirmations` from the quote. */
1147
+ minConfirmations: number;
1148
+ /** The fill's outpoint, learned on first sighting. Without it a SPENT
1149
+ * HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
1150
+ funding?: {
1151
+ txid: string;
1152
+ vout: number;
1153
+ };
1154
+ /** Our L1 claim's txid. */
1155
+ claimTxid?: string;
1156
+ }
1157
+ /**
1158
+ * `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
1159
+ * and the TRADER claims it, and that claim is what publishes `P` and lets the
1160
+ * solver settle the payer's held Lightning HTLC.
1161
+ *
1162
+ * Two consequences shape how this record is driven, both of them absent from
1163
+ * the send legs:
1164
+ *
1165
+ * - **There is no trader-side refund.** Every non-claim leaf of this covenant
1166
+ * is the solver's, so the manager never calls
1167
+ * {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
1168
+ * is not claimed is simply lost — the solver reclaims at
1169
+ * {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
1170
+ * held HTLC lapses.
1171
+ * - **The claim is the whole swap, and it is on a deadline.** The trader must
1172
+ * be online for it: covclaimd cannot claim this covenant today, so the claim
1173
+ * packet's offline path does not run.
1174
+ */
1175
+ interface LightningReceiveSwap extends RfqSwapCommon {
1176
+ kind: "lightning_receive";
1177
+ /**
1178
+ * What the lockup must carry — the quote's `to_amount`, captured at REQUEST
1179
+ * time and persisted with the record.
1180
+ *
1181
+ * **Not re-derivable, and not optional.** Captured at claim time it would
1182
+ * be whatever the solver funded, which is the dust-funding attack rather
1183
+ * than a check on it. A record that reaches the manager without a finite
1184
+ * value here is reported `needs_counterparty` and never claimed: a
1185
+ * comparison against `undefined` or `NaN` is false, so an unusable
1186
+ * comparand does not fail the value gate, it deletes it.
1187
+ */
1188
+ expectedAmount: number;
1189
+ /** Our Arkade claim's txid, once submitted. Set from the callback's return
1190
+ * and never from a chain read — the chain's answer is `settled`. */
1191
+ claimArkTxid?: string;
1192
+ }
1193
+ /**
1194
+ * A monitored swap.
1195
+ *
1196
+ * This is a live record, not a serialization format: `lockupPkScript` and
1197
+ * `htlc` hold derived `Uint8Array`s, and
1198
+ * {@link RfqSwapManagerCallbacks.saveSwap} is where a caller projects it into
1199
+ * whatever it stores. Rebuild it on restart the way it was made —
1200
+ * `lightningSendVtxoScript` / `receiveVtxoScript` / `onchainHtlcScript` over
1201
+ * the quote's binding fields — and hand the result to
1202
+ * {@link RfqSwapManager.start}.
1203
+ *
1204
+ * **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
1205
+ * half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
1206
+ * it also has an L1 half the trader funds and must take back itself
1207
+ * (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
1208
+ * deadline, a second observation seam and a second action callback. Adding the
1209
+ * lockup half alone would produce a manager that silently lets that L1 refund
1210
+ * window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
1211
+ * by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
1212
+ * refund is driven too, that corridor is better served by the request and claim
1213
+ * functions directly than by a monitor that covers half of it.
1214
+ */
1215
+ type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
1216
+ /** What the manager should do next about an onchain-send swap's L1 half. */
1217
+ type OnchainSendAction =
1218
+ /** Not funded yet, or not confirmed deep enough. */
1219
+ "wait"
1220
+ /** Funded, confirmed, and far enough from the refund leaf to claim safely. */
1221
+ | "claim"
1222
+ /** The claim is off the table for good; the money comes back through the
1223
+ * Arkade lockup instead. */
1224
+ | "claim_window_closed"
1225
+ /** Our claim already landed (only the trader holds P). */
1226
+ | "claimed"
1227
+ /** The solver took its L1 refund — the fill is gone. */
1228
+ | "swept";
1229
+ /**
1230
+ * The decision a "poll status, refund on timeout" loop gets wrong.
1231
+ *
1232
+ * {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
1233
+ * claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
1234
+ * "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
1235
+ * SOLVER's refund, and reaching it means the trader's claim was missed. So the
1236
+ * L1 claim has to be driven before it, and from it the only remaining move is
1237
+ * the Arkade-side refund.
1238
+ *
1239
+ * There is a second, quieter trap between those two functions:
1240
+ * `classifyOnchainHtlc` reports `claimable` right up until median-time-past
1241
+ * reaches `refundLocktime`, while `claimOnchainFill` refuses from
1242
+ * {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
1243
+ * publishes P, and doing that into the counterparty's live refund window risks
1244
+ * losing the race AND giving away the preimage. Driving straight off the phase
1245
+ * would therefore spend that whole margin throwing `claim_window_closed` at
1246
+ * every poll and never fall back. This function applies the margin, so
1247
+ * "claimable" here means claimable by `claimOnchainFill` too.
1248
+ */
1249
+ declare function nextOnchainAction(input: {
1250
+ phase: OnchainHtlcPhase;
1251
+ /** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
1252
+ htlcLocktime: number;
1253
+ /** Unix seconds. */
1254
+ now: number;
1255
+ }): OnchainSendAction;
1256
+ /** What the trader's own `refundWithoutReceiver` push returned, or `null` when
1257
+ * the lockup held nothing to return. */
1258
+ type ArkadeRefundResult = {
1259
+ arkTxid: string;
1260
+ amount: number;
1261
+ } | null;
1262
+ /**
1263
+ * The money-moving half, injected. The manager decides when; these do it.
1264
+ *
1265
+ * Neither action gets a retry loop of its own here — see the module doc. Do
1266
+ * NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
1267
+ * single-swap version of this whole class and brings its own status polling
1268
+ * and its own MTP retry loop, which would nest inside the manager's. Wire it
1269
+ * to `findLockupVtxos` + `pushRefundWithoutReceiver`, which is the atomic push
1270
+ * `refundIfUnresolved` itself calls.
1271
+ *
1272
+ * Resolve the sender key through `senderIdentityForSwapRecord`: it is
1273
+ * what turns "this wallet cannot sign this swap" into
1274
+ * {@link RefundNotLocallyPossibleError}, which the manager reports as
1275
+ * `needs_counterparty` instead of retrying for the whole refund window.
1276
+ */
1277
+ interface RfqSwapManagerCallbacks {
1278
+ /** Build and broadcast the L1 claim. See `claimOnchainFill`. */
1279
+ claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
1280
+ txid: string;
1281
+ }>;
1282
+ /**
1283
+ * Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
1284
+ * to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
1285
+ * already been called and `claimReceiveLockup`'s wait would only sit on a
1286
+ * lockup the manager has just seen.
1287
+ *
1288
+ * **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
1289
+ * manager checks the funded value before calling this, but that check
1290
+ * decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
1291
+ * it is the one that runs with nothing between it and the signature. Two
1292
+ * checks, one of which is load-bearing — do not drop the inner one because
1293
+ * the outer one exists.
1294
+ *
1295
+ * Required rather than optional, like {@link claimOnchain}: a receive swap
1296
+ * monitored with nothing wired to claim it is a swap that quietly expires,
1297
+ * and a compile error is the right way to learn that.
1298
+ */
1299
+ claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
1300
+ /** A claim of ours is already out, so `P` is public and the value
1301
+ * gate has nothing left to protect — pass this to `pushClaim` so a
1302
+ * funding that arrived piecemeal can still be swept. */
1303
+ partiallyClaimed: boolean;
1304
+ }) => Promise<{
1305
+ arkTxid: string;
1306
+ amount: number;
1307
+ }>;
1308
+ /** Push `refundWithoutReceiver` for every output at the lockup. See
1309
+ * `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
1310
+ * called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
1311
+ * solver's. */
1312
+ refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
1313
+ /**
1314
+ * Whether a local refund is possible at all — the record's secrets, against
1315
+ * this wallet. Called every pass, including *before* the refund window
1316
+ * opens, so a swap nobody can refund says so while the solver can still
1317
+ * act, instead of at the deadline; and so restoring the right wallet lifts
1318
+ * the state again. Never called for a receive swap: there is no local
1319
+ * refund there to probe for.
1320
+ *
1321
+ * Optional: omit to answer "yes" and learn at push time, from
1322
+ * {@link RefundNotLocallyPossibleError}. Local by contract — no network
1323
+ * call belongs here.
1324
+ */
1325
+ canRefundArkade?: (swap: RfqSwap) => Promise<{
1326
+ ok: true;
1327
+ } | {
1328
+ ok: false;
1329
+ reason: string;
1330
+ }>;
1331
+ /** Persist the record. Called after any pass that changed it. */
1332
+ saveSwap: (swap: RfqSwap) => Promise<void>;
1333
+ }
1334
+ /** The actions the manager executes on a caller's behalf. */
1335
+ type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
1336
+ interface RfqSwapManagerEvents {
1337
+ /** Every state change, including ones that read as going backwards.
1338
+ * `claimed -> claimable` is legal and expected on a receive swap the solver
1339
+ * funds piecemeal: a lockup topped up after a claim is a new claimable
1340
+ * event, and the label says so before the sweep goes out. Treat these
1341
+ * states as a description of what to do next, not as a progress bar. */
1342
+ onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
1343
+ /** Fired once, when a swap leaves monitoring `settled` or `refunded`.
1344
+ * A swap that ends `failed` reports through `onSwapFailed` instead — the
1345
+ * two are mutually exclusive. */
1346
+ onSwapCompleted?: (swap: RfqSwap) => void;
1347
+ /** Fired for any action that threw — including ones the manager will retry
1348
+ * on the next pass — and once more when the swap finally ends `failed`. */
1349
+ onSwapFailed?: (swap: RfqSwap, error: Error) => void;
1350
+ onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
1351
+ }
1352
+ type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
1353
+ type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
1354
+ type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
1355
+ type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
1356
+ interface RfqSwapManagerConfig {
1357
+ /** Drive claims and refunds automatically (default: true). With this off
1358
+ * the manager still watches and reports, so a caller can act by hand off
1359
+ * `claimable`. */
1360
+ enableAutoActions?: boolean;
1361
+ /** How often to run a pass, ms. Default 5000 — the same interval
1362
+ * `awaitOnchainFill` and `refundIfUnresolved` poll at. */
1363
+ pollIntervalMs?: number;
1364
+ /** Injected for tests; defaults to wall clock, in unix seconds — the same
1365
+ * convention `refundIfUnresolved` uses. */
1366
+ now?: () => number;
1367
+ events?: RfqSwapManagerEvents;
1368
+ }
1369
+ /** The contract-manager surface this needs, narrowed for injection — the same
1370
+ * seam style as {@link LockupSpendIndexer} and `refund.ts`'s
1371
+ * {@link RefundArkProvider}, and satisfied structurally by a real
1372
+ * `ContractManager` (`await wallet.getContractManager()`). */
1373
+ type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
1374
+ /** The observation seams. None is owned by the manager, and none holds keys —
1375
+ * same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
1376
+ * `RfqTransport` here on purpose: nothing this manager decides depends on the
1377
+ * solver answering (see the module doc). */
1378
+ interface RfqSwapManagerDeps {
1379
+ /** Arkade access. Required: this is how a swap's resolution is determined,
1380
+ * for both legs. */
1381
+ indexer: LockupSpendIndexer;
1382
+ /** L1 access. Required to monitor onchain-send swaps; a lightning-only
1383
+ * caller can leave it out. */
1384
+ chain?: ChainSource;
1385
+ /**
1386
+ * The wallet's contract manager, when there is one. Optional in the same
1387
+ * way {@link chain} is: a caller with no wallet, or one that only wants the
1388
+ * timer, still gets a fully working manager — the subscription is a
1389
+ * LATENCY optimization and nothing depends on it.
1390
+ *
1391
+ * Supplying it buys two things. The lockup gets REGISTERED, which is what
1392
+ * puts it in the wallet's own contract set at all — a prerequisite for
1393
+ * anything that has to act on the lockup before its batch expires, since an
1394
+ * expired lockup is swept and loses every cooperative path. And the indexer
1395
+ * PUSHES its funding and its spend, so a settlement is noticed when it
1396
+ * happens rather than up to `pollIntervalMs` later.
1397
+ *
1398
+ * Prefer `await wallet.getContractManager()` over constructing one, the way
1399
+ * `createOffer` does.
1400
+ */
1401
+ contracts?: SwapContractRegistry;
1402
+ }
1403
+ /**
1404
+ * Watches a set of live RFQ swaps and drives each to its end.
1405
+ *
1406
+ * One pass per swap, in this order, every
1407
+ * {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
1408
+ * contract event names that swap's lockup, which changes only WHEN a pass runs,
1409
+ * never what it concludes (see {@link subscribe}):
1410
+ *
1411
+ * 0. **Register the lockup**, if a contract manager was supplied and it is not
1412
+ * registered yet. Best-effort; never blocks the steps below.
1413
+ * 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
1414
+ * spend whose witness HASHES to the quote's `payment_hash` ends the swap
1415
+ * `settled`; a lockup fully spent by anything else ends it `refunded`.
1416
+ * Anything the indexer could not answer is `unknown`, which is NOT an
1417
+ * answer: the pass carries on to the steps below, whose deadlines an indexer
1418
+ * outage has no bearing on.
1419
+ * 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
1420
+ * {@link nextOnchainAction}. On a receive it is the lockup itself, and it
1421
+ * ends the pass: that leg has no step 3.
1422
+ * 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
1423
+ * and step 1 has not ended the swap. This runs for onchain-send too,
1424
+ * including after a successful claim: the trader's lockup is still funded and
1425
+ * still theirs to recover if the solver never comes for it. When no local
1426
+ * refund is possible at all — no secrets, another wallet's descriptor,
1427
+ * nothing wired — the swap reports `needs_counterparty` instead of retrying
1428
+ * a push that cannot work.
1429
+ *
1430
+ * **What step 1 proves depends on the direction.** On a send leg every non-claim
1431
+ * leaf pays the trader's own committed address or needs the trader's own
1432
+ * signature, so "spent, but not by a hash-verified claim" means the money came
1433
+ * back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
1434
+ * trader's, so the same two readings mean the opposite things — `settled` is the
1435
+ * trader's own claim landing, `refunded` is the solver taking back a lockup the
1436
+ * trader failed to claim. The read is identical; only the state docs differ.
1437
+ *
1438
+ * Two things about the receive arm that are easy to get wrong, and are asserted
1439
+ * in the tests rather than left to be inferred:
1440
+ *
1441
+ * - **A claim is matched by its preimage, never by our txid.** The covenant's
1442
+ * `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
1443
+ * claim that lands without us — covclaimd, the day it works — still pays the
1444
+ * trader and is still `settled`. Matching on the txid we submitted would turn
1445
+ * that success into an anomaly.
1446
+ * - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
1447
+ * state `claimed`.** The two words live one layer apart: the fate is the
1448
+ * chain's, the state is ours, and the state `claimed` means only that we
1449
+ * submitted something.
1450
+ */
1451
+ declare class RfqSwapManager {
1452
+ private readonly deps;
1453
+ private readonly config;
1454
+ private callbacks;
1455
+ private readonly swapUpdateListeners;
1456
+ private readonly swapCompletedListeners;
1457
+ private readonly swapFailedListeners;
1458
+ private readonly actionExecutedListeners;
1459
+ private readonly monitored;
1460
+ /** Monitored swaps by lockup script hex, so a contract event — which names
1461
+ * a script and nothing else — can find the swap it belongs to. */
1462
+ private readonly byLockupScript;
1463
+ /**
1464
+ * Swaps whose lockup registration has been SETTLED one way or another,
1465
+ * mapped to whether a contract row actually resulted. Membership is what
1466
+ * stops a per-pass retry from becoming a per-pass round trip; the value is
1467
+ * what keeps a swap that could never be registered from later trying to
1468
+ * retire a row that does not exist, which would report a spurious failure
1469
+ * on a swap that in fact succeeded.
1470
+ */
1471
+ private readonly registered;
1472
+ /**
1473
+ * Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
1474
+ * in this process. Membership stops the push from being re-issued every
1475
+ * pass — it cannot start working on its own, and re-issuing it is the
1476
+ * grind `needs_counterparty` exists to remove. Only
1477
+ * {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
1478
+ * with no probe learns again on the next start, when the wallet that can
1479
+ * sign may well have been restored.
1480
+ */
1481
+ private readonly refundRefused;
1482
+ /**
1483
+ * The last error a receive swap's claim callback threw, by rfqId.
1484
+ *
1485
+ * Kept only to tell two terminal outcomes apart once the claim window
1486
+ * shuts: a swap whose claim was attempted and kept failing ends `failed`
1487
+ * with that reason, while one that simply never became claimable ends
1488
+ * `refunded`. Without it a broken claim callback would resolve a caller's
1489
+ * {@link waitForSwapCompletion} as an ordinary unwind.
1490
+ *
1491
+ * Process-local, like {@link refundRefused}: after a restart the same swap
1492
+ * ends `refunded` instead, which costs the caller a reason and nothing else
1493
+ * — every throw was already reported through `onSwapFailed` as it happened.
1494
+ */
1495
+ private readonly lastClaimError;
1496
+ /**
1497
+ * The lockup outpoints a receive swap's claim callback has already been
1498
+ * handed, by rfqId.
1499
+ *
1500
+ * What this exists to prevent: a claim SUCCEEDS, and for the next few
1501
+ * passes the indexer still lists those outputs as unspent. Without a
1502
+ * record of what was already claimed, every one of those passes would
1503
+ * re-submit the same spend, fail against the server, and report a swap
1504
+ * that in fact worked as failing. With one, a re-claim happens only when
1505
+ * an outpoint appears that was never claimed — a lockup funded piecemeal,
1506
+ * which is legitimate and which `partiallyClaimed` exists for.
1507
+ *
1508
+ * Process-local: after a restart a swap with a live claim tries once more.
1509
+ * That is the recovery case rather than the spam one — a claim that never
1510
+ * landed leaves its outputs unspent, and one that did leaves a single
1511
+ * rejection.
1512
+ */
1513
+ private readonly claimedOutpoints;
1514
+ /** Live `onContractEvent` subscription, held so `stop()` can drop it. */
1515
+ private unsubscribeContracts;
1516
+ /** Terminal records, kept so a late {@link waitForSwapCompletion} still
1517
+ * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
1518
+ private readonly finished;
1519
+ private readonly waiters;
1520
+ /** Records changed during the current pass, flushed through `saveSwap`. */
1521
+ private readonly dirty;
1522
+ /** Race guard: one action at a time per swap. */
1523
+ private readonly inProgress;
1524
+ private timer;
1525
+ private running;
1526
+ constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
1527
+ /** Wire the money-moving half. Without it the manager only watches. */
1528
+ setCallbacks(callbacks: RfqSwapManagerCallbacks): void;
1529
+ onSwapUpdate(listener: SwapUpdateListener): () => void;
1530
+ onSwapCompleted(listener: SwapCompletedListener): () => void;
1531
+ onSwapFailed(listener: SwapFailedListener): () => void;
1532
+ onActionExecuted(listener: ActionExecutedListener): () => void;
1533
+ /**
1534
+ * Load records and begin monitoring. Runs one pass immediately — a caller
1535
+ * resuming after a restart may be well past a deadline already — then
1536
+ * every `pollIntervalMs`. Records that are already terminal are kept only
1537
+ * so {@link waitForSwapCompletion} can answer for them.
1538
+ *
1539
+ * Calling it again while running loads the records and returns rather than
1540
+ * re-arming — dropping them silently would strand a funded swap on a
1541
+ * caller's harmless double-start.
1542
+ */
1543
+ start(swaps?: readonly RfqSwap[]): Promise<void>;
1544
+ /**
1545
+ * Stop monitoring and clear the timer. In-flight actions are not
1546
+ * cancellable and run to completion; outstanding
1547
+ * {@link waitForSwapCompletion} promises are left pending, since
1548
+ * stop/start is a pause rather than a cancellation.
1549
+ *
1550
+ * The contract subscription is dropped too — an open stream with nothing
1551
+ * reacting to it is a leak, and {@link start} puts it back. What is NOT
1552
+ * undone is the contract registration: those rows are the wallet's, they
1553
+ * outlive this manager's lifecycle, and dropping them would unwatch a
1554
+ * lockup that is still funded.
1555
+ */
1556
+ stop(): Promise<void>;
1557
+ /** Begin monitoring a swap. Polled immediately when the manager is running,
1558
+ * so a just-funded swap does not wait out a whole interval. */
1559
+ addSwap(swap: RfqSwap): Promise<void>;
1560
+ /** Forget a swap entirely, monitored or finished.
1561
+ *
1562
+ * Its contract row is left alone: registration is a wallet-level fact about
1563
+ * a script that may still hold money, and this call says only that THIS
1564
+ * manager stops driving the swap. Retiring the row is reserved for a swap
1565
+ * that reached a terminal state, where the lockup is provably done. */
1566
+ removeSwap(rfqId: string): Promise<void>;
1567
+ /** Every swap still being monitored. */
1568
+ getPendingSwaps(): Promise<RfqSwap[]>;
1569
+ hasSwap(rfqId: string): Promise<boolean>;
1570
+ /** True while an action for this swap holds the per-swap lock. */
1571
+ isProcessing(rfqId: string): Promise<boolean>;
1572
+ getStats(): Promise<{
1573
+ isRunning: boolean;
1574
+ monitoredSwaps: number;
1575
+ finishedSwaps: number;
1576
+ inProgress: number;
1577
+ pollIntervalMs: number;
1578
+ }>;
1579
+ /**
1580
+ * Run one monitoring pass over every swap now.
1581
+ *
1582
+ * {@link start} calls this on an interval, but it is public on purpose: a
1583
+ * caller that sleeps its process (a mobile app resuming, a service worker
1584
+ * waking) wants a pass on that event rather than at the next tick. Passes
1585
+ * do not overlap per swap — the in-progress lock makes a concurrent call a
1586
+ * no-op for any swap already being worked on.
1587
+ */
1588
+ poll(): Promise<void>;
1589
+ /**
1590
+ * Resolve once this swap's PAYOUT is decided — which for onchain-send is
1591
+ * the L1 claim, not the end of the record's life: once `claimTxid` is set
1592
+ * the trader has the coins it swapped for, and what remains is the manager
1593
+ * watching the Arkade lockup close. That holds however the record is
1594
+ * labelled afterwards, `needs_counterparty` included. Lightning-send has no
1595
+ * such split and resolves at `settled`/`refunded`, and so does lightning
1596
+ * receive — see {@link isPayoutDecided} for why its own claim txid does not
1597
+ * decide it.
1598
+ *
1599
+ * Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
1600
+ * an outcome the caller asked this manager to drive, not an exception. On a
1601
+ * receive leg it is the swap being lost, which is still an answer and not
1602
+ * an error — read `state`, do not infer success from resolution.
1603
+ */
1604
+ waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
1605
+ private track;
1606
+ /** Drops the swap from BOTH indexes. The event index is the one that stops
1607
+ * a late event finding a swap that is gone; `pollSwap`'s own
1608
+ * `monitored` check would also catch it, and deliberately still does —
1609
+ * either alone is sufficient, which is what keeps a future change to one of
1610
+ * them from silently re-driving a cancelled swap. */
1611
+ private untrack;
1612
+ /**
1613
+ * Turn the indexer's push into an extra reason to run a pass — and nothing
1614
+ * more.
1615
+ *
1616
+ * **This is deliberately not a source of truth.** An event names a script;
1617
+ * the reaction is to run the ordinary pass for the swap at that script, and
1618
+ * that pass re-reads the lockup through {@link readLockupFate} exactly as
1619
+ * the timer's pass does. So an event that is missed, duplicated, reordered
1620
+ * or outright FORGED can only cost or save latency — it can never change
1621
+ * what this manager believes about a swap, and it can never on its own
1622
+ * cause a claim or a refund. That property is what makes it safe to bolt a
1623
+ * best-effort stream onto a money path, and it must survive any future
1624
+ * change here: the moment an event is BELIEVED rather than merely acted on,
1625
+ * a relay outage becomes a correctness problem instead of a latency one.
1626
+ *
1627
+ * The timer stays armed regardless, and is the failsafe. Every deadline
1628
+ * that moves money — `refundLocktime`, the L1 claim window — is an absolute
1629
+ * timelock that passes whether or not a single event ever arrives.
1630
+ */
1631
+ private subscribe;
1632
+ /**
1633
+ * Register this swap's lockup with the wallet's contract manager, once.
1634
+ *
1635
+ * The backstop, not the primary site: `requestLightningSend` /
1636
+ * `requestOnchainSend` register before the caller can fund, so this covers
1637
+ * swaps whose records predate that — and costs nothing when it does not,
1638
+ * since `createContract` is first-writer-wins.
1639
+ *
1640
+ * Best-effort by design: a failure here is reported and retried on the next
1641
+ * pass, and never aborts the pass it is part of. Registration buys latency
1642
+ * and puts the lockup in the wallet's contract set; it decides nothing. The
1643
+ * money path below it reads the indexer directly and is gated on timelocks
1644
+ * that a missing contract row has no bearing on, so failing the pass over
1645
+ * this would trade a real deadline for a bookkeeping one.
1646
+ */
1647
+ private ensureRegistered;
1648
+ /** Stop watching a finished swap's lockup. Retained, not deleted: the row
1649
+ * is what keeps the lockup's own VTXOs annotatable and its history
1650
+ * readable, while `retained` is what drops it from the subscription and
1651
+ * the poll — a settled swap that stayed watched would cost the wallet a
1652
+ * script for its whole life. Best-effort — the swap is over either way. */
1653
+ private retireContract;
1654
+ private arm;
1655
+ private pollSwap;
1656
+ private runPass;
1657
+ /**
1658
+ * The receive leg's whole state machine: claim the solver-funded lockup
1659
+ * while the window is open, and recognise the shapes in which it can be
1660
+ * lost.
1661
+ *
1662
+ * **The window closes at `refundLocktime`, on wall clock, with no margin.**
1663
+ * Both halves of that are deliberate. It closes there because publishing
1664
+ * `P` into the solver's live refund window risks losing the race and
1665
+ * handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
1666
+ * guards on the L1 side. It takes no margin because the two situations are
1667
+ * not alike: that one budgets for confirmation depth, while this claim is an
1668
+ * offchain spend that lands in seconds. Wall clock is already the
1669
+ * conservative reading — the solver's leaf is a CLTV, which matures against
1670
+ * median-time-past, and MTP trails wall clock — so the real window extends
1671
+ * PAST this deadline rather than ending before it. Every second of margin
1672
+ * subtracted here is a second of live claim window given away for nothing.
1673
+ *
1674
+ * **The trader has no move after it.** Nothing here can take the lockup
1675
+ * back, so once the window shuts the swap is the solver's to resolve and
1676
+ * this manager's job is to watch it happen and then stop.
1677
+ */
1678
+ private driveReceiveClaim;
1679
+ /**
1680
+ * Claim what the solver funded, once it is enough.
1681
+ *
1682
+ * The value gate here decides WHEN to act. `pushClaim`'s decides whether
1683
+ * `P` is published, and runs with nothing between it and the signature —
1684
+ * the check that matters is the inner one, and this is not a reason to
1685
+ * relax it.
1686
+ */
1687
+ private claimIfFunded;
1688
+ /** `handled` ends the pass; `continue` falls through to the refund gate. */
1689
+ private driveOnchain;
1690
+ private driveArkadeRefund;
1691
+ /** Whether any of these outputs has never been handed to the claim
1692
+ * callback — the only reason to claim a lockup a second time. */
1693
+ private hasUnclaimedOutpoint;
1694
+ private rememberClaimed;
1695
+ /**
1696
+ * L1 progress, which past the refund window must not overwrite a refusal.
1697
+ * The two halves are independent — a claimed fill says nothing about
1698
+ * whether this wallet can take the Arkade lockup back — and `claimed` is
1699
+ * re-asserted from chain on every pass, so without this a blocked swap
1700
+ * would flip between the two states forever. The claim itself always runs;
1701
+ * only the label defers, and only once the refund is the live half.
1702
+ */
1703
+ private setOnchainState;
1704
+ /** The probe's refusal reason, or `undefined` when a local refund is
1705
+ * possible as far as anyone here can tell. A probe that throws is treated
1706
+ * as a refusal: a capability check that cannot answer is not a yes. */
1707
+ private probeRefusal;
1708
+ /** Report that no local refund will happen, without ending the swap. */
1709
+ private block;
1710
+ /** The way back out, taken as soon as the swap becomes actionable again.
1711
+ * Back to what the record can prove, not to `pending` unconditionally: a
1712
+ * swap that already made its claim has a txid for it, and reporting that
1713
+ * swap as `pending` would un-say something true. */
1714
+ private unblock;
1715
+ private touch;
1716
+ private setState;
1717
+ /** Terminal failure. The `onSwapFailed` emission is left to
1718
+ * {@link finalize}, so this does not double-report. */
1719
+ private fail;
1720
+ private emitFailed;
1721
+ private emitAction;
1722
+ /** Whether the record is now persisted — false only when `saveSwap` threw. */
1723
+ private save;
1724
+ /**
1725
+ * Drop a terminal swap from monitoring and report it exactly once.
1726
+ *
1727
+ * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
1728
+ * Boltz's manager, which fires completion for every swap that leaves
1729
+ * monitoring including the failed ones — a listener named "completed" that
1730
+ * also fires on failure is a trap worth not inheriting.
1731
+ */
1732
+ private finalize;
1733
+ private settleWaiters;
1734
+ }
1735
+ /** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
1736
+ * trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
1737
+ * receive — or the ark txid for a refund the trader pushed; a solver-side
1738
+ * settlement or refund carries none, and a receive swap that ended `refunded`
1739
+ * carries none either, however far its claim got (see {@link outcomeOf}). So
1740
+ * a `txid` here always names something that happened, and `state` remains the
1741
+ * only thing to read for whether the swap paid out. */
1742
+ interface RfqSwapOutcome {
1743
+ state: RfqSwapState;
1744
+ txid?: string;
1745
+ }
1746
+
1747
+ export { type ArkadeRefundResult, AssetSwap, AssetSwapRepository, ChainSource, ChainUtxo, type ClaimArkProvider, type ClaimPacketInput, type DiscoverMarketsOptions, IndexedDbAssetSwapRepository, type LightningReceiveSwap, type LightningSendSwap, LockupAmountMismatchError, type LockupContractWriter, type LockupFate, LockupNeedsRecoveryError, LockupRegistrationFailed, type LockupSpendIndexer, type LockupVtxo, MarketsCacheEntry, OFFER_PACKET_TYPE, type Offer, type OfferContractRetirer, type OfferSwapWatcher, OnchainHtlc, OnchainHtlcPhase, type OnchainSendAction, type OnchainSendSwap, type PlanError, QUOTE_OPTIONS, REFUND_MTP_LAG_SECONDS, RFQ_RESOLVED_STATES, RFQ_SWAP_TERMINAL_STATES, type RefundArkProvider, type RefundIndexer, type RefundOutcome, type RestoreIndexer, RfqStatus, type RfqSwap, type RfqSwapActionName, type RfqSwapLockup, RfqSwapManager, type RfqSwapManagerCallbacks, type RfqSwapManagerConfig, type RfqSwapManagerDeps, type RfqSwapManagerEvents, type RfqSwapOutcome, type RfqSwapState, RfqTransport, SWAP_LOCKUP_CONTRACT_KIND, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_TYPE, type SealedClaimPacket, type SpendKind, type SwapContractRegistry, type Tx, type WatchOfferSwapsParams, awaitLockupFunding, awaitRfqResolution, cancelOffer, claimReceiveLockup, classifyDepositSpend, classifySpend, createOffer, decodeOffer, discoverMarkets, encodeOffer, findLockupVtxos, findMarket, isRfqSwapTerminal, isRfqTerminal, makeCachedFeedFetch, nextOnchainAction, offerVtxoScript, pushClaim, pushRefundWithoutReceiver, readLockupFate, refundIfUnresolved, registerLockupContract, restoreAssetSwaps, retireSettledOfferContracts, sealClaimPacket, spendTxidsOf, spendUpdate, swapPrograms, validatePlan, watchOfferSwaps };