@arkade-os/sdk 0.4.17 → 0.4.18

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 (68) hide show
  1. package/README.md +16 -6
  2. package/dist/cjs/contracts/arkcontract.js +0 -2
  3. package/dist/cjs/contracts/contractManager.js +111 -215
  4. package/dist/cjs/contracts/contractWatcher.js +86 -115
  5. package/dist/cjs/repositories/indexedDB/manager.js +6 -3
  6. package/dist/cjs/repositories/indexedDB/schema.js +47 -2
  7. package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
  8. package/dist/cjs/repositories/realm/contractRepository.js +0 -4
  9. package/dist/cjs/repositories/realm/index.js +3 -1
  10. package/dist/cjs/repositories/realm/schemas.js +50 -1
  11. package/dist/cjs/repositories/realm/walletRepository.js +8 -4
  12. package/dist/cjs/repositories/scriptFromAddress.js +16 -0
  13. package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
  14. package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
  15. package/dist/cjs/utils/syncCursors.js +48 -56
  16. package/dist/cjs/wallet/expo/background.js +0 -13
  17. package/dist/cjs/wallet/expo/wallet.js +1 -6
  18. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
  19. package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
  20. package/dist/cjs/wallet/utils.js +41 -10
  21. package/dist/cjs/wallet/vtxo-manager.js +153 -39
  22. package/dist/cjs/wallet/wallet.js +72 -195
  23. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
  24. package/dist/cjs/worker/expo/taskRunner.js +2 -11
  25. package/dist/esm/contracts/arkcontract.js +0 -2
  26. package/dist/esm/contracts/contractManager.js +113 -217
  27. package/dist/esm/contracts/contractWatcher.js +86 -115
  28. package/dist/esm/repositories/indexedDB/manager.js +6 -3
  29. package/dist/esm/repositories/indexedDB/schema.js +46 -2
  30. package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
  31. package/dist/esm/repositories/realm/contractRepository.js +0 -4
  32. package/dist/esm/repositories/realm/index.js +1 -1
  33. package/dist/esm/repositories/realm/schemas.js +48 -0
  34. package/dist/esm/repositories/realm/walletRepository.js +8 -4
  35. package/dist/esm/repositories/scriptFromAddress.js +13 -0
  36. package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
  37. package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
  38. package/dist/esm/utils/syncCursors.js +47 -53
  39. package/dist/esm/wallet/expo/background.js +0 -13
  40. package/dist/esm/wallet/expo/wallet.js +2 -7
  41. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
  42. package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
  43. package/dist/esm/wallet/utils.js +41 -9
  44. package/dist/esm/wallet/vtxo-manager.js +153 -39
  45. package/dist/esm/wallet/wallet.js +75 -198
  46. package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
  47. package/dist/esm/worker/expo/taskRunner.js +3 -12
  48. package/dist/types/contracts/arkcontract.d.ts +0 -2
  49. package/dist/types/contracts/contractManager.d.ts +38 -9
  50. package/dist/types/contracts/contractWatcher.d.ts +22 -21
  51. package/dist/types/contracts/types.d.ts +0 -7
  52. package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
  53. package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
  54. package/dist/types/repositories/realm/index.d.ts +1 -1
  55. package/dist/types/repositories/realm/schemas.d.ts +41 -0
  56. package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
  57. package/dist/types/repositories/serialization.d.ts +1 -1
  58. package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
  59. package/dist/types/repositories/walletRepository.d.ts +10 -2
  60. package/dist/types/utils/syncCursors.d.ts +25 -23
  61. package/dist/types/wallet/index.d.ts +1 -1
  62. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
  63. package/dist/types/wallet/utils.d.ts +20 -4
  64. package/dist/types/wallet/vtxo-manager.d.ts +16 -6
  65. package/dist/types/wallet/wallet.d.ts +5 -17
  66. package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
  67. package/dist/types/worker/expo/taskRunner.d.ts +6 -3
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -878,9 +878,22 @@ const wallet = await Wallet.create({
878
878
  For React Native apps using Realm, pass your Realm instance directly:
879
879
 
880
880
  ```typescript
881
- import { RealmWalletRepository, RealmContractRepository, ArkRealmSchemas } from '@arkade-os/sdk/repositories/realm'
882
-
883
- const realm = await Realm.open({ schema: [...ArkRealmSchemas, ...yourSchemas] })
881
+ import {
882
+ RealmWalletRepository,
883
+ RealmContractRepository,
884
+ ArkRealmSchemas,
885
+ ARK_REALM_SCHEMA_VERSION,
886
+ runArkRealmMigrations,
887
+ } from '@arkade-os/sdk/repositories/realm'
888
+
889
+ const realm = await Realm.open({
890
+ schema: [...ArkRealmSchemas, ...yourSchemas],
891
+ schemaVersion: Math.max(ARK_REALM_SCHEMA_VERSION, yourSchemaVersion),
892
+ onMigration: (oldRealm, newRealm) => {
893
+ runArkRealmMigrations(oldRealm, newRealm)
894
+ // your own migrations
895
+ },
896
+ })
884
897
  const wallet = await Wallet.create({
885
898
  identity,
886
899
  arkServerUrl: 'https://arkade.computer',
@@ -1074,9 +1087,6 @@ const unsubscribe = await manager.onContractEvent((event) => {
1074
1087
  case 'vtxo_spent':
1075
1088
  console.log(`Spent virtual outputs from ${event.contractScript}`)
1076
1089
  break
1077
- case 'contract_expired':
1078
- console.log(`Contract ${event.contractScript} expired`)
1079
- break
1080
1090
  }
1081
1091
  })
1082
1092
 
@@ -110,7 +110,6 @@ function contractFromArkContract(encoded, options = {}) {
110
110
  params,
111
111
  state: options.state || "active",
112
112
  createdAt: Date.now(),
113
- expiresAt: options.expiresAt,
114
113
  metadata: options.metadata,
115
114
  };
116
115
  }
@@ -136,7 +135,6 @@ function contractFromArkContractWithAddress(encoded, serverPubKey, addressPrefix
136
135
  address: vtxoScript.address(addressPrefix, serverPubKey).encode(),
137
136
  state: options.state || "active",
138
137
  createdAt: Date.now(),
139
- expiresAt: options.expiresAt,
140
138
  metadata: options.metadata,
141
139
  };
142
140
  }
@@ -14,7 +14,7 @@ const DEFAULT_PAGE_SIZE = 500;
14
14
  * - Create and persist contracts
15
15
  * - Query stored contracts (optionally with their virtual outputs)
16
16
  * - Provide spendable path selection for a contract
17
- * - Emit contract-related events (virtual output received/spent/expired, connection reset)
17
+ * - Emit contract-related events (virtual output received/spent, connection reset)
18
18
  *
19
19
  * Notes:
20
20
  * - Implementations typically start watching automatically during initialization
@@ -84,29 +84,16 @@ class ContractManager {
84
84
  if (this.initialized) {
85
85
  return;
86
86
  }
87
- // Load persisted contracts
87
+ // Register persisted contracts with the watcher BEFORE the first
88
+ // sync. `addContract` seeds `lastKnownVtxos` from the repo without
89
+ // starting to poll, so it's cheap, and it populates
90
+ // `getWatchedContracts()` so the sync below can scope itself to the
91
+ // real watched set instead of every contract ever persisted.
88
92
  const contracts = await this.config.contractRepository.getContracts();
89
- // Delta-sync: fetch only virtual outputs that changed since the last cursor,
90
- // falling back to a full bootstrap for scripts seen for the first time.
91
- await this.deltaSyncContracts(contracts, undefined, true);
92
- // Reconcile the pending frontier: fetch all not-yet-finalized virtual outputs
93
- // to catch any that the delta window may have missed.
94
- if (contracts.length > 0) {
95
- await this.reconcilePendingFrontier(contracts);
96
- }
97
- // add all contracts to the watcher
98
- const now = Date.now();
99
93
  for (const contract of contracts) {
100
- // Check for expired contracts and mark as inactive
101
- if (contract.state === "active" &&
102
- contract.expiresAt &&
103
- contract.expiresAt <= now) {
104
- contract.state = "inactive";
105
- await this.config.contractRepository.saveContract(contract);
106
- }
107
- // Add to watcher
108
94
  await this.watcher.addContract(contract);
109
95
  }
96
+ await this.reconcileWatched();
110
97
  this.initialized = true;
111
98
  // Start watching automatically
112
99
  this.stopWatcherFn = await this.watcher.startWatching((event) => {
@@ -115,6 +102,23 @@ class ContractManager {
115
102
  });
116
103
  });
117
104
  }
105
+ /**
106
+ * Delta-sync the full watched set and reconcile the pending frontier.
107
+ *
108
+ * Shared recovery path used on initial boot and after a subscription
109
+ * reconnect. `syncContracts({})` scopes to the current watched set
110
+ * (see {@link ContractWatcher.getWatchedContracts}), uses the
111
+ * cursor-derived delta window, and advances the cursor on success.
112
+ * `reconcilePendingFrontier` catches not-yet-finalized virtual
113
+ * outputs that could sit outside any delta window.
114
+ */
115
+ async reconcileWatched() {
116
+ await this.syncContracts({});
117
+ const watched = this.watcher.getWatchedContracts();
118
+ if (watched.length > 0) {
119
+ await this.reconcilePendingFrontier(watched);
120
+ }
121
+ }
118
122
  /**
119
123
  * Create and register a new contract.
120
124
  *
@@ -159,15 +163,7 @@ class ContractManager {
159
163
  // Persist
160
164
  await this.config.contractRepository.saveContract(contract);
161
165
  // fetch all virtual outputs (including spent/swept) for this contract
162
- const requestStartedAt = Date.now();
163
- await this.fetchContractVxosFromIndexer([contract], true);
164
- // Advance the sync cursor so that the watcher's vtxo_received
165
- // event (triggered by addContract below) doesn't re-bootstrap
166
- // the same script via deltaSyncContracts.
167
- const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
168
- await (0, syncCursors_1.advanceSyncCursors)(this.config.walletRepository, {
169
- [contract.script]: cutoff,
170
- });
166
+ await this.fetchContractVxosFromIndexer([contract]);
171
167
  // Add to watcher
172
168
  await this.watcher.addContract(contract);
173
169
  return contract;
@@ -193,12 +189,26 @@ class ContractManager {
193
189
  }
194
190
  async getContractsWithVtxos(filter, pageSize) {
195
191
  const contracts = await this.getContracts(filter);
196
- const vtxos = await this.getVtxosForContracts(contracts, pageSize);
192
+ await this.syncContracts({ contracts, pageSize });
193
+ const vtxos = await this.getVtxosForContracts(contracts);
197
194
  return contracts.map((contract) => ({
198
195
  contract,
199
- vtxos: vtxos.get(contract.script) ?? [],
196
+ vtxos: vtxos.filter((vtxo) => vtxo.contractScript === contract.script),
200
197
  }));
201
198
  }
199
+ async annotateVtxos(vtxos) {
200
+ if (vtxos.length === 0)
201
+ return [];
202
+ const scripts = Array.from(new Set(vtxos.map((v) => v.script)));
203
+ const byScript = new Map();
204
+ const contracts = await this.config.contractRepository.getContracts({
205
+ script: scripts,
206
+ });
207
+ for (const contract of contracts) {
208
+ byScript.set(contract.script, contract);
209
+ }
210
+ return vtxos.map((vtxo) => (0, utils_1.extendVirtualCoinForContract)(vtxo, byScript));
211
+ }
202
212
  buildContractsDbFilter(filter) {
203
213
  return {
204
214
  script: filter.script,
@@ -339,41 +349,18 @@ class ContractManager {
339
349
  /**
340
350
  * Force refresh virtual outputs from the indexer.
341
351
  *
342
- * Without options, clears all sync cursors and re-fetches every contract.
352
+ * Without options, re-fetches every contract and advances the global cursor.
343
353
  * With options, narrows the refresh to specific scripts and/or a time window.
354
+ * Subset refreshes (scripts filter) intentionally do not advance the cursor.
344
355
  */
345
356
  async refreshVtxos(opts) {
346
- let contracts = await this.config.contractRepository.getContracts();
347
- if (opts?.scripts && opts.scripts.length > 0) {
348
- const scriptSet = new Set(opts.scripts);
349
- contracts = contracts.filter((c) => scriptSet.has(c.script));
350
- }
351
- const syncWindow = opts?.after !== undefined || opts?.before !== undefined
352
- ? {
353
- after: opts.after ?? 0,
354
- before: opts.before ?? Date.now(),
355
- }
357
+ const contracts = opts?.scripts
358
+ ? await this.getContracts({ script: opts.scripts })
356
359
  : undefined;
357
- if (!syncWindow) {
358
- // Full refresh — clear cursors so the next delta sync re-bootstraps.
359
- if (opts?.scripts && opts.scripts.length > 0) {
360
- await (0, syncCursors_1.clearSyncCursors)(this.config.walletRepository, opts.scripts);
361
- }
362
- else {
363
- await (0, syncCursors_1.clearSyncCursors)(this.config.walletRepository);
364
- }
365
- }
366
- const requestStartedAt = Date.now();
367
- const fetched = await this.fetchContractVxosFromIndexer(contracts, true, undefined, syncWindow);
368
- // Persist cursors so subsequent incremental syncs don't re-bootstrap.
369
- const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
370
- const cursorUpdates = {};
371
- for (const script of fetched.keys()) {
372
- cursorUpdates[script] = cutoff;
373
- }
374
- if (Object.keys(cursorUpdates).length > 0) {
375
- await (0, syncCursors_1.advanceSyncCursors)(this.config.walletRepository, cursorUpdates);
376
- }
360
+ await this.syncContracts({
361
+ contracts,
362
+ window: { after: opts?.after, before: opts?.before },
363
+ });
377
364
  }
378
365
  /**
379
366
  * Check if currently watching.
@@ -402,94 +389,54 @@ class ContractManager {
402
389
  // Delta-sync only the changed virtual outputs for this contract.
403
390
  case "vtxo_received":
404
391
  case "vtxo_spent":
405
- await this.deltaSyncContracts([event.contract]);
392
+ await this.syncContracts({ contracts: [event.contract] });
406
393
  break;
407
- case "connection_reset": {
408
- // After a reconnect we don't know what we missed — full refetch.
409
- const activeWatchedContracts = this.watcher.getActiveContracts();
410
- await this.fetchContractVxosFromIndexer(activeWatchedContracts, true);
394
+ case "connection_reset":
395
+ // Same recovery path as boot: delta-sync the watched set
396
+ // and reconcile the pending frontier. `advanceSyncCursor`
397
+ // is monotonic so this never rewinds the cursor.
398
+ await this.reconcileWatched();
411
399
  break;
412
- }
413
- case "contract_expired":
414
- // just update DB
415
- await this.config.contractRepository.saveContract(event.contract);
416
400
  }
417
401
  // Forward to all callbacks
418
402
  this.emitEvent(event);
419
403
  }
420
- async getVtxosForContracts(contracts, pageSize) {
421
- if (contracts.length === 0) {
422
- return new Map();
423
- }
424
- // Deduplicate concurrent callers against an in-flight fetch so we don't
425
- // issue redundant round-trips. Once the fetch settles we clear the
426
- // reference so the next call triggers a fresh fetch.
427
- // TODO: can be removed once we fix the persistence layer (address vs scripts)
428
- if (this.syncVtxosCallInflight) {
429
- return this.syncVtxosCallInflight;
430
- }
431
- this.syncVtxosCallInflight = this.fetchContractVxosFromIndexer(contracts, true, pageSize).finally(() => {
432
- this.syncVtxosCallInflight = undefined;
433
- });
434
- return this.syncVtxosCallInflight;
404
+ async getVtxosForContracts(contracts) {
405
+ const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) => vtxos.map((vtxo) => ({
406
+ ...vtxo,
407
+ contractScript: script,
408
+ })))));
409
+ return res.flat();
435
410
  }
436
411
  /**
437
- * Incrementally sync virtual outputs for the given contracts.
438
- * Uses per-script cursors to fetch only what changed since the last sync.
439
- * Scripts without a cursor are bootstrapped with a full fetch.
412
+ * Sync virtual outputs for the given contracts against the indexer.
413
+ *
414
+ * When `options.contracts` is omitted the sync covers the full
415
+ * watched set (active contracts plus any inactive contracts still
416
+ * holding cached VTXOs) and the global cursor is advanced on
417
+ * success. Passing an explicit subset leaves the cursor alone so a
418
+ * narrow poll can't hide data that other contracts still need to
419
+ * pick up.
440
420
  */
441
- async deltaSyncContracts(contracts, pageSize, force) {
442
- if (contracts.length === 0)
443
- return new Map();
444
- // If forced, we are treating all contracts as boostrapped and we clean the VTXO list
445
- if (force === true) {
446
- await Promise.all(contracts.map((contract) => this.config.walletRepository.deleteVtxos(contract.address)));
447
- }
448
- const cursors = await (0, syncCursors_1.getAllSyncCursors)(this.config.walletRepository);
449
- // Partition into bootstrap (no cursor) and delta (has cursor) groups.
450
- const bootstrap = [];
451
- const delta = [];
452
- for (const c of contracts) {
453
- if (force) {
454
- bootstrap.push(c);
455
- continue;
456
- }
457
- if (cursors[c.script] !== undefined) {
458
- delta.push(c);
459
- }
460
- else {
461
- bootstrap.push(c);
462
- }
463
- }
464
- const result = new Map();
465
- const cursorUpdates = {};
466
- // Full bootstrap for new scripts.
467
- if (bootstrap.length > 0) {
468
- const requestStartedAt = Date.now();
469
- const fetched = await this.fetchContractVxosFromIndexer(bootstrap, true);
421
+ async syncContracts(options) {
422
+ const cursor = await (0, syncCursors_1.getSyncCursor)(this.config.walletRepository);
423
+ const window = options.window ?? (0, syncCursors_1.computeSyncWindow)(cursor);
424
+ // Advance the global cursor only on full-scope, cursor-derived delta
425
+ // syncs. A caller-supplied window is targeted (e.g. `refreshVtxos`)
426
+ // and must not move the cursor — it may skip data outside its bounds.
427
+ // `<=` lets the bootstrap case (cursor=0, window.after=0) write the
428
+ // migration marker on first boot; otherwise the marker would never
429
+ // be written and every subsequent boot would treat the cursor as
430
+ // legacy and re-bootstrap.
431
+ const mustUpdateCursor = options.contracts === undefined &&
432
+ options.window === undefined &&
433
+ (window.after ?? 0) <= cursor;
434
+ const contracts = options.contracts ?? this.watcher.getWatchedContracts();
435
+ const requestStartedAt = Date.now();
436
+ const result = await this.fetchContractVxosFromIndexer(contracts, options.pageSize, window);
437
+ if (mustUpdateCursor) {
470
438
  const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
471
- for (const [script, vtxos] of fetched) {
472
- result.set(script, vtxos);
473
- cursorUpdates[script] = cutoff;
474
- }
475
- }
476
- // Delta sync for scripts with an existing cursor.
477
- if (delta.length > 0) {
478
- // Use the oldest cursor so the shared window covers every script.
479
- const minCursor = Math.min(...delta.map((c) => cursors[c.script]));
480
- const window = (0, syncCursors_1.computeSyncWindow)(minCursor);
481
- if (window) {
482
- const requestStartedAt = Date.now();
483
- const fetched = await this.fetchContractVxosFromIndexer(delta, true, pageSize, window);
484
- const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
485
- for (const [script, vtxos] of fetched) {
486
- result.set(script, vtxos);
487
- cursorUpdates[script] = cutoff;
488
- }
489
- }
490
- }
491
- if (Object.keys(cursorUpdates).length > 0) {
492
- await (0, syncCursors_1.advanceSyncCursors)(this.config.walletRepository, cursorUpdates);
439
+ await (0, syncCursors_1.advanceSyncCursor)(this.config.walletRepository, cutoff);
493
440
  }
494
441
  return result;
495
442
  }
@@ -505,21 +452,20 @@ class ContractManager {
505
452
  scripts,
506
453
  pendingOnly: true,
507
454
  });
508
- // Group by contract and upsert.
455
+ // Share the annotation path with external callers so the two entry
456
+ // points can't drift.
457
+ const owned = vtxos.filter((v) => scriptToContract.has(v.script));
458
+ const annotated = await this.annotateVtxos(owned);
509
459
  const byContract = new Map();
510
- for (const vtxo of vtxos) {
511
- if (!vtxo.script)
512
- continue;
460
+ for (const vtxo of annotated) {
513
461
  const contract = scriptToContract.get(vtxo.script);
514
- if (!contract)
515
- continue;
516
462
  let arr = byContract.get(contract.address);
517
463
  if (!arr) {
518
464
  arr = [];
519
465
  byContract.set(contract.address, arr);
520
466
  }
521
467
  arr.push({
522
- ...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
468
+ ...vtxo,
523
469
  contractScript: contract.script,
524
470
  });
525
471
  }
@@ -527,8 +473,8 @@ class ContractManager {
527
473
  await this.config.walletRepository.saveVtxos(addr, contractVtxos);
528
474
  }
529
475
  }
530
- async fetchContractVxosFromIndexer(contracts, includeSpent, pageSize, syncWindow) {
531
- const fetched = await this.fetchContractVtxosBulk(contracts, includeSpent, pageSize, syncWindow);
476
+ async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
477
+ const fetched = await this.fetchContractVtxosBulk(contracts, pageSize, syncWindow);
532
478
  const result = new Map();
533
479
  for (const [contractScript, vtxos] of fetched) {
534
480
  result.set(contractScript, vtxos);
@@ -539,23 +485,17 @@ class ContractManager {
539
485
  }
540
486
  return result;
541
487
  }
542
- async fetchContractVtxosBulk(contracts, includeSpent, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
488
+ async fetchContractVtxosBulk(contracts, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
543
489
  if (contracts.length === 0) {
544
490
  return new Map();
545
491
  }
546
- // For a single contract, use the paginated path directly.
547
- if (contracts.length === 1) {
548
- const contract = contracts[0];
549
- const vtxos = await this.fetchContractVtxosPaginated(contract, includeSpent, pageSize, syncWindow);
550
- return new Map([[contract.script, vtxos]]);
551
- }
552
- // For multiple contracts, batch all scripts into a single indexer call
553
- // per page to minimise round-trips. Results are keyed by script so we
554
- // can distribute them back to the correct contract afterwards.
492
+ // Batch all scripts into a single indexer call per page to minimise
493
+ // round-trips. Results are keyed by script so we can distribute them
494
+ // back to the correct contract afterwards. Always fetches the full
495
+ // history (spent/swept included) so the repo is the source of truth.
555
496
  const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
556
497
  const result = new Map(contracts.map((c) => [c.script, []]));
557
498
  const scripts = contracts.map((c) => c.script);
558
- const opts = includeSpent ? {} : { spendableOnly: true };
559
499
  const windowOpts = syncWindow
560
500
  ? {
561
501
  ...(syncWindow.after !== undefined && {
@@ -571,22 +511,20 @@ class ContractManager {
571
511
  while (hasMore) {
572
512
  const { vtxos, page } = await this.config.indexerProvider.getVtxos({
573
513
  scripts,
574
- ...opts,
575
514
  ...windowOpts,
576
515
  pageIndex,
577
516
  pageSize,
578
517
  });
579
- for (const vtxo of vtxos) {
580
- // Match the virtual output back to its contract via the script field
581
- // populated by the indexer.
582
- if (!vtxo.script)
583
- continue;
584
- const contract = scriptToContract.get(vtxo.script);
585
- if (!contract)
586
- continue;
587
- result.get(contract.script).push({
588
- ...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
589
- contractScript: contract.script,
518
+ // Match virtual outputs back to their contract via the script field
519
+ // populated by the indexer, then share the annotation path with
520
+ // external callers via annotateVtxos so the two entry points can't
521
+ // drift.
522
+ const owned = vtxos.filter((v) => scriptToContract.has(v.script));
523
+ const annotated = await this.annotateVtxos(owned);
524
+ for (const vtxo of annotated) {
525
+ result.get(vtxo.script).push({
526
+ ...vtxo,
527
+ contractScript: vtxo.script,
590
528
  });
591
529
  }
592
530
  hasMore = page ? vtxos.length === pageSize : false;
@@ -596,42 +534,6 @@ class ContractManager {
596
534
  }
597
535
  return result;
598
536
  }
599
- async fetchContractVtxosPaginated(contract, includeSpent, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
600
- const allVtxos = [];
601
- let pageIndex = 0;
602
- let hasMore = true;
603
- const opts = includeSpent ? {} : { spendableOnly: true };
604
- const windowOpts = syncWindow
605
- ? {
606
- ...(syncWindow.after !== undefined && {
607
- after: syncWindow.after,
608
- }),
609
- ...(syncWindow.before !== undefined && {
610
- before: syncWindow.before,
611
- }),
612
- }
613
- : {};
614
- while (hasMore) {
615
- const { vtxos, page } = await this.config.indexerProvider.getVtxos({
616
- scripts: [contract.script],
617
- ...opts,
618
- ...windowOpts,
619
- pageIndex,
620
- pageSize,
621
- });
622
- for (const vtxo of vtxos) {
623
- allVtxos.push({
624
- ...(0, utils_1.extendVtxoFromContract)(vtxo, contract),
625
- contractScript: contract.script,
626
- });
627
- }
628
- hasMore = page ? vtxos.length === pageSize : false;
629
- pageIndex++;
630
- if (hasMore)
631
- await new Promise((r) => setTimeout(r, 500));
632
- }
633
- return allVtxos;
634
- }
635
537
  /**
636
538
  * Dispose of the ContractManager and release all resources.
637
539
  *
@@ -660,13 +562,7 @@ class ContractManager {
660
562
  * ```
661
563
  */
662
564
  [Symbol.dispose]() {
663
- // Stop watching
664
- this.stopWatcherFn?.();
665
- this.stopWatcherFn = undefined;
666
- // Clear callbacks
667
- this.eventCallbacks.clear();
668
- // Mark as uninitialized
669
- this.initialized = false;
565
+ this.dispose();
670
566
  }
671
567
  }
672
568
  exports.ContractManager = ContractManager;