@arkade-os/sdk 0.4.24 → 0.4.25

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 (34) hide show
  1. package/dist/cjs/contracts/contractManager.js +44 -8
  2. package/dist/cjs/contracts/contractWatcher.js +2 -2
  3. package/dist/cjs/contracts/vtxoOwnership.js +18 -0
  4. package/dist/cjs/repositories/inMemory/walletRepository.js +35 -0
  5. package/dist/cjs/repositories/indexedDB/walletRepository.js +117 -0
  6. package/dist/cjs/repositories/realm/walletRepository.js +28 -0
  7. package/dist/cjs/repositories/sqlite/walletRepository.js +23 -0
  8. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +14 -3
  9. package/dist/cjs/wallet/serviceWorker/wallet.js +10 -0
  10. package/dist/cjs/wallet/vtxo-manager.js +112 -16
  11. package/dist/cjs/wallet/wallet.js +3 -17
  12. package/dist/cjs/worker/expo/processors/contractPollProcessor.js +1 -1
  13. package/dist/esm/contracts/contractManager.js +45 -9
  14. package/dist/esm/contracts/contractWatcher.js +3 -3
  15. package/dist/esm/contracts/vtxoOwnership.js +16 -0
  16. package/dist/esm/repositories/inMemory/walletRepository.js +35 -0
  17. package/dist/esm/repositories/indexedDB/walletRepository.js +117 -0
  18. package/dist/esm/repositories/realm/walletRepository.js +28 -0
  19. package/dist/esm/repositories/sqlite/walletRepository.js +23 -0
  20. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +15 -4
  21. package/dist/esm/wallet/serviceWorker/wallet.js +10 -0
  22. package/dist/esm/wallet/vtxo-manager.js +112 -16
  23. package/dist/esm/wallet/wallet.js +4 -18
  24. package/dist/esm/worker/expo/processors/contractPollProcessor.js +2 -2
  25. package/dist/types/contracts/contractManager.d.ts +17 -1
  26. package/dist/types/contracts/vtxoOwnership.d.ts +9 -1
  27. package/dist/types/repositories/inMemory/walletRepository.d.ts +4 -1
  28. package/dist/types/repositories/indexedDB/walletRepository.d.ts +4 -1
  29. package/dist/types/repositories/realm/walletRepository.d.ts +4 -1
  30. package/dist/types/repositories/sqlite/walletRepository.d.ts +4 -1
  31. package/dist/types/repositories/walletRepository.d.ts +21 -0
  32. package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +14 -2
  33. package/dist/types/wallet/vtxo-manager.d.ts +32 -5
  34. package/package.json +1 -1
@@ -1873,7 +1873,6 @@ class Wallet extends ReadonlyWallet {
1873
1873
  arr.push(v);
1874
1874
  spentByScript.set(v.script, arr);
1875
1875
  }
1876
- const byAddress = new Map();
1877
1876
  for (const [script, vtxos] of spentByScript) {
1878
1877
  // User-initiated send path: a wrong-script row here means the
1879
1878
  // wallet is about to record ownership against the wrong
@@ -1883,18 +1882,11 @@ class Wallet extends ReadonlyWallet {
1883
1882
  if (!targetAddr) {
1884
1883
  throw new Error(`Wallet.updateDbAfterOffchainTx: no contract owns script ${script}`);
1885
1884
  }
1886
- const bucket = byAddress.get(targetAddr) ?? [];
1887
- bucket.push(...vtxos);
1888
- byAddress.set(targetAddr, bucket);
1885
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.walletRepository, { script, address: targetAddr }, vtxos);
1889
1886
  }
1890
1887
  // Change is always primary-script by construction.
1891
1888
  if (changeVtxo) {
1892
- const bucket = byAddress.get(primaryAddr) ?? [];
1893
- bucket.push(changeVtxo);
1894
- byAddress.set(primaryAddr, bucket);
1895
- }
1896
- for (const [addr, vtxos] of byAddress) {
1897
- await this.walletRepository.saveVtxos(addr, vtxos);
1889
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.walletRepository, { script: changeVtxo.script, address: primaryAddr }, [changeVtxo]);
1898
1890
  }
1899
1891
  await this.walletRepository.saveTransactions(primaryAddr, [
1900
1892
  {
@@ -1957,7 +1949,6 @@ class Wallet extends ReadonlyWallet {
1957
1949
  // alongside the rest.
1958
1950
  const contracts = await cm.getContracts();
1959
1951
  const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
1960
- const byAddress = new Map();
1961
1952
  const byScript = new Map();
1962
1953
  for (const v of spentVtxos) {
1963
1954
  if (!v.script) {
@@ -1975,12 +1966,7 @@ class Wallet extends ReadonlyWallet {
1975
1966
  if (!targetAddr) {
1976
1967
  throw new Error(`Wallet.updateDbAfterSettle: no contract owns script ${script}`);
1977
1968
  }
1978
- const bucket = byAddress.get(targetAddr) ?? [];
1979
- bucket.push(...vtxos);
1980
- byAddress.set(targetAddr, bucket);
1981
- }
1982
- for (const [bucketAddr, vtxos] of byAddress) {
1983
- await this.walletRepository.saveVtxos(bucketAddr, vtxos);
1969
+ await (0, vtxoOwnership_1.saveVtxosForContract)(this.walletRepository, { script, address: targetAddr }, vtxos);
1984
1970
  }
1985
1971
  }
1986
1972
  if (boardingUtxoToRemove.size > 0) {
@@ -48,7 +48,7 @@ exports.contractPollProcessor = {
48
48
  // before persisting; the loop must keep going for the remaining
49
49
  // contracts even when one row is rejected.
50
50
  const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(allVtxos, contract.script, "contractPollProcessor");
51
- await walletRepository.saveVtxos(contract.address, filtered);
51
+ await (0, vtxoOwnership_1.saveVtxosForContract)(walletRepository, contract, filtered);
52
52
  vtxosSaved += filtered.length;
53
53
  contractsProcessed++;
54
54
  }
@@ -3,7 +3,7 @@ import { ContractWatcher } from './contractWatcher.js';
3
3
  import { contractHandlers } from './handlers/index.js';
4
4
  import { extendVirtualCoinForContract } from '../wallet/utils.js';
5
5
  import { advanceSyncCursor, computeSyncWindow, cursorCutoff, getSyncCursor, } from '../utils/syncCursors.js';
6
- import { filterVtxosForScript, warnAndFilterVtxosForScript, } from './vtxoOwnership.js';
6
+ import { getVtxosForContract, saveVtxosForContract, warnAndFilterVtxosForScript, } from './vtxoOwnership.js';
7
7
  const DEFAULT_PAGE_SIZE = 500;
8
8
  /**
9
9
  * Central manager for contract lifecycle and operations.
@@ -370,6 +370,46 @@ export class ContractManager {
370
370
  : undefined,
371
371
  });
372
372
  }
373
+ async refreshOutpoints(outpoints) {
374
+ if (outpoints.length === 0)
375
+ return;
376
+ const { vtxos } = await this.config.indexerProvider.getVtxos({
377
+ outpoints,
378
+ });
379
+ if (vtxos.length === 0)
380
+ return;
381
+ // Filter to outputs whose script we own. Map them to their owning
382
+ // contract so we can write through to the right per-address entry
383
+ // in the wallet repository.
384
+ const scripts = Array.from(new Set(vtxos.map((v) => v.script)));
385
+ const contracts = await this.config.contractRepository.getContracts({
386
+ script: scripts,
387
+ });
388
+ const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
389
+ const owned = vtxos.filter((v) => scriptToContract.has(v.script));
390
+ if (owned.length === 0)
391
+ return;
392
+ const annotated = await this.annotateVtxos(owned);
393
+ const byAddress = new Map();
394
+ for (const vtxo of annotated) {
395
+ const contract = scriptToContract.get(vtxo.script);
396
+ if (!contract)
397
+ continue;
398
+ const address = contract.address;
399
+ const arr = byAddress.get(address) ?? [];
400
+ arr.push(vtxo);
401
+ byAddress.set(address, arr);
402
+ }
403
+ for (const [address, addressVtxos] of byAddress) {
404
+ const contract = contracts.find((c) => c.address === address);
405
+ if (contract) {
406
+ await saveVtxosForContract(this.config.walletRepository, contract, addressVtxos);
407
+ }
408
+ else {
409
+ await this.config.walletRepository.saveVtxos(address, addressVtxos);
410
+ }
411
+ }
412
+ }
373
413
  /**
374
414
  * Check if currently watching.
375
415
  */
@@ -410,13 +450,9 @@ export class ContractManager {
410
450
  this.emitEvent(event);
411
451
  }
412
452
  async getVtxosForContracts(contracts) {
413
- const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) =>
414
- // Address buckets may carry legacy duplicate rows from
415
- // other contracts that once shared the same address —
416
- // gate by script so the wrong-script row never wins.
417
- filterVtxosForScript(vtxos, script).map((vtxo) => ({
453
+ const res = await Promise.all(contracts.map((contract) => getVtxosForContract(this.config.walletRepository, contract).then((vtxos) => vtxos.map((vtxo) => ({
418
454
  ...vtxo,
419
- contractScript: script,
455
+ contractScript: contract.script,
420
456
  })))));
421
457
  return res.flat();
422
458
  }
@@ -489,7 +525,7 @@ export class ContractManager {
489
525
  const filtered = warnAndFilterVtxosForScript(contractVtxos, contract.script, "ContractManager.reconcilePendingFrontier");
490
526
  if (filtered.length === 0)
491
527
  continue;
492
- await this.config.walletRepository.saveVtxos(addr, filtered);
528
+ await saveVtxosForContract(this.config.walletRepository, contract, filtered);
493
529
  }
494
530
  }
495
531
  async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
@@ -502,7 +538,7 @@ export class ContractManager {
502
538
  const filtered = warnAndFilterVtxosForScript(vtxos, contract.script, "ContractManager.fetchContractVxosFromIndexer");
503
539
  if (filtered.length === 0)
504
540
  continue;
505
- await this.config.walletRepository.saveVtxos(contract.address, filtered);
541
+ await saveVtxosForContract(this.config.walletRepository, contract, filtered);
506
542
  }
507
543
  }
508
544
  return result;
@@ -1,6 +1,6 @@
1
1
  import { extendVirtualCoinForContract } from '../wallet/utils.js';
2
2
  import { isEventSourceError } from '../providers/utils.js';
3
- import { filterVtxosForScript } from './vtxoOwnership.js';
3
+ import { getVtxosForContract } from './vtxoOwnership.js';
4
4
  /**
5
5
  * Watches multiple contracts for virtual output state changes with resilient connection handling.
6
6
  *
@@ -91,7 +91,7 @@ export class ContractWatcher {
91
91
  // Apply the same script gate used by getContractVtxos so a legacy
92
92
  // wrong-script row in the address bucket can't seed the baseline
93
93
  // and then look "spent" on the first poll.
94
- const cached = filterVtxosForScript(await this.config.walletRepository.getVtxos(state.contract.address), state.contract.script);
94
+ const cached = await getVtxosForContract(this.config.walletRepository, state.contract);
95
95
  for (const vtxo of cached) {
96
96
  if (vtxo.isSpent)
97
97
  continue;
@@ -173,7 +173,7 @@ export class ContractWatcher {
173
173
  // Use contract address as cache key. Legacy address buckets
174
174
  // can contain rows from other contracts; gate by script before
175
175
  // converting so a wrong-script row never reaches the watcher.
176
- const cached = filterVtxosForScript(await repo.getVtxos(state.contract.address), state.contract.script);
176
+ const cached = await getVtxosForContract(repo, state.contract);
177
177
  if (cached.length > 0) {
178
178
  // Convert to ContractVtxo with contractScript
179
179
  const contractVtxos = cached.map((v) => ({
@@ -51,3 +51,19 @@ export function validateVtxosForScript(vtxos, script, context) {
51
51
  .join(", ");
52
52
  throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
53
53
  }
54
+ /**
55
+ * Tier 2 dispatch helpers: route to script-scoped repository methods when
56
+ * available, falling back to Tier 1 address-based filtering otherwise.
57
+ */
58
+ export async function getVtxosForContract(repo, contract) {
59
+ return repo.getVtxosForScript
60
+ ? repo.getVtxosForScript(contract.script)
61
+ : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
62
+ }
63
+ export async function saveVtxosForContract(repo, contract, vtxos) {
64
+ if (repo.saveVtxosForScript) {
65
+ return repo.saveVtxosForScript({ script: contract.script, address: contract.address }, vtxos);
66
+ }
67
+ validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
68
+ return repo.saveVtxos(contract.address, vtxos);
69
+ }
@@ -1,3 +1,4 @@
1
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
1
2
  /**
2
3
  * In-memory implementation of WalletRepository.
3
4
  * Data is ephemeral and scoped to the instance.
@@ -21,6 +22,40 @@ export class InMemoryWalletRepository {
21
22
  async deleteVtxos(address) {
22
23
  this.vtxosByAddress.delete(address);
23
24
  }
25
+ async getVtxosForScript(script) {
26
+ const allMatches = [];
27
+ for (const bucket of this.vtxosByAddress.values()) {
28
+ for (const vtxo of bucket) {
29
+ if (isVtxoForScript(vtxo, script)) {
30
+ allMatches.push(vtxo);
31
+ }
32
+ }
33
+ }
34
+ // Dedup by outpoint (last-write-wins across address buckets)
35
+ return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
36
+ }
37
+ async saveVtxosForScript(key, vtxos) {
38
+ if (!key.address) {
39
+ throw new Error("InMemoryWalletRepository requires an address");
40
+ }
41
+ for (const vtxo of vtxos) {
42
+ if (!isVtxoForScript(vtxo, key.script)) {
43
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
44
+ }
45
+ }
46
+ return this.saveVtxos(key.address, vtxos);
47
+ }
48
+ async deleteVtxosForScript(script) {
49
+ for (const [address, bucket] of this.vtxosByAddress.entries()) {
50
+ const next = bucket.filter((v) => !isVtxoForScript(v, script));
51
+ if (next.length === 0) {
52
+ this.vtxosByAddress.delete(address);
53
+ }
54
+ else {
55
+ this.vtxosByAddress.set(address, next);
56
+ }
57
+ }
58
+ }
24
59
  async getUtxos(address) {
25
60
  return this.utxosByAddress.get(address) ?? [];
26
61
  }
@@ -3,6 +3,7 @@ import { closeDatabase, openDatabase } from './manager.js';
3
3
  import { initDatabase } from './schema.js';
4
4
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
5
5
  import { DEFAULT_DB_NAME } from '../../worker/browser/utils.js';
6
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
6
7
  /**
7
8
  * IndexedDB-based implementation of WalletRepository.
8
9
  */
@@ -141,6 +142,85 @@ export class IndexedDBWalletRepository {
141
142
  throw error;
142
143
  }
143
144
  }
145
+ async getVtxosForScript(script) {
146
+ try {
147
+ const db = await this.getDB();
148
+ return new Promise((resolve, reject) => {
149
+ const transaction = db.transaction([STORE_VTXOS], "readonly");
150
+ const store = transaction.objectStore(STORE_VTXOS);
151
+ const index = store.index("script");
152
+ const request = index.getAll(script);
153
+ request.onerror = () => reject(request.error);
154
+ request.onsuccess = () => {
155
+ const results = request.result || [];
156
+ try {
157
+ // Defensive filter: only rows whose script matches.
158
+ const matching = results.filter((r) => r.script === script);
159
+ // Dedup same outpoint rows across address buckets.
160
+ // Work on raw rows so the address field is available
161
+ // for the canonicality tiebreaker.
162
+ const byOutpoint = new Map();
163
+ for (const row of matching) {
164
+ const outpoint = `${row.txid}:${row.vout}`;
165
+ const existing = byOutpoint.get(outpoint);
166
+ if (!existing) {
167
+ byOutpoint.set(outpoint, row);
168
+ continue;
169
+ }
170
+ if (shouldReplaceVtxo(existing, row)) {
171
+ byOutpoint.set(outpoint, row);
172
+ }
173
+ }
174
+ resolve(Array.from(byOutpoint.values()).map(deserializeVtxoWithBackfill));
175
+ }
176
+ catch (err) {
177
+ reject(err);
178
+ }
179
+ };
180
+ });
181
+ }
182
+ catch (error) {
183
+ console.error(`Failed to get VTXOs for script ${script}:`, error);
184
+ throw error;
185
+ }
186
+ }
187
+ async saveVtxosForScript(key, vtxos) {
188
+ if (!key.address) {
189
+ throw new Error("IndexedDBWalletRepository requires an address");
190
+ }
191
+ for (const vtxo of vtxos) {
192
+ if (!isVtxoForScript(vtxo, key.script)) {
193
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
194
+ }
195
+ }
196
+ return this.saveVtxos(key.address, vtxos);
197
+ }
198
+ async deleteVtxosForScript(script) {
199
+ try {
200
+ const db = await this.getDB();
201
+ return new Promise((resolve, reject) => {
202
+ const transaction = db.transaction([STORE_VTXOS], "readwrite");
203
+ const store = transaction.objectStore(STORE_VTXOS);
204
+ const index = store.index("script");
205
+ const request = index.openCursor(IDBKeyRange.only(script));
206
+ request.onerror = () => reject(request.error);
207
+ request.onsuccess = () => {
208
+ const cursor = request.result;
209
+ if (cursor) {
210
+ cursor.delete();
211
+ cursor.continue();
212
+ }
213
+ else {
214
+ resolve();
215
+ }
216
+ };
217
+ });
218
+ }
219
+ catch (error) {
220
+ console.error(`Failed to clear VTXOs for script ${script}:`, error);
221
+ throw error;
222
+ }
223
+ }
144
224
  async getUtxos(address) {
145
225
  try {
146
226
  const db = await this.getDB();
@@ -351,3 +431,40 @@ function deserializeVtxoWithBackfill(o) {
351
431
  }
352
432
  return deserializeVtxo(o);
353
433
  }
434
+ function isCanonicalRow(row) {
435
+ try {
436
+ return scriptFromArkAddress(row.address) === row.script;
437
+ }
438
+ catch {
439
+ return false;
440
+ }
441
+ }
442
+ function shouldReplaceVtxo(existing, incoming) {
443
+ const existingCanonical = isCanonicalRow(existing);
444
+ const incomingCanonical = isCanonicalRow(incoming);
445
+ if (incomingCanonical && !existingCanonical)
446
+ return true;
447
+ if (existingCanonical && !incomingCanonical)
448
+ return false;
449
+ // Tie on canonicality, check lifecycle completeness
450
+ const existingWeight = getLifecycleWeight(existing);
451
+ const incomingWeight = getLifecycleWeight(incoming);
452
+ if (incomingWeight > existingWeight)
453
+ return true;
454
+ if (existingWeight > incomingWeight)
455
+ return false;
456
+ // Tie on weight, stable sort by address
457
+ return incoming.address < existing.address;
458
+ }
459
+ function getLifecycleWeight(v) {
460
+ let weight = 0;
461
+ if (v.isSpent !== undefined)
462
+ weight += 1;
463
+ if (v.spentBy)
464
+ weight += 2;
465
+ if (v.settledBy)
466
+ weight += 2;
467
+ if (v.arkTxId)
468
+ weight += 2;
469
+ return weight;
470
+ }
@@ -1,5 +1,6 @@
1
1
  import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
2
2
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
3
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
3
4
  /**
4
5
  * Realm-based implementation of WalletRepository.
5
6
  *
@@ -85,6 +86,33 @@ export class RealmWalletRepository {
85
86
  this.realm.delete(toDelete);
86
87
  });
87
88
  }
89
+ async getVtxosForScript(script) {
90
+ await this.ensureInit();
91
+ const results = this.realm
92
+ .objects("ArkVtxo")
93
+ .filtered("script == $0", script);
94
+ return [...results].map(vtxoObjectToDomain);
95
+ }
96
+ async saveVtxosForScript(key, vtxos) {
97
+ if (!key.address) {
98
+ throw new Error("RealmWalletRepository requires an address");
99
+ }
100
+ for (const vtxo of vtxos) {
101
+ if (!isVtxoForScript(vtxo, key.script)) {
102
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
103
+ }
104
+ }
105
+ return this.saveVtxos(key.address, vtxos);
106
+ }
107
+ async deleteVtxosForScript(script) {
108
+ await this.ensureInit();
109
+ this.realm.write(() => {
110
+ const toDelete = this.realm
111
+ .objects("ArkVtxo")
112
+ .filtered("script == $0", script);
113
+ this.realm.delete(toDelete);
114
+ });
115
+ }
88
116
  // ── UTXO management ────────────────────────────────────────────────
89
117
  async getUtxos(address) {
90
118
  await this.ensureInit();
@@ -1,5 +1,6 @@
1
1
  import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
2
2
  import { scriptFromArkAddress } from '../scriptFromAddress.js';
3
+ import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
3
4
  /**
4
5
  * SQLite-based implementation of WalletRepository.
5
6
  *
@@ -242,6 +243,28 @@ export class SQLiteWalletRepository {
242
243
  await this.ensureInit();
243
244
  await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
244
245
  }
246
+ async getVtxosForScript(script) {
247
+ await this.ensureInit();
248
+ const rows = await this.db.all(`SELECT * FROM ${this.tables.vtxos} WHERE script = ?`, [script]);
249
+ return rows.map(vtxoRowToDomain);
250
+ }
251
+ async saveVtxosForScript(key, vtxos) {
252
+ if (!key.address) {
253
+ throw new Error("SQLiteWalletRepository requires an address");
254
+ }
255
+ for (const vtxo of vtxos) {
256
+ if (!isVtxoForScript(vtxo, key.script)) {
257
+ throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
258
+ }
259
+ }
260
+ return this.saveVtxos(key.address, vtxos);
261
+ }
262
+ async deleteVtxosForScript(script) {
263
+ await this.ensureInit();
264
+ await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE script = ?`, [
265
+ script,
266
+ ]);
267
+ }
245
268
  // ── UTXO management ────────────────────────────────────────────────
246
269
  async getUtxos(address) {
247
270
  await this.ensureInit();
@@ -2,7 +2,7 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
2
2
  import { isExpired, isRecoverable, isSpendable, isSubdust, } from '../index.js';
3
3
  import { extendCoin } from '../utils.js';
4
4
  import { buildTransactionHistory } from '../../utils/transactionHistory.js';
5
- import { filterVtxosForScript, warnAndFilterVtxosForScript, } from '../../contracts/vtxoOwnership.js';
5
+ import { filterVtxosForScript, getVtxosForContract, saveVtxosForContract, warnAndFilterVtxosForScript, } from '../../contracts/vtxoOwnership.js';
6
6
  import { scriptFromArkAddress } from '../../repositories/scriptFromAddress.js';
7
7
  export class WalletNotInitializedError extends Error {
8
8
  constructor() {
@@ -313,6 +313,16 @@ export class WalletMessageHandler {
313
313
  type: "REFRESH_VTXOS_SUCCESS",
314
314
  });
315
315
  }
316
+ case "REFRESH_OUTPOINTS": {
317
+ const manager = await this.readonlyWallet.getContractManager();
318
+ const { outpoints } = message
319
+ .payload;
320
+ await manager.refreshOutpoints(outpoints);
321
+ return this.tagged({
322
+ id,
323
+ type: "REFRESH_OUTPOINTS_SUCCESS",
324
+ });
325
+ }
316
326
  case "SEND": {
317
327
  const { recipients } = message.payload;
318
328
  const txid = await this.wallet.send(...recipients);
@@ -620,7 +630,9 @@ export class WalletMessageHandler {
620
630
  : addrByScript.get(script);
621
631
  if (!targetAddress)
622
632
  continue;
623
- await this.walletRepository?.saveVtxos(targetAddress, filtered);
633
+ if (this.walletRepository) {
634
+ await saveVtxosForContract(this.walletRepository, { script, address: targetAddress }, filtered);
635
+ }
624
636
  }
625
637
  // notify all clients about the virtual output state update
626
638
  this.scheduleForNextTick(() => this.tagged({
@@ -840,8 +852,7 @@ export class WalletMessageHandler {
840
852
  const manager = await this.readonlyWallet.getContractManager();
841
853
  const contracts = await manager.getContracts();
842
854
  for (const contract of contracts) {
843
- const vtxos = await this.walletRepository.getVtxos(contract.address);
844
- addVtxos(filterVtxosForScript(vtxos, contract.script));
855
+ addVtxos(await getVtxosForContract(this.walletRepository, contract));
845
856
  }
846
857
  // Also check the wallet's primary address. Decode it to its script
847
858
  // and apply the same script gate. Failing to decode the wallet's own
@@ -57,6 +57,7 @@ export const DEFAULT_MESSAGE_TIMEOUTS = {
57
57
  UPDATE_CONTRACT: 30000,
58
58
  DELETE_CONTRACT: 10000,
59
59
  REFRESH_VTXOS: 30000,
60
+ REFRESH_OUTPOINTS: 30000,
60
61
  };
61
62
  const DEDUPABLE_REQUEST_TYPES = new Set([
62
63
  "GET_ADDRESS",
@@ -821,6 +822,15 @@ export class ServiceWorkerReadonlyWallet {
821
822
  };
822
823
  await sendContractMessage(message);
823
824
  },
825
+ async refreshOutpoints(outpoints) {
826
+ const message = {
827
+ type: "REFRESH_OUTPOINTS",
828
+ id: getRandomId(),
829
+ tag: messageTag,
830
+ payload: { outpoints },
831
+ };
832
+ await sendContractMessage(message);
833
+ },
824
834
  async isWatching() {
825
835
  const message = {
826
836
  type: "IS_CONTRACT_MANAGER_WATCHING",