@arkade-os/sdk 0.4.7 → 0.4.9

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 (39) hide show
  1. package/dist/cjs/contracts/contractManager.js +59 -11
  2. package/dist/cjs/contracts/contractWatcher.js +21 -2
  3. package/dist/cjs/identity/seedIdentity.js +2 -2
  4. package/dist/cjs/index.js +9 -2
  5. package/dist/cjs/providers/expoIndexer.js +1 -0
  6. package/dist/cjs/providers/indexer.js +1 -0
  7. package/dist/cjs/utils/transactionHistory.js +2 -1
  8. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +249 -36
  9. package/dist/cjs/wallet/serviceWorker/wallet.js +286 -34
  10. package/dist/cjs/wallet/vtxo-manager.js +123 -86
  11. package/dist/cjs/wallet/wallet.js +140 -68
  12. package/dist/cjs/worker/errors.js +17 -0
  13. package/dist/cjs/worker/messageBus.js +14 -2
  14. package/dist/esm/contracts/contractManager.js +59 -11
  15. package/dist/esm/contracts/contractWatcher.js +21 -2
  16. package/dist/esm/identity/seedIdentity.js +2 -2
  17. package/dist/esm/index.js +3 -2
  18. package/dist/esm/providers/expoIndexer.js +1 -0
  19. package/dist/esm/providers/indexer.js +1 -0
  20. package/dist/esm/utils/transactionHistory.js +2 -1
  21. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +245 -35
  22. package/dist/esm/wallet/serviceWorker/wallet.js +286 -34
  23. package/dist/esm/wallet/vtxo-manager.js +123 -86
  24. package/dist/esm/wallet/wallet.js +140 -68
  25. package/dist/esm/worker/errors.js +12 -0
  26. package/dist/esm/worker/messageBus.js +14 -2
  27. package/dist/types/contracts/contractManager.d.ts +10 -0
  28. package/dist/types/identity/seedIdentity.d.ts +5 -2
  29. package/dist/types/index.d.ts +5 -4
  30. package/dist/types/repositories/serialization.d.ts +1 -0
  31. package/dist/types/utils/transactionHistory.d.ts +1 -1
  32. package/dist/types/wallet/index.d.ts +2 -0
  33. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +101 -7
  34. package/dist/types/wallet/serviceWorker/wallet.d.ts +16 -0
  35. package/dist/types/wallet/vtxo-manager.d.ts +29 -2
  36. package/dist/types/wallet/wallet.d.ts +10 -0
  37. package/dist/types/worker/errors.d.ts +6 -0
  38. package/dist/types/worker/messageBus.d.ts +6 -0
  39. package/package.json +1 -1
@@ -3,6 +3,37 @@ import { setupServiceWorker } from '../../worker/browser/utils.js';
3
3
  import { IndexedDBContractRepository, IndexedDBWalletRepository, } from '../../repositories/index.js';
4
4
  import { DEFAULT_MESSAGE_TAG, } from './wallet-message-handler.js';
5
5
  import { getRandomId } from '../utils.js';
6
+ import { ServiceWorkerTimeoutError } from '../../worker/errors.js';
7
+ // Check by error name instead of instanceof because postMessage uses the
8
+ // structured clone algorithm which strips the prototype chain — the page
9
+ // receives a plain Error, not the original MessageBusNotInitializedError.
10
+ function isMessageBusNotInitializedError(error) {
11
+ return (error instanceof Error && error.name === "MessageBusNotInitializedError");
12
+ }
13
+ const DEDUPABLE_REQUEST_TYPES = new Set([
14
+ "GET_ADDRESS",
15
+ "GET_BALANCE",
16
+ "GET_BOARDING_ADDRESS",
17
+ "GET_BOARDING_UTXOS",
18
+ "GET_STATUS",
19
+ "GET_TRANSACTION_HISTORY",
20
+ "IS_CONTRACT_MANAGER_WATCHING",
21
+ "GET_DELEGATE_INFO",
22
+ "GET_RECOVERABLE_BALANCE",
23
+ "GET_EXPIRED_BOARDING_UTXOS",
24
+ "GET_VTXOS",
25
+ "GET_CONTRACTS",
26
+ "GET_CONTRACTS_WITH_VTXOS",
27
+ "GET_SPENDABLE_PATHS",
28
+ "GET_ALL_SPENDING_PATHS",
29
+ "GET_ASSET_DETAILS",
30
+ "GET_EXPIRING_VTXOS",
31
+ "RELOAD_WALLET",
32
+ ]);
33
+ function getRequestDedupKey(request) {
34
+ const { id, tag, ...rest } = request;
35
+ return JSON.stringify(rest);
36
+ }
6
37
  const isPrivateKeyIdentity = (identity) => {
7
38
  return typeof identity.toHex === "function";
8
39
  };
@@ -79,7 +110,7 @@ const initializeMessageBus = (serviceWorker, config, timeoutMs = 2000) => {
79
110
  };
80
111
  const timeoutId = setTimeout(() => {
81
112
  cleanup();
82
- reject(new Error("MessageBus timed out!"));
113
+ reject(new ServiceWorkerTimeoutError("MessageBus timed out"));
83
114
  }, timeoutMs);
84
115
  navigator.serviceWorker.addEventListener("message", onMessage);
85
116
  serviceWorker.postMessage(initCmd);
@@ -95,6 +126,8 @@ export class ServiceWorkerReadonlyWallet {
95
126
  this.initConfig = null;
96
127
  this.initWalletPayload = null;
97
128
  this.reinitPromise = null;
129
+ this.pingPromise = null;
130
+ this.inflightRequests = new Map();
98
131
  this.identity = identity;
99
132
  this.walletRepository = walletRepository;
100
133
  this.contractRepository = contractRepository;
@@ -125,7 +158,10 @@ export class ServiceWorkerReadonlyWallet {
125
158
  publicKey: initConfig.arkServerPublicKey,
126
159
  },
127
160
  delegatorUrl: initConfig.delegatorUrl,
161
+ indexerUrl: options.indexerUrl,
162
+ esploraUrl: options.esploraUrl,
128
163
  timeoutMs: options.messageBusTimeoutMs,
164
+ watcherConfig: options.watcherConfig,
129
165
  }, options.messageBusTimeoutMs);
130
166
  // Initialize the wallet handler
131
167
  const initMessage = {
@@ -142,6 +178,9 @@ export class ServiceWorkerReadonlyWallet {
142
178
  publicKey: initConfig.arkServerPublicKey,
143
179
  },
144
180
  delegatorUrl: initConfig.delegatorUrl,
181
+ indexerUrl: options.indexerUrl,
182
+ esploraUrl: options.esploraUrl,
183
+ watcherConfig: options.watcherConfig,
145
184
  };
146
185
  wallet.initWalletPayload = initConfig;
147
186
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -188,7 +227,7 @@ export class ServiceWorkerReadonlyWallet {
188
227
  };
189
228
  const timeoutId = setTimeout(() => {
190
229
  cleanup();
191
- reject(new Error(`Service worker message timed out (${request.type})`));
230
+ reject(new ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
192
231
  }, 30000);
193
232
  const messageHandler = (event) => {
194
233
  const response = event.data;
@@ -207,18 +246,137 @@ export class ServiceWorkerReadonlyWallet {
207
246
  this.serviceWorker.postMessage(request);
208
247
  });
209
248
  }
249
+ // Like sendMessageDirect but supports streaming responses: intermediate
250
+ // messages are forwarded via onEvent while the promise resolves on the
251
+ // first response for which isComplete returns true. The timeout resets
252
+ // on every intermediate event so long-running but progressing operations
253
+ // don't time out prematurely.
254
+ sendMessageStreaming(request, onEvent, isComplete) {
255
+ return new Promise((resolve, reject) => {
256
+ const resetTimeout = () => {
257
+ clearTimeout(timeoutId);
258
+ timeoutId = setTimeout(() => {
259
+ cleanup();
260
+ reject(new ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
261
+ }, 30000);
262
+ };
263
+ const cleanup = () => {
264
+ clearTimeout(timeoutId);
265
+ navigator.serviceWorker.removeEventListener("message", messageHandler);
266
+ };
267
+ let timeoutId;
268
+ resetTimeout();
269
+ const messageHandler = (event) => {
270
+ const response = event.data;
271
+ if (request.id !== response.id)
272
+ return;
273
+ if (response.error) {
274
+ cleanup();
275
+ reject(response.error);
276
+ return;
277
+ }
278
+ if (isComplete(response)) {
279
+ cleanup();
280
+ resolve(response);
281
+ }
282
+ else {
283
+ resetTimeout();
284
+ onEvent(response);
285
+ }
286
+ };
287
+ navigator.serviceWorker.addEventListener("message", messageHandler);
288
+ this.serviceWorker.postMessage(request);
289
+ });
290
+ }
291
+ async sendMessage(request) {
292
+ if (!DEDUPABLE_REQUEST_TYPES.has(request.type)) {
293
+ return this.sendMessageWithRetry(request);
294
+ }
295
+ const key = getRequestDedupKey(request);
296
+ const existing = this.inflightRequests.get(key);
297
+ if (existing)
298
+ return existing;
299
+ const promise = this.sendMessageWithRetry(request).finally(() => {
300
+ this.inflightRequests.delete(key);
301
+ });
302
+ this.inflightRequests.set(key, promise);
303
+ return promise;
304
+ }
305
+ pingServiceWorker() {
306
+ if (this.pingPromise)
307
+ return this.pingPromise;
308
+ this.pingPromise = new Promise((resolve, reject) => {
309
+ const pingId = getRandomId();
310
+ const cleanup = () => {
311
+ clearTimeout(timeoutId);
312
+ navigator.serviceWorker.removeEventListener("message", onMessage);
313
+ };
314
+ const timeoutId = setTimeout(() => {
315
+ cleanup();
316
+ reject(new ServiceWorkerTimeoutError("Service worker ping timed out"));
317
+ }, 2000);
318
+ const onMessage = (event) => {
319
+ if (event.data?.id === pingId && event.data?.tag === "PONG") {
320
+ cleanup();
321
+ resolve();
322
+ }
323
+ };
324
+ navigator.serviceWorker.addEventListener("message", onMessage);
325
+ this.serviceWorker.postMessage({
326
+ id: pingId,
327
+ tag: "PING",
328
+ });
329
+ }).finally(() => {
330
+ this.pingPromise = null;
331
+ });
332
+ return this.pingPromise;
333
+ }
210
334
  // send a message, retrying up to 2 times if the service worker was
211
335
  // killed and restarted by the OS (mobile browsers do this aggressively)
212
- async sendMessage(request) {
336
+ async sendMessageWithRetry(request) {
337
+ // Skip the preflight ping during the initial INIT_WALLET call:
338
+ // create() hasn't set initConfig yet, so reinitialize() would throw.
339
+ if (this.initConfig) {
340
+ try {
341
+ await this.pingServiceWorker();
342
+ }
343
+ catch {
344
+ await this.reinitialize();
345
+ }
346
+ }
213
347
  const maxRetries = 2;
214
348
  for (let attempt = 0;; attempt++) {
215
349
  try {
216
350
  return await this.sendMessageDirect(request);
217
351
  }
218
352
  catch (error) {
219
- const isNotInitialized = typeof error?.message === "string" &&
220
- error.message.includes("MessageBus not initialized");
221
- if (!isNotInitialized || attempt >= maxRetries) {
353
+ if (!isMessageBusNotInitializedError(error) ||
354
+ attempt >= maxRetries) {
355
+ throw error;
356
+ }
357
+ await this.reinitialize();
358
+ }
359
+ }
360
+ }
361
+ // Like sendMessage but for streaming responses — retries with
362
+ // reinitialize when the service worker has been killed/restarted.
363
+ async sendMessageWithEvents(request, onEvent, isComplete) {
364
+ if (this.initConfig) {
365
+ try {
366
+ await this.pingServiceWorker();
367
+ }
368
+ catch {
369
+ await this.reinitialize();
370
+ }
371
+ }
372
+ const maxRetries = 2;
373
+ for (let attempt = 0;; attempt++) {
374
+ try {
375
+ return await this.sendMessageStreaming(request, onEvent, isComplete);
376
+ }
377
+ catch (error) {
378
+ if (!isMessageBusNotInitializedError(error) ||
379
+ attempt >= maxRetries) {
222
380
  throw error;
223
381
  }
224
382
  await this.reinitialize();
@@ -523,6 +681,14 @@ export class ServiceWorkerReadonlyWallet {
523
681
  navigator.serviceWorker.removeEventListener("message", messageHandler);
524
682
  };
525
683
  },
684
+ async refreshVtxos() {
685
+ const message = {
686
+ type: "REFRESH_VTXOS",
687
+ id: getRandomId(),
688
+ tag: messageTag,
689
+ };
690
+ await sendContractMessage(message);
691
+ },
526
692
  async isWatching() {
527
693
  const message = {
528
694
  type: "IS_CONTRACT_MANAGER_WATCHING",
@@ -592,7 +758,11 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
592
758
  publicKey: initConfig.arkServerPublicKey,
593
759
  },
594
760
  delegatorUrl: initConfig.delegatorUrl,
761
+ indexerUrl: options.indexerUrl,
762
+ esploraUrl: options.esploraUrl,
595
763
  timeoutMs: options.messageBusTimeoutMs,
764
+ settlementConfig: options.settlementConfig,
765
+ watcherConfig: options.watcherConfig,
596
766
  }, options.messageBusTimeoutMs);
597
767
  // Initialize the service worker with the config
598
768
  const initMessage = {
@@ -610,6 +780,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
610
780
  publicKey: initConfig.arkServerPublicKey,
611
781
  },
612
782
  delegatorUrl: initConfig.delegatorUrl,
783
+ indexerUrl: options.indexerUrl,
784
+ esploraUrl: options.esploraUrl,
785
+ settlementConfig: options.settlementConfig,
786
+ watcherConfig: options.watcherConfig,
613
787
  };
614
788
  wallet.initWalletPayload = initConfig;
615
789
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -671,34 +845,8 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
671
845
  payload: { params },
672
846
  };
673
847
  try {
674
- return new Promise((resolve, reject) => {
675
- const messageHandler = (event) => {
676
- const response = event.data;
677
- if (response.id !== message.id) {
678
- return;
679
- }
680
- if (response.error) {
681
- navigator.serviceWorker.removeEventListener("message", messageHandler);
682
- reject(response.error);
683
- return;
684
- }
685
- switch (response.type) {
686
- case "SETTLE_EVENT":
687
- if (callback) {
688
- callback(response.payload);
689
- }
690
- break;
691
- case "SETTLE_SUCCESS":
692
- navigator.serviceWorker.removeEventListener("message", messageHandler);
693
- resolve(response.payload.txid);
694
- break;
695
- default:
696
- console.error(`Unexpected response type for SETTLE request: ${response.type}`);
697
- }
698
- };
699
- navigator.serviceWorker.addEventListener("message", messageHandler);
700
- this.serviceWorker.postMessage(message);
701
- });
848
+ const response = await this.sendMessageWithEvents(message, (resp) => callback?.(resp.payload), (resp) => resp.type === "SETTLE_SUCCESS");
849
+ return response.payload.txid;
702
850
  }
703
851
  catch (error) {
704
852
  throw new Error(`Settlement failed: ${error}`);
@@ -772,4 +920,108 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
772
920
  };
773
921
  return manager;
774
922
  }
923
+ async getVtxoManager() {
924
+ const wallet = this;
925
+ const messageTag = this.messageTag;
926
+ const manager = {
927
+ async recoverVtxos(eventCallback) {
928
+ const message = {
929
+ tag: messageTag,
930
+ type: "RECOVER_VTXOS",
931
+ id: getRandomId(),
932
+ };
933
+ try {
934
+ const response = await wallet.sendMessageWithEvents(message, (resp) => eventCallback?.(resp.payload), (resp) => resp.type === "RECOVER_VTXOS_SUCCESS");
935
+ return response.payload.txid;
936
+ }
937
+ catch (e) {
938
+ throw new Error(`Failed to recover vtxos: ${e}`);
939
+ }
940
+ },
941
+ async getRecoverableBalance() {
942
+ const message = {
943
+ tag: messageTag,
944
+ type: "GET_RECOVERABLE_BALANCE",
945
+ id: getRandomId(),
946
+ };
947
+ try {
948
+ const response = await wallet.sendMessage(message);
949
+ const payload = response
950
+ .payload;
951
+ return {
952
+ recoverable: BigInt(payload.recoverable),
953
+ subdust: BigInt(payload.subdust),
954
+ includesSubdust: payload.includesSubdust,
955
+ vtxoCount: payload.vtxoCount,
956
+ };
957
+ }
958
+ catch (e) {
959
+ throw new Error(`Failed to get recoverable balance: ${e}`);
960
+ }
961
+ },
962
+ async getExpiringVtxos(thresholdMs) {
963
+ const message = {
964
+ tag: messageTag,
965
+ type: "GET_EXPIRING_VTXOS",
966
+ id: getRandomId(),
967
+ payload: { thresholdMs },
968
+ };
969
+ try {
970
+ const response = await wallet.sendMessage(message);
971
+ return response.payload.vtxos;
972
+ }
973
+ catch (e) {
974
+ throw new Error(`Failed to get expiring vtxos: ${e}`);
975
+ }
976
+ },
977
+ async renewVtxos(eventCallback) {
978
+ const message = {
979
+ tag: messageTag,
980
+ type: "RENEW_VTXOS",
981
+ id: getRandomId(),
982
+ };
983
+ try {
984
+ const response = await wallet.sendMessageWithEvents(message, (resp) => eventCallback?.(resp.payload), (resp) => resp.type === "RENEW_VTXOS_SUCCESS");
985
+ return response.payload.txid;
986
+ }
987
+ catch (e) {
988
+ throw new Error(`Failed to renew vtxos: ${e}`);
989
+ }
990
+ },
991
+ async getExpiredBoardingUtxos() {
992
+ const message = {
993
+ tag: messageTag,
994
+ type: "GET_EXPIRED_BOARDING_UTXOS",
995
+ id: getRandomId(),
996
+ };
997
+ try {
998
+ const response = await wallet.sendMessage(message);
999
+ return response.payload
1000
+ .utxos;
1001
+ }
1002
+ catch (e) {
1003
+ throw new Error(`Failed to get expired boarding utxos: ${e}`);
1004
+ }
1005
+ },
1006
+ async sweepExpiredBoardingUtxos() {
1007
+ const message = {
1008
+ tag: messageTag,
1009
+ type: "SWEEP_EXPIRED_BOARDING_UTXOS",
1010
+ id: getRandomId(),
1011
+ };
1012
+ try {
1013
+ const response = await wallet.sendMessage(message);
1014
+ return response
1015
+ .payload.txid;
1016
+ }
1017
+ catch (e) {
1018
+ throw new Error(`Failed to sweep expired boarding utxos: ${e}`);
1019
+ }
1020
+ },
1021
+ async dispose() {
1022
+ return;
1023
+ },
1024
+ };
1025
+ return manager;
1026
+ }
775
1027
  }