@arkade-os/sdk 0.4.8 → 0.4.10

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 (37) hide show
  1. package/dist/cjs/contracts/contractManager.js +59 -11
  2. package/dist/cjs/contracts/contractWatcher.js +21 -2
  3. package/dist/cjs/index.js +4 -3
  4. package/dist/cjs/providers/expoIndexer.js +1 -0
  5. package/dist/cjs/providers/indexer.js +1 -0
  6. package/dist/cjs/utils/arkTransaction.js +17 -6
  7. package/dist/cjs/utils/transactionHistory.js +2 -1
  8. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +109 -29
  9. package/dist/cjs/wallet/serviceWorker/wallet.js +25 -2
  10. package/dist/cjs/wallet/vtxo-manager.js +81 -50
  11. package/dist/cjs/wallet/wallet.js +46 -34
  12. package/dist/cjs/worker/errors.js +3 -4
  13. package/dist/cjs/worker/messageBus.js +7 -0
  14. package/dist/esm/contracts/contractManager.js +59 -11
  15. package/dist/esm/contracts/contractWatcher.js +21 -2
  16. package/dist/esm/index.js +2 -2
  17. package/dist/esm/providers/expoIndexer.js +1 -0
  18. package/dist/esm/providers/indexer.js +1 -0
  19. package/dist/esm/utils/arkTransaction.js +17 -6
  20. package/dist/esm/utils/transactionHistory.js +2 -1
  21. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +109 -29
  22. package/dist/esm/wallet/serviceWorker/wallet.js +26 -3
  23. package/dist/esm/wallet/vtxo-manager.js +81 -50
  24. package/dist/esm/wallet/wallet.js +46 -34
  25. package/dist/esm/worker/errors.js +2 -3
  26. package/dist/esm/worker/messageBus.js +7 -0
  27. package/dist/types/contracts/contractManager.d.ts +10 -0
  28. package/dist/types/index.d.ts +2 -2
  29. package/dist/types/repositories/serialization.d.ts +1 -0
  30. package/dist/types/utils/transactionHistory.d.ts +1 -1
  31. package/dist/types/wallet/index.d.ts +2 -0
  32. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +23 -6
  33. package/dist/types/wallet/serviceWorker/wallet.d.ts +9 -1
  34. package/dist/types/wallet/vtxo-manager.d.ts +5 -0
  35. package/dist/types/worker/errors.d.ts +1 -0
  36. package/dist/types/worker/messageBus.d.ts +6 -0
  37. package/package.json +1 -1
@@ -1,6 +1,7 @@
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';
4
5
  export class WalletNotInitializedError extends Error {
5
6
  constructor() {
6
7
  super("Wallet handler not initialized");
@@ -169,7 +170,8 @@ export class WalletMessageHandler {
169
170
  });
170
171
  }
171
172
  case "GET_TRANSACTION_HISTORY": {
172
- const transactions = await this.readonlyWallet.getTransactionHistory();
173
+ const allVtxos = await this.getVtxosFromRepo();
174
+ const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
173
175
  return this.tagged({
174
176
  id,
175
177
  type: "TRANSACTION_HISTORY",
@@ -196,7 +198,7 @@ export class WalletMessageHandler {
196
198
  });
197
199
  }
198
200
  case "RELOAD_WALLET": {
199
- await this.onWalletInitialized();
201
+ await this.reloadWallet();
200
202
  return this.tagged({
201
203
  id,
202
204
  type: "RELOAD_SUCCESS",
@@ -282,6 +284,14 @@ export class WalletMessageHandler {
282
284
  payload: { isWatching },
283
285
  });
284
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
+ }
285
295
  case "SEND": {
286
296
  const { recipients } = message.payload;
287
297
  const txid = await this.wallet.send(...recipients);
@@ -438,10 +448,9 @@ export class WalletMessageHandler {
438
448
  await this.onWalletInitialized();
439
449
  }
440
450
  async handleGetBalance() {
441
- const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
451
+ const [boardingUtxos, allVtxos] = await Promise.all([
442
452
  this.getAllBoardingUtxos(),
443
- this.getSpendableVtxos(),
444
- this.getSweptVtxos(),
453
+ this.getVtxosFromRepo(),
445
454
  ]);
446
455
  // boarding
447
456
  let confirmed = 0;
@@ -454,7 +463,9 @@ export class WalletMessageHandler {
454
463
  unconfirmed += utxo.value;
455
464
  }
456
465
  }
457
- // 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");
458
469
  let settled = 0;
459
470
  let preconfirmed = 0;
460
471
  let recoverable = 0;
@@ -504,23 +515,12 @@ export class WalletMessageHandler {
504
515
  return this.readonlyWallet.getBoardingUtxos();
505
516
  }
506
517
  /**
507
- * Get spendable vtxos for the current wallet address
518
+ * Get spendable vtxos from the repository
508
519
  */
509
520
  async getSpendableVtxos() {
510
- if (!this.readonlyWallet)
511
- return [];
512
- const vtxos = await this.readonlyWallet.getVtxos();
521
+ const vtxos = await this.getVtxosFromRepo();
513
522
  return vtxos.filter(isSpendable);
514
523
  }
515
- /**
516
- * Get swept vtxos for the current wallet address
517
- */
518
- async getSweptVtxos() {
519
- if (!this.readonlyWallet)
520
- return [];
521
- const vtxos = await this.readonlyWallet.getVtxos();
522
- return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
523
- }
524
524
  async onWalletInitialized() {
525
525
  if (!this.readonlyWallet ||
526
526
  !this.arkProvider ||
@@ -528,10 +528,11 @@ export class WalletMessageHandler {
528
528
  !this.walletRepository) {
529
529
  return;
530
530
  }
531
- // Get all wallet scripts (current + historical delegate/non-delegate)
532
- const scripts = await this.readonlyWallet.getWalletScripts();
533
- const response = await this.indexerProvider.getVtxos({ scripts });
534
- 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();
535
536
  if (this.wallet) {
536
537
  try {
537
538
  // recover pending transactions if possible
@@ -543,15 +544,13 @@ export class WalletMessageHandler {
543
544
  console.error("Error recovering pending transactions:", error);
544
545
  }
545
546
  }
546
- // Get wallet address and save vtxos using unified repository
547
- const address = await this.readonlyWallet.getAddress();
548
- await this.walletRepository.saveVtxos(address, vtxos);
549
547
  // Fetch boarding utxos and save using unified repository
550
548
  const boardingAddress = await this.readonlyWallet.getBoardingAddress();
551
549
  const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
552
550
  await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => extendCoin(this.readonlyWallet, utxo)));
553
- // Get transaction history to cache boarding txs
554
- 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);
555
554
  if (txs)
556
555
  await this.walletRepository.saveTransactions(address, txs);
557
556
  // unsubscribe previous subscription if any
@@ -596,7 +595,6 @@ export class WalletMessageHandler {
596
595
  }));
597
596
  }
598
597
  });
599
- await this.ensureContractEventBroadcasting();
600
598
  // Eagerly start the VtxoManager so its background tasks (auto-renewal,
601
599
  // boarding UTXO polling/sweep) run inside the service worker without
602
600
  // waiting for a client to send a vtxo-manager message first.
@@ -609,6 +607,17 @@ export class WalletMessageHandler {
609
607
  }
610
608
  }
611
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();
620
+ }
612
621
  async handleSettle(message) {
613
622
  const wallet = this.requireWallet();
614
623
  const txid = await wallet.settle(message.payload.params, (e) => {
@@ -732,6 +741,77 @@ export class WalletMessageHandler {
732
741
  this.arkProvider = undefined;
733
742
  this.indexerProvider = undefined;
734
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
+ }
735
815
  async ensureContractEventBroadcasting() {
736
816
  if (!this.readonlyWallet)
737
817
  return;
@@ -3,12 +3,13 @@ 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
6
+ import { MESSAGE_BUS_NOT_INITIALIZED, ServiceWorkerTimeoutError, } from '../../worker/errors.js';
7
+ // Check by error message content instead of instanceof because postMessage uses the
8
8
  // structured clone algorithm which strips the prototype chain — the page
9
9
  // receives a plain Error, not the original MessageBusNotInitializedError.
10
10
  function isMessageBusNotInitializedError(error) {
11
- return (error instanceof Error && error.name === "MessageBusNotInitializedError");
11
+ return (error instanceof Error &&
12
+ error.message.includes(MESSAGE_BUS_NOT_INITIALIZED));
12
13
  }
13
14
  const DEDUPABLE_REQUEST_TYPES = new Set([
14
15
  "GET_ADDRESS",
@@ -158,7 +159,10 @@ export class ServiceWorkerReadonlyWallet {
158
159
  publicKey: initConfig.arkServerPublicKey,
159
160
  },
160
161
  delegatorUrl: initConfig.delegatorUrl,
162
+ indexerUrl: options.indexerUrl,
163
+ esploraUrl: options.esploraUrl,
161
164
  timeoutMs: options.messageBusTimeoutMs,
165
+ watcherConfig: options.watcherConfig,
162
166
  }, options.messageBusTimeoutMs);
163
167
  // Initialize the wallet handler
164
168
  const initMessage = {
@@ -175,6 +179,9 @@ export class ServiceWorkerReadonlyWallet {
175
179
  publicKey: initConfig.arkServerPublicKey,
176
180
  },
177
181
  delegatorUrl: initConfig.delegatorUrl,
182
+ indexerUrl: options.indexerUrl,
183
+ esploraUrl: options.esploraUrl,
184
+ watcherConfig: options.watcherConfig,
178
185
  };
179
186
  wallet.initWalletPayload = initConfig;
180
187
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -675,6 +682,14 @@ export class ServiceWorkerReadonlyWallet {
675
682
  navigator.serviceWorker.removeEventListener("message", messageHandler);
676
683
  };
677
684
  },
685
+ async refreshVtxos() {
686
+ const message = {
687
+ type: "REFRESH_VTXOS",
688
+ id: getRandomId(),
689
+ tag: messageTag,
690
+ };
691
+ await sendContractMessage(message);
692
+ },
678
693
  async isWatching() {
679
694
  const message = {
680
695
  type: "IS_CONTRACT_MANAGER_WATCHING",
@@ -744,7 +759,11 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
744
759
  publicKey: initConfig.arkServerPublicKey,
745
760
  },
746
761
  delegatorUrl: initConfig.delegatorUrl,
762
+ indexerUrl: options.indexerUrl,
763
+ esploraUrl: options.esploraUrl,
747
764
  timeoutMs: options.messageBusTimeoutMs,
765
+ settlementConfig: options.settlementConfig,
766
+ watcherConfig: options.watcherConfig,
748
767
  }, options.messageBusTimeoutMs);
749
768
  // Initialize the service worker with the config
750
769
  const initMessage = {
@@ -762,6 +781,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
762
781
  publicKey: initConfig.arkServerPublicKey,
763
782
  },
764
783
  delegatorUrl: initConfig.delegatorUrl,
784
+ indexerUrl: options.indexerUrl,
785
+ esploraUrl: options.esploraUrl,
786
+ settlementConfig: options.settlementConfig,
787
+ watcherConfig: options.watcherConfig,
765
788
  };
766
789
  wallet.initWalletPayload = initConfig;
767
790
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -152,7 +152,12 @@ export class VtxoManager {
152
152
  this.knownBoardingUtxos = new Set();
153
153
  this.sweptBoardingUtxos = new Set();
154
154
  this.pollInProgress = false;
155
+ this.disposed = false;
155
156
  this.consecutivePollFailures = 0;
157
+ // Guards against renewal feedback loop: when renewVtxos() settles, the
158
+ // server emits new VTXOs → vtxo_received → renewVtxos() again → infinite loop.
159
+ this.renewalInProgress = false;
160
+ this.lastRenewalTimestamp = 0;
156
161
  // Normalize: prefer settlementConfig, fall back to renewalConfig, default to enabled
157
162
  if (settlementConfig !== undefined) {
158
163
  this.settlementConfig = settlementConfig;
@@ -336,32 +341,43 @@ export class VtxoManager {
336
341
  * ```
337
342
  */
338
343
  async renewVtxos(eventCallback) {
339
- // Get all VTXOs (including recoverable ones)
340
- // Use default threshold to bypass settlementConfig gate (manual API should always work)
341
- const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
342
- this.settlementConfig?.vtxoThreshold !== undefined
343
- ? this.settlementConfig.vtxoThreshold * 1000
344
- : DEFAULT_RENEWAL_CONFIG.thresholdMs);
345
- if (vtxos.length === 0) {
346
- throw new Error("No VTXOs available to renew");
347
- }
348
- const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
349
- // Get dust amount from wallet
350
- const dustAmount = getDustAmount(this.wallet);
351
- // Check if total amount is above dust threshold
352
- if (BigInt(totalAmount) < dustAmount) {
353
- throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
344
+ if (this.renewalInProgress) {
345
+ throw new Error("Renewal already in progress");
346
+ }
347
+ this.renewalInProgress = true;
348
+ try {
349
+ // Get all VTXOs (including recoverable ones)
350
+ // Use default threshold to bypass settlementConfig gate (manual API should always work)
351
+ const vtxos = await this.getExpiringVtxos(this.settlementConfig !== false &&
352
+ this.settlementConfig?.vtxoThreshold !== undefined
353
+ ? this.settlementConfig.vtxoThreshold * 1000
354
+ : DEFAULT_RENEWAL_CONFIG.thresholdMs);
355
+ if (vtxos.length === 0) {
356
+ throw new Error("No VTXOs available to renew");
357
+ }
358
+ const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
359
+ // Get dust amount from wallet
360
+ const dustAmount = getDustAmount(this.wallet);
361
+ // Check if total amount is above dust threshold
362
+ if (BigInt(totalAmount) < dustAmount) {
363
+ throw new Error(`Total amount ${totalAmount} is below dust threshold ${dustAmount}`);
364
+ }
365
+ const arkAddress = await this.wallet.getAddress();
366
+ const txid = await this.wallet.settle({
367
+ inputs: vtxos,
368
+ outputs: [
369
+ {
370
+ address: arkAddress,
371
+ amount: BigInt(totalAmount),
372
+ },
373
+ ],
374
+ }, eventCallback);
375
+ this.lastRenewalTimestamp = Date.now();
376
+ return txid;
377
+ }
378
+ finally {
379
+ this.renewalInProgress = false;
354
380
  }
355
- const arkAddress = await this.wallet.getAddress();
356
- return this.wallet.settle({
357
- inputs: vtxos,
358
- outputs: [
359
- {
360
- address: arkAddress,
361
- amount: BigInt(totalAmount),
362
- },
363
- ],
364
- }, eventCallback);
365
381
  }
366
382
  // ========== Boarding UTXO Sweep Methods ==========
367
383
  /**
@@ -529,7 +545,11 @@ export class VtxoManager {
529
545
  }
530
546
  // Start polling for boarding UTXOs independently of contract manager
531
547
  // SSE setup. Use a short delay to let the wallet finish construction.
532
- setTimeout(() => this.startBoardingUtxoPoll(), 1000);
548
+ this.startupPollTimeoutId = setTimeout(() => {
549
+ if (this.disposed)
550
+ return;
551
+ this.startBoardingUtxoPoll();
552
+ }, 1000);
533
553
  try {
534
554
  const [delegatorManager, contractManager, destination] = await Promise.all([
535
555
  this.wallet.getDelegatorManager(),
@@ -540,28 +560,33 @@ export class VtxoManager {
540
560
  if (event.type !== "vtxo_received") {
541
561
  return;
542
562
  }
543
- this.renewVtxos().catch((e) => {
544
- if (e instanceof Error) {
545
- if (e.message.includes("No VTXOs available to renew")) {
546
- // Not an error, just no VTXO eligible for renewal.
547
- return;
563
+ const msSinceLastRenewal = Date.now() - this.lastRenewalTimestamp;
564
+ const shouldRenew = !this.renewalInProgress &&
565
+ msSinceLastRenewal >= VtxoManager.RENEWAL_COOLDOWN_MS;
566
+ if (shouldRenew) {
567
+ this.renewVtxos().catch((e) => {
568
+ if (e instanceof Error) {
569
+ if (e.message.includes("No VTXOs available to renew")) {
570
+ // Not an error, just no VTXO eligible for renewal.
571
+ return;
572
+ }
573
+ if (e.message.includes("is below dust threshold")) {
574
+ // Not an error, just below dust threshold.
575
+ // As more VTXOs are received, the threshold will be raised.
576
+ return;
577
+ }
578
+ if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
579
+ e.message.includes("duplicated input")) {
580
+ // VTXO is already being used in a concurrent
581
+ // user-initiated operation. Skip silently — the
582
+ // wallet's tx lock serializes these, but the
583
+ // renewal will retry on the next cycle.
584
+ return;
585
+ }
548
586
  }
549
- if (e.message.includes("is below dust threshold")) {
550
- // Not an error, just below dust threshold.
551
- // As more VTXOs are received, the threshold will be raised.
552
- return;
553
- }
554
- if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
555
- e.message.includes("duplicated input")) {
556
- // VTXO is already being used in a concurrent
557
- // user-initiated operation. Skip silently — the
558
- // wallet's tx lock serializes these, but the
559
- // renewal will retry on the next cycle.
560
- return;
561
- }
562
- }
563
- console.error("Error renewing VTXOs:", e);
564
- });
587
+ console.error("Error renewing VTXOs:", e);
588
+ });
589
+ }
565
590
  delegatorManager
566
591
  ?.delegate(event.vtxos, destination)
567
592
  .catch((e) => {
@@ -601,7 +626,7 @@ export class VtxoManager {
601
626
  this.pollBoardingUtxos();
602
627
  }
603
628
  schedulePoll() {
604
- if (this.settlementConfig === false)
629
+ if (this.disposed || this.settlementConfig === false)
605
630
  return;
606
631
  const delay = this.getNextPollDelay();
607
632
  this.pollTimeoutId = setTimeout(() => this.pollBoardingUtxos(), delay);
@@ -675,8 +700,8 @@ export class VtxoManager {
675
700
  const expired = boardingUtxos.filter((utxo) => hasBoardingTxExpired(utxo, boardingTimelock, chainTipHeight));
676
701
  expiredSet = new Set(expired.map((u) => `${u.txid}:${u.vout}`));
677
702
  }
678
- catch {
679
- return;
703
+ catch (e) {
704
+ throw e instanceof Error ? e : new Error(String(e));
680
705
  }
681
706
  const unsettledUtxos = boardingUtxos.filter((u) => !this.knownBoardingUtxos.has(`${u.txid}:${u.vout}`) &&
682
707
  !expiredSet.has(`${u.txid}:${u.vout}`));
@@ -698,6 +723,11 @@ export class VtxoManager {
698
723
  }
699
724
  async dispose() {
700
725
  this.disposePromise ?? (this.disposePromise = (async () => {
726
+ this.disposed = true;
727
+ if (this.startupPollTimeoutId) {
728
+ clearTimeout(this.startupPollTimeoutId);
729
+ this.startupPollTimeoutId = undefined;
730
+ }
701
731
  if (this.pollTimeoutId) {
702
732
  clearTimeout(this.pollTimeoutId);
703
733
  this.pollTimeoutId = undefined;
@@ -713,3 +743,4 @@ export class VtxoManager {
713
743
  }
714
744
  }
715
745
  VtxoManager.MAX_BACKOFF_MS = 5 * 60 * 1000; // 5 minutes
746
+ VtxoManager.RENEWAL_COOLDOWN_MS = 30000; // 30 seconds
@@ -273,27 +273,33 @@ export class ReadonlyWallet {
273
273
  const scriptMap = await this.getScriptMap();
274
274
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
275
275
  const allExtended = [];
276
- // Query each script separately so we can extend VTXOs with the correct tapscript
277
- for (const [scriptHex, vtxoScript] of scriptMap) {
278
- const response = await this.indexerProvider.getVtxos({
279
- scripts: [scriptHex],
280
- });
281
- let vtxos = response.vtxos.filter(isSpendable);
282
- if (!f.withRecoverable) {
283
- vtxos = vtxos.filter((vtxo) => !isRecoverable(vtxo) && !isExpired(vtxo));
284
- }
285
- if (f.withUnrolled) {
286
- const spentVtxos = response.vtxos.filter((vtxo) => !isSpendable(vtxo));
287
- vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
276
+ // Batch all scripts into a single indexer call
277
+ const allScripts = [...scriptMap.keys()];
278
+ const response = await this.indexerProvider.getVtxos({
279
+ scripts: allScripts,
280
+ });
281
+ for (const vtxo of response.vtxos) {
282
+ const vtxoScript = vtxo.script
283
+ ? scriptMap.get(vtxo.script)
284
+ : undefined;
285
+ if (!vtxoScript)
286
+ continue;
287
+ if (isSpendable(vtxo)) {
288
+ if (!f.withRecoverable &&
289
+ (isRecoverable(vtxo) || isExpired(vtxo))) {
290
+ continue;
291
+ }
288
292
  }
289
- for (const vtxo of vtxos) {
290
- allExtended.push({
291
- ...vtxo,
292
- forfeitTapLeafScript: vtxoScript.forfeit(),
293
- intentTapLeafScript: vtxoScript.forfeit(),
294
- tapTree: vtxoScript.encode(),
295
- });
293
+ else {
294
+ if (!f.withUnrolled || !vtxo.isUnrolled)
295
+ continue;
296
296
  }
297
+ allExtended.push({
298
+ ...vtxo,
299
+ forfeitTapLeafScript: vtxoScript.forfeit(),
300
+ intentTapLeafScript: vtxoScript.forfeit(),
301
+ tapTree: vtxoScript.encode(),
302
+ });
297
303
  }
298
304
  // Update cache with fresh data
299
305
  await this.walletRepository.saveVtxos(address, allExtended);
@@ -305,7 +311,7 @@ export class ReadonlyWallet {
305
311
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
306
312
  const getTxCreatedAt = (txid) => this.indexerProvider
307
313
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
308
- .then((res) => res.vtxos[0]?.createdAt.getTime() || 0);
314
+ .then((res) => res.vtxos[0]?.createdAt.getTime());
309
315
  return buildTransactionHistory(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
310
316
  }
311
317
  async getBoardingTxs() {
@@ -1338,23 +1344,29 @@ export class Wallet extends ReadonlyWallet {
1338
1344
  async finalizePendingTxs(vtxos) {
1339
1345
  const MAX_INPUTS_PER_INTENT = 20;
1340
1346
  if (!vtxos || vtxos.length === 0) {
1341
- // Query per-script so each VTXO is extended with the correct tapscript
1347
+ // Batch all scripts into a single indexer call
1342
1348
  const scriptMap = await this.getScriptMap();
1343
1349
  const allExtended = [];
1344
- for (const [scriptHex, vtxoScript] of scriptMap) {
1345
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1346
- scripts: [scriptHex],
1347
- });
1348
- const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1349
- vtxo.virtualStatus.state !== "settled");
1350
- for (const vtxo of pending) {
1351
- allExtended.push({
1352
- ...vtxo,
1353
- forfeitTapLeafScript: vtxoScript.forfeit(),
1354
- intentTapLeafScript: vtxoScript.forfeit(),
1355
- tapTree: vtxoScript.encode(),
1356
- });
1350
+ const allScripts = [...scriptMap.keys()];
1351
+ const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1352
+ scripts: allScripts,
1353
+ });
1354
+ for (const vtxo of fetchedVtxos) {
1355
+ const vtxoScript = vtxo.script
1356
+ ? scriptMap.get(vtxo.script)
1357
+ : undefined;
1358
+ if (!vtxoScript)
1359
+ continue;
1360
+ if (vtxo.virtualStatus.state === "swept" ||
1361
+ vtxo.virtualStatus.state === "settled") {
1362
+ continue;
1357
1363
  }
1364
+ allExtended.push({
1365
+ ...vtxo,
1366
+ forfeitTapLeafScript: vtxoScript.forfeit(),
1367
+ intentTapLeafScript: vtxoScript.forfeit(),
1368
+ tapTree: vtxoScript.encode(),
1369
+ });
1358
1370
  }
1359
1371
  if (allExtended.length === 0) {
1360
1372
  return { finalized: [], pending: [] };
@@ -1,12 +1,11 @@
1
+ export const MESSAGE_BUS_NOT_INITIALIZED = "MessageBus not initialized";
1
2
  export class MessageBusNotInitializedError extends Error {
2
3
  constructor() {
3
- super("MessageBus not initialized");
4
- this.name = "MessageBusNotInitializedError";
4
+ super(MESSAGE_BUS_NOT_INITIALIZED);
5
5
  }
6
6
  }
7
7
  export class ServiceWorkerTimeoutError extends Error {
8
8
  constructor(detail) {
9
9
  super(detail);
10
- this.name = "ServiceWorkerTimeoutError";
11
10
  }
12
11
  }
@@ -149,8 +149,12 @@ export class MessageBus {
149
149
  identity,
150
150
  arkServerUrl: config.arkServer.url,
151
151
  arkServerPublicKey: config.arkServer.publicKey,
152
+ indexerUrl: config.indexerUrl,
153
+ esploraUrl: config.esploraUrl,
152
154
  storage,
153
155
  delegatorProvider,
156
+ settlementConfig: config.settlementConfig,
157
+ watcherConfig: config.watcherConfig,
154
158
  });
155
159
  return { wallet, arkProvider, readonlyWallet: wallet };
156
160
  }
@@ -160,8 +164,11 @@ export class MessageBus {
160
164
  identity,
161
165
  arkServerUrl: config.arkServer.url,
162
166
  arkServerPublicKey: config.arkServer.publicKey,
167
+ indexerUrl: config.indexerUrl,
168
+ esploraUrl: config.esploraUrl,
163
169
  storage,
164
170
  delegatorProvider,
171
+ watcherConfig: config.watcherConfig,
165
172
  });
166
173
  return { readonlyWallet, arkProvider };
167
174
  }
@@ -102,6 +102,11 @@ export interface IContractManager extends Disposable {
102
102
  * @returns Unsubscribe function
103
103
  */
104
104
  onContractEvent(callback: ContractEventCallback): () => void;
105
+ /**
106
+ * Force a full VTXO refresh from the indexer for all contracts.
107
+ * Populates the wallet repository with complete VTXO history.
108
+ */
109
+ refreshVtxos(): Promise<void>;
105
110
  /**
106
111
  * Whether the underlying watcher is currently active.
107
112
  */
@@ -292,6 +297,11 @@ export declare class ContractManager implements IContractManager {
292
297
  * ```
293
298
  */
294
299
  onContractEvent(callback: ContractEventCallback): () => void;
300
+ /**
301
+ * Force a full VTXO refresh from the indexer for all contracts.
302
+ * Populates the wallet repository with complete VTXO history.
303
+ */
304
+ refreshVtxos(): Promise<void>;
295
305
  /**
296
306
  * Check if currently watching.
297
307
  */