@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
@@ -74,9 +74,10 @@ class ContractManager {
74
74
  }
75
75
  // Load persisted contracts
76
76
  const contracts = await this.config.contractRepository.getContracts();
77
- // fetch latest VTXOs for all contracts, ensure cache is up to date
77
+ // fetch all VTXOs (including spent/swept) for all contracts,
78
+ // so the repository has full history for transaction history and balance
78
79
  // TODO: what if the user has 1k contracts?
79
- await this.getVtxosForContracts(contracts);
80
+ await this.fetchContractVxosFromIndexer(contracts, true);
80
81
  // add all contracts to the watcher
81
82
  const now = Date.now();
82
83
  for (const contract of contracts) {
@@ -139,8 +140,8 @@ class ContractManager {
139
140
  };
140
141
  // Persist
141
142
  await this.config.contractRepository.saveContract(contract);
142
- // ensure we have the latest VTXOs for this contract
143
- await this.getVtxosForContracts([contract]);
143
+ // fetch all VTXOs (including spent/swept) for this contract
144
+ await this.fetchContractVxosFromIndexer([contract], true);
144
145
  // Add to watcher
145
146
  await this.watcher.addContract(contract);
146
147
  return contract;
@@ -305,6 +306,14 @@ class ContractManager {
305
306
  this.eventCallbacks.delete(callback);
306
307
  };
307
308
  }
309
+ /**
310
+ * Force a full VTXO refresh from the indexer for all contracts.
311
+ * Populates the wallet repository with complete VTXO history.
312
+ */
313
+ async refreshVtxos() {
314
+ const contracts = await this.config.contractRepository.getContracts();
315
+ await this.fetchContractVxosFromIndexer(contracts, true);
316
+ }
308
317
  /**
309
318
  * Check if currently watching.
310
319
  */
@@ -334,11 +343,13 @@ class ContractManager {
334
343
  case "vtxo_spent":
335
344
  await this.fetchContractVxosFromIndexer([event.contract], true);
336
345
  break;
337
- case "connection_reset":
338
- // Refetch all VTXOs for all active contracts
346
+ case "connection_reset": {
347
+ // Refetch all VTXOs (including spent/swept) for all active
348
+ // contracts so the repo stays consistent with bootstrap state
339
349
  const activeWatchedContracts = this.watcher.getActiveContracts();
340
- await this.fetchContractVxosFromIndexer(activeWatchedContracts, false);
350
+ await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
341
351
  break;
352
+ }
342
353
  case "contract_expired":
343
354
  // just update DB
344
355
  await this.config.contractRepository.saveContract(event.contract);
@@ -365,11 +376,48 @@ class ContractManager {
365
376
  return result;
366
377
  }
367
378
  async fetchContractVtxosBulk(contracts, includeSpent) {
368
- const result = new Map();
369
- await Promise.all(contracts.map(async (contract) => {
379
+ if (contracts.length === 0) {
380
+ return new Map();
381
+ }
382
+ // For a single contract, use the paginated path directly.
383
+ if (contracts.length === 1) {
384
+ const contract = contracts[0];
370
385
  const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
371
- result.set(contract.script, vtxos);
372
- }));
386
+ return new Map([[contract.script, vtxos]]);
387
+ }
388
+ // For multiple contracts, batch all scripts into a single indexer call
389
+ // per page to minimise round-trips. Results are keyed by script so we
390
+ // can distribute them back to the correct contract afterwards.
391
+ const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
392
+ const result = new Map(contracts.map((c) => [c.script, []]));
393
+ const scripts = contracts.map((c) => c.script);
394
+ const pageSize = 100;
395
+ const opts = includeSpent ? {} : { spendableOnly: true };
396
+ let pageIndex = 0;
397
+ let hasMore = true;
398
+ while (hasMore) {
399
+ const { vtxos, page } = await this.config.indexerProvider.getVtxos({
400
+ scripts,
401
+ ...opts,
402
+ pageIndex,
403
+ pageSize,
404
+ });
405
+ for (const vtxo of vtxos) {
406
+ // Match the VTXO back to its contract via the script field
407
+ // populated by the indexer.
408
+ if (!vtxo.script)
409
+ continue;
410
+ const contract = scriptToContract.get(vtxo.script);
411
+ if (!contract)
412
+ continue;
413
+ result.get(contract.script).push({
414
+ ...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
415
+ contractScript: contract.script,
416
+ });
417
+ }
418
+ hasMore = page ? vtxos.length === pageSize : false;
419
+ pageIndex++;
420
+ }
373
421
  return result;
374
422
  }
375
423
  async fetchContractVtxosPaginated(contract, includeSpent) {
@@ -413,8 +413,27 @@ class ContractWatcher {
413
413
  }
414
414
  return;
415
415
  }
416
- this.subscriptionId =
417
- await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
416
+ try {
417
+ this.subscriptionId =
418
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
419
+ }
420
+ catch (error) {
421
+ // If we sent a stale subscription ID that the server no longer
422
+ // recognises, clear it and retry to create a fresh subscription.
423
+ // The server currently returns HTTP 500 with a JSON body whose
424
+ // message field looks like "subscription <uuid> not found".
425
+ // All other errors (network failures, parse errors, etc.) are rethrown.
426
+ const isStale = error instanceof Error &&
427
+ /subscription\s+\S+\s+not\s+found/i.test(error.message);
428
+ if (this.subscriptionId && isStale) {
429
+ this.subscriptionId = undefined;
430
+ this.subscriptionId =
431
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
432
+ }
433
+ else {
434
+ throw error;
435
+ }
436
+ }
418
437
  }
419
438
  /**
420
439
  * Main listening loop for subscription events.
package/dist/cjs/index.js CHANGED
@@ -36,9 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.ServiceWorkerReadonlyWallet = exports.ServiceWorkerWallet = exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = exports.WalletMessageHandler = exports.MessageBus = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DelegateVtxo = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.RestDelegatorProvider = exports.DelegatorManagerImpl = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = exports.ReadonlySingleKey = exports.SingleKey = exports.ReadonlyWallet = exports.Wallet = exports.asset = void 0;
40
- exports.contractFromArkContractWithAddress = exports.contractFromArkContract = exports.decodeArkContract = exports.encodeArkContract = exports.VHTLCContractHandler = exports.DelegateContractHandler = exports.DefaultContractHandler = exports.contractHandlers = exports.ContractWatcher = exports.ContractManager = exports.getSequence = exports.isExpired = exports.isSubdust = exports.isSpendable = exports.isRecoverable = exports.buildForfeitTx = exports.validateConnectorsTxGraph = exports.validateVtxoTxGraph = exports.Batch = exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.BIP322 = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.rollbackMigration = exports.getMigrationStatus = exports.requiresMigration = exports.migrateWalletRepository = exports.MIGRATION_KEY = exports.InMemoryContractRepository = exports.InMemoryWalletRepository = exports.IndexedDBContractRepository = exports.IndexedDBWalletRepository = exports.openDatabase = exports.closeDatabase = exports.networks = exports.ArkNote = exports.isValidArkAddress = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = void 0;
41
- exports.isArkContract = void 0;
39
+ exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.ServiceWorkerReadonlyWallet = exports.ServiceWorkerWallet = exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.MESSAGE_BUS_NOT_INITIALIZED = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = exports.WalletMessageHandler = exports.MessageBus = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DelegateVtxo = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.RestDelegatorProvider = exports.DelegatorManagerImpl = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = exports.ReadonlySingleKey = exports.SingleKey = exports.ReadonlyWallet = exports.Wallet = exports.asset = void 0;
40
+ exports.contractFromArkContract = exports.decodeArkContract = exports.encodeArkContract = exports.VHTLCContractHandler = exports.DelegateContractHandler = exports.DefaultContractHandler = exports.contractHandlers = exports.ContractWatcher = exports.ContractManager = exports.getSequence = exports.isExpired = exports.isSubdust = exports.isSpendable = exports.isRecoverable = exports.buildForfeitTx = exports.validateConnectorsTxGraph = exports.validateVtxoTxGraph = exports.Batch = exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.BIP322 = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.rollbackMigration = exports.getMigrationStatus = exports.requiresMigration = exports.migrateWalletRepository = exports.MIGRATION_KEY = exports.InMemoryContractRepository = exports.InMemoryWalletRepository = exports.IndexedDBContractRepository = exports.IndexedDBWalletRepository = exports.openDatabase = exports.closeDatabase = exports.networks = exports.ArkNote = exports.isValidArkAddress = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = void 0;
41
+ exports.isArkContract = exports.contractFromArkContractWithAddress = void 0;
42
42
  const transaction_1 = require("./utils/transaction");
43
43
  Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
44
44
  const singleKey_1 = require("./identity/singleKey");
@@ -180,5 +180,6 @@ Object.defineProperty(exports, "WalletNotInitializedError", { enumerable: true,
180
180
  Object.defineProperty(exports, "ReadonlyWalletError", { enumerable: true, get: function () { return wallet_message_handler_1.ReadonlyWalletError; } });
181
181
  Object.defineProperty(exports, "DelegatorNotConfiguredError", { enumerable: true, get: function () { return wallet_message_handler_1.DelegatorNotConfiguredError; } });
182
182
  const errors_2 = require("./worker/errors");
183
+ Object.defineProperty(exports, "MESSAGE_BUS_NOT_INITIALIZED", { enumerable: true, get: function () { return errors_2.MESSAGE_BUS_NOT_INITIALIZED; } });
183
184
  Object.defineProperty(exports, "MessageBusNotInitializedError", { enumerable: true, get: function () { return errors_2.MessageBusNotInitializedError; } });
184
185
  Object.defineProperty(exports, "ServiceWorkerTimeoutError", { enumerable: true, get: function () { return errors_2.ServiceWorkerTimeoutError; } });
@@ -31,6 +31,7 @@ function convertVtxo(vtxo) {
31
31
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
32
32
  isUnrolled: vtxo.isUnrolled,
33
33
  isSpent: vtxo.isSpent,
34
+ script: vtxo.script,
34
35
  assets: vtxo.assets?.map((a) => ({
35
36
  assetId: a.assetId,
36
37
  amount: Number(a.amount),
@@ -406,6 +406,7 @@ function convertVtxo(vtxo) {
406
406
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
407
407
  isUnrolled: vtxo.isUnrolled,
408
408
  isSpent: vtxo.isSpent,
409
+ script: vtxo.script,
409
410
  assets: vtxo.assets?.map((a) => ({
410
411
  assetId: a.assetId,
411
412
  amount: Number(a.amount),
@@ -15,6 +15,7 @@ const anchor_1 = require("./anchor");
15
15
  const unknownFields_1 = require("./unknownFields");
16
16
  const transaction_1 = require("./transaction");
17
17
  const address_1 = require("../script/address");
18
+ const extension_1 = require("../extension");
18
19
  /**
19
20
  * Builds an offchain transaction with checkpoint transactions.
20
21
  *
@@ -28,16 +29,26 @@ const address_1 = require("../script/address");
28
29
  * @returns Object containing the virtual transaction and checkpoint transactions
29
30
  */
30
31
  function buildOffchainTx(inputs, outputs, serverUnrollScript) {
31
- let hasOpReturn = false;
32
+ // TODO: use arkd /info
33
+ const MAX_OP_RETURN = 2;
34
+ let countOpReturn = 0;
35
+ let hasExtensionOutput = false;
32
36
  for (const [index, output] of outputs.entries()) {
33
37
  if (!output.script)
34
38
  throw new Error(`missing output script ${index}`);
35
- const isOpReturn = btc_signer_1.Script.decode(output.script)[0] === "RETURN";
36
- if (!isOpReturn)
39
+ const isExtension = extension_1.Extension.isExtension(output.script);
40
+ const isOpReturn = isExtension || btc_signer_1.Script.decode(output.script)[0] === "RETURN";
41
+ if (isOpReturn) {
42
+ countOpReturn++;
43
+ }
44
+ if (!isExtension)
37
45
  continue;
38
- if (hasOpReturn)
39
- throw new Error("multiple OP_RETURN outputs");
40
- hasOpReturn = true;
46
+ if (hasExtensionOutput)
47
+ throw new Error("multiple extension outputs");
48
+ hasExtensionOutput = true;
49
+ }
50
+ if (countOpReturn > MAX_OP_RETURN) {
51
+ throw new Error(`too many OP_RETURN outputs: ${countOpReturn} > ${MAX_OP_RETURN}`);
41
52
  }
42
53
  const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
43
54
  const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
@@ -118,7 +118,8 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
118
118
  txAmount = spentAmount;
119
119
  // TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
120
120
  txTime = getTxCreatedAt
121
- ? await getTxCreatedAt(vtxo.arkTxId)
121
+ ? ((await getTxCreatedAt(vtxo.arkTxId)) ??
122
+ vtxo.createdAt.getTime() + 1)
122
123
  : vtxo.createdAt.getTime() + 1;
123
124
  }
124
125
  const assets = subtractAssets(allSpent, changes);
@@ -4,6 +4,7 @@ exports.WalletMessageHandler = exports.DEFAULT_MESSAGE_TAG = exports.DelegatorNo
4
4
  const indexer_1 = require("../../providers/indexer");
5
5
  const index_1 = require("../index");
6
6
  const utils_1 = require("../utils");
7
+ const transactionHistory_1 = require("../../utils/transactionHistory");
7
8
  class WalletNotInitializedError extends Error {
8
9
  constructor() {
9
10
  super("Wallet handler not initialized");
@@ -175,7 +176,8 @@ class WalletMessageHandler {
175
176
  });
176
177
  }
177
178
  case "GET_TRANSACTION_HISTORY": {
178
- const transactions = await this.readonlyWallet.getTransactionHistory();
179
+ const allVtxos = await this.getVtxosFromRepo();
180
+ const transactions = (await this.buildTransactionHistoryFromCache(allVtxos)) ?? [];
179
181
  return this.tagged({
180
182
  id,
181
183
  type: "TRANSACTION_HISTORY",
@@ -202,7 +204,7 @@ class WalletMessageHandler {
202
204
  });
203
205
  }
204
206
  case "RELOAD_WALLET": {
205
- await this.onWalletInitialized();
207
+ await this.reloadWallet();
206
208
  return this.tagged({
207
209
  id,
208
210
  type: "RELOAD_SUCCESS",
@@ -288,6 +290,14 @@ class WalletMessageHandler {
288
290
  payload: { isWatching },
289
291
  });
290
292
  }
293
+ case "REFRESH_VTXOS": {
294
+ const manager = await this.readonlyWallet.getContractManager();
295
+ await manager.refreshVtxos();
296
+ return this.tagged({
297
+ id,
298
+ type: "REFRESH_VTXOS_SUCCESS",
299
+ });
300
+ }
291
301
  case "SEND": {
292
302
  const { recipients } = message.payload;
293
303
  const txid = await this.wallet.send(...recipients);
@@ -444,10 +454,9 @@ class WalletMessageHandler {
444
454
  await this.onWalletInitialized();
445
455
  }
446
456
  async handleGetBalance() {
447
- const [boardingUtxos, spendableVtxos, sweptVtxos] = await Promise.all([
457
+ const [boardingUtxos, allVtxos] = await Promise.all([
448
458
  this.getAllBoardingUtxos(),
449
- this.getSpendableVtxos(),
450
- this.getSweptVtxos(),
459
+ this.getVtxosFromRepo(),
451
460
  ]);
452
461
  // boarding
453
462
  let confirmed = 0;
@@ -460,7 +469,9 @@ class WalletMessageHandler {
460
469
  unconfirmed += utxo.value;
461
470
  }
462
471
  }
463
- // offchain
472
+ // offchain — split spendable vs swept from single repo read
473
+ const spendableVtxos = allVtxos.filter(index_1.isSpendable);
474
+ const sweptVtxos = allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
464
475
  let settled = 0;
465
476
  let preconfirmed = 0;
466
477
  let recoverable = 0;
@@ -510,23 +521,12 @@ class WalletMessageHandler {
510
521
  return this.readonlyWallet.getBoardingUtxos();
511
522
  }
512
523
  /**
513
- * Get spendable vtxos for the current wallet address
524
+ * Get spendable vtxos from the repository
514
525
  */
515
526
  async getSpendableVtxos() {
516
- if (!this.readonlyWallet)
517
- return [];
518
- const vtxos = await this.readonlyWallet.getVtxos();
527
+ const vtxos = await this.getVtxosFromRepo();
519
528
  return vtxos.filter(index_1.isSpendable);
520
529
  }
521
- /**
522
- * Get swept vtxos for the current wallet address
523
- */
524
- async getSweptVtxos() {
525
- if (!this.readonlyWallet)
526
- return [];
527
- const vtxos = await this.readonlyWallet.getVtxos();
528
- return vtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
529
- }
530
530
  async onWalletInitialized() {
531
531
  if (!this.readonlyWallet ||
532
532
  !this.arkProvider ||
@@ -534,10 +534,11 @@ class WalletMessageHandler {
534
534
  !this.walletRepository) {
535
535
  return;
536
536
  }
537
- // Get all wallet scripts (current + historical delegate/non-delegate)
538
- const scripts = await this.readonlyWallet.getWalletScripts();
539
- const response = await this.indexerProvider.getVtxos({ scripts });
540
- const vtxos = response.vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this.readonlyWallet, vtxo));
537
+ // Initialize contract manager FIRST this populates the repository
538
+ // with full VTXO history for all contracts (one indexer call per contract)
539
+ await this.ensureContractEventBroadcasting();
540
+ // Read VTXOs from repository (now populated by contract manager)
541
+ const vtxos = await this.getVtxosFromRepo();
541
542
  if (this.wallet) {
542
543
  try {
543
544
  // recover pending transactions if possible
@@ -549,15 +550,13 @@ class WalletMessageHandler {
549
550
  console.error("Error recovering pending transactions:", error);
550
551
  }
551
552
  }
552
- // Get wallet address and save vtxos using unified repository
553
- const address = await this.readonlyWallet.getAddress();
554
- await this.walletRepository.saveVtxos(address, vtxos);
555
553
  // Fetch boarding utxos and save using unified repository
556
554
  const boardingAddress = await this.readonlyWallet.getBoardingAddress();
557
555
  const coins = await this.readonlyWallet.onchainProvider.getCoins(boardingAddress);
558
556
  await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => (0, utils_1.extendCoin)(this.readonlyWallet, utxo)));
559
- // Get transaction history to cache boarding txs
560
- const txs = await this.readonlyWallet.getTransactionHistory();
557
+ // Build transaction history from cached VTXOs (no indexer call)
558
+ const address = await this.readonlyWallet.getAddress();
559
+ const txs = await this.buildTransactionHistoryFromCache(vtxos);
561
560
  if (txs)
562
561
  await this.walletRepository.saveTransactions(address, txs);
563
562
  // unsubscribe previous subscription if any
@@ -602,7 +601,6 @@ class WalletMessageHandler {
602
601
  }));
603
602
  }
604
603
  });
605
- await this.ensureContractEventBroadcasting();
606
604
  // Eagerly start the VtxoManager so its background tasks (auto-renewal,
607
605
  // boarding UTXO polling/sweep) run inside the service worker without
608
606
  // waiting for a client to send a vtxo-manager message first.
@@ -615,6 +613,17 @@ class WalletMessageHandler {
615
613
  }
616
614
  }
617
615
  }
616
+ /**
617
+ * Force a full VTXO refresh from the indexer, then re-run bootstrap.
618
+ * Used by RELOAD_WALLET to ensure fresh data.
619
+ */
620
+ async reloadWallet() {
621
+ if (!this.readonlyWallet)
622
+ return;
623
+ const manager = await this.readonlyWallet.getContractManager();
624
+ await manager.refreshVtxos();
625
+ await this.onWalletInitialized();
626
+ }
618
627
  async handleSettle(message) {
619
628
  const wallet = this.requireWallet();
620
629
  const txid = await wallet.settle(message.payload.params, (e) => {
@@ -738,6 +747,77 @@ class WalletMessageHandler {
738
747
  this.arkProvider = undefined;
739
748
  this.indexerProvider = undefined;
740
749
  }
750
+ /**
751
+ * Read all VTXOs from the repository, aggregated across all contract
752
+ * addresses and the wallet's primary address, with deduplication.
753
+ */
754
+ async getVtxosFromRepo() {
755
+ if (!this.walletRepository || !this.readonlyWallet)
756
+ return [];
757
+ const seen = new Set();
758
+ const allVtxos = [];
759
+ const addVtxos = (vtxos) => {
760
+ for (const vtxo of vtxos) {
761
+ const key = `${vtxo.txid}:${vtxo.vout}`;
762
+ if (!seen.has(key)) {
763
+ seen.add(key);
764
+ allVtxos.push(vtxo);
765
+ }
766
+ }
767
+ };
768
+ // Aggregate VTXOs from all contract addresses
769
+ const manager = await this.readonlyWallet.getContractManager();
770
+ const contracts = await manager.getContracts();
771
+ for (const contract of contracts) {
772
+ const vtxos = await this.walletRepository.getVtxos(contract.address);
773
+ addVtxos(vtxos);
774
+ }
775
+ // Also check the wallet's primary address
776
+ const walletAddress = await this.readonlyWallet.getAddress();
777
+ const walletVtxos = await this.walletRepository.getVtxos(walletAddress);
778
+ addVtxos(walletVtxos);
779
+ return allVtxos;
780
+ }
781
+ /**
782
+ * Build transaction history from cached VTXOs without hitting the indexer.
783
+ * Falls back to indexer only for uncached transaction timestamps.
784
+ */
785
+ async buildTransactionHistoryFromCache(vtxos) {
786
+ if (!this.readonlyWallet)
787
+ return null;
788
+ const { boardingTxs, commitmentsToIgnore } = await this.readonlyWallet.getBoardingTxs();
789
+ // Build a lookup for cached VTXO timestamps, keyed by txid.
790
+ // Multiple VTXOs can share a txid (different vouts) — we keep the
791
+ // earliest createdAt so the history ordering is stable.
792
+ const vtxoCreatedAt = new Map();
793
+ for (const vtxo of vtxos) {
794
+ const existing = vtxoCreatedAt.get(vtxo.txid);
795
+ const ts = vtxo.createdAt.getTime();
796
+ if (existing === undefined || ts < existing) {
797
+ vtxoCreatedAt.set(vtxo.txid, ts);
798
+ }
799
+ }
800
+ // getTxCreatedAt resolves the creation timestamp of a transaction.
801
+ // buildTransactionHistory calls this for spent-offchain VTXOs with
802
+ // no change outputs to determine the time of the sending tx.
803
+ // Returns undefined on miss so buildTransactionHistory uses its
804
+ // own fallback (vtxo.createdAt + 1) rather than epoch 0.
805
+ // The vout:0 lookup in the indexer fallback mirrors the pre-existing
806
+ // convention in ReadonlyWallet.getTransactionHistory().
807
+ const getTxCreatedAt = async (txid) => {
808
+ const cached = vtxoCreatedAt.get(txid);
809
+ if (cached !== undefined)
810
+ return cached;
811
+ if (this.indexerProvider) {
812
+ const res = await this.indexerProvider.getVtxos({
813
+ outpoints: [{ txid, vout: 0 }],
814
+ });
815
+ return res.vtxos[0]?.createdAt.getTime();
816
+ }
817
+ return undefined;
818
+ };
819
+ return (0, transactionHistory_1.buildTransactionHistory)(vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
820
+ }
741
821
  async ensureContractEventBroadcasting() {
742
822
  if (!this.readonlyWallet)
743
823
  return;
@@ -7,11 +7,12 @@ const repositories_1 = require("../../repositories");
7
7
  const wallet_message_handler_1 = require("./wallet-message-handler");
8
8
  const utils_2 = require("../utils");
9
9
  const errors_1 = require("../../worker/errors");
10
- // Check by error name instead of instanceof because postMessage uses the
10
+ // Check by error message content instead of instanceof because postMessage uses the
11
11
  // structured clone algorithm which strips the prototype chain — the page
12
12
  // receives a plain Error, not the original MessageBusNotInitializedError.
13
13
  function isMessageBusNotInitializedError(error) {
14
- return (error instanceof Error && error.name === "MessageBusNotInitializedError");
14
+ return (error instanceof Error &&
15
+ error.message.includes(errors_1.MESSAGE_BUS_NOT_INITIALIZED));
15
16
  }
16
17
  const DEDUPABLE_REQUEST_TYPES = new Set([
17
18
  "GET_ADDRESS",
@@ -161,7 +162,10 @@ class ServiceWorkerReadonlyWallet {
161
162
  publicKey: initConfig.arkServerPublicKey,
162
163
  },
163
164
  delegatorUrl: initConfig.delegatorUrl,
165
+ indexerUrl: options.indexerUrl,
166
+ esploraUrl: options.esploraUrl,
164
167
  timeoutMs: options.messageBusTimeoutMs,
168
+ watcherConfig: options.watcherConfig,
165
169
  }, options.messageBusTimeoutMs);
166
170
  // Initialize the wallet handler
167
171
  const initMessage = {
@@ -178,6 +182,9 @@ class ServiceWorkerReadonlyWallet {
178
182
  publicKey: initConfig.arkServerPublicKey,
179
183
  },
180
184
  delegatorUrl: initConfig.delegatorUrl,
185
+ indexerUrl: options.indexerUrl,
186
+ esploraUrl: options.esploraUrl,
187
+ watcherConfig: options.watcherConfig,
181
188
  };
182
189
  wallet.initWalletPayload = initConfig;
183
190
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -678,6 +685,14 @@ class ServiceWorkerReadonlyWallet {
678
685
  navigator.serviceWorker.removeEventListener("message", messageHandler);
679
686
  };
680
687
  },
688
+ async refreshVtxos() {
689
+ const message = {
690
+ type: "REFRESH_VTXOS",
691
+ id: (0, utils_2.getRandomId)(),
692
+ tag: messageTag,
693
+ };
694
+ await sendContractMessage(message);
695
+ },
681
696
  async isWatching() {
682
697
  const message = {
683
698
  type: "IS_CONTRACT_MANAGER_WATCHING",
@@ -748,7 +763,11 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
748
763
  publicKey: initConfig.arkServerPublicKey,
749
764
  },
750
765
  delegatorUrl: initConfig.delegatorUrl,
766
+ indexerUrl: options.indexerUrl,
767
+ esploraUrl: options.esploraUrl,
751
768
  timeoutMs: options.messageBusTimeoutMs,
769
+ settlementConfig: options.settlementConfig,
770
+ watcherConfig: options.watcherConfig,
752
771
  }, options.messageBusTimeoutMs);
753
772
  // Initialize the service worker with the config
754
773
  const initMessage = {
@@ -766,6 +785,10 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
766
785
  publicKey: initConfig.arkServerPublicKey,
767
786
  },
768
787
  delegatorUrl: initConfig.delegatorUrl,
788
+ indexerUrl: options.indexerUrl,
789
+ esploraUrl: options.esploraUrl,
790
+ settlementConfig: options.settlementConfig,
791
+ watcherConfig: options.watcherConfig,
769
792
  };
770
793
  wallet.initWalletPayload = initConfig;
771
794
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;