@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SQLiteWalletRepository = void 0;
|
|
4
4
|
const serialization_1 = require("../serialization");
|
|
5
|
+
const scriptFromAddress_1 = require("../scriptFromAddress");
|
|
5
6
|
/**
|
|
6
7
|
* SQLite-based implementation of WalletRepository.
|
|
7
8
|
*
|
|
@@ -32,30 +33,7 @@ class SQLiteWalletRepository {
|
|
|
32
33
|
return this.initPromise;
|
|
33
34
|
}
|
|
34
35
|
async init() {
|
|
35
|
-
await this.
|
|
36
|
-
CREATE TABLE IF NOT EXISTS ${this.tables.vtxos} (
|
|
37
|
-
txid TEXT NOT NULL,
|
|
38
|
-
vout INTEGER NOT NULL,
|
|
39
|
-
value INTEGER NOT NULL,
|
|
40
|
-
address TEXT NOT NULL,
|
|
41
|
-
tap_tree TEXT NOT NULL,
|
|
42
|
-
forfeit_cb TEXT NOT NULL,
|
|
43
|
-
forfeit_s TEXT NOT NULL,
|
|
44
|
-
intent_cb TEXT NOT NULL,
|
|
45
|
-
intent_s TEXT NOT NULL,
|
|
46
|
-
status_json TEXT NOT NULL,
|
|
47
|
-
virtual_status_json TEXT NOT NULL,
|
|
48
|
-
created_at TEXT NOT NULL,
|
|
49
|
-
is_unrolled INTEGER NOT NULL DEFAULT 0,
|
|
50
|
-
is_spent INTEGER,
|
|
51
|
-
spent_by TEXT,
|
|
52
|
-
settled_by TEXT,
|
|
53
|
-
ark_tx_id TEXT,
|
|
54
|
-
extra_witness_json TEXT,
|
|
55
|
-
assets_json TEXT,
|
|
56
|
-
PRIMARY KEY (txid, vout)
|
|
57
|
-
)
|
|
58
|
-
`);
|
|
36
|
+
await this.migrateVtxosTable();
|
|
59
37
|
await this.db.run(`
|
|
60
38
|
CREATE TABLE IF NOT EXISTS ${this.tables.utxos} (
|
|
61
39
|
txid TEXT NOT NULL,
|
|
@@ -89,14 +67,121 @@ class SQLiteWalletRepository {
|
|
|
89
67
|
await this.db.run(`
|
|
90
68
|
CREATE TABLE IF NOT EXISTS ${this.tables.walletState} (
|
|
91
69
|
key TEXT PRIMARY KEY,
|
|
92
|
-
|
|
93
|
-
|
|
70
|
+
settings_json TEXT,
|
|
71
|
+
last_sync_time INTEGER
|
|
94
72
|
)
|
|
95
73
|
`);
|
|
96
74
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_address ON ${this.tables.vtxos} (address)`);
|
|
75
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}vtxos_script ON ${this.tables.vtxos} (script)`);
|
|
97
76
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}utxos_address ON ${this.tables.utxos} (address)`);
|
|
98
77
|
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_${this.prefix}transactions_address ON ${this.tables.transactions} (address)`);
|
|
99
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Bring the `vtxos` table to the current schema (v1 = `script` NOT NULL).
|
|
81
|
+
*
|
|
82
|
+
* Three cases:
|
|
83
|
+
* - Fresh install: create the v1 schema directly.
|
|
84
|
+
* - Legacy install without a `script` column: add it, backfill from
|
|
85
|
+
* `address`, then rebuild the table with NOT NULL (SQLite cannot add
|
|
86
|
+
* the NOT NULL constraint in place).
|
|
87
|
+
* - Legacy install with a nullable `script` column: backfill the NULLs
|
|
88
|
+
* and rebuild.
|
|
89
|
+
*
|
|
90
|
+
* The backfill derives `script` from the Ark address, matching what the
|
|
91
|
+
* indexer would have returned — new rows from the indexer always carry a
|
|
92
|
+
* populated `script`, so the migration is idempotent.
|
|
93
|
+
*
|
|
94
|
+
* The rebuild path is wrapped in a transaction: without it, a crash
|
|
95
|
+
* between the `DROP TABLE vtxos` and the `RENAME tmp → vtxos` commits
|
|
96
|
+
* would leave the next startup seeing no `vtxos` table and create a
|
|
97
|
+
* fresh empty one, silently orphaning every row in the temp table.
|
|
98
|
+
*/
|
|
99
|
+
async migrateVtxosTable() {
|
|
100
|
+
const tableExists = await this.db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [this.tables.vtxos]);
|
|
101
|
+
if (!tableExists) {
|
|
102
|
+
await this.db.run(this.vtxosCreateSql(this.tables.vtxos));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const cols = await this.db.all(`PRAGMA table_info(${this.tables.vtxos})`);
|
|
106
|
+
const scriptCol = cols.find((c) => c.name === "script");
|
|
107
|
+
if (scriptCol && scriptCol.notnull === 1) {
|
|
108
|
+
// Already on v1 schema.
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await this.db.run("BEGIN IMMEDIATE");
|
|
112
|
+
try {
|
|
113
|
+
if (!scriptCol) {
|
|
114
|
+
await this.db.run(`ALTER TABLE ${this.tables.vtxos} ADD COLUMN script TEXT`);
|
|
115
|
+
}
|
|
116
|
+
// Backfill any NULL scripts from the owning address. Derivation
|
|
117
|
+
// is deterministic, so re-running after a rolled-back attempt
|
|
118
|
+
// produces the same values.
|
|
119
|
+
const nullRows = await this.db.all(`SELECT txid, vout, address FROM ${this.tables.vtxos} WHERE script IS NULL`);
|
|
120
|
+
for (const row of nullRows) {
|
|
121
|
+
await this.db.run(`UPDATE ${this.tables.vtxos} SET script = ? WHERE txid = ? AND vout = ?`, [(0, scriptFromAddress_1.scriptFromArkAddress)(row.address), row.txid, row.vout]);
|
|
122
|
+
}
|
|
123
|
+
// SQLite can't turn a nullable column into NOT NULL in place —
|
|
124
|
+
// do the canonical 12-step table rebuild. Drop any stale temp
|
|
125
|
+
// table from a prior aborted attempt first.
|
|
126
|
+
const tempName = `${this.tables.vtxos}__migrate_tmp`;
|
|
127
|
+
await this.db.run(`DROP TABLE IF EXISTS ${tempName}`);
|
|
128
|
+
await this.db.run(this.vtxosCreateSql(tempName));
|
|
129
|
+
await this.db.run(`
|
|
130
|
+
INSERT INTO ${tempName}
|
|
131
|
+
(txid, vout, value, address, tap_tree,
|
|
132
|
+
forfeit_cb, forfeit_s, intent_cb, intent_s,
|
|
133
|
+
status_json, virtual_status_json, created_at,
|
|
134
|
+
is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
|
|
135
|
+
extra_witness_json, assets_json, script)
|
|
136
|
+
SELECT txid, vout, value, address, tap_tree,
|
|
137
|
+
forfeit_cb, forfeit_s, intent_cb, intent_s,
|
|
138
|
+
status_json, virtual_status_json, created_at,
|
|
139
|
+
is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
|
|
140
|
+
extra_witness_json, assets_json, script
|
|
141
|
+
FROM ${this.tables.vtxos}
|
|
142
|
+
`);
|
|
143
|
+
await this.db.run(`DROP TABLE ${this.tables.vtxos}`);
|
|
144
|
+
await this.db.run(`ALTER TABLE ${tempName} RENAME TO ${this.tables.vtxos}`);
|
|
145
|
+
await this.db.run("COMMIT");
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
// A failed COMMIT auto-rolls-back in SQLite, which makes a
|
|
149
|
+
// follow-up ROLLBACK throw "no transaction is active". Swallow
|
|
150
|
+
// that secondary error so the original cause surfaces.
|
|
151
|
+
try {
|
|
152
|
+
await this.db.run("ROLLBACK");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// already rolled back
|
|
156
|
+
}
|
|
157
|
+
throw e;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
vtxosCreateSql(tableName) {
|
|
161
|
+
return `CREATE TABLE ${tableName} (
|
|
162
|
+
txid TEXT NOT NULL,
|
|
163
|
+
vout INTEGER NOT NULL,
|
|
164
|
+
value INTEGER NOT NULL,
|
|
165
|
+
address TEXT NOT NULL,
|
|
166
|
+
tap_tree TEXT NOT NULL,
|
|
167
|
+
forfeit_cb TEXT NOT NULL,
|
|
168
|
+
forfeit_s TEXT NOT NULL,
|
|
169
|
+
intent_cb TEXT NOT NULL,
|
|
170
|
+
intent_s TEXT NOT NULL,
|
|
171
|
+
status_json TEXT NOT NULL,
|
|
172
|
+
virtual_status_json TEXT NOT NULL,
|
|
173
|
+
created_at TEXT NOT NULL,
|
|
174
|
+
is_unrolled INTEGER NOT NULL DEFAULT 0,
|
|
175
|
+
is_spent INTEGER,
|
|
176
|
+
spent_by TEXT,
|
|
177
|
+
settled_by TEXT,
|
|
178
|
+
ark_tx_id TEXT,
|
|
179
|
+
extra_witness_json TEXT,
|
|
180
|
+
assets_json TEXT,
|
|
181
|
+
script TEXT NOT NULL,
|
|
182
|
+
PRIMARY KEY (txid, vout)
|
|
183
|
+
)`;
|
|
184
|
+
}
|
|
100
185
|
async [Symbol.asyncDispose]() {
|
|
101
186
|
// no-op — consumer owns the SQLExecutor lifecycle
|
|
102
187
|
}
|
|
@@ -123,12 +208,12 @@ class SQLiteWalletRepository {
|
|
|
123
208
|
tap_tree, forfeit_cb, forfeit_s, intent_cb, intent_s,
|
|
124
209
|
status_json, virtual_status_json, created_at,
|
|
125
210
|
is_unrolled, is_spent, spent_by, settled_by, ark_tx_id,
|
|
126
|
-
extra_witness_json, assets_json)
|
|
211
|
+
extra_witness_json, assets_json, script)
|
|
127
212
|
VALUES (?, ?, ?, ?,
|
|
128
213
|
?, ?, ?, ?, ?,
|
|
129
214
|
?, ?, ?,
|
|
130
215
|
?, ?, ?, ?, ?,
|
|
131
|
-
?, ?)`, [
|
|
216
|
+
?, ?, ?)`, [
|
|
132
217
|
s.txid,
|
|
133
218
|
s.vout,
|
|
134
219
|
s.value,
|
|
@@ -152,6 +237,7 @@ class SQLiteWalletRepository {
|
|
|
152
237
|
s.arkTxId ?? null,
|
|
153
238
|
s.extraWitness ? JSON.stringify(s.extraWitness) : null,
|
|
154
239
|
s.assets ? JSON.stringify(s.assets) : null,
|
|
240
|
+
s.script ?? null,
|
|
155
241
|
]);
|
|
156
242
|
}
|
|
157
243
|
}
|
|
@@ -231,22 +317,20 @@ class SQLiteWalletRepository {
|
|
|
231
317
|
if (!row)
|
|
232
318
|
return null;
|
|
233
319
|
const state = {};
|
|
234
|
-
if (row.last_sync_time !== null && row.last_sync_time !== undefined) {
|
|
235
|
-
state.lastSyncTime = row.last_sync_time;
|
|
236
|
-
}
|
|
237
320
|
if (row.settings_json) {
|
|
238
321
|
state.settings = JSON.parse(row.settings_json);
|
|
239
322
|
}
|
|
323
|
+
state.lastSyncTime = row.last_sync_time ?? undefined;
|
|
240
324
|
return state;
|
|
241
325
|
}
|
|
242
326
|
async saveWalletState(state) {
|
|
243
327
|
await this.ensureInit();
|
|
244
328
|
await this.db.run(`INSERT OR REPLACE INTO ${this.tables.walletState}
|
|
245
|
-
(key,
|
|
329
|
+
(key, settings_json, last_sync_time)
|
|
246
330
|
VALUES (?, ?, ?)`, [
|
|
247
331
|
"state",
|
|
248
|
-
state.lastSyncTime ?? null,
|
|
249
332
|
state.settings ? JSON.stringify(state.settings) : null,
|
|
333
|
+
state.lastSyncTime ?? null,
|
|
250
334
|
]);
|
|
251
335
|
}
|
|
252
336
|
}
|
|
@@ -285,6 +369,10 @@ function vtxoRowToDomain(row) {
|
|
|
285
369
|
? JSON.parse(row.extra_witness_json)
|
|
286
370
|
: undefined,
|
|
287
371
|
assets: row.assets_json ? JSON.parse(row.assets_json) : undefined,
|
|
372
|
+
// Post-migration every row has `script`, but the backfill is
|
|
373
|
+
// idempotent: derive from `address` if the legacy column is still
|
|
374
|
+
// null (e.g. the migration hasn't run yet on this handle).
|
|
375
|
+
script: row.script ?? (0, scriptFromAddress_1.scriptFromArkAddress)(row.address),
|
|
288
376
|
};
|
|
289
377
|
return (0, serialization_1.deserializeVtxo)(serialized);
|
|
290
378
|
}
|
|
@@ -3,20 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.OVERLAP_MS = exports.SAFETY_LAG_MS = void 0;
|
|
4
4
|
exports.updateWalletState = updateWalletState;
|
|
5
5
|
exports.getSyncCursor = getSyncCursor;
|
|
6
|
-
exports.getAllSyncCursors = getAllSyncCursors;
|
|
7
6
|
exports.advanceSyncCursor = advanceSyncCursor;
|
|
8
|
-
exports.
|
|
9
|
-
exports.clearSyncCursors = clearSyncCursors;
|
|
7
|
+
exports.clearSyncCursor = clearSyncCursor;
|
|
10
8
|
exports.computeSyncWindow = computeSyncWindow;
|
|
11
9
|
exports.cursorCutoff = cursorCutoff;
|
|
12
10
|
/** Lag behind real-time to avoid racing with indexer writes. */
|
|
13
11
|
exports.SAFETY_LAG_MS = 30000;
|
|
14
12
|
/** Overlap window so boundary virtual outputs are never missed. */
|
|
15
|
-
exports.OVERLAP_MS =
|
|
13
|
+
exports.OVERLAP_MS = 24 * 60 * 60 * 1000;
|
|
16
14
|
/**
|
|
17
15
|
* Per-repository mutex that serializes wallet-state mutations so that
|
|
18
|
-
* concurrent read-modify-write cycles
|
|
19
|
-
* with clearSyncCursors or setPendingTxFlag) never silently overwrite
|
|
16
|
+
* concurrent read-modify-write cycles never silently overwrite
|
|
20
17
|
* each other's changes.
|
|
21
18
|
*/
|
|
22
19
|
const walletStateLocks = new WeakMap();
|
|
@@ -36,88 +33,83 @@ async function updateWalletState(repo, updater) {
|
|
|
36
33
|
return op;
|
|
37
34
|
}
|
|
38
35
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* Settings key that gates interpretation of the `lastSyncTime` field.
|
|
37
|
+
*
|
|
38
|
+
* The `lastSyncTime` column existed pre-PR with a different semantic
|
|
39
|
+
* (wall-clock at sync completion, written by the buggy sync loop this
|
|
40
|
+
* PR fixes). On upgrade we cannot trust any pre-existing value, so the
|
|
41
|
+
* cursor is only honoured after the first successful post-upgrade
|
|
42
|
+
* advance writes this marker into the `settings` JSON blob. Reusing
|
|
43
|
+
* `settings` avoids any schema migration.
|
|
41
44
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return state?.settings?.
|
|
45
|
+
const CURSOR_MIGRATED_KEY = "vtxoCursorMigrated";
|
|
46
|
+
function hasMigrationMarker(state) {
|
|
47
|
+
return state?.settings?.[CURSOR_MIGRATED_KEY] === true;
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
|
-
* Read
|
|
50
|
+
* Read the global high-water mark for VTXO indexer syncs.
|
|
51
|
+
*
|
|
52
|
+
* Returns `0` when:
|
|
53
|
+
* - the wallet has never been synced (bootstrap case), or
|
|
54
|
+
* - the stored `lastSyncTime` was written by pre-PR code and is not
|
|
55
|
+
* safe to reuse under the new semantics (see {@link CURSOR_MIGRATED_KEY}).
|
|
48
56
|
*/
|
|
49
|
-
async function
|
|
57
|
+
async function getSyncCursor(repo) {
|
|
50
58
|
const state = await repo.getWalletState();
|
|
51
|
-
|
|
59
|
+
if (!hasMigrationMarker(state))
|
|
60
|
+
return 0;
|
|
61
|
+
return state?.lastSyncTime ?? 0;
|
|
52
62
|
}
|
|
53
63
|
/**
|
|
54
|
-
* Advance the cursor
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...state.settings,
|
|
64
|
-
vtxoSyncCursors: { ...existing, [script]: cursor },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Advance cursors for multiple scripts in a single write.
|
|
64
|
+
* Advance the global cursor after a successful full-scope delta sync.
|
|
65
|
+
*
|
|
66
|
+
* Clamped with `Math.max` against the current value so concurrent syncs
|
|
67
|
+
* that finish out of order can't rewind the cursor: `lastUpdatedAt` is
|
|
68
|
+
* captured before each sync enters the `updateWalletState` mutex, and
|
|
69
|
+
* the later-started sync would otherwise overwrite the earlier-captured
|
|
70
|
+
* one with a smaller value. The legacy value is discarded on the first
|
|
71
|
+
* advance if the migration marker is absent so pre-PR data doesn't
|
|
72
|
+
* survive the upgrade.
|
|
71
73
|
*/
|
|
72
|
-
async function
|
|
74
|
+
async function advanceSyncCursor(repo, lastUpdatedAt) {
|
|
73
75
|
await updateWalletState(repo, (state) => {
|
|
74
|
-
const
|
|
76
|
+
const current = hasMigrationMarker(state)
|
|
77
|
+
? (state.lastSyncTime ?? 0)
|
|
78
|
+
: 0;
|
|
75
79
|
return {
|
|
76
80
|
...state,
|
|
81
|
+
lastSyncTime: Math.max(current, lastUpdatedAt),
|
|
77
82
|
settings: {
|
|
78
|
-
...state.settings,
|
|
79
|
-
|
|
83
|
+
...(state.settings ?? {}),
|
|
84
|
+
[CURSOR_MIGRATED_KEY]: true,
|
|
80
85
|
},
|
|
81
86
|
};
|
|
82
87
|
});
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
85
|
-
* Remove sync
|
|
86
|
-
*
|
|
90
|
+
* Remove the sync cursor, forcing a full re-bootstrap on next sync.
|
|
91
|
+
*
|
|
92
|
+
* Also clears the migration marker so any stored `lastSyncTime` is
|
|
93
|
+
* treated as untrusted on the next read.
|
|
87
94
|
*/
|
|
88
|
-
async function
|
|
95
|
+
async function clearSyncCursor(repo) {
|
|
89
96
|
await updateWalletState(repo, (state) => {
|
|
90
|
-
|
|
91
|
-
const { vtxoSyncCursors: _, ...restSettings } = state.settings ?? {};
|
|
92
|
-
return {
|
|
93
|
-
...state,
|
|
94
|
-
settings: restSettings,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
const existing = state.settings?.vtxoSyncCursors ?? {};
|
|
98
|
-
const filtered = { ...existing };
|
|
99
|
-
for (const s of scripts)
|
|
100
|
-
delete filtered[s];
|
|
97
|
+
const { [CURSOR_MIGRATED_KEY]: _, ...restSettings } = state.settings ?? {};
|
|
101
98
|
return {
|
|
102
99
|
...state,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
vtxoSyncCursors: filtered,
|
|
106
|
-
},
|
|
100
|
+
lastSyncTime: undefined,
|
|
101
|
+
settings: restSettings,
|
|
107
102
|
};
|
|
108
103
|
});
|
|
109
104
|
}
|
|
110
105
|
/**
|
|
111
106
|
* Compute the `after` lower-bound for a delta sync query.
|
|
112
|
-
* Returns `undefined` when the script has no cursor (bootstrap needed).
|
|
113
107
|
*
|
|
114
108
|
* No upper bound (`before`) is applied to the query so that freshly
|
|
115
109
|
* created virtual outputs are never excluded. The safety lag is applied only
|
|
116
110
|
* when advancing the cursor (see @see cursorCutoff).
|
|
117
111
|
*/
|
|
118
112
|
function computeSyncWindow(cursor) {
|
|
119
|
-
if (cursor === undefined)
|
|
120
|
-
return undefined;
|
|
121
113
|
const after = Math.max(0, cursor - exports.OVERLAP_MS);
|
|
122
114
|
return { after };
|
|
123
115
|
}
|
|
@@ -3,10 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.defineExpoBackgroundTask = defineExpoBackgroundTask;
|
|
4
4
|
exports.registerExpoBackgroundTask = registerExpoBackgroundTask;
|
|
5
5
|
exports.unregisterExpoBackgroundTask = unregisterExpoBackgroundTask;
|
|
6
|
-
const base_1 = require("@scure/base");
|
|
7
6
|
const taskRunner_1 = require("../../worker/expo/taskRunner");
|
|
8
7
|
const processors_1 = require("../../worker/expo/processors");
|
|
9
|
-
const default_1 = require("../../script/default");
|
|
10
8
|
const expoArk_1 = require("../../providers/expoArk");
|
|
11
9
|
const expoIndexer_1 = require("../../providers/expoIndexer");
|
|
12
10
|
const utils_1 = require("../utils");
|
|
@@ -64,22 +62,11 @@ function defineExpoBackgroundTask(taskName, options) {
|
|
|
64
62
|
// Reconstruct providers
|
|
65
63
|
const indexerProvider = new expoIndexer_1.ExpoIndexerProvider(config.arkServerUrl);
|
|
66
64
|
const arkProvider = new expoArk_1.ExpoArkProvider(config.arkServerUrl);
|
|
67
|
-
// Reconstruct default offchainTapscript as fallback
|
|
68
|
-
// for virtual outputs not associated with a contract.
|
|
69
|
-
const offchainTapscript = new default_1.DefaultVtxo.Script({
|
|
70
|
-
pubKey: base_1.hex.decode(config.pubkeyHex),
|
|
71
|
-
serverPubKey: base_1.hex.decode(config.serverPubKeyHex),
|
|
72
|
-
csvTimelock: {
|
|
73
|
-
value: BigInt(config.exitTimelockValue),
|
|
74
|
-
type: config.exitTimelockType,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
65
|
const deps = (0, taskRunner_1.createTaskDependencies)({
|
|
78
66
|
walletRepository,
|
|
79
67
|
contractRepository,
|
|
80
68
|
indexerProvider,
|
|
81
69
|
arkProvider,
|
|
82
|
-
offchainTapscript,
|
|
83
70
|
});
|
|
84
71
|
await (0, taskRunner_1.runTasks)(taskQueue, processors, deps);
|
|
85
72
|
// Acknowledge outbox results (no foreground to consume them)
|
|
@@ -103,12 +103,7 @@ class ExpoWallet {
|
|
|
103
103
|
contractRepository: wallet.contractRepository,
|
|
104
104
|
indexerProvider: wallet.indexerProvider,
|
|
105
105
|
arkProvider: wallet.arkProvider,
|
|
106
|
-
extendVtxo: (vtxo, contract) =>
|
|
107
|
-
if (contract) {
|
|
108
|
-
return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
|
|
109
|
-
}
|
|
110
|
-
return (0, utils_1.extendVirtualCoin)(wallet, vtxo);
|
|
111
|
-
},
|
|
106
|
+
extendVtxo: (vtxo, contract) => (0, utils_1.extendVirtualCoinForContract)(vtxo, contract),
|
|
112
107
|
};
|
|
113
108
|
const { taskQueue } = config.background;
|
|
114
109
|
// Persist wallet params so the background handler can rehydrate
|
|
@@ -245,6 +245,15 @@ class WalletMessageHandler {
|
|
|
245
245
|
payload: { contracts },
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
|
+
case "ANNOTATE_VTXOS": {
|
|
249
|
+
const manager = await this.readonlyWallet.getContractManager();
|
|
250
|
+
const annotated = await manager.annotateVtxos(message.payload.vtxos);
|
|
251
|
+
return this.tagged({
|
|
252
|
+
id,
|
|
253
|
+
type: "ANNOTATED_VTXOS",
|
|
254
|
+
payload: { vtxos: annotated },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
248
257
|
case "UPDATE_CONTRACT": {
|
|
249
258
|
const manager = await this.readonlyWallet.getContractManager();
|
|
250
259
|
const contract = await manager.updateContract(message.payload.script, message.payload.updates);
|
|
@@ -560,13 +569,13 @@ class WalletMessageHandler {
|
|
|
560
569
|
this.incomingFundsSubscription =
|
|
561
570
|
await this.readonlyWallet.notifyIncomingFunds(async (funds) => {
|
|
562
571
|
if (funds.type === "vtxo") {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (
|
|
572
|
+
// `funds.newVtxos` / `funds.spentVtxos` are already
|
|
573
|
+
// ExtendedVirtualCoin — annotation happened inside the
|
|
574
|
+
// underlying Wallet's subscription handler before this
|
|
575
|
+
// callback fired. Re-annotating here would only duplicate
|
|
576
|
+
// work and re-expose us to `annotateVtxos` throws.
|
|
577
|
+
const { newVtxos, spentVtxos } = funds;
|
|
578
|
+
if (newVtxos.length + spentVtxos.length === 0)
|
|
570
579
|
return;
|
|
571
580
|
// save virtual outputs using unified repository
|
|
572
581
|
await this.walletRepository?.saveVtxos(address, [
|
|
@@ -28,6 +28,7 @@ exports.DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
28
28
|
GET_TRANSACTION_HISTORY: 20000,
|
|
29
29
|
GET_CONTRACTS: 20000,
|
|
30
30
|
GET_CONTRACTS_WITH_VTXOS: 20000,
|
|
31
|
+
ANNOTATE_VTXOS: 20000,
|
|
31
32
|
GET_SPENDABLE_PATHS: 20000,
|
|
32
33
|
GET_ALL_SPENDING_PATHS: 20000,
|
|
33
34
|
GET_ASSET_DETAILS: 20000,
|
|
@@ -69,6 +70,7 @@ const DEDUPABLE_REQUEST_TYPES = new Set([
|
|
|
69
70
|
"GET_VTXOS",
|
|
70
71
|
"GET_CONTRACTS",
|
|
71
72
|
"GET_CONTRACTS_WITH_VTXOS",
|
|
73
|
+
"ANNOTATE_VTXOS",
|
|
72
74
|
"GET_SPENDABLE_PATHS",
|
|
73
75
|
"GET_ALL_SPENDING_PATHS",
|
|
74
76
|
"GET_ASSET_DETAILS",
|
|
@@ -656,6 +658,23 @@ class ServiceWorkerReadonlyWallet {
|
|
|
656
658
|
throw new Error("Failed to get contracts with vtxos");
|
|
657
659
|
}
|
|
658
660
|
},
|
|
661
|
+
async annotateVtxos(vtxos) {
|
|
662
|
+
if (vtxos.length === 0)
|
|
663
|
+
return [];
|
|
664
|
+
const message = {
|
|
665
|
+
type: "ANNOTATE_VTXOS",
|
|
666
|
+
id: (0, utils_2.getRandomId)(),
|
|
667
|
+
tag: messageTag,
|
|
668
|
+
payload: { vtxos },
|
|
669
|
+
};
|
|
670
|
+
try {
|
|
671
|
+
const response = await sendContractMessage(message);
|
|
672
|
+
return response.payload.vtxos;
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
throw new Error("Failed to annotate vtxos");
|
|
676
|
+
}
|
|
677
|
+
},
|
|
659
678
|
async updateContract(script, updates) {
|
|
660
679
|
const message = {
|
|
661
680
|
type: "UPDATE_CONTRACT",
|
package/dist/cjs/wallet/utils.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DUST_AMOUNT = void 0;
|
|
4
|
-
exports.extendVirtualCoin = extendVirtualCoin;
|
|
5
4
|
exports.extendCoin = extendCoin;
|
|
6
|
-
exports.
|
|
5
|
+
exports.extendVirtualCoinForContract = extendVirtualCoinForContract;
|
|
7
6
|
exports.getRandomId = getRandomId;
|
|
8
7
|
exports.isValidArkAddress = isValidArkAddress;
|
|
9
8
|
exports.validateRecipients = validateRecipients;
|
|
@@ -11,14 +10,6 @@ const __1 = require("..");
|
|
|
11
10
|
const handlers_1 = require("../contracts/handlers");
|
|
12
11
|
const base_1 = require("@scure/base");
|
|
13
12
|
exports.DUST_AMOUNT = 546; // sats
|
|
14
|
-
function extendVirtualCoin(wallet, vtxo) {
|
|
15
|
-
return {
|
|
16
|
-
...vtxo,
|
|
17
|
-
forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
|
|
18
|
-
intentTapLeafScript: wallet.offchainTapscript.forfeit(),
|
|
19
|
-
tapTree: wallet.offchainTapscript.encode(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
13
|
function extendCoin(wallet, utxo) {
|
|
23
14
|
return {
|
|
24
15
|
...utxo,
|
|
@@ -40,6 +31,46 @@ function extendVtxoFromContract(vtxo, contract) {
|
|
|
40
31
|
tapTree: script.encode(),
|
|
41
32
|
};
|
|
42
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Extend a VirtualCoin with the tap scripts of whichever contract locks it.
|
|
36
|
+
*
|
|
37
|
+
* The second argument accepts either form, so each callsite passes what it
|
|
38
|
+
* already has:
|
|
39
|
+
* - a single `Contract` (when the caller already knows the owning contract,
|
|
40
|
+
* e.g. the contract manager iterating its own `scriptToContract` map), or
|
|
41
|
+
* - a `ReadonlyMap<script, Contract>` (when the caller resolves by
|
|
42
|
+
* `vtxo.script`, populated by the indexer).
|
|
43
|
+
*
|
|
44
|
+
* Throws when no contract can be resolved — there is intentionally no
|
|
45
|
+
* default-tapscript fallback. When the wallet owns multiple contracts
|
|
46
|
+
* (default + delegate, several active vHTLCs, etc.) a default-tapscript path
|
|
47
|
+
* silently stamps every VTXO with the same forfeit/intent data, overwriting
|
|
48
|
+
* the correct data for any VTXO locked to a non-default contract. Callers
|
|
49
|
+
* must feed a Contract or a populated script→Contract map; otherwise the
|
|
50
|
+
* caller (typically `ContractManager.annotateVtxos`) should fetch the owning
|
|
51
|
+
* contract first.
|
|
52
|
+
*/
|
|
53
|
+
function extendVirtualCoinForContract(vtxo, contractOrMap) {
|
|
54
|
+
const contract = resolveContract(vtxo, contractOrMap);
|
|
55
|
+
if (!contract) {
|
|
56
|
+
throw new Error("extendVirtualCoinForContract: no contract matched vtxo.script — callers must resolve the owning contract before annotating");
|
|
57
|
+
}
|
|
58
|
+
return extendVtxoFromContract(vtxo, contract);
|
|
59
|
+
}
|
|
60
|
+
function isContractMap(value) {
|
|
61
|
+
// A `Contract` is a plain object with a string `type`. `ReadonlyMap` is
|
|
62
|
+
// an interface so `instanceof Map` is not enough to narrow it — but a
|
|
63
|
+
// contract has no `get` method, so duck-typing on that is unambiguous.
|
|
64
|
+
return typeof value.get === "function";
|
|
65
|
+
}
|
|
66
|
+
function resolveContract(vtxo, contractOrMap) {
|
|
67
|
+
if (!contractOrMap)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (isContractMap(contractOrMap)) {
|
|
70
|
+
return contractOrMap.get(vtxo.script);
|
|
71
|
+
}
|
|
72
|
+
return contractOrMap;
|
|
73
|
+
}
|
|
43
74
|
function getRandomId() {
|
|
44
75
|
const randomValue = crypto.getRandomValues(new Uint8Array(16));
|
|
45
76
|
return base_1.hex.encode(randomValue);
|