@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
@@ -1,6 +1,25 @@
1
1
  import { RestIndexerProvider } from '../../providers/indexer.js';
2
2
  import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
3
3
  import { extendCoin, extendVirtualCoin } from '../utils.js';
4
+ import { buildTransactionHistory } from '../../utils/transactionHistory.js';
5
+ export class WalletNotInitializedError extends Error {
6
+ constructor() {
7
+ super("Wallet handler not initialized");
8
+ this.name = "WalletNotInitializedError";
9
+ }
10
+ }
11
+ export class ReadonlyWalletError extends Error {
12
+ constructor() {
13
+ super("Read-only wallet: operation requires signing");
14
+ this.name = "ReadonlyWalletError";
15
+ }
16
+ }
17
+ export class DelegatorNotConfiguredError extends Error {
18
+ constructor() {
19
+ super("Delegator not configured");
20
+ this.name = "DelegatorNotConfiguredError";
21
+ }
22
+ }
4
23
  export const DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
5
24
  export class WalletMessageHandler {
6
25
  /**
@@ -21,7 +40,31 @@ export class WalletMessageHandler {
21
40
  this.walletRepository = repositories.walletRepository;
22
41
  }
23
42
  async stop() {
24
- // optional cleanup and persistence
43
+ if (this.incomingFundsSubscription) {
44
+ this.incomingFundsSubscription();
45
+ this.incomingFundsSubscription = undefined;
46
+ }
47
+ if (this.contractEventsSubscription) {
48
+ this.contractEventsSubscription();
49
+ this.contractEventsSubscription = undefined;
50
+ }
51
+ // Dispose the wallet to stop VtxoManager background tasks
52
+ // (auto-renewal, boarding UTXO polling) and ContractWatcher.
53
+ try {
54
+ if (this.wallet) {
55
+ await this.wallet.dispose();
56
+ }
57
+ else if (this.readonlyWallet) {
58
+ await this.readonlyWallet.dispose();
59
+ }
60
+ }
61
+ catch (_) {
62
+ // best-effort teardown
63
+ }
64
+ this.wallet = undefined;
65
+ this.readonlyWallet = undefined;
66
+ this.arkProvider = undefined;
67
+ this.indexerProvider = undefined;
25
68
  }
26
69
  async tick(_now) {
27
70
  const results = await Promise.allSettled(this.onNextTick.map((fn) => fn()));
@@ -44,7 +87,7 @@ export class WalletMessageHandler {
44
87
  }
45
88
  requireWallet() {
46
89
  if (!this.wallet) {
47
- throw new Error("Read-only wallet: operation requires signing");
90
+ throw new ReadonlyWalletError();
48
91
  }
49
92
  return this.wallet;
50
93
  }
@@ -66,7 +109,7 @@ export class WalletMessageHandler {
66
109
  if (!this.readonlyWallet) {
67
110
  return this.tagged({
68
111
  id,
69
- error: new Error("Wallet handler not initialized"),
112
+ error: new WalletNotInitializedError(),
70
113
  });
71
114
  }
72
115
  try {
@@ -127,7 +170,8 @@ export class WalletMessageHandler {
127
170
  });
128
171
  }
129
172
  case "GET_TRANSACTION_HISTORY": {
130
- const transactions = await this.readonlyWallet.getTransactionHistory();
173
+ const allVtxos = await this.getVtxosFromRepo();
174
+ const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
131
175
  return this.tagged({
132
176
  id,
133
177
  type: "TRANSACTION_HISTORY",
@@ -154,7 +198,7 @@ export class WalletMessageHandler {
154
198
  });
155
199
  }
156
200
  case "RELOAD_WALLET": {
157
- await this.onWalletInitialized();
201
+ await this.reloadWallet();
158
202
  return this.tagged({
159
203
  id,
160
204
  type: "RELOAD_SUCCESS",
@@ -240,6 +284,14 @@ export class WalletMessageHandler {
240
284
  payload: { isWatching },
241
285
  });
242
286
  }
287
+ case "REFRESH_VTXOS": {
288
+ const manager = await this.readonlyWallet.getContractManager();
289
+ await manager.refreshVtxos();
290
+ return this.tagged({
291
+ id,
292
+ type: "REFRESH_VTXOS_SUCCESS",
293
+ });
294
+ }
243
295
  case "SEND": {
244
296
  const { recipients } = message.payload;
245
297
  const txid = await this.wallet.send(...recipients);
@@ -294,7 +346,7 @@ export class WalletMessageHandler {
294
346
  const wallet = this.requireWallet();
295
347
  const delegatorManager = await wallet.getDelegatorManager();
296
348
  if (!delegatorManager) {
297
- throw new Error("Delegator not configured");
349
+ throw new DelegatorNotConfiguredError();
298
350
  }
299
351
  const info = await delegatorManager.getDelegateInfo();
300
352
  return this.tagged({
@@ -303,6 +355,83 @@ export class WalletMessageHandler {
303
355
  payload: { info },
304
356
  });
305
357
  }
358
+ case "RECOVER_VTXOS": {
359
+ const wallet = this.requireWallet();
360
+ const vtxoManager = await wallet.getVtxoManager();
361
+ const txid = await vtxoManager.recoverVtxos((e) => {
362
+ this.scheduleForNextTick(() => this.tagged({
363
+ id,
364
+ type: "RECOVER_VTXOS_EVENT",
365
+ payload: e,
366
+ }));
367
+ });
368
+ return this.tagged({
369
+ id,
370
+ type: "RECOVER_VTXOS_SUCCESS",
371
+ payload: { txid },
372
+ });
373
+ }
374
+ case "GET_RECOVERABLE_BALANCE": {
375
+ const wallet = this.requireWallet();
376
+ const vtxoManager = await wallet.getVtxoManager();
377
+ const balance = await vtxoManager.getRecoverableBalance();
378
+ return this.tagged({
379
+ id,
380
+ type: "RECOVERABLE_BALANCE",
381
+ payload: {
382
+ recoverable: balance.recoverable.toString(),
383
+ subdust: balance.subdust.toString(),
384
+ includesSubdust: balance.includesSubdust,
385
+ vtxoCount: balance.vtxoCount,
386
+ },
387
+ });
388
+ }
389
+ case "GET_EXPIRING_VTXOS": {
390
+ const wallet = this.requireWallet();
391
+ const vtxoManager = await wallet.getVtxoManager();
392
+ const vtxos = await vtxoManager.getExpiringVtxos(message.payload.thresholdMs);
393
+ return this.tagged({
394
+ id,
395
+ type: "EXPIRING_VTXOS",
396
+ payload: { vtxos },
397
+ });
398
+ }
399
+ case "RENEW_VTXOS": {
400
+ const wallet = this.requireWallet();
401
+ const vtxoManager = await wallet.getVtxoManager();
402
+ const txid = await vtxoManager.renewVtxos((e) => {
403
+ this.scheduleForNextTick(() => this.tagged({
404
+ id,
405
+ type: "RENEW_VTXOS_EVENT",
406
+ payload: e,
407
+ }));
408
+ });
409
+ return this.tagged({
410
+ id,
411
+ type: "RENEW_VTXOS_SUCCESS",
412
+ payload: { txid },
413
+ });
414
+ }
415
+ case "GET_EXPIRED_BOARDING_UTXOS": {
416
+ const wallet = this.requireWallet();
417
+ const vtxoManager = await wallet.getVtxoManager();
418
+ const utxos = await vtxoManager.getExpiredBoardingUtxos();
419
+ return this.tagged({
420
+ id,
421
+ type: "EXPIRED_BOARDING_UTXOS",
422
+ payload: { utxos },
423
+ });
424
+ }
425
+ case "SWEEP_EXPIRED_BOARDING_UTXOS": {
426
+ const wallet = this.requireWallet();
427
+ const vtxoManager = await wallet.getVtxoManager();
428
+ const txid = await vtxoManager.sweepExpiredBoardingUtxos();
429
+ return this.tagged({
430
+ id,
431
+ type: "SWEEP_EXPIRED_BOARDING_UTXOS_SUCCESS",
432
+ payload: { txid },
433
+ });
434
+ }
306
435
  default:
307
436
  console.error("Unknown message type", message);
308
437
  throw new Error("Unknown message");
@@ -319,10 +448,9 @@ export class WalletMessageHandler {
319
448
  await this.onWalletInitialized();
320
449
  }
321
450
  async handleGetBalance() {
322
- const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
451
+ const [boardingUtxos, allVtxos] = await Promise.all([
323
452
  this.getAllBoardingUtxos(),
324
- this.getSpendableVtxos(),
325
- this.getSweptVtxos(),
453
+ this.getVtxosFromRepo(),
326
454
  ]);
327
455
  // boarding
328
456
  let confirmed = 0;
@@ -335,7 +463,9 @@ export class WalletMessageHandler {
335
463
  unconfirmed += utxo.value;
336
464
  }
337
465
  }
338
- // offchain
466
+ // offchain — split spendable vs swept from single repo read
467
+ const spendableVtxos = allVtxos.filter(isSpendable);
468
+ const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
339
469
  let settled = 0;
340
470
  let preconfirmed = 0;
341
471
  let recoverable = 0;
@@ -385,23 +515,12 @@ export class WalletMessageHandler {
385
515
  return this.readonlyWallet.getBoardingUtxos();
386
516
  }
387
517
  /**
388
- * Get spendable vtxos for the current wallet address
518
+ * Get spendable vtxos from the repository
389
519
  */
390
520
  async getSpendableVtxos() {
391
- if (!this.readonlyWallet)
392
- return [];
393
- const vtxos = await this.readonlyWallet.getVtxos();
521
+ const vtxos = await this.getVtxosFromRepo();
394
522
  return vtxos.filter(isSpendable);
395
523
  }
396
- /**
397
- * Get swept vtxos for the current wallet address
398
- */
399
- async getSweptVtxos() {
400
- if (!this.readonlyWallet)
401
- return [];
402
- const vtxos = await this.readonlyWallet.getVtxos();
403
- return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
404
- }
405
524
  async onWalletInitialized() {
406
525
  if (!this.readonlyWallet ||
407
526
  !this.arkProvider ||
@@ -409,10 +528,11 @@ export class WalletMessageHandler {
409
528
  !this.walletRepository) {
410
529
  return;
411
530
  }
412
- // Get all wallet scripts (current + historical delegate/non-delegate)
413
- const scripts = await this.readonlyWallet.getWalletScripts();
414
- const response = await this.indexerProvider.getVtxos({ scripts });
415
- const vtxos = response.vtxos.map((vtxo) => extendVirtualCoin(this.readonlyWallet, vtxo));
531
+ // Initialize contract manager FIRST this populates the repository
532
+ // with full VTXO history for all contracts (one indexer call per contract)
533
+ await this.ensureContractEventBroadcasting();
534
+ // Read VTXOs from repository (now populated by contract manager)
535
+ const vtxos = await this.getVtxosFromRepo();
416
536
  if (this.wallet) {
417
537
  try {
418
538
  // recover pending transactions if possible
@@ -424,15 +544,13 @@ export class WalletMessageHandler {
424
544
  console.error("Error recovering pending transactions:", error);
425
545
  }
426
546
  }
427
- // Get wallet address and save vtxos using unified repository
428
- const address = await this.readonlyWallet.getAddress();
429
- await this.walletRepository.saveVtxos(address, vtxos);
430
547
  // Fetch boarding utxos and save using unified repository
431
548
  const boardingAddress = await this.readonlyWallet.getBoardingAddress();
432
549
  const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
433
550
  await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => extendCoin(this.readonlyWallet, utxo)));
434
- // Get transaction history to cache boarding txs
435
- const txs = await this.readonlyWallet.getTransactionHistory();
551
+ // Build transaction history from cached VTXOs (no indexer call)
552
+ const address = await this.readonlyWallet.getAddress();
553
+ const txs = await this.buildTransactionHistoryFromCache(vtxos);
436
554
  if (txs)
437
555
  await this.walletRepository.saveTransactions(address, txs);
438
556
  // unsubscribe previous subscription if any
@@ -477,7 +595,28 @@ export class WalletMessageHandler {
477
595
  }));
478
596
  }
479
597
  });
480
- await this.ensureContractEventBroadcasting();
598
+ // Eagerly start the VtxoManager so its background tasks (auto-renewal,
599
+ // boarding UTXO polling/sweep) run inside the service worker without
600
+ // waiting for a client to send a vtxo-manager message first.
601
+ if (this.wallet) {
602
+ try {
603
+ await this.wallet.getVtxoManager();
604
+ }
605
+ catch (error) {
606
+ console.error("Error starting VtxoManager:", error);
607
+ }
608
+ }
609
+ }
610
+ /**
611
+ * Force a full VTXO refresh from the indexer, then re-run bootstrap.
612
+ * Used by RELOAD_WALLET to ensure fresh data.
613
+ */
614
+ async reloadWallet() {
615
+ if (!this.readonlyWallet)
616
+ return;
617
+ const manager = await this.readonlyWallet.getContractManager();
618
+ await manager.refreshVtxos();
619
+ await this.onWalletInitialized();
481
620
  }
482
621
  async handleSettle(message) {
483
622
  const wallet = this.requireWallet();
@@ -520,7 +659,7 @@ export class WalletMessageHandler {
520
659
  const wallet = this.requireWallet();
521
660
  const delegatorManager = await wallet.getDelegatorManager();
522
661
  if (!delegatorManager) {
523
- throw new Error("Delegator not configured");
662
+ throw new DelegatorNotConfiguredError();
524
663
  }
525
664
  const { vtxoOutpoints, destination, delegateAt } = message.payload;
526
665
  const allVtxos = await wallet.getVtxos();
@@ -547,7 +686,7 @@ export class WalletMessageHandler {
547
686
  }
548
687
  async handleGetVtxos(message) {
549
688
  if (!this.readonlyWallet) {
550
- throw new Error("Wallet handler not initialized");
689
+ throw new WalletNotInitializedError();
551
690
  }
552
691
  const vtxos = await this.getSpendableVtxos();
553
692
  const dustAmount = this.readonlyWallet.dustAmount;
@@ -602,6 +741,77 @@ export class WalletMessageHandler {
602
741
  this.arkProvider = undefined;
603
742
  this.indexerProvider = undefined;
604
743
  }
744
+ /**
745
+ * Read all VTXOs from the repository, aggregated across all contract
746
+ * addresses and the wallet's primary address, with deduplication.
747
+ */
748
+ async getVtxosFromRepo() {
749
+ if (!this.walletRepository || !this.readonlyWallet)
750
+ return [];
751
+ const seen = new Set();
752
+ const allVtxos = [];
753
+ const addVtxos = (vtxos) => {
754
+ for (const vtxo of vtxos) {
755
+ const key = `${vtxo.txid}:${vtxo.vout}`;
756
+ if (!seen.has(key)) {
757
+ seen.add(key);
758
+ allVtxos.push(vtxo);
759
+ }
760
+ }
761
+ };
762
+ // Aggregate VTXOs from all contract addresses
763
+ const manager = await this.readonlyWallet.getContractManager();
764
+ const contracts = await manager.getContracts();
765
+ for (const contract of contracts) {
766
+ const vtxos = await this.walletRepository.getVtxos(contract.address);
767
+ addVtxos(vtxos);
768
+ }
769
+ // Also check the wallet's primary address
770
+ const walletAddress = await this.readonlyWallet.getAddress();
771
+ const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
772
+ addVtxos(walletVtxos);
773
+ return allVtxos;
774
+ }
775
+ /**
776
+ * Build transaction history from cached VTXOs without hitting the indexer.
777
+ * Falls back to indexer only for uncached transaction timestamps.
778
+ */
779
+ async buildTransactionHistoryFromCache(vtxos) {
780
+ if (!this.readonlyWallet)
781
+ return null;
782
+ const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
783
+ // Build a lookup for cached VTXO timestamps, keyed by txid.
784
+ // Multiple VTXOs can share a txid (different vouts) — we keep the
785
+ // earliest createdAt so the history ordering is stable.
786
+ const vtxoCreatedAt = new Map();
787
+ for (const vtxo of vtxos) {
788
+ const existing = vtxoCreatedAt.get(vtxo.txid);
789
+ const ts = vtxo.createdAt.getTime();
790
+ if (existing === undefined || ts < existing) {
791
+ vtxoCreatedAt.set(vtxo.txid, ts);
792
+ }
793
+ }
794
+ // getTxCreatedAt resolves the creation timestamp of a transaction.
795
+ // buildTransactionHistory calls this for spent-offchain VTXOs with
796
+ // no change outputs to determine the time of the sending tx.
797
+ // Returns undefined on miss so buildTransactionHistory uses its
798
+ // own fallback (vtxo.createdAt + 1) rather than epoch 0.
799
+ // The vout:0 lookup in the indexer fallback mirrors the pre-existing
800
+ // convention in ReadonlyWallet.getTransactionHistory().
801
+ const getTxCreatedAt = async (txid) => {
802
+ const cached = vtxoCreatedAt.get(txid);
803
+ if (cached !== undefined)
804
+ return cached;
805
+ if (this.indexerProvider) {
806
+ const res = await this.indexerProvider.getVtxos({
807
+ outpoints: [{ txid, vout: 0 }],
808
+ });
809
+ return res.vtxos[0]?.createdAt.getTime();
810
+ }
811
+ return undefined;
812
+ };
813
+ return buildTransactionHistory(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
814
+ }
605
815
  async ensureContractEventBroadcasting() {
606
816
  if (!this.readonlyWallet)
607
817
  return;