@satora/swap 0.0.5 → 0.1.0
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 +5 -3
- package/dist/bitcoin-reader-esplora-GRUP7RAG.js +11 -0
- package/dist/bitcoin-reader-esplora-GRUP7RAG.js.map +1 -0
- package/dist/chunk-J35RZBDW.js +65 -0
- package/dist/chunk-J35RZBDW.js.map +1 -0
- package/dist/index.cjs +1856 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +955 -3
- package/dist/index.d.ts +955 -3
- package/dist/index.js +1770 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.cts +2 -0
- package/dist/node.d.ts +2 -0
- package/package.json +10 -3
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,13 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
6
13
|
var __copyProps = (to, from, except, desc) => {
|
|
7
14
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
15
|
for (let key of __getOwnPropNames(from))
|
|
@@ -14,12 +21,1861 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
14
21
|
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
15
22
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
16
23
|
|
|
24
|
+
// src/contracts/bitcoin-reader-esplora.ts
|
|
25
|
+
var bitcoin_reader_esplora_exports = {};
|
|
26
|
+
__export(bitcoin_reader_esplora_exports, {
|
|
27
|
+
DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
|
|
28
|
+
esploraReader: () => esploraReader,
|
|
29
|
+
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs
|
|
30
|
+
});
|
|
31
|
+
function htlcFactsFromEsploraTxs(txs, address) {
|
|
32
|
+
for (const tx of txs) {
|
|
33
|
+
const spend = tx.vin.find(
|
|
34
|
+
(vin) => vin.prevout?.scriptpubkey_address === address
|
|
35
|
+
);
|
|
36
|
+
if (spend)
|
|
37
|
+
return {
|
|
38
|
+
funding: "confirmed",
|
|
39
|
+
fundedSats: 0,
|
|
40
|
+
// already spent — the amount no longer matters
|
|
41
|
+
spendWitness: spend.witness ?? []
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const funding = txs.find(
|
|
45
|
+
(tx) => tx.vout.some((vout) => vout.scriptpubkey_address === address)
|
|
46
|
+
);
|
|
47
|
+
if (funding) {
|
|
48
|
+
const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
|
|
49
|
+
return {
|
|
50
|
+
funding: funding.status?.confirmed ? "confirmed" : "mempool",
|
|
51
|
+
fundedSats
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { funding: "absent", fundedSats: 0 };
|
|
55
|
+
}
|
|
56
|
+
function esploraReader(esploraUrls, fetchImpl = fetch) {
|
|
57
|
+
const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(
|
|
58
|
+
(url) => url.replace(/\/+$/, "")
|
|
59
|
+
);
|
|
60
|
+
let start = 0;
|
|
61
|
+
return {
|
|
62
|
+
async getHtlcFacts(address) {
|
|
63
|
+
const from = start++ % bases.length;
|
|
64
|
+
let lastError;
|
|
65
|
+
for (let i = 0; i < bases.length; i++) {
|
|
66
|
+
const base = bases[(from + i) % bases.length];
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetchImpl(
|
|
69
|
+
`${base}/address/${encodeURIComponent(address)}/txs`
|
|
70
|
+
);
|
|
71
|
+
if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);
|
|
72
|
+
return htlcFactsFromEsploraTxs(
|
|
73
|
+
await res.json(),
|
|
74
|
+
address
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
lastError = error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError ?? new Error("no esplora endpoints configured");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
var DEFAULT_ESPLORA_URLS;
|
|
85
|
+
var init_bitcoin_reader_esplora = __esm({
|
|
86
|
+
"src/contracts/bitcoin-reader-esplora.ts"() {
|
|
87
|
+
"use strict";
|
|
88
|
+
DEFAULT_ESPLORA_URLS = [
|
|
89
|
+
"https://mempool.space/api",
|
|
90
|
+
"https://blockstream.info/api"
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
17
95
|
// src/index.ts
|
|
18
96
|
var src_exports = {};
|
|
97
|
+
__export(src_exports, {
|
|
98
|
+
ArkadeContractManager: () => ArkadeContractManager,
|
|
99
|
+
BitcoinContractManager: () => BitcoinContractManager,
|
|
100
|
+
Client: () => Client,
|
|
101
|
+
ClientBuilder: () => ClientBuilder,
|
|
102
|
+
DEFAULT_ESPLORA_URLS: () => DEFAULT_ESPLORA_URLS,
|
|
103
|
+
DEFAULT_EVM_RPCS: () => DEFAULT_EVM_RPCS,
|
|
104
|
+
EvmContractManager: () => EvmContractManager,
|
|
105
|
+
SwapTracker: () => SwapTracker,
|
|
106
|
+
buildArkadeVhtlcRef: () => buildArkadeVhtlcRef,
|
|
107
|
+
createEvmRpcReader: () => createEvmRpcReader,
|
|
108
|
+
defaultArkadeServerUrl: () => defaultArkadeServerUrl,
|
|
109
|
+
defaultEvmReaders: () => defaultEvmReaders,
|
|
110
|
+
deriveSwapActions: () => deriveSwapActions,
|
|
111
|
+
deriveSwapStatus: () => deriveSwapStatus,
|
|
112
|
+
esploraReader: () => esploraReader,
|
|
113
|
+
evmReaderFromClient: () => evmReaderFromClient,
|
|
114
|
+
htlcFactsFromEsploraTxs: () => htlcFactsFromEsploraTxs,
|
|
115
|
+
htlcKey: () => htlcKey,
|
|
116
|
+
swapToTracked: () => swapToTracked
|
|
117
|
+
});
|
|
19
118
|
module.exports = __toCommonJS(src_exports);
|
|
20
119
|
__reExport(src_exports, require("@lendasat/lendaswap-sdk-pure"), module.exports);
|
|
120
|
+
|
|
121
|
+
// src/actions/derive.ts
|
|
122
|
+
function deriveSwapActions(input) {
|
|
123
|
+
const {
|
|
124
|
+
status,
|
|
125
|
+
clientChainNow,
|
|
126
|
+
serverChainNow,
|
|
127
|
+
clientRefundLocktime,
|
|
128
|
+
serverRefundLocktime,
|
|
129
|
+
clientFunds = true
|
|
130
|
+
} = input;
|
|
131
|
+
switch (status) {
|
|
132
|
+
case "pending": {
|
|
133
|
+
if (!clientFunds) {
|
|
134
|
+
const wait = {
|
|
135
|
+
id: "wait",
|
|
136
|
+
recommended: true,
|
|
137
|
+
automation: "auto",
|
|
138
|
+
reason: "Waiting for the Lightning payment to be locked in."
|
|
139
|
+
};
|
|
140
|
+
return { recommended: "wait", actions: [wait] };
|
|
141
|
+
}
|
|
142
|
+
if (clientChainNow >= clientRefundLocktime) return { actions: [] };
|
|
143
|
+
const fund = {
|
|
144
|
+
id: "fund",
|
|
145
|
+
recommended: true,
|
|
146
|
+
// Funding spends new user money, so it is never auto-run.
|
|
147
|
+
automation: "manual",
|
|
148
|
+
reason: "Send the deposit to start the swap."
|
|
149
|
+
};
|
|
150
|
+
return { recommended: "fund", actions: [fund] };
|
|
151
|
+
}
|
|
152
|
+
case "clientfundingseen": {
|
|
153
|
+
const wait = {
|
|
154
|
+
id: "wait",
|
|
155
|
+
recommended: true,
|
|
156
|
+
automation: "auto",
|
|
157
|
+
reason: "Waiting for your funding transaction to confirm."
|
|
158
|
+
};
|
|
159
|
+
return { recommended: "wait", actions: [wait] };
|
|
160
|
+
}
|
|
161
|
+
case "clientfunded": {
|
|
162
|
+
const refundUnlocked = clientChainNow >= clientRefundLocktime;
|
|
163
|
+
if (refundUnlocked) {
|
|
164
|
+
const refund2 = {
|
|
165
|
+
id: "refund_unilateral",
|
|
166
|
+
recommended: true,
|
|
167
|
+
automation: "confirm",
|
|
168
|
+
reason: "Reclaim your deposit."
|
|
169
|
+
};
|
|
170
|
+
return { recommended: "refund_unilateral", actions: [refund2] };
|
|
171
|
+
}
|
|
172
|
+
const wait = {
|
|
173
|
+
id: "wait",
|
|
174
|
+
recommended: true,
|
|
175
|
+
automation: "auto",
|
|
176
|
+
reason: "Waiting for the server to fund the swap."
|
|
177
|
+
};
|
|
178
|
+
const refund = {
|
|
179
|
+
id: "refund_unilateral",
|
|
180
|
+
recommended: false,
|
|
181
|
+
automation: "confirm",
|
|
182
|
+
reason: "Refund becomes available once the timelock passes.",
|
|
183
|
+
blockedBy: {
|
|
184
|
+
kind: "timelock_not_expired",
|
|
185
|
+
message: "The refund timelock has not passed yet."
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
return { recommended: "wait", actions: [wait, refund] };
|
|
189
|
+
}
|
|
190
|
+
case "serverfunded": {
|
|
191
|
+
const claimWindowClosed = serverChainNow >= serverRefundLocktime;
|
|
192
|
+
if (!claimWindowClosed) {
|
|
193
|
+
const claim = {
|
|
194
|
+
id: "claim",
|
|
195
|
+
recommended: true,
|
|
196
|
+
automation: "auto",
|
|
197
|
+
reason: "Redeem the swap with your preimage."
|
|
198
|
+
};
|
|
199
|
+
return { recommended: "claim", actions: [claim] };
|
|
200
|
+
}
|
|
201
|
+
if (!clientFunds) {
|
|
202
|
+
const done = {
|
|
203
|
+
id: "none",
|
|
204
|
+
recommended: true,
|
|
205
|
+
automation: "auto",
|
|
206
|
+
reason: "The claim window passed \u2014 your Lightning payment will be refunded automatically."
|
|
207
|
+
};
|
|
208
|
+
return { recommended: "none", actions: [done] };
|
|
209
|
+
}
|
|
210
|
+
const refundUnlocked = clientChainNow >= clientRefundLocktime;
|
|
211
|
+
if (refundUnlocked) {
|
|
212
|
+
const refund2 = {
|
|
213
|
+
id: "refund_unilateral",
|
|
214
|
+
recommended: true,
|
|
215
|
+
automation: "confirm",
|
|
216
|
+
reason: "Reclaim your deposit \u2014 do not claim, as revealing the preimage would let the server take it."
|
|
217
|
+
};
|
|
218
|
+
return { recommended: "refund_unilateral", actions: [refund2] };
|
|
219
|
+
}
|
|
220
|
+
const wait = {
|
|
221
|
+
id: "wait",
|
|
222
|
+
recommended: true,
|
|
223
|
+
automation: "auto",
|
|
224
|
+
reason: "Wait for your refund timelock \u2014 do not claim, as revealing the preimage would let the server take your deposit."
|
|
225
|
+
};
|
|
226
|
+
const refund = {
|
|
227
|
+
id: "refund_unilateral",
|
|
228
|
+
recommended: false,
|
|
229
|
+
automation: "confirm",
|
|
230
|
+
reason: "Refund becomes available once the timelock passes.",
|
|
231
|
+
blockedBy: {
|
|
232
|
+
kind: "timelock_not_expired",
|
|
233
|
+
message: "The refund timelock has not passed yet."
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
return { recommended: "wait", actions: [wait, refund] };
|
|
237
|
+
}
|
|
238
|
+
case "clientredeeming": {
|
|
239
|
+
const wait = {
|
|
240
|
+
id: "wait",
|
|
241
|
+
recommended: true,
|
|
242
|
+
automation: "auto",
|
|
243
|
+
reason: "Redeeming your funds \u2014 waiting for the claim to confirm."
|
|
244
|
+
};
|
|
245
|
+
return { recommended: "wait", actions: [wait] };
|
|
246
|
+
}
|
|
247
|
+
case "clientredeemed":
|
|
248
|
+
case "serverredeemed": {
|
|
249
|
+
const done = {
|
|
250
|
+
id: "none",
|
|
251
|
+
recommended: true,
|
|
252
|
+
automation: "auto",
|
|
253
|
+
reason: "Swap complete \u2014 you've received your funds."
|
|
254
|
+
};
|
|
255
|
+
return { recommended: "none", actions: [done] };
|
|
256
|
+
}
|
|
257
|
+
case "expired": {
|
|
258
|
+
const done = {
|
|
259
|
+
id: "none",
|
|
260
|
+
recommended: true,
|
|
261
|
+
automation: "auto",
|
|
262
|
+
reason: "The swap expired without funding \u2014 nothing was locked."
|
|
263
|
+
};
|
|
264
|
+
return { recommended: "none", actions: [done] };
|
|
265
|
+
}
|
|
266
|
+
case "clientrefunded":
|
|
267
|
+
case "clientrefundedserverrefunded":
|
|
268
|
+
case "clientrefundedserverfunded": {
|
|
269
|
+
const done = {
|
|
270
|
+
id: "none",
|
|
271
|
+
recommended: true,
|
|
272
|
+
automation: "auto",
|
|
273
|
+
reason: "Refunded \u2014 your deposit was returned."
|
|
274
|
+
};
|
|
275
|
+
return { recommended: "none", actions: [done] };
|
|
276
|
+
}
|
|
277
|
+
case "clientredeemedandclientrefunded": {
|
|
278
|
+
const done = {
|
|
279
|
+
id: "none",
|
|
280
|
+
recommended: true,
|
|
281
|
+
automation: "auto",
|
|
282
|
+
reason: "This swap ended in an anomalous state (redeemed and refunded); no action needed."
|
|
283
|
+
};
|
|
284
|
+
return { recommended: "none", actions: [done] };
|
|
285
|
+
}
|
|
286
|
+
case "clientfundedserverrefunded":
|
|
287
|
+
case "serverwontfund":
|
|
288
|
+
case "clientfundedtoolate":
|
|
289
|
+
case "clientinvalidfunded": {
|
|
290
|
+
const refundUnlocked = clientChainNow >= clientRefundLocktime;
|
|
291
|
+
if (refundUnlocked) {
|
|
292
|
+
const refund2 = {
|
|
293
|
+
id: "refund_unilateral",
|
|
294
|
+
recommended: true,
|
|
295
|
+
automation: "confirm",
|
|
296
|
+
reason: "Reclaim your deposit \u2014 the swap did not complete."
|
|
297
|
+
};
|
|
298
|
+
return { recommended: "refund_unilateral", actions: [refund2] };
|
|
299
|
+
}
|
|
300
|
+
const wait = {
|
|
301
|
+
id: "wait",
|
|
302
|
+
recommended: true,
|
|
303
|
+
automation: "auto",
|
|
304
|
+
reason: "The swap did not complete; waiting for the refund timelock to reclaim your deposit."
|
|
305
|
+
};
|
|
306
|
+
const refund = {
|
|
307
|
+
id: "refund_unilateral",
|
|
308
|
+
recommended: false,
|
|
309
|
+
automation: "confirm",
|
|
310
|
+
reason: "Refund becomes available once the timelock passes.",
|
|
311
|
+
blockedBy: {
|
|
312
|
+
kind: "timelock_not_expired",
|
|
313
|
+
message: "The refund timelock has not passed yet."
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
return { recommended: "wait", actions: [wait, refund] };
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
return assertNever(status);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function assertNever(status) {
|
|
323
|
+
throw new Error(`unhandled swap status: ${String(status)}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/actions/status.ts
|
|
327
|
+
function deriveSwapStatus(obs) {
|
|
328
|
+
const { clientHtlc, serverHtlc } = obs;
|
|
329
|
+
if (serverHtlc === void 0 && clientHtlc !== void 0) {
|
|
330
|
+
return receiveOnLightningStatus(clientHtlc);
|
|
331
|
+
}
|
|
332
|
+
if (clientHtlc === void 0 && serverHtlc !== void 0) {
|
|
333
|
+
return payOnLightningStatus(serverHtlc);
|
|
334
|
+
}
|
|
335
|
+
if (clientHtlc === void 0 || serverHtlc === void 0) return void 0;
|
|
336
|
+
if (clientHtlc === "spent_claim" && serverHtlc === "spent_claim") {
|
|
337
|
+
return "serverredeemed";
|
|
338
|
+
}
|
|
339
|
+
if (clientHtlc === "spent_refund" && serverHtlc === "spent_claim") {
|
|
340
|
+
return "clientredeemedandclientrefunded";
|
|
341
|
+
}
|
|
342
|
+
if (clientHtlc === "spent_refund" && serverHtlc === "spent_refund") {
|
|
343
|
+
return "clientrefundedserverrefunded";
|
|
344
|
+
}
|
|
345
|
+
if (clientHtlc === "spent_refund" && serverHtlc === "confirmed") {
|
|
346
|
+
return "clientrefundedserverfunded";
|
|
347
|
+
}
|
|
348
|
+
if (clientHtlc === "spent_refund" && (serverHtlc === "absent" || serverHtlc === "mempool")) {
|
|
349
|
+
return "clientrefunded";
|
|
350
|
+
}
|
|
351
|
+
if (clientHtlc === "confirmed" && serverHtlc === "spent_claim") {
|
|
352
|
+
return "clientredeemed";
|
|
353
|
+
}
|
|
354
|
+
if (clientHtlc === "confirmed" && serverHtlc === "spent_refund") {
|
|
355
|
+
return "clientfundedserverrefunded";
|
|
356
|
+
}
|
|
357
|
+
if (clientHtlc === "invalid") return "clientinvalidfunded";
|
|
358
|
+
if (clientHtlc === "absent") return "pending";
|
|
359
|
+
if (clientHtlc === "mempool") return "clientfundingseen";
|
|
360
|
+
if (clientHtlc === "confirmed") {
|
|
361
|
+
return serverHtlc === "confirmed" ? "serverfunded" : "clientfunded";
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function receiveOnLightningStatus(clientHtlc) {
|
|
366
|
+
switch (clientHtlc) {
|
|
367
|
+
case "spent_claim":
|
|
368
|
+
return "serverredeemed";
|
|
369
|
+
// server swept the deposit ⟹ it paid the invoice
|
|
370
|
+
case "spent_refund":
|
|
371
|
+
return "clientrefunded";
|
|
372
|
+
case "invalid":
|
|
373
|
+
return "clientinvalidfunded";
|
|
374
|
+
case "confirmed":
|
|
375
|
+
return "clientfunded";
|
|
376
|
+
// funded; waiting for the server to pay + sweep
|
|
377
|
+
case "mempool":
|
|
378
|
+
return "clientfundingseen";
|
|
379
|
+
case "absent":
|
|
380
|
+
return "pending";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function payOnLightningStatus(serverHtlc) {
|
|
384
|
+
switch (serverHtlc) {
|
|
385
|
+
case "spent_claim":
|
|
386
|
+
return "clientredeemed";
|
|
387
|
+
// client claimed the on-chain leg ⟹ complete
|
|
388
|
+
case "confirmed":
|
|
389
|
+
return "serverfunded";
|
|
390
|
+
// ready to claim
|
|
391
|
+
case "spent_refund":
|
|
392
|
+
return "clientrefunded";
|
|
393
|
+
case "invalid":
|
|
394
|
+
case "mempool":
|
|
395
|
+
case "absent":
|
|
396
|
+
return "pending";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/client.ts
|
|
401
|
+
var import_lendaswap_sdk_pure = require("@lendasat/lendaswap-sdk-pure");
|
|
402
|
+
|
|
403
|
+
// src/contracts/arkade-manager.ts
|
|
404
|
+
var import_sdk2 = require("@arkade-os/sdk");
|
|
405
|
+
var import_base3 = require("@scure/base");
|
|
406
|
+
|
|
407
|
+
// src/contracts/arkade.ts
|
|
408
|
+
var import_sdk = require("@arkade-os/sdk");
|
|
409
|
+
var import_base2 = require("@scure/base");
|
|
410
|
+
|
|
411
|
+
// src/contracts/preimage.ts
|
|
412
|
+
var import_legacy = require("@noble/hashes/legacy.js");
|
|
413
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
414
|
+
var import_base = require("@scure/base");
|
|
415
|
+
function preimageMatches(element, hashLock) {
|
|
416
|
+
const digest = hashLock.length === 20 ? (0, import_legacy.ripemd160)((0, import_sha2.sha256)(element)) : (0, import_sha2.sha256)(element);
|
|
417
|
+
return import_base.hex.encode(digest) === import_base.hex.encode(hashLock);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/contracts/arkade.ts
|
|
421
|
+
function arkadeObservation(facts) {
|
|
422
|
+
if (facts.spend === "claim") return "spent_claim";
|
|
423
|
+
if (facts.spend === "refund") return "spent_refund";
|
|
424
|
+
if (!facts.funded) return "absent";
|
|
425
|
+
return facts.sufficient === false ? "invalid" : "confirmed";
|
|
426
|
+
}
|
|
427
|
+
function classifyArkadeSpend(encodedSpendTx, paymentHash) {
|
|
428
|
+
const tx = import_sdk.Transaction.fromPSBT(import_base2.base64.decode(encodedSpendTx));
|
|
429
|
+
for (const witness of (0, import_sdk.getArkPsbtFields)(tx, 0, import_sdk.ConditionWitness)) {
|
|
430
|
+
for (const element of witness) {
|
|
431
|
+
if (element.length === 32 && preimageMatches(element, paymentHash)) {
|
|
432
|
+
return { spend: "claim", preimage: element };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return { spend: "refund" };
|
|
437
|
+
}
|
|
438
|
+
async function fetchArkadeSpend(source, spentByTxid, paymentHash) {
|
|
439
|
+
const { txs } = await source.getVirtualTxs([spentByTxid]);
|
|
440
|
+
const encoded = txs[0];
|
|
441
|
+
return encoded ? classifyArkadeSpend(encoded, paymentHash) : void 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/contracts/arkade-manager.ts
|
|
445
|
+
function isFunded(vtxo) {
|
|
446
|
+
const { state } = vtxo.virtualStatus;
|
|
447
|
+
return state === "preconfirmed" || state === "settled";
|
|
448
|
+
}
|
|
449
|
+
function spendTxid(vtxos) {
|
|
450
|
+
const spent = vtxos.find(
|
|
451
|
+
(v) => v.arkTxId || v.spentBy || v.isSpent || v.virtualStatus.state === "spent"
|
|
452
|
+
);
|
|
453
|
+
return spent?.arkTxId ?? spent?.spentBy;
|
|
454
|
+
}
|
|
455
|
+
var ArkadeContractManager = class _ArkadeContractManager {
|
|
456
|
+
ledger = "arkade";
|
|
457
|
+
#indexer;
|
|
458
|
+
#chainTime;
|
|
459
|
+
/** pkScript → the ref we're tracking. */
|
|
460
|
+
#refs = /* @__PURE__ */ new Map();
|
|
461
|
+
/** pkScript → last known observation. */
|
|
462
|
+
#obs = /* @__PURE__ */ new Map();
|
|
463
|
+
/** pkScript → the verified preimage recovered from a claim spend. */
|
|
464
|
+
#preimages = /* @__PURE__ */ new Map();
|
|
465
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
466
|
+
#now;
|
|
467
|
+
constructor(deps) {
|
|
468
|
+
this.#indexer = deps.indexer;
|
|
469
|
+
this.#chainTime = deps.chainTime;
|
|
470
|
+
}
|
|
471
|
+
static fromDeps(deps) {
|
|
472
|
+
return new _ArkadeContractManager(deps);
|
|
473
|
+
}
|
|
474
|
+
/** Construct a manager backed by a `RestIndexerProvider` over the Ark server. */
|
|
475
|
+
static async create(config) {
|
|
476
|
+
const indexer = new import_sdk2.RestIndexerProvider(config.serverUrl);
|
|
477
|
+
return _ArkadeContractManager.fromDeps({
|
|
478
|
+
indexer,
|
|
479
|
+
chainTime: config.chainTime
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async register(ref) {
|
|
483
|
+
if (ref.ledger !== "arkade")
|
|
484
|
+
throw new Error(
|
|
485
|
+
`ArkadeContractManager can't track a '${ref.ledger}' HTLC`
|
|
486
|
+
);
|
|
487
|
+
this.#refs.set(ref.script, ref);
|
|
488
|
+
await this.#reconcileRef(ref);
|
|
489
|
+
}
|
|
490
|
+
async unregister(ref) {
|
|
491
|
+
if (ref.ledger !== "arkade") return;
|
|
492
|
+
this.#refs.delete(ref.script);
|
|
493
|
+
this.#obs.delete(ref.script);
|
|
494
|
+
this.#preimages.delete(ref.script);
|
|
495
|
+
}
|
|
496
|
+
getState(ref) {
|
|
497
|
+
return ref.ledger === "arkade" ? this.#obs.get(ref.script) : void 0;
|
|
498
|
+
}
|
|
499
|
+
chainNow(_ref) {
|
|
500
|
+
return this.#now;
|
|
501
|
+
}
|
|
502
|
+
onEvent(cb) {
|
|
503
|
+
this.#listeners.add(cb);
|
|
504
|
+
return () => this.#listeners.delete(cb);
|
|
505
|
+
}
|
|
506
|
+
async refresh() {
|
|
507
|
+
if (this.#chainTime) this.#now = await this.#chainTime();
|
|
508
|
+
await Promise.all(
|
|
509
|
+
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
dispose() {
|
|
513
|
+
this.#listeners.clear();
|
|
514
|
+
}
|
|
515
|
+
/** The verified preimage recovered from a claim spend, if one was seen. */
|
|
516
|
+
getPreimage(ref) {
|
|
517
|
+
return ref.ledger === "arkade" ? this.#preimages.get(ref.script) : void 0;
|
|
518
|
+
}
|
|
519
|
+
/** Read the VHTLC's vtxos and map them to an observation. */
|
|
520
|
+
async #reconcileRef(ref) {
|
|
521
|
+
const { vtxos } = await this.#indexer.getVtxos({ scripts: [ref.script] });
|
|
522
|
+
const spend = spendTxid(vtxos);
|
|
523
|
+
if (spend) {
|
|
524
|
+
const resolved = await fetchArkadeSpend(
|
|
525
|
+
this.#indexer,
|
|
526
|
+
spend,
|
|
527
|
+
import_base3.hex.decode(ref.preimageHash)
|
|
528
|
+
);
|
|
529
|
+
if (resolved) {
|
|
530
|
+
if (resolved.spend === "claim")
|
|
531
|
+
this.#preimages.set(ref.script, resolved.preimage);
|
|
532
|
+
this.#set(
|
|
533
|
+
ref.script,
|
|
534
|
+
arkadeObservation({ funded: true, spend: resolved.spend })
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const funded = vtxos.filter(isFunded);
|
|
540
|
+
const total = funded.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
541
|
+
this.#set(
|
|
542
|
+
ref.script,
|
|
543
|
+
arkadeObservation({
|
|
544
|
+
funded: funded.length > 0,
|
|
545
|
+
sufficient: total >= ref.expectedSats
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
550
|
+
#set(script, observation) {
|
|
551
|
+
const ref = this.#refs.get(script);
|
|
552
|
+
if (!ref) return;
|
|
553
|
+
const current = this.#obs.get(script);
|
|
554
|
+
if (current === observation) return;
|
|
555
|
+
const spendStates = ["spent_claim", "spent_refund"];
|
|
556
|
+
if (current && spendStates.includes(current) && !spendStates.includes(observation))
|
|
557
|
+
return;
|
|
558
|
+
this.#obs.set(script, observation);
|
|
559
|
+
for (const listener of this.#listeners) listener(ref, observation);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// src/contracts/arkade-network.ts
|
|
564
|
+
var DEFAULT_ARKADE_URLS = {
|
|
565
|
+
bitcoin: "https://arkade.computer",
|
|
566
|
+
mainnet: "https://arkade.computer",
|
|
567
|
+
signet: "https://mutinynet.arkade.sh",
|
|
568
|
+
mutinynet: "https://mutinynet.arkade.sh"
|
|
569
|
+
};
|
|
570
|
+
function defaultArkadeServerUrl(network) {
|
|
571
|
+
return DEFAULT_ARKADE_URLS[network.toLowerCase()];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/contracts/bitcoin-manager.ts
|
|
575
|
+
var import_base5 = require("@scure/base");
|
|
576
|
+
|
|
577
|
+
// src/contracts/bitcoin.ts
|
|
578
|
+
var import_base4 = require("@scure/base");
|
|
579
|
+
function classifyBitcoinSpend(witness, paymentHash) {
|
|
580
|
+
for (const item of witness) {
|
|
581
|
+
if (item.length !== 64) continue;
|
|
582
|
+
const bytes = import_base4.hex.decode(item);
|
|
583
|
+
if (preimageMatches(bytes, paymentHash))
|
|
584
|
+
return { spend: "claim", preimage: bytes };
|
|
585
|
+
}
|
|
586
|
+
return { spend: "refund" };
|
|
587
|
+
}
|
|
588
|
+
function bitcoinObservation(facts, paymentHash, expectedSats) {
|
|
589
|
+
if (facts.spendWitness) {
|
|
590
|
+
const spend = classifyBitcoinSpend(facts.spendWitness, paymentHash);
|
|
591
|
+
return spend.spend === "claim" ? { observation: "spent_claim", preimage: spend.preimage } : { observation: "spent_refund" };
|
|
592
|
+
}
|
|
593
|
+
if (facts.funding === "confirmed" && facts.fundedSats < expectedSats) {
|
|
594
|
+
return { observation: "invalid" };
|
|
595
|
+
}
|
|
596
|
+
return { observation: facts.funding };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// src/contracts/bitcoin-manager.ts
|
|
600
|
+
var BitcoinContractManager = class _BitcoinContractManager {
|
|
601
|
+
ledger = "bitcoin";
|
|
602
|
+
#reader;
|
|
603
|
+
#chainTime;
|
|
604
|
+
/** address → the ref we're tracking. */
|
|
605
|
+
#refs = /* @__PURE__ */ new Map();
|
|
606
|
+
/** address → last known observation. */
|
|
607
|
+
#obs = /* @__PURE__ */ new Map();
|
|
608
|
+
/** address → the preimage a claim revealed. */
|
|
609
|
+
#preimages = /* @__PURE__ */ new Map();
|
|
610
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
611
|
+
#now;
|
|
612
|
+
constructor(deps) {
|
|
613
|
+
this.#reader = deps.reader;
|
|
614
|
+
this.#chainTime = deps.chainTime;
|
|
615
|
+
}
|
|
616
|
+
static fromDeps(deps) {
|
|
617
|
+
return new _BitcoinContractManager(deps);
|
|
618
|
+
}
|
|
619
|
+
static async create(config) {
|
|
620
|
+
const { esploraReader: esploraReader2 } = await Promise.resolve().then(() => (init_bitcoin_reader_esplora(), bitcoin_reader_esplora_exports));
|
|
621
|
+
return _BitcoinContractManager.fromDeps({
|
|
622
|
+
reader: esploraReader2(config.esploraUrl),
|
|
623
|
+
chainTime: config.chainTime
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
async register(ref) {
|
|
627
|
+
if (ref.ledger !== "bitcoin")
|
|
628
|
+
throw new Error(
|
|
629
|
+
`BitcoinContractManager can't track a '${ref.ledger}' HTLC`
|
|
630
|
+
);
|
|
631
|
+
this.#refs.set(ref.address, ref);
|
|
632
|
+
await this.#reconcileRef(ref);
|
|
633
|
+
}
|
|
634
|
+
async unregister(ref) {
|
|
635
|
+
if (ref.ledger !== "bitcoin") return;
|
|
636
|
+
this.#refs.delete(ref.address);
|
|
637
|
+
this.#obs.delete(ref.address);
|
|
638
|
+
this.#preimages.delete(ref.address);
|
|
639
|
+
}
|
|
640
|
+
getState(ref) {
|
|
641
|
+
return ref.ledger === "bitcoin" ? this.#obs.get(ref.address) : void 0;
|
|
642
|
+
}
|
|
643
|
+
chainNow(_ref) {
|
|
644
|
+
return this.#now;
|
|
645
|
+
}
|
|
646
|
+
onEvent(cb) {
|
|
647
|
+
this.#listeners.add(cb);
|
|
648
|
+
return () => this.#listeners.delete(cb);
|
|
649
|
+
}
|
|
650
|
+
async refresh() {
|
|
651
|
+
if (this.#chainTime) this.#now = await this.#chainTime();
|
|
652
|
+
await Promise.all(
|
|
653
|
+
[...this.#refs.values()].map((ref) => this.#reconcileRef(ref))
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
dispose() {
|
|
657
|
+
this.#listeners.clear();
|
|
658
|
+
}
|
|
659
|
+
/** The preimage a claim revealed on this HTLC, if one was seen. */
|
|
660
|
+
getPreimage(ref) {
|
|
661
|
+
return ref.ledger === "bitcoin" ? this.#preimages.get(ref.address) : void 0;
|
|
662
|
+
}
|
|
663
|
+
async #reconcileRef(ref) {
|
|
664
|
+
const facts = await this.#reader.getHtlcFacts(ref.address);
|
|
665
|
+
const { observation, preimage } = bitcoinObservation(
|
|
666
|
+
facts,
|
|
667
|
+
import_base5.hex.decode(ref.preimageHash),
|
|
668
|
+
ref.expectedSats
|
|
669
|
+
);
|
|
670
|
+
if (preimage) this.#preimages.set(ref.address, preimage);
|
|
671
|
+
this.#set(ref.address, observation);
|
|
672
|
+
}
|
|
673
|
+
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
674
|
+
#set(address, observation) {
|
|
675
|
+
const ref = this.#refs.get(address);
|
|
676
|
+
if (!ref) return;
|
|
677
|
+
const current = this.#obs.get(address);
|
|
678
|
+
if (current === observation) return;
|
|
679
|
+
const spendStates = ["spent_claim", "spent_refund"];
|
|
680
|
+
if (current && spendStates.includes(current) && !spendStates.includes(observation))
|
|
681
|
+
return;
|
|
682
|
+
this.#obs.set(address, observation);
|
|
683
|
+
for (const listener of this.#listeners) listener(ref, observation);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/client.ts
|
|
688
|
+
init_bitcoin_reader_esplora();
|
|
689
|
+
|
|
690
|
+
// src/contracts/evm.ts
|
|
691
|
+
function evmObservation(events, expected, pending = false) {
|
|
692
|
+
const redeemed = events.find((e) => e.kind === "redeemed");
|
|
693
|
+
if (redeemed?.kind === "redeemed")
|
|
694
|
+
return { observation: "spent_claim", preimage: redeemed.preimage };
|
|
695
|
+
if (events.some((e) => e.kind === "refunded"))
|
|
696
|
+
return { observation: "spent_refund" };
|
|
697
|
+
const created = events.find((e) => e.kind === "created");
|
|
698
|
+
if (created?.kind === "created") {
|
|
699
|
+
const amountOk = created.amount >= expected.amount;
|
|
700
|
+
const tokenOk = expected.token === void 0 || created.token.toLowerCase() === expected.token.toLowerCase();
|
|
701
|
+
return { observation: amountOk && tokenOk ? "confirmed" : "invalid" };
|
|
702
|
+
}
|
|
703
|
+
return { observation: pending ? "mempool" : "absent" };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/contracts/types.ts
|
|
707
|
+
function htlcKey(ref) {
|
|
708
|
+
switch (ref.ledger) {
|
|
709
|
+
case "arkade":
|
|
710
|
+
return `arkade:${ref.script}`;
|
|
711
|
+
case "bitcoin":
|
|
712
|
+
return `bitcoin:${ref.address}`;
|
|
713
|
+
case "evm":
|
|
714
|
+
return `evm:${ref.chainId}:${ref.preimageHash}`;
|
|
715
|
+
case "lightning":
|
|
716
|
+
return `lightning:${ref.paymentHash}`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/contracts/evm-manager.ts
|
|
721
|
+
var EvmContractManager = class _EvmContractManager {
|
|
722
|
+
ledger = "evm";
|
|
723
|
+
#readers;
|
|
724
|
+
/** htlcKey → the ref we're tracking. */
|
|
725
|
+
#refs = /* @__PURE__ */ new Map();
|
|
726
|
+
/** htlcKey → last known observation. */
|
|
727
|
+
#obs = /* @__PURE__ */ new Map();
|
|
728
|
+
/** htlcKey → the preimage a claim revealed. */
|
|
729
|
+
#preimages = /* @__PURE__ */ new Map();
|
|
730
|
+
/** chainId → its latest block time (ms). */
|
|
731
|
+
#now = /* @__PURE__ */ new Map();
|
|
732
|
+
/** chainId → unsubscribe for its (single, shared) watch. */
|
|
733
|
+
#watchUnsubs = /* @__PURE__ */ new Map();
|
|
734
|
+
/** chainId → a block-event reconcile is currently running (coalescing). */
|
|
735
|
+
#reconciling = /* @__PURE__ */ new Set();
|
|
736
|
+
/** chainId → a block event arrived mid-reconcile; run exactly one more. */
|
|
737
|
+
#reconcileAgain = /* @__PURE__ */ new Set();
|
|
738
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
739
|
+
constructor(deps) {
|
|
740
|
+
this.#readers = deps.readers;
|
|
741
|
+
}
|
|
742
|
+
static fromDeps(deps) {
|
|
743
|
+
return new _EvmContractManager(deps);
|
|
744
|
+
}
|
|
745
|
+
async register(ref) {
|
|
746
|
+
if (ref.ledger !== "evm")
|
|
747
|
+
throw new Error(`EvmContractManager can't track a '${ref.ledger}' HTLC`);
|
|
748
|
+
if (!this.#readers.has(ref.chainId))
|
|
749
|
+
throw new Error(
|
|
750
|
+
`no EVM reader for chain ${ref.chainId} \u2014 configure it via ClientBuilder.withEvmRpcUrls()`
|
|
751
|
+
);
|
|
752
|
+
this.#refs.set(htlcKey(ref), ref);
|
|
753
|
+
this.#ensureWatch(ref.chainId);
|
|
754
|
+
await this.#reconcileChain(ref.chainId);
|
|
755
|
+
}
|
|
756
|
+
async unregister(ref) {
|
|
757
|
+
if (ref.ledger !== "evm") return;
|
|
758
|
+
const key = htlcKey(ref);
|
|
759
|
+
this.#refs.delete(key);
|
|
760
|
+
this.#obs.delete(key);
|
|
761
|
+
this.#preimages.delete(key);
|
|
762
|
+
if (![...this.#refs.values()].some((r) => r.chainId === ref.chainId)) {
|
|
763
|
+
this.#watchUnsubs.get(ref.chainId)?.();
|
|
764
|
+
this.#watchUnsubs.delete(ref.chainId);
|
|
765
|
+
this.#now.delete(ref.chainId);
|
|
766
|
+
this.#reconcileAgain.delete(ref.chainId);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
getState(ref) {
|
|
770
|
+
return ref.ledger === "evm" ? this.#obs.get(htlcKey(ref)) : void 0;
|
|
771
|
+
}
|
|
772
|
+
chainNow(ref) {
|
|
773
|
+
return ref.ledger === "evm" ? this.#now.get(ref.chainId) : void 0;
|
|
774
|
+
}
|
|
775
|
+
onEvent(cb) {
|
|
776
|
+
this.#listeners.add(cb);
|
|
777
|
+
return () => this.#listeners.delete(cb);
|
|
778
|
+
}
|
|
779
|
+
async refresh() {
|
|
780
|
+
const chainIds = new Set([...this.#refs.values()].map((r) => r.chainId));
|
|
781
|
+
await Promise.all([...chainIds].map((c) => this.#reconcileChain(c)));
|
|
782
|
+
}
|
|
783
|
+
dispose() {
|
|
784
|
+
for (const unsub of this.#watchUnsubs.values()) unsub();
|
|
785
|
+
this.#watchUnsubs.clear();
|
|
786
|
+
this.#reconciling.clear();
|
|
787
|
+
this.#reconcileAgain.clear();
|
|
788
|
+
this.#listeners.clear();
|
|
789
|
+
}
|
|
790
|
+
/** The preimage a claim revealed on this HTLC, if one was seen. */
|
|
791
|
+
getPreimage(ref) {
|
|
792
|
+
return ref.ledger === "evm" ? this.#preimages.get(htlcKey(ref)) : void 0;
|
|
793
|
+
}
|
|
794
|
+
#ensureWatch(chainId) {
|
|
795
|
+
if (this.#watchUnsubs.has(chainId)) return;
|
|
796
|
+
const reader = this.#readers.get(chainId);
|
|
797
|
+
if (!reader) return;
|
|
798
|
+
this.#watchUnsubs.set(
|
|
799
|
+
chainId,
|
|
800
|
+
reader.watch(() => this.#scheduleReconcile(chainId))
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Reconcile a chain in response to a block/log event, coalesced: at most one
|
|
805
|
+
* reconcile runs per chain at a time, with at most one queued rerun to pick up
|
|
806
|
+
* changes that arrived mid-run. This stops fast chains / slow RPCs from stacking
|
|
807
|
+
* redundant full-log scans (which amplify rate limits), and the `.catch` keeps a
|
|
808
|
+
* transient RPC failure in a background reconcile from becoming an unhandled
|
|
809
|
+
* rejection. `register`/`refresh` still await `#reconcileChain` directly.
|
|
810
|
+
*/
|
|
811
|
+
#scheduleReconcile(chainId) {
|
|
812
|
+
if (this.#reconciling.has(chainId)) {
|
|
813
|
+
this.#reconcileAgain.add(chainId);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.#reconciling.add(chainId);
|
|
817
|
+
void this.#reconcileChain(chainId).catch((error) => {
|
|
818
|
+
console.warn(`EVM reconcile failed for chain ${chainId}:`, error);
|
|
819
|
+
}).finally(() => {
|
|
820
|
+
this.#reconciling.delete(chainId);
|
|
821
|
+
if (this.#reconcileAgain.delete(chainId))
|
|
822
|
+
this.#scheduleReconcile(chainId);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/** Refresh one chain's clock and re-observe every HTLC tracked on it. */
|
|
826
|
+
async #reconcileChain(chainId) {
|
|
827
|
+
const reader = this.#readers.get(chainId);
|
|
828
|
+
if (!reader) return;
|
|
829
|
+
this.#now.set(chainId, await reader.getBlockTimeMs());
|
|
830
|
+
await Promise.all(
|
|
831
|
+
[...this.#refs.values()].filter((r) => r.chainId === chainId).map((r) => this.#reconcileRef(reader, r))
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
async #reconcileRef(reader, ref) {
|
|
835
|
+
const events = await reader.getHtlcEvents(
|
|
836
|
+
ref.htlc,
|
|
837
|
+
ref.preimageHash,
|
|
838
|
+
ref.claimAddress
|
|
839
|
+
);
|
|
840
|
+
const { observation, preimage } = evmObservation(events, {
|
|
841
|
+
amount: ref.expectedAmount,
|
|
842
|
+
token: ref.expectedToken
|
|
843
|
+
});
|
|
844
|
+
const key = htlcKey(ref);
|
|
845
|
+
if (preimage) this.#preimages.set(key, preimage);
|
|
846
|
+
this.#set(key, observation);
|
|
847
|
+
}
|
|
848
|
+
/** Update an observation and notify on change; never downgrade a resolved spend. */
|
|
849
|
+
#set(key, observation) {
|
|
850
|
+
const ref = this.#refs.get(key);
|
|
851
|
+
if (!ref) return;
|
|
852
|
+
const current = this.#obs.get(key);
|
|
853
|
+
if (current === observation) return;
|
|
854
|
+
const spendStates = ["spent_claim", "spent_refund"];
|
|
855
|
+
if (current && spendStates.includes(current) && !spendStates.includes(observation))
|
|
856
|
+
return;
|
|
857
|
+
this.#obs.set(key, observation);
|
|
858
|
+
for (const listener of this.#listeners) listener(ref, observation);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/contracts/evm-reader-viem.ts
|
|
863
|
+
var import_viem = require("viem");
|
|
864
|
+
var DEFAULT_EVM_RPCS = {
|
|
865
|
+
// Polygon — viem's default (polygon.drpc.org) misbehaves for some calls, so
|
|
866
|
+
// list working public RPCs explicitly.
|
|
867
|
+
137: [
|
|
868
|
+
"https://polygon.drpc.org",
|
|
869
|
+
"https://tenderly.rpc.polygon.community",
|
|
870
|
+
"https://polygon-bor-rpc.publicnode.com"
|
|
871
|
+
],
|
|
872
|
+
1: [
|
|
873
|
+
"https://ethereum-rpc.publicnode.com",
|
|
874
|
+
"https://rpc.ankr.com/eth",
|
|
875
|
+
"https://eth.drpc.org"
|
|
876
|
+
],
|
|
877
|
+
42161: [
|
|
878
|
+
"https://arbitrum-one-rpc.publicnode.com",
|
|
879
|
+
"https://rpc.ankr.com/arbitrum",
|
|
880
|
+
"https://arb1.arbitrum.io/rpc"
|
|
881
|
+
]
|
|
882
|
+
};
|
|
883
|
+
var SWAP_CREATED = (0, import_viem.parseAbiItem)(
|
|
884
|
+
"event SwapCreated(bytes32 indexed preimageHash, address indexed refundAddress, address indexed claimAddress, address token, uint256 amount, uint256 timelock)"
|
|
885
|
+
);
|
|
886
|
+
var SWAP_REDEEMED = (0, import_viem.parseAbiItem)(
|
|
887
|
+
"event SwapRedeemed(bytes32 indexed preimageHash, bytes32 preimage)"
|
|
888
|
+
);
|
|
889
|
+
var SWAP_REFUNDED = (0, import_viem.parseAbiItem)(
|
|
890
|
+
"event SwapRefunded(bytes32 indexed preimageHash)"
|
|
891
|
+
);
|
|
892
|
+
function evmReaderFromClient(client) {
|
|
893
|
+
return {
|
|
894
|
+
async getHtlcEvents(htlc, preimageHash, claimAddress) {
|
|
895
|
+
const [created, redeemed, refunded] = await Promise.all([
|
|
896
|
+
// Filter by claimAddress too (also indexed), so only the HTLC actually
|
|
897
|
+
// claimable on the swap's terms is seen.
|
|
898
|
+
client.getLogs({
|
|
899
|
+
address: htlc,
|
|
900
|
+
event: SWAP_CREATED,
|
|
901
|
+
args: { preimageHash, claimAddress },
|
|
902
|
+
fromBlock: "earliest"
|
|
903
|
+
}),
|
|
904
|
+
client.getLogs({
|
|
905
|
+
address: htlc,
|
|
906
|
+
event: SWAP_REDEEMED,
|
|
907
|
+
args: { preimageHash },
|
|
908
|
+
fromBlock: "earliest"
|
|
909
|
+
}),
|
|
910
|
+
client.getLogs({
|
|
911
|
+
address: htlc,
|
|
912
|
+
event: SWAP_REFUNDED,
|
|
913
|
+
args: { preimageHash },
|
|
914
|
+
fromBlock: "earliest"
|
|
915
|
+
})
|
|
916
|
+
]);
|
|
917
|
+
const events = [];
|
|
918
|
+
for (const log of created) {
|
|
919
|
+
const args = log.args;
|
|
920
|
+
events.push({
|
|
921
|
+
kind: "created",
|
|
922
|
+
amount: args?.amount ?? 0n,
|
|
923
|
+
token: args?.token ?? "0x"
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
for (const log of redeemed) {
|
|
927
|
+
const preimage = log.args?.preimage;
|
|
928
|
+
if (preimage) events.push({ kind: "redeemed", preimage });
|
|
929
|
+
}
|
|
930
|
+
if (refunded.length > 0) events.push({ kind: "refunded" });
|
|
931
|
+
return events;
|
|
932
|
+
},
|
|
933
|
+
async getBlockTimeMs() {
|
|
934
|
+
const block = await client.getBlock();
|
|
935
|
+
return Number(block.timestamp) * 1e3;
|
|
936
|
+
},
|
|
937
|
+
watch(cb) {
|
|
938
|
+
return client.watchBlocks({ onBlock: () => cb() });
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function createEvmRpcReader(rpcUrls) {
|
|
943
|
+
const urls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
944
|
+
const transport = urls.length > 1 ? (0, import_viem.fallback)(
|
|
945
|
+
urls.map((url) => (0, import_viem.http)(url)),
|
|
946
|
+
{ rank: { interval: 6e4, sampleCount: 3, timeout: 1e3 } }
|
|
947
|
+
) : (0, import_viem.http)(urls[0]);
|
|
948
|
+
const client = (0, import_viem.createPublicClient)({ transport });
|
|
949
|
+
return evmReaderFromClient(client);
|
|
950
|
+
}
|
|
951
|
+
function defaultEvmReaders(overrides) {
|
|
952
|
+
const chainIds = /* @__PURE__ */ new Set([
|
|
953
|
+
...Object.keys(DEFAULT_EVM_RPCS).map(Number),
|
|
954
|
+
...Object.keys(overrides ?? {}).map(Number)
|
|
955
|
+
]);
|
|
956
|
+
const readers = /* @__PURE__ */ new Map();
|
|
957
|
+
for (const chainId of chainIds) {
|
|
958
|
+
const override = overrides?.[chainId];
|
|
959
|
+
const defaults = DEFAULT_EVM_RPCS[chainId] ?? [];
|
|
960
|
+
const urls = override ? [override, ...defaults] : defaults;
|
|
961
|
+
if (urls.length > 0) readers.set(chainId, createEvmRpcReader(urls));
|
|
962
|
+
}
|
|
963
|
+
return readers;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/contracts/arkade-vhtlc.ts
|
|
967
|
+
var import_sdk3 = require("@arkade-os/sdk");
|
|
968
|
+
var import_legacy2 = require("@noble/hashes/legacy.js");
|
|
969
|
+
var import_base6 = require("@scure/base");
|
|
970
|
+
function strip0x(value) {
|
|
971
|
+
return value.startsWith("0x") ? value.slice(2) : value;
|
|
972
|
+
}
|
|
973
|
+
function xOnly(pubKeyHex) {
|
|
974
|
+
const bytes = import_base6.hex.decode(strip0x(pubKeyHex));
|
|
975
|
+
if (bytes.length === 33) return bytes.slice(1);
|
|
976
|
+
if (bytes.length === 32) return bytes;
|
|
977
|
+
throw new Error(`invalid public key length: ${bytes.length}`);
|
|
978
|
+
}
|
|
979
|
+
var seconds = (value) => ({ type: "seconds", value: BigInt(value) });
|
|
980
|
+
function buildArkadeVhtlcRef(input) {
|
|
981
|
+
const hashLockBytes = import_base6.hex.decode(strip0x(input.hashLock));
|
|
982
|
+
const params = {
|
|
983
|
+
sender: xOnly(input.senderPk),
|
|
984
|
+
receiver: xOnly(input.receiverPk),
|
|
985
|
+
server: xOnly(input.serverPk),
|
|
986
|
+
preimageHash: hashLockBytes.length === 20 ? hashLockBytes : (0, import_legacy2.ripemd160)(hashLockBytes),
|
|
987
|
+
refundLocktime: BigInt(input.refundLocktime),
|
|
988
|
+
unilateralClaimDelay: seconds(input.unilateralClaimDelay),
|
|
989
|
+
unilateralRefundDelay: seconds(input.unilateralRefundDelay),
|
|
990
|
+
unilateralRefundWithoutReceiverDelay: seconds(
|
|
991
|
+
input.unilateralRefundWithoutReceiverDelay
|
|
992
|
+
)
|
|
993
|
+
};
|
|
994
|
+
const vhtlc = new import_sdk3.VHTLC.Script(params);
|
|
995
|
+
return {
|
|
996
|
+
ledger: "arkade",
|
|
997
|
+
script: import_base6.hex.encode(vhtlc.pkScript),
|
|
998
|
+
address: input.address,
|
|
999
|
+
preimageHash: strip0x(input.hashLock),
|
|
1000
|
+
expectedSats: input.expectedSats,
|
|
1001
|
+
params: import_sdk3.VHTLCContractHandler.serializeParams(params)
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/tracker/from-swap.ts
|
|
1006
|
+
var ms = (seconds2) => seconds2 * 1e3;
|
|
1007
|
+
var ensure0x = (value) => value.startsWith("0x") ? value : `0x${value}`;
|
|
1008
|
+
var strip0x2 = (value) => value.startsWith("0x") ? value.slice(2) : value;
|
|
1009
|
+
function arkadeLeg(r, address, expectedSats) {
|
|
1010
|
+
return buildArkadeVhtlcRef({
|
|
1011
|
+
senderPk: r.sender_pk,
|
|
1012
|
+
receiverPk: r.receiver_pk,
|
|
1013
|
+
serverPk: r.arkade_server_pk,
|
|
1014
|
+
hashLock: r.hash_lock,
|
|
1015
|
+
address,
|
|
1016
|
+
refundLocktime: r.vhtlc_refund_locktime,
|
|
1017
|
+
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1018
|
+
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1019
|
+
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1020
|
+
expectedSats
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
function evmLeg(args) {
|
|
1024
|
+
return {
|
|
1025
|
+
ledger: "evm",
|
|
1026
|
+
chainId: args.chainId,
|
|
1027
|
+
htlc: ensure0x(args.htlc),
|
|
1028
|
+
preimageHash: ensure0x(args.hashLock),
|
|
1029
|
+
claimAddress: ensure0x(args.claimAddress),
|
|
1030
|
+
expectedAmount: BigInt(args.expectedSats),
|
|
1031
|
+
expectedToken: args.token ? ensure0x(args.token) : void 0
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function bitcoinLeg(address, hashLock, expectedSats) {
|
|
1035
|
+
return {
|
|
1036
|
+
ledger: "bitcoin",
|
|
1037
|
+
address,
|
|
1038
|
+
preimageHash: strip0x2(hashLock),
|
|
1039
|
+
expectedSats
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function swapToTracked(stored) {
|
|
1043
|
+
const r = stored.response;
|
|
1044
|
+
switch (r.direction) {
|
|
1045
|
+
// Client sends Arkade (funds the VHTLC with source_amount sats) and claims EVM.
|
|
1046
|
+
case "arkade_to_evm":
|
|
1047
|
+
return {
|
|
1048
|
+
swapId: r.id,
|
|
1049
|
+
clientHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.source_amount)),
|
|
1050
|
+
serverHtlc: evmLeg({
|
|
1051
|
+
chainId: r.evm_chain_id,
|
|
1052
|
+
htlc: r.evm_htlc_address,
|
|
1053
|
+
hashLock: r.hash_lock,
|
|
1054
|
+
claimAddress: r.client_evm_address,
|
|
1055
|
+
// the client claims the server's EVM HTLC
|
|
1056
|
+
expectedSats: r.evm_expected_sats,
|
|
1057
|
+
token: r.wbtc_address
|
|
1058
|
+
}),
|
|
1059
|
+
clientRefundLocktime: ms(r.vhtlc_refund_locktime),
|
|
1060
|
+
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
1061
|
+
};
|
|
1062
|
+
// Client sends EVM and claims the Arkade VHTLC (server funds it with target_amount sats).
|
|
1063
|
+
case "evm_to_arkade":
|
|
1064
|
+
return {
|
|
1065
|
+
swapId: r.id,
|
|
1066
|
+
clientHtlc: evmLeg({
|
|
1067
|
+
chainId: r.evm_chain_id,
|
|
1068
|
+
htlc: r.evm_htlc_address,
|
|
1069
|
+
hashLock: r.hash_lock,
|
|
1070
|
+
claimAddress: r.server_evm_address,
|
|
1071
|
+
// the server claims the client's EVM HTLC
|
|
1072
|
+
expectedSats: r.evm_expected_sats
|
|
1073
|
+
// evm_to_arkade doesn't expose the locked token — verify amount only.
|
|
1074
|
+
}),
|
|
1075
|
+
serverHtlc: arkadeLeg(r, r.btc_vhtlc_address, Number(r.target_amount)),
|
|
1076
|
+
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1077
|
+
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
1078
|
+
};
|
|
1079
|
+
// Client sends BTC (funds the on-chain HTLC with source_amount sats) and claims EVM.
|
|
1080
|
+
case "bitcoin_to_evm":
|
|
1081
|
+
return {
|
|
1082
|
+
swapId: r.id,
|
|
1083
|
+
clientHtlc: bitcoinLeg(
|
|
1084
|
+
r.btc_htlc_address,
|
|
1085
|
+
r.evm_hash_lock,
|
|
1086
|
+
Number(r.source_amount)
|
|
1087
|
+
),
|
|
1088
|
+
serverHtlc: evmLeg({
|
|
1089
|
+
chainId: r.evm_chain_id,
|
|
1090
|
+
htlc: r.evm_htlc_address,
|
|
1091
|
+
hashLock: r.evm_hash_lock,
|
|
1092
|
+
claimAddress: r.client_evm_address,
|
|
1093
|
+
// the client claims the server's EVM HTLC
|
|
1094
|
+
expectedSats: r.evm_expected_sats,
|
|
1095
|
+
token: r.wbtc_address
|
|
1096
|
+
}),
|
|
1097
|
+
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1098
|
+
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
1099
|
+
};
|
|
1100
|
+
// Client sends EVM and claims the BTC HTLC (server funds it with target_amount sats).
|
|
1101
|
+
case "evm_to_bitcoin":
|
|
1102
|
+
return {
|
|
1103
|
+
swapId: r.id,
|
|
1104
|
+
clientHtlc: evmLeg({
|
|
1105
|
+
chainId: r.evm_chain_id,
|
|
1106
|
+
htlc: r.evm_htlc_address,
|
|
1107
|
+
hashLock: r.evm_hash_lock,
|
|
1108
|
+
claimAddress: r.server_evm_address,
|
|
1109
|
+
// the server claims the client's EVM HTLC
|
|
1110
|
+
expectedSats: r.evm_expected_sats,
|
|
1111
|
+
token: r.wbtc_address
|
|
1112
|
+
}),
|
|
1113
|
+
serverHtlc: bitcoinLeg(
|
|
1114
|
+
r.btc_htlc_address,
|
|
1115
|
+
r.evm_hash_lock,
|
|
1116
|
+
Number(r.target_amount)
|
|
1117
|
+
),
|
|
1118
|
+
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1119
|
+
serverRefundLocktime: ms(r.btc_refund_locktime)
|
|
1120
|
+
};
|
|
1121
|
+
// Client sends BTC (funds the on-chain HTLC) and claims the Arkade VHTLC (the
|
|
1122
|
+
// server funds it). Unlike the other Arkade directions, the VHTLC's receiver is
|
|
1123
|
+
// the client's own key (not in the response) and its funder is `server_vhtlc_pk`;
|
|
1124
|
+
// the hash lock is a 20-byte HASH160, which buildArkadeVhtlcRef takes as-is.
|
|
1125
|
+
case "btc_to_arkade":
|
|
1126
|
+
return {
|
|
1127
|
+
swapId: r.id,
|
|
1128
|
+
clientHtlc: bitcoinLeg(
|
|
1129
|
+
r.btc_htlc_address,
|
|
1130
|
+
r.hash_lock,
|
|
1131
|
+
Number(r.source_amount)
|
|
1132
|
+
),
|
|
1133
|
+
serverHtlc: buildArkadeVhtlcRef({
|
|
1134
|
+
senderPk: r.server_vhtlc_pk,
|
|
1135
|
+
// the server funds the VHTLC
|
|
1136
|
+
receiverPk: stored.publicKey,
|
|
1137
|
+
// the client claims it
|
|
1138
|
+
serverPk: r.arkade_server_pk,
|
|
1139
|
+
hashLock: r.hash_lock,
|
|
1140
|
+
address: r.arkade_vhtlc_address,
|
|
1141
|
+
refundLocktime: r.vhtlc_refund_locktime,
|
|
1142
|
+
unilateralClaimDelay: r.unilateral_claim_delay,
|
|
1143
|
+
unilateralRefundDelay: r.unilateral_refund_delay,
|
|
1144
|
+
unilateralRefundWithoutReceiverDelay: r.unilateral_refund_without_receiver_delay,
|
|
1145
|
+
expectedSats: Number(r.target_amount)
|
|
1146
|
+
}),
|
|
1147
|
+
clientRefundLocktime: ms(r.btc_refund_locktime),
|
|
1148
|
+
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
1149
|
+
};
|
|
1150
|
+
// ─── Lightning: one on-chain leg, one off-chain LN payment ───────────────
|
|
1151
|
+
// Receive-on-Lightning: the client funds an on-chain HTLC and is paid over
|
|
1152
|
+
// Lightning. No server leg to watch — the server sweeping the client's leg
|
|
1153
|
+
// (with the preimage it got by paying the invoice) is the done signal.
|
|
1154
|
+
// Client funds the Arkade VHTLC; Boltz pays the invoice. The deposit is
|
|
1155
|
+
// `boltz_amount_sats` (source minus fees) — the amount actually locked, not
|
|
1156
|
+
// `source_amount` — else the client's own funding reads as under-funded.
|
|
1157
|
+
case "arkade_to_lightning":
|
|
1158
|
+
return {
|
|
1159
|
+
swapId: r.id,
|
|
1160
|
+
clientHtlc: arkadeLeg(
|
|
1161
|
+
r,
|
|
1162
|
+
r.arkade_vhtlc_address,
|
|
1163
|
+
Number(r.boltz_amount_sats)
|
|
1164
|
+
),
|
|
1165
|
+
clientRefundLocktime: ms(r.vhtlc_refund_locktime),
|
|
1166
|
+
serverRefundLocktime: 0
|
|
1167
|
+
// no on-chain server leg
|
|
1168
|
+
};
|
|
1169
|
+
// Client funds the EVM HTLC (server claims it after paying the invoice).
|
|
1170
|
+
case "evm_to_lightning":
|
|
1171
|
+
return {
|
|
1172
|
+
swapId: r.id,
|
|
1173
|
+
clientHtlc: evmLeg({
|
|
1174
|
+
chainId: r.evm_chain_id,
|
|
1175
|
+
htlc: r.evm_htlc_address,
|
|
1176
|
+
hashLock: r.hash_lock,
|
|
1177
|
+
claimAddress: r.server_evm_address,
|
|
1178
|
+
// the server claims the client's leg
|
|
1179
|
+
expectedSats: r.evm_expected_sats
|
|
1180
|
+
// evm_to_lightning doesn't expose the locked token — verify amount only.
|
|
1181
|
+
}),
|
|
1182
|
+
clientRefundLocktime: ms(r.evm_refund_locktime),
|
|
1183
|
+
serverRefundLocktime: 0
|
|
1184
|
+
// no on-chain server leg
|
|
1185
|
+
};
|
|
1186
|
+
// Pay-on-Lightning: the client pays a Lightning invoice (off-chain, nothing to
|
|
1187
|
+
// watch) and claims an on-chain HTLC. No client-funded leg — the client's
|
|
1188
|
+
// claim completes the swap; a hold invoice that never settles auto-unwinds.
|
|
1189
|
+
// Client pays the invoice, then claims the Arkade VHTLC (target_amount).
|
|
1190
|
+
case "lightning_to_arkade":
|
|
1191
|
+
return {
|
|
1192
|
+
swapId: r.id,
|
|
1193
|
+
serverHtlc: arkadeLeg(
|
|
1194
|
+
r,
|
|
1195
|
+
r.arkade_vhtlc_address,
|
|
1196
|
+
Number(r.target_amount)
|
|
1197
|
+
),
|
|
1198
|
+
clientRefundLocktime: 0,
|
|
1199
|
+
// no on-chain client leg
|
|
1200
|
+
serverRefundLocktime: ms(r.vhtlc_refund_locktime)
|
|
1201
|
+
};
|
|
1202
|
+
// Client pays the invoice, then claims the EVM HTLC.
|
|
1203
|
+
case "lightning_to_evm":
|
|
1204
|
+
return {
|
|
1205
|
+
swapId: r.id,
|
|
1206
|
+
serverHtlc: evmLeg({
|
|
1207
|
+
chainId: r.evm_chain_id,
|
|
1208
|
+
htlc: r.evm_htlc_address,
|
|
1209
|
+
hashLock: r.hash_lock,
|
|
1210
|
+
claimAddress: r.client_evm_address,
|
|
1211
|
+
// the client claims the server's leg
|
|
1212
|
+
expectedSats: r.evm_expected_sats,
|
|
1213
|
+
token: r.wbtc_address
|
|
1214
|
+
}),
|
|
1215
|
+
clientRefundLocktime: 0,
|
|
1216
|
+
// no on-chain client leg
|
|
1217
|
+
serverRefundLocktime: ms(r.evm_refund_locktime)
|
|
1218
|
+
};
|
|
1219
|
+
default:
|
|
1220
|
+
return void 0;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/tracker/swap-tracker.ts
|
|
1225
|
+
var SwapTracker = class {
|
|
1226
|
+
#managers;
|
|
1227
|
+
#swaps = /* @__PURE__ */ new Map();
|
|
1228
|
+
/** Last actions emitted per swap — for dedupe and the subscribe-time snapshot. */
|
|
1229
|
+
#lastActions = /* @__PURE__ */ new Map();
|
|
1230
|
+
#subscribers = /* @__PURE__ */ new Set();
|
|
1231
|
+
#refreshIntervalMs;
|
|
1232
|
+
#eventUnsubs = [];
|
|
1233
|
+
#timer;
|
|
1234
|
+
constructor(managers, options) {
|
|
1235
|
+
this.#managers = managers;
|
|
1236
|
+
this.#refreshIntervalMs = options?.refreshIntervalMs ?? 0;
|
|
1237
|
+
}
|
|
1238
|
+
/** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
|
|
1239
|
+
async startTracking(swaps) {
|
|
1240
|
+
for (const swap of swaps) {
|
|
1241
|
+
this.#swaps.set(swap.swapId, swap);
|
|
1242
|
+
for (const leg of legsOf(swap)) await this.#managerFor(leg).register(leg);
|
|
1243
|
+
}
|
|
1244
|
+
for (const manager of new Set(this.#managers.values())) {
|
|
1245
|
+
this.#eventUnsubs.push(manager.onEvent(() => this.#recomputeAll()));
|
|
1246
|
+
}
|
|
1247
|
+
await this.#refreshManagers();
|
|
1248
|
+
this.#recomputeAll();
|
|
1249
|
+
if (this.#refreshIntervalMs > 0)
|
|
1250
|
+
this.#timer = setInterval(
|
|
1251
|
+
() => void this.#tick(),
|
|
1252
|
+
this.#refreshIntervalMs
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
async #tick() {
|
|
1256
|
+
await this.#refreshManagers();
|
|
1257
|
+
this.#recomputeAll();
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Re-poll the managers that have a registered leg — seeding/advancing their
|
|
1261
|
+
* clocks and reconciling observations. Managers with nothing tracked are
|
|
1262
|
+
* skipped: refreshing them still hits their clock source (e.g. Arkade/Bitcoin
|
|
1263
|
+
* MTP via the API), so a swap that doesn't touch a ledger can't fail on that
|
|
1264
|
+
* ledger's endpoint being down (the default client builds every manager).
|
|
1265
|
+
*/
|
|
1266
|
+
async #refreshManagers() {
|
|
1267
|
+
const active = /* @__PURE__ */ new Set();
|
|
1268
|
+
for (const swap of this.#swaps.values())
|
|
1269
|
+
for (const leg of legsOf(swap)) active.add(leg.ledger);
|
|
1270
|
+
const managers = new Set(
|
|
1271
|
+
[...active].map((ledger) => this.#managers.get(ledger)).filter((m) => m !== void 0)
|
|
1272
|
+
);
|
|
1273
|
+
await Promise.all([...managers].map((manager) => manager.refresh()));
|
|
1274
|
+
}
|
|
1275
|
+
/** Notify `cb` of the current action for each tracked swap, then on every change. */
|
|
1276
|
+
subscribeToActions(cb) {
|
|
1277
|
+
this.#subscribers.add(cb);
|
|
1278
|
+
for (const [swapId, actions] of this.#lastActions) cb(swapId, actions);
|
|
1279
|
+
return () => this.#subscribers.delete(cb);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Stop tracking: unregister every still-tracked leg (releasing its manager's
|
|
1283
|
+
* watch — e.g. the EVM per-chain block watch that only `unregister` tears down),
|
|
1284
|
+
* drop the poll timer, and detach event listeners + subscribers. Mirrors
|
|
1285
|
+
* `startTracking`'s `register`, so no manager keeps watching a stopped tracker's
|
|
1286
|
+
* swaps. The managers themselves are not owned here, so they are unregistered but
|
|
1287
|
+
* never disposed.
|
|
1288
|
+
*/
|
|
1289
|
+
stop() {
|
|
1290
|
+
if (this.#timer) clearInterval(this.#timer);
|
|
1291
|
+
this.#timer = void 0;
|
|
1292
|
+
for (const unsub of this.#eventUnsubs) unsub();
|
|
1293
|
+
this.#eventUnsubs = [];
|
|
1294
|
+
for (const swap of this.#swaps.values())
|
|
1295
|
+
for (const leg of legsOf(swap))
|
|
1296
|
+
void this.#managerFor(leg).unregister(leg);
|
|
1297
|
+
this.#swaps.clear();
|
|
1298
|
+
this.#subscribers.clear();
|
|
1299
|
+
}
|
|
1300
|
+
#managerFor(ref) {
|
|
1301
|
+
const manager = this.#managers.get(ref.ledger);
|
|
1302
|
+
if (!manager)
|
|
1303
|
+
throw new Error(`no ContractManager for ledger '${ref.ledger}'`);
|
|
1304
|
+
return manager;
|
|
1305
|
+
}
|
|
1306
|
+
#recomputeAll() {
|
|
1307
|
+
for (const swap of this.#swaps.values()) this.#recompute(swap);
|
|
1308
|
+
}
|
|
1309
|
+
#recompute(swap) {
|
|
1310
|
+
let clientHtlc;
|
|
1311
|
+
let serverHtlc;
|
|
1312
|
+
let clientChainNow = 0;
|
|
1313
|
+
let serverChainNow = 0;
|
|
1314
|
+
if (swap.clientHtlc) {
|
|
1315
|
+
const m = this.#managerFor(swap.clientHtlc);
|
|
1316
|
+
clientHtlc = m.getState(swap.clientHtlc);
|
|
1317
|
+
const now = m.chainNow(swap.clientHtlc);
|
|
1318
|
+
if (clientHtlc === void 0 || now === void 0) return;
|
|
1319
|
+
clientChainNow = now;
|
|
1320
|
+
}
|
|
1321
|
+
if (swap.serverHtlc) {
|
|
1322
|
+
const m = this.#managerFor(swap.serverHtlc);
|
|
1323
|
+
serverHtlc = m.getState(swap.serverHtlc);
|
|
1324
|
+
const now = m.chainNow(swap.serverHtlc);
|
|
1325
|
+
if (serverHtlc === void 0 || now === void 0) return;
|
|
1326
|
+
serverChainNow = now;
|
|
1327
|
+
}
|
|
1328
|
+
const status = deriveSwapStatus({ clientHtlc, serverHtlc });
|
|
1329
|
+
if (status === void 0) return;
|
|
1330
|
+
const actions = deriveSwapActions({
|
|
1331
|
+
status,
|
|
1332
|
+
clientChainNow,
|
|
1333
|
+
serverChainNow,
|
|
1334
|
+
clientRefundLocktime: swap.clientRefundLocktime,
|
|
1335
|
+
serverRefundLocktime: swap.serverRefundLocktime,
|
|
1336
|
+
// Pay-on-Lightning swaps have no client-funded on-chain leg.
|
|
1337
|
+
clientFunds: swap.clientHtlc !== void 0
|
|
1338
|
+
});
|
|
1339
|
+
const previous = this.#lastActions.get(swap.swapId);
|
|
1340
|
+
if (previous && JSON.stringify(previous) === JSON.stringify(actions))
|
|
1341
|
+
return;
|
|
1342
|
+
this.#lastActions.set(swap.swapId, actions);
|
|
1343
|
+
for (const cb of this.#subscribers) cb(swap.swapId, actions);
|
|
1344
|
+
if (actions.recommended === "none") this.#untrack(swap);
|
|
1345
|
+
}
|
|
1346
|
+
#untrack(swap) {
|
|
1347
|
+
this.#swaps.delete(swap.swapId);
|
|
1348
|
+
for (const leg of legsOf(swap)) void this.#managerFor(leg).unregister(leg);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
function legsOf(swap) {
|
|
1352
|
+
const legs = [];
|
|
1353
|
+
if (swap.clientHtlc) legs.push(swap.clientHtlc);
|
|
1354
|
+
if (swap.serverHtlc) legs.push(swap.serverHtlc);
|
|
1355
|
+
return legs;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/client.ts
|
|
1359
|
+
var Client = class {
|
|
1360
|
+
/** The wrapped legacy client. Calls are forwarded here until migrated. */
|
|
1361
|
+
#legacy;
|
|
1362
|
+
/** Tracking configuration; managers are built lazily from it on first use. */
|
|
1363
|
+
#tracking;
|
|
1364
|
+
/** Per-ledger monitors, resolved once from {@link #tracking}. */
|
|
1365
|
+
#managers;
|
|
1366
|
+
#tracker;
|
|
1367
|
+
#started = false;
|
|
1368
|
+
constructor(...args) {
|
|
1369
|
+
if (args[0] instanceof import_lendaswap_sdk_pure.Client) {
|
|
1370
|
+
this.#legacy = args[0];
|
|
1371
|
+
this.#tracking = args[1] ?? {
|
|
1372
|
+
enabled: false
|
|
1373
|
+
};
|
|
1374
|
+
} else {
|
|
1375
|
+
this.#legacy = new import_lendaswap_sdk_pure.Client(
|
|
1376
|
+
...args
|
|
1377
|
+
);
|
|
1378
|
+
this.#tracking = { enabled: false };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/** Start building a {@link Client}. */
|
|
1382
|
+
static builder() {
|
|
1383
|
+
return new ClientBuilder();
|
|
1384
|
+
}
|
|
1385
|
+
/** Escape hatch: the underlying legacy client, during migration. */
|
|
1386
|
+
get legacy() {
|
|
1387
|
+
return this.#legacy;
|
|
1388
|
+
}
|
|
1389
|
+
// --- delegated legacy surface (getters) ---
|
|
1390
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1391
|
+
get api() {
|
|
1392
|
+
return this.#legacy.api;
|
|
1393
|
+
}
|
|
1394
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1395
|
+
get baseUrl() {
|
|
1396
|
+
return this.#legacy.baseUrl;
|
|
1397
|
+
}
|
|
1398
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1399
|
+
get cctpInbound() {
|
|
1400
|
+
return this.#legacy.cctpInbound;
|
|
1401
|
+
}
|
|
1402
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1403
|
+
get swapStorage() {
|
|
1404
|
+
return this.#legacy.swapStorage;
|
|
1405
|
+
}
|
|
1406
|
+
// --- delegated legacy surface (methods) ---
|
|
1407
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1408
|
+
subscribeToSwaps(...args) {
|
|
1409
|
+
return this.#legacy.subscribeToSwaps(...args);
|
|
1410
|
+
}
|
|
1411
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1412
|
+
unsubscribeFromSwaps(...args) {
|
|
1413
|
+
return this.#legacy.unsubscribeFromSwaps(...args);
|
|
1414
|
+
}
|
|
1415
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1416
|
+
closeSwapStatusSocket(...args) {
|
|
1417
|
+
return this.#legacy.closeSwapStatusSocket(...args);
|
|
1418
|
+
}
|
|
1419
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1420
|
+
getMnemonic(...args) {
|
|
1421
|
+
return this.#legacy.getMnemonic(...args);
|
|
1422
|
+
}
|
|
1423
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1424
|
+
loadMnemonic(...args) {
|
|
1425
|
+
return this.#legacy.loadMnemonic(...args);
|
|
1426
|
+
}
|
|
1427
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1428
|
+
getNostrKeyHex(...args) {
|
|
1429
|
+
return this.#legacy.getNostrKeyHex(...args);
|
|
1430
|
+
}
|
|
1431
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1432
|
+
getUserIdXpub(...args) {
|
|
1433
|
+
return this.#legacy.getUserIdXpub(...args);
|
|
1434
|
+
}
|
|
1435
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1436
|
+
deriveSwapParams(...args) {
|
|
1437
|
+
return this.#legacy.deriveSwapParams(...args);
|
|
1438
|
+
}
|
|
1439
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1440
|
+
deriveSwapParamsAtIndex(...args) {
|
|
1441
|
+
return this.#legacy.deriveSwapParamsAtIndex(...args);
|
|
1442
|
+
}
|
|
1443
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1444
|
+
getEvmAddress(...args) {
|
|
1445
|
+
return this.#legacy.getEvmAddress(...args);
|
|
1446
|
+
}
|
|
1447
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1448
|
+
getKeyIndex(...args) {
|
|
1449
|
+
return this.#legacy.getKeyIndex(...args);
|
|
1450
|
+
}
|
|
1451
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1452
|
+
setKeyIndex(...args) {
|
|
1453
|
+
return this.#legacy.setKeyIndex(...args);
|
|
1454
|
+
}
|
|
1455
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1456
|
+
getStatus(...args) {
|
|
1457
|
+
return this.#legacy.getStatus(...args);
|
|
1458
|
+
}
|
|
1459
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1460
|
+
healthCheck(...args) {
|
|
1461
|
+
return this.#legacy.healthCheck(...args);
|
|
1462
|
+
}
|
|
1463
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1464
|
+
getVersion(...args) {
|
|
1465
|
+
return this.#legacy.getVersion(...args);
|
|
1466
|
+
}
|
|
1467
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1468
|
+
getSupportAgents(...args) {
|
|
1469
|
+
return this.#legacy.getSupportAgents(...args);
|
|
1470
|
+
}
|
|
1471
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1472
|
+
getMtp(...args) {
|
|
1473
|
+
return this.#legacy.getMtp(...args);
|
|
1474
|
+
}
|
|
1475
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1476
|
+
getTokens(...args) {
|
|
1477
|
+
return this.#legacy.getTokens(...args);
|
|
1478
|
+
}
|
|
1479
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1480
|
+
getSwapPairs(...args) {
|
|
1481
|
+
return this.#legacy.getSwapPairs(...args);
|
|
1482
|
+
}
|
|
1483
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1484
|
+
getChainConfig(...args) {
|
|
1485
|
+
return this.#legacy.getChainConfig(...args);
|
|
1486
|
+
}
|
|
1487
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1488
|
+
getReferralFee(...args) {
|
|
1489
|
+
return this.#legacy.getReferralFee(...args);
|
|
1490
|
+
}
|
|
1491
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1492
|
+
getNetworkFees(...args) {
|
|
1493
|
+
return this.#legacy.getNetworkFees(...args);
|
|
1494
|
+
}
|
|
1495
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1496
|
+
getDexQuote(...args) {
|
|
1497
|
+
return this.#legacy.getDexQuote(...args);
|
|
1498
|
+
}
|
|
1499
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1500
|
+
composeQuote(...args) {
|
|
1501
|
+
return this.#legacy.composeQuote(...args);
|
|
1502
|
+
}
|
|
1503
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1504
|
+
getQuote(...args) {
|
|
1505
|
+
return this.#legacy.getQuote(...args);
|
|
1506
|
+
}
|
|
1507
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1508
|
+
getSwap(...args) {
|
|
1509
|
+
return this.#legacy.getSwap(...args);
|
|
1510
|
+
}
|
|
1511
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1512
|
+
getBulkStatus(...args) {
|
|
1513
|
+
return this.#legacy.getBulkStatus(...args);
|
|
1514
|
+
}
|
|
1515
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1516
|
+
getStoredSwap(...args) {
|
|
1517
|
+
return this.#legacy.getStoredSwap(...args);
|
|
1518
|
+
}
|
|
1519
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1520
|
+
listAllSwaps(...args) {
|
|
1521
|
+
return this.#legacy.listAllSwaps(...args);
|
|
1522
|
+
}
|
|
1523
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1524
|
+
deleteSwap(...args) {
|
|
1525
|
+
return this.#legacy.deleteSwap(...args);
|
|
1526
|
+
}
|
|
1527
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1528
|
+
clearSwapStorage(...args) {
|
|
1529
|
+
return this.#legacy.clearSwapStorage(...args);
|
|
1530
|
+
}
|
|
1531
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1532
|
+
recoverSwaps(...args) {
|
|
1533
|
+
return this.#legacy.recoverSwaps(...args);
|
|
1534
|
+
}
|
|
1535
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1536
|
+
recoverAllSwaps(...args) {
|
|
1537
|
+
return this.#legacy.recoverAllSwaps(...args);
|
|
1538
|
+
}
|
|
1539
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1540
|
+
amountsForSwap(...args) {
|
|
1541
|
+
return this.#legacy.amountsForSwap(...args);
|
|
1542
|
+
}
|
|
1543
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1544
|
+
claim(...args) {
|
|
1545
|
+
return this.#legacy.claim(...args);
|
|
1546
|
+
}
|
|
1547
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1548
|
+
claimViaGasless(...args) {
|
|
1549
|
+
return this.#legacy.claimViaGasless(...args);
|
|
1550
|
+
}
|
|
1551
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1552
|
+
claimArkade(...args) {
|
|
1553
|
+
return this.#legacy.claimArkade(...args);
|
|
1554
|
+
}
|
|
1555
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1556
|
+
continueArkadeClaimSwap(...args) {
|
|
1557
|
+
return this.#legacy.continueArkadeClaimSwap(...args);
|
|
1558
|
+
}
|
|
1559
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1560
|
+
refundSwap(...args) {
|
|
1561
|
+
return this.#legacy.refundSwap(...args);
|
|
1562
|
+
}
|
|
1563
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1564
|
+
getCollabRefundEvmParams(...args) {
|
|
1565
|
+
return this.#legacy.getCollabRefundEvmParams(...args);
|
|
1566
|
+
}
|
|
1567
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1568
|
+
buildCollabRefundEvmTypedData(...args) {
|
|
1569
|
+
return this.#legacy.buildCollabRefundEvmTypedData(...args);
|
|
1570
|
+
}
|
|
1571
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1572
|
+
collabRefundEvmSwap(...args) {
|
|
1573
|
+
return this.#legacy.collabRefundEvmSwap(...args);
|
|
1574
|
+
}
|
|
1575
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1576
|
+
submitCollabRefundEvm(...args) {
|
|
1577
|
+
return this.#legacy.submitCollabRefundEvm(...args);
|
|
1578
|
+
}
|
|
1579
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1580
|
+
createSwap(...args) {
|
|
1581
|
+
return this.#legacy.createSwap(...args);
|
|
1582
|
+
}
|
|
1583
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1584
|
+
createArkadeToEvmSwapGeneric(...args) {
|
|
1585
|
+
return this.#legacy.createArkadeToEvmSwapGeneric(...args);
|
|
1586
|
+
}
|
|
1587
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1588
|
+
createLightningToEvmSwapGeneric(...args) {
|
|
1589
|
+
return this.#legacy.createLightningToEvmSwapGeneric(...args);
|
|
1590
|
+
}
|
|
1591
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1592
|
+
createBitcoinToEvmSwap(...args) {
|
|
1593
|
+
return this.#legacy.createBitcoinToEvmSwap(...args);
|
|
1594
|
+
}
|
|
1595
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1596
|
+
createBitcoinToArkadeSwap(...args) {
|
|
1597
|
+
return this.#legacy.createBitcoinToArkadeSwap(...args);
|
|
1598
|
+
}
|
|
1599
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1600
|
+
createLightningToArkadeSwap(...args) {
|
|
1601
|
+
return this.#legacy.createLightningToArkadeSwap(...args);
|
|
1602
|
+
}
|
|
1603
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1604
|
+
createArkadeToLightningSwap(...args) {
|
|
1605
|
+
return this.#legacy.createArkadeToLightningSwap(...args);
|
|
1606
|
+
}
|
|
1607
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1608
|
+
getArkadeToLightningQuote(...args) {
|
|
1609
|
+
return this.#legacy.getArkadeToLightningQuote(...args);
|
|
1610
|
+
}
|
|
1611
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1612
|
+
retryArkadeToLightningSwap(...args) {
|
|
1613
|
+
return this.#legacy.retryArkadeToLightningSwap(...args);
|
|
1614
|
+
}
|
|
1615
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1616
|
+
createEvmToArkadeSwapGeneric(...args) {
|
|
1617
|
+
return this.#legacy.createEvmToArkadeSwapGeneric(...args);
|
|
1618
|
+
}
|
|
1619
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1620
|
+
createEvmToBitcoinSwap(...args) {
|
|
1621
|
+
return this.#legacy.createEvmToBitcoinSwap(...args);
|
|
1622
|
+
}
|
|
1623
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1624
|
+
createEvmToLightningSwapGeneric(...args) {
|
|
1625
|
+
return this.#legacy.createEvmToLightningSwapGeneric(...args);
|
|
1626
|
+
}
|
|
1627
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1628
|
+
getCoordinatorFundingCallDataPermit2(...args) {
|
|
1629
|
+
return this.#legacy.getCoordinatorFundingCallDataPermit2(...args);
|
|
1630
|
+
}
|
|
1631
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1632
|
+
getPermit2FundingParamsUnsigned(...args) {
|
|
1633
|
+
return this.#legacy.getPermit2FundingParamsUnsigned(...args);
|
|
1634
|
+
}
|
|
1635
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1636
|
+
fundSwap(...args) {
|
|
1637
|
+
return this.#legacy.fundSwap(...args);
|
|
1638
|
+
}
|
|
1639
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1640
|
+
refundEvmWithSigner(...args) {
|
|
1641
|
+
return this.#legacy.refundEvmWithSigner(...args);
|
|
1642
|
+
}
|
|
1643
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1644
|
+
collabRefundEvmWithSigner(...args) {
|
|
1645
|
+
return this.#legacy.collabRefundEvmWithSigner(...args);
|
|
1646
|
+
}
|
|
1647
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1648
|
+
fundSwapGasless(...args) {
|
|
1649
|
+
return this.#legacy.fundSwapGasless(...args);
|
|
1650
|
+
}
|
|
1651
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1652
|
+
getSwapDepositorKey(...args) {
|
|
1653
|
+
return this.#legacy.getSwapDepositorKey(...args);
|
|
1654
|
+
}
|
|
1655
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1656
|
+
getEvmDepositorKey(...args) {
|
|
1657
|
+
return this.#legacy.getEvmDepositorKey(...args);
|
|
1658
|
+
}
|
|
1659
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1660
|
+
recoverGaslessFunds(...args) {
|
|
1661
|
+
return this.#legacy.recoverGaslessFunds(...args);
|
|
1662
|
+
}
|
|
1663
|
+
/** Delegated to the legacy client (migration checkpoint). */
|
|
1664
|
+
hasReceivedVtxo(...args) {
|
|
1665
|
+
return this.#legacy.hasReceivedVtxo(...args);
|
|
1666
|
+
}
|
|
1667
|
+
// --- Satora-native features go below ---
|
|
1668
|
+
/**
|
|
1669
|
+
* Start observing the user's active swaps and deriving each one's next action.
|
|
1670
|
+
*
|
|
1671
|
+
* Loads stored swaps, maps the ones whose ledgers are observable to
|
|
1672
|
+
* {@link TrackedSwap}s, and runs the per-ledger monitors. Idempotent. Requires
|
|
1673
|
+
* the client to have been built with {@link ClientBuilder.withTracking}.
|
|
1674
|
+
* Subscribe with {@link subscribeToActions}; release with
|
|
1675
|
+
* {@link stopTracking}.
|
|
1676
|
+
*/
|
|
1677
|
+
async startTracking() {
|
|
1678
|
+
if (!this.#tracking.enabled)
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
"tracking is disabled \u2014 remove .withoutTracking() to enable it"
|
|
1681
|
+
);
|
|
1682
|
+
if (this.#started) return;
|
|
1683
|
+
this.#started = true;
|
|
1684
|
+
try {
|
|
1685
|
+
const tracker = new SwapTracker(await this.#ensureManagers(), {
|
|
1686
|
+
refreshIntervalMs: this.#tracking.refreshIntervalMs ?? 5e3
|
|
1687
|
+
});
|
|
1688
|
+
this.#tracker = tracker;
|
|
1689
|
+
const swaps = await this.listAllSwaps();
|
|
1690
|
+
const tracked = swaps.map(swapToTracked).filter((s) => s !== void 0);
|
|
1691
|
+
await tracker.startTracking(tracked);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
this.#tracker?.stop();
|
|
1694
|
+
this.#tracker = void 0;
|
|
1695
|
+
this.#started = false;
|
|
1696
|
+
throw error;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Subscribe to next-action updates: `cb` fires with the current action for each
|
|
1701
|
+
* tracked swap immediately, then on every change. Call after
|
|
1702
|
+
* {@link startTracking}. Returns an unsubscribe fn.
|
|
1703
|
+
*/
|
|
1704
|
+
subscribeToActions(cb) {
|
|
1705
|
+
if (!this.#tracker)
|
|
1706
|
+
throw new Error("call startTracking() before subscribeToActions()");
|
|
1707
|
+
return this.#tracker.subscribeToActions(cb);
|
|
1708
|
+
}
|
|
1709
|
+
/** Stop tracking and drop subscribers. The managers themselves are not disposed. */
|
|
1710
|
+
stopTracking() {
|
|
1711
|
+
this.#tracker?.stop();
|
|
1712
|
+
this.#tracker = void 0;
|
|
1713
|
+
this.#started = false;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Resolve the per-ledger managers once — the explicit override if given, else
|
|
1717
|
+
* auto-built from config: an {@link ArkadeContractManager} from the configured
|
|
1718
|
+
* Ark server URL (defaulting to the mainnet server), clocked by `getMtp`, and an
|
|
1719
|
+
* {@link EvmContractManager} from the tested default RPCs.
|
|
1720
|
+
*/
|
|
1721
|
+
async #ensureManagers() {
|
|
1722
|
+
if (this.#managers) return this.#managers;
|
|
1723
|
+
if (this.#tracking.managers) {
|
|
1724
|
+
this.#managers = this.#tracking.managers;
|
|
1725
|
+
return this.#managers;
|
|
1726
|
+
}
|
|
1727
|
+
const managers = /* @__PURE__ */ new Map();
|
|
1728
|
+
const { arkadeServerUrl, evmRpcUrls, esploraUrl } = this.#tracking;
|
|
1729
|
+
const chainTime = async () => (await this.getMtp()).mtp * 1e3;
|
|
1730
|
+
const arkadeUrl = arkadeServerUrl ?? defaultArkadeServerUrl("bitcoin");
|
|
1731
|
+
if (arkadeUrl) {
|
|
1732
|
+
managers.set(
|
|
1733
|
+
"arkade",
|
|
1734
|
+
await ArkadeContractManager.create({ serverUrl: arkadeUrl, chainTime })
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const readers = defaultEvmReaders(evmRpcUrls);
|
|
1738
|
+
if (readers.size > 0)
|
|
1739
|
+
managers.set("evm", EvmContractManager.fromDeps({ readers }));
|
|
1740
|
+
managers.set(
|
|
1741
|
+
"bitcoin",
|
|
1742
|
+
await BitcoinContractManager.create({
|
|
1743
|
+
esploraUrl: esploraUrl ? [esploraUrl] : DEFAULT_ESPLORA_URLS,
|
|
1744
|
+
chainTime
|
|
1745
|
+
})
|
|
1746
|
+
);
|
|
1747
|
+
this.#managers = managers;
|
|
1748
|
+
return managers;
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
var ClientBuilder = class {
|
|
1752
|
+
#inner = import_lendaswap_sdk_pure.Client.builder();
|
|
1753
|
+
// Observe-mode tracking is on by default; the built client auto-builds its
|
|
1754
|
+
// managers from config unless disabled or given an explicit override.
|
|
1755
|
+
#trackingEnabled = true;
|
|
1756
|
+
#arkadeServerUrl;
|
|
1757
|
+
#esploraUrl;
|
|
1758
|
+
#evmRpcUrls;
|
|
1759
|
+
#managers;
|
|
1760
|
+
/**
|
|
1761
|
+
* Override the EVM JSON-RPC endpoint per chainId. Optional — tracking uses
|
|
1762
|
+
* tested public defaults otherwise; an override is tried first, with the
|
|
1763
|
+
* defaults kept as fallbacks.
|
|
1764
|
+
*/
|
|
1765
|
+
withEvmRpcUrls(urls) {
|
|
1766
|
+
this.#evmRpcUrls = urls;
|
|
1767
|
+
return this;
|
|
1768
|
+
}
|
|
1769
|
+
/** Turn observe-mode tracking off. */
|
|
1770
|
+
withoutTracking() {
|
|
1771
|
+
this.#trackingEnabled = false;
|
|
1772
|
+
return this;
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Advanced: supply per-ledger {@link ContractManager}s explicitly instead of
|
|
1776
|
+
* letting the client auto-build them from config (useful for tests or custom
|
|
1777
|
+
* chain sources).
|
|
1778
|
+
*/
|
|
1779
|
+
withContractManagers(managers) {
|
|
1780
|
+
this.#managers = managers;
|
|
1781
|
+
return this;
|
|
1782
|
+
}
|
|
1783
|
+
withBaseUrl(...args) {
|
|
1784
|
+
this.#inner.withBaseUrl(...args);
|
|
1785
|
+
return this;
|
|
1786
|
+
}
|
|
1787
|
+
withReferralCode(...args) {
|
|
1788
|
+
this.#inner.withReferralCode(...args);
|
|
1789
|
+
return this;
|
|
1790
|
+
}
|
|
1791
|
+
withOrgCode(...args) {
|
|
1792
|
+
this.#inner.withOrgCode(...args);
|
|
1793
|
+
return this;
|
|
1794
|
+
}
|
|
1795
|
+
withDefaultHeaders(...args) {
|
|
1796
|
+
this.#inner.withDefaultHeaders(...args);
|
|
1797
|
+
return this;
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Override the Esplora REST URL. Optional for tracking — the Bitcoin manager
|
|
1801
|
+
* otherwise defaults to mainnet mempool.space; set this for a dev/regtest node.
|
|
1802
|
+
*/
|
|
1803
|
+
withEsploraUrl(url) {
|
|
1804
|
+
this.#inner.withEsploraUrl(url);
|
|
1805
|
+
this.#esploraUrl = url;
|
|
1806
|
+
return this;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Override the Ark server URL. Optional for tracking — it otherwise defaults to
|
|
1810
|
+
* the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
|
|
1811
|
+
*/
|
|
1812
|
+
withArkadeServerUrl(url) {
|
|
1813
|
+
this.#inner.withArkadeServerUrl(url);
|
|
1814
|
+
this.#arkadeServerUrl = url;
|
|
1815
|
+
return this;
|
|
1816
|
+
}
|
|
1817
|
+
withSignerStorage(...args) {
|
|
1818
|
+
this.#inner.withSignerStorage(...args);
|
|
1819
|
+
return this;
|
|
1820
|
+
}
|
|
1821
|
+
withSwapStorage(...args) {
|
|
1822
|
+
this.#inner.withSwapStorage(...args);
|
|
1823
|
+
return this;
|
|
1824
|
+
}
|
|
1825
|
+
withMnemonic(...args) {
|
|
1826
|
+
this.#inner.withMnemonic(...args);
|
|
1827
|
+
return this;
|
|
1828
|
+
}
|
|
1829
|
+
withXprv(...args) {
|
|
1830
|
+
this.#inner.withXprv(...args);
|
|
1831
|
+
return this;
|
|
1832
|
+
}
|
|
1833
|
+
withAa(...args) {
|
|
1834
|
+
this.#inner.withAa(...args);
|
|
1835
|
+
return this;
|
|
1836
|
+
}
|
|
1837
|
+
withLogger(...args) {
|
|
1838
|
+
this.#inner.withLogger(...args);
|
|
1839
|
+
return this;
|
|
1840
|
+
}
|
|
1841
|
+
withLogLevel(...args) {
|
|
1842
|
+
this.#inner.withLogLevel(...args);
|
|
1843
|
+
return this;
|
|
1844
|
+
}
|
|
1845
|
+
async build() {
|
|
1846
|
+
return new Client(await this.#inner.build(), {
|
|
1847
|
+
enabled: this.#trackingEnabled,
|
|
1848
|
+
managers: this.#managers,
|
|
1849
|
+
arkadeServerUrl: this.#arkadeServerUrl,
|
|
1850
|
+
esploraUrl: this.#esploraUrl,
|
|
1851
|
+
evmRpcUrls: this.#evmRpcUrls
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
// src/index.ts
|
|
1857
|
+
init_bitcoin_reader_esplora();
|
|
21
1858
|
// Annotate the CommonJS export names for ESM import in node:
|
|
22
1859
|
0 && (module.exports = {
|
|
1860
|
+
ArkadeContractManager,
|
|
1861
|
+
BitcoinContractManager,
|
|
1862
|
+
Client,
|
|
1863
|
+
ClientBuilder,
|
|
1864
|
+
DEFAULT_ESPLORA_URLS,
|
|
1865
|
+
DEFAULT_EVM_RPCS,
|
|
1866
|
+
EvmContractManager,
|
|
1867
|
+
SwapTracker,
|
|
1868
|
+
buildArkadeVhtlcRef,
|
|
1869
|
+
createEvmRpcReader,
|
|
1870
|
+
defaultArkadeServerUrl,
|
|
1871
|
+
defaultEvmReaders,
|
|
1872
|
+
deriveSwapActions,
|
|
1873
|
+
deriveSwapStatus,
|
|
1874
|
+
esploraReader,
|
|
1875
|
+
evmReaderFromClient,
|
|
1876
|
+
htlcFactsFromEsploraTxs,
|
|
1877
|
+
htlcKey,
|
|
1878
|
+
swapToTracked,
|
|
23
1879
|
...require("@lendasat/lendaswap-sdk-pure")
|
|
24
1880
|
});
|
|
25
1881
|
//# sourceMappingURL=index.cjs.map
|