@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
@@ -65,6 +65,12 @@ class ContractWatcher {
65
65
  lastKnownVtxos: new Map(),
66
66
  };
67
67
  this.contracts.set(contract.script, state);
68
+ // Seed the baseline from the repository BEFORE any poll or event
69
+ // emits. Without this, the first poll after (re)start treats every
70
+ // persisted vtxo as "new" and emits `vtxo_received` for each —
71
+ // which downstream triggers a redundant per-vtxo sync on every
72
+ // app launch and can confuse consumers that react to the event.
73
+ await this.seedLastKnownVtxos(state);
68
74
  // If we're already watching, poll to discover virtual outputs and update subscription
69
75
  if (this.isWatching) {
70
76
  // Poll first to discover virtual outputs (may affect whether we watch this contract).
@@ -73,6 +79,31 @@ class ContractWatcher {
73
79
  await this.tryUpdateSubscription();
74
80
  }
75
81
  }
82
+ /**
83
+ * Pre-populate `lastKnownVtxos` from the wallet repository.
84
+ *
85
+ * Runs on add (and can be re-run after reconnect) so polling always
86
+ * compares the indexer's view against what is already persisted,
87
+ * emitting only genuine deltas.
88
+ */
89
+ async seedLastKnownVtxos(state) {
90
+ try {
91
+ const cached = await this.config.walletRepository.getVtxos(state.contract.address);
92
+ for (const vtxo of cached) {
93
+ if (vtxo.isSpent)
94
+ continue;
95
+ const key = `${vtxo.txid}:${vtxo.vout}`;
96
+ state.lastKnownVtxos.set(key, vtxo);
97
+ }
98
+ }
99
+ catch (error) {
100
+ // Don't throw — the watcher can still recover via poll and
101
+ // subscription events. A failed seed just means the first poll
102
+ // may emit some redundant `vtxo_received` events for already
103
+ // known vtxos.
104
+ console.error(`ContractWatcher: failed to seed lastKnownVtxos for ${state.contract.script}`, error);
105
+ }
106
+ }
76
107
  /**
77
108
  * Update an existing contract.
78
109
  */
@@ -105,35 +136,20 @@ class ContractWatcher {
105
136
  return Array.from(this.contracts.values()).map((s) => s.contract);
106
137
  }
107
138
  /**
108
- * Get all active in-memory contracts.
109
- */
110
- getActiveContracts() {
111
- return this.getAllContracts().filter((c) => c.state === "active");
112
- }
113
- /**
114
- * Get scripts that should be watched.
139
+ * Contracts the watcher is actually tracking:
140
+ * - all active contracts, plus
141
+ * - inactive contracts that still hold known virtual outputs
142
+ * (the subscription keeps watching them so `vtxo_spent` events for
143
+ * those unspent outputs are still observed).
115
144
  *
116
- * Returns scripts for:
117
- * - All active contracts
118
- * - All contracts with known virtual outputs (regardless of state)
119
- *
120
- * This ensures we continue monitoring contracts even after they're
121
- * deactivated, as long as they have unspent virtual outputs.
145
+ * This is the single source of truth for "contracts whose VTXO state
146
+ * we still care about" — callers and the subscription itself fan out
147
+ * over the same set so nothing is reconciled that isn't also watched.
122
148
  */
123
- getScriptsToWatch() {
124
- const scripts = new Set();
125
- for (const [, state] of this.contracts) {
126
- // Always watch active contracts
127
- if (state.contract.state === "active") {
128
- scripts.add(state.contract.script);
129
- continue;
130
- }
131
- // Also watch inactive/expired contracts that have virtual outputs.
132
- if (state.lastKnownVtxos.size > 0) {
133
- scripts.add(state.contract.script);
134
- }
135
- }
136
- return Array.from(scripts);
149
+ getWatchedContracts() {
150
+ return Array.from(this.contracts.values())
151
+ .filter((s) => s.contract.state === "active" || s.lastKnownVtxos.size > 0)
152
+ .map((s) => s.contract);
137
153
  }
138
154
  /**
139
155
  * Get virtual outputs for contracts, grouped by contract script.
@@ -235,28 +251,6 @@ class ContractWatcher {
235
251
  return;
236
252
  await this.pollAllContracts();
237
253
  }
238
- /**
239
- * Check for expired contracts, update their state, and emit events.
240
- */
241
- checkExpiredContracts() {
242
- const now = Date.now();
243
- const expired = [];
244
- for (const state of this.contracts.values()) {
245
- const contract = state.contract;
246
- if (contract.state === "active" &&
247
- contract.expiresAt &&
248
- contract.expiresAt <= now) {
249
- contract.state = "inactive";
250
- expired.push(contract);
251
- this.eventCallback?.({
252
- type: "contract_expired",
253
- contractScript: contract.script,
254
- contract,
255
- timestamp: now,
256
- });
257
- }
258
- }
259
- }
260
254
  /**
261
255
  * Connect to the subscription.
262
256
  */
@@ -332,14 +326,11 @@ class ContractWatcher {
332
326
  }
333
327
  }, this.config.failsafePollIntervalMs);
334
328
  }
335
- /**
336
- * Poll all active contracts for current state.
337
- */
338
329
  async pollAllContracts() {
339
- const activeScripts = this.getActiveContracts().map((c) => c.script);
340
- if (activeScripts.length === 0)
330
+ const scripts = this.getWatchedContracts().map((c) => c.script);
331
+ if (scripts.length === 0)
341
332
  return;
342
- await this.pollContracts(activeScripts);
333
+ await this.pollContracts(scripts);
343
334
  }
344
335
  /**
345
336
  * Poll specific contracts and emit events for changes.
@@ -407,7 +398,7 @@ class ContractWatcher {
407
398
  * Watches both active contracts and contracts with virtual outputs.
408
399
  */
409
400
  async updateSubscription() {
410
- const scriptsToWatch = this.getScriptsToWatch();
401
+ const scriptsToWatch = this.getWatchedContracts().map((c) => c.script);
411
402
  if (scriptsToWatch.length === 0) {
412
403
  if (this.subscriptionId) {
413
404
  try {
@@ -472,61 +463,55 @@ class ContractWatcher {
472
463
  if (!this.eventCallback)
473
464
  return;
474
465
  const timestamp = Date.now();
475
- const scripts = update.scripts || [];
476
466
  if (update.newVtxos?.length) {
477
- this.processSubscriptionVtxos(update.newVtxos, scripts, "vtxo_received", timestamp);
467
+ this.processSubscriptionVtxos(update.newVtxos, "vtxo_received", timestamp);
478
468
  }
479
469
  if (update.spentVtxos?.length) {
480
- this.processSubscriptionVtxos(update.spentVtxos, scripts, "vtxo_spent", timestamp);
470
+ this.processSubscriptionVtxos(update.spentVtxos, "vtxo_spent", timestamp);
481
471
  }
482
472
  }
483
473
  /**
484
- * Process virtual outputs from subscription and route to correct contracts.
485
- * Uses the scripts from the subscription response to determine contract ownership.
474
+ * Process virtual outputs from subscription and route each VTXO to the
475
+ * single contract that actually locks it via `vtxo.script`. If the script
476
+ * doesn't match any watched contract, skip the VTXO rather than fan it
477
+ * out to every matching contract — fan-out produced phantom state in
478
+ * non-owning contracts that then never reconciled.
486
479
  */
487
- processSubscriptionVtxos(vtxos, scripts, eventType, timestamp) {
488
- // If we have exactly one script, all virtual outputs belong to that contract
489
- // Otherwise, we can't reliably determine ownership without script in VirtualCoin
490
- if (scripts.length === 1) {
491
- const contractScript = scripts[0];
492
- if (contractScript) {
493
- // Update tracking
494
- const state = this.contracts.get(contractScript);
495
- if (state) {
496
- for (const vtxo of vtxos) {
497
- const key = `${vtxo.txid}:${vtxo.vout}`;
498
- if (eventType === "vtxo_received") {
499
- state.lastKnownVtxos.set(key, vtxo);
500
- }
501
- else if (eventType === "vtxo_spent") {
502
- state.lastKnownVtxos.delete(key);
503
- }
504
- }
505
- }
506
- this.emitVtxoEvent(contractScript, vtxos, eventType, timestamp);
480
+ processSubscriptionVtxos(vtxos, eventType, timestamp) {
481
+ const byContract = new Map();
482
+ let unknownScript = 0;
483
+ for (const vtxo of vtxos) {
484
+ if (!this.contracts.has(vtxo.script)) {
485
+ unknownScript++;
486
+ continue;
507
487
  }
508
- return;
488
+ let bucket = byContract.get(vtxo.script);
489
+ if (!bucket) {
490
+ bucket = [];
491
+ byContract.set(vtxo.script, bucket);
492
+ }
493
+ bucket.push(vtxo);
509
494
  }
510
- // Multiple scripts - assign virtual outputs to all matching contracts
511
- // This is a limitation: we can't know which virtual output belongs to which script
512
- // In practice, subscription events usually come with a single script context
513
- for (const script of scripts) {
514
- const contractScript = script;
515
- if (contractScript) {
516
- const state = this.contracts.get(contractScript);
517
- if (state) {
518
- for (const vtxo of vtxos) {
519
- const key = `${vtxo.txid}:${vtxo.vout}`;
520
- if (eventType === "vtxo_received") {
521
- state.lastKnownVtxos.set(key, vtxo);
522
- }
523
- else {
524
- state.lastKnownVtxos.delete(key);
525
- }
495
+ if (unknownScript > 0) {
496
+ // The failsafe poll is the backstop for these; log at debug so we
497
+ // can correlate "VTXO state drift" reports with subscription
498
+ // drops rather than chase phantom bugs.
499
+ console.debug(`ContractWatcher.processSubscriptionVtxos[${eventType}]: dropped ${unknownScript} unknown-script VTXOs (${vtxos.length} total)`);
500
+ }
501
+ for (const [contractScript, bucketVtxos] of byContract) {
502
+ const state = this.contracts.get(contractScript);
503
+ if (state) {
504
+ for (const vtxo of bucketVtxos) {
505
+ const key = `${vtxo.txid}:${vtxo.vout}`;
506
+ if (eventType === "vtxo_received") {
507
+ state.lastKnownVtxos.set(key, vtxo);
508
+ }
509
+ else if (eventType === "vtxo_spent") {
510
+ state.lastKnownVtxos.delete(key);
526
511
  }
527
512
  }
528
- this.emitVtxoEvent(contractScript, vtxos, eventType, timestamp);
529
513
  }
514
+ this.emitVtxoEvent(contractScript, bucketVtxos, eventType, timestamp);
530
515
  }
531
516
  }
532
517
  /**
@@ -536,12 +521,10 @@ class ContractWatcher {
536
521
  if (!this.eventCallback)
537
522
  return;
538
523
  const state = this.contracts.get(contractScript);
539
- // ensure we check somehow regularly
540
- this.checkExpiredContracts();
524
+ if (!state)
525
+ return;
541
526
  switch (eventType) {
542
527
  case "vtxo_received":
543
- if (!state)
544
- return;
545
528
  this.eventCallback({
546
529
  type: "vtxo_received",
547
530
  vtxos: vtxos.map((v) => ({
@@ -558,8 +541,6 @@ class ContractWatcher {
558
541
  });
559
542
  return;
560
543
  case "vtxo_spent":
561
- if (!state)
562
- return;
563
544
  this.eventCallback({
564
545
  type: "vtxo_spent",
565
546
  vtxos: vtxos.map((v) => ({
@@ -575,16 +556,6 @@ class ContractWatcher {
575
556
  timestamp,
576
557
  });
577
558
  return;
578
- case "contract_expired":
579
- if (!state)
580
- return;
581
- this.eventCallback({
582
- type: "contract_expired",
583
- contractScript,
584
- contract: state.contract,
585
- timestamp,
586
- });
587
- return;
588
559
  default:
589
560
  return;
590
561
  }
@@ -26,7 +26,10 @@ const refCounts = new Map();
26
26
  *
27
27
  * @param dbName The name of the database to open.
28
28
  * @param dbVersion The database version to open.
29
- * @param initDatabase A function that migrates the database schema, called on `onupgradeneeded` only.
29
+ * @param initDatabase A function that migrates the database schema, called
30
+ * on `onupgradeneeded` only. Receives the database, the previous version
31
+ * (0 for fresh installs), and the upgrade transaction — the transaction is
32
+ * required for data migrations (cursor/update on existing stores).
30
33
  *
31
34
  * @returns A promise that resolves to the database instance.
32
35
  */
@@ -54,9 +57,9 @@ async function openDatabase(dbName, dbVersion, initDatabase) {
54
57
  request.onsuccess = () => {
55
58
  resolve(request.result);
56
59
  };
57
- request.onupgradeneeded = () => {
60
+ request.onupgradeneeded = (event) => {
58
61
  const db = request.result;
59
- initDatabase(db);
62
+ initDatabase(db, event.oldVersion, request.transaction);
60
63
  };
61
64
  request.onblocked = () => {
62
65
  console.warn("Database upgrade blocked - close other tabs/connections");
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DB_VERSION = exports.LEGACY_STORE_CONTRACT_COLLECTIONS = exports.STORE_CONTRACTS = exports.STORE_WALLET_STATE = exports.STORE_TRANSACTIONS = exports.STORE_UTXOS = exports.STORE_VTXOS = void 0;
4
4
  exports.initDatabase = initDatabase;
5
+ exports.backfillVtxoScripts = backfillVtxoScripts;
6
+ const scriptFromAddress_1 = require("../scriptFromAddress");
5
7
  // Store names introduced in V2, they are all new to the migration
6
8
  exports.STORE_VTXOS = "vtxos";
7
9
  exports.STORE_UTXOS = "utxos";
@@ -10,8 +12,15 @@ exports.STORE_WALLET_STATE = "walletState";
10
12
  exports.STORE_CONTRACTS = "contracts";
11
13
  // @deprecated use only for migrations, this is created in V1
12
14
  exports.LEGACY_STORE_CONTRACT_COLLECTIONS = "contractsCollections";
13
- exports.DB_VERSION = 2;
14
- function initDatabase(db) {
15
+ // Version history:
16
+ // v1 initial wallet repo schema, `contractsCollections` store.
17
+ // v2 — new `vtxos/utxos/transactions/walletState/contracts` stores.
18
+ // v3 — add `script` index on the vtxos store and backfill missing
19
+ // `vtxo.script` from `vtxo.address` so the field is always present
20
+ // at read time. Matches the `script` indexing already in place for
21
+ // Realm (`realm/schemas.ts`) and SQLite (`sqlite/walletRepository.ts`).
22
+ exports.DB_VERSION = 3;
23
+ function initDatabase(db, oldVersion, transaction) {
15
24
  // Create wallet stores
16
25
  if (!db.objectStoreNames.contains(exports.STORE_VTXOS)) {
17
26
  const vtxosStore = db.createObjectStore(exports.STORE_VTXOS, {
@@ -68,6 +77,11 @@ function initDatabase(db) {
68
77
  unique: false,
69
78
  });
70
79
  }
80
+ if (!vtxosStore.indexNames.contains("script")) {
81
+ vtxosStore.createIndex("script", "script", {
82
+ unique: false,
83
+ });
84
+ }
71
85
  }
72
86
  if (!db.objectStoreNames.contains(exports.STORE_UTXOS)) {
73
87
  const utxosStore = db.createObjectStore(exports.STORE_UTXOS, {
@@ -156,4 +170,35 @@ function initDatabase(db) {
156
170
  keyPath: "key",
157
171
  });
158
172
  }
173
+ // v2 → v3: add the `script` index on the existing vtxos store and
174
+ // backfill missing `script` on legacy VTXO rows. The upgrade transaction
175
+ // is null only on a brand-new database (oldVersion === 0), where no
176
+ // legacy rows exist. `createIndex` scans existing records; rows still
177
+ // missing `script` are skipped and get indexed automatically when the
178
+ // backfill's `cursor.update()` adds the field.
179
+ if (oldVersion >= 1 && oldVersion < 3 && transaction) {
180
+ const vtxosStore = transaction.objectStore(exports.STORE_VTXOS);
181
+ if (!vtxosStore.indexNames.contains("script")) {
182
+ vtxosStore.createIndex("script", "script", { unique: false });
183
+ }
184
+ backfillVtxoScripts(transaction);
185
+ }
186
+ }
187
+ // Exported for unit tests — the `onupgradeneeded` transaction can't be
188
+ // forged in-process, so tests exercise the cursor logic with a regular
189
+ // readwrite transaction on a live DB.
190
+ function backfillVtxoScripts(transaction) {
191
+ const store = transaction.objectStore(exports.STORE_VTXOS);
192
+ const cursorRequest = store.openCursor();
193
+ cursorRequest.onsuccess = () => {
194
+ const cursor = cursorRequest.result;
195
+ if (!cursor)
196
+ return;
197
+ const value = cursor.value;
198
+ if (!value.script) {
199
+ value.script = (0, scriptFromAddress_1.scriptFromArkAddress)(value.address);
200
+ cursor.update(value);
201
+ }
202
+ cursor.continue();
203
+ };
159
204
  }
@@ -4,6 +4,7 @@ exports.IndexedDBWalletRepository = void 0;
4
4
  const db_1 = require("./db");
5
5
  const manager_1 = require("./manager");
6
6
  const schema_1 = require("./schema");
7
+ const scriptFromAddress_1 = require("../scriptFromAddress");
7
8
  const utils_1 = require("../../worker/browser/utils");
8
9
  /**
9
10
  * IndexedDB-based implementation of WalletRepository.
@@ -69,8 +70,17 @@ class IndexedDBWalletRepository {
69
70
  request.onerror = () => reject(request.error);
70
71
  request.onsuccess = () => {
71
72
  const results = request.result || [];
72
- const vtxos = results.map(db_1.deserializeVtxo);
73
- resolve(vtxos);
73
+ // Wrap `.map` in try/catch so a bad row (e.g. a legacy
74
+ // VTXO whose address can't be decoded during backfill)
75
+ // rejects the promise instead of silently throwing
76
+ // inside the IDB event handler, which would otherwise
77
+ // hang the caller.
78
+ try {
79
+ resolve(results.map(deserializeVtxoWithBackfill));
80
+ }
81
+ catch (err) {
82
+ reject(err);
83
+ }
74
84
  };
75
85
  });
76
86
  }
@@ -336,3 +346,12 @@ class IndexedDBWalletRepository {
336
346
  }
337
347
  }
338
348
  exports.IndexedDBWalletRepository = IndexedDBWalletRepository;
349
+ // Post-migration every row has `script`, but the backfill is idempotent: if a
350
+ // legacy row is ever read before the upgrade-path completes, derive `script`
351
+ // from `address` the same way the indexer would have populated it.
352
+ function deserializeVtxoWithBackfill(o) {
353
+ if (!o.script) {
354
+ o = { ...o, script: (0, scriptFromAddress_1.scriptFromArkAddress)(o.address) };
355
+ }
356
+ return (0, db_1.deserializeVtxo)(o);
357
+ }
@@ -57,7 +57,6 @@ class RealmContractRepository {
57
57
  state: contract.state,
58
58
  paramsJson: JSON.stringify(contract.params),
59
59
  createdAt: contract.createdAt,
60
- expiresAt: contract.expiresAt ?? null,
61
60
  label: contract.label ?? null,
62
61
  metadataJson: contract.metadata
63
62
  ? JSON.stringify(contract.metadata)
@@ -107,9 +106,6 @@ function contractObjectToDomain(obj) {
107
106
  params: JSON.parse(obj.paramsJson),
108
107
  createdAt: obj.createdAt,
109
108
  };
110
- if (obj.expiresAt !== null && obj.expiresAt !== undefined) {
111
- contract.expiresAt = obj.expiresAt;
112
- }
113
109
  if (obj.label !== null && obj.label !== undefined) {
114
110
  contract.label = obj.label;
115
111
  }
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ArkRealmSchemas = exports.RealmContractRepository = exports.RealmWalletRepository = void 0;
3
+ exports.runArkRealmMigrations = exports.ARK_REALM_SCHEMA_VERSION = exports.ArkRealmSchemas = exports.RealmContractRepository = exports.RealmWalletRepository = void 0;
4
4
  var walletRepository_1 = require("./walletRepository");
5
5
  Object.defineProperty(exports, "RealmWalletRepository", { enumerable: true, get: function () { return walletRepository_1.RealmWalletRepository; } });
6
6
  var contractRepository_1 = require("./contractRepository");
7
7
  Object.defineProperty(exports, "RealmContractRepository", { enumerable: true, get: function () { return contractRepository_1.RealmContractRepository; } });
8
8
  var schemas_1 = require("./schemas");
9
9
  Object.defineProperty(exports, "ArkRealmSchemas", { enumerable: true, get: function () { return schemas_1.ArkRealmSchemas; } });
10
+ Object.defineProperty(exports, "ARK_REALM_SCHEMA_VERSION", { enumerable: true, get: function () { return schemas_1.ARK_REALM_SCHEMA_VERSION; } });
11
+ Object.defineProperty(exports, "runArkRealmMigrations", { enumerable: true, get: function () { return schemas_1.runArkRealmMigrations; } });
@@ -10,7 +10,9 @@
10
10
  * ObjectSchema shape.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.ArkRealmSchemas = exports.ArkContractSchema = exports.ArkWalletStateSchema = exports.ArkTransactionSchema = exports.ArkUtxoSchema = exports.ArkVtxoSchema = void 0;
13
+ exports.ARK_REALM_SCHEMA_VERSION = exports.ArkRealmSchemas = exports.ArkContractSchema = exports.ArkWalletStateSchema = exports.ArkTransactionSchema = exports.ArkUtxoSchema = exports.ArkVtxoSchema = void 0;
14
+ exports.runArkRealmMigrations = runArkRealmMigrations;
15
+ const scriptFromAddress_1 = require("../scriptFromAddress");
14
16
  exports.ArkVtxoSchema = {
15
17
  name: "ArkVtxo",
16
18
  primaryKey: "pk",
@@ -35,6 +37,11 @@ exports.ArkVtxoSchema = {
35
37
  isUnrolled: "bool",
36
38
  isSpent: "bool?",
37
39
  assetsJson: "string?",
40
+ // scriptPubKey (hex) locking this VTXO, indexed so contract-scoped
41
+ // queries can resolve ownership without touching address mapping.
42
+ // Required as of schema v2; legacy rows are backfilled from `address`
43
+ // during migration (see `runArkRealmMigrations`).
44
+ script: { type: "string", indexed: true },
38
45
  },
39
46
  };
40
47
  exports.ArkUtxoSchema = {
@@ -106,3 +113,45 @@ exports.ArkRealmSchemas = [
106
113
  exports.ArkWalletStateSchema,
107
114
  exports.ArkContractSchema,
108
115
  ];
116
+ /**
117
+ * Current Realm schema version for the Arkade wallet.
118
+ *
119
+ * Consumers opening Realm must pass a `schemaVersion` at least this high so
120
+ * legacy databases get migrated; merge it with your own app's version:
121
+ *
122
+ * ```ts
123
+ * await Realm.open({
124
+ * schema: [...ArkRealmSchemas, ...yourSchemas],
125
+ * schemaVersion: Math.max(ARK_REALM_SCHEMA_VERSION, yourSchemaVersion),
126
+ * onMigration: (oldRealm, newRealm) => {
127
+ * runArkRealmMigrations(oldRealm, newRealm);
128
+ * // your own migrations
129
+ * },
130
+ * });
131
+ * ```
132
+ *
133
+ * History:
134
+ * - v1: initial ArkVtxo/ArkUtxo/... schemas, `script` nullable.
135
+ * - v2: ArkVtxo.script becomes required; NULL values are backfilled from
136
+ * the owning Ark address during migration.
137
+ */
138
+ exports.ARK_REALM_SCHEMA_VERSION = 2;
139
+ /**
140
+ * Run every Arkade schema migration applicable to the open Realm.
141
+ *
142
+ * Designed to be composed with the consumer's own migrations inside a single
143
+ * `onMigration` callback. Each migration step does a per-row check so it
144
+ * remains idempotent and independent of the app's global `schemaVersion` —
145
+ * a consumer whose app is already at version 10 can still trigger the
146
+ * Arkade v1→v2 script backfill when the row has never been populated.
147
+ */
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ function runArkRealmMigrations(oldRealm, newRealm) {
150
+ const newVtxos = newRealm.objects("ArkVtxo");
151
+ for (let i = 0; i < newVtxos.length; i++) {
152
+ const newVtxo = newVtxos[i];
153
+ if (!newVtxo.script) {
154
+ newVtxo.script = (0, scriptFromAddress_1.scriptFromArkAddress)(newVtxo.address);
155
+ }
156
+ }
157
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RealmWalletRepository = void 0;
4
4
  const serialization_1 = require("../serialization");
5
+ const scriptFromAddress_1 = require("../scriptFromAddress");
5
6
  /**
6
7
  * Realm-based implementation of WalletRepository.
7
8
  *
@@ -73,6 +74,7 @@ class RealmWalletRepository {
73
74
  ? JSON.stringify(s.extraWitness)
74
75
  : null,
75
76
  assetsJson: s.assets ? JSON.stringify(s.assets) : null,
77
+ script: s.script ?? null,
76
78
  }, "modified");
77
79
  }
78
80
  });
@@ -178,12 +180,10 @@ class RealmWalletRepository {
178
180
  return null;
179
181
  const obj = items[0];
180
182
  const state = {};
181
- if (obj.lastSyncTime !== null && obj.lastSyncTime !== undefined) {
182
- state.lastSyncTime = obj.lastSyncTime;
183
- }
184
183
  if (obj.settingsJson) {
185
184
  state.settings = JSON.parse(obj.settingsJson);
186
185
  }
186
+ state.lastSyncTime = obj.lastSyncTime ?? undefined;
187
187
  return state;
188
188
  }
189
189
  async saveWalletState(state) {
@@ -191,7 +191,7 @@ class RealmWalletRepository {
191
191
  this.realm.write(() => {
192
192
  this.realm.create("ArkWalletState", {
193
193
  key: "state",
194
- lastSyncTime: state.lastSyncTime ?? null,
194
+ lastSyncTime: state.lastSyncTime,
195
195
  settingsJson: state.settings
196
196
  ? JSON.stringify(state.settings)
197
197
  : null,
@@ -228,6 +228,10 @@ function vtxoObjectToDomain(obj) {
228
228
  ? JSON.parse(obj.extraWitnessJson)
229
229
  : undefined,
230
230
  assets: obj.assetsJson ? JSON.parse(obj.assetsJson) : undefined,
231
+ // Post-migration every row has `script`, but the backfill is
232
+ // idempotent: derive from `address` if the legacy column is still
233
+ // null (e.g. the migration hasn't run yet on this handle).
234
+ script: obj.script ?? (0, scriptFromAddress_1.scriptFromArkAddress)(obj.address),
231
235
  };
232
236
  return (0, serialization_1.deserializeVtxo)(serialized);
233
237
  }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scriptFromArkAddress = scriptFromArkAddress;
4
+ const base_1 = require("@scure/base");
5
+ const index_1 = require("../index");
6
+ /**
7
+ * Compute the hex-encoded `scriptPubKey` locking a VTXO from its owning Ark
8
+ * address. Used by repository-layer migrations to backfill `script` on legacy
9
+ * rows that pre-date the column (the indexer now guarantees the field, so new
10
+ * rows never go through this path). The `script` field is required by the
11
+ * domain type, so backfill must produce the same value the indexer would
12
+ * have returned — which is the hex of the address's `pkScript`.
13
+ */
14
+ function scriptFromArkAddress(address) {
15
+ return base_1.hex.encode(index_1.ArkAddress.decode(address).pkScript);
16
+ }
@@ -71,16 +71,15 @@ class SQLiteContractRepository {
71
71
  await this.ensureInit();
72
72
  await this.db.run(`INSERT OR REPLACE INTO ${this.table}
73
73
  (script, address, type, state, params_json,
74
- created_at, expires_at, label, metadata_json)
74
+ created_at, label, metadata_json)
75
75
  VALUES (?, ?, ?, ?, ?,
76
- ?, ?, ?, ?)`, [
76
+ ?, ?, ?)`, [
77
77
  contract.script,
78
78
  contract.address,
79
79
  contract.type,
80
80
  contract.state,
81
81
  JSON.stringify(contract.params),
82
82
  contract.createdAt,
83
- contract.expiresAt ?? null,
84
83
  contract.label ?? null,
85
84
  contract.metadata ? JSON.stringify(contract.metadata) : null,
86
85
  ]);
@@ -126,9 +125,6 @@ function contractRowToDomain(row) {
126
125
  params: JSON.parse(row.params_json),
127
126
  createdAt: row.created_at,
128
127
  };
129
- if (row.expires_at !== null) {
130
- contract.expiresAt = row.expires_at;
131
- }
132
128
  if (row.label !== null) {
133
129
  contract.label = row.label;
134
130
  }