@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
|
@@ -8,27 +8,24 @@ exports.CONTRACT_POLL_TASK_TYPE = "contract-poll";
|
|
|
8
8
|
*
|
|
9
9
|
* Replicates the polling subset of @see ContractManager.initialize:
|
|
10
10
|
* 1. Load all contracts from the contract repository.
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4.
|
|
14
|
-
*
|
|
11
|
+
* 2. Paginated fetch of every VTXO (including spent) from the indexer.
|
|
12
|
+
* 3. Extend each VTXO with tapscript data.
|
|
13
|
+
* 4. Save to the wallet repository.
|
|
14
|
+
*
|
|
15
|
+
* NOTE: the indexer query deliberately omits `spendableOnly`. Every
|
|
16
|
+
* repository implements `saveVtxos` as an upsert with no batch delete,
|
|
17
|
+
* so filtering to spendable-only would leave VTXOs that became spent
|
|
18
|
+
* between polls marked as spendable forever. Fetching the full set lets
|
|
19
|
+
* the upsert overwrite stale records with their latest state.
|
|
15
20
|
*/
|
|
16
21
|
exports.contractPollProcessor = {
|
|
17
22
|
taskType: exports.CONTRACT_POLL_TASK_TYPE,
|
|
18
23
|
async execute(item, deps) {
|
|
19
24
|
const { contractRepository, walletRepository, indexerProvider, extendVtxo, } = deps;
|
|
20
25
|
const contracts = await contractRepository.getContracts();
|
|
21
|
-
const now = Date.now();
|
|
22
26
|
let contractsProcessed = 0;
|
|
23
27
|
let vtxosSaved = 0;
|
|
24
28
|
for (const contract of contracts) {
|
|
25
|
-
// Mark expired active contracts as inactive
|
|
26
|
-
if (contract.state === "active" &&
|
|
27
|
-
contract.expiresAt &&
|
|
28
|
-
contract.expiresAt <= now) {
|
|
29
|
-
contract.state = "inactive";
|
|
30
|
-
await contractRepository.saveContract(contract);
|
|
31
|
-
}
|
|
32
29
|
// Paginated fetch of spendable virtual outputs
|
|
33
30
|
const pageSize = 100;
|
|
34
31
|
let pageIndex = 0;
|
|
@@ -37,7 +34,6 @@ exports.contractPollProcessor = {
|
|
|
37
34
|
while (hasMore) {
|
|
38
35
|
const { vtxos, page } = await indexerProvider.getVtxos({
|
|
39
36
|
scripts: [contract.script],
|
|
40
|
-
spendableOnly: true,
|
|
41
37
|
pageIndex,
|
|
42
38
|
pageSize,
|
|
43
39
|
});
|
|
@@ -66,17 +66,8 @@ async function runTasks(queue, processors, deps) {
|
|
|
66
66
|
* can build deps without depending on Expo.
|
|
67
67
|
*/
|
|
68
68
|
function createTaskDependencies(options) {
|
|
69
|
-
const { walletRepository, contractRepository, indexerProvider, arkProvider, offchainTapscript, } = options;
|
|
70
69
|
return {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
indexerProvider,
|
|
74
|
-
arkProvider,
|
|
75
|
-
extendVtxo: (vtxo, contract) => {
|
|
76
|
-
if (contract) {
|
|
77
|
-
return (0, utils_1.extendVtxoFromContract)(vtxo, contract);
|
|
78
|
-
}
|
|
79
|
-
return (0, utils_1.extendVirtualCoin)({ offchainTapscript }, vtxo);
|
|
80
|
-
},
|
|
70
|
+
...options,
|
|
71
|
+
extendVtxo: (vtxo, contract) => (0, utils_1.extendVirtualCoinForContract)(vtxo, contract),
|
|
81
72
|
};
|
|
82
73
|
}
|
|
@@ -103,7 +103,6 @@ export function contractFromArkContract(encoded, options = {}) {
|
|
|
103
103
|
params,
|
|
104
104
|
state: options.state || "active",
|
|
105
105
|
createdAt: Date.now(),
|
|
106
|
-
expiresAt: options.expiresAt,
|
|
107
106
|
metadata: options.metadata,
|
|
108
107
|
};
|
|
109
108
|
}
|
|
@@ -129,7 +128,6 @@ export function contractFromArkContractWithAddress(encoded, serverPubKey, addres
|
|
|
129
128
|
address: vtxoScript.address(addressPrefix, serverPubKey).encode(),
|
|
130
129
|
state: options.state || "active",
|
|
131
130
|
createdAt: Date.now(),
|
|
132
|
-
expiresAt: options.expiresAt,
|
|
133
131
|
metadata: options.metadata,
|
|
134
132
|
};
|
|
135
133
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { ContractWatcher } from './contractWatcher.js';
|
|
3
3
|
import { contractHandlers } from './handlers/index.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { extendVirtualCoinForContract } from '../wallet/utils.js';
|
|
5
|
+
import { advanceSyncCursor, computeSyncWindow, cursorCutoff, getSyncCursor, } from '../utils/syncCursors.js';
|
|
6
6
|
const DEFAULT_PAGE_SIZE = 500;
|
|
7
7
|
/**
|
|
8
8
|
* Central manager for contract lifecycle and operations.
|
|
@@ -11,7 +11,7 @@ const DEFAULT_PAGE_SIZE = 500;
|
|
|
11
11
|
* - Create and persist contracts
|
|
12
12
|
* - Query stored contracts (optionally with their virtual outputs)
|
|
13
13
|
* - Provide spendable path selection for a contract
|
|
14
|
-
* - Emit contract-related events (virtual output received/spent
|
|
14
|
+
* - Emit contract-related events (virtual output received/spent, connection reset)
|
|
15
15
|
*
|
|
16
16
|
* Notes:
|
|
17
17
|
* - Implementations typically start watching automatically during initialization
|
|
@@ -81,29 +81,16 @@ export class ContractManager {
|
|
|
81
81
|
if (this.initialized) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
-
//
|
|
84
|
+
// Register persisted contracts with the watcher BEFORE the first
|
|
85
|
+
// sync. `addContract` seeds `lastKnownVtxos` from the repo without
|
|
86
|
+
// starting to poll, so it's cheap, and it populates
|
|
87
|
+
// `getWatchedContracts()` so the sync below can scope itself to the
|
|
88
|
+
// real watched set instead of every contract ever persisted.
|
|
85
89
|
const contracts = await this.config.contractRepository.getContracts();
|
|
86
|
-
// Delta-sync: fetch only virtual outputs that changed since the last cursor,
|
|
87
|
-
// falling back to a full bootstrap for scripts seen for the first time.
|
|
88
|
-
await this.deltaSyncContracts(contracts, undefined, true);
|
|
89
|
-
// Reconcile the pending frontier: fetch all not-yet-finalized virtual outputs
|
|
90
|
-
// to catch any that the delta window may have missed.
|
|
91
|
-
if (contracts.length > 0) {
|
|
92
|
-
await this.reconcilePendingFrontier(contracts);
|
|
93
|
-
}
|
|
94
|
-
// add all contracts to the watcher
|
|
95
|
-
const now = Date.now();
|
|
96
90
|
for (const contract of contracts) {
|
|
97
|
-
// Check for expired contracts and mark as inactive
|
|
98
|
-
if (contract.state === "active" &&
|
|
99
|
-
contract.expiresAt &&
|
|
100
|
-
contract.expiresAt <= now) {
|
|
101
|
-
contract.state = "inactive";
|
|
102
|
-
await this.config.contractRepository.saveContract(contract);
|
|
103
|
-
}
|
|
104
|
-
// Add to watcher
|
|
105
91
|
await this.watcher.addContract(contract);
|
|
106
92
|
}
|
|
93
|
+
await this.reconcileWatched();
|
|
107
94
|
this.initialized = true;
|
|
108
95
|
// Start watching automatically
|
|
109
96
|
this.stopWatcherFn = await this.watcher.startWatching((event) => {
|
|
@@ -112,6 +99,23 @@ export class ContractManager {
|
|
|
112
99
|
});
|
|
113
100
|
});
|
|
114
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Delta-sync the full watched set and reconcile the pending frontier.
|
|
104
|
+
*
|
|
105
|
+
* Shared recovery path used on initial boot and after a subscription
|
|
106
|
+
* reconnect. `syncContracts({})` scopes to the current watched set
|
|
107
|
+
* (see {@link ContractWatcher.getWatchedContracts}), uses the
|
|
108
|
+
* cursor-derived delta window, and advances the cursor on success.
|
|
109
|
+
* `reconcilePendingFrontier` catches not-yet-finalized virtual
|
|
110
|
+
* outputs that could sit outside any delta window.
|
|
111
|
+
*/
|
|
112
|
+
async reconcileWatched() {
|
|
113
|
+
await this.syncContracts({});
|
|
114
|
+
const watched = this.watcher.getWatchedContracts();
|
|
115
|
+
if (watched.length > 0) {
|
|
116
|
+
await this.reconcilePendingFrontier(watched);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
115
119
|
/**
|
|
116
120
|
* Create and register a new contract.
|
|
117
121
|
*
|
|
@@ -156,15 +160,7 @@ export class ContractManager {
|
|
|
156
160
|
// Persist
|
|
157
161
|
await this.config.contractRepository.saveContract(contract);
|
|
158
162
|
// fetch all virtual outputs (including spent/swept) for this contract
|
|
159
|
-
|
|
160
|
-
await this.fetchContractVxosFromIndexer([contract], true);
|
|
161
|
-
// Advance the sync cursor so that the watcher's vtxo_received
|
|
162
|
-
// event (triggered by addContract below) doesn't re-bootstrap
|
|
163
|
-
// the same script via deltaSyncContracts.
|
|
164
|
-
const cutoff = cursorCutoff(requestStartedAt);
|
|
165
|
-
await advanceSyncCursors(this.config.walletRepository, {
|
|
166
|
-
[contract.script]: cutoff,
|
|
167
|
-
});
|
|
163
|
+
await this.fetchContractVxosFromIndexer([contract]);
|
|
168
164
|
// Add to watcher
|
|
169
165
|
await this.watcher.addContract(contract);
|
|
170
166
|
return contract;
|
|
@@ -190,12 +186,26 @@ export class ContractManager {
|
|
|
190
186
|
}
|
|
191
187
|
async getContractsWithVtxos(filter, pageSize) {
|
|
192
188
|
const contracts = await this.getContracts(filter);
|
|
193
|
-
|
|
189
|
+
await this.syncContracts({ contracts, pageSize });
|
|
190
|
+
const vtxos = await this.getVtxosForContracts(contracts);
|
|
194
191
|
return contracts.map((contract) => ({
|
|
195
192
|
contract,
|
|
196
|
-
vtxos: vtxos.
|
|
193
|
+
vtxos: vtxos.filter((vtxo) => vtxo.contractScript === contract.script),
|
|
197
194
|
}));
|
|
198
195
|
}
|
|
196
|
+
async annotateVtxos(vtxos) {
|
|
197
|
+
if (vtxos.length === 0)
|
|
198
|
+
return [];
|
|
199
|
+
const scripts = Array.from(new Set(vtxos.map((v) => v.script)));
|
|
200
|
+
const byScript = new Map();
|
|
201
|
+
const contracts = await this.config.contractRepository.getContracts({
|
|
202
|
+
script: scripts,
|
|
203
|
+
});
|
|
204
|
+
for (const contract of contracts) {
|
|
205
|
+
byScript.set(contract.script, contract);
|
|
206
|
+
}
|
|
207
|
+
return vtxos.map((vtxo) => extendVirtualCoinForContract(vtxo, byScript));
|
|
208
|
+
}
|
|
199
209
|
buildContractsDbFilter(filter) {
|
|
200
210
|
return {
|
|
201
211
|
script: filter.script,
|
|
@@ -336,41 +346,18 @@ export class ContractManager {
|
|
|
336
346
|
/**
|
|
337
347
|
* Force refresh virtual outputs from the indexer.
|
|
338
348
|
*
|
|
339
|
-
* Without options,
|
|
349
|
+
* Without options, re-fetches every contract and advances the global cursor.
|
|
340
350
|
* With options, narrows the refresh to specific scripts and/or a time window.
|
|
351
|
+
* Subset refreshes (scripts filter) intentionally do not advance the cursor.
|
|
341
352
|
*/
|
|
342
353
|
async refreshVtxos(opts) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const scriptSet = new Set(opts.scripts);
|
|
346
|
-
contracts = contracts.filter((c) => scriptSet.has(c.script));
|
|
347
|
-
}
|
|
348
|
-
const syncWindow = opts?.after !== undefined || opts?.before !== undefined
|
|
349
|
-
? {
|
|
350
|
-
after: opts.after ?? 0,
|
|
351
|
-
before: opts.before ?? Date.now(),
|
|
352
|
-
}
|
|
354
|
+
const contracts = opts?.scripts
|
|
355
|
+
? await this.getContracts({ script: opts.scripts })
|
|
353
356
|
: undefined;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
await clearSyncCursors(this.config.walletRepository);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const requestStartedAt = Date.now();
|
|
364
|
-
const fetched = await this.fetchContractVxosFromIndexer(contracts, true, undefined, syncWindow);
|
|
365
|
-
// Persist cursors so subsequent incremental syncs don't re-bootstrap.
|
|
366
|
-
const cutoff = cursorCutoff(requestStartedAt);
|
|
367
|
-
const cursorUpdates = {};
|
|
368
|
-
for (const script of fetched.keys()) {
|
|
369
|
-
cursorUpdates[script] = cutoff;
|
|
370
|
-
}
|
|
371
|
-
if (Object.keys(cursorUpdates).length > 0) {
|
|
372
|
-
await advanceSyncCursors(this.config.walletRepository, cursorUpdates);
|
|
373
|
-
}
|
|
357
|
+
await this.syncContracts({
|
|
358
|
+
contracts,
|
|
359
|
+
window: { after: opts?.after, before: opts?.before },
|
|
360
|
+
});
|
|
374
361
|
}
|
|
375
362
|
/**
|
|
376
363
|
* Check if currently watching.
|
|
@@ -399,94 +386,54 @@ export class ContractManager {
|
|
|
399
386
|
// Delta-sync only the changed virtual outputs for this contract.
|
|
400
387
|
case "vtxo_received":
|
|
401
388
|
case "vtxo_spent":
|
|
402
|
-
await this.
|
|
389
|
+
await this.syncContracts({ contracts: [event.contract] });
|
|
403
390
|
break;
|
|
404
|
-
case "connection_reset":
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
391
|
+
case "connection_reset":
|
|
392
|
+
// Same recovery path as boot: delta-sync the watched set
|
|
393
|
+
// and reconcile the pending frontier. `advanceSyncCursor`
|
|
394
|
+
// is monotonic so this never rewinds the cursor.
|
|
395
|
+
await this.reconcileWatched();
|
|
408
396
|
break;
|
|
409
|
-
}
|
|
410
|
-
case "contract_expired":
|
|
411
|
-
// just update DB
|
|
412
|
-
await this.config.contractRepository.saveContract(event.contract);
|
|
413
397
|
}
|
|
414
398
|
// Forward to all callbacks
|
|
415
399
|
this.emitEvent(event);
|
|
416
400
|
}
|
|
417
|
-
async getVtxosForContracts(contracts
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// reference so the next call triggers a fresh fetch.
|
|
424
|
-
// TODO: can be removed once we fix the persistence layer (address vs scripts)
|
|
425
|
-
if (this.syncVtxosCallInflight) {
|
|
426
|
-
return this.syncVtxosCallInflight;
|
|
427
|
-
}
|
|
428
|
-
this.syncVtxosCallInflight = this.fetchContractVxosFromIndexer(contracts, true, pageSize).finally(() => {
|
|
429
|
-
this.syncVtxosCallInflight = undefined;
|
|
430
|
-
});
|
|
431
|
-
return this.syncVtxosCallInflight;
|
|
401
|
+
async getVtxosForContracts(contracts) {
|
|
402
|
+
const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) => vtxos.map((vtxo) => ({
|
|
403
|
+
...vtxo,
|
|
404
|
+
contractScript: script,
|
|
405
|
+
})))));
|
|
406
|
+
return res.flat();
|
|
432
407
|
}
|
|
433
408
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
409
|
+
* Sync virtual outputs for the given contracts against the indexer.
|
|
410
|
+
*
|
|
411
|
+
* When `options.contracts` is omitted the sync covers the full
|
|
412
|
+
* watched set (active contracts plus any inactive contracts still
|
|
413
|
+
* holding cached VTXOs) and the global cursor is advanced on
|
|
414
|
+
* success. Passing an explicit subset leaves the cursor alone so a
|
|
415
|
+
* narrow poll can't hide data that other contracts still need to
|
|
416
|
+
* pick up.
|
|
437
417
|
*/
|
|
438
|
-
async
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
delta.push(c);
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
bootstrap.push(c);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
const result = new Map();
|
|
462
|
-
const cursorUpdates = {};
|
|
463
|
-
// Full bootstrap for new scripts.
|
|
464
|
-
if (bootstrap.length > 0) {
|
|
465
|
-
const requestStartedAt = Date.now();
|
|
466
|
-
const fetched = await this.fetchContractVxosFromIndexer(bootstrap, true);
|
|
418
|
+
async syncContracts(options) {
|
|
419
|
+
const cursor = await getSyncCursor(this.config.walletRepository);
|
|
420
|
+
const window = options.window ?? computeSyncWindow(cursor);
|
|
421
|
+
// Advance the global cursor only on full-scope, cursor-derived delta
|
|
422
|
+
// syncs. A caller-supplied window is targeted (e.g. `refreshVtxos`)
|
|
423
|
+
// and must not move the cursor — it may skip data outside its bounds.
|
|
424
|
+
// `<=` lets the bootstrap case (cursor=0, window.after=0) write the
|
|
425
|
+
// migration marker on first boot; otherwise the marker would never
|
|
426
|
+
// be written and every subsequent boot would treat the cursor as
|
|
427
|
+
// legacy and re-bootstrap.
|
|
428
|
+
const mustUpdateCursor = options.contracts === undefined &&
|
|
429
|
+
options.window === undefined &&
|
|
430
|
+
(window.after ?? 0) <= cursor;
|
|
431
|
+
const contracts = options.contracts ?? this.watcher.getWatchedContracts();
|
|
432
|
+
const requestStartedAt = Date.now();
|
|
433
|
+
const result = await this.fetchContractVxosFromIndexer(contracts, options.pageSize, window);
|
|
434
|
+
if (mustUpdateCursor) {
|
|
467
435
|
const cutoff = cursorCutoff(requestStartedAt);
|
|
468
|
-
|
|
469
|
-
result.set(script, vtxos);
|
|
470
|
-
cursorUpdates[script] = cutoff;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
// Delta sync for scripts with an existing cursor.
|
|
474
|
-
if (delta.length > 0) {
|
|
475
|
-
// Use the oldest cursor so the shared window covers every script.
|
|
476
|
-
const minCursor = Math.min(...delta.map((c) => cursors[c.script]));
|
|
477
|
-
const window = computeSyncWindow(minCursor);
|
|
478
|
-
if (window) {
|
|
479
|
-
const requestStartedAt = Date.now();
|
|
480
|
-
const fetched = await this.fetchContractVxosFromIndexer(delta, true, pageSize, window);
|
|
481
|
-
const cutoff = cursorCutoff(requestStartedAt);
|
|
482
|
-
for (const [script, vtxos] of fetched) {
|
|
483
|
-
result.set(script, vtxos);
|
|
484
|
-
cursorUpdates[script] = cutoff;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (Object.keys(cursorUpdates).length > 0) {
|
|
489
|
-
await advanceSyncCursors(this.config.walletRepository, cursorUpdates);
|
|
436
|
+
await advanceSyncCursor(this.config.walletRepository, cutoff);
|
|
490
437
|
}
|
|
491
438
|
return result;
|
|
492
439
|
}
|
|
@@ -502,21 +449,20 @@ export class ContractManager {
|
|
|
502
449
|
scripts,
|
|
503
450
|
pendingOnly: true,
|
|
504
451
|
});
|
|
505
|
-
//
|
|
452
|
+
// Share the annotation path with external callers so the two entry
|
|
453
|
+
// points can't drift.
|
|
454
|
+
const owned = vtxos.filter((v) => scriptToContract.has(v.script));
|
|
455
|
+
const annotated = await this.annotateVtxos(owned);
|
|
506
456
|
const byContract = new Map();
|
|
507
|
-
for (const vtxo of
|
|
508
|
-
if (!vtxo.script)
|
|
509
|
-
continue;
|
|
457
|
+
for (const vtxo of annotated) {
|
|
510
458
|
const contract = scriptToContract.get(vtxo.script);
|
|
511
|
-
if (!contract)
|
|
512
|
-
continue;
|
|
513
459
|
let arr = byContract.get(contract.address);
|
|
514
460
|
if (!arr) {
|
|
515
461
|
arr = [];
|
|
516
462
|
byContract.set(contract.address, arr);
|
|
517
463
|
}
|
|
518
464
|
arr.push({
|
|
519
|
-
...
|
|
465
|
+
...vtxo,
|
|
520
466
|
contractScript: contract.script,
|
|
521
467
|
});
|
|
522
468
|
}
|
|
@@ -524,8 +470,8 @@ export class ContractManager {
|
|
|
524
470
|
await this.config.walletRepository.saveVtxos(addr, contractVtxos);
|
|
525
471
|
}
|
|
526
472
|
}
|
|
527
|
-
async fetchContractVxosFromIndexer(contracts,
|
|
528
|
-
const fetched = await this.fetchContractVtxosBulk(contracts,
|
|
473
|
+
async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
|
|
474
|
+
const fetched = await this.fetchContractVtxosBulk(contracts, pageSize, syncWindow);
|
|
529
475
|
const result = new Map();
|
|
530
476
|
for (const [contractScript, vtxos] of fetched) {
|
|
531
477
|
result.set(contractScript, vtxos);
|
|
@@ -536,23 +482,17 @@ export class ContractManager {
|
|
|
536
482
|
}
|
|
537
483
|
return result;
|
|
538
484
|
}
|
|
539
|
-
async fetchContractVtxosBulk(contracts,
|
|
485
|
+
async fetchContractVtxosBulk(contracts, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
|
|
540
486
|
if (contracts.length === 0) {
|
|
541
487
|
return new Map();
|
|
542
488
|
}
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return new Map([[contract.script, vtxos]]);
|
|
548
|
-
}
|
|
549
|
-
// For multiple contracts, batch all scripts into a single indexer call
|
|
550
|
-
// per page to minimise round-trips. Results are keyed by script so we
|
|
551
|
-
// can distribute them back to the correct contract afterwards.
|
|
489
|
+
// Batch all scripts into a single indexer call per page to minimise
|
|
490
|
+
// round-trips. Results are keyed by script so we can distribute them
|
|
491
|
+
// back to the correct contract afterwards. Always fetches the full
|
|
492
|
+
// history (spent/swept included) so the repo is the source of truth.
|
|
552
493
|
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
553
494
|
const result = new Map(contracts.map((c) => [c.script, []]));
|
|
554
495
|
const scripts = contracts.map((c) => c.script);
|
|
555
|
-
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
556
496
|
const windowOpts = syncWindow
|
|
557
497
|
? {
|
|
558
498
|
...(syncWindow.after !== undefined && {
|
|
@@ -568,22 +508,20 @@ export class ContractManager {
|
|
|
568
508
|
while (hasMore) {
|
|
569
509
|
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
570
510
|
scripts,
|
|
571
|
-
...opts,
|
|
572
511
|
...windowOpts,
|
|
573
512
|
pageIndex,
|
|
574
513
|
pageSize,
|
|
575
514
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
contractScript: contract.script,
|
|
515
|
+
// Match virtual outputs back to their contract via the script field
|
|
516
|
+
// populated by the indexer, then share the annotation path with
|
|
517
|
+
// external callers via annotateVtxos so the two entry points can't
|
|
518
|
+
// drift.
|
|
519
|
+
const owned = vtxos.filter((v) => scriptToContract.has(v.script));
|
|
520
|
+
const annotated = await this.annotateVtxos(owned);
|
|
521
|
+
for (const vtxo of annotated) {
|
|
522
|
+
result.get(vtxo.script).push({
|
|
523
|
+
...vtxo,
|
|
524
|
+
contractScript: vtxo.script,
|
|
587
525
|
});
|
|
588
526
|
}
|
|
589
527
|
hasMore = page ? vtxos.length === pageSize : false;
|
|
@@ -593,42 +531,6 @@ export class ContractManager {
|
|
|
593
531
|
}
|
|
594
532
|
return result;
|
|
595
533
|
}
|
|
596
|
-
async fetchContractVtxosPaginated(contract, includeSpent, pageSize = DEFAULT_PAGE_SIZE, syncWindow) {
|
|
597
|
-
const allVtxos = [];
|
|
598
|
-
let pageIndex = 0;
|
|
599
|
-
let hasMore = true;
|
|
600
|
-
const opts = includeSpent ? {} : { spendableOnly: true };
|
|
601
|
-
const windowOpts = syncWindow
|
|
602
|
-
? {
|
|
603
|
-
...(syncWindow.after !== undefined && {
|
|
604
|
-
after: syncWindow.after,
|
|
605
|
-
}),
|
|
606
|
-
...(syncWindow.before !== undefined && {
|
|
607
|
-
before: syncWindow.before,
|
|
608
|
-
}),
|
|
609
|
-
}
|
|
610
|
-
: {};
|
|
611
|
-
while (hasMore) {
|
|
612
|
-
const { vtxos, page } = await this.config.indexerProvider.getVtxos({
|
|
613
|
-
scripts: [contract.script],
|
|
614
|
-
...opts,
|
|
615
|
-
...windowOpts,
|
|
616
|
-
pageIndex,
|
|
617
|
-
pageSize,
|
|
618
|
-
});
|
|
619
|
-
for (const vtxo of vtxos) {
|
|
620
|
-
allVtxos.push({
|
|
621
|
-
...extendVtxoFromContract(vtxo, contract),
|
|
622
|
-
contractScript: contract.script,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
hasMore = page ? vtxos.length === pageSize : false;
|
|
626
|
-
pageIndex++;
|
|
627
|
-
if (hasMore)
|
|
628
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
629
|
-
}
|
|
630
|
-
return allVtxos;
|
|
631
|
-
}
|
|
632
534
|
/**
|
|
633
535
|
* Dispose of the ContractManager and release all resources.
|
|
634
536
|
*
|
|
@@ -657,12 +559,6 @@ export class ContractManager {
|
|
|
657
559
|
* ```
|
|
658
560
|
*/
|
|
659
561
|
[Symbol.dispose]() {
|
|
660
|
-
|
|
661
|
-
this.stopWatcherFn?.();
|
|
662
|
-
this.stopWatcherFn = undefined;
|
|
663
|
-
// Clear callbacks
|
|
664
|
-
this.eventCallbacks.clear();
|
|
665
|
-
// Mark as uninitialized
|
|
666
|
-
this.initialized = false;
|
|
562
|
+
this.dispose();
|
|
667
563
|
}
|
|
668
564
|
}
|