@arkade-os/swap 0.0.7 → 0.0.8

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,1862 @@
1
+ import { DiscoveredMarket } from '@arkade-os/solver-discovery';
2
+ import { IWallet, ProvisionedKey, ProvisionedClaimSecret, RestArkProvider, RestIndexerProvider, VHTLC, Identity, IContractManager } from '@arkade-os/sdk';
3
+ import { w as RfqStatus, x as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, s as OnchainHtlcPhase } from './rfq-C-aq5LDJ.js';
4
+
5
+ type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
6
+ /** The sentinel asset id for BTC itself, as opposed to a 68-hex asset id.
7
+ * Lives here with the {@link AssetSwap} fields it describes so the market and
8
+ * restore layers share one spelling instead of re-typing the literal. */
9
+ declare const BTC_ASSET_ID = "btc";
10
+ /**
11
+ * The record fields a wallet-provisioned secret becomes — what
12
+ * {@link swapSecretsToRecord} emits, and what every record type carrying swap
13
+ * secrets embeds.
14
+ *
15
+ * A named type rather than four fields restated per record: the mapper and the
16
+ * records it feeds must agree exactly, and a record that silently omits one of
17
+ * these round-trips a swap whose preimage cannot be re-derived. Embedding makes
18
+ * the omission a compile error instead.
19
+ *
20
+ * **Only `preimageHex` is secret.** `signingDescriptor` and `preimageSaltHex`
21
+ * are public derivation inputs — they must survive a field-mapped backend, but
22
+ * they leak nothing without the seed.
23
+ */
24
+ interface SwapSecretsProjection {
25
+ /**
26
+ * The wallet descriptor this swap's sender key comes from — a fresh HD
27
+ * child, or a static wallet's `tr(pubkey)`. Public — the signer
28
+ * re-derives from the wallet, so the record carries no key material.
29
+ */
30
+ signingDescriptor?: string;
31
+ /** P, hex, when it cannot be re-derived from the seed at all: the user
32
+ * supplied it, or the signer cannot sign deterministically. The swap's only
33
+ * claim secret when present. */
34
+ preimageHex?: string;
35
+ /**
36
+ * The salt P derives from, hex, on the salted arm — what a static wallet
37
+ * gets instead of storing P. **Public**, and unlike every other field here
38
+ * it is minted per swap: it is what stops one repeating key from handing
39
+ * every swap the same preimage.
40
+ */
41
+ preimageSaltHex?: string;
42
+ }
43
+ interface AssetSwap extends SwapSecretsProjection {
44
+ /** Funding txid — the swap's identity. */
45
+ id: string;
46
+ /** 'btc' or a 68-hex asset id. */
47
+ fromAsset: string;
48
+ toAsset: string;
49
+ /** Atomic amounts as strings (bigint is not JSON-safe). */
50
+ fromAmount: string;
51
+ /** The covenant wantAmount — a floor, the fill pays >= this. */
52
+ toAmount: string;
53
+ swapAddress: string;
54
+ /** Hex pkScript of the swap contract — the indexer monitoring key. */
55
+ swapPkScript: string;
56
+ /** TLV offer — needed to rebuild the contract for cancel. */
57
+ offerHex: string;
58
+ fundingTxid: string;
59
+ spentTxid?: string;
60
+ status: AssetSwapStatus;
61
+ createdAt: number;
62
+ completedAt?: number;
63
+ /** RFQ pair string, e.g. `arkade:BTC->onchain:BTC`. */
64
+ pair?: string;
65
+ /** `sha256(P)`, hex. Public, and how a restore confirms a candidate
66
+ * derivation is the right one. */
67
+ paymentHash?: string;
68
+ /** The L1 HTLC's pkScript, hex — the chain-watch key. */
69
+ htlcPkScriptHex?: string;
70
+ htlcLocktime?: number;
71
+ /** The L1 funding txid, once observed. */
72
+ l1Txid?: string;
73
+ }
74
+ /** All swaps, newest-first. Insertion order is not chronological — the restore
75
+ * scan rebuilds records in tx-scan order — so sort at read to keep
76
+ * newest-first canonical for every consumer. */
77
+ declare const getAssetSwapsOrThrow: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
78
+ /** The consumer read: a broken backend reads as no swaps rather than crashing
79
+ * a history view. Mutations must use {@link getAssetSwapsOrThrow} instead —
80
+ * swallowing the read there would let "the backend is gone" masquerade as "no
81
+ * such swap" and skip the write silently. */
82
+ declare const getAssetSwaps: (repository: AssetSwapRepository) => Promise<AssetSwap[]>;
83
+ /** Add a swap; no-op if the id is already stored. Returns the updated list.
84
+ * THROWS on a failed write — nothing irreversible may happen until this record
85
+ * is durable, so the caller must not fund on a failure. */
86
+ declare const addAssetSwap: (repository: AssetSwapRepository, swap: AssetSwap) => Promise<AssetSwap[]>;
87
+ /** Merge changes into a swap by id. Returns the updated list.
88
+ * THROWS on a failed read or write, like {@link addAssetSwap} — use this for a
89
+ * write that gates something irreversible. Transitions written *after* the
90
+ * irreversible act belong on {@link updateAssetSwapBestEffort}. */
91
+ declare const updateAssetSwap: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<AssetSwap[]>;
92
+ /**
93
+ * {@link updateAssetSwap} for transitions that follow an irreversible action (a
94
+ * broadcast claim, a spent lockup): failing the caller there would report as
95
+ * failed a swap whose funds already moved, and a stale status is recoverable —
96
+ * crash recovery re-derives the true state from the chain
97
+ * (`classifyOnchainHtlc`).
98
+ *
99
+ * `persisted` is the part that must not be hidden: a caller that notifies on a
100
+ * change, or treats one as terminal, has to know the store did not agree.
101
+ */
102
+ declare const updateAssetSwapBestEffort: (repository: AssetSwapRepository, id: string, changes: Partial<Omit<AssetSwap, "id">>) => Promise<{
103
+ swaps: AssetSwap[];
104
+ persisted: boolean;
105
+ }>;
106
+ /**
107
+ * The record fields a wallet-provisioned secret becomes.
108
+ *
109
+ * `signingDescriptor` is public and always stored — it is what recovers the
110
+ * signer. Then at most one of: `preimageHex`, when the wallet says it cannot
111
+ * re-derive P and it becomes the swap's only claim secret; or
112
+ * `preimageSaltHex`, the public input a derivable-but-repeating key needs.
113
+ */
114
+ declare const swapSecretsToRecord: (secrets: ProvisionedKey | ProvisionedClaimSecret) => SwapSecretsProjection & {
115
+ signingDescriptor: string;
116
+ };
117
+ /** Why a wallet cannot produce a swap's preimage. */
118
+ type PreimageBlockedReason =
119
+ /** The record carries no `signingDescriptor`. */
120
+ "no-secrets"
121
+ /** `preimageHex` or `preimageSaltHex` is present but not 32 bytes of hex. */
122
+ | "malformed-record"
123
+ /**
124
+ * Nothing to derive from: a descriptor that repeats across swaps, with
125
+ * neither a stored preimage nor a salt — or one this wallet holds no key
126
+ * for. Merged deliberately: `contractSigner` reports a key it does not
127
+ * hold as a plain `Error` for static wallets and a `ForeignDescriptorError`
128
+ * for HD ones, so splitting the two here would mean matching on message
129
+ * text, which is the thing this type exists to avoid. The `cause` carries
130
+ * whichever it was.
131
+ */
132
+ | "not-derivable"
133
+ /** Derived, but it does not hash to the record's `paymentHash`. */
134
+ | "hash-mismatch";
135
+ /**
136
+ * The wallet cannot produce this swap's preimage, and which of the four ways
137
+ * is `reason`.
138
+ *
139
+ * Deliberately **not** {@link RefundNotLocallyPossibleError}: that one means
140
+ * "no local refund is possible", and `RfqSwapManager` acts on it by reporting
141
+ * `needs_counterparty`. A claim-path read failure is a different verdict, and
142
+ * borrowing the refund error would have the manager announce one for the
143
+ * other.
144
+ */
145
+ declare class PreimageNotRecoverableError extends Error {
146
+ readonly reason: PreimageBlockedReason;
147
+ readonly name = "PreimageNotRecoverableError";
148
+ constructor(reason: PreimageBlockedReason, message: string, options?: {
149
+ cause?: unknown;
150
+ });
151
+ }
152
+ /**
153
+ * The preimage a swap record claims with — stored, or re-derived from the
154
+ * wallet.
155
+ *
156
+ * The record-shaped inverse of {@link swapSecretsToRecord}, and the one place
157
+ * that knows which of a record's fields `contractPreimage` needs. Wire claim
158
+ * paths here rather than composing it by hand: a caller that forgets to pass
159
+ * `preimageSaltHex` gets a *wrong* preimage from a wallet that can derive,
160
+ * not an error.
161
+ *
162
+ * Verifies the result against `paymentHash` when the record carries one. The
163
+ * salted arm has two inputs that can be wrong — the key and the salt — where
164
+ * the HD arm had one, and a wrong P otherwise surfaces as an opaque script
165
+ * failure at claim time, long after the mistake.
166
+ *
167
+ * Every refusal is a {@link PreimageNotRecoverableError} carrying a `reason`,
168
+ * so a caller can tell "this record predates the descriptor" from "the salt is
169
+ * corrupt" without reading message text.
170
+ */
171
+ declare const preimageForSwapRecord: (wallet: IWallet, record: SwapSecretsProjection & {
172
+ paymentHash?: string;
173
+ }) => Promise<Uint8Array>;
174
+
175
+ /** True for the states after which the solver will report nothing further. */
176
+ declare const isRfqTerminal: (state: string) => boolean;
177
+ /**
178
+ * The terminal states that mean the swap is OVER and the lockup is already
179
+ * gone — the solver either claimed it (`settled`, revealing the preimage) or
180
+ * returned it (`refunded`). A trader seeing one of these has nothing left to
181
+ * do.
182
+ *
183
+ * Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
184
+ * and `stuck` are terminal for the NEGOTIATION but say nothing about whether
185
+ * the trader's sats are still sitting at the lockup. A trader that funded just
186
+ * as the quote expired, or whose solver wedged mid-payment, is exactly the
187
+ * trader who needs the refund most — so those states must not short-circuit
188
+ * it. {@link refundIfUnresolved} treats them as "keep going", and lets the
189
+ * on-chain VTXO lookup be the authority on whether anything is actually there.
190
+ */
191
+ declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
192
+ /**
193
+ * Poll a swap's status until it reaches a terminal state.
194
+ *
195
+ * Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
196
+ * an optional unix-seconds `deadline`, and a thrown error carrying a stable
197
+ * `reason` when that deadline passes.
198
+ *
199
+ * A `null` status (the solver has no record of this `rfq_id`) is treated as
200
+ * "not yet", not as an answer — a status route can legitimately 404 for a
201
+ * moment after a quote is issued. The deadline is what bounds that wait.
202
+ *
203
+ * Transport errors are NOT swallowed; a failing `status()` call rejects this
204
+ * function. Callers polling across a long refund window should expect to
205
+ * restart it after a network blip — nothing is lost by doing so, since the
206
+ * refund path this feeds is gated on an absolute timelock that does not
207
+ * expire.
208
+ */
209
+ declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
210
+ pollMs?: number;
211
+ deadline?: number;
212
+ }): Promise<RfqStatus>;
213
+ /** The Ark surface the refund push needs — narrower than a full provider, and
214
+ * satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
215
+ type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
216
+ /** The indexer surface the lockup lookup needs. */
217
+ type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
218
+ /** A still-refundable virtual output sitting at the swap lockup. */
219
+ interface LockupVtxo {
220
+ txid: string;
221
+ vout: number;
222
+ value: number;
223
+ /**
224
+ * The batch this output lived in expired and the operator swept it, so it
225
+ * is no longer a live leaf — it can be RECOVERED, but not spent offchain.
226
+ *
227
+ * It is still the trader's money and it is still visible, which is why
228
+ * {@link findLockupVtxos} returns it. What it is not is refundable by
229
+ * {@link pushRefundWithoutReceiver}: that builds an offchain Ark
230
+ * transaction, and the SDK's own predicates make the two states mutually
231
+ * exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
232
+ * is true (`wallet/vtxo.ts`), and the latter is documented as "must be
233
+ * recovered into a fresh batch rather than spent offchain". Holding the
234
+ * trader's `sender` key does not change that; a sweep removes the leaf from
235
+ * the live tree, not the signature from the trader.
236
+ *
237
+ * `packages/boltz-swap` splits on exactly this fact rather than working
238
+ * around it: `settleRefundWithoutReceiver` sends a live VTXO through an
239
+ * offchain tx and a recoverable one through `joinBatch` — "a swept
240
+ * (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
241
+ * by re-registering it into a batch".
242
+ *
243
+ * So the remedy is recovery (renewing the output into a fresh batch),
244
+ * after which the ordinary CLTV refund works again. This package does not
245
+ * build that round — see {@link pushRefundWithoutReceiver}, which refuses
246
+ * rather than submitting a spend that cannot succeed.
247
+ */
248
+ recoverable: boolean;
249
+ }
250
+ /**
251
+ * Thrown when a refund was asked for over outputs that have been swept.
252
+ *
253
+ * Carries the outpoints so a caller can act — recover exactly those, then
254
+ * retry — instead of reading a server rejection and guessing. `reason` follows
255
+ * the same convention as `awaitOnchainFill`'s `fill_timeout` and
256
+ * `claimOnchainFill`'s `claim_window_closed`.
257
+ *
258
+ * **The remedy already exists; this package does not reimplement it.** The SDK
259
+ * recovers swept outputs by re-registering them into a fresh batch, through
260
+ * `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
261
+ * reaches via its own `joinBatch`. It reads the wallet's registered-contract
262
+ * snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
263
+ * `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
264
+ * covers a swap lockup as soon as that lockup is registered as a contract —
265
+ * which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
266
+ * therefore not only a latency optimization; it is what turns a swept lockup
267
+ * from a dead end into something the ordinary wallet path can recover.
268
+ *
269
+ * Two caveats a caller must hold, neither enforceable from here:
270
+ *
271
+ * - **The wallet must hold the lockup's `sender` key**, because recovery
272
+ * settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
273
+ * these VTXOs with.
274
+ * - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
275
+ * recovery round including this VTXO earlier is rejected. `recoverVtxos`
276
+ * sweeps every recoverable output in ONE settlement and has no CLTV
277
+ * awareness, so recovering early can fail the whole batch rather than just
278
+ * this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
279
+ * recoverable → skipped".
280
+ */
281
+ declare class LockupNeedsRecoveryError extends Error {
282
+ readonly name = "LockupNeedsRecoveryError";
283
+ readonly reason = "needs_recovery";
284
+ /** `txid:vout` for each output that must be recovered first. */
285
+ readonly outpoints: string[];
286
+ /**
287
+ * The contract's `refundLocktime`. Recovering before this matures is the
288
+ * hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
289
+ * into one settlement with no CLTV awareness, so an early attempt can fail
290
+ * the whole batch — including unrelated outputs that were otherwise fine.
291
+ *
292
+ * Exposed as a value, not only inside the message, so a caller can encode
293
+ * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
294
+ * parsing prose. Seconds-based locktimes mature against the chain tip's
295
+ * timestamp rather than wall clock, so treat this as a floor to wait past,
296
+ * not an exact alarm.
297
+ */
298
+ readonly recoverableAfter: bigint;
299
+ constructor(outpoints: string[], recoverableAfter: bigint);
300
+ }
301
+ /**
302
+ * Every output still sitting at the lockup script — spendable AND
303
+ * swept-but-recoverable, each tagged with which it is.
304
+ *
305
+ * All of them, not the first: a trader may fund a lockup in more than one
306
+ * send, and refunding only `vtxos[0]` returns part of the money and strands
307
+ * the rest at a script whose other refund paths are all longer.
308
+ *
309
+ * BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
310
+ * blind at exactly the wrong moment. A lockup whose batch expiry passed is
311
+ * swept into the recoverable set, and this function exists to serve swaps that
312
+ * sat unresolved — which are precisely the ones most likely to have got there.
313
+ * Reading only the spendable set would report `nothing_to_refund` over money
314
+ * that is still sitting at the script, which is worse than an error: it looks
315
+ * like a resolved swap. `packages/boltz-swap` merges the same two queries for
316
+ * the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
317
+ *
318
+ * **Visible is not the same as refundable.** A `recoverable` output cannot be
319
+ * spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
320
+ * "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
321
+ * That function refuses the recoverable ones by name rather than submitting a
322
+ * spend the server must reject.
323
+ *
324
+ * This read — not the RFQ's reported state — is the authority on whether
325
+ * there is anything left at the lockup.
326
+ *
327
+ * **Not replaced by the contract manager's VTXO state, deliberately.** Once a
328
+ * lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
329
+ * these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
330
+ * `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
331
+ * counts: it serves the wallet REPOSITORY, which a degraded sync will happily
332
+ * hand back stale (`getSyncState()` reports `degraded` and returns cached rows
333
+ * rather than failing), and its height-based expiry test needs a chain tip this
334
+ * module does not have. The two queries below ask the indexer itself and need
335
+ * neither. Ask-the-indexer, don't-trust-local-state — the same posture
336
+ * {@link readLockupFate} takes, and for the same reason: this decides money.
337
+ */
338
+ declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
339
+ /**
340
+ * The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
341
+ * raw transactions those vtxos were spent by. Same narrow-seam style as
342
+ * {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
343
+ * {@link RestIndexerProvider}.
344
+ */
345
+ type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
346
+ /**
347
+ * What chain data says became of a swap lockup — the whole answer, with no
348
+ * solver involvement and nothing taken on the solver's word.
349
+ */
350
+ interface LockupSpend {
351
+ /** What the vtxo's `spentBy` names — the checkpoint, never the ark
352
+ * transaction. */
353
+ checkpointTxid: string;
354
+ /** The ark transaction that spent the above checkpoint output. What
355
+ * history correlation matches on; absent when the indexer omitted it. */
356
+ arkTxid?: string;
357
+ }
358
+ type LockupFate =
359
+ /** At least one output at the lockup is still unspent. Not over. */
360
+ {
361
+ fate: "open";
362
+ }
363
+ /** Spent by a witness carrying a preimage that HASHES to the quote's
364
+ * `payment_hash`. Only the claim leaf can reveal one, and the only
365
+ * legitimate way the solver obtains it is by completing its side. */
366
+ | {
367
+ fate: "claimed";
368
+ preimage: Uint8Array;
369
+ spends: readonly LockupSpend[];
370
+ }
371
+ /** Fully spent, and nothing that spent it revealed a matching preimage —
372
+ * so the money went back to the trader. See {@link readLockupFate}. */
373
+ | {
374
+ fate: "returned";
375
+ spends: readonly LockupSpend[];
376
+ }
377
+ /** Nothing was learned: no outputs visible, an output spent by nothing the
378
+ * indexer names, a spend it could not produce, or a blob that would not
379
+ * decode. Never an answer. */
380
+ | {
381
+ fate: "unknown";
382
+ };
383
+ /**
384
+ * Decide from chain data alone whether a swap lockup settled, came back, or is
385
+ * still live.
386
+ *
387
+ * **Why this is decidable without asking anyone.** The lockup's claim leaf can
388
+ * only be spent by revealing `P`, so a spend witness carrying a value that
389
+ * hashes to the quote's `payment_hash` is proof the claim leaf was used — and
390
+ * the only legitimate way the counterparty obtains `P` is by completing its
391
+ * side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
392
+ * covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
393
+ * and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
394
+ * `unilateralRefundWithoutReceiver` all require the trader's own signature. So
395
+ * "spent, but not by a hash-verified claim" means the money went back to the
396
+ * trader, and nothing here has to trust a counterparty to say so.
397
+ *
398
+ * **A matching witness SHAPE is not proof.** Only a candidate that hashes to
399
+ * `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
400
+ * else is just bytes, and is treated as a refund. Getting this wrong in the
401
+ * permissive direction would report "settled" for a swap that actually
402
+ * refunded, which is precisely the fact a trader is relying on.
403
+ *
404
+ * **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
405
+ * not visible yet), a `spentBy` the indexer cannot produce a transaction for,
406
+ * or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
407
+ * may legitimately return fewer transactions than were asked for, so the
408
+ * observed set is counted rather than assumed complete. The caller's correct
409
+ * response to `unknown` is the same as to `open`: keep watching, and let the
410
+ * refund timelock — which no outage can move — be what ends the wait.
411
+ *
412
+ * Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
413
+ * cached, the same posture {@link findLockupVtxos} already establishes.
414
+ */
415
+ declare function readLockupFate(indexer: LockupSpendIndexer, input: {
416
+ swapPkScript: Uint8Array;
417
+ /** `sha256(P)`, hex — the quote's `payment_hash`. */
418
+ paymentHash: string;
419
+ }): Promise<LockupFate>;
420
+ /**
421
+ * Build, sign, and push the `refundWithoutReceiver` spend: return every funded
422
+ * output at the lockup to the trader's refund address.
423
+ *
424
+ * The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
425
+ * VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
426
+ * emulator is not involved: it co-signs only the two covenant leaves
427
+ * (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
428
+ * own escape hatch has to go through it and this one does not. So unlike that
429
+ * push, this transaction is submitted SIGNED, and the only counterparty is the
430
+ * Arkade server doing what it does for any collaborative spend.
431
+ *
432
+ * One aggregate output, not one per input — again unlike the solver's covenant
433
+ * refund, which needs index-aligned outputs because its ArkadeScript inspects
434
+ * the output at the current input's index. This leaf carries no covenant, so a
435
+ * single output paying the whole balance is both valid and cheaper.
436
+ *
437
+ * `refundPkScript` defaults to the destination the contract itself commits to
438
+ * (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
439
+ * at quote time), so the ordinary call cannot send the refund somewhere the
440
+ * trader did not intend. It is overridable because this leaf, having no
441
+ * covenant, genuinely does permit any destination.
442
+ *
443
+ * **Consensus, not wall clock, decides when this is spendable.** A seconds
444
+ * locktime matures against median-time-past, which trails real time by roughly
445
+ * an hour, so a push issued the moment `refundLocktime` passes can be rejected
446
+ * until enough blocks land. That is expected, not a failure — see
447
+ * {@link refundIfUnresolved}, which retries.
448
+ *
449
+ * **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
450
+ * a swept output is no longer a live leaf: `canSpendOffchain` and
451
+ * `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
452
+ * input cannot be spent this way whatever key signs it (see
453
+ * {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
454
+ * transaction, a single swept output would take the live ones down with it —
455
+ * so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
456
+ * outpoints, rather than submitted and rejected. Filtering them out silently
457
+ * would be worse still: it would report success over money that never moved.
458
+ */
459
+ declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
460
+ script: InstanceType<typeof VHTLC.ScriptV2>;
461
+ /** The `sender` signer. Build it from the swap record with
462
+ * {@link senderIdentityForSwapRecord} — on an HD wallet that resolves
463
+ * from the seed, with no stored key bytes anywhere, and every way the
464
+ * wallet can fail to produce it arrives as one typed
465
+ * {@link RefundNotLocallyPossibleError} the manager reads as permanent
466
+ * rather than retrying for the rest of the refund window. */
467
+ sender: Identity;
468
+ vtxos: readonly LockupVtxo[];
469
+ /** Defaults to the contract's own committed refund destination. */
470
+ refundPkScript?: Uint8Array;
471
+ }): Promise<{
472
+ arkTxid: string;
473
+ amount: number;
474
+ }>;
475
+ /**
476
+ * How long past `refundLocktime` to keep retrying the push before giving up
477
+ * and surfacing the server's refusal.
478
+ *
479
+ * Two hours because the CLTV matures against median-time-past (BIP-113), which
480
+ * lags wall clock by about an hour, plus room for a slow block. This is the
481
+ * mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
482
+ * of the same margin.
483
+ */
484
+ declare const REFUND_MTP_LAG_SECONDS: number;
485
+ type RefundOutcome =
486
+ /** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
487
+ {
488
+ outcome: "resolved";
489
+ status: RfqStatus;
490
+ }
491
+ /** The trader took it back via `refundWithoutReceiver`. */
492
+ | {
493
+ outcome: "refunded";
494
+ arkTxid: string;
495
+ amount: number;
496
+ status: RfqStatus | null;
497
+ }
498
+ /** The refund window opened but the lockup holds nothing to return. */
499
+ | {
500
+ outcome: "nothing_to_refund";
501
+ status: RfqStatus | null;
502
+ }
503
+ /**
504
+ * The money is still at the lockup, but its batch was swept, so no offchain
505
+ * spend can take it back until it is recovered into a fresh batch. Returned
506
+ * rather than retried: unlike a median-time-past refusal, no amount of
507
+ * waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
508
+ * named outpoints, then call this again.
509
+ */
510
+ | {
511
+ outcome: "needs_recovery";
512
+ outpoints: string[];
513
+ vtxos: LockupVtxo[];
514
+ status: RfqStatus | null;
515
+ };
516
+ /**
517
+ * Ask first, then fall back: watch the swap for the solver to resolve it, and
518
+ * if `refundLocktime` matures without that happening, take the lockup back
519
+ * with `refundWithoutReceiver`.
520
+ *
521
+ * This is the whole trader-side failure story in one call. It polls `status()`
522
+ * — the only "asking" this protocol has (see the module doc) — and returns as
523
+ * soon as the solver reports `settled` or `refunded`. Otherwise, once the
524
+ * quote's `refund_locktime` passes, it looks up what is actually at the lockup
525
+ * and pushes the refund.
526
+ *
527
+ * Two behaviours worth knowing:
528
+ *
529
+ * - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
530
+ * `stuck` are terminal states, but a trader can be holding a funded lockup in
531
+ * every one of them, so they do not end the wait — only `settled`/`refunded`
532
+ * do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
533
+ * timelock.
534
+ * - **The first push after the deadline may legitimately fail.** Median-time-
535
+ * past trails wall clock, so the server can still consider the leaf locked
536
+ * for a while after `refundLocktime` passes in real time. Failures are
537
+ * retried at the poll interval until `attemptDeadline`, after which the last
538
+ * error is rethrown rather than swallowed.
539
+ * - **A swept lockup ends the wait instead of consuming it.** Once the batch
540
+ * is gone the CLTV refund is not "not yet" but "not this way", so it returns
541
+ * `needs_recovery` naming the outpoints rather than retrying until the
542
+ * deadline. Recover them and call again.
543
+ *
544
+ * Safe to call late, and safe to call again: a caller recovering from a crash
545
+ * well past the deadline skips straight to the push, and a lockup that is
546
+ * already empty comes back as `nothing_to_refund` instead of an error.
547
+ */
548
+ declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
549
+ rfqId: string;
550
+ script: InstanceType<typeof VHTLC.ScriptV2>;
551
+ /** @see pushRefundWithoutReceiver */
552
+ sender: Identity;
553
+ /** `refund_locktime` from the quote, unix seconds. */
554
+ refundLocktime: number;
555
+ /** Defaults to the contract's own committed refund destination. */
556
+ refundPkScript?: Uint8Array;
557
+ pollMs?: number;
558
+ /** Stop retrying the push at this unix time, rethrowing the last
559
+ * error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
560
+ attemptDeadline?: number;
561
+ /** Injected for tests; defaults to wall clock, in unix seconds. */
562
+ now?: () => number;
563
+ }): Promise<RefundOutcome>;
564
+
565
+ /**
566
+ * Where a monitored RFQ swap stands, and which of those states end it.
567
+ *
568
+ * Its own module rather than a corner of `swapManager.ts` because the
569
+ * dependency runs the other way: the record layer decides retention from
570
+ * {@link isRfqSwapTerminal}, and the manager persists through the record
571
+ * layer. With the vocabulary here, neither has to import the other at runtime.
572
+ * `swapManager.ts` re-exports all three names, so nothing about the public
573
+ * surface moved.
574
+ */
575
+ /**
576
+ * Where a monitored swap stands.
577
+ *
578
+ * `claimable` and `claimed` are the states of a swap the TRADER has something
579
+ * to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
580
+ * a receive. Only `lightning_send` has neither — there the solver claims the
581
+ * lockup, and the trader's only move is the refund.
582
+ */
583
+ type RfqSwapState =
584
+ /** Live; nothing actionable yet. On a receive leg this covers the whole
585
+ * stretch before the solver funds anything. */
586
+ "pending"
587
+ /** There is something for the trader to take, and the window to take it is
588
+ * open: the confirmed L1 fill on an onchain send, or a lockup funded for at
589
+ * least `expectedAmount` on a receive. */
590
+ | "claimable"
591
+ /**
592
+ * The trader's claim has been made — its L1 broadcast on an onchain send,
593
+ * its Arkade submission on a receive.
594
+ *
595
+ * **On a receive this is a local belief and not a chain fact**, which is
596
+ * why it is not terminal: `settled` is the chain's answer, and `refunded`
597
+ * is still reachable from here if the claim never lands and the solver
598
+ * takes the lockup back.
599
+ */
600
+ | "claimed"
601
+ /**
602
+ * This wallet will not act, and only the counterparty can change that.
603
+ *
604
+ * On a send leg: the Arkade refund cannot be pushed from here — no secrets
605
+ * on the record, a descriptor from another seed, or nothing wired to act —
606
+ * so the lockup comes back only if the counterparty claims it or the wallet
607
+ * that can sign it is restored. On a receive leg: the trader holds no
608
+ * refund at all, so this is a lockup that cannot be claimed — funded for
609
+ * less than the swap agreed (publishing `P` for it is the whole attack
610
+ * `LockupAmountMismatchError` exists to refuse), or one whose claim window
611
+ * shut unclaimed. `RfqSwapCommon.blockedReason` says which.
612
+ *
613
+ * **Not terminal, and not a dead end.** The money is still at the lockup,
614
+ * so the counterparty's move is still observable and still ends the swap;
615
+ * and the refusal is re-checked every pass, so restoring the right wallet,
616
+ * wiring the callbacks, or the solver topping the lockup up returns the
617
+ * swap to `pending` and resumes the normal drive. For an onchain-send swap
618
+ * it says nothing about the L1 half, which keeps being driven and claimed.
619
+ */
620
+ | "needs_counterparty"
621
+ /**
622
+ * Terminal: the lockup was spent by a hash-verified claim. Read off chain,
623
+ * never reported.
624
+ *
625
+ * On a send leg that claim is the counterparty's, and it is proof the
626
+ * counterparty completed its side. On a receive leg it is the TRADER's own
627
+ * — matched by the hash and not by our txid, so a claim that lands without
628
+ * us still counts (see `RfqSwapManager`).
629
+ */
630
+ | "settled"
631
+ /**
632
+ * Terminal: the lockup was spent by something other than a claim.
633
+ *
634
+ * On a send leg that is the money coming back, by the solver's hand or the
635
+ * trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
636
+ * money, every non-claim leaf is the solver's, and a swap that ends here
637
+ * ended with the trader's incoming payment never arriving. It is also where
638
+ * a receive swap ends when its window closes with nothing left to observe —
639
+ * see `RfqSwapManager`.
640
+ */
641
+ | "refunded"
642
+ /** Terminal: an action failed and its window closed. */
643
+ | "failed";
644
+ /** The states after which the manager stops monitoring a swap. Deliberately
645
+ * without `needs_counterparty`: retiring on it would unwatch a funded lockup
646
+ * whose claim is still the thing that ends the swap. */
647
+ declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
648
+ declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
649
+
650
+ /**
651
+ * What the manager needs to register a swap's lockup with the wallet, so the
652
+ * indexer pushes its funding and its spend instead of being asked every few
653
+ * seconds.
654
+ *
655
+ * Both fields are things the caller already holds. `script` is the very object
656
+ * `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
657
+ * it in hand; `address` is the request entrypoint's own return value. The
658
+ * address is taken rather than re-derived on purpose — the row's address must be
659
+ * the one that was actually funded, and a local re-derivation would silently use
660
+ * the SDK's default network, which is the exact bug `registerOfferContract`
661
+ * guards against.
662
+ */
663
+ interface RfqSwapLockup {
664
+ /** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
665
+ script: InstanceType<typeof VHTLC.ScriptV2>;
666
+ /** The Arkade address that was funded. */
667
+ address: string;
668
+ }
669
+ interface RfqSwapCommon {
670
+ /** The negotiation id — this record's identity. */
671
+ rfqId: string;
672
+ state: RfqSwapState;
673
+ /** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
674
+ * request entrypoints. This is what the manager watches to decide the swap:
675
+ * it is the only handle on the covenant whose spend witness says whether
676
+ * the swap settled or came back. */
677
+ lockupPkScript: Uint8Array;
678
+ /** The covenant behind {@link lockupPkScript}, when the caller wants the
679
+ * lockup registered with a contract manager. Optional: without it the
680
+ * manager still watches the swap on its timer, it just cannot subscribe.
681
+ * See {@link RfqSwapManagerDeps.contracts}. */
682
+ lockup?: RfqSwapLockup;
683
+ /**
684
+ * `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
685
+ * be spent by revealing a value that hashes to this, which is what makes a
686
+ * settlement provable rather than reported. For an onchain send this is
687
+ * the SAME hash the L1 `htlc` carries: one `P` unlocks both legs.
688
+ *
689
+ * True of the three corridors that exist today and only of them: a hashlock
690
+ * belongs to a CORRIDOR, and a banco-style one settles without any. The
691
+ * stored record already says so — the hash lives in `profile.hashlock`, not
692
+ * on `RfqSwapRecord` — and this field follows onto the per-corridor swap
693
+ * types when the first such corridor lands. Do not read the current shape as
694
+ * settled.
695
+ */
696
+ paymentHash: string;
697
+ /**
698
+ * `refund_locktime` from the quote, unix seconds.
699
+ *
700
+ * Whose deadline it is inverts with the direction, and so does what to do
701
+ * about it. On a send leg it is the TRADER's: the lockup is the trader's
702
+ * money and this gates the refund that takes it back, so it is a moment to
703
+ * act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
704
+ * leaf at all, and this is the moment to have claimed BEFORE.
705
+ */
706
+ refundLocktime: number;
707
+ createdAt: number;
708
+ updatedAt: number;
709
+ /** Set once the trader's own `refundWithoutReceiver` push landed. */
710
+ refundArkTxid?: string;
711
+ /**
712
+ * The ark transactions that SPENT the lockup, stamped from the chain read
713
+ * that ended the swap — `LockupFate.spends`, whichever verdict it reached.
714
+ *
715
+ * The counterparty's move, on every leg but one: a solver claim on a send,
716
+ * a solver reclaim on a receive, and — the exception — the trader's own
717
+ * claim when a receive settles. What they have in common is that no local
718
+ * action produced them, so nothing else on this record can name them:
719
+ * {@link refundArkTxid} names only a push this wallet made, and
720
+ * `claimArkTxid` only a submission it made.
721
+ *
722
+ * Stamped so a terminal record answers "which transaction ended this" from
723
+ * storage. Without it the only source is another read of the lockup — a
724
+ * network round trip per terminal swap, which is what activity correlation
725
+ * has to pay on the offline-first path where it is least affordable.
726
+ *
727
+ * Absent when the swap ended without a chain verdict, or when the indexer
728
+ * named the checkpoint but not the ark transaction — the same `arkTxid`
729
+ * `LockupSpend` declares optional, for the same reason.
730
+ */
731
+ lockupSpendArkTxids?: string[];
732
+ /** Why `state` is `failed`. */
733
+ failure?: string;
734
+ /** Why `state` is `needs_counterparty`. Distinct from {@link failure},
735
+ * which means an action was attempted and did not work. */
736
+ blockedReason?: string;
737
+ }
738
+ /** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
739
+ * claims the lockup with the preimage it learns by paying the invoice — which
740
+ * is exactly why that spend's witness is proof the payment landed. */
741
+ interface LightningSendSwap extends RfqSwapCommon {
742
+ kind: "lightning_send";
743
+ }
744
+ /** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
745
+ interface OnchainSendSwap extends RfqSwapCommon {
746
+ kind: "onchain_send";
747
+ /** The locally derived HTLC from `requestOnchainSend` — the manager reads
748
+ * `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
749
+ htlc: OnchainHtlc;
750
+ /** `profile.min_confirmations` from the quote. */
751
+ minConfirmations: number;
752
+ /** The fill's outpoint, learned on first sighting. Without it a SPENT
753
+ * HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
754
+ funding?: {
755
+ txid: string;
756
+ vout: number;
757
+ };
758
+ /** Our L1 claim's txid. */
759
+ claimTxid?: string;
760
+ }
761
+ /**
762
+ * `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
763
+ * and the TRADER claims it, and that claim is what publishes `P` and lets the
764
+ * solver settle the payer's held Lightning HTLC.
765
+ *
766
+ * Two consequences shape how this record is driven, both of them absent from
767
+ * the send legs:
768
+ *
769
+ * - **There is no trader-side refund.** Every non-claim leaf of this covenant
770
+ * is the solver's, so the manager never calls
771
+ * {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
772
+ * is not claimed is simply lost — the solver reclaims at
773
+ * {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
774
+ * held HTLC lapses.
775
+ * - **The claim is the whole swap, and it is on a deadline.** The trader must
776
+ * be online for it: covclaimd cannot claim this covenant today, so the claim
777
+ * packet's offline path does not run.
778
+ */
779
+ interface LightningReceiveSwap extends RfqSwapCommon {
780
+ kind: "lightning_receive";
781
+ /**
782
+ * What the lockup must carry — the quote's `to_amount`, captured at REQUEST
783
+ * time and persisted with the record.
784
+ *
785
+ * **Not re-derivable, and not optional.** Captured at claim time it would
786
+ * be whatever the solver funded, which is the dust-funding attack rather
787
+ * than a check on it. A record that reaches the manager without a finite
788
+ * value here is reported `needs_counterparty` and never claimed: a
789
+ * comparison against `undefined` or `NaN` is false, so an unusable
790
+ * comparand does not fail the value gate, it deletes it.
791
+ */
792
+ expectedAmount: number;
793
+ /** Our Arkade claim's txid, once submitted. Set from the callback's return
794
+ * and never from a chain read — the chain's answer is `settled`. */
795
+ claimArkTxid?: string;
796
+ }
797
+ /**
798
+ * A monitored swap.
799
+ *
800
+ * This is a live record, not a serialization format: `lockupPkScript` and
801
+ * `htlc` hold derived `Uint8Array`s, and `RfqSwapRecord` is its storable
802
+ * projection. Give the manager a {@link RfqSwapManagerDeps.repository} and it
803
+ * writes and rebuilds these itself, through
804
+ * {@link RfqSwapManager.restoreFromRepository}. A caller keeping its own store
805
+ * projects it in {@link RfqSwapManagerCallbacks.saveSwap} instead, rebuilds it
806
+ * on restart the way it was made — `lightningSendVtxoScript` /
807
+ * `receiveVtxoScript` / `onchainHtlcScript` over the quote's binding fields —
808
+ * and hands the result to {@link RfqSwapManager.start}.
809
+ *
810
+ * **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
811
+ * half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
812
+ * it also has an L1 half the trader funds and must take back itself
813
+ * (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
814
+ * deadline, a second observation seam and a second action callback. Adding the
815
+ * lockup half alone would produce a manager that silently lets that L1 refund
816
+ * window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
817
+ * by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
818
+ * refund is driven too, that corridor is better served by the request and claim
819
+ * functions directly than by a monitor that covers half of it.
820
+ */
821
+ type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
822
+ /** What the manager should do next about an onchain-send swap's L1 half. */
823
+ type OnchainSendAction =
824
+ /** Not funded yet, or not confirmed deep enough. */
825
+ "wait"
826
+ /** Funded, confirmed, and far enough from the refund leaf to claim safely. */
827
+ | "claim"
828
+ /** The claim is off the table for good; the money comes back through the
829
+ * Arkade lockup instead. */
830
+ | "claim_window_closed"
831
+ /** Our claim already landed (only the trader holds P). */
832
+ | "claimed"
833
+ /** The solver took its L1 refund — the fill is gone. */
834
+ | "swept";
835
+ /**
836
+ * The decision a "poll status, refund on timeout" loop gets wrong.
837
+ *
838
+ * {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
839
+ * claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
840
+ * "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
841
+ * SOLVER's refund, and reaching it means the trader's claim was missed. So the
842
+ * L1 claim has to be driven before it, and from it the only remaining move is
843
+ * the Arkade-side refund.
844
+ *
845
+ * There is a second, quieter trap between those two functions:
846
+ * `classifyOnchainHtlc` reports `claimable` right up until median-time-past
847
+ * reaches `refundLocktime`, while `claimOnchainFill` refuses from
848
+ * {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
849
+ * publishes P, and doing that into the counterparty's live refund window risks
850
+ * losing the race AND giving away the preimage. Driving straight off the phase
851
+ * would therefore spend that whole margin throwing `claim_window_closed` at
852
+ * every poll and never fall back. This function applies the margin, so
853
+ * "claimable" here means claimable by `claimOnchainFill` too.
854
+ */
855
+ declare function nextOnchainAction(input: {
856
+ phase: OnchainHtlcPhase;
857
+ /** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
858
+ htlcLocktime: number;
859
+ /** Unix seconds. */
860
+ now: number;
861
+ }): OnchainSendAction;
862
+ /** What the trader's own `refundWithoutReceiver` push returned, or `null` when
863
+ * the lockup held nothing to return. */
864
+ type ArkadeRefundResult = {
865
+ arkTxid: string;
866
+ amount: number;
867
+ } | null;
868
+ /**
869
+ * The money-moving half, injected. The manager decides when; these do it.
870
+ *
871
+ * Neither action gets a retry loop of its own here — see the module doc. Take
872
+ * `arkadeRefunder` for `refundArkade` rather than assembling it: it composes
873
+ * the atomic push (`findLockupVtxos` + `senderIdentityForSwapRecord` +
874
+ * `pushRefundWithoutReceiver`) and keeps the three rules below structural.
875
+ *
876
+ * Do NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
877
+ * single-swap version of this whole class and brings its own status polling
878
+ * and its own MTP retry loop, which would nest inside the manager's.
879
+ *
880
+ * Resolve the sender key through `senderIdentityForSwapRecord`: it is
881
+ * what turns "this wallet cannot sign this swap" into
882
+ * {@link RefundNotLocallyPossibleError}, which the manager reports as
883
+ * `needs_counterparty` instead of retrying for the whole refund window.
884
+ */
885
+ interface RfqSwapManagerCallbacks {
886
+ /** Build and broadcast the L1 claim. See `claimOnchainFill`. */
887
+ claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
888
+ txid: string;
889
+ }>;
890
+ /**
891
+ * Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
892
+ * to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
893
+ * already been called and `claimReceiveLockup`'s wait would only sit on a
894
+ * lockup the manager has just seen.
895
+ *
896
+ * **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
897
+ * manager checks the funded value before calling this, but that check
898
+ * decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
899
+ * it is the one that runs with nothing between it and the signature. Two
900
+ * checks, one of which is load-bearing — do not drop the inner one because
901
+ * the outer one exists.
902
+ *
903
+ * Required here, like {@link claimOnchain}: a receive swap monitored with
904
+ * nothing wired to claim it is a swap that quietly expires, and a compile
905
+ * error is the right way to learn that. A consumer that drives only the
906
+ * kinds needing neither installs
907
+ * {@link AvailableRfqSwapManagerCallbacks} instead and takes the runtime
908
+ * refusal in its place.
909
+ */
910
+ claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
911
+ /** A claim of ours is already out, so `P` is public and the value
912
+ * gate has nothing left to protect — pass this to `pushClaim` so a
913
+ * funding that arrived piecemeal can still be swept. */
914
+ partiallyClaimed: boolean;
915
+ }) => Promise<{
916
+ arkTxid: string;
917
+ amount: number;
918
+ }>;
919
+ /**
920
+ * Push `refundWithoutReceiver` for every output at the lockup. See
921
+ * `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
922
+ * called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
923
+ * solver's.
924
+ *
925
+ * Only {@link RefundNotLocallyPossibleError} is read as permanent. Every
926
+ * other throw is treated as transient and RETRIED once a poll: each attempt
927
+ * reports through `onSwapFailed` unwrapped, and the swap ends `failed` only
928
+ * past `refundLocktime + REFUND_MTP_LAG_SECONDS`. That is deliberate for a
929
+ * genuinely transient failure — `LockupNeedsRecoveryError` is the
930
+ * case it is built for, since recovery is something the caller can perform
931
+ * while the window is still open — but it means a wiring mistake that
932
+ * throws unconditionally does not read as `needs_counterparty`. It reports
933
+ * once a poll for the rest of the refund window and then ends `failed`.
934
+ * Throw {@link RefundNotLocallyPossibleError} for anything this wallet will
935
+ * never be able to do.
936
+ */
937
+ refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
938
+ /**
939
+ * Whether a local refund is possible at all — the record's secrets, against
940
+ * this wallet. Called every pass, including *before* the refund window
941
+ * opens, so a swap nobody can refund says so while the solver can still
942
+ * act, instead of at the deadline; and so restoring the right wallet lifts
943
+ * the state again. Never called for a receive swap: there is no local
944
+ * refund there to probe for.
945
+ *
946
+ * Optional: omit to answer "yes" and learn at push time, from
947
+ * {@link RefundNotLocallyPossibleError}. Local by contract — no network
948
+ * call belongs here.
949
+ */
950
+ canRefundArkade?: (swap: RfqSwap) => Promise<{
951
+ ok: true;
952
+ } | {
953
+ ok: false;
954
+ reason: string;
955
+ }>;
956
+ /**
957
+ * Persist the record. Called after any pass that changed it.
958
+ *
959
+ * With {@link RfqSwapManagerDeps.repository} wired this is the SECOND
960
+ * write of the pass, not the only one: the manager writes the canonical
961
+ * `RfqSwapRecord` itself and then calls this. Both must succeed for the
962
+ * pass to count as persisted, so a rejection here still holds waiters and
963
+ * finalization back exactly as it does without a repository. A consumer
964
+ * whose `saveSwap` writes that same repository by hand should
965
+ * drop the duplicate when it wires the dep, and keep this for genuinely
966
+ * secondary sinks: metrics, a cache, a second store.
967
+ */
968
+ saveSwap: (swap: RfqSwap) => Promise<void>;
969
+ }
970
+ /**
971
+ * What {@link RfqSwapManager.setCallbacks} accepts: the full contract, minus
972
+ * the two claims that are already kind-gated at dispatch, and minus
973
+ * `saveSwap`. A consumer driving only lightning sends reaches neither claim,
974
+ * and stubbing them to throw is not a contract — it is a lie the compiler
975
+ * waves through.
976
+ *
977
+ * `saveSwap` is optional for the same reason one step removed: with
978
+ * {@link RfqSwapManagerDeps.repository} wired the manager writes the record
979
+ * itself, so a consumer with no second sink has nothing to put here, and a
980
+ * no-op stub would be that same lie. Omitting BOTH is the documented
981
+ * process-local mode: state is kept in memory and dies with the process,
982
+ * which is what a manager with no callbacks at all already does today.
983
+ *
984
+ * The strict {@link RfqSwapManagerCallbacks} is untouched and still means
985
+ * "fully wired", so a helper that takes one and calls `claimOnchain` keeps its
986
+ * guarantee. Only the parameter widens, which every existing caller satisfies.
987
+ *
988
+ * The compile-time guarantee this trades away is bought back at runtime: a
989
+ * kind whose claim is missing blocks — non-terminal, re-evaluated every pass,
990
+ * and lifted the moment `setCallbacks` supplies it.
991
+ */
992
+ type AvailableRfqSwapManagerCallbacks = Omit<RfqSwapManagerCallbacks, "claimOnchain" | "claimLockup" | "saveSwap"> & Partial<Pick<RfqSwapManagerCallbacks, "claimOnchain" | "claimLockup" | "saveSwap">>;
993
+ /** The actions the manager executes on a caller's behalf. */
994
+ type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
995
+ interface RfqSwapManagerEvents {
996
+ /** Every state change, including ones that read as going backwards.
997
+ * `claimed -> claimable` is legal and expected on a receive swap the solver
998
+ * funds piecemeal: a lockup topped up after a claim is a new claimable
999
+ * event, and the label says so before the sweep goes out. Treat these
1000
+ * states as a description of what to do next, not as a progress bar. */
1001
+ onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
1002
+ /** Fired once, when a swap leaves monitoring `settled` or `refunded`.
1003
+ * A swap that ends `failed` reports through `onSwapFailed` instead — the
1004
+ * two are mutually exclusive. */
1005
+ onSwapCompleted?: (swap: RfqSwap) => void;
1006
+ /** Fired for any action that threw — including ones the manager will retry
1007
+ * on the next pass — and once more when the swap finally ends `failed`. */
1008
+ onSwapFailed?: (swap: RfqSwap, error: Error) => void;
1009
+ onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
1010
+ }
1011
+ type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
1012
+ type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
1013
+ type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
1014
+ type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
1015
+ interface RfqSwapManagerConfig {
1016
+ /** Drive claims and refunds automatically (default: true). With this off
1017
+ * the manager still watches and reports, so a caller can act by hand off
1018
+ * `claimable`. */
1019
+ enableAutoActions?: boolean;
1020
+ /** How often to run a pass, ms. Default 5000 — the same interval
1021
+ * `awaitOnchainFill` and `refundIfUnresolved` poll at. */
1022
+ pollIntervalMs?: number;
1023
+ /** Injected for tests; defaults to wall clock, in unix seconds — the same
1024
+ * convention `refundIfUnresolved` uses. */
1025
+ now?: () => number;
1026
+ events?: RfqSwapManagerEvents;
1027
+ }
1028
+ /** The contract-manager surface this needs, narrowed for injection — the same
1029
+ * seam style as {@link LockupSpendIndexer} and `refund.ts`'s
1030
+ * {@link RefundArkProvider}, and satisfied structurally by a real
1031
+ * `ContractManager` (`await wallet.getContractManager()`). */
1032
+ type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
1033
+ /**
1034
+ * The record store the manager writes to, narrowed to the four RFQ methods —
1035
+ * the same seam style as {@link LockupSpendIndexer} and
1036
+ * {@link SwapContractRegistry}, and satisfied structurally by a real
1037
+ * `AssetSwapRepository`.
1038
+ *
1039
+ * Narrowed rather than imported whole so this module keeps no runtime edge to
1040
+ * `repository.ts`, and so a caller with a different backing store — a server
1041
+ * process holding many wallets' records in one table — can satisfy it without
1042
+ * implementing the offer-swap and markets halves it has no use for.
1043
+ *
1044
+ * Expect TWO calls per dirty swap per poll: `getRfqSwap` then `saveRfqSwap`.
1045
+ * The read is deliberate — the store is the system of record, so a consumer's
1046
+ * own edit to a record's origin half must not be overwritten by a copy the
1047
+ * manager took at boot — but a backend where a keyed read is expensive should
1048
+ * know it is on the write path, not just the restore path.
1049
+ */
1050
+ interface RfqSwapRecordStore {
1051
+ saveRfqSwap(record: RfqSwapRecord): Promise<void>;
1052
+ getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
1053
+ getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
1054
+ removeRfqSwap(rfqId: string): Promise<void>;
1055
+ }
1056
+ /**
1057
+ * A swap was handed to the manager with a repository wired, no origin, and no
1058
+ * record already in the store.
1059
+ *
1060
+ * Thrown at the door rather than at the first write, because the write happens
1061
+ * a pass later and by then the funding is broadcast: a swap admitted here and
1062
+ * refused at `save` would be monitored, acted on, and unwritable — the record
1063
+ * would exist only in memory while its lockup held money, and a restart would
1064
+ * lose it. The remedy is to pass the origin, which the caller has: it is what
1065
+ * the request entrypoint returned.
1066
+ */
1067
+ declare class RfqSwapOriginRequired extends Error {
1068
+ /** The swap that could not be admitted. */
1069
+ readonly rfqId: string;
1070
+ constructor(rfqId: string);
1071
+ }
1072
+ /** One stored record that could not be turned back into a live swap. */
1073
+ interface RfqRestoreFailure {
1074
+ rfqId: string;
1075
+ /** Why — a covenant that does not derive the funded address, a lockup with
1076
+ * no contract row, a corridor with no handler registered. */
1077
+ error: Error;
1078
+ }
1079
+ interface RfqRestoreOptions {
1080
+ /**
1081
+ * Where each record's covenant parameters come from. Defaults to the
1082
+ * lockup's own contract row, read through
1083
+ * {@link RfqSwapManagerDeps.contracts}.
1084
+ *
1085
+ * Override it when the covenant lives somewhere else — a consumer keeping
1086
+ * its own copy of `VHTLCV2ContractHandler.serializeParams(...)`, or a
1087
+ * process with no contract manager at all. Whatever it returns is still
1088
+ * checked against the record's funded address by `rebuildRfqSwap`, so an
1089
+ * override cannot produce a swap watching the wrong covenant.
1090
+ */
1091
+ params?: (record: RfqSwapRecord) => Promise<LockupParams>;
1092
+ }
1093
+ /** What {@link RfqSwapManager.restoreFromRepository} did. Every stored record
1094
+ * is in exactly one of the three lists. */
1095
+ interface RfqRestoreResult {
1096
+ /** Rebuilt: monitored, or kept as finished when already terminal. */
1097
+ restored: RfqSwap[];
1098
+ /** Kept in the store, but not rebuildable right now. */
1099
+ failed: RfqRestoreFailure[];
1100
+ /** Removed: terminal and past `RFQ_SWAP_RETENTION_SECONDS`. */
1101
+ pruned: string[];
1102
+ }
1103
+ /** The observation seams. None is owned by the manager, and none holds keys —
1104
+ * same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
1105
+ * `RfqTransport` here on purpose: nothing this manager decides depends on the
1106
+ * solver answering (see the module doc). */
1107
+ interface RfqSwapManagerDeps {
1108
+ /** Arkade access. Required: this is how a swap's resolution is determined,
1109
+ * for both legs. */
1110
+ indexer: LockupSpendIndexer;
1111
+ /** L1 access. Required to monitor onchain-send swaps; a lightning-only
1112
+ * caller can leave it out. */
1113
+ chain?: ChainSource;
1114
+ /**
1115
+ * Where the manager persists RFQ swap records, when a caller wants it to.
1116
+ *
1117
+ * Wiring it makes the repository the CANONICAL sink: every pass that
1118
+ * changed a swap is written here as an {@link RfqSwapRecord}, and
1119
+ * {@link RfqSwapManager.restoreFromRepository} reads it back. That removes
1120
+ * the origin trap a caller otherwise hits assembling the write by hand —
1121
+ * `updateRfqSwapRecord` needs the existing record and
1122
+ * `createRfqSwapRecord` needs request-time facts the live swap does not
1123
+ * carry, so the first write of a swap cannot be composed from the swap
1124
+ * alone. The manager resolves that itself, which is what
1125
+ * {@link RfqSwapManager.addSwap}'s `origin` parameter is for.
1126
+ *
1127
+ * Optional, like every other dep here: without it the manager persists
1128
+ * through {@link RfqSwapManagerCallbacks.saveSwap} exactly as before, and
1129
+ * without either it keeps its state in memory.
1130
+ */
1131
+ repository?: RfqSwapRecordStore;
1132
+ /**
1133
+ * The wallet's contract manager, when there is one. Optional in the same
1134
+ * way {@link chain} is: a caller with no wallet, or one that only wants the
1135
+ * timer, still gets a fully working manager — the subscription is a
1136
+ * LATENCY optimization and nothing depends on it.
1137
+ *
1138
+ * Supplying it buys two things. The lockup gets REGISTERED, which is what
1139
+ * puts it in the wallet's own contract set at all — a prerequisite for
1140
+ * anything that has to act on the lockup before its batch expires, since an
1141
+ * expired lockup is swept and loses every cooperative path. And the indexer
1142
+ * PUSHES its funding and its spend, so a settlement is noticed when it
1143
+ * happens rather than up to `pollIntervalMs` later.
1144
+ *
1145
+ * Prefer `await wallet.getContractManager()` over constructing one, the way
1146
+ * `createOffer` does.
1147
+ */
1148
+ contracts?: SwapContractRegistry;
1149
+ }
1150
+ /**
1151
+ * Watches a set of live RFQ swaps and drives each to its end.
1152
+ *
1153
+ * One pass per swap, in this order, every
1154
+ * {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
1155
+ * contract event names that swap's lockup, which changes only WHEN a pass runs,
1156
+ * never what it concludes (see {@link subscribe}):
1157
+ *
1158
+ * 0. **Register the lockup**, if a contract manager was supplied and it is not
1159
+ * registered yet. Best-effort; never blocks the steps below.
1160
+ * 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
1161
+ * spend whose witness HASHES to the quote's `payment_hash` ends the swap
1162
+ * `settled`; a lockup fully spent by anything else ends it `refunded`.
1163
+ * Anything the indexer could not answer is `unknown`, which is NOT an
1164
+ * answer: the pass carries on to the steps below, whose deadlines an indexer
1165
+ * outage has no bearing on.
1166
+ * 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
1167
+ * {@link nextOnchainAction}. On a receive it is the lockup itself, and it
1168
+ * ends the pass: that leg has no step 3.
1169
+ * 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
1170
+ * and step 1 has not ended the swap. This runs for onchain-send too,
1171
+ * including after a successful claim: the trader's lockup is still funded and
1172
+ * still theirs to recover if the solver never comes for it. When no local
1173
+ * refund is possible at all — no secrets, another wallet's descriptor,
1174
+ * nothing wired — the swap reports `needs_counterparty` instead of retrying
1175
+ * a push that cannot work.
1176
+ *
1177
+ * **What step 1 proves depends on the direction.** On a send leg every non-claim
1178
+ * leaf pays the trader's own committed address or needs the trader's own
1179
+ * signature, so "spent, but not by a hash-verified claim" means the money came
1180
+ * back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
1181
+ * trader's, so the same two readings mean the opposite things — `settled` is the
1182
+ * trader's own claim landing, `refunded` is the solver taking back a lockup the
1183
+ * trader failed to claim. The read is identical; only the state docs differ.
1184
+ *
1185
+ * Two things about the receive arm that are easy to get wrong, and are asserted
1186
+ * in the tests rather than left to be inferred:
1187
+ *
1188
+ * - **A claim is matched by its preimage, never by our txid.** The covenant's
1189
+ * `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
1190
+ * claim that lands without us — covclaimd, the day it works — still pays the
1191
+ * trader and is still `settled`. Matching on the txid we submitted would turn
1192
+ * that success into an anomaly.
1193
+ * - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
1194
+ * state `claimed`.** The two words live one layer apart: the fate is the
1195
+ * chain's, the state is ours, and the state `claimed` means only that we
1196
+ * submitted something.
1197
+ */
1198
+ declare class RfqSwapManager {
1199
+ private readonly deps;
1200
+ private readonly config;
1201
+ private callbacks;
1202
+ private readonly swapUpdateListeners;
1203
+ private readonly swapCompletedListeners;
1204
+ private readonly swapFailedListeners;
1205
+ private readonly actionExecutedListeners;
1206
+ private readonly monitored;
1207
+ /** Monitored swaps by lockup script hex, so a contract event — which names
1208
+ * a script and nothing else — can find the swap it belongs to. */
1209
+ private readonly byLockupScript;
1210
+ /**
1211
+ * Swaps whose lockup registration has been SETTLED one way or another,
1212
+ * mapped to whether a contract row actually resulted. Membership is what
1213
+ * stops a per-pass retry from becoming a per-pass round trip; the value is
1214
+ * what keeps a swap that could never be registered from later trying to
1215
+ * retire a row that does not exist, which would report a spurious failure
1216
+ * on a swap that in fact succeeded.
1217
+ */
1218
+ private readonly registered;
1219
+ /**
1220
+ * Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
1221
+ * in this process. Membership stops the push from being re-issued every
1222
+ * pass — it cannot start working on its own, and re-issuing it is the
1223
+ * grind `needs_counterparty` exists to remove. Only
1224
+ * {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
1225
+ * with no probe learns again on the next start, when the wallet that can
1226
+ * sign may well have been restored.
1227
+ */
1228
+ private readonly refundRefused;
1229
+ /**
1230
+ * The last error a receive swap's claim callback threw, by rfqId.
1231
+ *
1232
+ * Kept only to tell two terminal outcomes apart once the claim window
1233
+ * shuts: a swap whose claim was attempted and kept failing ends `failed`
1234
+ * with that reason, while one that simply never became claimable ends
1235
+ * `refunded`. Without it a broken claim callback would resolve a caller's
1236
+ * {@link waitForSwapCompletion} as an ordinary unwind.
1237
+ *
1238
+ * Process-local, like {@link refundRefused}: after a restart the same swap
1239
+ * ends `refunded` instead, which costs the caller a reason and nothing else
1240
+ * — every throw was already reported through `onSwapFailed` as it happened.
1241
+ */
1242
+ private readonly lastClaimError;
1243
+ /**
1244
+ * The lockup outpoints a receive swap's claim callback has already been
1245
+ * handed, by rfqId.
1246
+ *
1247
+ * What this exists to prevent: a claim SUCCEEDS, and for the next few
1248
+ * passes the indexer still lists those outputs as unspent. Without a
1249
+ * record of what was already claimed, every one of those passes would
1250
+ * re-submit the same spend, fail against the server, and report a swap
1251
+ * that in fact worked as failing. With one, a re-claim happens only when
1252
+ * an outpoint appears that was never claimed — a lockup funded piecemeal,
1253
+ * which is legitimate and which `partiallyClaimed` exists for.
1254
+ *
1255
+ * Process-local: after a restart a swap with a live claim tries once more.
1256
+ * That is the recovery case rather than the spam one — a claim that never
1257
+ * landed leaves its outputs unspent, and one that did leaves a single
1258
+ * rejection.
1259
+ */
1260
+ private readonly claimedOutpoints;
1261
+ /** Live `onContractEvent` subscription, held so `stop()` can drop it. */
1262
+ private unsubscribeContracts;
1263
+ /** Terminal records, kept so a late {@link waitForSwapCompletion} still
1264
+ * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
1265
+ private readonly finished;
1266
+ private readonly waiters;
1267
+ /**
1268
+ * The request-time origin of each swap the manager may have to CREATE a
1269
+ * record for, by rfqId.
1270
+ *
1271
+ * Needed only for a swap the store has never seen: once a record exists,
1272
+ * `updateRfqSwapRecord` carries the origin half through and the map is
1273
+ * redundant. It is kept anyway for the swap's whole life, so a record the
1274
+ * store loses between passes is rewritten rather than lost — and dropped
1275
+ * by {@link removeSwap} and by retention, which are the two places a swap
1276
+ * stops being this manager's business.
1277
+ */
1278
+ private readonly origins;
1279
+ /** Records changed during the current pass, flushed to the repository and
1280
+ * through `saveSwap`. */
1281
+ private readonly dirty;
1282
+ /** Race guard: one action at a time per swap. */
1283
+ private readonly inProgress;
1284
+ private timer;
1285
+ private running;
1286
+ constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
1287
+ /** Wire the money-moving half. Without it the manager only watches. The
1288
+ * two claims may be omitted for a consumer that drives no kind reaching
1289
+ * them — see {@link AvailableRfqSwapManagerCallbacks}. */
1290
+ setCallbacks(callbacks: AvailableRfqSwapManagerCallbacks): void;
1291
+ onSwapUpdate(listener: SwapUpdateListener): () => void;
1292
+ onSwapCompleted(listener: SwapCompletedListener): () => void;
1293
+ onSwapFailed(listener: SwapFailedListener): () => void;
1294
+ onActionExecuted(listener: ActionExecutedListener): () => void;
1295
+ /**
1296
+ * Rebuild every stored swap and take over monitoring them.
1297
+ *
1298
+ * The composition a consumer otherwise writes by hand, and the one place
1299
+ * all four pieces meet: retention decides what to keep
1300
+ * (`shouldRetainRfqSwap`), the lockup's contract row supplies the covenant
1301
+ * (`lockupContractParams`), `rebuildRfqSwap` turns a record back into a
1302
+ * live swap, and each rebuilt swap arrives with its own origin, so nothing
1303
+ * is asked of the caller.
1304
+ *
1305
+ * **Deliberately not part of {@link start}.** A consumer that wants to
1306
+ * look at its records — count them, show them, prune and stop — is not
1307
+ * forced to start driving money to do it. Call this first and `start()`
1308
+ * after; a manager already running polls the restored swaps at once.
1309
+ *
1310
+ * Retention runs BEFORE the rebuild, so a record past
1311
+ * `RFQ_SWAP_RETENTION_SECONDS` costs no contract lookup on its way to being
1312
+ * dropped.
1313
+ *
1314
+ * **A record that cannot be rebuilt is reported, never swallowed and never
1315
+ * fatal.** `rebuildRfqSwap` throws by design when the covenant params do
1316
+ * not derive the funded address, and `lockupContractParams` throws
1317
+ * `LockupContractMissing` when the wallet has no row for the lockup — both
1318
+ * say something true about that one record, and neither is a reason to
1319
+ * strand the others. They come back in {@link RfqRestoreResult.failed}.
1320
+ */
1321
+ restoreFromRepository(options?: RfqRestoreOptions): Promise<RfqRestoreResult>;
1322
+ /**
1323
+ * Drop stored records that are terminal and past
1324
+ * `RFQ_SWAP_RETENTION_SECONDS`, and return their ids.
1325
+ *
1326
+ * `needs_counterparty` is never dropped, however old: the money is still at
1327
+ * the lockup and the counterparty's move still ends the swap. That rule
1328
+ * lives in `shouldRetainRfqSwap`, which this defers to rather than
1329
+ * restating.
1330
+ *
1331
+ * Public because retention is a caller's cadence, not the manager's: a
1332
+ * long-lived process wants it on a timer of its own, a mobile app wants it
1333
+ * at boot. {@link restoreFromRepository} runs it first, so the boot path
1334
+ * needs no separate call.
1335
+ */
1336
+ pruneRetiredSwaps(): Promise<string[]>;
1337
+ private dropRetired;
1338
+ private requireRepository;
1339
+ /** The default covenant source: the wallet's own contract row for each
1340
+ * lockup, which is where registration put it before the address could be
1341
+ * funded. */
1342
+ private paramsFromContracts;
1343
+ /**
1344
+ * Load records and begin monitoring. Runs one pass immediately — a caller
1345
+ * resuming after a restart may be well past a deadline already — then
1346
+ * every `pollIntervalMs`. Records that are already terminal are kept only
1347
+ * so {@link waitForSwapCompletion} can answer for them.
1348
+ *
1349
+ * Calling it again while running loads the records and returns rather than
1350
+ * re-arming — dropping them silently would strand a funded swap on a
1351
+ * caller's harmless double-start.
1352
+ *
1353
+ * The signature is unchanged with a {@link RfqSwapManagerDeps.repository}
1354
+ * wired; the origins are resolved from the store instead, by the same rule
1355
+ * {@link addSwap} applies. A swap the store has never seen throws
1356
+ * {@link RfqSwapOriginRequired} — hand that one to `addSwap` with its
1357
+ * origin, or restore the whole set with {@link restoreFromRepository},
1358
+ * which needs no caller input at all. Every swap is checked before any is
1359
+ * tracked, so a bad one in the list does not leave a half-loaded manager.
1360
+ */
1361
+ start(swaps?: readonly RfqSwap[]): Promise<void>;
1362
+ /**
1363
+ * Stop monitoring and clear the timer. In-flight actions are not
1364
+ * cancellable and run to completion; outstanding
1365
+ * {@link waitForSwapCompletion} promises are left pending, since
1366
+ * stop/start is a pause rather than a cancellation.
1367
+ *
1368
+ * The contract subscription is dropped too — an open stream with nothing
1369
+ * reacting to it is a leak, and {@link start} puts it back. What is NOT
1370
+ * undone is the contract registration: those rows are the wallet's, they
1371
+ * outlive this manager's lifecycle, and dropping them would unwatch a
1372
+ * lockup that is still funded.
1373
+ */
1374
+ stop(): Promise<void>;
1375
+ /**
1376
+ * Begin monitoring a swap. Polled immediately when the manager is running,
1377
+ * so a just-funded swap does not wait out a whole interval.
1378
+ *
1379
+ * `origin` is the request-time half a live swap cannot carry — the corridor,
1380
+ * the funded address, the corridor's profile, the funding txid — and it is
1381
+ * what lets the manager write this swap's FIRST record. Supply it whenever
1382
+ * a {@link RfqSwapManagerDeps.repository} is wired and the swap is new. It
1383
+ * may be omitted for a swap the store already holds a record for, which is
1384
+ * then read to confirm it; omitting it for one the store has never seen
1385
+ * throws {@link RfqSwapOriginRequired} rather than admitting a swap whose
1386
+ * record could never be written. With no repository wired the parameter is
1387
+ * inert.
1388
+ */
1389
+ addSwap(swap: RfqSwap, origin?: RfqSwapOrigin): Promise<void>;
1390
+ /**
1391
+ * Settle where this swap's record will come from, before it is monitored.
1392
+ *
1393
+ * Three ways it can be answered, in order: the caller passed an origin, one
1394
+ * is already remembered from an earlier `addSwap`, or the store holds a
1395
+ * record — which is the origin, already written. Only the last costs a read,
1396
+ * and only when the first two are absent.
1397
+ */
1398
+ private admit;
1399
+ /** Forget a swap entirely, monitored or finished.
1400
+ *
1401
+ * Its contract row is left alone: registration is a wallet-level fact about
1402
+ * a script that may still hold money, and this call says only that THIS
1403
+ * manager stops driving the swap. Retiring the row is reserved for a swap
1404
+ * that reached a terminal state, where the lockup is provably done. */
1405
+ removeSwap(rfqId: string): Promise<void>;
1406
+ /** Every swap still being monitored. */
1407
+ getPendingSwaps(): Promise<RfqSwap[]>;
1408
+ hasSwap(rfqId: string): Promise<boolean>;
1409
+ /** True while an action for this swap holds the per-swap lock. */
1410
+ isProcessing(rfqId: string): Promise<boolean>;
1411
+ getStats(): Promise<{
1412
+ isRunning: boolean;
1413
+ monitoredSwaps: number;
1414
+ finishedSwaps: number;
1415
+ inProgress: number;
1416
+ pollIntervalMs: number;
1417
+ }>;
1418
+ /**
1419
+ * Run one monitoring pass over every swap now.
1420
+ *
1421
+ * {@link start} calls this on an interval, but it is public on purpose: a
1422
+ * caller that sleeps its process (a mobile app resuming, a service worker
1423
+ * waking) wants a pass on that event rather than at the next tick. Passes
1424
+ * do not overlap per swap — the in-progress lock makes a concurrent call a
1425
+ * no-op for any swap already being worked on.
1426
+ */
1427
+ poll(): Promise<void>;
1428
+ /**
1429
+ * Resolve once this swap's PAYOUT is decided — which for onchain-send is
1430
+ * the L1 claim, not the end of the record's life: once `claimTxid` is set
1431
+ * the trader has the coins it swapped for, and what remains is the manager
1432
+ * watching the Arkade lockup close. That holds however the record is
1433
+ * labelled afterwards, `needs_counterparty` included. Lightning-send has no
1434
+ * such split and resolves at `settled`/`refunded`, and so does lightning
1435
+ * receive — see {@link isPayoutDecided} for why its own claim txid does not
1436
+ * decide it.
1437
+ *
1438
+ * Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
1439
+ * an outcome the caller asked this manager to drive, not an exception. On a
1440
+ * receive leg it is the swap being lost, which is still an answer and not
1441
+ * an error — read `state`, do not infer success from resolution.
1442
+ */
1443
+ waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
1444
+ private track;
1445
+ /** Drops the swap from BOTH indexes. The event index is the one that stops
1446
+ * a late event finding a swap that is gone; `pollSwap`'s own
1447
+ * `monitored` check would also catch it, and deliberately still does —
1448
+ * either alone is sufficient, which is what keeps a future change to one of
1449
+ * them from silently re-driving a cancelled swap. */
1450
+ private untrack;
1451
+ /**
1452
+ * Turn the indexer's push into an extra reason to run a pass — and nothing
1453
+ * more.
1454
+ *
1455
+ * **This is deliberately not a source of truth.** An event names a script;
1456
+ * the reaction is to run the ordinary pass for the swap at that script, and
1457
+ * that pass re-reads the lockup through {@link readLockupFate} exactly as
1458
+ * the timer's pass does. So an event that is missed, duplicated, reordered
1459
+ * or outright FORGED can only cost or save latency — it can never change
1460
+ * what this manager believes about a swap, and it can never on its own
1461
+ * cause a claim or a refund. That property is what makes it safe to bolt a
1462
+ * best-effort stream onto a money path, and it must survive any future
1463
+ * change here: the moment an event is BELIEVED rather than merely acted on,
1464
+ * a relay outage becomes a correctness problem instead of a latency one.
1465
+ *
1466
+ * The timer stays armed regardless, and is the failsafe. Every deadline
1467
+ * that moves money — `refundLocktime`, the L1 claim window — is an absolute
1468
+ * timelock that passes whether or not a single event ever arrives.
1469
+ */
1470
+ private subscribe;
1471
+ /**
1472
+ * Register this swap's lockup with the wallet's contract manager, once.
1473
+ *
1474
+ * The backstop, not the primary site: `requestLightningSend` /
1475
+ * `requestOnchainSend` register before the caller can fund, so this covers
1476
+ * swaps whose records predate that — and costs nothing when it does not,
1477
+ * since `createContract` is first-writer-wins.
1478
+ *
1479
+ * Best-effort by design: a failure here is reported and retried on the next
1480
+ * pass, and never aborts the pass it is part of. Registration buys latency
1481
+ * and puts the lockup in the wallet's contract set; it decides nothing. The
1482
+ * money path below it reads the indexer directly and is gated on timelocks
1483
+ * that a missing contract row has no bearing on, so failing the pass over
1484
+ * this would trade a real deadline for a bookkeeping one.
1485
+ */
1486
+ private ensureRegistered;
1487
+ /** Stop watching a finished swap's lockup. Retained, not deleted: the row
1488
+ * is what keeps the lockup's own VTXOs annotatable and its history
1489
+ * readable, while `retained` is what drops it from the subscription and
1490
+ * the poll — a settled swap that stayed watched would cost the wallet a
1491
+ * script for its whole life. Best-effort — the swap is over either way. */
1492
+ private retireContract;
1493
+ private arm;
1494
+ private pollSwap;
1495
+ private runPass;
1496
+ /**
1497
+ * The receive leg's whole state machine: claim the solver-funded lockup
1498
+ * while the window is open, and recognise the shapes in which it can be
1499
+ * lost.
1500
+ *
1501
+ * **The window closes at `refundLocktime`, on wall clock, with no margin.**
1502
+ * Both halves of that are deliberate. It closes there because publishing
1503
+ * `P` into the solver's live refund window risks losing the race and
1504
+ * handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
1505
+ * guards on the L1 side. It takes no margin because the two situations are
1506
+ * not alike: that one budgets for confirmation depth, while this claim is an
1507
+ * offchain spend that lands in seconds. Wall clock is already the
1508
+ * conservative reading — the solver's leaf is a CLTV, which matures against
1509
+ * median-time-past, and MTP trails wall clock — so the real window extends
1510
+ * PAST this deadline rather than ending before it. Every second of margin
1511
+ * subtracted here is a second of live claim window given away for nothing.
1512
+ *
1513
+ * **The trader has no move after it.** Nothing here can take the lockup
1514
+ * back, so once the window shuts the swap is the solver's to resolve and
1515
+ * this manager's job is to watch it happen and then stop.
1516
+ */
1517
+ private driveReceiveClaim;
1518
+ /**
1519
+ * Claim what the solver funded, once it is enough.
1520
+ *
1521
+ * The value gate here decides WHEN to act. `pushClaim`'s decides whether
1522
+ * `P` is published, and runs with nothing between it and the signature —
1523
+ * the check that matters is the inner one, and this is not a reason to
1524
+ * relax it.
1525
+ */
1526
+ private claimIfFunded;
1527
+ /** `handled` ends the pass; `continue` falls through to the refund gate. */
1528
+ private driveOnchain;
1529
+ private driveArkadeRefund;
1530
+ /** Whether any of these outputs has never been handed to the claim
1531
+ * callback — the only reason to claim a lockup a second time. */
1532
+ private hasUnclaimedOutpoint;
1533
+ private rememberClaimed;
1534
+ /**
1535
+ * L1 progress, which past the refund window must not overwrite a refusal.
1536
+ * The two halves are independent — a claimed fill says nothing about
1537
+ * whether this wallet can take the Arkade lockup back — and `claimed` is
1538
+ * re-asserted from chain on every pass, so without this a blocked swap
1539
+ * would flip between the two states forever. The claim itself always runs;
1540
+ * only the label defers, and only once the refund is the live half.
1541
+ */
1542
+ private setOnchainState;
1543
+ /** The probe's refusal reason, or `undefined` when a local refund is
1544
+ * possible as far as anyone here can tell. A probe that throws is treated
1545
+ * as a refusal: a capability check that cannot answer is not a yes. */
1546
+ private probeRefusal;
1547
+ /** Report that no local refund will happen, without ending the swap. */
1548
+ private block;
1549
+ /** The way back out, taken as soon as the swap becomes actionable again.
1550
+ * Back to what the record can prove, not to `pending` unconditionally: a
1551
+ * swap that already made its claim has a txid for it, and reporting that
1552
+ * swap as `pending` would un-say something true. */
1553
+ private unblock;
1554
+ /**
1555
+ * Record which ark transactions ended the lockup.
1556
+ *
1557
+ * Only the ones the indexer actually named: `LockupSpend.arkTxid` is
1558
+ * optional, and a checkpoint txid is not what history correlates on — a
1559
+ * record carrying one would name a transaction the wallet's own activity
1560
+ * never shows. Fewer txids is the right failure here.
1561
+ *
1562
+ * Assigned rather than merged: the fate is one read of the whole lockup,
1563
+ * so it is the complete answer for this swap, and a swap only reaches a
1564
+ * verdict once.
1565
+ */
1566
+ private stampLockupSpends;
1567
+ private touch;
1568
+ private setState;
1569
+ /** Terminal failure. The `onSwapFailed` emission is left to
1570
+ * {@link finalize}, so this does not double-report. */
1571
+ private fail;
1572
+ private emitFailed;
1573
+ private emitAction;
1574
+ /**
1575
+ * Flush a changed record to every sink that is wired, and say whether all
1576
+ * of them took it.
1577
+ *
1578
+ * Up to two writes, in this order: the canonical `RfqSwapRecord` to
1579
+ * {@link RfqSwapManagerDeps.repository}, then
1580
+ * {@link RfqSwapManagerCallbacks.saveSwap}. Both gate — a rejection from
1581
+ * either leaves the record dirty and monitored, so waiters stay unsettled
1582
+ * and a terminal swap is not finalized until the write it claims lands.
1583
+ * That is exactly today's rule for `saveSwap`, applied to whichever sinks
1584
+ * exist; wiring the repository does not weaken it.
1585
+ *
1586
+ * The canonical write goes FIRST and a failure there skips the second.
1587
+ * `saveSwap` is a projection of the record, and projecting a state the
1588
+ * record of record has just refused would leave the secondary sink ahead
1589
+ * of the primary — the one ordering that survives no restart.
1590
+ *
1591
+ * With neither wired the state is process-local, which is what a manager
1592
+ * with no callbacks has always done.
1593
+ */
1594
+ private save;
1595
+ /** The canonical write. True when there is no repository to write to. */
1596
+ private saveRecord;
1597
+ private originOrThrow;
1598
+ /**
1599
+ * Drop a terminal swap from monitoring and report it exactly once.
1600
+ *
1601
+ * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
1602
+ * Boltz's manager, which fires completion for every swap that leaves
1603
+ * monitoring including the failed ones — a listener named "completed" that
1604
+ * also fires on failure is a trap worth not inheriting.
1605
+ */
1606
+ private finalize;
1607
+ private settleWaiters;
1608
+ }
1609
+ /** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
1610
+ * trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
1611
+ * receive — or the ark txid for a refund the trader pushed; a solver-side
1612
+ * settlement or refund carries none, and a receive swap that ended `refunded`
1613
+ * carries none either, however far its claim got (see {@link outcomeOf}). So
1614
+ * a `txid` here always names something that happened, and `state` remains the
1615
+ * only thing to read for whether the swap paid out. */
1616
+ interface RfqSwapOutcome {
1617
+ state: RfqSwapState;
1618
+ txid?: string;
1619
+ }
1620
+
1621
+ /**
1622
+ * The swap kinds this projection covers — all three the manager monitors.
1623
+ *
1624
+ * `onchain_send` carries an L1 half nothing else can rebuild. Its Arkade lockup
1625
+ * has a contract row like the others, but the HTLC is Bitcoin L1, not an Arkade
1626
+ * contract, so no row exists for it; and `OnchainHtlc` exposes only derived
1627
+ * values — `address`, `pkScript`, `leaves`, `controlBlocks` — never the
1628
+ * `claimKey`/`refundKey` `onchainHtlcScript` takes as inputs. So they ride in
1629
+ * that corridor's {@link RfqSwapOrigin.profile}, and without them a restored
1630
+ * swap would let its L1 refund window pass unwatched.
1631
+ */
1632
+ type PersistableRfqSwap = LightningSendSwap | LightningReceiveSwap | OnchainSendSwap;
1633
+ /**
1634
+ * The serialized covenant parameters a rebuild is given.
1635
+ *
1636
+ * `VHTLCV2ContractHandler`'s own wire shape — what `serializeParams` writes and
1637
+ * `createScript` reads — which is exactly what a lockup's contract row stores
1638
+ * under `params`.
1639
+ */
1640
+ type LockupParams = Record<string, string>;
1641
+ /**
1642
+ * How long a retired swap's record is kept, in SECONDS.
1643
+ *
1644
+ * Terminal records are history, not garbage: the covenant's spender is a
1645
+ * transaction the wallet never signed, so its own history cannot reconstruct
1646
+ * them. Kept for a month, then dropped so a hot wallet's store stays bounded.
1647
+ *
1648
+ * Seconds, not milliseconds, because that is the unit `RfqSwap.updatedAt`
1649
+ * carries; the manager stamps it from `RfqSwapManagerConfig.now`, which is
1650
+ * "wall clock, in unix seconds". Comparing it against `Date.now()` would drop
1651
+ * every terminal record after ~43 minutes.
1652
+ */
1653
+ declare const RFQ_SWAP_RETENTION_SECONDS: number;
1654
+ /** The immutable request-time half, and only what EVERY corridor has. Hex for
1655
+ * everything binary, so the record is plain JSON and survives any
1656
+ * structured-clone backend unchanged. */
1657
+ interface RfqSwapOrigin {
1658
+ /**
1659
+ * Which corridor this is. Resolves the handler that owns {@link profile};
1660
+ * see `rfqCorridor.ts`.
1661
+ *
1662
+ * The manager's own union, not an open string: `RfqSwapManager` branches on
1663
+ * `kind` to decide what to drive, so a corridor it does not know could be
1664
+ * persisted and rebuilt here and then never driven — which is the failure
1665
+ * this whole file is arranged to make impossible.
1666
+ */
1667
+ kind: PersistableRfqSwap["kind"];
1668
+ /**
1669
+ * The Arkade address that was funded.
1670
+ *
1671
+ * Both the swap's handle on its covenant — {@link lockupContractParams}
1672
+ * looks the contract row up by the script it decodes to — and, being taken
1673
+ * from the entry point rather than re-derived, the check that the
1674
+ * parameters a caller supplies belong to THIS swap.
1675
+ */
1676
+ lockupAddress: string;
1677
+ /**
1678
+ * The corridor's own half, as plain JSON — written by the caller from the
1679
+ * request result, kept current by the handler's `project`.
1680
+ *
1681
+ * Opaque here on purpose. Nothing in this file, the repository or the
1682
+ * IndexedDB store interprets it, which is what lets a new corridor ship
1683
+ * without touching any of them. It carries the corridor's keys as well as
1684
+ * its state: `signer` (which wallet key signs this leg) and, on a corridor
1685
+ * locked to a preimage, `hashlock` — see `rfqProfileParts.ts`, and write
1686
+ * both with `rfqSecretsProfile` rather than by hand.
1687
+ *
1688
+ * Not a consumer scratchpad: every write merges it as `{ ...profile,
1689
+ * ...handler.project(swap) }`, so a consumer key colliding with one the
1690
+ * handler projects is silently overwritten on every pass.
1691
+ */
1692
+ profile: Record<string, unknown>;
1693
+ /** Consumer display metadata. The rebuild ignores it — `RfqSwapCommon`
1694
+ * carries no amount of its own. */
1695
+ amount?: number;
1696
+ /**
1697
+ * The ark transaction that funded {@link lockupAddress}.
1698
+ *
1699
+ * Origin, not manager state: the caller broadcasts the funding and knows
1700
+ * its txid, while the manager watches the lockup by script and never
1701
+ * learns it. So it is written once at record creation, like {@link amount},
1702
+ * and no corridor `project` emits it.
1703
+ */
1704
+ fundingArkTxid?: string;
1705
+ }
1706
+ /** The stored record: the origin plus the manager's mutable state. */
1707
+ interface RfqSwapRecord extends RfqSwapOrigin {
1708
+ rfqId: string;
1709
+ state: RfqSwapState;
1710
+ createdAt: number;
1711
+ updatedAt: number;
1712
+ refundArkTxid?: string;
1713
+ /** The ark transactions that spent the lockup, stamped by the manager from
1714
+ * the chain read that ended the swap. See
1715
+ * `RfqSwapCommon.lockupSpendArkTxids`. */
1716
+ lockupSpendArkTxids?: string[];
1717
+ failure?: string;
1718
+ blockedReason?: string;
1719
+ }
1720
+ /** First write, at the moment the caller hands the swap to the manager. */
1721
+ declare function createRfqSwapRecord(origin: RfqSwapOrigin, swap: PersistableRfqSwap): RfqSwapRecord;
1722
+ /**
1723
+ * Every later write. The origin half is carried through untouched.
1724
+ *
1725
+ * The mutable half is REPLACED, not merged. `managerState` omits a key the live
1726
+ * swap no longer carries, so spreading it over the old record could only ever
1727
+ * set these fields, never clear them. The manager clears them on purpose: it
1728
+ * deletes `blockedReason` when a swap leaves `needs_counterparty`, precisely
1729
+ * because a stale `blockedReason` reads as a live refusal.
1730
+ */
1731
+ declare function updateRfqSwapRecord(record: RfqSwapRecord, swap: PersistableRfqSwap): RfqSwapRecord;
1732
+ /**
1733
+ * The immutable half of a stored record, on its own.
1734
+ *
1735
+ * A record IS an origin plus manager state, so `record` where an
1736
+ * {@link RfqSwapOrigin} is wanted type-checks — and is a bug. Spread into
1737
+ * {@link createRfqSwapRecord} it carries the OLD state's `failure`,
1738
+ * `blockedReason` and `refundArkTxid` past `managerState`, which omits a field
1739
+ * the live swap no longer has and therefore cannot clear one. That is the same
1740
+ * trap {@link updateRfqSwapRecord} strips those three fields to avoid; this is
1741
+ * how a caller holding only a record gets an origin that is safe to keep.
1742
+ *
1743
+ * What `RfqSwapManager.restoreFromRepository` remembers for each record it
1744
+ * rebuilds, so a later write can create the record again if the store lost it.
1745
+ */
1746
+ declare function rfqSwapOriginOf(record: RfqSwapRecord): RfqSwapOrigin;
1747
+ /**
1748
+ * Rebuild the live record. Pure, and synchronous, given the covenant's
1749
+ * parameters.
1750
+ *
1751
+ * Hand the result to {@link RfqSwapManager.start}. Take `params` from the
1752
+ * lockup's contract row — {@link lockupContractParams} is the one-liner — or
1753
+ * from a copy of `VHTLCV2ContractHandler.serializeParams(script.options)` a
1754
+ * consumer keeps itself; either way they are checked against the address that
1755
+ * was actually funded, so the `lockupPkScript` this produces is the one the
1756
+ * funded lockup is keyed by.
1757
+ */
1758
+ declare function rebuildRfqSwap(record: RfqSwapRecord, params: LockupParams): PersistableRfqSwap;
1759
+ /**
1760
+ * Whether a retired swap's record should be kept.
1761
+ *
1762
+ * `needs_counterparty` is deliberately not terminal and is never dropped: the
1763
+ * money is still at the lockup, the counterparty's move is still what ends the
1764
+ * swap, and the refusal is re-checked every pass; restoring the right wallet
1765
+ * returns it to `pending`.
1766
+ *
1767
+ * @param now Current time in **unix seconds**; the same unit as
1768
+ * `RfqSwap.updatedAt`, which the manager stamps from
1769
+ * `RfqSwapManagerConfig.now`. Pass `Math.floor(Date.now() / 1000)`, never
1770
+ * `Date.now()`: milliseconds against a seconds window would retire every
1771
+ * terminal record after ~43 minutes.
1772
+ */
1773
+ declare function shouldRetainRfqSwap(record: RfqSwapRecord, now: number): boolean;
1774
+
1775
+ /** A registry discovery result held for reuse. Refetchable — unlike a swap
1776
+ * record, losing it costs one network round trip — but it must survive a cold
1777
+ * boot: serving it stale is what keeps quoting alive while a registry is down. */
1778
+ interface MarketsCacheEntry {
1779
+ markets: DiscoveredMarket[];
1780
+ fetchedAt: number;
1781
+ }
1782
+ /**
1783
+ * Everything the package persists, following the monorepo repository
1784
+ * convention (versioned interface, AsyncDisposable, one backend per
1785
+ * platform — see the Boltz plugin's SwapRepository). Consumers construct
1786
+ * exactly one of these; there is no second storage seam.
1787
+ *
1788
+ * Durable records (swaps) and rebuildable state (the restore scan's txid
1789
+ * cursor, the markets cache) live side by side because they share a
1790
+ * lifetime: all three belong to one wallet on one device, and a consumer
1791
+ * that wipes one wants all three gone.
1792
+ *
1793
+ * ponytail: no query filters — every consumer reads all swaps and filters
1794
+ * in memory; mirror the Boltz plugin's GetSwapsFilter when a consumer needs
1795
+ * subset queries.
1796
+ */
1797
+ interface AssetSwapRepository extends AsyncDisposable {
1798
+ /** 4 adds `getRfqSwap`. 3 added the other RFQ methods below; 2 was the
1799
+ * released shape — swaps, scan cursor, markets, with `preimageSaltHex` on
1800
+ * the swap record — so an implementor built against either cannot satisfy
1801
+ * this one silently. */
1802
+ readonly version: 4;
1803
+ /** Insert or replace a swap by id. Store the record whole: `preimageHex`
1804
+ * and `preimageSaltHex` both leave the swap unclaimable if a field-mapped
1805
+ * backend drops them — the first is the only claim secret of a swap whose
1806
+ * signer cannot derive, the second the public input every other static
1807
+ * wallet's preimage derives from.
1808
+ *
1809
+ * Records must be **JSON-safe**: the SQLite and Realm backends serialize
1810
+ * the record to JSON, so a `Date` in a consumer-added field comes back a
1811
+ * string, a `Set`/`Map` comes back empty, and a `bigint` throws here —
1812
+ * none of which happens on IndexedDB's structured clone. `AssetSwap` as
1813
+ * declared is JSON-safe; keep added fields that way. */
1814
+ saveSwap(swap: AssetSwap): Promise<void>;
1815
+ /** All stored swaps, in no particular order — `getAssetSwaps` is the
1816
+ * canonical newest-first read. */
1817
+ getAllSwaps(): Promise<AssetSwap[]>;
1818
+ /**
1819
+ * Insert or replace a monitored RFQ swap by `rfqId`.
1820
+ *
1821
+ * Store the record WHOLE. Every field is a covenant tree parameter or the
1822
+ * manager's own state, and a field-mapped backend that drops one round-trips
1823
+ * a record whose covenant `rebuildRfqSwap` cannot reproduce — which surfaces
1824
+ * as a refund that cannot be signed, long after the write.
1825
+ */
1826
+ saveRfqSwap(record: RfqSwapRecord): Promise<void>;
1827
+ /** One record by key. `undefined` on a miss — retention prunes terminal
1828
+ * records, so absence is ordinary and not an error. */
1829
+ getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
1830
+ /** Every stored RFQ swap record, in no particular order. */
1831
+ getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
1832
+ /** Drop one, once it is past retention — see `shouldRetainRfqSwap`. */
1833
+ removeRfqSwap(rfqId: string): Promise<void>;
1834
+ /** Sent txids already checked for offer packets (see restore.ts). */
1835
+ getScannedTxids(): Promise<Set<string>>;
1836
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
1837
+ /** Cached registry markets, or undefined on a miss. */
1838
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
1839
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
1840
+ clear(): Promise<void>;
1841
+ }
1842
+ declare class InMemoryAssetSwapRepository implements AssetSwapRepository {
1843
+ readonly version: 4;
1844
+ private readonly swaps;
1845
+ private readonly rfqSwaps;
1846
+ private readonly scanned;
1847
+ private readonly markets;
1848
+ saveSwap(swap: AssetSwap): Promise<void>;
1849
+ getAllSwaps(): Promise<AssetSwap[]>;
1850
+ saveRfqSwap(record: RfqSwapRecord): Promise<void>;
1851
+ getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
1852
+ getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
1853
+ removeRfqSwap(rfqId: string): Promise<void>;
1854
+ getScannedTxids(): Promise<Set<string>>;
1855
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
1856
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
1857
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
1858
+ clear(): Promise<void>;
1859
+ [Symbol.asyncDispose](): Promise<void>;
1860
+ }
1861
+
1862
+ export { isRfqSwapTerminal as $, type AssetSwapRepository as A, BTC_ASSET_ID as B, type RfqSwapActionName as C, type RfqSwapLockup as D, RfqSwapManager as E, type RfqSwapManagerCallbacks as F, type RfqSwapManagerConfig as G, type RfqSwapManagerDeps as H, InMemoryAssetSwapRepository as I, type RfqSwapManagerEvents as J, type RfqSwapOrigin as K, type LockupVtxo as L, type MarketsCacheEntry as M, RfqSwapOriginRequired as N, type OnchainSendAction as O, type PersistableRfqSwap as P, type RfqSwapOutcome as Q, type RfqSwapRecord as R, type SwapSecretsProjection as S, type RfqSwapRecordStore as T, type SwapContractRegistry as U, addAssetSwap as V, awaitRfqResolution as W, createRfqSwapRecord as X, findLockupVtxos as Y, getAssetSwaps as Z, getAssetSwapsOrThrow as _, type AssetSwap as a, isRfqTerminal as a0, nextOnchainAction as a1, preimageForSwapRecord as a2, pushRefundWithoutReceiver as a3, readLockupFate as a4, rebuildRfqSwap as a5, refundIfUnresolved as a6, rfqSwapOriginOf as a7, shouldRetainRfqSwap as a8, swapSecretsToRecord as a9, updateAssetSwap as aa, updateAssetSwapBestEffort as ab, updateRfqSwapRecord as ac, type RefundArkProvider as b, type RefundIndexer as c, type RfqSwap as d, type ArkadeRefundResult as e, type LockupSpendIndexer as f, type RfqSwapState as g, type AssetSwapStatus as h, type AvailableRfqSwapManagerCallbacks as i, type LightningReceiveSwap as j, type LightningSendSwap as k, type LockupFate as l, LockupNeedsRecoveryError as m, type LockupParams as n, type LockupSpend as o, type OnchainSendSwap as p, type PreimageBlockedReason as q, PreimageNotRecoverableError as r, REFUND_MTP_LAG_SECONDS as s, RFQ_RESOLVED_STATES as t, RFQ_SWAP_RETENTION_SECONDS as u, RFQ_SWAP_TERMINAL_STATES as v, type RefundOutcome as w, type RfqRestoreFailure as x, type RfqRestoreOptions as y, type RfqRestoreResult as z };