@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.
Files changed (59) hide show
  1. package/dist/cjs/contracts/contractWatcher.js +7 -1
  2. package/dist/cjs/contracts/handlers/default.js +10 -3
  3. package/dist/cjs/contracts/handlers/helpers.js +47 -5
  4. package/dist/cjs/contracts/handlers/vhtlc.js +4 -2
  5. package/dist/cjs/identity/descriptor.js +98 -0
  6. package/dist/cjs/identity/descriptorProvider.js +2 -0
  7. package/dist/cjs/identity/index.js +15 -1
  8. package/dist/cjs/identity/seedIdentity.js +91 -6
  9. package/dist/cjs/identity/serialize.js +166 -0
  10. package/dist/cjs/identity/staticDescriptorProvider.js +65 -0
  11. package/dist/cjs/index.js +6 -3
  12. package/dist/cjs/providers/ark.js +11 -3
  13. package/dist/cjs/providers/electrum.js +663 -0
  14. package/dist/cjs/providers/indexer.js +5 -1
  15. package/dist/cjs/providers/utils.js +4 -0
  16. package/dist/cjs/wallet/ramps.js +1 -1
  17. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +10 -0
  18. package/dist/cjs/wallet/serviceWorker/wallet.js +137 -91
  19. package/dist/cjs/wallet/vtxo-manager.js +56 -8
  20. package/dist/cjs/wallet/wallet.js +3 -3
  21. package/dist/cjs/worker/messageBus.js +200 -56
  22. package/dist/esm/contracts/contractWatcher.js +7 -1
  23. package/dist/esm/contracts/handlers/default.js +10 -3
  24. package/dist/esm/contracts/handlers/helpers.js +47 -5
  25. package/dist/esm/contracts/handlers/vhtlc.js +4 -2
  26. package/dist/esm/identity/descriptor.js +92 -0
  27. package/dist/esm/identity/descriptorProvider.js +1 -0
  28. package/dist/esm/identity/index.js +6 -1
  29. package/dist/esm/identity/seedIdentity.js +89 -6
  30. package/dist/esm/identity/serialize.js +159 -0
  31. package/dist/esm/identity/staticDescriptorProvider.js +61 -0
  32. package/dist/esm/index.js +2 -1
  33. package/dist/esm/providers/ark.js +12 -4
  34. package/dist/esm/providers/electrum.js +658 -0
  35. package/dist/esm/providers/indexer.js +6 -2
  36. package/dist/esm/providers/utils.js +3 -0
  37. package/dist/esm/wallet/ramps.js +1 -1
  38. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +10 -0
  39. package/dist/esm/wallet/serviceWorker/wallet.js +137 -91
  40. package/dist/esm/wallet/vtxo-manager.js +56 -8
  41. package/dist/esm/wallet/wallet.js +3 -3
  42. package/dist/esm/worker/messageBus.js +201 -57
  43. package/dist/types/contracts/handlers/default.d.ts +1 -1
  44. package/dist/types/contracts/handlers/helpers.d.ts +1 -1
  45. package/dist/types/contracts/types.d.ts +11 -3
  46. package/dist/types/identity/descriptor.d.ts +35 -0
  47. package/dist/types/identity/descriptorProvider.d.ts +28 -0
  48. package/dist/types/identity/index.d.ts +7 -1
  49. package/dist/types/identity/seedIdentity.d.ts +41 -4
  50. package/dist/types/identity/serialize.d.ts +84 -0
  51. package/dist/types/identity/staticDescriptorProvider.d.ts +18 -0
  52. package/dist/types/index.d.ts +4 -2
  53. package/dist/types/providers/electrum.d.ts +212 -0
  54. package/dist/types/providers/utils.d.ts +1 -0
  55. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +11 -2
  56. package/dist/types/wallet/serviceWorker/wallet.d.ts +27 -10
  57. package/dist/types/wallet/vtxo-manager.d.ts +2 -0
  58. package/dist/types/worker/messageBus.d.ts +68 -8
  59. 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 { ReadonlySingleKey, SingleKey } from '../identity/index.js';
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 response = await this.withTimeout(updater.tick(now), `${updater.messageTag}:tick`);
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
- if ("privateKey" in config.wallet) {
150
- const identity = SingleKey.fromHex(config.wallet.privateKey);
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
- else if ("publicKey" in config.wallet) {
165
- const identity = ReadonlySingleKey.fromPublicKey(hex.decode(config.wallet.publicKey));
166
- const readonlyWallet = await ReadonlyWallet.create({
167
- identity,
168
- arkServerUrl: config.arkServer.url,
169
- arkServerPublicKey: config.arkServer.publicKey,
170
- indexerUrl: config.indexerUrl,
171
- esploraUrl: config.esploraUrl,
172
- storage,
173
- delegatorProvider,
174
- watcherConfig: config.watcherConfig,
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?.postMessage({ id, tag: "PONG" });
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?.postMessage({ id, tag });
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
- event.source?.postMessage({
236
+ const fallbackTag = tag ?? "unknown";
237
+ this.deliverResponse(event.source, {
220
238
  id,
221
- tag: tag ?? "unknown",
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
- event.source?.postMessage({
247
+ const fallbackTag = tag ?? "unknown";
248
+ this.deliverResponse(event.source, {
230
249
  id,
231
- tag: tag ?? "unknown",
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 results = await Promise.allSettled(updaters.map((updater) => this.withTimeout(updater.handleMessage(event.data), updater.messageTag)));
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 = updaters[index];
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
- if (response) {
244
- event.source?.postMessage(response);
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(`[${updater.messageTag}] handleMessage failed`, result.reason);
250
- const error = result.reason instanceof Error
251
- ? result.reason
252
- : new Error(String(result.reason));
253
- event.source?.postMessage({
254
- id,
255
- tag: updater.messageTag,
256
- error,
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 = await this.withTimeout(updater.handleMessage(event.data), tag);
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
- if (response) {
273
- event.source?.postMessage(response);
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 = err instanceof Error ? err : new Error(String(err));
280
- event.source?.postMessage({ id, tag, error });
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. This is safe
286
- * here because only the caller (not the handler) posts the response.
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 (this.messageTimeoutMs <= 0)
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 ${this.messageTimeoutMs}ms (${label})`));
294
- }, this.messageTimeoutMs);
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 virtual outputs.
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 public key encoded as 32-byte x-only hex.
100
- * Used by handlers to determine the wallet's role in multi-party contracts.
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 from `walletPubKey`.
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 * from "./seedIdentity";
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. The descriptor
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;