@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.
- package/README.md +67 -22
- package/dist/{chunk-XXWOODUE.js → chunk-OEKSNYJW.js} +107 -76
- package/dist/{chunk-6ZUS47GA.js → chunk-U7VWWTL6.js} +15 -1
- package/dist/index.cjs +1457 -312
- package/dist/index.d.cts +542 -46
- package/dist/index.d.ts +542 -46
- package/dist/index.js +1312 -230
- package/dist/node/index.cjs +322 -0
- package/dist/node/index.d.cts +1055 -0
- package/dist/node/index.d.ts +1055 -0
- package/dist/node/index.js +295 -0
- package/dist/nostr.cjs +4 -1
- package/dist/nostr.d.cts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/nostr.js +1 -1
- package/dist/repositories/realm/index.cjs +50 -2
- package/dist/repositories/realm/index.d.cts +55 -10
- package/dist/repositories/realm/index.d.ts +55 -10
- package/dist/repositories/realm/index.js +49 -3
- package/dist/repositories/sqlite/index.cjs +42 -1
- package/dist/repositories/sqlite/index.d.cts +8 -3
- package/dist/repositories/sqlite/index.d.ts +8 -3
- package/dist/repositories/sqlite/index.js +43 -2
- package/dist/{repository-DaK1RzRq.d.cts → repository-DoR-ahHc.d.ts} +1081 -84
- package/dist/{repository-DlLvj_y6.d.ts → repository-oeW8KZo1.d.cts} +1081 -84
- package/dist/{rfq-DzsmhXX3.d.cts → rfq-D0KGjmnn.d.cts} +42 -32
- package/dist/{rfq-DzsmhXX3.d.ts → rfq-D0KGjmnn.d.ts} +42 -32
- package/package.json +22 -6
|
@@ -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 };
|