@arkade-os/sdk 0.4.16 → 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.
- package/README.md +16 -6
- package/dist/cjs/contracts/arkcontract.js +0 -2
- package/dist/cjs/contracts/contractManager.js +111 -199
- package/dist/cjs/contracts/contractWatcher.js +86 -115
- package/dist/cjs/repositories/indexedDB/manager.js +6 -3
- package/dist/cjs/repositories/indexedDB/schema.js +47 -2
- package/dist/cjs/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/cjs/repositories/realm/contractRepository.js +0 -4
- package/dist/cjs/repositories/realm/index.js +3 -1
- package/dist/cjs/repositories/realm/schemas.js +50 -1
- package/dist/cjs/repositories/realm/walletRepository.js +8 -4
- package/dist/cjs/repositories/scriptFromAddress.js +16 -0
- package/dist/cjs/repositories/sqlite/contractRepository.js +2 -6
- package/dist/cjs/repositories/sqlite/walletRepository.js +121 -33
- package/dist/cjs/utils/syncCursors.js +48 -56
- package/dist/cjs/wallet/expo/background.js +0 -13
- package/dist/cjs/wallet/expo/wallet.js +1 -6
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +16 -7
- package/dist/cjs/wallet/serviceWorker/wallet.js +19 -0
- package/dist/cjs/wallet/utils.js +41 -10
- package/dist/cjs/wallet/vtxo-manager.js +153 -39
- package/dist/cjs/wallet/wallet.js +84 -202
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/cjs/worker/expo/taskRunner.js +2 -11
- package/dist/esm/contracts/arkcontract.js +0 -2
- package/dist/esm/contracts/contractManager.js +113 -201
- package/dist/esm/contracts/contractWatcher.js +86 -115
- package/dist/esm/repositories/indexedDB/manager.js +6 -3
- package/dist/esm/repositories/indexedDB/schema.js +46 -2
- package/dist/esm/repositories/indexedDB/walletRepository.js +21 -2
- package/dist/esm/repositories/realm/contractRepository.js +0 -4
- package/dist/esm/repositories/realm/index.js +1 -1
- package/dist/esm/repositories/realm/schemas.js +48 -0
- package/dist/esm/repositories/realm/walletRepository.js +8 -4
- package/dist/esm/repositories/scriptFromAddress.js +13 -0
- package/dist/esm/repositories/sqlite/contractRepository.js +2 -6
- package/dist/esm/repositories/sqlite/walletRepository.js +121 -33
- package/dist/esm/utils/syncCursors.js +47 -53
- package/dist/esm/wallet/expo/background.js +0 -13
- package/dist/esm/wallet/expo/wallet.js +2 -7
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +17 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +19 -0
- package/dist/esm/wallet/utils.js +41 -9
- package/dist/esm/wallet/vtxo-manager.js +153 -39
- package/dist/esm/wallet/wallet.js +87 -205
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +9 -13
- package/dist/esm/worker/expo/taskRunner.js +3 -12
- package/dist/types/contracts/arkcontract.d.ts +0 -2
- package/dist/types/contracts/contractManager.d.ts +38 -12
- package/dist/types/contracts/contractWatcher.d.ts +22 -21
- package/dist/types/contracts/types.d.ts +0 -7
- package/dist/types/repositories/indexedDB/manager.d.ts +5 -2
- package/dist/types/repositories/indexedDB/schema.d.ts +3 -2
- package/dist/types/repositories/realm/index.d.ts +1 -1
- package/dist/types/repositories/realm/schemas.d.ts +41 -0
- package/dist/types/repositories/scriptFromAddress.d.ts +9 -0
- package/dist/types/repositories/serialization.d.ts +1 -1
- package/dist/types/repositories/sqlite/walletRepository.d.ts +22 -0
- package/dist/types/repositories/walletRepository.d.ts +10 -2
- package/dist/types/utils/syncCursors.d.ts +25 -23
- package/dist/types/wallet/index.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +15 -3
- package/dist/types/wallet/utils.d.ts +20 -4
- package/dist/types/wallet/vtxo-manager.d.ts +16 -6
- package/dist/types/wallet/wallet.d.ts +5 -17
- package/dist/types/worker/expo/processors/contractPollProcessor.d.ts +9 -4
- package/dist/types/worker/expo/taskRunner.d.ts +6 -3
- 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
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
340
|
-
if (
|
|
330
|
+
const scripts = this.getWatchedContracts().map((c) => c.script);
|
|
331
|
+
if (scripts.length === 0)
|
|
341
332
|
return;
|
|
342
|
-
await this.pollContracts(
|
|
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.
|
|
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,
|
|
467
|
+
this.processSubscriptionVtxos(update.newVtxos, "vtxo_received", timestamp);
|
|
478
468
|
}
|
|
479
469
|
if (update.spentVtxos?.length) {
|
|
480
|
-
this.processSubscriptionVtxos(update.spentVtxos,
|
|
470
|
+
this.processSubscriptionVtxos(update.spentVtxos, "vtxo_spent", timestamp);
|
|
481
471
|
}
|
|
482
472
|
}
|
|
483
473
|
/**
|
|
484
|
-
* Process virtual outputs from subscription and route to
|
|
485
|
-
*
|
|
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,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
540
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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,
|
|
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
|
}
|