@arkade-os/swap 0.0.7 → 0.0.9

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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,186 @@
1
- import { asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, RestArkProvider, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
2
- import { A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry } from './repository-BwnZ8N62.js';
3
- export { b as AssetSwapStatus, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, P as PreimageBlockedReason, c as PreimageNotRecoverableError, S as SwapSecretsProjection, d as addAssetSwap, g as getAssetSwaps, e as getAssetSwapsOrThrow, p as preimageForSwapRecord, s as swapSecretsToRecord, u as updateAssetSwap, f as updateAssetSwapBestEffort } from './repository-BwnZ8N62.js';
1
+ import { ProvisionedKey, ProvisionedClaimSecret, asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
2
+ import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-DIAr5XYk.js';
3
+ export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-DIAr5XYk.js';
4
4
  import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
5
- import { R as RfqStatus, a as RfqTransport, O as OnchainHtlc, C as ChainSource, b as ChainUtxo, c as OnchainHtlcPhase } from './rfq-DfT9dAss.js';
6
- export { A as ARKADE_ASSET, d as ARKADE_BTC, e as AddressMismatch, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, M as MAX_MIN_CONFIRMATIONS, h as MIN_CLAIM_WINDOW_SECONDS, i as MIN_HEADROOM_SECONDS, j as ONCHAIN_BTC, k as ONCHAIN_CLAIM_MARGIN_SECONDS, l as ONCHAIN_DUST_SATS, m as ONCHAIN_ORDER_MARGIN_SECONDS, n as ONCHAIN_RECEIVE_PAIR, o as ONCHAIN_SECONDS_PER_BLOCK, p as ONCHAIN_SEND_PAIR, q as OnchainHtlcParams, r as OnchainNetwork, s as RFQ_TERMINAL_STATES, t as RelaySocket, u as RfqQuote, v as RfqRefusalReason, S as SOLO_REFUND_HEADROOM_SECONDS, w as SwapRefusal, x as arkadeAssetLeg, y as arkadeSwapRequest, z as assertFundable, B as assertReceivable, D as awaitOnchainFill, E as buildHtlcClaim, F as buildHtlcRefund, G as claimOnchainFill, J as classifyOnchainHtlc, K as deriveLightningReceive, N as deriveOnchainReceive, P as deriveOnchainSend, Q as extractPreimage, T as httpTransport, U as lightningReceiveRequest, V as lightningSendRequest, W as lightningSendVtxoScript, X as newPreimage, Y as newRfqId, Z as offerTermsFromQuote, _ as onchainHtlcScript, $ as onchainReceiveRequest, a0 as onchainSendRequest, a1 as paymentHashOf, a2 as receiveVtxoScript, a3 as relayTransport, a4 as requestLightningReceive, a5 as requestLightningSend, a6 as requestOnchainReceive, a7 as requestOnchainSend, a8 as rfqPair, a9 as unilateralClaimDelay, aa as unilateralRefundDelay, ab as unilateralRefundWithoutReceiverDelay, ac as verifyLockupAddress, ad as verifyReceiveInvoice } from './rfq-DfT9dAss.js';
5
+ import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-hbzhTWHT.js';
6
+ export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-hbzhTWHT.js';
7
+
8
+ /**
9
+ * Which wallet key signs this leg. Stored at `profile.signer`.
10
+ *
11
+ * Independent of any hashlock: it feeds `senderIdentityForSwapRecord` on the
12
+ * refund side, and a corridor that settles with no preimage at all still needs
13
+ * it. A corridor whose leg this wallet never signs omits the key entirely — as
14
+ * with everything else here, absent rather than blank.
15
+ */
16
+ interface RfqSignerProjection {
17
+ /** Public. The wallet re-derives the signer from it; no key material is at
18
+ * rest. */
19
+ signingDescriptor: string;
20
+ }
21
+ /**
22
+ * What a leg locked to a preimage records about the LOCK. Stored at
23
+ * `profile.hashlock`, and absent entirely from a corridor that has none.
24
+ *
25
+ * `paymentHash` is identity, not capability — `lightning_send` carries one and
26
+ * can never open it. The preimage fields are the capability, and only a corridor
27
+ * where WE claim ever writes them: `provisionClaimSecret` produces that arm,
28
+ * `provisionRefundKey` does not.
29
+ */
30
+ type RfqHashlockProjection = Omit<SwapSecretsProjection, "signingDescriptor"> & {
31
+ /** `sha256(P)`, hex. Not recoverable from the covenant, which binds
32
+ * `hash160(P)`. */
33
+ paymentHash: string;
34
+ };
35
+ /**
36
+ * The composed view `preimageForSwapRecord` reads — the signer's descriptor plus
37
+ * the hashlock's. **Never a stored shape**: nothing writes this object,
38
+ * {@link rfqClaimSecretOf} assembles it from the two keys above, which is what
39
+ * keeps the descriptor stored once.
40
+ *
41
+ * Structurally `SwapSecretsProjection & { paymentHash }`, which is exactly that
42
+ * helper's parameter.
43
+ */
44
+ type RfqClaimSecretProjection = RfqSignerProjection & RfqHashlockProjection;
45
+ /**
46
+ * The corridor-owned counterpart of `onchainSendProfile`: the one supported way
47
+ * to turn a provisioned secret into stored profile keys.
48
+ *
49
+ * Writes what the provisioning result actually has. Omit `paymentHash` and you
50
+ * get `signer` alone — which is what a non-hashlock corridor calls, and why it
51
+ * never has to reach for `hashlock`.
52
+ */
53
+ declare const rfqSecretsProfile: (secrets: ProvisionedKey | ProvisionedClaimSecret, paymentHash?: string) => {
54
+ signer: RfqSignerProjection;
55
+ hashlock?: RfqHashlockProjection;
56
+ };
57
+ /**
58
+ * The stored signer projection, for `senderIdentityForSwapRecord`.
59
+ *
60
+ * `undefined` ONLY when the profile carries no `signer` key — a corridor whose
61
+ * leg this wallet does not sign. A `signer` that is present and unusable throws:
62
+ * "this corridor has no local signer" and "this record came back corrupt" are
63
+ * different answers, and `senderIdentityForSwapRecord` turns the first into a
64
+ * permanent `RefundNotLocallyPossibleError("no-secrets")` the manager acts on.
65
+ * Handing it a silently-emptied projection would report "no local refund is
66
+ * possible" for a storage bug.
67
+ */
68
+ declare const rfqSignerOf: (record: RfqSwapRecord) => RfqSignerProjection | undefined;
69
+ /**
70
+ * The claim inputs, or `undefined` when this record's corridor cannot produce P
71
+ * — no hashlock, or a leg we refund rather than claim.
72
+ *
73
+ * Answered by the corridor's handler (`claimSecret`), not by a kind list here:
74
+ * whether a leg claims is the corridor's fact.
75
+ *
76
+ * When the handler DOES claim, this validates and throws
77
+ * `PreimageNotRecoverableError("malformed-record")` rather than returning a
78
+ * partial projection — which `preimageForSwapRecord` would claim with, its hash
79
+ * check being conditional on the very field that went missing.
80
+ */
81
+ declare const rfqClaimSecretOf: (record: RfqSwapRecord) => RfqClaimSecretProjection | undefined;
82
+
83
+ /**
84
+ * `arkade:BTC->lightning:BTC`. Nothing beyond its keys and the covenant: the
85
+ * solver claims the lockup, the trader's only move is the refund, and the rest
86
+ * of the leg is fully described by the contract row.
87
+ *
88
+ * The one leg with a hashlock it can never open — P belongs to the payee — so
89
+ * `hashlock` here is `{ paymentHash }` alone and `signer` holds a REFUND key.
90
+ */
91
+ interface LightningSendProfile extends Record<string, unknown> {
92
+ signer: RfqSignerProjection;
93
+ hashlock: RfqHashlockProjection;
94
+ }
95
+ /** `lightning:BTC->arkade:BTC`. */
96
+ interface LightningReceiveProfile extends Record<string, unknown> {
97
+ signer: RfqSignerProjection;
98
+ hashlock: RfqHashlockProjection;
99
+ /** The quote's `to_amount`, captured at REQUEST time. */
100
+ expectedAmount: number;
101
+ /** Where the claim pays. */
102
+ payoutAddress: string;
103
+ /**
104
+ * Our Arkade claim's txid, once submitted.
105
+ *
106
+ * Here rather than on the record's common half, which is for fields all
107
+ * three legs carry: this one is the receive leg's alone, the counterpart of
108
+ * the onchain leg's `claimTxid`. Restoring without it re-arms the value gate
109
+ * against a lockup we have already partly claimed — `claimIfFunded` reads
110
+ * `partiallyClaimed` off exactly this — so the remainder is refused over a
111
+ * preimage that is already public, and a swap that did claim is relabelled
112
+ * `needs_counterparty` once its window shuts.
113
+ */
114
+ claimArkTxid?: string;
115
+ }
116
+ /** `arkade:BTC->onchain:BTC`. */
117
+ interface OnchainSendProfile extends Record<string, unknown> {
118
+ signer: RfqSignerProjection;
119
+ hashlock: RfqHashlockProjection;
120
+ /** The trader's L1 claim key. */
121
+ claimKey: string;
122
+ /** The solver's L1 key, from `profile.htlc_pubkey`. */
123
+ refundKey: string;
124
+ /** `profile.htlc_locktime` — the trader's L1 recourse deadline, distinct
125
+ * from the arkade lockup's `refundLocktime`. */
126
+ htlcLocktime: number;
127
+ network: OnchainNetwork;
128
+ /**
129
+ * The L1 address the fill was expected at — `htlc.address` from
130
+ * `requestOnchainSend`, which `deriveOnchainSend` has already checked
131
+ * against the quote's own `profile.htlc_address`.
132
+ *
133
+ * The counterpart of the record's `lockupAddress`, and here for the same
134
+ * reason: the inputs above and this address reach the record by independent
135
+ * routes, so requiring the rebuild to reproduce it is what catches a
136
+ * parameter that came back wrong. The arkade lockup gets that check from
137
+ * `rebuildRfqSwap`; this leg, the one whose parameters have no second source
138
+ * anywhere, would otherwise get none — and a swapped or corrupted key
139
+ * derives another perfectly valid HTLC, at an address nobody funded.
140
+ */
141
+ htlcAddress: string;
142
+ /** `profile.min_confirmations`; gates when the fill is claimable. */
143
+ minConfirmations: number;
144
+ /** The fill's outpoint, learned on first sighting. Without it a SPENT htlc
145
+ * reads as never funded — see `classifyOnchainHtlc`. */
146
+ funding?: {
147
+ txid: string;
148
+ vout: number;
149
+ };
150
+ /** Our own L1 claim, so a restart does not re-broadcast it. */
151
+ claimTxid?: string;
152
+ }
153
+ /**
154
+ * Build the profile from what `requestOnchainSend` returned.
155
+ *
156
+ * Pass the whole result: every field this needs is on it, so the mapping is
157
+ * done here, once, instead of at each call site.
158
+ *
159
+ * That mapping is the reason this exists. `htlcParams.refundLocktime` becomes
160
+ * `htlcLocktime` — the same value under a different name, because the record
161
+ * already has a `refundLocktime` and it is the arkade lockup's, a different
162
+ * deadline entirely. The keys go from bytes to hex. `htlcAddress` is not an
163
+ * input to anything, it is the derived value the rebuild checks the inputs
164
+ * against, so writing it is easy to skip and impossible to reconstruct later.
165
+ * A caller copying fields across by hand gets all three right or restores a
166
+ * swap that watches nothing.
167
+ *
168
+ * The other two corridors have no such builder, deliberately: their profiles
169
+ * are the request result's own fields under their own names, with nothing
170
+ * derived and nothing renamed.
171
+ *
172
+ * The L1 half ONLY. `signer` and `hashlock` come from `rfqSecretsProfile`, which
173
+ * every corridor calls — folding them in here would give this leg a one-call
174
+ * mapper the other two cannot have, and the uniform rule ("`rfqSecretsProfile`
175
+ * first, then whatever the corridor adds") is what keeps the per-corridor
176
+ * instructions short enough to follow.
177
+ */
178
+ declare function onchainSendProfile(result: {
179
+ htlc: Pick<OnchainHtlc, "address">;
180
+ htlcParams: OnchainHtlcParams;
181
+ l1Network: OnchainNetwork;
182
+ minConfirmations: number;
183
+ }): Omit<OnchainSendProfile, "signer" | "hashlock">;
7
184
 
8
185
  /** The contracts — pure data, shared verbatim with any other implementation. */
9
186
  declare const swapPrograms: Record<"wantAsset" | "wantBtc", ReturnType<typeof arkade.parseArtifact>>;
@@ -219,18 +396,21 @@ declare const validatePlan: (plan: OfferPlan, giveBalance: bigint, dust: bigint)
219
396
  /** Browser backend over the SDK's shared IndexedDB manager — the same
220
397
  * infrastructure the wallet already uses for its Boltz swap repository. */
221
398
  declare class IndexedDbAssetSwapRepository implements AssetSwapRepository {
222
- private readonly dbName;
223
- readonly version: 2;
224
- private dbPromise;
399
+ readonly version: 4;
400
+ private readonly connection;
225
401
  constructor(dbName?: string);
226
402
  private ensureDb;
227
403
  private readStore;
228
404
  /** Every write in one place, so none of them can forget to await the
229
405
  * commit. Requests need no individual await: a failed one aborts the
230
- * transaction, which `txDone` reports. */
406
+ * transaction, which `awaitTransaction` reports. */
231
407
  private write;
232
408
  saveSwap(swap: AssetSwap): Promise<void>;
233
409
  getAllSwaps(): Promise<AssetSwap[]>;
410
+ saveRfqSwap(record: RfqSwapRecord): Promise<void>;
411
+ getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
412
+ getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
413
+ removeRfqSwap(rfqId: string): Promise<void>;
234
414
  getScannedTxids(): Promise<Set<string>>;
235
415
  markTxidsScanned(txids: Iterable<string>): Promise<void>;
236
416
  getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
@@ -453,7 +633,7 @@ interface ClaimPacketInput {
453
633
  covclaimdPubkey: Uint8Array;
454
634
  }
455
635
  /**
456
- * Seal a preimage to covclaimd. WebCrypto AES-GCM, hence async.
636
+ * Seal a preimage to covclaimd.
457
637
  *
458
638
  * The ephemeral key and nonce are generated here and CANNOT be supplied by a
459
639
  * caller. That is the point of this signature: AES-GCM under a repeated
@@ -464,386 +644,6 @@ interface ClaimPacketInput {
464
644
  */
465
645
  declare function sealClaimPacket(input: ClaimPacketInput): Promise<SealedClaimPacket>;
466
646
 
467
- /** True for the states after which the solver will report nothing further. */
468
- declare const isRfqTerminal: (state: string) => boolean;
469
- /**
470
- * The terminal states that mean the swap is OVER and the lockup is already
471
- * gone — the solver either claimed it (`settled`, revealing the preimage) or
472
- * returned it (`refunded`). A trader seeing one of these has nothing left to
473
- * do.
474
- *
475
- * Deliberately narrower than {@link RFQ_TERMINAL_STATES}: `refused`, `expired`
476
- * and `stuck` are terminal for the NEGOTIATION but say nothing about whether
477
- * the trader's sats are still sitting at the lockup. A trader that funded just
478
- * as the quote expired, or whose solver wedged mid-payment, is exactly the
479
- * trader who needs the refund most — so those states must not short-circuit
480
- * it. {@link refundIfUnresolved} treats them as "keep going", and lets the
481
- * on-chain VTXO lookup be the authority on whether anything is actually there.
482
- */
483
- declare const RFQ_RESOLVED_STATES: readonly ["settled", "refunded"];
484
- /**
485
- * Poll a swap's status until it reaches a terminal state.
486
- *
487
- * Same shape and conventions as {@link awaitOnchainFill}: a `pollMs` interval,
488
- * an optional unix-seconds `deadline`, and a thrown error carrying a stable
489
- * `reason` when that deadline passes.
490
- *
491
- * A `null` status (the solver has no record of this `rfq_id`) is treated as
492
- * "not yet", not as an answer — a status route can legitimately 404 for a
493
- * moment after a quote is issued. The deadline is what bounds that wait.
494
- *
495
- * Transport errors are NOT swallowed; a failing `status()` call rejects this
496
- * function. Callers polling across a long refund window should expect to
497
- * restart it after a network blip — nothing is lost by doing so, since the
498
- * refund path this feeds is gated on an absolute timelock that does not
499
- * expire.
500
- */
501
- declare function awaitRfqResolution(transport: RfqTransport, rfqId: string, options?: {
502
- pollMs?: number;
503
- deadline?: number;
504
- }): Promise<RfqStatus>;
505
- /** The Ark surface the refund push needs — narrower than a full provider, and
506
- * satisfied by {@link RestArkProvider}. Same seam style as `RestoreIndexer`. */
507
- type RefundArkProvider = Pick<RestArkProvider, "getInfo" | "submitTx" | "finalizeTx">;
508
- /** The indexer surface the lockup lookup needs. */
509
- type RefundIndexer = Pick<RestIndexerProvider, "getVtxos">;
510
- /** A still-refundable virtual output sitting at the swap lockup. */
511
- interface LockupVtxo {
512
- txid: string;
513
- vout: number;
514
- value: number;
515
- /**
516
- * The batch this output lived in expired and the operator swept it, so it
517
- * is no longer a live leaf — it can be RECOVERED, but not spent offchain.
518
- *
519
- * It is still the trader's money and it is still visible, which is why
520
- * {@link findLockupVtxos} returns it. What it is not is refundable by
521
- * {@link pushRefundWithoutReceiver}: that builds an offchain Ark
522
- * transaction, and the SDK's own predicates make the two states mutually
523
- * exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
524
- * is true (`wallet/vtxo.ts`), and the latter is documented as "must be
525
- * recovered into a fresh batch rather than spent offchain". Holding the
526
- * trader's `sender` key does not change that; a sweep removes the leaf from
527
- * the live tree, not the signature from the trader.
528
- *
529
- * `packages/boltz-swap` splits on exactly this fact rather than working
530
- * around it: `settleRefundWithoutReceiver` sends a live VTXO through an
531
- * offchain tx and a recoverable one through `joinBatch` — "a swept
532
- * (recoverable) VTXO is no longer a live leaf, so it can only be reclaimed
533
- * by re-registering it into a batch".
534
- *
535
- * So the remedy is recovery (renewing the output into a fresh batch),
536
- * after which the ordinary CLTV refund works again. This package does not
537
- * build that round — see {@link pushRefundWithoutReceiver}, which refuses
538
- * rather than submitting a spend that cannot succeed.
539
- */
540
- recoverable: boolean;
541
- }
542
- /**
543
- * Thrown when a refund was asked for over outputs that have been swept.
544
- *
545
- * Carries the outpoints so a caller can act — recover exactly those, then
546
- * retry — instead of reading a server rejection and guessing. `reason` follows
547
- * the same convention as `awaitOnchainFill`'s `fill_timeout` and
548
- * `claimOnchainFill`'s `claim_window_closed`.
549
- *
550
- * **The remedy already exists; this package does not reimplement it.** The SDK
551
- * recovers swept outputs by re-registering them into a fresh batch, through
552
- * `IVtxoManager.recoverVtxos()` — the same batch round `packages/boltz-swap`
553
- * reaches via its own `joinBatch`. It reads the wallet's registered-contract
554
- * snapshot (`recoverVtxos` → `wallet.getVtxos({ withRecoverable: true })` →
555
- * `contractSnapshot()` → `contractManager.getContractsWithVtxos()`), so it
556
- * covers a swap lockup as soon as that lockup is registered as a contract —
557
- * which is what {@link RfqSwapManagerDeps.contracts} does. Registration is
558
- * therefore not only a latency optimization; it is what turns a swept lockup
559
- * from a dead end into something the ordinary wallet path can recover.
560
- *
561
- * Two caveats a caller must hold, neither enforceable from here:
562
- *
563
- * - **The wallet must hold the lockup's `sender` key**, because recovery
564
- * settles through `refundWithoutReceiver` — the leaf `vhtlc-v2` annotates
565
- * these VTXOs with.
566
- * - **`refundLocktime` must have matured.** That leaf carries a CLTV, so a
567
- * recovery round including this VTXO earlier is rejected. `recoverVtxos`
568
- * sweeps every recoverable output in ONE settlement and has no CLTV
569
- * awareness, so recovering early can fail the whole batch rather than just
570
- * this output. `packages/boltz-swap` encodes the same rule as "pre-CLTV
571
- * recoverable → skipped".
572
- */
573
- declare class LockupNeedsRecoveryError extends Error {
574
- readonly name = "LockupNeedsRecoveryError";
575
- readonly reason = "needs_recovery";
576
- /** `txid:vout` for each output that must be recovered first. */
577
- readonly outpoints: string[];
578
- /**
579
- * The contract's `refundLocktime`. Recovering before this matures is the
580
- * hazard described above: `recoverVtxos()` sweeps EVERY recoverable output
581
- * into one settlement with no CLTV awareness, so an early attempt can fail
582
- * the whole batch — including unrelated outputs that were otherwise fine.
583
- *
584
- * Exposed as a value, not only inside the message, so a caller can encode
585
- * `packages/boltz-swap`'s "pre-CLTV recoverable → skipped" rule without
586
- * parsing prose. Seconds-based locktimes mature against the chain tip's
587
- * timestamp rather than wall clock, so treat this as a floor to wait past,
588
- * not an exact alarm.
589
- */
590
- readonly recoverableAfter: bigint;
591
- constructor(outpoints: string[], recoverableAfter: bigint);
592
- }
593
- /**
594
- * Every output still sitting at the lockup script — spendable AND
595
- * swept-but-recoverable, each tagged with which it is.
596
- *
597
- * All of them, not the first: a trader may fund a lockup in more than one
598
- * send, and refunding only `vtxos[0]` returns part of the money and strands
599
- * the rest at a script whose other refund paths are all longer.
600
- *
601
- * BOTH queries, because they are disjoint sets and `spendableOnly` alone goes
602
- * blind at exactly the wrong moment. A lockup whose batch expiry passed is
603
- * swept into the recoverable set, and this function exists to serve swaps that
604
- * sat unresolved — which are precisely the ones most likely to have got there.
605
- * Reading only the spendable set would report `nothing_to_refund` over money
606
- * that is still sitting at the script, which is worse than an error: it looks
607
- * like a resolved swap. `packages/boltz-swap` merges the same two queries for
608
- * the same reason (`arkade-swaps.ts`'s `refundableVtxos`).
609
- *
610
- * **Visible is not the same as refundable.** A `recoverable` output cannot be
611
- * spent offchain at all — see {@link LockupVtxo.recoverable} — so this set is
612
- * "what is there", not "what {@link pushRefundWithoutReceiver} can take back".
613
- * That function refuses the recoverable ones by name rather than submitting a
614
- * spend the server must reject.
615
- *
616
- * This read — not the RFQ's reported state — is the authority on whether
617
- * there is anything left at the lockup.
618
- *
619
- * **Not replaced by the contract manager's VTXO state, deliberately.** Once a
620
- * lockup is registered (see `RfqSwapManagerDeps.contracts`) the wallet tracks
621
- * these same outputs, and `getContractsWithVtxos` plus `canSpendOffchain` /
622
- * `canRecoverOnchain` would classify them. That is a WEAKER answer here on two
623
- * counts: it serves the wallet REPOSITORY, which a degraded sync will happily
624
- * hand back stale (`getSyncState()` reports `degraded` and returns cached rows
625
- * rather than failing), and its height-based expiry test needs a chain tip this
626
- * module does not have. The two queries below ask the indexer itself and need
627
- * neither. Ask-the-indexer, don't-trust-local-state — the same posture
628
- * {@link readLockupFate} takes, and for the same reason: this decides money.
629
- */
630
- declare function findLockupVtxos(indexer: RefundIndexer, swapPkScript: Uint8Array): Promise<LockupVtxo[]>;
631
- /**
632
- * The indexer surface the lockup-spend read needs: the vtxo lookup, plus the
633
- * raw transactions those vtxos were spent by. Same narrow-seam style as
634
- * {@link RefundIndexer} and `restore.ts`'s `RestoreIndexer`, and satisfied by
635
- * {@link RestIndexerProvider}.
636
- */
637
- type LockupSpendIndexer = Pick<RestIndexerProvider, "getVtxos" | "getVirtualTxs">;
638
- /**
639
- * What chain data says became of a swap lockup — the whole answer, with no
640
- * solver involvement and nothing taken on the solver's word.
641
- */
642
- type LockupFate =
643
- /** At least one output at the lockup is still unspent. Not over. */
644
- {
645
- fate: "open";
646
- }
647
- /** Spent by a witness carrying a preimage that HASHES to the quote's
648
- * `payment_hash`. Only the claim leaf can reveal one, and the only
649
- * legitimate way the solver obtains it is by completing its side. */
650
- | {
651
- fate: "claimed";
652
- preimage: Uint8Array;
653
- }
654
- /** Fully spent, and nothing that spent it revealed a matching preimage —
655
- * so the money went back to the trader. See {@link readLockupFate}. */
656
- | {
657
- fate: "returned";
658
- }
659
- /** Nothing was learned: no outputs visible, an output spent by nothing the
660
- * indexer names, a spend it could not produce, or a blob that would not
661
- * decode. Never an answer. */
662
- | {
663
- fate: "unknown";
664
- };
665
- /**
666
- * Decide from chain data alone whether a swap lockup settled, came back, or is
667
- * still live.
668
- *
669
- * **Why this is decidable without asking anyone.** The lockup's claim leaf can
670
- * only be spent by revealing `P`, so a spend witness carrying a value that
671
- * hashes to the quote's `payment_hash` is proof the claim leaf was used — and
672
- * the only legitimate way the counterparty obtains `P` is by completing its
673
- * side of the swap. Every OTHER leaf is a refund: `nonInteractiveRefund` is
674
- * covenant-pinned to the trader's own address (`enforcePayTo(senderPkScript)`),
675
- * and `refund`, `refundWithoutReceiver`, `unilateralRefund` and
676
- * `unilateralRefundWithoutReceiver` all require the trader's own signature. So
677
- * "spent, but not by a hash-verified claim" means the money went back to the
678
- * trader, and nothing here has to trust a counterparty to say so.
679
- *
680
- * **A matching witness SHAPE is not proof.** Only a candidate that hashes to
681
- * `paymentHash` may be read as a claim; a 32-byte item that hashes to anything
682
- * else is just bytes, and is treated as a refund. Getting this wrong in the
683
- * permissive direction would report "settled" for a swap that actually
684
- * refunded, which is precisely the fact a trader is relying on.
685
- *
686
- * **`unknown` is not `returned`.** An empty vtxo set (indexer lag, or a lockup
687
- * not visible yet), a `spentBy` the indexer cannot produce a transaction for,
688
- * or a blob that will not decode all come back as `unknown`. `getVirtualTxs`
689
- * may legitimately return fewer transactions than were asked for, so the
690
- * observed set is counted rather than assumed complete. The caller's correct
691
- * response to `unknown` is the same as to `open`: keep watching, and let the
692
- * refund timelock — which no outage can move — be what ends the wait.
693
- *
694
- * Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
695
- * cached, the same posture {@link findLockupVtxos} already establishes.
696
- */
697
- declare function readLockupFate(indexer: LockupSpendIndexer, input: {
698
- swapPkScript: Uint8Array;
699
- /** `sha256(P)`, hex — the quote's `payment_hash`. */
700
- paymentHash: string;
701
- }): Promise<LockupFate>;
702
- /**
703
- * Build, sign, and push the `refundWithoutReceiver` spend: return every funded
704
- * output at the lockup to the trader's refund address.
705
- *
706
- * The leaf is `CLTV(refundLocktime) + <sender> + <server>` — the trader's own
707
- * VHTLC `sender` key and the Arkade server, and NOBODY else. In particular the
708
- * emulator is not involved: it co-signs only the two covenant leaves
709
- * (`nonInteractiveClaim` / `nonInteractiveRefund`), which is why the solver's
710
- * own escape hatch has to go through it and this one does not. So unlike that
711
- * push, this transaction is submitted SIGNED, and the only counterparty is the
712
- * Arkade server doing what it does for any collaborative spend.
713
- *
714
- * One aggregate output, not one per input — again unlike the solver's covenant
715
- * refund, which needs index-aligned outputs because its ArkadeScript inspects
716
- * the output at the current input's index. This leaf carries no covenant, so a
717
- * single output paying the whole balance is both valid and cheaper.
718
- *
719
- * `refundPkScript` defaults to the destination the contract itself commits to
720
- * (`nonInteractiveRefund`'s `senderPkScript`, i.e. the address the trader gave
721
- * at quote time), so the ordinary call cannot send the refund somewhere the
722
- * trader did not intend. It is overridable because this leaf, having no
723
- * covenant, genuinely does permit any destination.
724
- *
725
- * **Consensus, not wall clock, decides when this is spendable.** A seconds
726
- * locktime matures against median-time-past, which trails real time by roughly
727
- * an hour, so a push issued the moment `refundLocktime` passes can be rejected
728
- * until enough blocks land. That is expected, not a failure — see
729
- * {@link refundIfUnresolved}, which retries.
730
- *
731
- * **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
732
- * a swept output is no longer a live leaf: `canSpendOffchain` and
733
- * `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
734
- * input cannot be spent this way whatever key signs it (see
735
- * {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
736
- * transaction, a single swept output would take the live ones down with it —
737
- * so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
738
- * outpoints, rather than submitted and rejected. Filtering them out silently
739
- * would be worse still: it would report success over money that never moved.
740
- */
741
- declare function pushRefundWithoutReceiver(ark: RefundArkProvider, input: {
742
- script: InstanceType<typeof VHTLC.ScriptV2>;
743
- /** The `sender` signer. Build it from the swap record with
744
- * {@link senderIdentityForSwapRecord} — on an HD wallet that resolves
745
- * from the seed, with no stored key bytes anywhere, and every way the
746
- * wallet can fail to produce it arrives as one typed
747
- * {@link RefundNotLocallyPossibleError} the manager reads as permanent
748
- * rather than retrying for the rest of the refund window. */
749
- sender: Identity;
750
- vtxos: readonly LockupVtxo[];
751
- /** Defaults to the contract's own committed refund destination. */
752
- refundPkScript?: Uint8Array;
753
- }): Promise<{
754
- arkTxid: string;
755
- amount: number;
756
- }>;
757
- /**
758
- * How long past `refundLocktime` to keep retrying the push before giving up
759
- * and surfacing the server's refusal.
760
- *
761
- * Two hours because the CLTV matures against median-time-past (BIP-113), which
762
- * lags wall clock by about an hour, plus room for a slow block. This is the
763
- * mirror of `MIN_HEADROOM_SECONDS`, which refuses to FUND without 90 minutes
764
- * of the same margin.
765
- */
766
- declare const REFUND_MTP_LAG_SECONDS: number;
767
- type RefundOutcome =
768
- /** The solver resolved it — claimed (`settled`) or returned it (`refunded`). */
769
- {
770
- outcome: "resolved";
771
- status: RfqStatus;
772
- }
773
- /** The trader took it back via `refundWithoutReceiver`. */
774
- | {
775
- outcome: "refunded";
776
- arkTxid: string;
777
- amount: number;
778
- status: RfqStatus | null;
779
- }
780
- /** The refund window opened but the lockup holds nothing to return. */
781
- | {
782
- outcome: "nothing_to_refund";
783
- status: RfqStatus | null;
784
- }
785
- /**
786
- * The money is still at the lockup, but its batch was swept, so no offchain
787
- * spend can take it back until it is recovered into a fresh batch. Returned
788
- * rather than retried: unlike a median-time-past refusal, no amount of
789
- * waiting fixes this — see {@link LockupNeedsRecoveryError}. Recover the
790
- * named outpoints, then call this again.
791
- */
792
- | {
793
- outcome: "needs_recovery";
794
- outpoints: string[];
795
- vtxos: LockupVtxo[];
796
- status: RfqStatus | null;
797
- };
798
- /**
799
- * Ask first, then fall back: watch the swap for the solver to resolve it, and
800
- * if `refundLocktime` matures without that happening, take the lockup back
801
- * with `refundWithoutReceiver`.
802
- *
803
- * This is the whole trader-side failure story in one call. It polls `status()`
804
- * — the only "asking" this protocol has (see the module doc) — and returns as
805
- * soon as the solver reports `settled` or `refunded`. Otherwise, once the
806
- * quote's `refund_locktime` passes, it looks up what is actually at the lockup
807
- * and pushes the refund.
808
- *
809
- * Two behaviours worth knowing:
810
- *
811
- * - **A dead negotiation is not a reason to stop.** `refused`, `expired` and
812
- * `stuck` are terminal states, but a trader can be holding a funded lockup in
813
- * every one of them, so they do not end the wait — only `settled`/`refunded`
814
- * do (see {@link RFQ_RESOLVED_STATES}). What ends it otherwise is the
815
- * timelock.
816
- * - **The first push after the deadline may legitimately fail.** Median-time-
817
- * past trails wall clock, so the server can still consider the leaf locked
818
- * for a while after `refundLocktime` passes in real time. Failures are
819
- * retried at the poll interval until `attemptDeadline`, after which the last
820
- * error is rethrown rather than swallowed.
821
- * - **A swept lockup ends the wait instead of consuming it.** Once the batch
822
- * is gone the CLTV refund is not "not yet" but "not this way", so it returns
823
- * `needs_recovery` naming the outpoints rather than retrying until the
824
- * deadline. Recover them and call again.
825
- *
826
- * Safe to call late, and safe to call again: a caller recovering from a crash
827
- * well past the deadline skips straight to the push, and a lockup that is
828
- * already empty comes back as `nothing_to_refund` instead of an error.
829
- */
830
- declare function refundIfUnresolved(transport: RfqTransport, ark: RefundArkProvider, indexer: RefundIndexer, input: {
831
- rfqId: string;
832
- script: InstanceType<typeof VHTLC.ScriptV2>;
833
- /** @see pushRefundWithoutReceiver */
834
- sender: Identity;
835
- /** `refund_locktime` from the quote, unix seconds. */
836
- refundLocktime: number;
837
- /** Defaults to the contract's own committed refund destination. */
838
- refundPkScript?: Uint8Array;
839
- pollMs?: number;
840
- /** Stop retrying the push at this unix time, rethrowing the last
841
- * error. Defaults to `refundLocktime + REFUND_MTP_LAG_SECONDS`. */
842
- attemptDeadline?: number;
843
- /** Injected for tests; defaults to wall clock, in unix seconds. */
844
- now?: () => number;
845
- }): Promise<RefundOutcome>;
846
-
847
647
  /** The Ark surface the claim push needs — the same seam the refund push uses. */
848
648
  type ClaimArkProvider = RefundArkProvider;
849
649
  /**
@@ -991,6 +791,47 @@ declare function senderIdentityForSwapRecord(wallet: IWallet, record: {
991
791
  signingDescriptor?: string;
992
792
  }): Promise<Identity>;
993
793
 
794
+ /**
795
+ * The `refundArkade` callback, assembled.
796
+ *
797
+ * The wiring this replaces was prose in two places — `swapManager.ts`'s
798
+ * callback doc and the README — because it is the one callback whose obvious
799
+ * implementation is wrong: `refundIfUnresolved` is the single-swap version of
800
+ * `RfqSwapManager` itself and brings its own status polling and MTP retry loop,
801
+ * which would nest inside the manager's. Composing the atomic push here makes
802
+ * that mistake something a consumer has to go out of their way to make.
803
+ *
804
+ * The three semantic rules the manager relies on are structural rather than
805
+ * documented: an empty lockup returns `null`, and both
806
+ * {@link RefundNotLocallyPossibleError} and {@link LockupNeedsRecoveryError}
807
+ * propagate untouched — the manager reads the first as permanent and surfaces
808
+ * the second as `needs_recovery`, and catching either here would turn a state
809
+ * the trader must act on into a retry that grinds the window away.
810
+ */
811
+
812
+ interface ArkadeRefunderDeps {
813
+ ark: RefundArkProvider;
814
+ indexer: RefundIndexer;
815
+ /** Asked for the descriptor's signer; never asked to mint a key. */
816
+ wallet: IWallet;
817
+ /**
818
+ * The record store. The live `RfqSwap` the manager passes carries no
819
+ * `signingDescriptor` — that lives in the record's `profile.signer` — so
820
+ * the refund key is resolved by key, which is what `getRfqSwap` is for.
821
+ */
822
+ repository: Pick<AssetSwapRepository, "getRfqSwap">;
823
+ }
824
+ /**
825
+ * Build the `refundArkade` callback for `RfqSwapManager.setCallbacks`.
826
+ *
827
+ * @example
828
+ * manager.setCallbacks({
829
+ * refundArkade: arkadeRefunder({ ark, indexer, wallet, repository }),
830
+ * saveSwap,
831
+ * });
832
+ */
833
+ declare function arkadeRefunder(deps: ArkadeRefunderDeps): (swap: RfqSwap) => Promise<ArkadeRefundResult>;
834
+
994
835
  /** The contract type a swap lockup registers under. `@arkade-os/sdk`'s handler
995
836
  * for `VHTLC.ScriptV2` — the covenant script this corridor builds. */
996
837
  declare const SWAP_LOCKUP_CONTRACT_TYPE = "vhtlc-v2";
@@ -1000,6 +841,25 @@ declare const SWAP_LOCKUP_CONTRACT_KIND = "rfq-swap-lockup";
1000
841
  * injection style as `SwapContractRegistry`, and satisfied by a real
1001
842
  * `ContractManager` (`await wallet.getContractManager()`). */
1002
843
  type LockupContractWriter = Pick<IContractManager, "createContract">;
844
+ /** The read seam {@link lockupContractParams} needs. Same narrowing, same
845
+ * `ContractManager` satisfies it. */
846
+ type LockupContractReader = Pick<IContractManager, "getContracts">;
847
+ /**
848
+ * No row for a lockup a record claims was funded.
849
+ *
850
+ * Separate from a rebuild failure on purpose: the record is fine and its money
851
+ * may well be at the address — what is missing is the wallet's copy of the
852
+ * covenant, which registration writes before the address can be funded. So this
853
+ * means the contract store was cleared or was never the one that registered
854
+ * this swap, and the remedy is a store, not a re-quote.
855
+ */
856
+ declare class LockupContractMissing extends Error {
857
+ /** The lockup whose row is absent. */
858
+ readonly address: string;
859
+ /** Its pkScript hex — the key the row would have been under. */
860
+ readonly script: string;
861
+ constructor(address: string, script: string);
862
+ }
1003
863
  /**
1004
864
  * The lockup could not be written locally.
1005
865
  *
@@ -1040,769 +900,35 @@ declare class LockupRegistrationFailed extends Error {
1040
900
  * storage problem from a reason to walk away from the quote.
1041
901
  */
1042
902
  declare function registerLockupContract(contracts: LockupContractWriter, script: InstanceType<typeof VHTLC.ScriptV2>, address: string): Promise<void>;
1043
-
1044
903
  /**
1045
- * Where a monitored swap stands.
904
+ * The stored covenant parameters of a funded lockup — the other half of
905
+ * `rebuildRfqSwap`.
1046
906
  *
1047
- * `claimable` and `claimed` are the states of a swap the TRADER has something
1048
- * to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
1049
- * a receive. Only `lightning_send` has neither there the solver claims the
1050
- * lockup, and the trader's only move is the refund.
1051
- */
1052
- type RfqSwapState =
1053
- /** Live; nothing actionable yet. On a receive leg this covers the whole
1054
- * stretch before the solver funds anything. */
1055
- "pending"
1056
- /** There is something for the trader to take, and the window to take it is
1057
- * open: the confirmed L1 fill on an onchain send, or a lockup funded for at
1058
- * least `expectedAmount` on a receive. */
1059
- | "claimable"
1060
- /**
1061
- * The trader's claim has been made — its L1 broadcast on an onchain send,
1062
- * its Arkade submission on a receive.
907
+ * The row is the wallet's own copy of the tree, written from the covenant
908
+ * before the address could be funded and keyed by the script it derives, which
909
+ * `createContract` refuses to write unless the params reproduce it. That is why
910
+ * an RFQ swap record stores no tree parameters of its own.
1063
911
  *
1064
- * **On a receive this is a local belief and not a chain fact**, which is
1065
- * why it is not terminal: `settled` is the chain's answer, and `refunded`
1066
- * is still reachable from here if the claim never lands and the solver
1067
- * takes the lockup back.
1068
- */
1069
- | "claimed"
1070
- /**
1071
- * This wallet will not act, and only the counterparty can change that.
1072
- *
1073
- * On a send leg: the Arkade refund cannot be pushed from here — no secrets
1074
- * on the record, a descriptor from another seed, or nothing wired to act —
1075
- * so the lockup comes back only if the counterparty claims it or the wallet
1076
- * that can sign it is restored. On a receive leg: the trader holds no
1077
- * refund at all, so this is a lockup that cannot be claimed — funded for
1078
- * less than the swap agreed (publishing `P` for it is the whole attack
1079
- * `LockupAmountMismatchError` exists to refuse), or one whose claim window
1080
- * shut unclaimed. {@link RfqSwapCommon.blockedReason} says which.
1081
- *
1082
- * **Not terminal, and not a dead end.** The money is still at the lockup,
1083
- * so the counterparty's move is still observable and still ends the swap;
1084
- * and the refusal is re-checked every pass, so restoring the right wallet,
1085
- * wiring the callbacks, or the solver topping the lockup up returns the
1086
- * swap to `pending` and resumes the normal drive. For an onchain-send swap
1087
- * it says nothing about the L1 half, which keeps being driven and claimed.
1088
- */
1089
- | "needs_counterparty"
1090
- /**
1091
- * Terminal: the lockup was spent by a hash-verified claim. Read off chain,
1092
- * never reported.
912
+ * Looked up by script rather than address: the script is the row's key, and
913
+ * decoding it here means a record whose address does not decode fails as a bad
914
+ * address instead of as a missing row.
1093
915
  *
1094
- * On a send leg that claim is the counterparty's, and it is proof the
1095
- * counterparty completed its side. On a receive leg it is the TRADER's own
1096
- * — matched by the hash and not by our txid, so a claim that lands without
1097
- * us still counts (see {@link RfqSwapManager}).
1098
- */
1099
- | "settled"
1100
- /**
1101
- * Terminal: the lockup was spent by something other than a claim.
1102
- *
1103
- * On a send leg that is the money coming back, by the solver's hand or the
1104
- * trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
1105
- * money, every non-claim leaf is the solver's, and a swap that ends here
1106
- * ended with the trader's incoming payment never arriving. It is also where
1107
- * a receive swap ends when its window closes with nothing left to observe —
1108
- * see {@link RfqSwapManager}.
1109
- */
1110
- | "refunded"
1111
- /** Terminal: an action failed and its window closed. */
1112
- | "failed";
1113
- /** The states after which the manager stops monitoring a swap. Deliberately
1114
- * without `needs_counterparty`: retiring on it would unwatch a funded lockup
1115
- * whose claim is still the thing that ends the swap. */
1116
- declare const RFQ_SWAP_TERMINAL_STATES: readonly ["settled", "refunded", "failed"];
1117
- declare const isRfqSwapTerminal: (state: RfqSwapState) => boolean;
1118
- /**
1119
- * What the manager needs to register a swap's lockup with the wallet, so the
1120
- * indexer pushes its funding and its spend instead of being asked every few
1121
- * seconds.
1122
- *
1123
- * Both fields are things the caller already holds. `script` is the very object
1124
- * `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
1125
- * it in hand; `address` is the request entrypoint's own return value. The
1126
- * address is taken rather than re-derived on purpose — the row's address must be
1127
- * the one that was actually funded, and a local re-derivation would silently use
1128
- * the SDK's default network, which is the exact bug `registerOfferContract`
1129
- * guards against.
1130
- */
1131
- interface RfqSwapLockup {
1132
- /** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
1133
- script: InstanceType<typeof VHTLC.ScriptV2>;
1134
- /** The Arkade address that was funded. */
1135
- address: string;
1136
- }
1137
- interface RfqSwapCommon {
1138
- /** The negotiation id — this record's identity. */
1139
- rfqId: string;
1140
- state: RfqSwapState;
1141
- /** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
1142
- * request entrypoints. This is what the manager watches to decide the swap:
1143
- * it is the only handle on the covenant whose spend witness says whether
1144
- * the swap settled or came back. */
1145
- lockupPkScript: Uint8Array;
1146
- /** The covenant behind {@link lockupPkScript}, when the caller wants the
1147
- * lockup registered with a contract manager. Optional: without it the
1148
- * manager still watches the swap on its timer, it just cannot subscribe.
1149
- * See {@link RfqSwapManagerDeps.contracts}. */
1150
- lockup?: RfqSwapLockup;
1151
- /** `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
1152
- * be spent by revealing a value that hashes to this, which is what makes a
1153
- * settlement provable rather than reported. For an onchain send this is
1154
- * the SAME hash the L1 `htlc` carries: one `P` unlocks both legs. */
1155
- paymentHash: string;
1156
- /**
1157
- * `refund_locktime` from the quote, unix seconds.
1158
- *
1159
- * Whose deadline it is inverts with the direction, and so does what to do
1160
- * about it. On a send leg it is the TRADER's: the lockup is the trader's
1161
- * money and this gates the refund that takes it back, so it is a moment to
1162
- * act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
1163
- * leaf at all, and this is the moment to have claimed BEFORE.
1164
- */
1165
- refundLocktime: number;
1166
- createdAt: number;
1167
- updatedAt: number;
1168
- /** Set once the trader's own `refundWithoutReceiver` push landed. */
1169
- refundArkTxid?: string;
1170
- /** Why `state` is `failed`. */
1171
- failure?: string;
1172
- /** Why `state` is `needs_counterparty`. Distinct from {@link failure},
1173
- * which means an action was attempted and did not work. */
1174
- blockedReason?: string;
1175
- }
1176
- /** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
1177
- * claims the lockup with the preimage it learns by paying the invoice — which
1178
- * is exactly why that spend's witness is proof the payment landed. */
1179
- interface LightningSendSwap extends RfqSwapCommon {
1180
- kind: "lightning_send";
1181
- }
1182
- /** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
1183
- interface OnchainSendSwap extends RfqSwapCommon {
1184
- kind: "onchain_send";
1185
- /** The locally derived HTLC from `requestOnchainSend` — the manager reads
1186
- * `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
1187
- htlc: OnchainHtlc;
1188
- /** `profile.min_confirmations` from the quote. */
1189
- minConfirmations: number;
1190
- /** The fill's outpoint, learned on first sighting. Without it a SPENT
1191
- * HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
1192
- funding?: {
1193
- txid: string;
1194
- vout: number;
1195
- };
1196
- /** Our L1 claim's txid. */
1197
- claimTxid?: string;
1198
- }
1199
- /**
1200
- * `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
1201
- * and the TRADER claims it, and that claim is what publishes `P` and lets the
1202
- * solver settle the payer's held Lightning HTLC.
1203
- *
1204
- * Two consequences shape how this record is driven, both of them absent from
1205
- * the send legs:
1206
- *
1207
- * - **There is no trader-side refund.** Every non-claim leaf of this covenant
1208
- * is the solver's, so the manager never calls
1209
- * {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
1210
- * is not claimed is simply lost — the solver reclaims at
1211
- * {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
1212
- * held HTLC lapses.
1213
- * - **The claim is the whole swap, and it is on a deadline.** The trader must
1214
- * be online for it: covclaimd cannot claim this covenant today, so the claim
1215
- * packet's offline path does not run.
1216
- */
1217
- interface LightningReceiveSwap extends RfqSwapCommon {
1218
- kind: "lightning_receive";
1219
- /**
1220
- * What the lockup must carry — the quote's `to_amount`, captured at REQUEST
1221
- * time and persisted with the record.
1222
- *
1223
- * **Not re-derivable, and not optional.** Captured at claim time it would
1224
- * be whatever the solver funded, which is the dust-funding attack rather
1225
- * than a check on it. A record that reaches the manager without a finite
1226
- * value here is reported `needs_counterparty` and never claimed: a
1227
- * comparison against `undefined` or `NaN` is false, so an unusable
1228
- * comparand does not fail the value gate, it deletes it.
1229
- */
1230
- expectedAmount: number;
1231
- /** Our Arkade claim's txid, once submitted. Set from the callback's return
1232
- * and never from a chain read — the chain's answer is `settled`. */
1233
- claimArkTxid?: string;
1234
- }
1235
- /**
1236
- * A monitored swap.
1237
- *
1238
- * This is a live record, not a serialization format: `lockupPkScript` and
1239
- * `htlc` hold derived `Uint8Array`s, and
1240
- * {@link RfqSwapManagerCallbacks.saveSwap} is where a caller projects it into
1241
- * whatever it stores. Rebuild it on restart the way it was made —
1242
- * `lightningSendVtxoScript` / `receiveVtxoScript` / `onchainHtlcScript` over
1243
- * the quote's binding fields — and hand the result to
1244
- * {@link RfqSwapManager.start}.
1245
- *
1246
- * **`onchain:BTC->arkade:BTC` is deliberately not a member yet.** Its Arkade
1247
- * half is the same solver-funded lockup as {@link LightningReceiveSwap}'s, but
1248
- * it also has an L1 half the trader funds and must take back itself
1249
- * (`buildHtlcRefund` at the HTLC's own `htlc_locktime`), which is a second
1250
- * deadline, a second observation seam and a second action callback. Adding the
1251
- * lockup half alone would produce a manager that silently lets that L1 refund
1252
- * window pass — the one failure mode {@link RfqSwapManager} refuses elsewhere
1253
- * by name (see `driveOnchain`'s missing-`ChainSource` check). Until the L1
1254
- * refund is driven too, that corridor is better served by the request and claim
1255
- * functions directly than by a monitor that covers half of it.
1256
- */
1257
- type RfqSwap = LightningSendSwap | OnchainSendSwap | LightningReceiveSwap;
1258
- /** What the manager should do next about an onchain-send swap's L1 half. */
1259
- type OnchainSendAction =
1260
- /** Not funded yet, or not confirmed deep enough. */
1261
- "wait"
1262
- /** Funded, confirmed, and far enough from the refund leaf to claim safely. */
1263
- | "claim"
1264
- /** The claim is off the table for good; the money comes back through the
1265
- * Arkade lockup instead. */
1266
- | "claim_window_closed"
1267
- /** Our claim already landed (only the trader holds P). */
1268
- | "claimed"
1269
- /** The solver took its L1 refund — the fill is gone. */
1270
- | "swept";
1271
- /**
1272
- * The decision a "poll status, refund on timeout" loop gets wrong.
1273
- *
1274
- * {@link OnchainHtlcPhase} runs `unfunded -> awaiting_confirmations ->
1275
- * claimable -> (refundable | claimed | swept)`, and `refundable` does NOT mean
1276
- * "time to refund the L1 HTLC" — the trader has no key on that leaf; it is the
1277
- * SOLVER's refund, and reaching it means the trader's claim was missed. So the
1278
- * L1 claim has to be driven before it, and from it the only remaining move is
1279
- * the Arkade-side refund.
1280
- *
1281
- * There is a second, quieter trap between those two functions:
1282
- * `classifyOnchainHtlc` reports `claimable` right up until median-time-past
1283
- * reaches `refundLocktime`, while `claimOnchainFill` refuses from
1284
- * {@link ONCHAIN_CLAIM_MARGIN_SECONDS} before it — because broadcasting
1285
- * publishes P, and doing that into the counterparty's live refund window risks
1286
- * losing the race AND giving away the preimage. Driving straight off the phase
1287
- * would therefore spend that whole margin throwing `claim_window_closed` at
1288
- * every poll and never fall back. This function applies the margin, so
1289
- * "claimable" here means claimable by `claimOnchainFill` too.
1290
- */
1291
- declare function nextOnchainAction(input: {
1292
- phase: OnchainHtlcPhase;
1293
- /** `htlc.refundLocktime` — when the solver's L1 refund leaf opens. */
1294
- htlcLocktime: number;
1295
- /** Unix seconds. */
1296
- now: number;
1297
- }): OnchainSendAction;
1298
- /** What the trader's own `refundWithoutReceiver` push returned, or `null` when
1299
- * the lockup held nothing to return. */
1300
- type ArkadeRefundResult = {
1301
- arkTxid: string;
1302
- amount: number;
1303
- } | null;
1304
- /**
1305
- * The money-moving half, injected. The manager decides when; these do it.
1306
- *
1307
- * Neither action gets a retry loop of its own here — see the module doc. Do
1308
- * NOT wire `refundArkade` to `refundIfUnresolved`: that function is the
1309
- * single-swap version of this whole class and brings its own status polling
1310
- * and its own MTP retry loop, which would nest inside the manager's. Wire it
1311
- * to `findLockupVtxos` + `pushRefundWithoutReceiver`, which is the atomic push
1312
- * `refundIfUnresolved` itself calls.
1313
- *
1314
- * Resolve the sender key through `senderIdentityForSwapRecord`: it is
1315
- * what turns "this wallet cannot sign this swap" into
1316
- * {@link RefundNotLocallyPossibleError}, which the manager reports as
1317
- * `needs_counterparty` instead of retrying for the whole refund window.
1318
- */
1319
- interface RfqSwapManagerCallbacks {
1320
- /** Build and broadcast the L1 claim. See `claimOnchainFill`. */
1321
- claimOnchain: (swap: OnchainSendSwap, utxo: ChainUtxo) => Promise<{
1322
- txid: string;
1323
- }>;
1324
- /**
1325
- * Claim the solver-funded lockup on a receive leg, revealing `P`. Wire it
1326
- * to `pushClaim` — the outputs are supplied, so `findLockupVtxos` has
1327
- * already been called and `claimReceiveLockup`'s wait would only sit on a
1328
- * lockup the manager has just seen.
1329
- *
1330
- * **Pass `expectedAmount` and `partiallyClaimed` straight through.** The
1331
- * manager checks the funded value before calling this, but that check
1332
- * decides WHEN to act; `pushClaim`'s decides whether `P` is published, and
1333
- * it is the one that runs with nothing between it and the signature. Two
1334
- * checks, one of which is load-bearing — do not drop the inner one because
1335
- * the outer one exists.
1336
- *
1337
- * Required rather than optional, like {@link claimOnchain}: a receive swap
1338
- * monitored with nothing wired to claim it is a swap that quietly expires,
1339
- * and a compile error is the right way to learn that.
1340
- */
1341
- claimLockup: (swap: LightningReceiveSwap, vtxos: readonly LockupVtxo[], options: {
1342
- /** A claim of ours is already out, so `P` is public and the value
1343
- * gate has nothing left to protect — pass this to `pushClaim` so a
1344
- * funding that arrived piecemeal can still be swept. */
1345
- partiallyClaimed: boolean;
1346
- }) => Promise<{
1347
- arkTxid: string;
1348
- amount: number;
1349
- }>;
1350
- /** Push `refundWithoutReceiver` for every output at the lockup. See
1351
- * `pushRefundWithoutReceiver`; return `null` for an empty lockup. Never
1352
- * called for a {@link LightningReceiveSwap} — that leg's refund leaf is the
1353
- * solver's. */
1354
- refundArkade: (swap: RfqSwap) => Promise<ArkadeRefundResult>;
1355
- /**
1356
- * Whether a local refund is possible at all — the record's secrets, against
1357
- * this wallet. Called every pass, including *before* the refund window
1358
- * opens, so a swap nobody can refund says so while the solver can still
1359
- * act, instead of at the deadline; and so restoring the right wallet lifts
1360
- * the state again. Never called for a receive swap: there is no local
1361
- * refund there to probe for.
1362
- *
1363
- * Optional: omit to answer "yes" and learn at push time, from
1364
- * {@link RefundNotLocallyPossibleError}. Local by contract — no network
1365
- * call belongs here.
1366
- */
1367
- canRefundArkade?: (swap: RfqSwap) => Promise<{
1368
- ok: true;
1369
- } | {
1370
- ok: false;
1371
- reason: string;
1372
- }>;
1373
- /** Persist the record. Called after any pass that changed it. */
1374
- saveSwap: (swap: RfqSwap) => Promise<void>;
1375
- }
1376
- /** The actions the manager executes on a caller's behalf. */
1377
- type RfqSwapActionName = "claimOnchain" | "claimLockup" | "refundArkade";
1378
- interface RfqSwapManagerEvents {
1379
- /** Every state change, including ones that read as going backwards.
1380
- * `claimed -> claimable` is legal and expected on a receive swap the solver
1381
- * funds piecemeal: a lockup topped up after a claim is a new claimable
1382
- * event, and the label says so before the sweep goes out. Treat these
1383
- * states as a description of what to do next, not as a progress bar. */
1384
- onSwapUpdate?: (swap: RfqSwap, previous: RfqSwapState) => void;
1385
- /** Fired once, when a swap leaves monitoring `settled` or `refunded`.
1386
- * A swap that ends `failed` reports through `onSwapFailed` instead — the
1387
- * two are mutually exclusive. */
1388
- onSwapCompleted?: (swap: RfqSwap) => void;
1389
- /** Fired for any action that threw — including ones the manager will retry
1390
- * on the next pass — and once more when the swap finally ends `failed`. */
1391
- onSwapFailed?: (swap: RfqSwap, error: Error) => void;
1392
- onActionExecuted?: (swap: RfqSwap, action: RfqSwapActionName) => void;
1393
- }
1394
- type SwapUpdateListener = NonNullable<RfqSwapManagerEvents["onSwapUpdate"]>;
1395
- type SwapCompletedListener = NonNullable<RfqSwapManagerEvents["onSwapCompleted"]>;
1396
- type SwapFailedListener = NonNullable<RfqSwapManagerEvents["onSwapFailed"]>;
1397
- type ActionExecutedListener = NonNullable<RfqSwapManagerEvents["onActionExecuted"]>;
1398
- interface RfqSwapManagerConfig {
1399
- /** Drive claims and refunds automatically (default: true). With this off
1400
- * the manager still watches and reports, so a caller can act by hand off
1401
- * `claimable`. */
1402
- enableAutoActions?: boolean;
1403
- /** How often to run a pass, ms. Default 5000 — the same interval
1404
- * `awaitOnchainFill` and `refundIfUnresolved` poll at. */
1405
- pollIntervalMs?: number;
1406
- /** Injected for tests; defaults to wall clock, in unix seconds — the same
1407
- * convention `refundIfUnresolved` uses. */
1408
- now?: () => number;
1409
- events?: RfqSwapManagerEvents;
1410
- }
1411
- /** The contract-manager surface this needs, narrowed for injection — the same
1412
- * seam style as {@link LockupSpendIndexer} and `refund.ts`'s
1413
- * {@link RefundArkProvider}, and satisfied structurally by a real
1414
- * `ContractManager` (`await wallet.getContractManager()`). */
1415
- type SwapContractRegistry = Pick<IContractManager, "createContract" | "getContracts" | "onContractEvent" | "setContractWatchState">;
1416
- /** The observation seams. None is owned by the manager, and none holds keys —
1417
- * same philosophy as `onchainHtlc.ts`'s `ChainSource`. There is no
1418
- * `RfqTransport` here on purpose: nothing this manager decides depends on the
1419
- * solver answering (see the module doc). */
1420
- interface RfqSwapManagerDeps {
1421
- /** Arkade access. Required: this is how a swap's resolution is determined,
1422
- * for both legs. */
1423
- indexer: LockupSpendIndexer;
1424
- /** L1 access. Required to monitor onchain-send swaps; a lightning-only
1425
- * caller can leave it out. */
1426
- chain?: ChainSource;
1427
- /**
1428
- * The wallet's contract manager, when there is one. Optional in the same
1429
- * way {@link chain} is: a caller with no wallet, or one that only wants the
1430
- * timer, still gets a fully working manager — the subscription is a
1431
- * LATENCY optimization and nothing depends on it.
1432
- *
1433
- * Supplying it buys two things. The lockup gets REGISTERED, which is what
1434
- * puts it in the wallet's own contract set at all — a prerequisite for
1435
- * anything that has to act on the lockup before its batch expires, since an
1436
- * expired lockup is swept and loses every cooperative path. And the indexer
1437
- * PUSHES its funding and its spend, so a settlement is noticed when it
1438
- * happens rather than up to `pollIntervalMs` later.
1439
- *
1440
- * Prefer `await wallet.getContractManager()` over constructing one, the way
1441
- * `createOffer` does.
1442
- */
1443
- contracts?: SwapContractRegistry;
1444
- }
1445
- /**
1446
- * Watches a set of live RFQ swaps and drives each to its end.
1447
- *
1448
- * One pass per swap, in this order, every
1449
- * {@link RfqSwapManagerConfig.pollIntervalMs} — and additionally the moment a
1450
- * contract event names that swap's lockup, which changes only WHEN a pass runs,
1451
- * never what it concludes (see {@link subscribe}):
1452
- *
1453
- * 0. **Register the lockup**, if a contract manager was supplied and it is not
1454
- * registered yet. Best-effort; never blocks the steps below.
1455
- * 1. **Ask the chain what became of the lockup** — {@link readLockupFate}. A
1456
- * spend whose witness HASHES to the quote's `payment_hash` ends the swap
1457
- * `settled`; a lockup fully spent by anything else ends it `refunded`.
1458
- * Anything the indexer could not answer is `unknown`, which is NOT an
1459
- * answer: the pass carries on to the steps below, whose deadlines an indexer
1460
- * outage has no bearing on.
1461
- * 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
1462
- * {@link nextOnchainAction}. On a receive it is the lockup itself, and it
1463
- * ends the pass: that leg has no step 3.
1464
- * 3. **Take the lockup back**, send legs only, once `refundLocktime` has passed
1465
- * and step 1 has not ended the swap. This runs for onchain-send too,
1466
- * including after a successful claim: the trader's lockup is still funded and
1467
- * still theirs to recover if the solver never comes for it. When no local
1468
- * refund is possible at all — no secrets, another wallet's descriptor,
1469
- * nothing wired — the swap reports `needs_counterparty` instead of retrying
1470
- * a push that cannot work.
1471
- *
1472
- * **What step 1 proves depends on the direction.** On a send leg every non-claim
1473
- * leaf pays the trader's own committed address or needs the trader's own
1474
- * signature, so "spent, but not by a hash-verified claim" means the money came
1475
- * back. On a receive leg those leaves are the SOLVER's and the claim leaf is the
1476
- * trader's, so the same two readings mean the opposite things — `settled` is the
1477
- * trader's own claim landing, `refunded` is the solver taking back a lockup the
1478
- * trader failed to claim. The read is identical; only the state docs differ.
1479
- *
1480
- * Two things about the receive arm that are easy to get wrong, and are asserted
1481
- * in the tests rather than left to be inferred:
1482
- *
1483
- * - **A claim is matched by its preimage, never by our txid.** The covenant's
1484
- * `nonInteractiveClaim` leaf is pinned to the trader's own payout script, so a
1485
- * claim that lands without us — covclaimd, the day it works — still pays the
1486
- * trader and is still `settled`. Matching on the txid we submitted would turn
1487
- * that success into an anomaly.
1488
- * - **`LockupFate.fate === "claimed"` maps to the state `settled`, never to the
1489
- * state `claimed`.** The two words live one layer apart: the fate is the
1490
- * chain's, the state is ours, and the state `claimed` means only that we
1491
- * submitted something.
916
+ * Throws {@link LockupContractMissing} when there is no row.
1492
917
  */
1493
- declare class RfqSwapManager {
1494
- private readonly deps;
1495
- private readonly config;
1496
- private callbacks;
1497
- private readonly swapUpdateListeners;
1498
- private readonly swapCompletedListeners;
1499
- private readonly swapFailedListeners;
1500
- private readonly actionExecutedListeners;
1501
- private readonly monitored;
1502
- /** Monitored swaps by lockup script hex, so a contract event — which names
1503
- * a script and nothing else — can find the swap it belongs to. */
1504
- private readonly byLockupScript;
1505
- /**
1506
- * Swaps whose lockup registration has been SETTLED one way or another,
1507
- * mapped to whether a contract row actually resulted. Membership is what
1508
- * stops a per-pass retry from becoming a per-pass round trip; the value is
1509
- * what keeps a swap that could never be registered from later trying to
1510
- * retire a row that does not exist, which would report a spurious failure
1511
- * on a swap that in fact succeeded.
1512
- */
1513
- private readonly registered;
1514
- /**
1515
- * Swaps whose `refundArkade` answered {@link RefundNotLocallyPossibleError}
1516
- * in this process. Membership stops the push from being re-issued every
1517
- * pass — it cannot start working on its own, and re-issuing it is the
1518
- * grind `needs_counterparty` exists to remove. Only
1519
- * {@link RfqSwapManagerCallbacks.canRefundArkade} clears it, so a caller
1520
- * with no probe learns again on the next start, when the wallet that can
1521
- * sign may well have been restored.
1522
- */
1523
- private readonly refundRefused;
1524
- /**
1525
- * The last error a receive swap's claim callback threw, by rfqId.
1526
- *
1527
- * Kept only to tell two terminal outcomes apart once the claim window
1528
- * shuts: a swap whose claim was attempted and kept failing ends `failed`
1529
- * with that reason, while one that simply never became claimable ends
1530
- * `refunded`. Without it a broken claim callback would resolve a caller's
1531
- * {@link waitForSwapCompletion} as an ordinary unwind.
1532
- *
1533
- * Process-local, like {@link refundRefused}: after a restart the same swap
1534
- * ends `refunded` instead, which costs the caller a reason and nothing else
1535
- * — every throw was already reported through `onSwapFailed` as it happened.
1536
- */
1537
- private readonly lastClaimError;
1538
- /**
1539
- * The lockup outpoints a receive swap's claim callback has already been
1540
- * handed, by rfqId.
1541
- *
1542
- * What this exists to prevent: a claim SUCCEEDS, and for the next few
1543
- * passes the indexer still lists those outputs as unspent. Without a
1544
- * record of what was already claimed, every one of those passes would
1545
- * re-submit the same spend, fail against the server, and report a swap
1546
- * that in fact worked as failing. With one, a re-claim happens only when
1547
- * an outpoint appears that was never claimed — a lockup funded piecemeal,
1548
- * which is legitimate and which `partiallyClaimed` exists for.
1549
- *
1550
- * Process-local: after a restart a swap with a live claim tries once more.
1551
- * That is the recovery case rather than the spam one — a claim that never
1552
- * landed leaves its outputs unspent, and one that did leaves a single
1553
- * rejection.
1554
- */
1555
- private readonly claimedOutpoints;
1556
- /** Live `onContractEvent` subscription, held so `stop()` can drop it. */
1557
- private unsubscribeContracts;
1558
- /** Terminal records, kept so a late {@link waitForSwapCompletion} still
1559
- * answers instead of throwing "not found". Cleared by {@link removeSwap}. */
1560
- private readonly finished;
1561
- private readonly waiters;
1562
- /** Records changed during the current pass, flushed through `saveSwap`. */
1563
- private readonly dirty;
1564
- /** Race guard: one action at a time per swap. */
1565
- private readonly inProgress;
1566
- private timer;
1567
- private running;
1568
- constructor(deps: RfqSwapManagerDeps, config?: RfqSwapManagerConfig);
1569
- /** Wire the money-moving half. Without it the manager only watches. */
1570
- setCallbacks(callbacks: RfqSwapManagerCallbacks): void;
1571
- onSwapUpdate(listener: SwapUpdateListener): () => void;
1572
- onSwapCompleted(listener: SwapCompletedListener): () => void;
1573
- onSwapFailed(listener: SwapFailedListener): () => void;
1574
- onActionExecuted(listener: ActionExecutedListener): () => void;
1575
- /**
1576
- * Load records and begin monitoring. Runs one pass immediately — a caller
1577
- * resuming after a restart may be well past a deadline already — then
1578
- * every `pollIntervalMs`. Records that are already terminal are kept only
1579
- * so {@link waitForSwapCompletion} can answer for them.
1580
- *
1581
- * Calling it again while running loads the records and returns rather than
1582
- * re-arming — dropping them silently would strand a funded swap on a
1583
- * caller's harmless double-start.
1584
- */
1585
- start(swaps?: readonly RfqSwap[]): Promise<void>;
1586
- /**
1587
- * Stop monitoring and clear the timer. In-flight actions are not
1588
- * cancellable and run to completion; outstanding
1589
- * {@link waitForSwapCompletion} promises are left pending, since
1590
- * stop/start is a pause rather than a cancellation.
1591
- *
1592
- * The contract subscription is dropped too — an open stream with nothing
1593
- * reacting to it is a leak, and {@link start} puts it back. What is NOT
1594
- * undone is the contract registration: those rows are the wallet's, they
1595
- * outlive this manager's lifecycle, and dropping them would unwatch a
1596
- * lockup that is still funded.
1597
- */
1598
- stop(): Promise<void>;
1599
- /** Begin monitoring a swap. Polled immediately when the manager is running,
1600
- * so a just-funded swap does not wait out a whole interval. */
1601
- addSwap(swap: RfqSwap): Promise<void>;
1602
- /** Forget a swap entirely, monitored or finished.
1603
- *
1604
- * Its contract row is left alone: registration is a wallet-level fact about
1605
- * a script that may still hold money, and this call says only that THIS
1606
- * manager stops driving the swap. Retiring the row is reserved for a swap
1607
- * that reached a terminal state, where the lockup is provably done. */
1608
- removeSwap(rfqId: string): Promise<void>;
1609
- /** Every swap still being monitored. */
1610
- getPendingSwaps(): Promise<RfqSwap[]>;
1611
- hasSwap(rfqId: string): Promise<boolean>;
1612
- /** True while an action for this swap holds the per-swap lock. */
1613
- isProcessing(rfqId: string): Promise<boolean>;
1614
- getStats(): Promise<{
1615
- isRunning: boolean;
1616
- monitoredSwaps: number;
1617
- finishedSwaps: number;
1618
- inProgress: number;
1619
- pollIntervalMs: number;
1620
- }>;
1621
- /**
1622
- * Run one monitoring pass over every swap now.
1623
- *
1624
- * {@link start} calls this on an interval, but it is public on purpose: a
1625
- * caller that sleeps its process (a mobile app resuming, a service worker
1626
- * waking) wants a pass on that event rather than at the next tick. Passes
1627
- * do not overlap per swap — the in-progress lock makes a concurrent call a
1628
- * no-op for any swap already being worked on.
1629
- */
1630
- poll(): Promise<void>;
1631
- /**
1632
- * Resolve once this swap's PAYOUT is decided — which for onchain-send is
1633
- * the L1 claim, not the end of the record's life: once `claimTxid` is set
1634
- * the trader has the coins it swapped for, and what remains is the manager
1635
- * watching the Arkade lockup close. That holds however the record is
1636
- * labelled afterwards, `needs_counterparty` included. Lightning-send has no
1637
- * such split and resolves at `settled`/`refunded`, and so does lightning
1638
- * receive — see {@link isPayoutDecided} for why its own claim txid does not
1639
- * decide it.
1640
- *
1641
- * Rejects only on `failed`. `refunded` resolves: on a send leg a refund is
1642
- * an outcome the caller asked this manager to drive, not an exception. On a
1643
- * receive leg it is the swap being lost, which is still an answer and not
1644
- * an error — read `state`, do not infer success from resolution.
1645
- */
1646
- waitForSwapCompletion(rfqId: string): Promise<RfqSwapOutcome>;
1647
- private track;
1648
- /** Drops the swap from BOTH indexes. The event index is the one that stops
1649
- * a late event finding a swap that is gone; `pollSwap`'s own
1650
- * `monitored` check would also catch it, and deliberately still does —
1651
- * either alone is sufficient, which is what keeps a future change to one of
1652
- * them from silently re-driving a cancelled swap. */
1653
- private untrack;
1654
- /**
1655
- * Turn the indexer's push into an extra reason to run a pass — and nothing
1656
- * more.
1657
- *
1658
- * **This is deliberately not a source of truth.** An event names a script;
1659
- * the reaction is to run the ordinary pass for the swap at that script, and
1660
- * that pass re-reads the lockup through {@link readLockupFate} exactly as
1661
- * the timer's pass does. So an event that is missed, duplicated, reordered
1662
- * or outright FORGED can only cost or save latency — it can never change
1663
- * what this manager believes about a swap, and it can never on its own
1664
- * cause a claim or a refund. That property is what makes it safe to bolt a
1665
- * best-effort stream onto a money path, and it must survive any future
1666
- * change here: the moment an event is BELIEVED rather than merely acted on,
1667
- * a relay outage becomes a correctness problem instead of a latency one.
1668
- *
1669
- * The timer stays armed regardless, and is the failsafe. Every deadline
1670
- * that moves money — `refundLocktime`, the L1 claim window — is an absolute
1671
- * timelock that passes whether or not a single event ever arrives.
1672
- */
1673
- private subscribe;
1674
- /**
1675
- * Register this swap's lockup with the wallet's contract manager, once.
1676
- *
1677
- * The backstop, not the primary site: `requestLightningSend` /
1678
- * `requestOnchainSend` register before the caller can fund, so this covers
1679
- * swaps whose records predate that — and costs nothing when it does not,
1680
- * since `createContract` is first-writer-wins.
1681
- *
1682
- * Best-effort by design: a failure here is reported and retried on the next
1683
- * pass, and never aborts the pass it is part of. Registration buys latency
1684
- * and puts the lockup in the wallet's contract set; it decides nothing. The
1685
- * money path below it reads the indexer directly and is gated on timelocks
1686
- * that a missing contract row has no bearing on, so failing the pass over
1687
- * this would trade a real deadline for a bookkeeping one.
1688
- */
1689
- private ensureRegistered;
1690
- /** Stop watching a finished swap's lockup. Retained, not deleted: the row
1691
- * is what keeps the lockup's own VTXOs annotatable and its history
1692
- * readable, while `retained` is what drops it from the subscription and
1693
- * the poll — a settled swap that stayed watched would cost the wallet a
1694
- * script for its whole life. Best-effort — the swap is over either way. */
1695
- private retireContract;
1696
- private arm;
1697
- private pollSwap;
1698
- private runPass;
1699
- /**
1700
- * The receive leg's whole state machine: claim the solver-funded lockup
1701
- * while the window is open, and recognise the shapes in which it can be
1702
- * lost.
1703
- *
1704
- * **The window closes at `refundLocktime`, on wall clock, with no margin.**
1705
- * Both halves of that are deliberate. It closes there because publishing
1706
- * `P` into the solver's live refund window risks losing the race and
1707
- * handing over the preimage anyway — the hazard `ONCHAIN_CLAIM_MARGIN_SECONDS`
1708
- * guards on the L1 side. It takes no margin because the two situations are
1709
- * not alike: that one budgets for confirmation depth, while this claim is an
1710
- * offchain spend that lands in seconds. Wall clock is already the
1711
- * conservative reading — the solver's leaf is a CLTV, which matures against
1712
- * median-time-past, and MTP trails wall clock — so the real window extends
1713
- * PAST this deadline rather than ending before it. Every second of margin
1714
- * subtracted here is a second of live claim window given away for nothing.
1715
- *
1716
- * **The trader has no move after it.** Nothing here can take the lockup
1717
- * back, so once the window shuts the swap is the solver's to resolve and
1718
- * this manager's job is to watch it happen and then stop.
1719
- */
1720
- private driveReceiveClaim;
1721
- /**
1722
- * Claim what the solver funded, once it is enough.
1723
- *
1724
- * The value gate here decides WHEN to act. `pushClaim`'s decides whether
1725
- * `P` is published, and runs with nothing between it and the signature —
1726
- * the check that matters is the inner one, and this is not a reason to
1727
- * relax it.
1728
- */
1729
- private claimIfFunded;
1730
- /** `handled` ends the pass; `continue` falls through to the refund gate. */
1731
- private driveOnchain;
1732
- private driveArkadeRefund;
1733
- /** Whether any of these outputs has never been handed to the claim
1734
- * callback — the only reason to claim a lockup a second time. */
1735
- private hasUnclaimedOutpoint;
1736
- private rememberClaimed;
1737
- /**
1738
- * L1 progress, which past the refund window must not overwrite a refusal.
1739
- * The two halves are independent — a claimed fill says nothing about
1740
- * whether this wallet can take the Arkade lockup back — and `claimed` is
1741
- * re-asserted from chain on every pass, so without this a blocked swap
1742
- * would flip between the two states forever. The claim itself always runs;
1743
- * only the label defers, and only once the refund is the live half.
1744
- */
1745
- private setOnchainState;
1746
- /** The probe's refusal reason, or `undefined` when a local refund is
1747
- * possible as far as anyone here can tell. A probe that throws is treated
1748
- * as a refusal: a capability check that cannot answer is not a yes. */
1749
- private probeRefusal;
1750
- /** Report that no local refund will happen, without ending the swap. */
1751
- private block;
1752
- /** The way back out, taken as soon as the swap becomes actionable again.
1753
- * Back to what the record can prove, not to `pending` unconditionally: a
1754
- * swap that already made its claim has a txid for it, and reporting that
1755
- * swap as `pending` would un-say something true. */
1756
- private unblock;
1757
- private touch;
1758
- private setState;
1759
- /** Terminal failure. The `onSwapFailed` emission is left to
1760
- * {@link finalize}, so this does not double-report. */
1761
- private fail;
1762
- private emitFailed;
1763
- private emitAction;
1764
- /** Whether the record is now persisted — false only when `saveSwap` threw. */
1765
- private save;
1766
- /**
1767
- * Drop a terminal swap from monitoring and report it exactly once.
1768
- *
1769
- * `onSwapCompleted` and `onSwapFailed` are mutually exclusive here, unlike
1770
- * Boltz's manager, which fires completion for every swap that leaves
1771
- * monitoring including the failed ones — a listener named "completed" that
1772
- * also fires on failure is a trap worth not inheriting.
1773
- */
1774
- private finalize;
1775
- private settleWaiters;
1776
- }
1777
- /** What {@link RfqSwapManager.waitForSwapCompletion} reports. `txid` is the
1778
- * trader's own claim — L1 for a claimed onchain send, Arkade for a claimed
1779
- * receive — or the ark txid for a refund the trader pushed; a solver-side
1780
- * settlement or refund carries none, and a receive swap that ended `refunded`
1781
- * carries none either, however far its claim got (see {@link outcomeOf}). So
1782
- * a `txid` here always names something that happened, and `state` remains the
1783
- * only thing to read for whether the swap paid out. */
1784
- interface RfqSwapOutcome {
1785
- state: RfqSwapState;
1786
- txid?: string;
1787
- }
918
+ declare function lockupContractParams(contracts: LockupContractReader, lockupAddress: string): Promise<Record<string, string>>;
1788
919
 
1789
920
  /**
1790
921
  * One swap, flattened to what grouping needs: an identity, a corridor, an
1791
922
  * outcome, and every Arkade transaction that belongs to it.
1792
923
  *
1793
924
  * Deliberately not a stored swap record itself — resolution should stay
1794
- * testable with plain data rather than a repository. A correlation helper
1795
- * that derives these from the record store and the funding lockup's VTXOs
1796
- * lands separately.
925
+ * testable with plain data rather than a repository. {@link rfqSwapActivityInputs}
926
+ * derives these from the record store and, where a record cannot answer, the
927
+ * funding lockup's VTXOs.
1797
928
  */
1798
929
  interface SwapActivityInput {
1799
930
  rfqId: string;
1800
- /**
1801
- * Literal union rather than `RfqSwapRecord["kind"]` — `RfqSwapRecord` lives
1802
- * only on the unmerged rfq-persistence branch, not on master. Reconcile
1803
- * with `RfqSwapRecord["kind"]` once that branch lands.
1804
- */
1805
- kind: "lightning_send" | "lightning_receive" | "onchain_send";
931
+ kind: RfqSwapRecord["kind"];
1806
932
  state: RfqSwapState;
1807
933
  /** Funding, claim and refund txids, in whatever order. */
1808
934
  txids: readonly string[];
@@ -1824,5 +950,32 @@ interface SwapActivityInput {
1824
950
  declare function swapActivityResolver(deps: {
1825
951
  listSwaps(): Promise<readonly SwapActivityInput[]>;
1826
952
  }): ActivityResolver;
953
+ interface RfqSwapActivityDeps {
954
+ repository: Pick<AssetSwapRepository, "getAllRfqSwaps">;
955
+ /**
956
+ * Consulted only for what a record cannot answer: a record written before
957
+ * `fundingArkTxid` existed, and the counterparty's spend on a swap that
958
+ * ended without a refund of ours.
959
+ *
960
+ * Optional because the stored fields are the primary source — cheaper, and
961
+ * they work offline, which is the resolver's whole posture. An indexer that
962
+ * throws costs that record its extra txids and nothing else.
963
+ */
964
+ indexer?: LockupSpendIndexer;
965
+ }
966
+ /**
967
+ * Every stored RFQ swap, flattened into what {@link swapActivityResolver}
968
+ * groups on.
969
+ *
970
+ * The txids come from four places, in order of preference: the record's own
971
+ * `fundingArkTxid` and `refundArkTxid`, the corridor's `activityTxids` (the
972
+ * receive leg's Arkade claim, the onchain leg's L1 one), and — only when the
973
+ * first two cannot answer — one read of the lockup's VTXOs.
974
+ *
975
+ * A missing txid costs an activity a row, never a wrong one: a swap that
976
+ * contributes fewer txids simply leaves those transactions ungrouped, which is
977
+ * what they already are.
978
+ */
979
+ declare function rfqSwapActivityInputs(deps: RfqSwapActivityDeps): Promise<SwapActivityInput[]>;
1827
980
 
1828
- export { type ArkadeRefundResult, AssetSwap, AssetSwapRepository, ChainSource, ChainUtxo, type ClaimArkProvider, type ClaimPacketInput, type DiscoverMarketsOptions, IndexedDbAssetSwapRepository, type LightningReceiveSwap, type LightningSendSwap, LockupAmountMismatchError, type LockupContractWriter, type LockupFate, LockupNeedsRecoveryError, LockupRegistrationFailed, type LockupSpendIndexer, type LockupVtxo, MarketsCacheEntry, OFFER_PACKET_TYPE, type Offer, type OfferContractRetirer, type OfferSwapWatcher, OnchainHtlc, OnchainHtlcPhase, type OnchainSendAction, type OnchainSendSwap, type PlanError, QUOTE_OPTIONS, REFUND_MTP_LAG_SECONDS, RFQ_RESOLVED_STATES, RFQ_SWAP_TERMINAL_STATES, type RefundArkProvider, type RefundBlockedReason, type RefundIndexer, RefundNotLocallyPossibleError, type RefundOutcome, type RestoreIndexer, RfqStatus, type RfqSwap, type RfqSwapActionName, type RfqSwapLockup, RfqSwapManager, type RfqSwapManagerCallbacks, type RfqSwapManagerConfig, type RfqSwapManagerDeps, type RfqSwapManagerEvents, type RfqSwapOutcome, type RfqSwapState, RfqTransport, SWAP_LOCKUP_CONTRACT_KIND, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_TYPE, type SealedClaimPacket, type SpendKind, type SwapActivityInput, type SwapContractRegistry, type Tx, type WatchOfferSwapsParams, awaitLockupFunding, awaitRfqResolution, cancelOffer, claimReceiveLockup, classifyDepositSpend, classifySpend, createOffer, decodeOffer, discoverMarkets, encodeOffer, findLockupVtxos, findMarket, isRfqSwapTerminal, isRfqTerminal, makeCachedFeedFetch, nextOnchainAction, offerVtxoScript, pushClaim, pushRefundWithoutReceiver, readLockupFate, refundIfUnresolved, registerLockupContract, restoreAssetSwaps, retireSettledOfferContracts, sealClaimPacket, senderIdentityForSwapRecord, spendTxidsOf, spendUpdate, swapActivityResolver, swapPrograms, validatePlan, watchOfferSwaps };
981
+ export { ArkadeRefundResult, type ArkadeRefunderDeps, AssetSwap, AssetSwapRepository, type ClaimArkProvider, type ClaimPacketInput, type DiscoverMarketsOptions, IndexedDbAssetSwapRepository, type LightningReceiveProfile, type LightningSendProfile, LockupAmountMismatchError, LockupContractMissing, type LockupContractReader, type LockupContractWriter, LockupRegistrationFailed, LockupSpendIndexer, LockupVtxo, MarketsCacheEntry, OFFER_PACKET_TYPE, type Offer, type OfferContractRetirer, type OfferSwapWatcher, OnchainHtlc, OnchainHtlcParams, OnchainNetwork, type OnchainSendProfile, type PlanError, QUOTE_OPTIONS, RefundArkProvider, type RefundBlockedReason, RefundIndexer, RefundNotLocallyPossibleError, type RestoreIndexer, type RfqClaimSecretProjection, type RfqHashlockProjection, type RfqSignerProjection, RfqSwap, type RfqSwapActivityDeps, RfqSwapRecord, RfqSwapState, SWAP_LOCKUP_CONTRACT_KIND, SWAP_LOCKUP_CONTRACT_LABEL, SWAP_LOCKUP_CONTRACT_TYPE, type SealedClaimPacket, type SpendKind, type SwapActivityInput, SwapSecretsProjection, type Tx, type WatchOfferSwapsParams, arkadeRefunder, awaitLockupFunding, cancelOffer, claimReceiveLockup, classifyDepositSpend, classifySpend, createOffer, decodeOffer, discoverMarkets, encodeOffer, findMarket, lockupContractParams, makeCachedFeedFetch, offerVtxoScript, onchainSendProfile, pushClaim, registerLockupContract, restoreAssetSwaps, retireSettledOfferContracts, rfqClaimSecretOf, rfqSecretsProfile, rfqSignerOf, rfqSwapActivityInputs, sealClaimPacket, senderIdentityForSwapRecord, spendTxidsOf, spendUpdate, swapActivityResolver, swapPrograms, validatePlan, watchOfferSwaps };