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