@arkade-os/sdk 0.4.19 → 0.4.20
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 +7 -1
- 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 +11 -3
- package/dist/cjs/providers/electrum.js +663 -0
- package/dist/cjs/providers/indexer.js +5 -1
- package/dist/cjs/providers/utils.js +4 -0
- 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 +3 -3
- package/dist/cjs/worker/messageBus.js +200 -56
- package/dist/esm/contracts/contractWatcher.js +7 -1
- 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 +12 -4
- package/dist/esm/providers/electrum.js +658 -0
- package/dist/esm/providers/indexer.js +6 -2
- package/dist/esm/providers/utils.js +3 -0
- 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 +3 -3
- package/dist/esm/worker/messageBus.js +201 -57
- 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 +1 -0
- 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/worker/messageBus.d.ts +68 -8
- package/package.json +3 -2
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
import { getActiveServiceWorker, setupServiceWorkerOnce, } from './browser/service-worker-manager.js';
|
|
3
3
|
import { RestArkProvider } from '../providers/ark.js';
|
|
4
4
|
import { RestDelegatorProvider } from '../providers/delegator.js';
|
|
5
|
-
import {
|
|
5
|
+
import { hydrateIdentity, isSigningSerialized, normalizeSerializedIdentity, } from '../identity/index.js';
|
|
6
6
|
import { ReadonlyWallet, Wallet } from '../wallet/wallet.js';
|
|
7
|
-
import { hex } from "@scure/base";
|
|
8
7
|
import { MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Grace period after a handler times out during which late handler
|
|
10
|
+
* completion is still delivered to the client. Once this expires,
|
|
11
|
+
* the bus sends an "Operation abandoned" error so the message id
|
|
12
|
+
* never goes silent indefinitely.
|
|
13
|
+
*/
|
|
14
|
+
const LATE_DELIVERY_GRACE_MS = 5 * 60000;
|
|
9
15
|
export class MessageBus {
|
|
10
16
|
/** Create the service-worker message bus with repositories and handler configuration. */
|
|
11
|
-
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, debug = false, buildServices, }) {
|
|
17
|
+
constructor(walletRepository, contractRepository, { messageHandlers, tickIntervalMs = 10000, messageTimeoutMs = 30000, messageTimeoutOverrides = {}, debug = false, buildServices, }) {
|
|
12
18
|
this.walletRepository = walletRepository;
|
|
13
19
|
this.contractRepository = contractRepository;
|
|
20
|
+
this.lateDeliveries = new Set();
|
|
14
21
|
this.running = false;
|
|
15
22
|
this.tickTimeout = null;
|
|
16
23
|
this.tickInProgress = false;
|
|
@@ -20,6 +27,8 @@ export class MessageBus {
|
|
|
20
27
|
this.handlers = new Map(messageHandlers.map((u) => [u.messageTag, u]));
|
|
21
28
|
this.tickIntervalMs = tickIntervalMs;
|
|
22
29
|
this.messageTimeoutMs = messageTimeoutMs;
|
|
30
|
+
this.constructorTimeoutOverrides = { ...messageTimeoutOverrides };
|
|
31
|
+
this.messageTimeoutOverrides = { ...this.constructorTimeoutOverrides };
|
|
23
32
|
this.debug = debug;
|
|
24
33
|
this.buildServicesFn = buildServices ?? this.buildServices.bind(this);
|
|
25
34
|
}
|
|
@@ -55,6 +64,11 @@ export class MessageBus {
|
|
|
55
64
|
self.clearTimeout(this.tickTimeout);
|
|
56
65
|
this.tickTimeout = null;
|
|
57
66
|
}
|
|
67
|
+
for (const record of this.lateDeliveries) {
|
|
68
|
+
record.settled = true;
|
|
69
|
+
self.clearTimeout(record.deadline);
|
|
70
|
+
}
|
|
71
|
+
this.lateDeliveries.clear();
|
|
58
72
|
self.removeEventListener("message", this.boundOnMessage);
|
|
59
73
|
await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
|
|
60
74
|
}
|
|
@@ -81,7 +95,8 @@ export class MessageBus {
|
|
|
81
95
|
const now = Date.now();
|
|
82
96
|
for (const updater of this.handlers.values()) {
|
|
83
97
|
try {
|
|
84
|
-
const
|
|
98
|
+
const tickLabel = `${updater.messageTag}:tick`;
|
|
99
|
+
const response = await this.withTimeout(updater.tick(now), this.resolveTimeoutMs(tickLabel, updater.messageTag), tickLabel);
|
|
85
100
|
if (this.debug)
|
|
86
101
|
console.log(`[${updater.messageTag}] outgoing tick response:`, response);
|
|
87
102
|
if (response && response.length > 0) {
|
|
@@ -124,6 +139,12 @@ export class MessageBus {
|
|
|
124
139
|
this.initialized = false;
|
|
125
140
|
await Promise.all(Array.from(this.handlers.values()).map((h) => h.stop().catch(() => { })));
|
|
126
141
|
}
|
|
142
|
+
// Recompute the active timeout map from scratch so a prior init's
|
|
143
|
+
// keys cannot linger after re-init with a smaller map.
|
|
144
|
+
this.messageTimeoutOverrides = {
|
|
145
|
+
...this.constructorTimeoutOverrides,
|
|
146
|
+
...(config.messageTimeouts ?? {}),
|
|
147
|
+
};
|
|
127
148
|
const services = await this.buildServicesFn(config);
|
|
128
149
|
// Start all handlers
|
|
129
150
|
for (const updater of this.handlers.values()) {
|
|
@@ -146,8 +167,9 @@ export class MessageBus {
|
|
|
146
167
|
const delegatorProvider = config.delegatorUrl
|
|
147
168
|
? new RestDelegatorProvider(config.delegatorUrl)
|
|
148
169
|
: undefined;
|
|
149
|
-
|
|
150
|
-
|
|
170
|
+
const serialized = normalizeSerializedIdentity(config.wallet);
|
|
171
|
+
if (isSigningSerialized(serialized)) {
|
|
172
|
+
const identity = hydrateIdentity(serialized);
|
|
151
173
|
const wallet = await Wallet.create({
|
|
152
174
|
identity,
|
|
153
175
|
arkServerUrl: config.arkServer.url,
|
|
@@ -161,23 +183,18 @@ export class MessageBus {
|
|
|
161
183
|
});
|
|
162
184
|
return { wallet, arkProvider, readonlyWallet: wallet };
|
|
163
185
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return { readonlyWallet, arkProvider };
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
throw new Error("Missing privateKey or publicKey in configuration object");
|
|
180
|
-
}
|
|
186
|
+
const identity = hydrateIdentity(serialized);
|
|
187
|
+
const readonlyWallet = await ReadonlyWallet.create({
|
|
188
|
+
identity,
|
|
189
|
+
arkServerUrl: config.arkServer.url,
|
|
190
|
+
arkServerPublicKey: config.arkServer.publicKey,
|
|
191
|
+
indexerUrl: config.indexerUrl,
|
|
192
|
+
esploraUrl: config.esploraUrl,
|
|
193
|
+
storage,
|
|
194
|
+
delegatorProvider,
|
|
195
|
+
watcherConfig: config.watcherConfig,
|
|
196
|
+
});
|
|
197
|
+
return { readonlyWallet, arkProvider };
|
|
181
198
|
}
|
|
182
199
|
onMessage(event) {
|
|
183
200
|
// Keep the service worker alive while async work is pending.
|
|
@@ -192,7 +209,7 @@ export class MessageBus {
|
|
|
192
209
|
async processMessage(event) {
|
|
193
210
|
const { id, tag, broadcast } = event.data;
|
|
194
211
|
if (tag === "PING") {
|
|
195
|
-
event.source
|
|
212
|
+
this.deliverResponse(event.source, { id, tag: "PONG" }, { id, tag: "PONG" });
|
|
196
213
|
return;
|
|
197
214
|
}
|
|
198
215
|
if (tag === "INITIALIZE_MESSAGE_BUS") {
|
|
@@ -203,7 +220,7 @@ export class MessageBus {
|
|
|
203
220
|
// performs network calls (buildServices) and handler startup
|
|
204
221
|
// that may legitimately exceed the message timeout.
|
|
205
222
|
await this.waitForInit(event.data.config);
|
|
206
|
-
event.source
|
|
223
|
+
this.deliverResponse(event.source, { id, tag }, { id, tag });
|
|
207
224
|
if (this.debug) {
|
|
208
225
|
console.log("MessageBus initialized");
|
|
209
226
|
}
|
|
@@ -216,45 +233,60 @@ export class MessageBus {
|
|
|
216
233
|
// hanging forever. This happens when the browser kills and restarts
|
|
217
234
|
// the service worker — the new instance has initialized=false and
|
|
218
235
|
// messages arrive before INITIALIZE_MESSAGE_BUS is re-sent.
|
|
219
|
-
|
|
236
|
+
const fallbackTag = tag ?? "unknown";
|
|
237
|
+
this.deliverResponse(event.source, {
|
|
220
238
|
id,
|
|
221
|
-
tag:
|
|
239
|
+
tag: fallbackTag,
|
|
222
240
|
error: new MessageBusNotInitializedError(),
|
|
223
|
-
});
|
|
241
|
+
}, { id, tag: fallbackTag });
|
|
224
242
|
return;
|
|
225
243
|
}
|
|
226
244
|
if (!id || !tag) {
|
|
227
245
|
if (this.debug)
|
|
228
246
|
console.error("Invalid message received, missing required fields:", event.data);
|
|
229
|
-
|
|
247
|
+
const fallbackTag = tag ?? "unknown";
|
|
248
|
+
this.deliverResponse(event.source, {
|
|
230
249
|
id,
|
|
231
|
-
tag:
|
|
250
|
+
tag: fallbackTag,
|
|
232
251
|
error: new TypeError("Invalid message received, missing required fields"),
|
|
233
|
-
});
|
|
252
|
+
}, { id, tag: fallbackTag });
|
|
234
253
|
return;
|
|
235
254
|
}
|
|
255
|
+
const messageType = this.extractMessageType(event.data);
|
|
236
256
|
if (broadcast) {
|
|
237
257
|
const updaters = Array.from(this.handlers.values());
|
|
238
|
-
const
|
|
258
|
+
const entries = updaters.map((updater) => {
|
|
259
|
+
const label = this.labelFor(messageType, updater.messageTag);
|
|
260
|
+
const timeoutMs = this.resolveTimeoutMs(messageType, updater.messageTag);
|
|
261
|
+
const handlerPromise = updater.handleMessage(event.data);
|
|
262
|
+
const raced = updater.isLongRunning?.(event.data)
|
|
263
|
+
? handlerPromise
|
|
264
|
+
: this.withTimeout(handlerPromise, timeoutMs, label);
|
|
265
|
+
return { updater, handlerPromise, raced };
|
|
266
|
+
});
|
|
267
|
+
const results = await Promise.allSettled(entries.map((e) => e.raced));
|
|
239
268
|
results.forEach((result, index) => {
|
|
240
|
-
const updater =
|
|
269
|
+
const { updater, handlerPromise } = entries[index];
|
|
270
|
+
const handlerTag = updater.messageTag;
|
|
271
|
+
const context = { id, tag: handlerTag, messageType };
|
|
241
272
|
if (result.status === "fulfilled") {
|
|
242
273
|
const response = result.value;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
// Always deliver a response so the caller's message id
|
|
275
|
+
// never goes silent. Handlers returning null/undefined
|
|
276
|
+
// get an explicit ack envelope.
|
|
277
|
+
this.deliverResponse(event.source, response ?? { id, tag: handlerTag }, context);
|
|
246
278
|
}
|
|
247
279
|
else {
|
|
248
280
|
if (this.debug)
|
|
249
|
-
console.error(`[${
|
|
250
|
-
const error = result.reason
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
281
|
+
console.error(`[${handlerTag}] handleMessage failed`, result.reason);
|
|
282
|
+
const error = toError(result.reason);
|
|
283
|
+
this.deliverResponse(event.source, { id, tag: handlerTag, error }, context);
|
|
284
|
+
// If the error was a timeout, keep watching the
|
|
285
|
+
// underlying handler and surface its eventual result
|
|
286
|
+
// under the same id.
|
|
287
|
+
if (result.reason instanceof ServiceWorkerTimeoutError) {
|
|
288
|
+
this.attachLateDelivery(handlerPromise, event.source, id, handlerTag, messageType);
|
|
289
|
+
}
|
|
258
290
|
}
|
|
259
291
|
});
|
|
260
292
|
return;
|
|
@@ -263,35 +295,53 @@ export class MessageBus {
|
|
|
263
295
|
if (!updater) {
|
|
264
296
|
if (this.debug)
|
|
265
297
|
console.warn(`[${tag}] unknown message tag, ignoring message`);
|
|
298
|
+
this.deliverResponse(event.source, {
|
|
299
|
+
id,
|
|
300
|
+
tag,
|
|
301
|
+
error: new Error(`Unknown handler tag: ${tag}`),
|
|
302
|
+
}, { id, tag, messageType });
|
|
266
303
|
return;
|
|
267
304
|
}
|
|
305
|
+
const label = this.labelFor(messageType, tag);
|
|
306
|
+
const timeoutMs = this.resolveTimeoutMs(messageType, tag);
|
|
307
|
+
const handlerPromise = updater.handleMessage(event.data);
|
|
308
|
+
const context = { id, tag, messageType };
|
|
268
309
|
try {
|
|
269
|
-
const response =
|
|
310
|
+
const response = updater.isLongRunning?.(event.data)
|
|
311
|
+
? await handlerPromise
|
|
312
|
+
: await this.withTimeout(handlerPromise, timeoutMs, label);
|
|
270
313
|
if (this.debug)
|
|
271
314
|
console.log(`[${tag}] outgoing response:`, response);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
// Always deliver a response so the caller's message id never
|
|
316
|
+
// goes silent. A handler returning null/undefined yields an
|
|
317
|
+
// explicit ack envelope.
|
|
318
|
+
this.deliverResponse(event.source, response ?? { id, tag }, context);
|
|
275
319
|
}
|
|
276
320
|
catch (err) {
|
|
277
321
|
if (this.debug)
|
|
278
322
|
console.error(`[${tag}] handleMessage failed`, err);
|
|
279
|
-
const error =
|
|
280
|
-
event.source
|
|
323
|
+
const error = toError(err);
|
|
324
|
+
this.deliverResponse(event.source, { id, tag, error }, context);
|
|
325
|
+
// When we abandoned the handler via timeout, keep watching it
|
|
326
|
+
// so the client's message id eventually gets a final response.
|
|
327
|
+
if (err instanceof ServiceWorkerTimeoutError) {
|
|
328
|
+
this.attachLateDelivery(handlerPromise, event.source, id, tag, messageType);
|
|
329
|
+
}
|
|
281
330
|
}
|
|
282
331
|
}
|
|
283
332
|
/**
|
|
284
333
|
* Race `promise` against a timeout. Note: this does NOT cancel the
|
|
285
|
-
* underlying work — the original promise keeps running.
|
|
286
|
-
*
|
|
334
|
+
* underlying work — the original promise keeps running. Call
|
|
335
|
+
* `attachLateDelivery` after catching the timeout to surface the
|
|
336
|
+
* eventual result so the message id does not go silent.
|
|
287
337
|
*/
|
|
288
|
-
withTimeout(promise, label) {
|
|
289
|
-
if (
|
|
338
|
+
withTimeout(promise, timeoutMs, label) {
|
|
339
|
+
if (timeoutMs <= 0)
|
|
290
340
|
return promise;
|
|
291
341
|
return new Promise((resolve, reject) => {
|
|
292
342
|
const timer = self.setTimeout(() => {
|
|
293
|
-
reject(new ServiceWorkerTimeoutError(`Message handler timed out after ${
|
|
294
|
-
},
|
|
343
|
+
reject(new ServiceWorkerTimeoutError(`Message handler timed out after ${timeoutMs}ms (${label})`));
|
|
344
|
+
}, timeoutMs);
|
|
295
345
|
promise.then((val) => {
|
|
296
346
|
self.clearTimeout(timer);
|
|
297
347
|
resolve(val);
|
|
@@ -301,6 +351,97 @@ export class MessageBus {
|
|
|
301
351
|
});
|
|
302
352
|
});
|
|
303
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Extract the declared `type` from a request envelope (e.g. "SETTLE").
|
|
356
|
+
* Not every envelope carries a type (PING/INIT are special cased
|
|
357
|
+
* earlier), so this returns undefined for envelopes that lack one.
|
|
358
|
+
*/
|
|
359
|
+
extractMessageType(data) {
|
|
360
|
+
const maybeType = data.type;
|
|
361
|
+
return typeof maybeType === "string" ? maybeType : undefined;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resolve the timeout for an operation. Message-type overrides take
|
|
365
|
+
* precedence over handler-tag overrides, with the bus-wide default
|
|
366
|
+
* (`messageTimeoutMs`) as the final fallback.
|
|
367
|
+
*/
|
|
368
|
+
resolveTimeoutMs(messageType, handlerTag) {
|
|
369
|
+
if (messageType &&
|
|
370
|
+
Object.prototype.hasOwnProperty.call(this.messageTimeoutOverrides, messageType)) {
|
|
371
|
+
return this.messageTimeoutOverrides[messageType];
|
|
372
|
+
}
|
|
373
|
+
if (Object.prototype.hasOwnProperty.call(this.messageTimeoutOverrides, handlerTag)) {
|
|
374
|
+
return this.messageTimeoutOverrides[handlerTag];
|
|
375
|
+
}
|
|
376
|
+
return this.messageTimeoutMs;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Build a human-readable label for timeout errors. Format:
|
|
380
|
+
* `"<MESSAGE_TYPE> via <HANDLER_TAG>"` when both are known, else the
|
|
381
|
+
* handler tag alone. Used so timeout errors name the operation the
|
|
382
|
+
* client actually triggered (e.g. SETTLE) rather than just the
|
|
383
|
+
* handler that received it (e.g. WALLET_UPDATER).
|
|
384
|
+
*/
|
|
385
|
+
labelFor(messageType, handlerTag) {
|
|
386
|
+
return messageType ? `${messageType} via ${handlerTag}` : handlerTag;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Post a response to the originating client. When `source` is null
|
|
390
|
+
* (client tab closed, detached frame, etc.) the response cannot be
|
|
391
|
+
* delivered; we log the drop in debug mode so it is not invisible.
|
|
392
|
+
*/
|
|
393
|
+
deliverResponse(source, response, context) {
|
|
394
|
+
if (!source) {
|
|
395
|
+
if (this.debug)
|
|
396
|
+
console.warn(`[${context.tag}] cannot deliver response: event.source is null`, {
|
|
397
|
+
id: context.id,
|
|
398
|
+
messageType: context.messageType,
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
source.postMessage(response);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* After a handler times out the client has already received a timeout
|
|
406
|
+
* error, but the handler keeps running. Attach a follow-up so the
|
|
407
|
+
* handler's eventual result (or error) is delivered under the same
|
|
408
|
+
* message id, or — if the handler never completes within
|
|
409
|
+
* {@link LATE_DELIVERY_GRACE_MS} — an "Operation abandoned" error is
|
|
410
|
+
* sent so the client's listener (if still attached) does not hang.
|
|
411
|
+
*/
|
|
412
|
+
attachLateDelivery(handlerPromise, source, id, tag, messageType) {
|
|
413
|
+
const context = { id, tag, messageType };
|
|
414
|
+
const record = {
|
|
415
|
+
settled: false,
|
|
416
|
+
deadline: self.setTimeout(() => {
|
|
417
|
+
if (record.settled)
|
|
418
|
+
return;
|
|
419
|
+
record.settled = true;
|
|
420
|
+
this.lateDeliveries.delete(record);
|
|
421
|
+
this.deliverResponse(source, {
|
|
422
|
+
id,
|
|
423
|
+
tag,
|
|
424
|
+
error: new Error(`Operation abandoned: handler did not complete within ${LATE_DELIVERY_GRACE_MS}ms after timeout (${this.labelFor(messageType, tag)})`),
|
|
425
|
+
}, context);
|
|
426
|
+
}, LATE_DELIVERY_GRACE_MS),
|
|
427
|
+
};
|
|
428
|
+
this.lateDeliveries.add(record);
|
|
429
|
+
handlerPromise.then((response) => {
|
|
430
|
+
if (record.settled)
|
|
431
|
+
return;
|
|
432
|
+
record.settled = true;
|
|
433
|
+
self.clearTimeout(record.deadline);
|
|
434
|
+
this.lateDeliveries.delete(record);
|
|
435
|
+
this.deliverResponse(source, response ?? { id, tag }, context);
|
|
436
|
+
}, (err) => {
|
|
437
|
+
if (record.settled)
|
|
438
|
+
return;
|
|
439
|
+
record.settled = true;
|
|
440
|
+
self.clearTimeout(record.deadline);
|
|
441
|
+
this.lateDeliveries.delete(record);
|
|
442
|
+
this.deliverResponse(source, { id, tag, error: toError(err) }, context);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
304
445
|
/**
|
|
305
446
|
* Returns the registered SW for the path.
|
|
306
447
|
* It uses the functions in `service-worker-manager.ts` module.
|
|
@@ -323,3 +464,6 @@ export class MessageBus {
|
|
|
323
464
|
return getActiveServiceWorker(path);
|
|
324
465
|
}
|
|
325
466
|
}
|
|
467
|
+
function toError(value) {
|
|
468
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
469
|
+
}
|
|
@@ -10,7 +10,7 @@ export interface DefaultContractParams {
|
|
|
10
10
|
csvTimelock: RelativeTimelock;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Handler for default wallet
|
|
13
|
+
* Handler for default wallet VTXOs.
|
|
14
14
|
*
|
|
15
15
|
* Default contracts use the standard forfeit + exit tapscript:
|
|
16
16
|
* - forfeit: (Alice + Server) multisig for collaborative spending
|
|
@@ -9,7 +9,7 @@ export declare function timelockToSequence(timelock: RelativeTimelock): number;
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function sequenceToTimelock(sequence: number): RelativeTimelock;
|
|
11
11
|
/**
|
|
12
|
-
* Resolve wallet's role from explicit role or by matching pubkey.
|
|
12
|
+
* Resolve wallet's role from explicit role or by matching descriptor/pubkey.
|
|
13
13
|
*/
|
|
14
14
|
export declare function resolveRole(contract: Contract, context: PathContext): "sender" | "receiver" | undefined;
|
|
15
15
|
/**
|
|
@@ -96,13 +96,21 @@ export interface PathContext {
|
|
|
96
96
|
/** Current block height, when known. */
|
|
97
97
|
blockHeight?: number;
|
|
98
98
|
/**
|
|
99
|
-
* Wallet
|
|
100
|
-
*
|
|
99
|
+
* Wallet's descriptor for signing.
|
|
100
|
+
* Format: tr(pubkey) for static keys, tr([fingerprint/path']xpub/0/{index}) for HD.
|
|
101
|
+
* Used by handlers to determine wallet's role in multi-party contracts.
|
|
102
|
+
*/
|
|
103
|
+
walletDescriptor?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Wallet's public key (x-only, 32 bytes hex).
|
|
106
|
+
* @deprecated Use walletDescriptor instead.
|
|
101
107
|
*/
|
|
102
108
|
walletPubKey?: string;
|
|
103
109
|
/**
|
|
104
110
|
* Explicit role override for multi-party contracts such as VHTLC.
|
|
105
|
-
* If not provided, the handler may derive the role
|
|
111
|
+
* If not provided, the handler may derive the role by matching
|
|
112
|
+
* {@link walletDescriptor} (preferred) — or {@link walletPubKey} as a
|
|
113
|
+
* fallback — against the contract's sender/receiver params.
|
|
106
114
|
*/
|
|
107
115
|
role?: string;
|
|
108
116
|
/** The specific virtual output being evaluated. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a string is a descriptor of the shape `tr(...)`.
|
|
3
|
+
*
|
|
4
|
+
* This is a shape check only — it does not validate the inner key material.
|
|
5
|
+
* Use {@link expand} (via {@link extractPubKey} / {@link parseHDDescriptor})
|
|
6
|
+
* for full parsing. The guard rejects empty bodies and missing/trailing
|
|
7
|
+
* parentheses so callers can safely branch on descriptor vs. raw pubkey.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDescriptor(value: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a value to descriptor format.
|
|
12
|
+
* If already a descriptor, return as-is. If hex pubkey, wrap as tr(pubkey).
|
|
13
|
+
* Throws when the value is empty or not a string so we never produce
|
|
14
|
+
* malformed descriptors like `tr()` that downstream parsers would reject.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeToDescriptor(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Extract the public key from a simple descriptor.
|
|
19
|
+
* For simple descriptors (tr(pubkey)), extracts the pubkey using the library.
|
|
20
|
+
* For HD descriptors, throws — use DescriptorProvider to derive the key.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractPubKey(descriptor: string): string;
|
|
23
|
+
/** Parsed HD descriptor components. */
|
|
24
|
+
export interface ParsedHDDescriptor {
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
basePath: string;
|
|
27
|
+
xpub: string;
|
|
28
|
+
derivationPath: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse an HD descriptor into its components.
|
|
32
|
+
* HD descriptors have the format: tr([fingerprint/path']xpub/derivation)
|
|
33
|
+
* Returns null if the descriptor is not in HD format.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseHDDescriptor(descriptor: string): ParsedHDDescriptor | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Transaction } from "../utils/transaction";
|
|
2
|
+
/** A signing request that pairs a descriptor with a transaction. */
|
|
3
|
+
export interface DescriptorSigningRequest {
|
|
4
|
+
/** Descriptor identifying which key to sign with */
|
|
5
|
+
descriptor: string;
|
|
6
|
+
/** Transaction to sign */
|
|
7
|
+
tx: Transaction;
|
|
8
|
+
/** Specific input indexes to sign (signs all if omitted) */
|
|
9
|
+
inputIndexes?: number[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Provider interface for descriptor-based signing.
|
|
13
|
+
*
|
|
14
|
+
* Implementations include:
|
|
15
|
+
* - {@link StaticDescriptorProvider}: wraps a legacy {@link Identity} with a single key.
|
|
16
|
+
* - HD-wallet provider: signs with keys derived from an xpub-based descriptor
|
|
17
|
+
* (planned — tracked separately from this interface).
|
|
18
|
+
*/
|
|
19
|
+
export interface DescriptorProvider {
|
|
20
|
+
/** Returns the current signing descriptor. */
|
|
21
|
+
getSigningDescriptor(): string;
|
|
22
|
+
/** Checks if a descriptor belongs to this provider. */
|
|
23
|
+
isOurs(descriptor: string): boolean;
|
|
24
|
+
/** Signs transactions, each with its own descriptor-derived key. */
|
|
25
|
+
signWithDescriptor(requests: DescriptorSigningRequest[]): Promise<Transaction[]>;
|
|
26
|
+
/** Signs a message using the key derived from the descriptor. */
|
|
27
|
+
signMessageWithDescriptor(descriptor: string, message: Uint8Array, type?: "schnorr" | "ecdsa"): Promise<Uint8Array>;
|
|
28
|
+
}
|
|
@@ -46,4 +46,10 @@ export interface BatchSignableIdentity extends Identity {
|
|
|
46
46
|
/** Type guard for identities that support batch signing. */
|
|
47
47
|
export declare function isBatchSignable(identity: Identity): identity is BatchSignableIdentity;
|
|
48
48
|
export * from "./singleKey";
|
|
49
|
-
export
|
|
49
|
+
export type { NetworkOptions, DescriptorOptions, SeedIdentityOptions, MnemonicOptions, } from "./seedIdentity";
|
|
50
|
+
export { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, } from "./seedIdentity";
|
|
51
|
+
export * from "./serialize";
|
|
52
|
+
export { isDescriptor, normalizeToDescriptor, extractPubKey, parseHDDescriptor, } from "./descriptor";
|
|
53
|
+
export type { ParsedHDDescriptor } from "./descriptor";
|
|
54
|
+
export type { DescriptorProvider, DescriptorSigningRequest, } from "./descriptorProvider";
|
|
55
|
+
export { StaticDescriptorProvider } from "./staticDescriptorProvider";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Identity, ReadonlyIdentity } from ".";
|
|
2
2
|
import { Transaction } from "../utils/transaction";
|
|
3
3
|
import { SignerSession } from "../tree/signingSession";
|
|
4
|
+
import type { SerializedSigningIdentity, SerializedReadonlyIdentity } from "./serialize";
|
|
4
5
|
/** Used for default BIP86 derivation with network selection. */
|
|
5
6
|
export interface NetworkOptions {
|
|
6
7
|
/**
|
|
@@ -27,14 +28,14 @@ export type MnemonicOptions = SeedIdentityOptions & {
|
|
|
27
28
|
*
|
|
28
29
|
* This is the recommended identity type for most applications. It uses
|
|
29
30
|
* standard BIP86 (Taproot) derivation by default and stores an output
|
|
30
|
-
* descriptor for interoperability with other wallets.
|
|
31
|
-
* format is HD-ready, allowing future support for multiple addresses
|
|
32
|
-
* and change derivation.
|
|
31
|
+
* descriptor for interoperability with other wallets.
|
|
33
32
|
*
|
|
34
33
|
* Prefer this (or @see MnemonicIdentity) over `SingleKey` for new
|
|
35
34
|
* integrations — `SingleKey` exists for backward compatibility with
|
|
36
35
|
* raw nsec-style keys.
|
|
37
36
|
*
|
|
37
|
+
* For descriptor-based signing, wrap with {@link StaticDescriptorProvider}.
|
|
38
|
+
*
|
|
38
39
|
* @example
|
|
39
40
|
* ```typescript
|
|
40
41
|
* const seed = mnemonicToSeedSync(mnemonic);
|
|
@@ -50,7 +51,6 @@ export type MnemonicOptions = SeedIdentityOptions & {
|
|
|
50
51
|
* ```
|
|
51
52
|
*/
|
|
52
53
|
export declare class SeedIdentity implements Identity {
|
|
53
|
-
protected readonly seed: Uint8Array;
|
|
54
54
|
private readonly derivedKey;
|
|
55
55
|
readonly descriptor: string;
|
|
56
56
|
constructor(seed: Uint8Array, descriptor: string);
|
|
@@ -131,3 +131,40 @@ export declare class ReadonlyDescriptorIdentity implements ReadonlyIdentity {
|
|
|
131
131
|
xOnlyPublicKey(): Promise<Uint8Array>;
|
|
132
132
|
compressedPublicKey(): Promise<Uint8Array>;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Serialize a seed-backed signing identity into a
|
|
136
|
+
* {@link SerializedSigningIdentity} envelope without exposing the
|
|
137
|
+
* underlying secret material on the public instance surface.
|
|
138
|
+
*
|
|
139
|
+
* Called by {@link serializeSigningIdentity}; application code should
|
|
140
|
+
* prefer that public dispatcher instead of calling this directly. This
|
|
141
|
+
* helper is deliberately kept out of the `src/identity` barrel so it is
|
|
142
|
+
* not part of the package's public export surface.
|
|
143
|
+
*
|
|
144
|
+
* Secret-surface trade-off: the resulting envelope carries master-seed
|
|
145
|
+
* material — the BIP39 mnemonic (+ optional passphrase) for
|
|
146
|
+
* `MnemonicIdentity` or the raw 64-byte seed for `SeedIdentity`. A party
|
|
147
|
+
* that reads this envelope can derive any key under the HD tree, not
|
|
148
|
+
* just the key currently in use. The pre-change `SingleKey` flow only
|
|
149
|
+
* shipped one derived private key and therefore had a smaller blast
|
|
150
|
+
* radius. This is an intentional design trade to preserve class and
|
|
151
|
+
* descriptor identity across the page / service-worker boundary; the
|
|
152
|
+
* page already holds the same material so that it can re-initialize a
|
|
153
|
+
* killed worker. Transport is same-origin `postMessage` only. See the
|
|
154
|
+
* threat-model note in `src/worker/browser/README.md`.
|
|
155
|
+
*
|
|
156
|
+
* @internal
|
|
157
|
+
*/
|
|
158
|
+
export declare function serializeSeedOwnedSigningIdentity(identity: SeedIdentity): SerializedSigningIdentity;
|
|
159
|
+
/**
|
|
160
|
+
* Downgrade a seed-backed or descriptor-backed identity into a readonly
|
|
161
|
+
* descriptor envelope. Always produces a descriptor-only shape — secret
|
|
162
|
+
* material never crosses this path, even if the input is a signing
|
|
163
|
+
* identity.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately kept out of the `src/identity` barrel; consumers should go
|
|
166
|
+
* through {@link serializeReadonlyIdentity}.
|
|
167
|
+
*
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
export declare function serializeSeedOwnedReadonlyIdentity(identity: SeedIdentity | ReadonlyDescriptorIdentity): SerializedReadonlyIdentity;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Identity, ReadonlyIdentity } from ".";
|
|
2
|
+
/**
|
|
3
|
+
* Tagged envelope for a signing identity transported across the
|
|
4
|
+
* service-worker boundary. All variants are structured-clone safe
|
|
5
|
+
* (plain strings only — no functions or prototypes).
|
|
6
|
+
*
|
|
7
|
+
* Adding a new variant is a source change in every worker build; keep
|
|
8
|
+
* old variants around until all deployed workers handle them.
|
|
9
|
+
*/
|
|
10
|
+
export type SerializedSigningIdentity = {
|
|
11
|
+
type: "single-key";
|
|
12
|
+
privateKey: string;
|
|
13
|
+
} | {
|
|
14
|
+
type: "seed";
|
|
15
|
+
seed: string;
|
|
16
|
+
descriptor: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "mnemonic";
|
|
19
|
+
mnemonic: string;
|
|
20
|
+
descriptor: string;
|
|
21
|
+
passphrase?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Tagged envelope for a readonly identity transported across the
|
|
25
|
+
* service-worker boundary. All variants are structured-clone safe.
|
|
26
|
+
*/
|
|
27
|
+
export type SerializedReadonlyIdentity = {
|
|
28
|
+
type: "readonly-single-key";
|
|
29
|
+
publicKey: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: "readonly-descriptor";
|
|
32
|
+
descriptor: string;
|
|
33
|
+
};
|
|
34
|
+
export type SerializedIdentity = SerializedSigningIdentity | SerializedReadonlyIdentity;
|
|
35
|
+
/** Type guard — true for signing envelopes, false for readonly envelopes. */
|
|
36
|
+
export declare function isSigningSerialized(s: SerializedIdentity): s is SerializedSigningIdentity;
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a signing identity into a structured-clone safe envelope for
|
|
39
|
+
* transport across the service-worker boundary.
|
|
40
|
+
*
|
|
41
|
+
* Supports SDK-owned signing identities directly. For custom identities, a
|
|
42
|
+
* duck-typed `toHex()` fallback preserves compatibility with existing
|
|
43
|
+
* `SingleKey`-like implementations.
|
|
44
|
+
*/
|
|
45
|
+
export declare function serializeSigningIdentity(identity: Identity): SerializedSigningIdentity;
|
|
46
|
+
/**
|
|
47
|
+
* Serialize a readonly identity into a structured-clone safe envelope.
|
|
48
|
+
*
|
|
49
|
+
* Works for any `ReadonlyIdentity` via `compressedPublicKey()`. When called
|
|
50
|
+
* with a signing identity, produces a readonly envelope (never ships signing
|
|
51
|
+
* material) — callers that need to preserve signing capability across the
|
|
52
|
+
* boundary must use {@link serializeSigningIdentity}.
|
|
53
|
+
*/
|
|
54
|
+
export declare function serializeReadonlyIdentity(identity: ReadonlyIdentity): Promise<SerializedReadonlyIdentity>;
|
|
55
|
+
/**
|
|
56
|
+
* Rehydrate a serialized identity envelope back into an identity instance.
|
|
57
|
+
* The return type is the union of signing and readonly; use
|
|
58
|
+
* {@link isSigningSerialized} on the envelope before hydration if the caller
|
|
59
|
+
* needs to know which side it ends up on.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hydrateIdentity(s: SerializedIdentity): Identity | ReadonlyIdentity;
|
|
62
|
+
/**
|
|
63
|
+
* Legacy untagged shape emitted by page builds prior to the tagged
|
|
64
|
+
* SerializedIdentity envelope. Retained so newer workers can still accept
|
|
65
|
+
* older pages during a rolling upgrade. Slated for removal in the next major.
|
|
66
|
+
*
|
|
67
|
+
* @deprecated Use {@link SerializedIdentity}.
|
|
68
|
+
*/
|
|
69
|
+
export type LegacySerializedIdentity = {
|
|
70
|
+
privateKey: string;
|
|
71
|
+
} | {
|
|
72
|
+
publicKey: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Accept either a modern {@link SerializedIdentity} envelope or a legacy
|
|
76
|
+
* `{ privateKey }` / `{ publicKey }` shape and normalize to a
|
|
77
|
+
* {@link SerializedIdentity}. Emits a one-time deprecation warning when a
|
|
78
|
+
* legacy shape is seen.
|
|
79
|
+
*
|
|
80
|
+
* Intended for the worker-side boundary; new page builds always emit tagged
|
|
81
|
+
* envelopes via {@link serializeSigningIdentity} /
|
|
82
|
+
* {@link serializeReadonlyIdentity}.
|
|
83
|
+
*/
|
|
84
|
+
export declare function normalizeSerializedIdentity(shape: SerializedIdentity | LegacySerializedIdentity): SerializedIdentity;
|