@arkade-os/sdk 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/contracts/contractWatcher.js +33 -3
- package/dist/cjs/contracts/handlers/default.js +10 -3
- package/dist/cjs/contracts/handlers/helpers.js +47 -5
- package/dist/cjs/contracts/handlers/vhtlc.js +4 -2
- package/dist/cjs/identity/descriptor.js +98 -0
- package/dist/cjs/identity/descriptorProvider.js +2 -0
- package/dist/cjs/identity/index.js +15 -1
- package/dist/cjs/identity/seedIdentity.js +91 -6
- package/dist/cjs/identity/serialize.js +166 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +65 -0
- package/dist/cjs/index.js +6 -3
- package/dist/cjs/providers/ark.js +71 -46
- package/dist/cjs/providers/electrum.js +663 -0
- package/dist/cjs/providers/indexer.js +60 -43
- package/dist/cjs/providers/utils.js +62 -12
- package/dist/cjs/wallet/ramps.js +1 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +137 -91
- package/dist/cjs/wallet/vtxo-manager.js +56 -8
- package/dist/cjs/wallet/wallet.js +130 -156
- package/dist/cjs/worker/messageBus.js +200 -56
- package/dist/esm/contracts/contractWatcher.js +33 -3
- package/dist/esm/contracts/handlers/default.js +10 -3
- package/dist/esm/contracts/handlers/helpers.js +47 -5
- package/dist/esm/contracts/handlers/vhtlc.js +4 -2
- package/dist/esm/identity/descriptor.js +92 -0
- package/dist/esm/identity/descriptorProvider.js +1 -0
- package/dist/esm/identity/index.js +6 -1
- package/dist/esm/identity/seedIdentity.js +89 -6
- package/dist/esm/identity/serialize.js +159 -0
- package/dist/esm/identity/staticDescriptorProvider.js +61 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/providers/ark.js +72 -47
- package/dist/esm/providers/electrum.js +658 -0
- package/dist/esm/providers/indexer.js +61 -44
- package/dist/esm/providers/utils.js +61 -12
- package/dist/esm/wallet/ramps.js +1 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +10 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +137 -91
- package/dist/esm/wallet/vtxo-manager.js +56 -8
- package/dist/esm/wallet/wallet.js +130 -156
- package/dist/esm/worker/messageBus.js +201 -57
- package/dist/types/contracts/contractWatcher.d.ts +3 -0
- package/dist/types/contracts/handlers/default.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +1 -1
- package/dist/types/contracts/types.d.ts +11 -3
- package/dist/types/identity/descriptor.d.ts +35 -0
- package/dist/types/identity/descriptorProvider.d.ts +28 -0
- package/dist/types/identity/index.d.ts +7 -1
- package/dist/types/identity/seedIdentity.d.ts +41 -4
- package/dist/types/identity/serialize.d.ts +84 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +18 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/providers/electrum.d.ts +212 -0
- package/dist/types/providers/utils.d.ts +10 -5
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +11 -2
- package/dist/types/wallet/serviceWorker/wallet.d.ts +27 -10
- package/dist/types/wallet/vtxo-manager.d.ts +2 -0
- package/dist/types/wallet/wallet.d.ts +7 -6
- package/dist/types/worker/messageBus.d.ts +68 -8
- package/package.json +3 -2
|
@@ -7,13 +7,20 @@ const ark_1 = require("../providers/ark");
|
|
|
7
7
|
const delegator_1 = require("../providers/delegator");
|
|
8
8
|
const identity_1 = require("../identity");
|
|
9
9
|
const wallet_1 = require("../wallet/wallet");
|
|
10
|
-
const base_1 = require("@scure/base");
|
|
11
10
|
const errors_1 = require("./errors");
|
|
11
|
+
/**
|
|
12
|
+
* Grace period after a handler times out during which late handler
|
|
13
|
+
* completion is still delivered to the client. Once this expires,
|
|
14
|
+
* the bus sends an "Operation abandoned" error so the message id
|
|
15
|
+
* never goes silent indefinitely.
|
|
16
|
+
*/
|
|
17
|
+
const LATE_DELIVERY_GRACE_MS = 5 * 60000;
|
|
12
18
|
class MessageBus {
|
|
13
19
|
/** Create the service-worker message bus with repositories and handler configuration. */
|
|
14
|
-
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
|
|
20
|
+
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, messageTimeoutOverrides = {}, debug = false, buildServices, }) {
|
|
15
21
|
this.walletRepository = walletRepository;
|
|
16
22
|
this.contractRepository = contractRepository;
|
|
23
|
+
this.lateDeliveries = new Set();
|
|
17
24
|
this.running = false;
|
|
18
25
|
this.tickTimeout = null;
|
|
19
26
|
this.tickInProgress = false;
|
|
@@ -23,6 +30,8 @@ class MessageBus {
|
|
|
23
30
|
this.handlers = new Map(messageHandlers.map((u) => [u.messageTag, u]));
|
|
24
31
|
this.tickIntervalMs = tickIntervalMs;
|
|
25
32
|
this.messageTimeoutMs = messageTimeoutMs;
|
|
33
|
+
this.constructorTimeoutOverrides = { ...messageTimeoutOverrides };
|
|
34
|
+
this.messageTimeoutOverrides = { ...this.constructorTimeoutOverrides };
|
|
26
35
|
this.debug = debug;
|
|
27
36
|
this.buildServicesFn = buildServices ?? this.buildServices.bind(this);
|
|
28
37
|
}
|
|
@@ -58,6 +67,11 @@ class MessageBus {
|
|
|
58
67
|
self.clearTimeout(this.tickTimeout);
|
|
59
68
|
this.tickTimeout = null;
|
|
60
69
|
}
|
|
70
|
+
for (const record of this.lateDeliveries) {
|
|
71
|
+
record.settled = true;
|
|
72
|
+
self.clearTimeout(record.deadline);
|
|
73
|
+
}
|
|
74
|
+
this.lateDeliveries.clear();
|
|
61
75
|
self.removeEventListener("message", this.boundOnMessage);
|
|
62
76
|
await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
|
|
63
77
|
}
|
|
@@ -84,7 +98,8 @@ class MessageBus {
|
|
|
84
98
|
const now = Date.now();
|
|
85
99
|
for (const updater of this.handlers.values()) {
|
|
86
100
|
try {
|
|
87
|
-
const
|
|
101
|
+
const tickLabel = `${updater.messageTag}:tick`;
|
|
102
|
+
const response = await this.withTimeout(updater.tick(now), this.resolveTimeoutMs(tickLabel, updater.messageTag), tickLabel);
|
|
88
103
|
if (this.debug)
|
|
89
104
|
console.log(`[${updater.messageTag}] outgoing tick response:`, response);
|
|
90
105
|
if (response && response.length > 0) {
|
|
@@ -127,6 +142,12 @@ class MessageBus {
|
|
|
127
142
|
this.initialized = false;
|
|
128
143
|
await Promise.all(Array.from(this.handlers.values()).map((h) => h.stop().catch(() => { })));
|
|
129
144
|
}
|
|
145
|
+
// Recompute the active timeout map from scratch so a prior init's
|
|
146
|
+
// keys cannot linger after re-init with a smaller map.
|
|
147
|
+
this.messageTimeoutOverrides = {
|
|
148
|
+
...this.constructorTimeoutOverrides,
|
|
149
|
+
...(config.messageTimeouts ?? {}),
|
|
150
|
+
};
|
|
130
151
|
const services = await this.buildServicesFn(config);
|
|
131
152
|
// Start all handlers
|
|
132
153
|
for (const updater of this.handlers.values()) {
|
|
@@ -149,8 +170,9 @@ class MessageBus {
|
|
|
149
170
|
const delegatorProvider = config.delegatorUrl
|
|
150
171
|
? new delegator_1.RestDelegatorProvider(config.delegatorUrl)
|
|
151
172
|
: undefined;
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
const serialized = (0, identity_1.normalizeSerializedIdentity)(config.wallet);
|
|
174
|
+
if ((0, identity_1.isSigningSerialized)(serialized)) {
|
|
175
|
+
const identity = (0, identity_1.hydrateIdentity)(serialized);
|
|
154
176
|
const wallet = await wallet_1.Wallet.create({
|
|
155
177
|
identity,
|
|
156
178
|
arkServerUrl: config.arkServer.url,
|
|
@@ -164,23 +186,18 @@ class MessageBus {
|
|
|
164
186
|
});
|
|
165
187
|
return { wallet, arkProvider, readonlyWallet: wallet };
|
|
166
188
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return { readonlyWallet, arkProvider };
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
throw new Error("Missing privateKey or publicKey in configuration object");
|
|
183
|
-
}
|
|
189
|
+
const identity = (0, identity_1.hydrateIdentity)(serialized);
|
|
190
|
+
const readonlyWallet = await wallet_1.ReadonlyWallet.create({
|
|
191
|
+
identity,
|
|
192
|
+
arkServerUrl: config.arkServer.url,
|
|
193
|
+
arkServerPublicKey: config.arkServer.publicKey,
|
|
194
|
+
indexerUrl: config.indexerUrl,
|
|
195
|
+
esploraUrl: config.esploraUrl,
|
|
196
|
+
storage,
|
|
197
|
+
delegatorProvider,
|
|
198
|
+
watcherConfig: config.watcherConfig,
|
|
199
|
+
});
|
|
200
|
+
return { readonlyWallet, arkProvider };
|
|
184
201
|
}
|
|
185
202
|
onMessage(event) {
|
|
186
203
|
// Keep the service worker alive while async work is pending.
|
|
@@ -195,7 +212,7 @@ class MessageBus {
|
|
|
195
212
|
async processMessage(event) {
|
|
196
213
|
const { id, tag, broadcast } = event.data;
|
|
197
214
|
if (tag === "PING") {
|
|
198
|
-
event.source
|
|
215
|
+
this.deliverResponse(event.source, { id, tag: "PONG" }, { id, tag: "PONG" });
|
|
199
216
|
return;
|
|
200
217
|
}
|
|
201
218
|
if (tag === "INITIALIZE_MESSAGE_BUS") {
|
|
@@ -206,7 +223,7 @@ class MessageBus {
|
|
|
206
223
|
// performs network calls (buildServices) and handler startup
|
|
207
224
|
// that may legitimately exceed the message timeout.
|
|
208
225
|
await this.waitForInit(event.data.config);
|
|
209
|
-
event.source
|
|
226
|
+
this.deliverResponse(event.source, { id, tag }, { id, tag });
|
|
210
227
|
if (this.debug) {
|
|
211
228
|
console.log("MessageBus initialized");
|
|
212
229
|
}
|
|
@@ -219,45 +236,60 @@ class MessageBus {
|
|
|
219
236
|
// hanging forever. This happens when the browser kills and restarts
|
|
220
237
|
// the service worker — the new instance has initialized=false and
|
|
221
238
|
// messages arrive before INITIALIZE_MESSAGE_BUS is re-sent.
|
|
222
|
-
|
|
239
|
+
const fallbackTag = tag ?? "unknown";
|
|
240
|
+
this.deliverResponse(event.source, {
|
|
223
241
|
id,
|
|
224
|
-
tag:
|
|
242
|
+
tag: fallbackTag,
|
|
225
243
|
error: new errors_1.MessageBusNotInitializedError(),
|
|
226
|
-
});
|
|
244
|
+
}, { id, tag: fallbackTag });
|
|
227
245
|
return;
|
|
228
246
|
}
|
|
229
247
|
if (!id || !tag) {
|
|
230
248
|
if (this.debug)
|
|
231
249
|
console.error("Invalid message received, missing required fields:", event.data);
|
|
232
|
-
|
|
250
|
+
const fallbackTag = tag ?? "unknown";
|
|
251
|
+
this.deliverResponse(event.source, {
|
|
233
252
|
id,
|
|
234
|
-
tag:
|
|
253
|
+
tag: fallbackTag,
|
|
235
254
|
error: new TypeError("Invalid message received, missing required fields"),
|
|
236
|
-
});
|
|
255
|
+
}, { id, tag: fallbackTag });
|
|
237
256
|
return;
|
|
238
257
|
}
|
|
258
|
+
const messageType = this.extractMessageType(event.data);
|
|
239
259
|
if (broadcast) {
|
|
240
260
|
const updaters = Array.from(this.handlers.values());
|
|
241
|
-
const
|
|
261
|
+
const entries = updaters.map((updater) => {
|
|
262
|
+
const label = this.labelFor(messageType, updater.messageTag);
|
|
263
|
+
const timeoutMs = this.resolveTimeoutMs(messageType, updater.messageTag);
|
|
264
|
+
const handlerPromise = updater.handleMessage(event.data);
|
|
265
|
+
const raced = updater.isLongRunning?.(event.data)
|
|
266
|
+
? handlerPromise
|
|
267
|
+
: this.withTimeout(handlerPromise, timeoutMs, label);
|
|
268
|
+
return { updater, handlerPromise, raced };
|
|
269
|
+
});
|
|
270
|
+
const results = await Promise.allSettled(entries.map((e) => e.raced));
|
|
242
271
|
results.forEach((result, index) => {
|
|
243
|
-
const updater =
|
|
272
|
+
const { updater, handlerPromise } = entries[index];
|
|
273
|
+
const handlerTag = updater.messageTag;
|
|
274
|
+
const context = { id, tag: handlerTag, messageType };
|
|
244
275
|
if (result.status === "fulfilled") {
|
|
245
276
|
const response = result.value;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
// Always deliver a response so the caller's message id
|
|
278
|
+
// never goes silent. Handlers returning null/undefined
|
|
279
|
+
// get an explicit ack envelope.
|
|
280
|
+
this.deliverResponse(event.source, response ?? { id, tag: handlerTag }, context);
|
|
249
281
|
}
|
|
250
282
|
else {
|
|
251
283
|
if (this.debug)
|
|
252
|
-
console.error(`[${
|
|
253
|
-
const error = result.reason
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
284
|
+
console.error(`[${handlerTag}] handleMessage failed`, result.reason);
|
|
285
|
+
const error = toError(result.reason);
|
|
286
|
+
this.deliverResponse(event.source, { id, tag: handlerTag, error }, context);
|
|
287
|
+
// If the error was a timeout, keep watching the
|
|
288
|
+
// underlying handler and surface its eventual result
|
|
289
|
+
// under the same id.
|
|
290
|
+
if (result.reason instanceof errors_1.ServiceWorkerTimeoutError) {
|
|
291
|
+
this.attachLateDelivery(handlerPromise, event.source, id, handlerTag, messageType);
|
|
292
|
+
}
|
|
261
293
|
}
|
|
262
294
|
});
|
|
263
295
|
return;
|
|
@@ -266,35 +298,53 @@ class MessageBus {
|
|
|
266
298
|
if (!updater) {
|
|
267
299
|
if (this.debug)
|
|
268
300
|
console.warn(`[${tag}] unknown message tag, ignoring message`);
|
|
301
|
+
this.deliverResponse(event.source, {
|
|
302
|
+
id,
|
|
303
|
+
tag,
|
|
304
|
+
error: new Error(`Unknown handler tag: ${tag}`),
|
|
305
|
+
}, { id, tag, messageType });
|
|
269
306
|
return;
|
|
270
307
|
}
|
|
308
|
+
const label = this.labelFor(messageType, tag);
|
|
309
|
+
const timeoutMs = this.resolveTimeoutMs(messageType, tag);
|
|
310
|
+
const handlerPromise = updater.handleMessage(event.data);
|
|
311
|
+
const context = { id, tag, messageType };
|
|
271
312
|
try {
|
|
272
|
-
const response =
|
|
313
|
+
const response = updater.isLongRunning?.(event.data)
|
|
314
|
+
? await handlerPromise
|
|
315
|
+
: await this.withTimeout(handlerPromise, timeoutMs, label);
|
|
273
316
|
if (this.debug)
|
|
274
317
|
console.log(`[${tag}] outgoing response:`, response);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
318
|
+
// Always deliver a response so the caller's message id never
|
|
319
|
+
// goes silent. A handler returning null/undefined yields an
|
|
320
|
+
// explicit ack envelope.
|
|
321
|
+
this.deliverResponse(event.source, response ?? { id, tag }, context);
|
|
278
322
|
}
|
|
279
323
|
catch (err) {
|
|
280
324
|
if (this.debug)
|
|
281
325
|
console.error(`[${tag}] handleMessage failed`, err);
|
|
282
|
-
const error =
|
|
283
|
-
event.source
|
|
326
|
+
const error = toError(err);
|
|
327
|
+
this.deliverResponse(event.source, { id, tag, error }, context);
|
|
328
|
+
// When we abandoned the handler via timeout, keep watching it
|
|
329
|
+
// so the client's message id eventually gets a final response.
|
|
330
|
+
if (err instanceof errors_1.ServiceWorkerTimeoutError) {
|
|
331
|
+
this.attachLateDelivery(handlerPromise, event.source, id, tag, messageType);
|
|
332
|
+
}
|
|
284
333
|
}
|
|
285
334
|
}
|
|
286
335
|
/**
|
|
287
336
|
* Race `promise` against a timeout. Note: this does NOT cancel the
|
|
288
|
-
* underlying work — the original promise keeps running.
|
|
289
|
-
*
|
|
337
|
+
* underlying work — the original promise keeps running. Call
|
|
338
|
+
* `attachLateDelivery` after catching the timeout to surface the
|
|
339
|
+
* eventual result so the message id does not go silent.
|
|
290
340
|
*/
|
|
291
|
-
withTimeout(promise, label) {
|
|
292
|
-
if (
|
|
341
|
+
withTimeout(promise, timeoutMs, label) {
|
|
342
|
+
if (timeoutMs <= 0)
|
|
293
343
|
return promise;
|
|
294
344
|
return new Promise((resolve, reject) => {
|
|
295
345
|
const timer = self.setTimeout(() => {
|
|
296
|
-
reject(new errors_1.ServiceWorkerTimeoutError(`Message handler timed out after ${
|
|
297
|
-
},
|
|
346
|
+
reject(new errors_1.ServiceWorkerTimeoutError(`Message handler timed out after ${timeoutMs}ms (${label})`));
|
|
347
|
+
}, timeoutMs);
|
|
298
348
|
promise.then((val) => {
|
|
299
349
|
self.clearTimeout(timer);
|
|
300
350
|
resolve(val);
|
|
@@ -304,6 +354,97 @@ class MessageBus {
|
|
|
304
354
|
});
|
|
305
355
|
});
|
|
306
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Extract the declared `type` from a request envelope (e.g. "SETTLE").
|
|
359
|
+
* Not every envelope carries a type (PING/INIT are special cased
|
|
360
|
+
* earlier), so this returns undefined for envelopes that lack one.
|
|
361
|
+
*/
|
|
362
|
+
extractMessageType(data) {
|
|
363
|
+
const maybeType = data.type;
|
|
364
|
+
return typeof maybeType === "string" ? maybeType : undefined;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Resolve the timeout for an operation. Message-type overrides take
|
|
368
|
+
* precedence over handler-tag overrides, with the bus-wide default
|
|
369
|
+
* (`messageTimeoutMs`) as the final fallback.
|
|
370
|
+
*/
|
|
371
|
+
resolveTimeoutMs(messageType, handlerTag) {
|
|
372
|
+
if (messageType &&
|
|
373
|
+
Object.prototype.hasOwnProperty.call(this.messageTimeoutOverrides, messageType)) {
|
|
374
|
+
return this.messageTimeoutOverrides[messageType];
|
|
375
|
+
}
|
|
376
|
+
if (Object.prototype.hasOwnProperty.call(this.messageTimeoutOverrides, handlerTag)) {
|
|
377
|
+
return this.messageTimeoutOverrides[handlerTag];
|
|
378
|
+
}
|
|
379
|
+
return this.messageTimeoutMs;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Build a human-readable label for timeout errors. Format:
|
|
383
|
+
* `"<MESSAGE_TYPE> via <HANDLER_TAG>"` when both are known, else the
|
|
384
|
+
* handler tag alone. Used so timeout errors name the operation the
|
|
385
|
+
* client actually triggered (e.g. SETTLE) rather than just the
|
|
386
|
+
* handler that received it (e.g. WALLET_UPDATER).
|
|
387
|
+
*/
|
|
388
|
+
labelFor(messageType, handlerTag) {
|
|
389
|
+
return messageType ? `${messageType} via ${handlerTag}` : handlerTag;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Post a response to the originating client. When `source` is null
|
|
393
|
+
* (client tab closed, detached frame, etc.) the response cannot be
|
|
394
|
+
* delivered; we log the drop in debug mode so it is not invisible.
|
|
395
|
+
*/
|
|
396
|
+
deliverResponse(source, response, context) {
|
|
397
|
+
if (!source) {
|
|
398
|
+
if (this.debug)
|
|
399
|
+
console.warn(`[${context.tag}] cannot deliver response: event.source is null`, {
|
|
400
|
+
id: context.id,
|
|
401
|
+
messageType: context.messageType,
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
source.postMessage(response);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* After a handler times out the client has already received a timeout
|
|
409
|
+
* error, but the handler keeps running. Attach a follow-up so the
|
|
410
|
+
* handler's eventual result (or error) is delivered under the same
|
|
411
|
+
* message id, or — if the handler never completes within
|
|
412
|
+
* {@link LATE_DELIVERY_GRACE_MS} — an "Operation abandoned" error is
|
|
413
|
+
* sent so the client's listener (if still attached) does not hang.
|
|
414
|
+
*/
|
|
415
|
+
attachLateDelivery(handlerPromise, source, id, tag, messageType) {
|
|
416
|
+
const context = { id, tag, messageType };
|
|
417
|
+
const record = {
|
|
418
|
+
settled: false,
|
|
419
|
+
deadline: self.setTimeout(() => {
|
|
420
|
+
if (record.settled)
|
|
421
|
+
return;
|
|
422
|
+
record.settled = true;
|
|
423
|
+
this.lateDeliveries.delete(record);
|
|
424
|
+
this.deliverResponse(source, {
|
|
425
|
+
id,
|
|
426
|
+
tag,
|
|
427
|
+
error: new Error(`Operation abandoned: handler did not complete within ${LATE_DELIVERY_GRACE_MS}ms after timeout (${this.labelFor(messageType, tag)})`),
|
|
428
|
+
}, context);
|
|
429
|
+
}, LATE_DELIVERY_GRACE_MS),
|
|
430
|
+
};
|
|
431
|
+
this.lateDeliveries.add(record);
|
|
432
|
+
handlerPromise.then((response) => {
|
|
433
|
+
if (record.settled)
|
|
434
|
+
return;
|
|
435
|
+
record.settled = true;
|
|
436
|
+
self.clearTimeout(record.deadline);
|
|
437
|
+
this.lateDeliveries.delete(record);
|
|
438
|
+
this.deliverResponse(source, response ?? { id, tag }, context);
|
|
439
|
+
}, (err) => {
|
|
440
|
+
if (record.settled)
|
|
441
|
+
return;
|
|
442
|
+
record.settled = true;
|
|
443
|
+
self.clearTimeout(record.deadline);
|
|
444
|
+
this.lateDeliveries.delete(record);
|
|
445
|
+
this.deliverResponse(source, { id, tag, error: toError(err) }, context);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
307
448
|
/**
|
|
308
449
|
* Returns the registered SW for the path.
|
|
309
450
|
* It uses the functions in `service-worker-manager.ts` module.
|
|
@@ -327,3 +468,6 @@ class MessageBus {
|
|
|
327
468
|
}
|
|
328
469
|
}
|
|
329
470
|
exports.MessageBus = MessageBus;
|
|
471
|
+
function toError(value) {
|
|
472
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
473
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isEventSourceError } from '../providers/utils.js';
|
|
1
2
|
/**
|
|
2
3
|
* Watches multiple contracts for virtual output state changes with resilient connection handling.
|
|
3
4
|
*
|
|
@@ -250,13 +251,18 @@ export class ContractWatcher {
|
|
|
250
251
|
}
|
|
251
252
|
/**
|
|
252
253
|
* Connect to the subscription.
|
|
254
|
+
*
|
|
255
|
+
* @param skipUpdate - Skip the leading `updateSubscription` call when
|
|
256
|
+
* the caller has already established `subscriptionId`.
|
|
253
257
|
*/
|
|
254
|
-
async connect() {
|
|
258
|
+
async connect(skipUpdate = false) {
|
|
255
259
|
if (!this.isWatching)
|
|
256
260
|
return;
|
|
257
261
|
this.connectionState = "connecting";
|
|
258
262
|
try {
|
|
259
|
-
|
|
263
|
+
if (!skipUpdate) {
|
|
264
|
+
await this.updateSubscription();
|
|
265
|
+
}
|
|
260
266
|
// Poll immediately after connection to sync state
|
|
261
267
|
await this.pollAllContracts();
|
|
262
268
|
this.connectionState = "connected";
|
|
@@ -267,7 +273,12 @@ export class ContractWatcher {
|
|
|
267
273
|
// indefinitely and block the caller.
|
|
268
274
|
// Error management must be implemented to ensure the connection
|
|
269
275
|
// is restored and events are fired.
|
|
270
|
-
|
|
276
|
+
if (isEventSourceError(e)) {
|
|
277
|
+
console.debug("ContractWatcher subscription disconnected; reconnecting");
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
console.error(e);
|
|
281
|
+
}
|
|
271
282
|
this.connectionState = "disconnected";
|
|
272
283
|
this.eventCallback?.({
|
|
273
284
|
type: "connection_reset",
|
|
@@ -382,11 +393,30 @@ export class ContractWatcher {
|
|
|
382
393
|
}
|
|
383
394
|
}
|
|
384
395
|
async tryUpdateSubscription() {
|
|
396
|
+
const hadSubscription = this.subscriptionId !== undefined;
|
|
385
397
|
try {
|
|
386
398
|
await this.updateSubscription();
|
|
387
399
|
}
|
|
388
400
|
catch (error) {
|
|
389
401
|
// nothing, the connection will be retried later
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Cold start: `startWatching` may have run with zero scripts,
|
|
405
|
+
// leaving `listenLoop` parked behind the reconnect timer. Kick
|
|
406
|
+
// `connect` now so streaming resumes without waiting on the
|
|
407
|
+
// backoff. `skipUpdate` avoids re-issuing `subscribeForScripts`.
|
|
408
|
+
const justGotSubscription = !hadSubscription && this.subscriptionId !== undefined;
|
|
409
|
+
const listenerParked = this.connectionState === "disconnected" ||
|
|
410
|
+
this.connectionState === "reconnecting";
|
|
411
|
+
if (this.isWatching && justGotSubscription && listenerParked) {
|
|
412
|
+
if (this.reconnectTimeoutId) {
|
|
413
|
+
clearTimeout(this.reconnectTimeoutId);
|
|
414
|
+
this.reconnectTimeoutId = undefined;
|
|
415
|
+
}
|
|
416
|
+
this.reconnectAttempts = 0;
|
|
417
|
+
this.connect(true).catch((error) => {
|
|
418
|
+
console.warn("ContractWatcher cold-start connect failed:", error);
|
|
419
|
+
});
|
|
390
420
|
}
|
|
391
421
|
}
|
|
392
422
|
/**
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { DefaultVtxo } from '../../script/default.js';
|
|
3
3
|
import { isCsvSpendable, sequenceToTimelock, timelockToSequence, } from './helpers.js';
|
|
4
|
+
import { normalizeToDescriptor, extractPubKey, } from '../../identity/descriptor.js';
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
+
* Extract pubkey bytes from a descriptor or hex string.
|
|
7
|
+
*/
|
|
8
|
+
function extractPubKeyBytes(value) {
|
|
9
|
+
return hex.decode(extractPubKey(normalizeToDescriptor(value)));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Handler for default wallet VTXOs.
|
|
6
13
|
*
|
|
7
14
|
* Default contracts use the standard forfeit + exit tapscript:
|
|
8
15
|
* - forfeit: (Alice + Server) multisig for collaborative spending
|
|
@@ -26,8 +33,8 @@ export const DefaultContractHandler = {
|
|
|
26
33
|
? sequenceToTimelock(Number(params.csvTimelock))
|
|
27
34
|
: DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
28
35
|
return {
|
|
29
|
-
pubKey:
|
|
30
|
-
serverPubKey:
|
|
36
|
+
pubKey: extractPubKeyBytes(params.pubKey),
|
|
37
|
+
serverPubKey: extractPubKeyBytes(params.serverPubKey),
|
|
31
38
|
csvTimelock,
|
|
32
39
|
};
|
|
33
40
|
},
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import * as bip68 from "bip68";
|
|
2
|
+
import { isDescriptor, extractPubKey } from '../../identity/descriptor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extract raw hex pubkey from a value that may be a descriptor or raw hex.
|
|
5
|
+
* Returns undefined for HD descriptors or unparseable values so role
|
|
6
|
+
* resolution stays best-effort and never throws.
|
|
7
|
+
*/
|
|
8
|
+
function extractRawPubKey(value) {
|
|
9
|
+
if (!isDescriptor(value)) {
|
|
10
|
+
return value.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return extractPubKey(value).toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
2
19
|
/**
|
|
3
20
|
* Convert RelativeTimelock to BIP68 sequence number.
|
|
4
21
|
*/
|
|
@@ -21,21 +38,46 @@ export function sequenceToTimelock(sequence) {
|
|
|
21
38
|
throw new Error(`Invalid BIP68 sequence: ${sequence}`);
|
|
22
39
|
}
|
|
23
40
|
/**
|
|
24
|
-
* Resolve wallet's role from explicit role or by matching pubkey.
|
|
41
|
+
* Resolve wallet's role from explicit role or by matching descriptor/pubkey.
|
|
25
42
|
*/
|
|
26
43
|
export function resolveRole(contract, context) {
|
|
27
44
|
// Explicit role takes precedence
|
|
28
45
|
if (context.role === "sender" || context.role === "receiver") {
|
|
29
46
|
return context.role;
|
|
30
47
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
const senderKey = contract.params.sender
|
|
49
|
+
? extractRawPubKey(contract.params.sender)
|
|
50
|
+
: undefined;
|
|
51
|
+
const receiverKey = contract.params.receiver
|
|
52
|
+
? extractRawPubKey(contract.params.receiver)
|
|
53
|
+
: undefined;
|
|
54
|
+
const matchRole = (rawWalletKey) => {
|
|
55
|
+
if (!rawWalletKey)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (senderKey && rawWalletKey === senderKey) {
|
|
34
58
|
return "sender";
|
|
35
59
|
}
|
|
36
|
-
if (
|
|
60
|
+
if (receiverKey && rawWalletKey === receiverKey) {
|
|
37
61
|
return "receiver";
|
|
38
62
|
}
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
// Try the preferred descriptor first. If it cannot be resolved
|
|
66
|
+
// (for example an HD descriptor without derivation support), fall back
|
|
67
|
+
// to walletPubKey for backward compatibility.
|
|
68
|
+
if (context.walletDescriptor) {
|
|
69
|
+
const walletDescriptorKey = extractRawPubKey(context.walletDescriptor);
|
|
70
|
+
const matchedRole = matchRole(walletDescriptorKey);
|
|
71
|
+
if (matchedRole) {
|
|
72
|
+
return matchedRole;
|
|
73
|
+
}
|
|
74
|
+
if (!walletDescriptorKey && context.walletPubKey) {
|
|
75
|
+
return matchRole(extractRawPubKey(context.walletPubKey));
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (context.walletPubKey) {
|
|
80
|
+
return matchRole(extractRawPubKey(context.walletPubKey));
|
|
39
81
|
}
|
|
40
82
|
return undefined;
|
|
41
83
|
}
|
|
@@ -49,7 +49,8 @@ export const VHTLCContractHandler = {
|
|
|
49
49
|
/**
|
|
50
50
|
* Select spending path based on context.
|
|
51
51
|
*
|
|
52
|
-
* Role is determined from `context.role` or by matching
|
|
52
|
+
* Role is determined from `context.role` or by matching
|
|
53
|
+
* `context.walletDescriptor` (preferred) / `context.walletPubKey`
|
|
53
54
|
* against sender/receiver in contract params.
|
|
54
55
|
*/
|
|
55
56
|
selectPath(script, contract, context) {
|
|
@@ -98,7 +99,8 @@ export const VHTLCContractHandler = {
|
|
|
98
99
|
/**
|
|
99
100
|
* Get all possible spending paths (no timelock checks).
|
|
100
101
|
*
|
|
101
|
-
* Role is determined from `context.role` or by matching
|
|
102
|
+
* Role is determined from `context.role` or by matching
|
|
103
|
+
* `context.walletDescriptor` (preferred) / `context.walletPubKey`
|
|
102
104
|
* against sender/receiver in contract params.
|
|
103
105
|
*/
|
|
104
106
|
getAllSpendingPaths(script, contract, context) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { expand, networks, } from "@bitcoinerlab/descriptors-scure";
|
|
2
|
+
import { hex } from "@scure/base";
|
|
3
|
+
function inferNetwork(descriptor) {
|
|
4
|
+
return descriptor.includes("tpub") ? networks.testnet : networks.bitcoin;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Check if a string is a descriptor of the shape `tr(...)`.
|
|
8
|
+
*
|
|
9
|
+
* This is a shape check only — it does not validate the inner key material.
|
|
10
|
+
* Use {@link expand} (via {@link extractPubKey} / {@link parseHDDescriptor})
|
|
11
|
+
* for full parsing. The guard rejects empty bodies and missing/trailing
|
|
12
|
+
* parentheses so callers can safely branch on descriptor vs. raw pubkey.
|
|
13
|
+
*/
|
|
14
|
+
export function isDescriptor(value) {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return false;
|
|
17
|
+
if (!value.startsWith("tr(") || !value.endsWith(")"))
|
|
18
|
+
return false;
|
|
19
|
+
// body length > 0 after stripping "tr(" and ")"
|
|
20
|
+
return value.length > "tr()".length;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a value to descriptor format.
|
|
24
|
+
* If already a descriptor, return as-is. If hex pubkey, wrap as tr(pubkey).
|
|
25
|
+
* Throws when the value is empty or not a string so we never produce
|
|
26
|
+
* malformed descriptors like `tr()` that downstream parsers would reject.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeToDescriptor(value) {
|
|
29
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
30
|
+
throw new Error("normalizeToDescriptor: expected a non-empty string value");
|
|
31
|
+
}
|
|
32
|
+
if (isDescriptor(value)) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return `tr(${value})`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract the public key from a simple descriptor.
|
|
39
|
+
* For simple descriptors (tr(pubkey)), extracts the pubkey using the library.
|
|
40
|
+
* For HD descriptors, throws — use DescriptorProvider to derive the key.
|
|
41
|
+
*/
|
|
42
|
+
export function extractPubKey(descriptor) {
|
|
43
|
+
if (!isDescriptor(descriptor)) {
|
|
44
|
+
return descriptor;
|
|
45
|
+
}
|
|
46
|
+
const network = inferNetwork(descriptor);
|
|
47
|
+
const expansion = expand({ descriptor, network });
|
|
48
|
+
if (!expansion.expansionMap) {
|
|
49
|
+
throw new Error("Cannot extract pubkey from descriptor: expansion failed.");
|
|
50
|
+
}
|
|
51
|
+
const key = expansion.expansionMap["@0"];
|
|
52
|
+
// HD descriptors (have a bip32 key) require DescriptorProvider for derivation
|
|
53
|
+
if (key?.bip32) {
|
|
54
|
+
throw new Error("Cannot extract pubkey from HD descriptor without derivation. " +
|
|
55
|
+
"Use DescriptorProvider to derive the key from the xpub.");
|
|
56
|
+
}
|
|
57
|
+
if (!key?.pubkey) {
|
|
58
|
+
throw new Error("Cannot extract pubkey from descriptor: no key found.");
|
|
59
|
+
}
|
|
60
|
+
return hex.encode(key.pubkey);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse an HD descriptor into its components.
|
|
64
|
+
* HD descriptors have the format: tr([fingerprint/path']xpub/derivation)
|
|
65
|
+
* Returns null if the descriptor is not in HD format.
|
|
66
|
+
*/
|
|
67
|
+
export function parseHDDescriptor(descriptor) {
|
|
68
|
+
if (!isDescriptor(descriptor)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let expansion;
|
|
72
|
+
try {
|
|
73
|
+
const network = inferNetwork(descriptor);
|
|
74
|
+
expansion = expand({ descriptor, network });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const key = expansion.expansionMap?.["@0"];
|
|
80
|
+
if (!key?.masterFingerprint ||
|
|
81
|
+
!key.originPath ||
|
|
82
|
+
!key.keyPath ||
|
|
83
|
+
!key.bip32) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
fingerprint: hex.encode(key.masterFingerprint),
|
|
88
|
+
basePath: key.originPath.replace(/^\//, ""),
|
|
89
|
+
xpub: key.bip32.toBase58(),
|
|
90
|
+
derivationPath: key.keyPath.replace(/^\//, ""),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|