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