@arkade-os/sdk 0.4.8 → 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 (29) hide show
  1. package/dist/cjs/contracts/contractManager.js +59 -11
  2. package/dist/cjs/contracts/contractWatcher.js +21 -2
  3. package/dist/cjs/providers/expoIndexer.js +1 -0
  4. package/dist/cjs/providers/indexer.js +1 -0
  5. package/dist/cjs/utils/transactionHistory.js +2 -1
  6. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +109 -29
  7. package/dist/cjs/wallet/serviceWorker/wallet.js +22 -0
  8. package/dist/cjs/wallet/vtxo-manager.js +81 -50
  9. package/dist/cjs/wallet/wallet.js +46 -34
  10. package/dist/cjs/worker/messageBus.js +7 -0
  11. package/dist/esm/contracts/contractManager.js +59 -11
  12. package/dist/esm/contracts/contractWatcher.js +21 -2
  13. package/dist/esm/providers/expoIndexer.js +1 -0
  14. package/dist/esm/providers/indexer.js +1 -0
  15. package/dist/esm/utils/transactionHistory.js +2 -1
  16. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +109 -29
  17. package/dist/esm/wallet/serviceWorker/wallet.js +22 -0
  18. package/dist/esm/wallet/vtxo-manager.js +81 -50
  19. package/dist/esm/wallet/wallet.js +46 -34
  20. package/dist/esm/worker/messageBus.js +7 -0
  21. package/dist/types/contracts/contractManager.d.ts +10 -0
  22. package/dist/types/repositories/serialization.d.ts +1 -0
  23. package/dist/types/utils/transactionHistory.d.ts +1 -1
  24. package/dist/types/wallet/index.d.ts +2 -0
  25. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +23 -6
  26. package/dist/types/wallet/serviceWorker/wallet.d.ts +9 -1
  27. package/dist/types/wallet/vtxo-manager.d.ts +5 -0
  28. package/dist/types/worker/messageBus.d.ts +6 -0
  29. package/package.json +1 -1
@@ -278,27 +278,33 @@ class ReadonlyWallet {
278
278
  const scriptMap = await this.getScriptMap();
279
279
  const f = filter ?? { withRecoverable: true, withUnrolled: false };
280
280
  const allExtended = [];
281
- // Query each script separately so we can extend VTXOs with the correct tapscript
282
- for (const [scriptHex, vtxoScript] of scriptMap) {
283
- const response = await this.indexerProvider.getVtxos({
284
- scripts: [scriptHex],
285
- });
286
- let vtxos = response.vtxos.filter(_1.isSpendable);
287
- if (!f.withRecoverable) {
288
- vtxos = vtxos.filter((vtxo) => !(0, _1.isRecoverable)(vtxo) && !(0, _1.isExpired)(vtxo));
289
- }
290
- if (f.withUnrolled) {
291
- const spentVtxos = response.vtxos.filter((vtxo) => !(0, _1.isSpendable)(vtxo));
292
- vtxos.push(...spentVtxos.filter((vtxo) => vtxo.isUnrolled));
281
+ // Batch all scripts into a single indexer call
282
+ const allScripts = [...scriptMap.keys()];
283
+ const response = await this.indexerProvider.getVtxos({
284
+ scripts: allScripts,
285
+ });
286
+ for (const vtxo of response.vtxos) {
287
+ const vtxoScript = vtxo.script
288
+ ? scriptMap.get(vtxo.script)
289
+ : undefined;
290
+ if (!vtxoScript)
291
+ continue;
292
+ if ((0, _1.isSpendable)(vtxo)) {
293
+ if (!f.withRecoverable &&
294
+ ((0, _1.isRecoverable)(vtxo) || (0, _1.isExpired)(vtxo))) {
295
+ continue;
296
+ }
293
297
  }
294
- for (const vtxo of vtxos) {
295
- allExtended.push({
296
- ...vtxo,
297
- forfeitTapLeafScript: vtxoScript.forfeit(),
298
- intentTapLeafScript: vtxoScript.forfeit(),
299
- tapTree: vtxoScript.encode(),
300
- });
298
+ else {
299
+ if (!f.withUnrolled || !vtxo.isUnrolled)
300
+ continue;
301
301
  }
302
+ allExtended.push({
303
+ ...vtxo,
304
+ forfeitTapLeafScript: vtxoScript.forfeit(),
305
+ intentTapLeafScript: vtxoScript.forfeit(),
306
+ tapTree: vtxoScript.encode(),
307
+ });
302
308
  }
303
309
  // Update cache with fresh data
304
310
  await this.walletRepository.saveVtxos(address, allExtended);
@@ -310,7 +316,7 @@ class ReadonlyWallet {
310
316
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
311
317
  const getTxCreatedAt = (txid) => this.indexerProvider
312
318
  .getVtxos({ outpoints: [{ txid, vout: 0 }] })
313
- .then((res) => res.vtxos[0]?.createdAt.getTime() || 0);
319
+ .then((res) => res.vtxos[0]?.createdAt.getTime());
314
320
  return (0, transactionHistory_1.buildTransactionHistory)(response.vtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
315
321
  }
316
322
  async getBoardingTxs() {
@@ -1344,23 +1350,29 @@ class Wallet extends ReadonlyWallet {
1344
1350
  async finalizePendingTxs(vtxos) {
1345
1351
  const MAX_INPUTS_PER_INTENT = 20;
1346
1352
  if (!vtxos || vtxos.length === 0) {
1347
- // Query per-script so each VTXO is extended with the correct tapscript
1353
+ // Batch all scripts into a single indexer call
1348
1354
  const scriptMap = await this.getScriptMap();
1349
1355
  const allExtended = [];
1350
- for (const [scriptHex, vtxoScript] of scriptMap) {
1351
- const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1352
- scripts: [scriptHex],
1353
- });
1354
- const pending = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
1355
- vtxo.virtualStatus.state !== "settled");
1356
- for (const vtxo of pending) {
1357
- allExtended.push({
1358
- ...vtxo,
1359
- forfeitTapLeafScript: vtxoScript.forfeit(),
1360
- intentTapLeafScript: vtxoScript.forfeit(),
1361
- tapTree: vtxoScript.encode(),
1362
- });
1356
+ const allScripts = [...scriptMap.keys()];
1357
+ const { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
1358
+ scripts: allScripts,
1359
+ });
1360
+ for (const vtxo of fetchedVtxos) {
1361
+ const vtxoScript = vtxo.script
1362
+ ? scriptMap.get(vtxo.script)
1363
+ : undefined;
1364
+ if (!vtxoScript)
1365
+ continue;
1366
+ if (vtxo.virtualStatus.state === "swept" ||
1367
+ vtxo.virtualStatus.state === "settled") {
1368
+ continue;
1363
1369
  }
1370
+ allExtended.push({
1371
+ ...vtxo,
1372
+ forfeitTapLeafScript: vtxoScript.forfeit(),
1373
+ intentTapLeafScript: vtxoScript.forfeit(),
1374
+ tapTree: vtxoScript.encode(),
1375
+ });
1364
1376
  }
1365
1377
  if (allExtended.length === 0) {
1366
1378
  return { finalized: [], pending: [] };
@@ -152,8 +152,12 @@ class MessageBus {
152
152
  identity,
153
153
  arkServerUrl: config.arkServer.url,
154
154
  arkServerPublicKey: config.arkServer.publicKey,
155
+ indexerUrl: config.indexerUrl,
156
+ esploraUrl: config.esploraUrl,
155
157
  storage,
156
158
  delegatorProvider,
159
+ settlementConfig: config.settlementConfig,
160
+ watcherConfig: config.watcherConfig,
157
161
  });
158
162
  return { wallet, arkProvider, readonlyWallet: wallet };
159
163
  }
@@ -163,8 +167,11 @@ class MessageBus {
163
167
  identity,
164
168
  arkServerUrl: config.arkServer.url,
165
169
  arkServerPublicKey: config.arkServer.publicKey,
170
+ indexerUrl: config.indexerUrl,
171
+ esploraUrl: config.esploraUrl,
166
172
  storage,
167
173
  delegatorProvider,
174
+ watcherConfig: config.watcherConfig,
168
175
  });
169
176
  return { readonlyWallet, arkProvider };
170
177
  }
@@ -71,9 +71,10 @@ export class ContractManager {
71
71
  }
72
72
  // Load persisted contracts
73
73
  const contracts = await this.config.contractRepository.getContracts();
74
- // fetch latest VTXOs for all contracts, ensure cache is up to date
74
+ // fetch all VTXOs (including spent/swept) for all contracts,
75
+ // so the repository has full history for transaction history and balance
75
76
  // TODO: what if the user has 1k contracts?
76
- await this.getVtxosForContracts(contracts);
77
+ await this.fetchContractVxosFromIndexer(contracts, true);
77
78
  // add all contracts to the watcher
78
79
  const now = Date.now();
79
80
  for (const contract of contracts) {
@@ -136,8 +137,8 @@ export class ContractManager {
136
137
  };
137
138
  // Persist
138
139
  await this.config.contractRepository.saveContract(contract);
139
- // ensure we have the latest VTXOs for this contract
140
- await this.getVtxosForContracts([contract]);
140
+ // fetch all VTXOs (including spent/swept) for this contract
141
+ await this.fetchContractVxosFromIndexer([contract], true);
141
142
  // Add to watcher
142
143
  await this.watcher.addContract(contract);
143
144
  return contract;
@@ -302,6 +303,14 @@ export class ContractManager {
302
303
  this.eventCallbacks.delete(callback);
303
304
  };
304
305
  }
306
+ /**
307
+ * Force a full VTXO refresh from the indexer for all contracts.
308
+ * Populates the wallet repository with complete VTXO history.
309
+ */
310
+ async refreshVtxos() {
311
+ const contracts = await this.config.contractRepository.getContracts();
312
+ await this.fetchContractVxosFromIndexer(contracts, true);
313
+ }
305
314
  /**
306
315
  * Check if currently watching.
307
316
  */
@@ -331,11 +340,13 @@ export class ContractManager {
331
340
  case "vtxo_spent":
332
341
  await this.fetchContractVxosFromIndexer([event.contract], true);
333
342
  break;
334
- case "connection_reset":
335
- // Refetch all VTXOs for all active contracts
343
+ case "connection_reset": {
344
+ // Refetch all VTXOs (including spent/swept) for all active
345
+ // contracts so the repo stays consistent with bootstrap state
336
346
  const activeWatchedContracts = this.watcher.getActiveContracts();
337
- await this.fetchContractVxosFromIndexer(activeWatchedContracts, false);
347
+ await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
338
348
  break;
349
+ }
339
350
  case "contract_expired":
340
351
  // just update DB
341
352
  await this.config.contractRepository.saveContract(event.contract);
@@ -362,11 +373,48 @@ export class ContractManager {
362
373
  return result;
363
374
  }
364
375
  async fetchContractVtxosBulk(contracts, includeSpent) {
365
- const result = new Map();
366
- await Promise.all(contracts.map(async (contract) => {
376
+ if (contracts.length === 0) {
377
+ return new Map();
378
+ }
379
+ // For a single contract, use the paginated path directly.
380
+ if (contracts.length === 1) {
381
+ const contract = contracts[0];
367
382
  const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent);
368
- result.set(contract.script, vtxos);
369
- }));
383
+ return new Map([[contract.script, vtxos]]);
384
+ }
385
+ // For multiple contracts, batch all scripts into a single indexer call
386
+ // per page to minimise round-trips. Results are keyed by script so we
387
+ // can distribute them back to the correct contract afterwards.
388
+ const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
389
+ const result = new Map(contracts.map((c) => [c.script, []]));
390
+ const scripts = contracts.map((c) => c.script);
391
+ const pageSize = 100;
392
+ const opts = includeSpent ? {} : { spendableOnly: true };
393
+ let pageIndex = 0;
394
+ let hasMore = true;
395
+ while (hasMore) {
396
+ const { vtxos, page } = await this.config.indexerProvider.getVtxos({
397
+ scripts,
398
+ ...opts,
399
+ pageIndex,
400
+ pageSize,
401
+ });
402
+ for (const vtxo of vtxos) {
403
+ // Match the VTXO back to its contract via the script field
404
+ // populated by the indexer.
405
+ if (!vtxo.script)
406
+ continue;
407
+ const contract = scriptToContract.get(vtxo.script);
408
+ if (!contract)
409
+ continue;
410
+ result.get(contract.script).push({
411
+ ...extendVtxoFromContract(vtxo, contract),
412
+ contractScript: contract.script,
413
+ });
414
+ }
415
+ hasMore = page ? vtxos.length === pageSize : false;
416
+ pageIndex++;
417
+ }
370
418
  return result;
371
419
  }
372
420
  async fetchContractVtxosPaginated(contract, includeSpent) {
@@ -410,8 +410,27 @@ export class ContractWatcher {
410
410
  }
411
411
  return;
412
412
  }
413
- this.subscriptionId =
414
- await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
413
+ try {
414
+ this.subscriptionId =
415
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch, this.subscriptionId);
416
+ }
417
+ catch (error) {
418
+ // If we sent a stale subscription ID that the server no longer
419
+ // recognises, clear it and retry to create a fresh subscription.
420
+ // The server currently returns HTTP 500 with a JSON body whose
421
+ // message field looks like "subscription <uuid> not found".
422
+ // All other errors (network failures, parse errors, etc.) are rethrown.
423
+ const isStale = error instanceof Error &&
424
+ /subscription\s+\S+\s+not\s+found/i.test(error.message);
425
+ if (this.subscriptionId && isStale) {
426
+ this.subscriptionId = undefined;
427
+ this.subscriptionId =
428
+ await this.config.indexerProvider.subscribeForScripts(scriptsToWatch);
429
+ }
430
+ else {
431
+ throw error;
432
+ }
433
+ }
415
434
  }
416
435
  /**
417
436
  * Main listening loop for subscription events.
@@ -28,6 +28,7 @@ function convertVtxo(vtxo) {
28
28
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
29
29
  isUnrolled: vtxo.isUnrolled,
30
30
  isSpent: vtxo.isSpent,
31
+ script: vtxo.script,
31
32
  assets: vtxo.assets?.map((a) => ({
32
33
  assetId: a.assetId,
33
34
  amount: Number(a.amount),
@@ -402,6 +402,7 @@ function convertVtxo(vtxo) {
402
402
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
403
403
  isUnrolled: vtxo.isUnrolled,
404
404
  isSpent: vtxo.isSpent,
405
+ script: vtxo.script,
405
406
  assets: vtxo.assets?.map((a) => ({
406
407
  assetId: a.assetId,
407
408
  amount: Number(a.amount),
@@ -115,7 +115,8 @@ export async function buildTransactionHistory(vtxos, allBoardingTxs, commitments
115
115
  txAmount = spentAmount;
116
116
  // TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
117
117
  txTime = getTxCreatedAt
118
- ? await getTxCreatedAt(vtxo.arkTxId)
118
+ ? ((await getTxCreatedAt(vtxo.arkTxId)) ??
119
+ vtxo.createdAt.getTime() + 1)
119
120
  : vtxo.createdAt.getTime() + 1;
120
121
  }
121
122
  const assets = subtractAssets(allSpent, changes);
@@ -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;
@@ -158,7 +158,10 @@ export class ServiceWorkerReadonlyWallet {
158
158
  publicKey: initConfig.arkServerPublicKey,
159
159
  },
160
160
  delegatorUrl: initConfig.delegatorUrl,
161
+ indexerUrl: options.indexerUrl,
162
+ esploraUrl: options.esploraUrl,
161
163
  timeoutMs: options.messageBusTimeoutMs,
164
+ watcherConfig: options.watcherConfig,
162
165
  }, options.messageBusTimeoutMs);
163
166
  // Initialize the wallet handler
164
167
  const initMessage = {
@@ -175,6 +178,9 @@ export class ServiceWorkerReadonlyWallet {
175
178
  publicKey: initConfig.arkServerPublicKey,
176
179
  },
177
180
  delegatorUrl: initConfig.delegatorUrl,
181
+ indexerUrl: options.indexerUrl,
182
+ esploraUrl: options.esploraUrl,
183
+ watcherConfig: options.watcherConfig,
178
184
  };
179
185
  wallet.initWalletPayload = initConfig;
180
186
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
@@ -675,6 +681,14 @@ export class ServiceWorkerReadonlyWallet {
675
681
  navigator.serviceWorker.removeEventListener("message", messageHandler);
676
682
  };
677
683
  },
684
+ async refreshVtxos() {
685
+ const message = {
686
+ type: "REFRESH_VTXOS",
687
+ id: getRandomId(),
688
+ tag: messageTag,
689
+ };
690
+ await sendContractMessage(message);
691
+ },
678
692
  async isWatching() {
679
693
  const message = {
680
694
  type: "IS_CONTRACT_MANAGER_WATCHING",
@@ -744,7 +758,11 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
744
758
  publicKey: initConfig.arkServerPublicKey,
745
759
  },
746
760
  delegatorUrl: initConfig.delegatorUrl,
761
+ indexerUrl: options.indexerUrl,
762
+ esploraUrl: options.esploraUrl,
747
763
  timeoutMs: options.messageBusTimeoutMs,
764
+ settlementConfig: options.settlementConfig,
765
+ watcherConfig: options.watcherConfig,
748
766
  }, options.messageBusTimeoutMs);
749
767
  // Initialize the service worker with the config
750
768
  const initMessage = {
@@ -762,6 +780,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
762
780
  publicKey: initConfig.arkServerPublicKey,
763
781
  },
764
782
  delegatorUrl: initConfig.delegatorUrl,
783
+ indexerUrl: options.indexerUrl,
784
+ esploraUrl: options.esploraUrl,
785
+ settlementConfig: options.settlementConfig,
786
+ watcherConfig: options.watcherConfig,
765
787
  };
766
788
  wallet.initWalletPayload = initConfig;
767
789
  wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;