@arkade-os/swap 0.0.12 → 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1055 @@
1
+ import { SQLExecutor } from '@arkade-os/sdk/repositories/sqlite';
2
+ import { Corridor as Corridor$1, DiscoveredMarket } from '@arkade-os/solver-discovery';
3
+ import { VHTLC, NetworkName } from '@arkade-os/sdk';
4
+
5
+ /**
6
+ * The platform's per-user configuration directory.
7
+ *
8
+ * The three conventions, in the order each platform expects:
9
+ *
10
+ * - Linux and the BSDs follow the XDG Base Directory spec — `$XDG_CONFIG_HOME`
11
+ * when set to an absolute path, else `~/.config`. A relative value is
12
+ * ignored, which the spec requires rather than suggests.
13
+ * - macOS uses `~/Library/Application Support`.
14
+ * - Windows uses `%APPDATA%`, falling back to the roaming path under the home
15
+ * directory when the variable is missing — as it is in some service contexts.
16
+ */
17
+ declare const configDir: (env?: NodeJS.ProcessEnv) => string;
18
+ /**
19
+ * The default database path for one network.
20
+ *
21
+ * Returned rather than created: opening it is what creates the directory, and a
22
+ * caller that only wants to know where the file would be — to report it, to
23
+ * back it up, to delete it — should not have to make one as a side effect.
24
+ */
25
+ declare const swapDatabasePath: (network: string, env?: NodeJS.ProcessEnv) => string;
26
+
27
+ type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
28
+ /**
29
+ * The record fields a wallet-provisioned secret becomes — what
30
+ * {@link swapSecretsToRecord} emits, and what every record type carrying swap
31
+ * secrets embeds.
32
+ *
33
+ * A named type rather than four fields restated per record: the mapper and the
34
+ * records it feeds must agree exactly, and a record that silently omits one of
35
+ * these round-trips a swap whose preimage cannot be re-derived. Embedding makes
36
+ * the omission a compile error instead.
37
+ *
38
+ * **Only `preimageHex` is secret.** `signingDescriptor` and `preimageSaltHex`
39
+ * are public derivation inputs — they must survive a field-mapped backend, but
40
+ * they leak nothing without the seed.
41
+ */
42
+ interface SwapSecretsProjection {
43
+ /**
44
+ * The wallet descriptor this swap's sender key comes from — a fresh HD
45
+ * child, or a static wallet's `tr(pubkey)`. Public — the signer
46
+ * re-derives from the wallet, so the record carries no key material.
47
+ */
48
+ signingDescriptor?: string;
49
+ /** P, hex, when it cannot be re-derived from the seed at all: the user
50
+ * supplied it, or the signer cannot sign deterministically. The swap's only
51
+ * claim secret when present. */
52
+ preimageHex?: string;
53
+ /**
54
+ * The salt P derives from, hex, on the salted arm — what a static wallet
55
+ * gets instead of storing P. **Public**, and unlike every other field here
56
+ * it is minted per swap: it is what stops one repeating key from handing
57
+ * every swap the same preimage.
58
+ */
59
+ preimageSaltHex?: string;
60
+ }
61
+ interface AssetSwap extends SwapSecretsProjection {
62
+ /** Funding txid — the swap's identity. */
63
+ id: string;
64
+ /** 'btc' or a 68-hex asset id. */
65
+ fromAsset: string;
66
+ toAsset: string;
67
+ /** Atomic amounts as strings (bigint is not JSON-safe). */
68
+ fromAmount: string;
69
+ /** The covenant wantAmount — a floor, the fill pays >= this. */
70
+ toAmount: string;
71
+ swapAddress: string;
72
+ /** Hex pkScript of the swap contract — the indexer monitoring key. */
73
+ swapPkScript: string;
74
+ /** TLV offer — needed to rebuild the contract for cancel. */
75
+ offerHex: string;
76
+ fundingTxid: string;
77
+ spentTxid?: string;
78
+ status: AssetSwapStatus;
79
+ createdAt: number;
80
+ completedAt?: number;
81
+ /** RFQ pair string, e.g. `arkade:BTC->onchain:BTC`. */
82
+ pair?: string;
83
+ /** `sha256(P)`, hex. Public, and how a restore confirms a candidate
84
+ * derivation is the right one. */
85
+ paymentHash?: string;
86
+ /** The L1 HTLC's pkScript, hex — the chain-watch key. */
87
+ htlcPkScriptHex?: string;
88
+ htlcLocktime?: number;
89
+ /** The L1 funding txid, once observed. */
90
+ l1Txid?: string;
91
+ }
92
+
93
+ interface OnchainHtlc {
94
+ address: string;
95
+ /** `0x5120…` — the P2TR output script. */
96
+ pkScript: Uint8Array;
97
+ leaves: {
98
+ claim: Uint8Array;
99
+ refund: Uint8Array;
100
+ };
101
+ /** Serialized control blocks per leaf, ready for a script-path witness. */
102
+ controlBlocks: {
103
+ claim: Uint8Array;
104
+ refund: Uint8Array;
105
+ };
106
+ paymentHash: string;
107
+ refundLocktime: number;
108
+ }
109
+
110
+ /**
111
+ * Where a monitored RFQ swap stands, and which of those states end it.
112
+ *
113
+ * Its own module rather than a corner of `swapManager.ts` because the
114
+ * dependency runs the other way: the record layer decides retention from
115
+ * {@link isRfqSwapTerminal}, and the manager persists through the record
116
+ * layer. With the vocabulary here, neither has to import the other at runtime.
117
+ * `swapManager.ts` re-exports all three names, so nothing about the public
118
+ * surface moved.
119
+ */
120
+ /**
121
+ * Where a monitored swap stands.
122
+ *
123
+ * `claimable` and `claimed` are the states of a swap the TRADER has something
124
+ * to claim on: the L1 fill on an onchain send, and the solver-funded lockup on
125
+ * a receive. Only `lightning_send` has neither — there the solver claims the
126
+ * lockup, and the trader's only move is the refund.
127
+ */
128
+ type RfqSwapState =
129
+ /** Live; nothing actionable yet. On a receive leg this covers the whole
130
+ * stretch before the solver funds anything. */
131
+ "pending"
132
+ /** There is something for the trader to take, and the window to take it is
133
+ * open: the confirmed L1 fill on an onchain send, or a lockup funded for at
134
+ * least `expectedAmount` on a receive. */
135
+ | "claimable"
136
+ /**
137
+ * The trader's claim has been made — its L1 broadcast on an onchain send,
138
+ * its Arkade submission on a receive.
139
+ *
140
+ * **On a receive this is a local belief and not a chain fact**, which is
141
+ * why it is not terminal: `settled` is the chain's answer, and `refunded`
142
+ * is still reachable from here if the claim never lands and the solver
143
+ * takes the lockup back.
144
+ */
145
+ | "claimed"
146
+ /**
147
+ * This wallet will not act, and only the counterparty can change that.
148
+ *
149
+ * On a send leg: the Arkade refund cannot be pushed from here — no secrets
150
+ * on the record, a descriptor from another seed, or nothing wired to act —
151
+ * so the lockup comes back only if the counterparty claims it or the wallet
152
+ * that can sign it is restored. On a receive leg: the trader holds no
153
+ * refund at all, so this is a lockup that cannot be claimed — funded for
154
+ * less than the swap agreed (publishing `P` for it is the whole attack
155
+ * `LockupAmountMismatchError` exists to refuse), or one whose claim window
156
+ * shut unclaimed. `RfqSwapCommon.blockedReason` says which.
157
+ *
158
+ * **Not terminal, and not a dead end.** The money is still at the lockup,
159
+ * so the counterparty's move is still observable and still ends the swap;
160
+ * and the refusal is re-checked every pass, so restoring the right wallet,
161
+ * wiring the callbacks, or the solver topping the lockup up returns the
162
+ * swap to `pending` and resumes the normal drive. For an onchain-send swap
163
+ * it says nothing about the L1 half, which keeps being driven and claimed.
164
+ */
165
+ | "needs_counterparty"
166
+ /**
167
+ * Terminal: the lockup was spent by a hash-verified claim. Read off chain,
168
+ * never reported.
169
+ *
170
+ * On a send leg that claim is the counterparty's, and it is proof the
171
+ * counterparty completed its side. On a receive leg it is the TRADER's own
172
+ * — matched by the hash and not by our txid, so a claim that lands without
173
+ * us still counts (see `RfqSwapManager`).
174
+ */
175
+ | "settled"
176
+ /**
177
+ * Terminal: the lockup was spent by something other than a claim.
178
+ *
179
+ * On a send leg that is the money coming back, by the solver's hand or the
180
+ * trader's. **On a receive leg it is a LOSS**: the lockup was the solver's
181
+ * money, every non-claim leaf is the solver's, and a swap that ends here
182
+ * ended with the trader's incoming payment never arriving. It is also where
183
+ * a receive swap ends when its window closes with nothing left to observe —
184
+ * see `RfqSwapManager`.
185
+ */
186
+ | "refunded"
187
+ /** Terminal: an action failed and its window closed. */
188
+ | "failed";
189
+
190
+ /**
191
+ * What the manager needs to register a swap's lockup with the wallet, so the
192
+ * indexer pushes its funding and its spend instead of being asked every few
193
+ * seconds.
194
+ *
195
+ * Both fields are things the caller already holds. `script` is the very object
196
+ * `pushRefundWithoutReceiver` and `pushClaim` take, so a caller wired to act has
197
+ * it in hand; `address` is the request entrypoint's own return value. The
198
+ * address is taken rather than re-derived on purpose — the row's address must be
199
+ * the one that was actually funded, and a local re-derivation would silently use
200
+ * the SDK's default network, which is the exact bug `registerOfferContract`
201
+ * guards against.
202
+ */
203
+ interface RfqSwapLockup {
204
+ /** The covenant. Its `pkScript` MUST equal the record's `lockupPkScript`. */
205
+ script: InstanceType<typeof VHTLC.ScriptV2>;
206
+ /** The Arkade address that was funded. */
207
+ address: string;
208
+ }
209
+ interface RfqSwapCommon {
210
+ /** The negotiation id — this record's identity. */
211
+ rfqId: string;
212
+ state: RfqSwapState;
213
+ /** The Arkade lockup's scriptPubKey — `swapPkScript` from any of the four
214
+ * request entrypoints. This is what the manager watches to decide the swap:
215
+ * it is the only handle on the covenant whose spend witness says whether
216
+ * the swap settled or came back. */
217
+ lockupPkScript: Uint8Array;
218
+ /** The covenant behind {@link lockupPkScript}, when the caller wants the
219
+ * lockup registered with a contract manager. Optional: without it the
220
+ * manager still watches the swap on its timer, it just cannot subscribe.
221
+ * See {@link RfqSwapManagerDeps.contracts}. */
222
+ lockup?: RfqSwapLockup;
223
+ /**
224
+ * `sha256(P)`, hex — the quote's `payment_hash`. The claim leaf can only
225
+ * be spent by revealing a value that hashes to this, which is what makes a
226
+ * settlement provable rather than reported. For an onchain send this is
227
+ * the SAME hash the L1 `htlc` carries: one `P` unlocks both legs.
228
+ *
229
+ * True of the three corridors that exist today and only of them: a hashlock
230
+ * belongs to a CORRIDOR, and a banco-style one settles without any. The
231
+ * stored record already says so — the hash lives in `profile.hashlock`, not
232
+ * on `RfqSwapRecord` — and this field follows onto the per-corridor swap
233
+ * types when the first such corridor lands. Do not read the current shape as
234
+ * settled.
235
+ */
236
+ paymentHash: string;
237
+ /**
238
+ * `refund_locktime` from the quote, unix seconds.
239
+ *
240
+ * Whose deadline it is inverts with the direction, and so does what to do
241
+ * about it. On a send leg it is the TRADER's: the lockup is the trader's
242
+ * money and this gates the refund that takes it back, so it is a moment to
243
+ * act AFTER. On a receive leg it is the SOLVER's: the trader has no refund
244
+ * leaf at all, and this is the moment to have claimed BEFORE.
245
+ */
246
+ refundLocktime: number;
247
+ createdAt: number;
248
+ updatedAt: number;
249
+ /** Set once the trader's own `refundWithoutReceiver` push landed. */
250
+ refundTxid?: string;
251
+ /**
252
+ * The ark transactions that SPENT the lockup, stamped from the chain read
253
+ * that ended the swap — `LockupFate.spends`, whichever verdict it reached.
254
+ *
255
+ * The counterparty's move, on every leg but one: a solver claim on a send,
256
+ * a solver reclaim on a receive, and — the exception — the trader's own
257
+ * claim when a receive settles. What they have in common is that no local
258
+ * action produced them, so nothing else on this record can name them:
259
+ * {@link refundTxid} names only a push this wallet made, and
260
+ * `claimTxid` only a submission it made.
261
+ *
262
+ * Stamped so a terminal record answers "which transaction ended this" from
263
+ * storage. Without it the only source is another read of the lockup — a
264
+ * network round trip per terminal swap, which is what activity correlation
265
+ * has to pay on the offline-first path where it is least affordable.
266
+ *
267
+ * Absent when the swap ended without a chain verdict, or when the indexer
268
+ * named the checkpoint but not the ark transaction — the same `txid`
269
+ * `LockupSpend` declares optional, for the same reason.
270
+ */
271
+ lockupSpendTxids?: string[];
272
+ /** Why `state` is `failed`. */
273
+ failure?: string;
274
+ /**
275
+ * Last local claim error while the receive swap is still retryable.
276
+ *
277
+ * Distinct from terminal `failure`: this records a claim attempt that failed
278
+ * before the window closed. If the window later closes without a submitted
279
+ * claim, it becomes the terminal failure reason.
280
+ */
281
+ claimFailure?: string;
282
+ /** Why `state` is `needs_counterparty`. Distinct from {@link failure},
283
+ * which means an action was attempted and did not work. */
284
+ blockedReason?: string;
285
+ }
286
+ /** `arkade:BTC->lightning:BTC`. Nothing for the trader to claim: the solver
287
+ * claims the lockup with the preimage it learns by paying the invoice — which
288
+ * is exactly why that spend's witness is proof the payment landed. */
289
+ interface LightningSendSwap extends RfqSwapCommon {
290
+ kind: "lightning_send";
291
+ }
292
+ /** `arkade:BTC->onchain:BTC`. Carries the L1 half the trader must claim. */
293
+ interface OnchainSendSwap extends RfqSwapCommon {
294
+ kind: "onchain_send";
295
+ /** The locally derived HTLC from `requestOnchainSend` — the manager reads
296
+ * `pkScript`, `paymentHash` and `refundLocktime` off it to classify. */
297
+ htlc: OnchainHtlc;
298
+ /** `profile.min_confirmations` from the quote. */
299
+ minConfirmations: number;
300
+ /** Where {@link RfqSwapManagerCallbacks.claimOnchain} pays; optional only
301
+ * so records predating its profile slot still restore. */
302
+ payoutPkScript?: Uint8Array;
303
+ /** The fill's outpoint, learned on first sighting. Without it a SPENT
304
+ * HTLC reads as never funded — see {@link classifyOnchainHtlc}. */
305
+ funding?: {
306
+ txid: string;
307
+ vout: number;
308
+ };
309
+ /** Our L1 claim's txid. */
310
+ claimTxid?: string;
311
+ }
312
+ /**
313
+ * `lightning:BTC->arkade:BTC`. The inverted leg: the SOLVER funds the lockup
314
+ * and the TRADER claims it, and that claim is what publishes `P` and lets the
315
+ * solver settle the payer's held Lightning HTLC.
316
+ *
317
+ * Two consequences shape how this record is driven, both of them absent from
318
+ * the send legs:
319
+ *
320
+ * - **There is no trader-side refund.** Every non-claim leaf of this covenant
321
+ * is the solver's, so the manager never calls
322
+ * {@link RfqSwapManagerCallbacks.refundArkade} for one of these. A swap that
323
+ * is not claimed is simply lost — the solver reclaims at
324
+ * {@link RfqSwapCommon.refundLocktime} and the payer is refunded when the
325
+ * held HTLC lapses.
326
+ * - **The claim is the whole swap, and it is on a deadline.** The trader must
327
+ * be online for it: covclaimd cannot claim this covenant today, so the claim
328
+ * packet's offline path does not run.
329
+ */
330
+ interface LightningReceiveSwap extends RfqSwapCommon {
331
+ kind: "lightning_receive";
332
+ /**
333
+ * What the lockup must carry — the quote's `to_amount`, captured at REQUEST
334
+ * time and persisted with the record.
335
+ *
336
+ * **Not re-derivable, and not optional.** Captured at claim time it would
337
+ * be whatever the solver funded, which is the dust-funding attack rather
338
+ * than a check on it. A record that reaches the manager without a finite
339
+ * value here is reported `needs_counterparty` and never claimed: a
340
+ * comparison against `undefined` or `NaN` is false, so an unusable
341
+ * comparand does not fail the value gate, it deletes it.
342
+ */
343
+ expectedAmount: number;
344
+ /** Our Arkade claim's txid, once submitted. Set from the callback's return
345
+ * and never from a chain read — the chain's answer is `settled`. */
346
+ claimTxid?: string;
347
+ }
348
+
349
+ /**
350
+ * The swap kinds this projection covers — all three the manager monitors.
351
+ *
352
+ * `onchain_send` carries an L1 half nothing else can rebuild. Its Arkade lockup
353
+ * has a contract row like the others, but the HTLC is Bitcoin L1, not an Arkade
354
+ * contract, so no row exists for it; and `OnchainHtlc` exposes only derived
355
+ * values — `address`, `pkScript`, `leaves`, `controlBlocks` — never the
356
+ * `claimKey`/`refundKey` `onchainHtlcScript` takes as inputs. So they ride in
357
+ * that corridor's {@link RfqSwapOrigin.profile}, and without them a restored
358
+ * swap would let its L1 refund window pass unwatched.
359
+ */
360
+ type PersistableRfqSwap = LightningSendSwap | LightningReceiveSwap | OnchainSendSwap;
361
+ /** The immutable request-time half, and only what EVERY corridor has. Hex for
362
+ * everything binary, so the record is plain JSON and survives any
363
+ * structured-clone backend unchanged. */
364
+ interface RfqSwapOrigin {
365
+ /**
366
+ * Which corridor this is. Resolves the handler that owns {@link profile};
367
+ * see `rfqCorridor.ts`.
368
+ *
369
+ * The manager's own union, not an open string: `RfqSwapManager` branches on
370
+ * `kind` to decide what to drive, so a corridor it does not know could be
371
+ * persisted and rebuilt here and then never driven — which is the failure
372
+ * this whole file is arranged to make impossible.
373
+ */
374
+ kind: PersistableRfqSwap["kind"];
375
+ /**
376
+ * The Arkade address that was funded.
377
+ *
378
+ * Both the swap's handle on its covenant — {@link lockupContractParams}
379
+ * looks the contract row up by the script it decodes to — and, being taken
380
+ * from the entry point rather than re-derived, the check that the
381
+ * parameters a caller supplies belong to THIS swap.
382
+ */
383
+ lockupAddress: string;
384
+ /**
385
+ * The corridor's own half, as plain JSON — written by the caller from the
386
+ * request result, kept current by the handler's `project`.
387
+ *
388
+ * Opaque here on purpose. Nothing in this file, the repository or the
389
+ * IndexedDB store interprets it, which is what lets a new corridor ship
390
+ * without touching any of them. It carries the corridor's keys as well as
391
+ * its state: `signer` (which wallet key signs this leg) and, on a corridor
392
+ * locked to a preimage, `hashlock` — see `rfqProfileParts.ts`, and write
393
+ * both with `rfqSecretsProfile` rather than by hand.
394
+ *
395
+ * Not a consumer scratchpad: every write merges it as `{ ...profile,
396
+ * ...handler.project(swap) }`, so a consumer key colliding with one the
397
+ * handler projects is silently overwritten on every pass.
398
+ */
399
+ profile: Record<string, unknown>;
400
+ /** Consumer display metadata. The rebuild ignores it — `RfqSwapCommon`
401
+ * carries no amount of its own. */
402
+ amount?: number;
403
+ /**
404
+ * The ark transaction that funded {@link lockupAddress}.
405
+ *
406
+ * Origin, not manager state: the caller broadcasts the funding and knows
407
+ * its txid, while the manager watches the lockup by script and never
408
+ * learns it. So it is written once at record creation, like {@link amount},
409
+ * and no corridor `project` emits it.
410
+ */
411
+ fundingTxid?: string;
412
+ }
413
+ /** The stored record: the origin plus the manager's mutable state. */
414
+ interface RfqSwapRecord extends RfqSwapOrigin {
415
+ rfqId: string;
416
+ state: RfqSwapState;
417
+ createdAt: number;
418
+ updatedAt: number;
419
+ refundTxid?: string;
420
+ /** The ark transactions that spent the lockup, stamped by the manager from
421
+ * the chain read that ended the swap. See
422
+ * `RfqSwapCommon.lockupSpendTxids`. */
423
+ lockupSpendTxids?: string[];
424
+ failure?: string;
425
+ /** Last local receive-claim error while the swap is still retryable. */
426
+ claimFailure?: string;
427
+ blockedReason?: string;
428
+ }
429
+
430
+ declare const ATOMIC_DECIMAL: unique symbol;
431
+ /** Atomic units written out: the form records and the wire hold. */
432
+ type AtomicDecimal = string & {
433
+ readonly [ATOMIC_DECIMAL]: true;
434
+ };
435
+
436
+ /**
437
+ * Asset identity for the v2 client: CAIP-19 with the rail as the CAIP-2
438
+ * namespace — `<rail>:<network>/<asset-ns>:<reference>`.
439
+ *
440
+ * The rail is the namespace rather than the settlement chain because sameness
441
+ * across rails is then a comparison on the asset part instead of a shared
442
+ * string: `arkade:bitcoin/slip44:0` and `bitcoin:bitcoin/slip44:0` are one BTC
443
+ * on two rails, and nothing has to agree on a single id for them. Arkade has no
444
+ * CAIP-2 namespace and no bitcoin-chain identity to nest under, so
445
+ * `bip122:…/arkade:…` would assert a relationship it does not have.
446
+ *
447
+ * These ids parse under CAIP-19 and resolve under no published namespace spec:
448
+ * `arkade`, `bitcoin` and `bolt11` are registered in no CASA registry. That is
449
+ * the price of naming rails instead of chains, and it costs nothing here — this
450
+ * module is the grammar, and what an id *means* is the alias layer's job.
451
+ */
452
+
453
+ /**
454
+ * A CAIP-2 namespace this client can spell. Closed rather than open to the
455
+ * CAIP-2 character class, so a rail nobody implements is a parse failure here
456
+ * rather than a lookup miss three layers down.
457
+ *
458
+ * `bolt11` is lightning: CAIP-2 caps a namespace at eight characters, which
459
+ * `lightning` overruns, and floors it at three, which `ln` misses. It names the
460
+ * instrument the rail carries today; BOLT12 is a separate corridor when it
461
+ * ships.
462
+ *
463
+ * `eip155` is grammar and nothing else. §9's EVM corridor is deferred, and §9
464
+ * exists to prove the seams hold, so its own examples have to parse; the
465
+ * refusal belongs where a route is chosen, not where a string is read, and the
466
+ * alias layer is where it happens.
467
+ */
468
+ declare const RAILS: readonly ["arkade", "bitcoin", "bolt11", "eip155"];
469
+ type Rail = (typeof RAILS)[number];
470
+ /** The rails whose CAIP-2 reference is a bitcoin network rather than a chain id. */
471
+ declare const BITCOIN_RAILS: readonly ["arkade", "bitcoin", "bolt11"];
472
+ type BitcoinRail = (typeof BITCOIN_RAILS)[number];
473
+ /**
474
+ * The network half of a bitcoin-family chain part: core's own
475
+ * {@link NetworkName}, because the wallet is the only source of the network —
476
+ * v2 accepts no server URL anywhere — and this is the vocabulary a wallet
477
+ * resolves to.
478
+ *
479
+ * It is one wider than discovery's `NETWORKS`, which omits `testnet`. That
480
+ * difference belongs to the alias layer and not to the grammar: an asset on
481
+ * testnet exists whether or not anyone publishes a market index for it, so
482
+ * amputating the identity to match the index would make a network the SDK fully
483
+ * supports unnameable.
484
+ */
485
+ type NetworkRef = NetworkName;
486
+ type BitcoinAssetId<R extends BitcoinRail> = `${R}:${NetworkRef}/${string}:${string}`;
487
+ /**
488
+ * A public asset id.
489
+ *
490
+ * A template literal type and not `string`, which is what makes the other three
491
+ * asset spellings in this package — core's 68-hex `asset.AssetId#toString()`,
492
+ * discovery's `AssetInfo.id`, the RFQ leg's `arkade:BTC` — a compile error in a
493
+ * slot that wants a public id, rather than a wrong pair string three layers
494
+ * down. The rail parameter carries that further: `AssetId<"arkade">` accepts no
495
+ * `bitcoin:` string, which is what makes an endpoint whose corridor and asset
496
+ * disagree a compile error (see `route.ts`).
497
+ *
498
+ * Deliberately not branded — `client.quote({ give: "arkade:bitcoin/slip44:0" })`
499
+ * must stay writable, and the spec's own examples are written that way — so the
500
+ * shape is what the type checks and {@link parseAssetId} is the gate for
501
+ * everything else. A value out of a record or off the wire is a `string`: parse
502
+ * it, never cast it.
503
+ */
504
+ type AssetId<R extends Rail = Rail> = R extends BitcoinRail ? BitcoinAssetId<R> : `eip155:${number}/${string}:${string}`;
505
+
506
+ /**
507
+ * The corridor axis, and its bijection with the rail namespaces.
508
+ *
509
+ * One axis, two vocabularies: discovery speaks `arkade | lightning | onchain`
510
+ * and an asset id's CAIP-2 namespace is `arkade | bolt11 | bitcoin`. They agree
511
+ * on one member of three. Collapsing them was the alternative and it loses
512
+ * either way — take the rail names and every market lookup translates on the
513
+ * way out to discovery; take the corridor names and `lightning:` overruns
514
+ * CAIP-2's eight-character namespace cap.
515
+ *
516
+ * So both stay, and the disagreement is spent once, here: `Corridor` is
517
+ * discovery's type verbatim, {@link railOfCorridor} is total, and `route.ts`
518
+ * ties an endpoint's corridor to its asset's rail in the type system so the two
519
+ * cannot disagree in a value.
520
+ */
521
+
522
+ /**
523
+ * The corridor a leg settles on.
524
+ *
525
+ * Aliased from discovery rather than re-declared: it is discovery's vocabulary,
526
+ * the alias layer has to speak it, and a re-declaration would drift silently
527
+ * the day discovery adds a corridor.
528
+ */
529
+ type Corridor = Corridor$1;
530
+ /**
531
+ * A corridor id: the three implemented corridors plus §9's EVM chains.
532
+ *
533
+ * The template arm stays open against the registry's closed enum on purpose —
534
+ * which chains a registry lists is a listing decision, not an id-grammar one.
535
+ */
536
+ type CorridorId = Corridor | `eip155:${number}`;
537
+
538
+ /**
539
+ * The two string aliases the v2 surface is written in.
540
+ *
541
+ * Aliases rather than branded types: they document what a field carries at the
542
+ * places §3.1's `Quote` reads (`lock.hash`, `solver`) without making every
543
+ * literal go through a constructor. Core exports neither, so nothing here is a
544
+ * duplicate.
545
+ */
546
+ /** Lowercase hex, no `0x` prefix — the encoding `@scure/base`'s `hex` emits. */
547
+ type Hex = string;
548
+ /** A secp256k1 public key as {@link Hex}: x-only (32 bytes) or compressed (33). */
549
+ type Pubkey = Hex;
550
+
551
+ /**
552
+ * What a quote is: the order, fully resolved, plus the provenance that says who
553
+ * priced it and where that card came from.
554
+ *
555
+ * M1 named these shapes and left them to whichever milestone decided their
556
+ * semantics; this is that milestone, so `QuoteId`, `QuoteInput`, `Quote`,
557
+ * `MarketRef` and `AuctionProvenance` are declared here rather than beside the
558
+ * types they are built out of. Nothing in this module has behaviour — the quote
559
+ * path assembles these, `accept()` (M4) consumes them.
560
+ *
561
+ * The one rule worth restating at the top: a `Quote` is binding terms plus the
562
+ * evidence for them. Every field is either something the caller must act on
563
+ * (the two obligations, the artifact, the deadline) or something they must be
564
+ * able to audit afterwards (the market, the solver, the checks that passed).
565
+ * Nothing internal rides along — no covenant, no secret, no transport.
566
+ */
567
+
568
+ /**
569
+ * A quote's identity, minted by the client at quote time.
570
+ *
571
+ * Client-minted everywhere, not just where the wire offers no id: a feed-priced
572
+ * offer quote has no solver-minted id at all, and `accept()` is idempotent by
573
+ * quote id *and only* by quote id (§3.2), so an identity that exists on one
574
+ * backend and not the other could not carry that rule. An alias rather than a
575
+ * brand, matching `Hex` and `Pubkey` beside it.
576
+ */
577
+ type QuoteId = string;
578
+ /** Which backend priced a quote. The card decides it; no client switch does. */
579
+ type MarketBackend = "rfq" | "feed";
580
+ /** Where a snapshot came from, and how fresh it is. */
581
+ interface SnapshotRef {
582
+ /** Unix ms the markets were read from their sources. */
583
+ readonly fetchedAt: number;
584
+ /**
585
+ * The registry answered in this read, so the cards are registry-served
586
+ * rather than replayed out of local storage.
587
+ *
588
+ * `false` is not an error: a stale snapshot still resolves and still prices
589
+ * a feed-priced quote — it is marked, not refused. What it cannot do is
590
+ * supply the key an addressed RFQ's responder is checked against, because
591
+ * that field is unvalidated cache content (`isMarketShaped` revalidates
592
+ * four fields and trusts the rest).
593
+ */
594
+ readonly live: boolean;
595
+ /** How the markets were obtained. */
596
+ readonly source: "live" | "cache" | "injected";
597
+ /** The registry URL behind it, or `undefined` for an injected snapshot. */
598
+ readonly registry?: string;
599
+ }
600
+ /**
601
+ * Which card priced a quote, and from which registry.
602
+ *
603
+ * A union rather than a bag of optionals, because §10's published RFQ is the
604
+ * one place the sentence "the market picks the backend" stops: a quote closed
605
+ * out of an open auction has a market *key* and no card behind it, so every
606
+ * card-derived field is absent at once rather than one at a time. Sizing that
607
+ * arm now costs a discriminant and keeps the addressed arm total.
608
+ */
609
+ type MarketRef = CardMarketRef | AuctionMarketRef;
610
+ interface CardMarketRef {
611
+ readonly kind: "card";
612
+ /**
613
+ * The canonical market key, `<corridor>:<id>/<corridor>:<id>`, derived under
614
+ * rfq-protocol.md §2's leg order — arkade first when exactly one leg is
615
+ * arkade, lexicographic otherwise — and never read off the card's own
616
+ * base/quote order. The two agree for every card the registry's reducer
617
+ * validated, and a card published outside it is exactly where the silent
618
+ * miss lives.
619
+ */
620
+ readonly key: string;
621
+ readonly backend: MarketBackend;
622
+ /** The registry URL, or the label a locally pinned card was loaded under. */
623
+ readonly source: string;
624
+ readonly sourceType: "registry" | "local";
625
+ /** The solver's name, as the card publishes it. Display, never identity. */
626
+ readonly solver: string;
627
+ /** The card's signing key. Absent on spot cards, which need no rendezvous. */
628
+ readonly discoveryPubkey?: Pubkey;
629
+ /** The card's display label, e.g. `BTC/lightning:BTC`. Display only. */
630
+ readonly pair: string;
631
+ readonly snapshot: SnapshotRef;
632
+ }
633
+ /** §10, reserved: a quote closed out of a published auction has no card. */
634
+ interface AuctionMarketRef {
635
+ readonly kind: "auction";
636
+ readonly key: string;
637
+ readonly backend: "rfq";
638
+ }
639
+
640
+ /**
641
+ * What survives a crash, and the public shape `accept()` hands back.
642
+ *
643
+ * Two types, one key. {@link SwapRecord} is the storage form — JSON-safe by
644
+ * declaration, every amount a canonical decimal string — and {@link Swap} is the
645
+ * answer a caller reads, with `bigint` amounts and the resolved `Route`. The
646
+ * conversion between them is §D's record-boundary codec, and it is not a third
647
+ * law: `toAtomicDecimal`/`fromAtomicDecimal` already ship in `./amount`, minted
648
+ * by M1 and used by M3's wire adapter, so both sites emit the same canonical
649
+ * form — atomic units, unsigned, no leading zeros, never a scaled display
650
+ * decimal.
651
+ *
652
+ * **The key is the quote id, on every route.** That is what makes persist-first
653
+ * representable at all: v1's `AssetSwap.id` *is* the funding txid
654
+ * (`store.ts:70-71`), so a record could not exist before the money did, and
655
+ * `coverage.ts` carries a process-local issuance mark precisely to paper over
656
+ * the gap. Here the record precedes the funding and `fundingTxid` is a later,
657
+ * best-effort write.
658
+ *
659
+ * **JSON-safe means no `bigint` anywhere, at any depth.** The SQLite and Realm
660
+ * backends `JSON.stringify` the record whole, so a `bigint` throws on two
661
+ * backends and round-trips on the third — the asymmetry
662
+ * `test/repository.test.ts` refuses to paper over. Every amount here is an
663
+ * {@link AtomicDecimal}; `test/client/record.types.ts` proves the absence
664
+ * structurally rather than by review.
665
+ */
666
+
667
+ /**
668
+ * Which family a record belongs to, and the discriminant M5's `RawState` keys
669
+ * on.
670
+ *
671
+ * Not a cosmetic tag. v1's two families occupied one `string` id space by
672
+ * accident — an offer record's id a funding txid, a corridor record's an
673
+ * `rfqId` — and keying both on `QuoteId` closes that collision (§B). The tag is
674
+ * what M6's public id carries, and what lets M5 branch its outcome table
675
+ * without a repository read.
676
+ */
677
+ type SwapFamily = "offer" | "rfq";
678
+ /** One leg's obligation, in the form a record holds it. */
679
+ interface RecordedLeg {
680
+ readonly asset: AssetId;
681
+ /** Atomic units as a canonical decimal string — never a `bigint`. */
682
+ readonly amount: AtomicDecimal;
683
+ }
684
+ /**
685
+ * An endpoint as the record holds it: the corridor, the asset, the instrument.
686
+ *
687
+ * `Instrument`'s invoice arm carries a `bigint` amount, so it cannot be stored
688
+ * as declared — {@link RecordedInstrument} is the same union with that one field
689
+ * in decimal form. The rest is field for field identical, which is what keeps the
690
+ * comparison in `acceptConflict` honest.
691
+ */
692
+ interface RecordedEndpoint {
693
+ readonly corridor: CorridorId;
694
+ readonly asset: AssetId;
695
+ readonly instrument: RecordedInstrument;
696
+ }
697
+ /** {@link Instrument}, with the invoice arm's amount in decimal form. */
698
+ type RecordedInstrument = {
699
+ readonly kind: "wallet";
700
+ } | {
701
+ readonly kind: "address";
702
+ readonly address: string;
703
+ } | {
704
+ readonly kind: "invoice";
705
+ readonly bolt11: string;
706
+ readonly paymentHash: Hex;
707
+ readonly amount?: AtomicDecimal;
708
+ readonly expiresAt: number;
709
+ };
710
+ /** {@link Artifact}, with the deposit arm's amount in decimal form. */
711
+ type RecordedArtifact = {
712
+ readonly kind: "invoice";
713
+ readonly bolt11: string;
714
+ } | {
715
+ readonly kind: "deposit";
716
+ readonly corridor: CorridorId;
717
+ readonly address: string;
718
+ readonly asset: AssetId;
719
+ readonly amount: AtomicDecimal;
720
+ readonly expiresAt?: number;
721
+ };
722
+ /**
723
+ * The half both families carry.
724
+ *
725
+ * Every field is either something `AcceptConflict` compares (§3.2's list), or
726
+ * something M5 named as a cross-milestone ask, or the two timestamps. Nothing
727
+ * is here for display: a record carries what no covenant and no chain read can
728
+ * give back, which is the same rule `RfqSwapRecord` is arranged by.
729
+ */
730
+ interface SwapRecordCommon {
731
+ /** The client-minted quote id — the primary key, per C1 and §B. */
732
+ readonly id: QuoteId;
733
+ readonly family: SwapFamily;
734
+ /**
735
+ * Both endpoints, instruments included — `AcceptConflict` items 1 and 3.
736
+ *
737
+ * Nested under `route` so the record's field names are `Quote`'s field
738
+ * names: the conflict check walks the two shapes together, and a record
739
+ * that spelled the same fact differently would make every comparison a
740
+ * translation.
741
+ */
742
+ readonly route: {
743
+ readonly give: RecordedEndpoint;
744
+ readonly take: RecordedEndpoint;
745
+ };
746
+ /** The two obligations — `AcceptConflict` item 2. */
747
+ readonly give: RecordedLeg;
748
+ readonly take: RecordedLeg;
749
+ /** The spread, as the quote precomputed it. */
750
+ readonly fee: RecordedLeg;
751
+ /**
752
+ * Which card priced this, from which registry, how fresh — stored WHOLE.
753
+ *
754
+ * A trimmed projection could not rebuild the type {@link Swap.market}
755
+ * promises: `CardMarketRef` requires `kind`, `pair` and `snapshot` beside
756
+ * the fields a summary would keep. Every member is a string, number or
757
+ * boolean, so the union round-trips through JSON untouched, and `snapshot`
758
+ * is restated as read at accept rather than restamped — a past `fetchedAt`
759
+ * beside the recorded `live` is the honest answer about how fresh the card
760
+ * was when this swap was accepted.
761
+ */
762
+ readonly market: MarketRef;
763
+ /**
764
+ * The committed counterparty, from the quote's covenant role.
765
+ *
766
+ * NOT `CardMarketRef.solver`, which is the card's display name. Two
767
+ * different facts that v1 spelled with one word: this one is a key that
768
+ * ends up in a covenant leaf, the other is a label. `AcceptConflict`
769
+ * compares this one.
770
+ */
771
+ readonly solver?: Pubkey;
772
+ /** The quote's own deadline, unix seconds — what makes a stalled accept a
773
+ * benign abandon rather than a live obligation. */
774
+ readonly expiresAt: number;
775
+ /**
776
+ * The one thing a counterparty must see, when this route has one.
777
+ *
778
+ * Durable because a duplicate accept must return the SAME invoice, and the
779
+ * invoice lives on the quote object — nowhere in the corridor profile. A
780
+ * caller that re-accepts after a restart has no quote object left, so
781
+ * without this field the only honest answer would be a second invoice,
782
+ * which §3.2 forbids by name.
783
+ */
784
+ readonly artifact?: RecordedArtifact;
785
+ /**
786
+ * The transaction that funded this swap, once known.
787
+ *
788
+ * A later, best-effort write, and the field that separates M5's `accepted`
789
+ * from `funding`. Set-where-absent is a benign resume and never an
790
+ * `AcceptConflict` — §3.2 says so by name.
791
+ */
792
+ readonly fundingTxid?: string;
793
+ /**
794
+ * Last local receive-claim error while the swap is still retryable. If the
795
+ * claim window later closes without a submitted claim, this becomes the
796
+ * terminal failure reason after restore.
797
+ */
798
+ readonly claimFailure?: string;
799
+ /** Terminal failure reason. */
800
+ readonly failure?: string;
801
+ /** Refusal reason while `state` is `needs_counterparty`. */
802
+ readonly blockedReason?: string;
803
+ /** Unix **seconds**, both — the unit `RfqSwapRecord` carries and
804
+ * `shouldRetainRfqSwap` compares against, not `AssetSwap`'s milliseconds. */
805
+ readonly createdAt: number;
806
+ readonly updatedAt: number;
807
+ }
808
+ /**
809
+ * `arkade <-> arkade`: the offer covenant, and what cancels it.
810
+ *
811
+ * `offerHex` is the whole covenant — `cancelOffer` needs nothing else to
812
+ * rebuild it — so this arm stores no tree parameters of its own.
813
+ */
814
+ interface OfferSwapRecord extends SwapRecordCommon {
815
+ readonly family: "offer";
816
+ /** v1's raw status vocabulary, which M5's `RawState` reads verbatim. */
817
+ readonly status: AssetSwapStatus;
818
+ /** The TLV offer, hex. The only input `cancelOffer` needs. */
819
+ readonly offerHex: string;
820
+ readonly swapAddress: string;
821
+ /** The covenant's scriptPubKey, hex — the indexer's monitoring key, and
822
+ * what §F's reconcile matches a discovered deposit against. */
823
+ readonly swapPkScript: string;
824
+ readonly spentTxid?: string;
825
+ readonly completedAt?: number;
826
+ }
827
+ /**
828
+ * The three corridor routes: a VHTLC lockup, its clocks and its secrets.
829
+ *
830
+ * **No covenant tree here.** Every lockup registers a contract row before its
831
+ * address can be funded, and that row already holds the parameters, keyed by
832
+ * the script they derive — a key `createContract` refuses to write unless the
833
+ * params reproduce it. Storing the tree a second time would be two sources for
834
+ * one covenant. `accept()` is what writes that row (see `./accept.ts`), which
835
+ * is why a persisted record always has one.
836
+ */
837
+ interface CorridorSwapRecord extends SwapRecordCommon {
838
+ readonly family: "rfq";
839
+ /** v1's raw state vocabulary, read verbatim by M5's `RawState`. */
840
+ readonly state: RfqSwapState;
841
+ /**
842
+ * Which corridor, in the manager's own vocabulary.
843
+ *
844
+ * `PersistableRfqSwap["kind"]` rather than a `Corridor`: it is a route pair
845
+ * — `lightning_send` and `lightning_receive` are one corridor from opposite
846
+ * ends — and it is what resolves the handler that owns {@link profile}.
847
+ */
848
+ readonly kind: PersistableRfqSwap["kind"];
849
+ /** The solver's own id for the negotiation, echoed back on the wire. */
850
+ readonly rfqId: string;
851
+ /** The Arkade address that was funded, and the swap's handle on its
852
+ * covenant row. */
853
+ readonly lockupAddress: string;
854
+ /** Its pkScript, hex — the row's key, and §F's matching key. */
855
+ readonly lockupPkScript: string;
856
+ /** The hash both covenants commit to. `sha256(P)`, hex. */
857
+ readonly lock: {
858
+ readonly hash: Hex;
859
+ };
860
+ /** When the trader's value comes back if the swap does not complete. */
861
+ readonly refundLocktime: number;
862
+ /**
863
+ * The corridor's own half, as plain JSON.
864
+ *
865
+ * v1's opaque bag (`rfqRecord.ts:108-123`), written with `rfqSecretsProfile`
866
+ * and read by `rfqCorridorHandlers.hydrate` — reused rather than
867
+ * reinvented, so M5 rebuilds through machinery that already exists and a new
868
+ * corridor still ships without touching this file. It is also what carries
869
+ * `expectedAmount`, the claim value gate's request-time input.
870
+ *
871
+ * Amounts inside it follow v1's shapes (`expectedAmount` is a `number`),
872
+ * which is JSON-safe and therefore fine: the decimal-string law governs
873
+ * this record's OWN amount fields, not the bag it carries forward.
874
+ *
875
+ * **This is also where the swap's secrets live** — `profile.signer` and,
876
+ * on a leg locked to a preimage, `profile.hashlock`. Deliberately not a
877
+ * second copy at the record's top level: `rfqClaimSecretOf` and
878
+ * `preimageForSwapRecord` already read them from here, and two homes for
879
+ * one claim secret is two things to keep in step with one of them always
880
+ * empty. At most one of `preimageHex`/`preimageSaltHex` is ever written,
881
+ * and which arm exists is decided by the wallet's provisioning result, not
882
+ * here.
883
+ */
884
+ readonly profile: Record<string, unknown>;
885
+ readonly refundTxid?: string;
886
+ readonly lockupSpendTxids?: readonly string[];
887
+ }
888
+ /** Everything `accept()` persists, both families in one key space. */
889
+ type SwapRecord = OfferSwapRecord | CorridorSwapRecord;
890
+
891
+ /** A registry discovery result held for reuse. Refetchable — unlike a swap
892
+ * record, losing it costs one network round trip — but it must survive a cold
893
+ * boot: serving it stale is what keeps quoting alive while a registry is down. */
894
+ interface MarketsCacheEntry {
895
+ markets: DiscoveredMarket[];
896
+ fetchedAt: number;
897
+ }
898
+ /**
899
+ * Everything the package persists, following the monorepo repository
900
+ * convention: versioned interface, AsyncDisposable, one backend per platform.
901
+ * Consumers construct exactly one of these; there is no second storage seam.
902
+ *
903
+ * Durable records (swaps) and rebuildable state (the restore scan's txid
904
+ * cursor, the markets cache) live side by side because they share a
905
+ * lifetime: all three belong to one wallet on one device, and a consumer
906
+ * that wipes one wants all three gone.
907
+ *
908
+ * ponytail: no query filters — every consumer reads all swaps and filters
909
+ * in memory; add a filter type when a consumer needs subset queries.
910
+ */
911
+ interface AssetSwapRepository extends AsyncDisposable {
912
+ /** 5 adds the v2 swap-record store — one row per accepted swap, keyed by
913
+ * the client-minted quote id. 4 added `getRfqSwap`; 3 added the other RFQ
914
+ * methods below; 2 was the released shape — swaps, scan cursor, markets,
915
+ * with `preimageSaltHex` on the swap record — so an implementor built
916
+ * against any of them cannot satisfy this one silently. */
917
+ readonly version: 5;
918
+ /** Insert or replace a swap by id. Store the record whole: `preimageHex`
919
+ * and `preimageSaltHex` both leave the swap unclaimable if a field-mapped
920
+ * backend drops them — the first is the only claim secret of a swap whose
921
+ * signer cannot derive, the second the public input every other static
922
+ * wallet's preimage derives from.
923
+ *
924
+ * Records must be **JSON-safe**: the SQLite and Realm backends serialize
925
+ * the record to JSON, so a `Date` in a consumer-added field comes back a
926
+ * string, a `Set`/`Map` comes back empty, and a `bigint` throws here —
927
+ * none of which happens on IndexedDB's structured clone. `AssetSwap` as
928
+ * declared is JSON-safe; keep added fields that way. */
929
+ saveSwap(swap: AssetSwap): Promise<void>;
930
+ /** All stored swaps, in no particular order — `getAssetSwaps` is the
931
+ * canonical newest-first read. */
932
+ getAllSwaps(): Promise<AssetSwap[]>;
933
+ /**
934
+ * Insert or replace a monitored RFQ swap by `rfqId`.
935
+ *
936
+ * Store the record WHOLE. Every field is a covenant tree parameter or the
937
+ * manager's own state, and a field-mapped backend that drops one round-trips
938
+ * a record whose covenant `rebuildRfqSwap` cannot reproduce — which surfaces
939
+ * as a refund that cannot be signed, long after the write.
940
+ */
941
+ saveRfqSwap(record: RfqSwapRecord): Promise<void>;
942
+ /** One record by key. `undefined` on a miss — retention prunes terminal
943
+ * records, so absence is ordinary and not an error. */
944
+ getRfqSwap(rfqId: string): Promise<RfqSwapRecord | undefined>;
945
+ /** Every stored RFQ swap record, in no particular order. */
946
+ getAllRfqSwaps(): Promise<RfqSwapRecord[]>;
947
+ /** Drop one, once it is past retention — see `shouldRetainRfqSwap`. */
948
+ removeRfqSwap(rfqId: string): Promise<void>;
949
+ /**
950
+ * Insert or replace a v2 swap record by its quote id.
951
+ *
952
+ * The store the v2 client's `accept()` writes, and the reason this
953
+ * interface is at 5. Separate from `swaps` rather than sharing it: the v1
954
+ * read path drops any row carrying neither `offerHex` nor `paymentHash`
955
+ * (`getAssetSwapsOrThrow`), silently and as corrupt, so a v2 record in that
956
+ * store would be pinned by a v1 predicate — and the two histories are meant
957
+ * to be disjoint for the deprecation window anyway. A v1 reader not seeing
958
+ * v2 rows is the design, asserted in the conformance suite rather than
959
+ * tolerated.
960
+ *
961
+ * Store the record WHOLE, and note that it is **JSON-safe by declaration**:
962
+ * every amount on it is a canonical decimal string, precisely so the SQLite
963
+ * and Realm backends' `JSON.stringify` and IndexedDB's structured clone
964
+ * agree. A `bigint` reaching here would throw on two backends and
965
+ * round-trip on the third.
966
+ */
967
+ saveSwapRecord(record: SwapRecord): Promise<void>;
968
+ /** One record by quote id. `undefined` on a miss — which is the ordinary
969
+ * answer for a first `accept()`, and what makes it idempotent. */
970
+ getSwapRecord(id: string): Promise<SwapRecord | undefined>;
971
+ /** Every stored v2 record, in no particular order. */
972
+ getAllSwapRecords(): Promise<SwapRecord[]>;
973
+ /** Drop one, once it is past retention. */
974
+ removeSwapRecord(id: string): Promise<void>;
975
+ /**
976
+ * Sent txids already checked for offer packets (see restore.ts).
977
+ *
978
+ * **Shared across both record families, deliberately.** This is not
979
+ * record-family data — it marks txids of transactions a scan has answered,
980
+ * whatever family a later record belongs to — and both families walk the
981
+ * same sent-txid set during the deprecation window. Two cursors would have
982
+ * each side re-walking deposits the other already answered.
983
+ */
984
+ getScannedTxids(): Promise<Set<string>>;
985
+ markTxidsScanned(txids: Iterable<string>): Promise<void>;
986
+ /** Cached registry markets, or undefined on a miss. Shared across both
987
+ * record families for the reason the cursor is: the key is network-and-
988
+ * registry, not a store, and two caches would serve two staleness clocks
989
+ * for one registry. */
990
+ getCachedMarkets(network: string, registry: string): Promise<MarketsCacheEntry | undefined>;
991
+ saveCachedMarkets(network: string, registry: string, entry: MarketsCacheEntry): Promise<void>;
992
+ clear(): Promise<void>;
993
+ }
994
+
995
+ /**
996
+ * An executor that owns its connection.
997
+ *
998
+ * `SQLExecutor` declares no lifecycle — the interface is three query methods,
999
+ * because every other implementation wraps a connection the consumer already
1000
+ * opened. This one opens the file itself, so it is the one that has something
1001
+ * to close.
1002
+ */
1003
+ interface NodeSqlExecutor extends SQLExecutor {
1004
+ /** Close the underlying database handle. Idempotent. */
1005
+ close(): Promise<void>;
1006
+ }
1007
+ /**
1008
+ * Open (creating if absent) a SQLite database at `path`.
1009
+ *
1010
+ * **One instance per file, held for the process's life.** `runInTransaction`
1011
+ * keys its write chain on the executor *object* through a `WeakMap`, so a
1012
+ * second executor over the same file forks the chain and two "transactions" can
1013
+ * interleave on one connection. Every repository on a database must be handed
1014
+ * the same instance — which is what {@link nodeSwapRepository} does for the
1015
+ * common case.
1016
+ *
1017
+ * The parent directory is created on open: the config dir exists on any real
1018
+ * machine but `arkade/swaps` under it does not, and failing on a first run for
1019
+ * a directory the caller never chose would be a poor default.
1020
+ */
1021
+ declare const createNodeSqlExecutor: (path: string) => NodeSqlExecutor;
1022
+ interface NodeSwapRepositoryOptions {
1023
+ /**
1024
+ * Which network's database. Used to build the default path — pass the same
1025
+ * name the wallet reports.
1026
+ */
1027
+ readonly network: string;
1028
+ /** An explicit path, overriding the platform default. */
1029
+ readonly path?: string;
1030
+ /** Table-name prefix, passed through to the SQLite backend. */
1031
+ readonly prefix?: string;
1032
+ }
1033
+ /**
1034
+ * The Node storage default, connection included.
1035
+ *
1036
+ * This is where D1/D2's ownership rule lands: the returned repository's
1037
+ * `[Symbol.asyncDispose]` closes the connection **it** opened, and an injected
1038
+ * repository is disposed by whoever built it — which is already true of every
1039
+ * other backend, whose disposal is a no-op precisely because the consumer owns
1040
+ * the handle.
1041
+ *
1042
+ * Hanging ownership on the repository rather than on a client lifecycle method
1043
+ * is deliberate: the v2 client has no `stop()` or `dispose()` yet — the
1044
+ * lifecycle is the next milestone's — and a connection that could only be
1045
+ * closed through a method that does not exist would leak until the process
1046
+ * ended.
1047
+ *
1048
+ * ```ts
1049
+ * await using repository = nodeSwapRepository({ network: "mainnet" });
1050
+ * const client = createSwapClient({ wallet, repository });
1051
+ * ```
1052
+ */
1053
+ declare const nodeSwapRepository: (options: NodeSwapRepositoryOptions) => AssetSwapRepository;
1054
+
1055
+ export { type NodeSqlExecutor, type NodeSwapRepositoryOptions, configDir, createNodeSqlExecutor, nodeSwapRepository, swapDatabasePath };