@orbinum/sdk 1.2.0 → 1.3.0
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/index.d.mts +17 -6
- package/dist/index.d.ts +17 -6
- package/dist/index.js +162 -97
- package/dist/index.mjs +151 -86
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -4503,9 +4503,9 @@ declare function windowSizeForCounter(counter: number): number;
|
|
|
4503
4503
|
* Phase 1 — walk the hint feed and trial-decrypt every memo against the wallet's
|
|
4504
4504
|
* keys.
|
|
4505
4505
|
*
|
|
4506
|
-
* Pure collection:
|
|
4507
|
-
*
|
|
4508
|
-
*
|
|
4506
|
+
* Pure collection: this phase itself writes nothing — persistence happens only
|
|
4507
|
+
* through the injected `onPage`/`onBatchDone` callbacks, so the caller decides
|
|
4508
|
+
* what (if anything) is checkpointed while the scan runs.
|
|
4509
4509
|
*
|
|
4510
4510
|
* Transport, in order of preference:
|
|
4511
4511
|
* 1. Sealed chunks — the immutable bulk of the feed, digest-addressed and
|
|
@@ -4600,6 +4600,15 @@ interface CollectScanEntriesParams {
|
|
|
4600
4600
|
note: ZkNote;
|
|
4601
4601
|
isNew: boolean;
|
|
4602
4602
|
}>) => Promise<void>) | undefined;
|
|
4603
|
+
/**
|
|
4604
|
+
* Awaited after each fully processed chunk/page — AFTER `onPage`, so the
|
|
4605
|
+
* batch's notes are already in the caller's hands — with the highest valid
|
|
4606
|
+
* leaf seen so far. Fires for every batch, including ones with no owned
|
|
4607
|
+
* notes: that is the common case, and the checkpoint must advance past
|
|
4608
|
+
* other people's leaves too. Lets callers persist the scan cursor
|
|
4609
|
+
* incrementally so an aborted scan resumes instead of restarting.
|
|
4610
|
+
*/
|
|
4611
|
+
onBatchDone?: ((maxLeafIndex: number | undefined) => Promise<void>) | undefined;
|
|
4603
4612
|
/**
|
|
4604
4613
|
* Bounds `outcome.onChainHexes` to commitments the wallet actually holds.
|
|
4605
4614
|
* Pass the pre-scan snapshot, since `existingHexes` grows during the scan.
|
|
@@ -4678,14 +4687,16 @@ declare function resolveSpentStatus(params: {
|
|
|
4678
4687
|
* casing between runs cannot split one nullifier into two entries.
|
|
4679
4688
|
*/
|
|
4680
4689
|
|
|
4681
|
-
interface
|
|
4690
|
+
interface OpenSpentSetParams {
|
|
4682
4691
|
source: NullifierSource;
|
|
4683
4692
|
cache: NullifierCache;
|
|
4684
|
-
/** The wallet's own nullifiers, lowercase hex. */
|
|
4685
|
-
ownNullifiers: Set<string>;
|
|
4686
4693
|
signal?: AbortSignal | undefined;
|
|
4687
4694
|
onWarning?: ((message: string, cause?: unknown) => void) | undefined;
|
|
4688
4695
|
}
|
|
4696
|
+
interface ResolveSpentSetParams extends OpenSpentSetParams {
|
|
4697
|
+
/** The wallet's own nullifiers, lowercase hex. */
|
|
4698
|
+
ownNullifiers: Set<string>;
|
|
4699
|
+
}
|
|
4689
4700
|
/**
|
|
4690
4701
|
* Returns ONLY the spent members of `ownNullifiers` as a Map hex → spend details
|
|
4691
4702
|
* (block timestamp + spending tx hash, each null when the row carried none).
|
package/dist/index.d.ts
CHANGED
|
@@ -4503,9 +4503,9 @@ declare function windowSizeForCounter(counter: number): number;
|
|
|
4503
4503
|
* Phase 1 — walk the hint feed and trial-decrypt every memo against the wallet's
|
|
4504
4504
|
* keys.
|
|
4505
4505
|
*
|
|
4506
|
-
* Pure collection:
|
|
4507
|
-
*
|
|
4508
|
-
*
|
|
4506
|
+
* Pure collection: this phase itself writes nothing — persistence happens only
|
|
4507
|
+
* through the injected `onPage`/`onBatchDone` callbacks, so the caller decides
|
|
4508
|
+
* what (if anything) is checkpointed while the scan runs.
|
|
4509
4509
|
*
|
|
4510
4510
|
* Transport, in order of preference:
|
|
4511
4511
|
* 1. Sealed chunks — the immutable bulk of the feed, digest-addressed and
|
|
@@ -4600,6 +4600,15 @@ interface CollectScanEntriesParams {
|
|
|
4600
4600
|
note: ZkNote;
|
|
4601
4601
|
isNew: boolean;
|
|
4602
4602
|
}>) => Promise<void>) | undefined;
|
|
4603
|
+
/**
|
|
4604
|
+
* Awaited after each fully processed chunk/page — AFTER `onPage`, so the
|
|
4605
|
+
* batch's notes are already in the caller's hands — with the highest valid
|
|
4606
|
+
* leaf seen so far. Fires for every batch, including ones with no owned
|
|
4607
|
+
* notes: that is the common case, and the checkpoint must advance past
|
|
4608
|
+
* other people's leaves too. Lets callers persist the scan cursor
|
|
4609
|
+
* incrementally so an aborted scan resumes instead of restarting.
|
|
4610
|
+
*/
|
|
4611
|
+
onBatchDone?: ((maxLeafIndex: number | undefined) => Promise<void>) | undefined;
|
|
4603
4612
|
/**
|
|
4604
4613
|
* Bounds `outcome.onChainHexes` to commitments the wallet actually holds.
|
|
4605
4614
|
* Pass the pre-scan snapshot, since `existingHexes` grows during the scan.
|
|
@@ -4678,14 +4687,16 @@ declare function resolveSpentStatus(params: {
|
|
|
4678
4687
|
* casing between runs cannot split one nullifier into two entries.
|
|
4679
4688
|
*/
|
|
4680
4689
|
|
|
4681
|
-
interface
|
|
4690
|
+
interface OpenSpentSetParams {
|
|
4682
4691
|
source: NullifierSource;
|
|
4683
4692
|
cache: NullifierCache;
|
|
4684
|
-
/** The wallet's own nullifiers, lowercase hex. */
|
|
4685
|
-
ownNullifiers: Set<string>;
|
|
4686
4693
|
signal?: AbortSignal | undefined;
|
|
4687
4694
|
onWarning?: ((message: string, cause?: unknown) => void) | undefined;
|
|
4688
4695
|
}
|
|
4696
|
+
interface ResolveSpentSetParams extends OpenSpentSetParams {
|
|
4697
|
+
/** The wallet's own nullifiers, lowercase hex. */
|
|
4698
|
+
ownNullifiers: Set<string>;
|
|
4699
|
+
}
|
|
4689
4700
|
/**
|
|
4690
4701
|
* Returns ONLY the spent members of `ownNullifiers` as a Map hex → spend details
|
|
4691
4702
|
* (block timestamp + spending tx hash, each null when the row carried none).
|
package/dist/index.js
CHANGED
|
@@ -5924,6 +5924,7 @@ async function processHints(ctx, hints, total) {
|
|
|
5924
5924
|
pageEntries.push({ note, isNew });
|
|
5925
5925
|
}
|
|
5926
5926
|
if (pageEntries.length > 0) await ctx.onPage?.(pageEntries);
|
|
5927
|
+
await ctx.onBatchDone?.(outcome.maxLeafIndex);
|
|
5927
5928
|
ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
|
|
5928
5929
|
}
|
|
5929
5930
|
async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
@@ -5958,6 +5959,7 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
|
5958
5959
|
}
|
|
5959
5960
|
async function collectScanEntries(params) {
|
|
5960
5961
|
const { source, pool, keys, existingHexes, sinceLeafIndex, onProgress, signal, onPage } = params;
|
|
5962
|
+
const { onBatchDone } = params;
|
|
5961
5963
|
if (signal?.aborted) throw scanAbortError();
|
|
5962
5964
|
const outcome = {
|
|
5963
5965
|
scanEntries: [],
|
|
@@ -5981,7 +5983,8 @@ async function collectScanEntries(params) {
|
|
|
5981
5983
|
vaultHexes: params.vaultHexes ?? existingHexes,
|
|
5982
5984
|
onProgress,
|
|
5983
5985
|
signal,
|
|
5984
|
-
onPage
|
|
5986
|
+
onPage,
|
|
5987
|
+
onBatchDone
|
|
5985
5988
|
};
|
|
5986
5989
|
let tailSince = sinceLeafIndex;
|
|
5987
5990
|
try {
|
|
@@ -6022,63 +6025,12 @@ async function collectScanEntries(params) {
|
|
|
6022
6025
|
return outcome;
|
|
6023
6026
|
}
|
|
6024
6027
|
|
|
6025
|
-
// src/wallet/worker/kernel/types.ts
|
|
6026
|
-
var SELF_EPH_WINDOW = 1024;
|
|
6027
|
-
var PAIRWISE_EPH_WINDOW = 64;
|
|
6028
|
-
var EMPTY_BATCH_RESULT = {
|
|
6029
|
-
notes: [],
|
|
6030
|
-
tagFiltered: 0,
|
|
6031
|
-
selfMatched: 0,
|
|
6032
|
-
pairwiseMatched: 0,
|
|
6033
|
-
maxSelfEphIndex: null
|
|
6034
|
-
};
|
|
6035
|
-
|
|
6036
|
-
// src/wallet/scanner/selfEphGap.ts
|
|
6037
|
-
function noteEphPkHex(note) {
|
|
6038
|
-
if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
|
|
6039
|
-
return toHex(note.memo.slice(-32)).toLowerCase();
|
|
6040
|
-
}
|
|
6041
|
-
function gapMargin(windowSize) {
|
|
6042
|
-
return Math.max(1, Math.floor(windowSize / 16));
|
|
6043
|
-
}
|
|
6044
|
-
function resolveSelfEphCeiling(params) {
|
|
6045
|
-
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
6046
|
-
const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
|
|
6047
|
-
if (scanMaxIndex === null) return null;
|
|
6048
|
-
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
6049
|
-
const ownEphPks = /* @__PURE__ */ new Set();
|
|
6050
|
-
for (const note of notes) {
|
|
6051
|
-
const hex = noteEphPkHex(note);
|
|
6052
|
-
if (hex) ownEphPks.add(hex);
|
|
6053
|
-
}
|
|
6054
|
-
const ivkPacked = deriveViewingPublicKey(viewingKey);
|
|
6055
|
-
let max = scanMaxIndex;
|
|
6056
|
-
let from = windowSize;
|
|
6057
|
-
for (; ; ) {
|
|
6058
|
-
const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
|
|
6059
|
-
let matched = false;
|
|
6060
|
-
for (const entry of slice) {
|
|
6061
|
-
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
6062
|
-
matched = true;
|
|
6063
|
-
if (entry.index > max) max = entry.index;
|
|
6064
|
-
}
|
|
6065
|
-
}
|
|
6066
|
-
if (!matched) break;
|
|
6067
|
-
from += windowSize;
|
|
6068
|
-
}
|
|
6069
|
-
return max;
|
|
6070
|
-
}
|
|
6071
|
-
function windowSizeForCounter(counter) {
|
|
6072
|
-
const needed = counter + gapMargin(SELF_EPH_WINDOW);
|
|
6073
|
-
return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
|
|
6074
|
-
}
|
|
6075
|
-
|
|
6076
6028
|
// src/wallet/scanner/phases/spentSet.ts
|
|
6077
6029
|
function normalizeNullifierHex(hex) {
|
|
6078
6030
|
return hex.toLowerCase();
|
|
6079
6031
|
}
|
|
6080
|
-
async function
|
|
6081
|
-
const { source, cache,
|
|
6032
|
+
async function openSpentSet(params) {
|
|
6033
|
+
const { source, cache, signal } = params;
|
|
6082
6034
|
const throwIfAborted = () => {
|
|
6083
6035
|
if (signal?.aborted) throw scanAbortError();
|
|
6084
6036
|
};
|
|
@@ -6087,20 +6039,29 @@ async function resolveSpentSet(params) {
|
|
|
6087
6039
|
await syncNullifierCache(params, manifest, throwIfAborted);
|
|
6088
6040
|
throwIfAborted();
|
|
6089
6041
|
const tail = await getConsistentTail(params, throwIfAborted);
|
|
6090
|
-
const stored = await cache.getSpentNullifiers([...ownNullifiers]);
|
|
6091
6042
|
const tailMap = new Map(
|
|
6092
6043
|
tail.data.map((h, i) => [
|
|
6093
6044
|
normalizeNullifierHex(h),
|
|
6094
6045
|
{ spentAt: tail.timestampsMs?.[i] ?? null, txHash: tail.txHashes?.[i] ?? null }
|
|
6095
6046
|
])
|
|
6096
6047
|
);
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6048
|
+
return {
|
|
6049
|
+
async query(ownNullifiers) {
|
|
6050
|
+
if (ownNullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
6051
|
+
const stored = await cache.getSpentNullifiers([...ownNullifiers]);
|
|
6052
|
+
const spent = /* @__PURE__ */ new Map();
|
|
6053
|
+
const unknown = { spentAt: null, txHash: null };
|
|
6054
|
+
for (const h of ownNullifiers) {
|
|
6055
|
+
if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
|
|
6056
|
+
else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
|
|
6057
|
+
}
|
|
6058
|
+
return spent;
|
|
6059
|
+
}
|
|
6060
|
+
};
|
|
6061
|
+
}
|
|
6062
|
+
async function resolveSpentSet(params) {
|
|
6063
|
+
const handle = await openSpentSet(params);
|
|
6064
|
+
return handle.query(params.ownNullifiers);
|
|
6104
6065
|
}
|
|
6105
6066
|
async function syncNullifierCache(params, manifest, throwIfAborted) {
|
|
6106
6067
|
const { source, cache } = params;
|
|
@@ -6150,40 +6111,6 @@ async function getConsistentTail(params, throwIfAborted) {
|
|
|
6150
6111
|
throw new Error("nullifier tail inconsistent with local chunks after retry");
|
|
6151
6112
|
}
|
|
6152
6113
|
|
|
6153
|
-
// src/wallet/scanner/phases/spentStatus.ts
|
|
6154
|
-
function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
|
|
6155
|
-
const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
|
|
6156
|
-
const nullifiers = /* @__PURE__ */ new Set();
|
|
6157
|
-
for (const { note } of scanEntries) {
|
|
6158
|
-
nullifiers.add(note.nullifierHex.toLowerCase());
|
|
6159
|
-
}
|
|
6160
|
-
for (const vaultNote of vaultNotes) {
|
|
6161
|
-
if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
|
|
6162
|
-
if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
|
|
6163
|
-
if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
|
|
6164
|
-
}
|
|
6165
|
-
return nullifiers;
|
|
6166
|
-
}
|
|
6167
|
-
async function resolveSpentStatus(params) {
|
|
6168
|
-
if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
6169
|
-
try {
|
|
6170
|
-
return await resolveSpentSet({
|
|
6171
|
-
source: params.source,
|
|
6172
|
-
cache: params.cache,
|
|
6173
|
-
ownNullifiers: params.nullifiers,
|
|
6174
|
-
signal: params.signal,
|
|
6175
|
-
onWarning: params.onWarning
|
|
6176
|
-
});
|
|
6177
|
-
} catch (err) {
|
|
6178
|
-
if (isAbortError(err)) throw err;
|
|
6179
|
-
params.onWarning?.(
|
|
6180
|
-
"could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
|
|
6181
|
-
err
|
|
6182
|
-
);
|
|
6183
|
-
return /* @__PURE__ */ new Map();
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6186
|
-
|
|
6187
6114
|
// src/wallet/scanner/phases/persist.ts
|
|
6188
6115
|
function purgeIsTrustworthy(hintsScanned, purgeableNotes) {
|
|
6189
6116
|
if (purgeableNotes === 0) return true;
|
|
@@ -6242,6 +6169,134 @@ async function persistCursor(storage, maxLeafIndex, isIncremental) {
|
|
|
6242
6169
|
});
|
|
6243
6170
|
}
|
|
6244
6171
|
|
|
6172
|
+
// src/wallet/scanner/phases/checkpoint.ts
|
|
6173
|
+
function createScanCheckpoint(params) {
|
|
6174
|
+
const { vault, storage, nullifiers, isIncremental, signal, onWarning } = params;
|
|
6175
|
+
const throwIfAborted = () => {
|
|
6176
|
+
if (signal?.aborted) throw scanAbortError();
|
|
6177
|
+
};
|
|
6178
|
+
let spentSetPromise = null;
|
|
6179
|
+
const getSpentSet = () => spentSetPromise ??= openSpentSet({
|
|
6180
|
+
source: nullifiers,
|
|
6181
|
+
cache: storage,
|
|
6182
|
+
signal,
|
|
6183
|
+
onWarning
|
|
6184
|
+
}).catch(() => null);
|
|
6185
|
+
return {
|
|
6186
|
+
async savePageNotes(entries) {
|
|
6187
|
+
throwIfAborted();
|
|
6188
|
+
const fresh = entries.filter((e) => e.isNew);
|
|
6189
|
+
if (fresh.length === 0) return;
|
|
6190
|
+
const own = new Set(fresh.map((e) => e.note.nullifierHex.toLowerCase()));
|
|
6191
|
+
const spentSet = await getSpentSet();
|
|
6192
|
+
const spent = spentSet ? await spentSet.query(own).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
|
|
6193
|
+
throwIfAborted();
|
|
6194
|
+
await vault.saveMany(
|
|
6195
|
+
fresh.map(({ note }) => {
|
|
6196
|
+
const spend = spent.get(note.nullifierHex.toLowerCase());
|
|
6197
|
+
return {
|
|
6198
|
+
note: spend ? stampSpentTxHash(note, spend.txHash) : note,
|
|
6199
|
+
noteStatus: {
|
|
6200
|
+
spent: spend !== void 0,
|
|
6201
|
+
spentAt: spend?.spentAt ?? null
|
|
6202
|
+
}
|
|
6203
|
+
};
|
|
6204
|
+
})
|
|
6205
|
+
);
|
|
6206
|
+
},
|
|
6207
|
+
async advanceCursor(maxLeafIndex) {
|
|
6208
|
+
if (maxLeafIndex === void 0) return;
|
|
6209
|
+
throwIfAborted();
|
|
6210
|
+
await persistCursor(storage, maxLeafIndex, isIncremental);
|
|
6211
|
+
}
|
|
6212
|
+
};
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
// src/wallet/worker/kernel/types.ts
|
|
6216
|
+
var SELF_EPH_WINDOW = 1024;
|
|
6217
|
+
var PAIRWISE_EPH_WINDOW = 64;
|
|
6218
|
+
var EMPTY_BATCH_RESULT = {
|
|
6219
|
+
notes: [],
|
|
6220
|
+
tagFiltered: 0,
|
|
6221
|
+
selfMatched: 0,
|
|
6222
|
+
pairwiseMatched: 0,
|
|
6223
|
+
maxSelfEphIndex: null
|
|
6224
|
+
};
|
|
6225
|
+
|
|
6226
|
+
// src/wallet/scanner/selfEphGap.ts
|
|
6227
|
+
function noteEphPkHex(note) {
|
|
6228
|
+
if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
|
|
6229
|
+
return toHex(note.memo.slice(-32)).toLowerCase();
|
|
6230
|
+
}
|
|
6231
|
+
function gapMargin(windowSize) {
|
|
6232
|
+
return Math.max(1, Math.floor(windowSize / 16));
|
|
6233
|
+
}
|
|
6234
|
+
function resolveSelfEphCeiling(params) {
|
|
6235
|
+
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
6236
|
+
const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
|
|
6237
|
+
if (scanMaxIndex === null) return null;
|
|
6238
|
+
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
6239
|
+
const ownEphPks = /* @__PURE__ */ new Set();
|
|
6240
|
+
for (const note of notes) {
|
|
6241
|
+
const hex = noteEphPkHex(note);
|
|
6242
|
+
if (hex) ownEphPks.add(hex);
|
|
6243
|
+
}
|
|
6244
|
+
const ivkPacked = deriveViewingPublicKey(viewingKey);
|
|
6245
|
+
let max = scanMaxIndex;
|
|
6246
|
+
let from = windowSize;
|
|
6247
|
+
for (; ; ) {
|
|
6248
|
+
const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
|
|
6249
|
+
let matched = false;
|
|
6250
|
+
for (const entry of slice) {
|
|
6251
|
+
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
6252
|
+
matched = true;
|
|
6253
|
+
if (entry.index > max) max = entry.index;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
if (!matched) break;
|
|
6257
|
+
from += windowSize;
|
|
6258
|
+
}
|
|
6259
|
+
return max;
|
|
6260
|
+
}
|
|
6261
|
+
function windowSizeForCounter(counter) {
|
|
6262
|
+
const needed = counter + gapMargin(SELF_EPH_WINDOW);
|
|
6263
|
+
return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
|
|
6264
|
+
}
|
|
6265
|
+
|
|
6266
|
+
// src/wallet/scanner/phases/spentStatus.ts
|
|
6267
|
+
function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
|
|
6268
|
+
const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
|
|
6269
|
+
const nullifiers = /* @__PURE__ */ new Set();
|
|
6270
|
+
for (const { note } of scanEntries) {
|
|
6271
|
+
nullifiers.add(note.nullifierHex.toLowerCase());
|
|
6272
|
+
}
|
|
6273
|
+
for (const vaultNote of vaultNotes) {
|
|
6274
|
+
if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
|
|
6275
|
+
if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
|
|
6276
|
+
if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
|
|
6277
|
+
}
|
|
6278
|
+
return nullifiers;
|
|
6279
|
+
}
|
|
6280
|
+
async function resolveSpentStatus(params) {
|
|
6281
|
+
if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
6282
|
+
try {
|
|
6283
|
+
return await resolveSpentSet({
|
|
6284
|
+
source: params.source,
|
|
6285
|
+
cache: params.cache,
|
|
6286
|
+
ownNullifiers: params.nullifiers,
|
|
6287
|
+
signal: params.signal,
|
|
6288
|
+
onWarning: params.onWarning
|
|
6289
|
+
});
|
|
6290
|
+
} catch (err) {
|
|
6291
|
+
if (isAbortError(err)) throw err;
|
|
6292
|
+
params.onWarning?.(
|
|
6293
|
+
"could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
|
|
6294
|
+
err
|
|
6295
|
+
);
|
|
6296
|
+
return /* @__PURE__ */ new Map();
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
|
|
6245
6300
|
// src/wallet/scanner/pipeline.ts
|
|
6246
6301
|
async function runScan(params) {
|
|
6247
6302
|
const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
|
|
@@ -6260,6 +6315,14 @@ async function runScan(params) {
|
|
|
6260
6315
|
}
|
|
6261
6316
|
}
|
|
6262
6317
|
);
|
|
6318
|
+
const checkpoint = createScanCheckpoint({
|
|
6319
|
+
vault,
|
|
6320
|
+
storage,
|
|
6321
|
+
nullifiers,
|
|
6322
|
+
isIncremental,
|
|
6323
|
+
signal,
|
|
6324
|
+
onWarning
|
|
6325
|
+
});
|
|
6263
6326
|
const scan = await collectScanEntries({
|
|
6264
6327
|
source: hints,
|
|
6265
6328
|
pool,
|
|
@@ -6280,7 +6343,9 @@ async function runScan(params) {
|
|
|
6280
6343
|
// consumer probes it with a vault commitment, so keeping the whole pool
|
|
6281
6344
|
// only bought heap.
|
|
6282
6345
|
vaultHexes: preScanHexes,
|
|
6283
|
-
onWarning
|
|
6346
|
+
onWarning,
|
|
6347
|
+
onPage: (entries) => checkpoint.savePageNotes(entries),
|
|
6348
|
+
onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
|
|
6284
6349
|
});
|
|
6285
6350
|
const nullifiersToQuery = collectNullifiersToQuery(
|
|
6286
6351
|
scan.scanEntries,
|
package/dist/index.mjs
CHANGED
|
@@ -4698,6 +4698,7 @@ async function processHints(ctx, hints, total) {
|
|
|
4698
4698
|
pageEntries.push({ note, isNew });
|
|
4699
4699
|
}
|
|
4700
4700
|
if (pageEntries.length > 0) await ctx.onPage?.(pageEntries);
|
|
4701
|
+
await ctx.onBatchDone?.(outcome.maxLeafIndex);
|
|
4701
4702
|
ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
|
|
4702
4703
|
}
|
|
4703
4704
|
async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
@@ -4732,6 +4733,7 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
|
4732
4733
|
}
|
|
4733
4734
|
async function collectScanEntries(params) {
|
|
4734
4735
|
const { source, pool, keys, existingHexes, sinceLeafIndex, onProgress, signal, onPage } = params;
|
|
4736
|
+
const { onBatchDone } = params;
|
|
4735
4737
|
if (signal?.aborted) throw scanAbortError();
|
|
4736
4738
|
const outcome = {
|
|
4737
4739
|
scanEntries: [],
|
|
@@ -4755,7 +4757,8 @@ async function collectScanEntries(params) {
|
|
|
4755
4757
|
vaultHexes: params.vaultHexes ?? existingHexes,
|
|
4756
4758
|
onProgress,
|
|
4757
4759
|
signal,
|
|
4758
|
-
onPage
|
|
4760
|
+
onPage,
|
|
4761
|
+
onBatchDone
|
|
4759
4762
|
};
|
|
4760
4763
|
let tailSince = sinceLeafIndex;
|
|
4761
4764
|
try {
|
|
@@ -4796,52 +4799,12 @@ async function collectScanEntries(params) {
|
|
|
4796
4799
|
return outcome;
|
|
4797
4800
|
}
|
|
4798
4801
|
|
|
4799
|
-
// src/wallet/scanner/selfEphGap.ts
|
|
4800
|
-
function noteEphPkHex(note) {
|
|
4801
|
-
if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
|
|
4802
|
-
return toHex(note.memo.slice(-32)).toLowerCase();
|
|
4803
|
-
}
|
|
4804
|
-
function gapMargin(windowSize) {
|
|
4805
|
-
return Math.max(1, Math.floor(windowSize / 16));
|
|
4806
|
-
}
|
|
4807
|
-
function resolveSelfEphCeiling(params) {
|
|
4808
|
-
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
4809
|
-
const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
|
|
4810
|
-
if (scanMaxIndex === null) return null;
|
|
4811
|
-
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
4812
|
-
const ownEphPks = /* @__PURE__ */ new Set();
|
|
4813
|
-
for (const note of notes) {
|
|
4814
|
-
const hex = noteEphPkHex(note);
|
|
4815
|
-
if (hex) ownEphPks.add(hex);
|
|
4816
|
-
}
|
|
4817
|
-
const ivkPacked = deriveViewingPublicKey(viewingKey);
|
|
4818
|
-
let max = scanMaxIndex;
|
|
4819
|
-
let from = windowSize;
|
|
4820
|
-
for (; ; ) {
|
|
4821
|
-
const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
|
|
4822
|
-
let matched = false;
|
|
4823
|
-
for (const entry of slice) {
|
|
4824
|
-
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
4825
|
-
matched = true;
|
|
4826
|
-
if (entry.index > max) max = entry.index;
|
|
4827
|
-
}
|
|
4828
|
-
}
|
|
4829
|
-
if (!matched) break;
|
|
4830
|
-
from += windowSize;
|
|
4831
|
-
}
|
|
4832
|
-
return max;
|
|
4833
|
-
}
|
|
4834
|
-
function windowSizeForCounter(counter) {
|
|
4835
|
-
const needed = counter + gapMargin(SELF_EPH_WINDOW);
|
|
4836
|
-
return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
|
|
4837
|
-
}
|
|
4838
|
-
|
|
4839
4802
|
// src/wallet/scanner/phases/spentSet.ts
|
|
4840
4803
|
function normalizeNullifierHex(hex) {
|
|
4841
4804
|
return hex.toLowerCase();
|
|
4842
4805
|
}
|
|
4843
|
-
async function
|
|
4844
|
-
const { source, cache,
|
|
4806
|
+
async function openSpentSet(params) {
|
|
4807
|
+
const { source, cache, signal } = params;
|
|
4845
4808
|
const throwIfAborted = () => {
|
|
4846
4809
|
if (signal?.aborted) throw scanAbortError();
|
|
4847
4810
|
};
|
|
@@ -4850,20 +4813,29 @@ async function resolveSpentSet(params) {
|
|
|
4850
4813
|
await syncNullifierCache(params, manifest, throwIfAborted);
|
|
4851
4814
|
throwIfAborted();
|
|
4852
4815
|
const tail = await getConsistentTail(params, throwIfAborted);
|
|
4853
|
-
const stored = await cache.getSpentNullifiers([...ownNullifiers]);
|
|
4854
4816
|
const tailMap = new Map(
|
|
4855
4817
|
tail.data.map((h, i) => [
|
|
4856
4818
|
normalizeNullifierHex(h),
|
|
4857
4819
|
{ spentAt: tail.timestampsMs?.[i] ?? null, txHash: tail.txHashes?.[i] ?? null }
|
|
4858
4820
|
])
|
|
4859
4821
|
);
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4822
|
+
return {
|
|
4823
|
+
async query(ownNullifiers) {
|
|
4824
|
+
if (ownNullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
4825
|
+
const stored = await cache.getSpentNullifiers([...ownNullifiers]);
|
|
4826
|
+
const spent = /* @__PURE__ */ new Map();
|
|
4827
|
+
const unknown = { spentAt: null, txHash: null };
|
|
4828
|
+
for (const h of ownNullifiers) {
|
|
4829
|
+
if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
|
|
4830
|
+
else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
|
|
4831
|
+
}
|
|
4832
|
+
return spent;
|
|
4833
|
+
}
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4836
|
+
async function resolveSpentSet(params) {
|
|
4837
|
+
const handle = await openSpentSet(params);
|
|
4838
|
+
return handle.query(params.ownNullifiers);
|
|
4867
4839
|
}
|
|
4868
4840
|
async function syncNullifierCache(params, manifest, throwIfAborted) {
|
|
4869
4841
|
const { source, cache } = params;
|
|
@@ -4913,40 +4885,6 @@ async function getConsistentTail(params, throwIfAborted) {
|
|
|
4913
4885
|
throw new Error("nullifier tail inconsistent with local chunks after retry");
|
|
4914
4886
|
}
|
|
4915
4887
|
|
|
4916
|
-
// src/wallet/scanner/phases/spentStatus.ts
|
|
4917
|
-
function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
|
|
4918
|
-
const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
|
|
4919
|
-
const nullifiers = /* @__PURE__ */ new Set();
|
|
4920
|
-
for (const { note } of scanEntries) {
|
|
4921
|
-
nullifiers.add(note.nullifierHex.toLowerCase());
|
|
4922
|
-
}
|
|
4923
|
-
for (const vaultNote of vaultNotes) {
|
|
4924
|
-
if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
|
|
4925
|
-
if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
|
|
4926
|
-
if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
|
|
4927
|
-
}
|
|
4928
|
-
return nullifiers;
|
|
4929
|
-
}
|
|
4930
|
-
async function resolveSpentStatus(params) {
|
|
4931
|
-
if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
4932
|
-
try {
|
|
4933
|
-
return await resolveSpentSet({
|
|
4934
|
-
source: params.source,
|
|
4935
|
-
cache: params.cache,
|
|
4936
|
-
ownNullifiers: params.nullifiers,
|
|
4937
|
-
signal: params.signal,
|
|
4938
|
-
onWarning: params.onWarning
|
|
4939
|
-
});
|
|
4940
|
-
} catch (err) {
|
|
4941
|
-
if (isAbortError(err)) throw err;
|
|
4942
|
-
params.onWarning?.(
|
|
4943
|
-
"could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
|
|
4944
|
-
err
|
|
4945
|
-
);
|
|
4946
|
-
return /* @__PURE__ */ new Map();
|
|
4947
|
-
}
|
|
4948
|
-
}
|
|
4949
|
-
|
|
4950
4888
|
// src/wallet/scanner/phases/persist.ts
|
|
4951
4889
|
function purgeIsTrustworthy(hintsScanned, purgeableNotes) {
|
|
4952
4890
|
if (purgeableNotes === 0) return true;
|
|
@@ -5005,6 +4943,123 @@ async function persistCursor(storage, maxLeafIndex, isIncremental) {
|
|
|
5005
4943
|
});
|
|
5006
4944
|
}
|
|
5007
4945
|
|
|
4946
|
+
// src/wallet/scanner/phases/checkpoint.ts
|
|
4947
|
+
function createScanCheckpoint(params) {
|
|
4948
|
+
const { vault, storage, nullifiers, isIncremental, signal, onWarning } = params;
|
|
4949
|
+
const throwIfAborted = () => {
|
|
4950
|
+
if (signal?.aborted) throw scanAbortError();
|
|
4951
|
+
};
|
|
4952
|
+
let spentSetPromise = null;
|
|
4953
|
+
const getSpentSet = () => spentSetPromise ??= openSpentSet({
|
|
4954
|
+
source: nullifiers,
|
|
4955
|
+
cache: storage,
|
|
4956
|
+
signal,
|
|
4957
|
+
onWarning
|
|
4958
|
+
}).catch(() => null);
|
|
4959
|
+
return {
|
|
4960
|
+
async savePageNotes(entries) {
|
|
4961
|
+
throwIfAborted();
|
|
4962
|
+
const fresh = entries.filter((e) => e.isNew);
|
|
4963
|
+
if (fresh.length === 0) return;
|
|
4964
|
+
const own = new Set(fresh.map((e) => e.note.nullifierHex.toLowerCase()));
|
|
4965
|
+
const spentSet = await getSpentSet();
|
|
4966
|
+
const spent = spentSet ? await spentSet.query(own).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
|
|
4967
|
+
throwIfAborted();
|
|
4968
|
+
await vault.saveMany(
|
|
4969
|
+
fresh.map(({ note }) => {
|
|
4970
|
+
const spend = spent.get(note.nullifierHex.toLowerCase());
|
|
4971
|
+
return {
|
|
4972
|
+
note: spend ? stampSpentTxHash(note, spend.txHash) : note,
|
|
4973
|
+
noteStatus: {
|
|
4974
|
+
spent: spend !== void 0,
|
|
4975
|
+
spentAt: spend?.spentAt ?? null
|
|
4976
|
+
}
|
|
4977
|
+
};
|
|
4978
|
+
})
|
|
4979
|
+
);
|
|
4980
|
+
},
|
|
4981
|
+
async advanceCursor(maxLeafIndex) {
|
|
4982
|
+
if (maxLeafIndex === void 0) return;
|
|
4983
|
+
throwIfAborted();
|
|
4984
|
+
await persistCursor(storage, maxLeafIndex, isIncremental);
|
|
4985
|
+
}
|
|
4986
|
+
};
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
// src/wallet/scanner/selfEphGap.ts
|
|
4990
|
+
function noteEphPkHex(note) {
|
|
4991
|
+
if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
|
|
4992
|
+
return toHex(note.memo.slice(-32)).toLowerCase();
|
|
4993
|
+
}
|
|
4994
|
+
function gapMargin(windowSize) {
|
|
4995
|
+
return Math.max(1, Math.floor(windowSize / 16));
|
|
4996
|
+
}
|
|
4997
|
+
function resolveSelfEphCeiling(params) {
|
|
4998
|
+
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
4999
|
+
const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
|
|
5000
|
+
if (scanMaxIndex === null) return null;
|
|
5001
|
+
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
5002
|
+
const ownEphPks = /* @__PURE__ */ new Set();
|
|
5003
|
+
for (const note of notes) {
|
|
5004
|
+
const hex = noteEphPkHex(note);
|
|
5005
|
+
if (hex) ownEphPks.add(hex);
|
|
5006
|
+
}
|
|
5007
|
+
const ivkPacked = deriveViewingPublicKey(viewingKey);
|
|
5008
|
+
let max = scanMaxIndex;
|
|
5009
|
+
let from = windowSize;
|
|
5010
|
+
for (; ; ) {
|
|
5011
|
+
const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
|
|
5012
|
+
let matched = false;
|
|
5013
|
+
for (const entry of slice) {
|
|
5014
|
+
if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
|
|
5015
|
+
matched = true;
|
|
5016
|
+
if (entry.index > max) max = entry.index;
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
if (!matched) break;
|
|
5020
|
+
from += windowSize;
|
|
5021
|
+
}
|
|
5022
|
+
return max;
|
|
5023
|
+
}
|
|
5024
|
+
function windowSizeForCounter(counter) {
|
|
5025
|
+
const needed = counter + gapMargin(SELF_EPH_WINDOW);
|
|
5026
|
+
return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
// src/wallet/scanner/phases/spentStatus.ts
|
|
5030
|
+
function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
|
|
5031
|
+
const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
|
|
5032
|
+
const nullifiers = /* @__PURE__ */ new Set();
|
|
5033
|
+
for (const { note } of scanEntries) {
|
|
5034
|
+
nullifiers.add(note.nullifierHex.toLowerCase());
|
|
5035
|
+
}
|
|
5036
|
+
for (const vaultNote of vaultNotes) {
|
|
5037
|
+
if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
|
|
5038
|
+
if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
|
|
5039
|
+
if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
|
|
5040
|
+
}
|
|
5041
|
+
return nullifiers;
|
|
5042
|
+
}
|
|
5043
|
+
async function resolveSpentStatus(params) {
|
|
5044
|
+
if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
|
|
5045
|
+
try {
|
|
5046
|
+
return await resolveSpentSet({
|
|
5047
|
+
source: params.source,
|
|
5048
|
+
cache: params.cache,
|
|
5049
|
+
ownNullifiers: params.nullifiers,
|
|
5050
|
+
signal: params.signal,
|
|
5051
|
+
onWarning: params.onWarning
|
|
5052
|
+
});
|
|
5053
|
+
} catch (err) {
|
|
5054
|
+
if (isAbortError(err)) throw err;
|
|
5055
|
+
params.onWarning?.(
|
|
5056
|
+
"could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
|
|
5057
|
+
err
|
|
5058
|
+
);
|
|
5059
|
+
return /* @__PURE__ */ new Map();
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5008
5063
|
// src/wallet/scanner/pipeline.ts
|
|
5009
5064
|
async function runScan(params) {
|
|
5010
5065
|
const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
|
|
@@ -5023,6 +5078,14 @@ async function runScan(params) {
|
|
|
5023
5078
|
}
|
|
5024
5079
|
}
|
|
5025
5080
|
);
|
|
5081
|
+
const checkpoint = createScanCheckpoint({
|
|
5082
|
+
vault,
|
|
5083
|
+
storage,
|
|
5084
|
+
nullifiers,
|
|
5085
|
+
isIncremental,
|
|
5086
|
+
signal,
|
|
5087
|
+
onWarning
|
|
5088
|
+
});
|
|
5026
5089
|
const scan = await collectScanEntries({
|
|
5027
5090
|
source: hints,
|
|
5028
5091
|
pool,
|
|
@@ -5043,7 +5106,9 @@ async function runScan(params) {
|
|
|
5043
5106
|
// consumer probes it with a vault commitment, so keeping the whole pool
|
|
5044
5107
|
// only bought heap.
|
|
5045
5108
|
vaultHexes: preScanHexes,
|
|
5046
|
-
onWarning
|
|
5109
|
+
onWarning,
|
|
5110
|
+
onPage: (entries) => checkpoint.savePageNotes(entries),
|
|
5111
|
+
onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
|
|
5047
5112
|
});
|
|
5048
5113
|
const nullifiersToQuery = collectNullifiersToQuery(
|
|
5049
5114
|
scan.scanEntries,
|