@orbinum/sdk 1.3.0 → 1.4.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 +34 -5
- package/dist/index.d.ts +34 -5
- package/dist/index.js +100 -68
- package/dist/index.mjs +100 -68
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -240,8 +240,27 @@ declare function reserveSelfEphIndex(storage: NoteStorage): Promise<number>;
|
|
|
240
240
|
* viewing public key. Registering the counterparty is a side effect worth
|
|
241
241
|
* having: it makes the REVERSE direction cheap too, since their future payments
|
|
242
242
|
* to this wallet become hash lookups instead of one trial ECDH per note.
|
|
243
|
+
*
|
|
244
|
+
* Returns `null` when this vault holds no history for that counterparty, and
|
|
245
|
+
* the caller must then use a random ephemeral. The reason is that "no history"
|
|
246
|
+
* has two causes this function cannot tell apart:
|
|
247
|
+
*
|
|
248
|
+
* - a genuine first payment, where index 0 is correct;
|
|
249
|
+
* - a counter that was LOST — a restored seed, a cleared IndexedDB, a wipe
|
|
250
|
+
* and rescan — where index 0 was already published and re-deriving it
|
|
251
|
+
* republishes that ephPk, linking the two notes in public.
|
|
252
|
+
*
|
|
253
|
+
* Unlike `selfEphCounter`, this one cannot be recovered: the index was
|
|
254
|
+
* published on a note encrypted toward someone else, so it never appears in
|
|
255
|
+
* this wallet's own scan, and asking a server whether a given ephPk exists
|
|
256
|
+
* would reveal which notes are ours. So the ambiguity is resolved the safe way
|
|
257
|
+
* — the caller degrades to random, costing the recipient one trial scan, and
|
|
258
|
+
* every later payment to the same counterparty takes the fast path again.
|
|
259
|
+
*
|
|
260
|
+
* The entry is still created: the NEXT payment has a counter, and registering
|
|
261
|
+
* the counterparty is what makes the reverse direction cheap.
|
|
243
262
|
*/
|
|
244
|
-
declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number>;
|
|
263
|
+
declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number | null>;
|
|
245
264
|
|
|
246
265
|
/**
|
|
247
266
|
* Turning a note into a stored record and back.
|
|
@@ -4495,7 +4514,13 @@ declare function resolveSelfEphCeiling(params: {
|
|
|
4495
4514
|
/**
|
|
4496
4515
|
* Discovery window size for the next scan, given the persisted counter: at least
|
|
4497
4516
|
* the default, rounded up so every index the wallet may already have used (plus
|
|
4498
|
-
* the gap margin) falls inside the fast path
|
|
4517
|
+
* the gap margin) falls inside the fast path, and never past `MAX_EPH_WINDOW`.
|
|
4518
|
+
*
|
|
4519
|
+
* The counter is read from a config that survives restores and hand-editing, so
|
|
4520
|
+
* a non-finite value is treated as "no history" rather than propagated: `NaN`
|
|
4521
|
+
* would make the builder's `i < from + count` false immediately and return an
|
|
4522
|
+
* EMPTY window, silently disabling the fast path, while `Infinity` would make
|
|
4523
|
+
* that same loop never terminate.
|
|
4499
4524
|
*/
|
|
4500
4525
|
declare function windowSizeForCounter(counter: number): number;
|
|
4501
4526
|
|
|
@@ -4515,9 +4540,9 @@ declare function windowSizeForCounter(counter: number): number;
|
|
|
4515
4540
|
*
|
|
4516
4541
|
* The expensive part (one ECDH per memo) runs in the injected decrypt pool —
|
|
4517
4542
|
* Web Workers when the host provides a factory, a yielded loop otherwise — while
|
|
4518
|
-
* the cheap bookkeeping (cursor, on-chain set, counters) stays here.
|
|
4519
|
-
*
|
|
4520
|
-
* and CPU.
|
|
4543
|
+
* the cheap bookkeeping (cursor, on-chain set, counters) stays here. Both
|
|
4544
|
+
* transports download through the same PREFETCH-deep sliding window
|
|
4545
|
+
* (`runPrefetched`), overlapping network and CPU.
|
|
4521
4546
|
*/
|
|
4522
4547
|
|
|
4523
4548
|
/**
|
|
@@ -4552,8 +4577,11 @@ interface ScanOutcome {
|
|
|
4552
4577
|
onChainHexes: Set<string>;
|
|
4553
4578
|
/** Highest leafIndex seen — the next incremental scan resumes after it. */
|
|
4554
4579
|
maxLeafIndex: number | undefined;
|
|
4580
|
+
/** Hints walked this call, across both transports. */
|
|
4555
4581
|
scanned: number;
|
|
4582
|
+
/** Notes recovered that the vault did not hold yet. */
|
|
4556
4583
|
found: number;
|
|
4584
|
+
/** Hints carrying no memo or ephemeral key — nothing to decrypt. */
|
|
4557
4585
|
noMemo: number;
|
|
4558
4586
|
/**
|
|
4559
4587
|
* Hints whose memo did not open with this wallet's keys.
|
|
@@ -4564,6 +4592,7 @@ interface ScanOutcome {
|
|
|
4564
4592
|
* recovers nothing while this equals the whole pool has a key problem.
|
|
4565
4593
|
*/
|
|
4566
4594
|
decryptFailed: number;
|
|
4595
|
+
/** Notes recovered that the vault already held. */
|
|
4567
4596
|
alreadyPresent: number;
|
|
4568
4597
|
/** Hints discarded by the view-tag fast filter (no AEAD decrypt attempted). */
|
|
4569
4598
|
tagFiltered: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -240,8 +240,27 @@ declare function reserveSelfEphIndex(storage: NoteStorage): Promise<number>;
|
|
|
240
240
|
* viewing public key. Registering the counterparty is a side effect worth
|
|
241
241
|
* having: it makes the REVERSE direction cheap too, since their future payments
|
|
242
242
|
* to this wallet become hash lookups instead of one trial ECDH per note.
|
|
243
|
+
*
|
|
244
|
+
* Returns `null` when this vault holds no history for that counterparty, and
|
|
245
|
+
* the caller must then use a random ephemeral. The reason is that "no history"
|
|
246
|
+
* has two causes this function cannot tell apart:
|
|
247
|
+
*
|
|
248
|
+
* - a genuine first payment, where index 0 is correct;
|
|
249
|
+
* - a counter that was LOST — a restored seed, a cleared IndexedDB, a wipe
|
|
250
|
+
* and rescan — where index 0 was already published and re-deriving it
|
|
251
|
+
* republishes that ephPk, linking the two notes in public.
|
|
252
|
+
*
|
|
253
|
+
* Unlike `selfEphCounter`, this one cannot be recovered: the index was
|
|
254
|
+
* published on a note encrypted toward someone else, so it never appears in
|
|
255
|
+
* this wallet's own scan, and asking a server whether a given ephPk exists
|
|
256
|
+
* would reveal which notes are ours. So the ambiguity is resolved the safe way
|
|
257
|
+
* — the caller degrades to random, costing the recipient one trial scan, and
|
|
258
|
+
* every later payment to the same counterparty takes the fast path again.
|
|
259
|
+
*
|
|
260
|
+
* The entry is still created: the NEXT payment has a counter, and registering
|
|
261
|
+
* the counterparty is what makes the reverse direction cheap.
|
|
243
262
|
*/
|
|
244
|
-
declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number>;
|
|
263
|
+
declare function reservePairwiseIndex(storage: NoteStorage, ivkHex: string): Promise<number | null>;
|
|
245
264
|
|
|
246
265
|
/**
|
|
247
266
|
* Turning a note into a stored record and back.
|
|
@@ -4495,7 +4514,13 @@ declare function resolveSelfEphCeiling(params: {
|
|
|
4495
4514
|
/**
|
|
4496
4515
|
* Discovery window size for the next scan, given the persisted counter: at least
|
|
4497
4516
|
* the default, rounded up so every index the wallet may already have used (plus
|
|
4498
|
-
* the gap margin) falls inside the fast path
|
|
4517
|
+
* the gap margin) falls inside the fast path, and never past `MAX_EPH_WINDOW`.
|
|
4518
|
+
*
|
|
4519
|
+
* The counter is read from a config that survives restores and hand-editing, so
|
|
4520
|
+
* a non-finite value is treated as "no history" rather than propagated: `NaN`
|
|
4521
|
+
* would make the builder's `i < from + count` false immediately and return an
|
|
4522
|
+
* EMPTY window, silently disabling the fast path, while `Infinity` would make
|
|
4523
|
+
* that same loop never terminate.
|
|
4499
4524
|
*/
|
|
4500
4525
|
declare function windowSizeForCounter(counter: number): number;
|
|
4501
4526
|
|
|
@@ -4515,9 +4540,9 @@ declare function windowSizeForCounter(counter: number): number;
|
|
|
4515
4540
|
*
|
|
4516
4541
|
* The expensive part (one ECDH per memo) runs in the injected decrypt pool —
|
|
4517
4542
|
* Web Workers when the host provides a factory, a yielded loop otherwise — while
|
|
4518
|
-
* the cheap bookkeeping (cursor, on-chain set, counters) stays here.
|
|
4519
|
-
*
|
|
4520
|
-
* and CPU.
|
|
4543
|
+
* the cheap bookkeeping (cursor, on-chain set, counters) stays here. Both
|
|
4544
|
+
* transports download through the same PREFETCH-deep sliding window
|
|
4545
|
+
* (`runPrefetched`), overlapping network and CPU.
|
|
4521
4546
|
*/
|
|
4522
4547
|
|
|
4523
4548
|
/**
|
|
@@ -4552,8 +4577,11 @@ interface ScanOutcome {
|
|
|
4552
4577
|
onChainHexes: Set<string>;
|
|
4553
4578
|
/** Highest leafIndex seen — the next incremental scan resumes after it. */
|
|
4554
4579
|
maxLeafIndex: number | undefined;
|
|
4580
|
+
/** Hints walked this call, across both transports. */
|
|
4555
4581
|
scanned: number;
|
|
4582
|
+
/** Notes recovered that the vault did not hold yet. */
|
|
4556
4583
|
found: number;
|
|
4584
|
+
/** Hints carrying no memo or ephemeral key — nothing to decrypt. */
|
|
4557
4585
|
noMemo: number;
|
|
4558
4586
|
/**
|
|
4559
4587
|
* Hints whose memo did not open with this wallet's keys.
|
|
@@ -4564,6 +4592,7 @@ interface ScanOutcome {
|
|
|
4564
4592
|
* recovers nothing while this equals the whole pool has a key problem.
|
|
4565
4593
|
*/
|
|
4566
4594
|
decryptFailed: number;
|
|
4595
|
+
/** Notes recovered that the vault already held. */
|
|
4567
4596
|
alreadyPresent: number;
|
|
4568
4597
|
/** Hints discarded by the view-tag fast filter (no AEAD decrypt attempted). */
|
|
4569
4598
|
tagFiltered: number;
|
package/dist/index.js
CHANGED
|
@@ -5414,13 +5414,29 @@ function mergeCounters(current, fallback) {
|
|
|
5414
5414
|
...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
|
|
5415
5415
|
};
|
|
5416
5416
|
}
|
|
5417
|
-
var
|
|
5417
|
+
var counterOrNone = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
5418
|
+
var higher = (a, b) => {
|
|
5419
|
+
const x = counterOrNone(a);
|
|
5420
|
+
const y = counterOrNone(b);
|
|
5421
|
+
return x === void 0 ? y : y === void 0 ? x : Math.max(x, y);
|
|
5422
|
+
};
|
|
5423
|
+
function usableEntry(entry) {
|
|
5424
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
5425
|
+
const { nextIndex, addedAt } = entry;
|
|
5426
|
+
const index = counterOrNone(nextIndex);
|
|
5427
|
+
if (index === void 0) return void 0;
|
|
5428
|
+
return { nextIndex: index, addedAt: counterOrNone(addedAt) ?? Date.now() };
|
|
5429
|
+
}
|
|
5418
5430
|
function mergePairwise(current, fallback) {
|
|
5419
5431
|
if (!current && !fallback) return void 0;
|
|
5420
|
-
const merged = {
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5432
|
+
const merged = {};
|
|
5433
|
+
for (const [key, entry] of Object.entries(fallback ?? {})) {
|
|
5434
|
+
const usable = usableEntry(entry);
|
|
5435
|
+
if (usable) merged[key] = usable;
|
|
5436
|
+
}
|
|
5437
|
+
for (const [key, raw] of Object.entries(current ?? {})) {
|
|
5438
|
+
const entry = usableEntry(raw);
|
|
5439
|
+
if (!entry) continue;
|
|
5424
5440
|
const prev = merged[key];
|
|
5425
5441
|
merged[key] = prev ? {
|
|
5426
5442
|
nextIndex: Math.max(prev.nextIndex, entry.nextIndex),
|
|
@@ -5431,33 +5447,42 @@ function mergePairwise(current, fallback) {
|
|
|
5431
5447
|
}
|
|
5432
5448
|
|
|
5433
5449
|
// src/wallet/vault/storage/ephemeralIndex.ts
|
|
5450
|
+
var MAX_EPH_INDEX = 4294967294;
|
|
5451
|
+
function usableCounter(value) {
|
|
5452
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return null;
|
|
5453
|
+
if (value < 0 || value > MAX_EPH_INDEX) return null;
|
|
5454
|
+
return value;
|
|
5455
|
+
}
|
|
5434
5456
|
async function reserveSelfEphIndex(storage) {
|
|
5435
5457
|
const updated = await storage.updateConfig((config) => ({
|
|
5436
5458
|
...config,
|
|
5437
|
-
selfEphCounter: (config.selfEphCounter ?? 0) + 1,
|
|
5459
|
+
selfEphCounter: (usableCounter(config.selfEphCounter) ?? 0) + 1,
|
|
5438
5460
|
updatedAt: Date.now()
|
|
5439
5461
|
}));
|
|
5440
5462
|
if (!updated) throw new Error("vault not initialized");
|
|
5441
|
-
return (updated.selfEphCounter ?? 1) - 1;
|
|
5463
|
+
return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
|
|
5442
5464
|
}
|
|
5443
5465
|
async function reservePairwiseIndex(storage, ivkHex) {
|
|
5444
5466
|
const key = ivkHex.toLowerCase();
|
|
5445
5467
|
const updated = await storage.updateConfig((config) => {
|
|
5446
|
-
const
|
|
5468
|
+
const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
|
|
5469
|
+
const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
|
|
5447
5470
|
return {
|
|
5448
5471
|
...config,
|
|
5449
5472
|
pairwiseCounterparties: {
|
|
5450
5473
|
...config.pairwiseCounterparties,
|
|
5451
5474
|
[key]: {
|
|
5452
|
-
nextIndex: (
|
|
5453
|
-
addedAt:
|
|
5475
|
+
nextIndex: (stored ?? 0) + 1,
|
|
5476
|
+
addedAt: typeof addedAt === "number" ? addedAt : Date.now()
|
|
5454
5477
|
}
|
|
5455
5478
|
},
|
|
5456
5479
|
updatedAt: Date.now()
|
|
5457
5480
|
};
|
|
5458
5481
|
});
|
|
5459
5482
|
if (!updated) throw new Error("vault not initialized");
|
|
5460
|
-
|
|
5483
|
+
const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
|
|
5484
|
+
if (nextIndex === null || nextIndex <= 1) return null;
|
|
5485
|
+
return nextIndex - 1;
|
|
5461
5486
|
}
|
|
5462
5487
|
|
|
5463
5488
|
// src/wallet/vault/notes/record.ts
|
|
@@ -5880,6 +5905,7 @@ var VaultStore = class {
|
|
|
5880
5905
|
|
|
5881
5906
|
// src/wallet/scanner/phases/collect.ts
|
|
5882
5907
|
var PAGE_SIZE = 2500;
|
|
5908
|
+
var PREFETCH = 3;
|
|
5883
5909
|
async function processHints(ctx, hints, total) {
|
|
5884
5910
|
const { outcome } = ctx;
|
|
5885
5911
|
const toDecrypt = [];
|
|
@@ -5927,6 +5953,24 @@ async function processHints(ctx, hints, total) {
|
|
|
5927
5953
|
await ctx.onBatchDone?.(outcome.maxLeafIndex);
|
|
5928
5954
|
ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
|
|
5929
5955
|
}
|
|
5956
|
+
async function runPrefetched(count, fetchItem, signal, handle) {
|
|
5957
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
5958
|
+
const ensureFetching = (i) => {
|
|
5959
|
+
if (i < count && !inFlight.has(i)) inFlight.set(i, fetchItem(i));
|
|
5960
|
+
};
|
|
5961
|
+
try {
|
|
5962
|
+
for (let i = 0; i < count; i++) {
|
|
5963
|
+
if (signal?.aborted) throw scanAbortError();
|
|
5964
|
+
for (let n = i; n < i + PREFETCH; n++) ensureFetching(n);
|
|
5965
|
+
const item = await inFlight.get(i);
|
|
5966
|
+
inFlight.delete(i);
|
|
5967
|
+
await handle(item);
|
|
5968
|
+
}
|
|
5969
|
+
} finally {
|
|
5970
|
+
for (const pending of inFlight.values()) pending.catch(() => {
|
|
5971
|
+
});
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5930
5974
|
async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
5931
5975
|
if (!source.chunks) return null;
|
|
5932
5976
|
const manifest = await source.chunks.manifest().catch(() => null);
|
|
@@ -5939,27 +5983,36 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
|
5939
5983
|
if (pending.length === 0) return null;
|
|
5940
5984
|
const total = Math.max(manifest.total - startLeaf, 0);
|
|
5941
5985
|
const fetchChunk = (i) => source.chunks.chunk(pending[i].idx, pending[i].digest);
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5986
|
+
await runPrefetched(
|
|
5987
|
+
pending.length,
|
|
5988
|
+
fetchChunk,
|
|
5989
|
+
ctx.signal,
|
|
5990
|
+
(hints) => (
|
|
5991
|
+
// The first chunk may straddle the cursor — drop already-scanned leaves.
|
|
5992
|
+
processHints(
|
|
5949
5993
|
ctx,
|
|
5950
5994
|
hints.filter((h) => (h.leafIndex ?? 0) >= startLeaf),
|
|
5951
5995
|
total
|
|
5952
|
-
)
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
nextChunk?.catch(() => {
|
|
5956
|
-
});
|
|
5957
|
-
}
|
|
5996
|
+
)
|
|
5997
|
+
)
|
|
5998
|
+
);
|
|
5958
5999
|
return manifest.lastSealedLeaf + 1;
|
|
5959
6000
|
}
|
|
6001
|
+
async function processPaginatedTail(ctx, source, sinceLeafIndex) {
|
|
6002
|
+
const fetchPage = (page) => source.listHints({ page, limit: PAGE_SIZE, sinceLeafIndex });
|
|
6003
|
+
const firstPage = await fetchPage(1);
|
|
6004
|
+
const total = ctx.outcome.scanned + firstPage.pagination.total;
|
|
6005
|
+
const effectiveLimit = firstPage.pagination.limit || PAGE_SIZE;
|
|
6006
|
+
const totalPages = Math.max(Math.ceil(firstPage.pagination.total / effectiveLimit), 1);
|
|
6007
|
+
await runPrefetched(
|
|
6008
|
+
totalPages,
|
|
6009
|
+
(i) => i === 0 ? Promise.resolve(firstPage) : fetchPage(i + 1),
|
|
6010
|
+
ctx.signal,
|
|
6011
|
+
(page) => processHints(ctx, page.data, total)
|
|
6012
|
+
);
|
|
6013
|
+
}
|
|
5960
6014
|
async function collectScanEntries(params) {
|
|
5961
|
-
const { source,
|
|
5962
|
-
const { onBatchDone } = params;
|
|
6015
|
+
const { source, sinceLeafIndex, signal } = params;
|
|
5963
6016
|
if (signal?.aborted) throw scanAbortError();
|
|
5964
6017
|
const outcome = {
|
|
5965
6018
|
scanEntries: [],
|
|
@@ -5977,14 +6030,14 @@ async function collectScanEntries(params) {
|
|
|
5977
6030
|
};
|
|
5978
6031
|
const ctx = {
|
|
5979
6032
|
outcome,
|
|
5980
|
-
pool,
|
|
5981
|
-
keys,
|
|
5982
|
-
existingHexes,
|
|
5983
|
-
vaultHexes: params.vaultHexes ?? existingHexes,
|
|
5984
|
-
onProgress,
|
|
6033
|
+
pool: params.pool,
|
|
6034
|
+
keys: params.keys,
|
|
6035
|
+
existingHexes: params.existingHexes,
|
|
6036
|
+
vaultHexes: params.vaultHexes ?? params.existingHexes,
|
|
6037
|
+
onProgress: params.onProgress,
|
|
5985
6038
|
signal,
|
|
5986
|
-
onPage,
|
|
5987
|
-
onBatchDone
|
|
6039
|
+
onPage: params.onPage,
|
|
6040
|
+
onBatchDone: params.onBatchDone
|
|
5988
6041
|
};
|
|
5989
6042
|
let tailSince = sinceLeafIndex;
|
|
5990
6043
|
try {
|
|
@@ -5995,33 +6048,7 @@ async function collectScanEntries(params) {
|
|
|
5995
6048
|
params.onWarning?.("sealed-chunk path failed; falling back to paginated hints", err);
|
|
5996
6049
|
if (outcome.maxLeafIndex !== void 0) tailSince = outcome.maxLeafIndex + 1;
|
|
5997
6050
|
}
|
|
5998
|
-
|
|
5999
|
-
const firstPage = await fetchPage(1);
|
|
6000
|
-
const total = outcome.scanned + firstPage.pagination.total;
|
|
6001
|
-
const effectiveLimit = firstPage.pagination.limit || PAGE_SIZE;
|
|
6002
|
-
const totalPages = Math.ceil(firstPage.pagination.total / effectiveLimit);
|
|
6003
|
-
const PREFETCH = 3;
|
|
6004
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
6005
|
-
const ensureFetching = (page) => {
|
|
6006
|
-
if (page >= 2 && page <= totalPages && !inFlight.has(page)) {
|
|
6007
|
-
inFlight.set(page, fetchPage(page));
|
|
6008
|
-
}
|
|
6009
|
-
};
|
|
6010
|
-
try {
|
|
6011
|
-
for (let p = 2; p < 2 + PREFETCH; p++) ensureFetching(p);
|
|
6012
|
-
await processHints(ctx, firstPage.data, total);
|
|
6013
|
-
for (let page = 2; page <= totalPages; page++) {
|
|
6014
|
-
if (signal?.aborted) throw scanAbortError();
|
|
6015
|
-
ensureFetching(page);
|
|
6016
|
-
const current = await inFlight.get(page);
|
|
6017
|
-
inFlight.delete(page);
|
|
6018
|
-
for (let p = page + 1; p <= page + PREFETCH; p++) ensureFetching(p);
|
|
6019
|
-
await processHints(ctx, current.data, total);
|
|
6020
|
-
}
|
|
6021
|
-
} finally {
|
|
6022
|
-
for (const pending of inFlight.values()) pending.catch(() => {
|
|
6023
|
-
});
|
|
6024
|
-
}
|
|
6051
|
+
await processPaginatedTail(ctx, source, tailSince);
|
|
6025
6052
|
return outcome;
|
|
6026
6053
|
}
|
|
6027
6054
|
|
|
@@ -6233,7 +6260,7 @@ function gapMargin(windowSize) {
|
|
|
6233
6260
|
}
|
|
6234
6261
|
function resolveSelfEphCeiling(params) {
|
|
6235
6262
|
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
6236
|
-
const windowSize = params.windowSize
|
|
6263
|
+
const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
|
|
6237
6264
|
if (scanMaxIndex === null) return null;
|
|
6238
6265
|
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
6239
6266
|
const ownEphPks = /* @__PURE__ */ new Set();
|
|
@@ -6258,9 +6285,12 @@ function resolveSelfEphCeiling(params) {
|
|
|
6258
6285
|
}
|
|
6259
6286
|
return max;
|
|
6260
6287
|
}
|
|
6288
|
+
var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
|
|
6261
6289
|
function windowSizeForCounter(counter) {
|
|
6262
|
-
const
|
|
6263
|
-
|
|
6290
|
+
const used = Number.isFinite(counter) ? counter : 0;
|
|
6291
|
+
const needed = used + gapMargin(SELF_EPH_WINDOW);
|
|
6292
|
+
const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
|
|
6293
|
+
return Math.min(MAX_EPH_WINDOW, Math.max(SELF_EPH_WINDOW, rounded));
|
|
6264
6294
|
}
|
|
6265
6295
|
|
|
6266
6296
|
// src/wallet/scanner/phases/spentStatus.ts
|
|
@@ -6518,11 +6548,13 @@ async function buildZkNote(params, deps) {
|
|
|
6518
6548
|
} else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
|
|
6519
6549
|
try {
|
|
6520
6550
|
const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6551
|
+
if (index !== null) {
|
|
6552
|
+
const pairSecret = derivePairwiseSharedSecret(
|
|
6553
|
+
keys.viewingSecretKey,
|
|
6554
|
+
params.viewingPublicKey
|
|
6555
|
+
);
|
|
6556
|
+
ephSkOverride = derivePairwiseEphSk(pairSecret, index);
|
|
6557
|
+
}
|
|
6526
6558
|
} catch {
|
|
6527
6559
|
}
|
|
6528
6560
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -4188,13 +4188,29 @@ function mergeCounters(current, fallback) {
|
|
|
4188
4188
|
...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
|
|
4189
4189
|
};
|
|
4190
4190
|
}
|
|
4191
|
-
var
|
|
4191
|
+
var counterOrNone = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
4192
|
+
var higher = (a, b) => {
|
|
4193
|
+
const x = counterOrNone(a);
|
|
4194
|
+
const y = counterOrNone(b);
|
|
4195
|
+
return x === void 0 ? y : y === void 0 ? x : Math.max(x, y);
|
|
4196
|
+
};
|
|
4197
|
+
function usableEntry(entry) {
|
|
4198
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
4199
|
+
const { nextIndex, addedAt } = entry;
|
|
4200
|
+
const index = counterOrNone(nextIndex);
|
|
4201
|
+
if (index === void 0) return void 0;
|
|
4202
|
+
return { nextIndex: index, addedAt: counterOrNone(addedAt) ?? Date.now() };
|
|
4203
|
+
}
|
|
4192
4204
|
function mergePairwise(current, fallback) {
|
|
4193
4205
|
if (!current && !fallback) return void 0;
|
|
4194
|
-
const merged = {
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4206
|
+
const merged = {};
|
|
4207
|
+
for (const [key, entry] of Object.entries(fallback ?? {})) {
|
|
4208
|
+
const usable = usableEntry(entry);
|
|
4209
|
+
if (usable) merged[key] = usable;
|
|
4210
|
+
}
|
|
4211
|
+
for (const [key, raw] of Object.entries(current ?? {})) {
|
|
4212
|
+
const entry = usableEntry(raw);
|
|
4213
|
+
if (!entry) continue;
|
|
4198
4214
|
const prev = merged[key];
|
|
4199
4215
|
merged[key] = prev ? {
|
|
4200
4216
|
nextIndex: Math.max(prev.nextIndex, entry.nextIndex),
|
|
@@ -4205,33 +4221,42 @@ function mergePairwise(current, fallback) {
|
|
|
4205
4221
|
}
|
|
4206
4222
|
|
|
4207
4223
|
// src/wallet/vault/storage/ephemeralIndex.ts
|
|
4224
|
+
var MAX_EPH_INDEX = 4294967294;
|
|
4225
|
+
function usableCounter(value) {
|
|
4226
|
+
if (typeof value !== "number" || !Number.isInteger(value)) return null;
|
|
4227
|
+
if (value < 0 || value > MAX_EPH_INDEX) return null;
|
|
4228
|
+
return value;
|
|
4229
|
+
}
|
|
4208
4230
|
async function reserveSelfEphIndex(storage) {
|
|
4209
4231
|
const updated = await storage.updateConfig((config) => ({
|
|
4210
4232
|
...config,
|
|
4211
|
-
selfEphCounter: (config.selfEphCounter ?? 0) + 1,
|
|
4233
|
+
selfEphCounter: (usableCounter(config.selfEphCounter) ?? 0) + 1,
|
|
4212
4234
|
updatedAt: Date.now()
|
|
4213
4235
|
}));
|
|
4214
4236
|
if (!updated) throw new Error("vault not initialized");
|
|
4215
|
-
return (updated.selfEphCounter ?? 1) - 1;
|
|
4237
|
+
return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
|
|
4216
4238
|
}
|
|
4217
4239
|
async function reservePairwiseIndex(storage, ivkHex) {
|
|
4218
4240
|
const key = ivkHex.toLowerCase();
|
|
4219
4241
|
const updated = await storage.updateConfig((config) => {
|
|
4220
|
-
const
|
|
4242
|
+
const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
|
|
4243
|
+
const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
|
|
4221
4244
|
return {
|
|
4222
4245
|
...config,
|
|
4223
4246
|
pairwiseCounterparties: {
|
|
4224
4247
|
...config.pairwiseCounterparties,
|
|
4225
4248
|
[key]: {
|
|
4226
|
-
nextIndex: (
|
|
4227
|
-
addedAt:
|
|
4249
|
+
nextIndex: (stored ?? 0) + 1,
|
|
4250
|
+
addedAt: typeof addedAt === "number" ? addedAt : Date.now()
|
|
4228
4251
|
}
|
|
4229
4252
|
},
|
|
4230
4253
|
updatedAt: Date.now()
|
|
4231
4254
|
};
|
|
4232
4255
|
});
|
|
4233
4256
|
if (!updated) throw new Error("vault not initialized");
|
|
4234
|
-
|
|
4257
|
+
const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
|
|
4258
|
+
if (nextIndex === null || nextIndex <= 1) return null;
|
|
4259
|
+
return nextIndex - 1;
|
|
4235
4260
|
}
|
|
4236
4261
|
|
|
4237
4262
|
// src/wallet/vault/notes/record.ts
|
|
@@ -4654,6 +4679,7 @@ var VaultStore = class {
|
|
|
4654
4679
|
|
|
4655
4680
|
// src/wallet/scanner/phases/collect.ts
|
|
4656
4681
|
var PAGE_SIZE = 2500;
|
|
4682
|
+
var PREFETCH = 3;
|
|
4657
4683
|
async function processHints(ctx, hints, total) {
|
|
4658
4684
|
const { outcome } = ctx;
|
|
4659
4685
|
const toDecrypt = [];
|
|
@@ -4701,6 +4727,24 @@ async function processHints(ctx, hints, total) {
|
|
|
4701
4727
|
await ctx.onBatchDone?.(outcome.maxLeafIndex);
|
|
4702
4728
|
ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
|
|
4703
4729
|
}
|
|
4730
|
+
async function runPrefetched(count, fetchItem, signal, handle) {
|
|
4731
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
4732
|
+
const ensureFetching = (i) => {
|
|
4733
|
+
if (i < count && !inFlight.has(i)) inFlight.set(i, fetchItem(i));
|
|
4734
|
+
};
|
|
4735
|
+
try {
|
|
4736
|
+
for (let i = 0; i < count; i++) {
|
|
4737
|
+
if (signal?.aborted) throw scanAbortError();
|
|
4738
|
+
for (let n = i; n < i + PREFETCH; n++) ensureFetching(n);
|
|
4739
|
+
const item = await inFlight.get(i);
|
|
4740
|
+
inFlight.delete(i);
|
|
4741
|
+
await handle(item);
|
|
4742
|
+
}
|
|
4743
|
+
} finally {
|
|
4744
|
+
for (const pending of inFlight.values()) pending.catch(() => {
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4704
4748
|
async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
4705
4749
|
if (!source.chunks) return null;
|
|
4706
4750
|
const manifest = await source.chunks.manifest().catch(() => null);
|
|
@@ -4713,27 +4757,36 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
|
|
|
4713
4757
|
if (pending.length === 0) return null;
|
|
4714
4758
|
const total = Math.max(manifest.total - startLeaf, 0);
|
|
4715
4759
|
const fetchChunk = (i) => source.chunks.chunk(pending[i].idx, pending[i].digest);
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4760
|
+
await runPrefetched(
|
|
4761
|
+
pending.length,
|
|
4762
|
+
fetchChunk,
|
|
4763
|
+
ctx.signal,
|
|
4764
|
+
(hints) => (
|
|
4765
|
+
// The first chunk may straddle the cursor — drop already-scanned leaves.
|
|
4766
|
+
processHints(
|
|
4723
4767
|
ctx,
|
|
4724
4768
|
hints.filter((h) => (h.leafIndex ?? 0) >= startLeaf),
|
|
4725
4769
|
total
|
|
4726
|
-
)
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
nextChunk?.catch(() => {
|
|
4730
|
-
});
|
|
4731
|
-
}
|
|
4770
|
+
)
|
|
4771
|
+
)
|
|
4772
|
+
);
|
|
4732
4773
|
return manifest.lastSealedLeaf + 1;
|
|
4733
4774
|
}
|
|
4775
|
+
async function processPaginatedTail(ctx, source, sinceLeafIndex) {
|
|
4776
|
+
const fetchPage = (page) => source.listHints({ page, limit: PAGE_SIZE, sinceLeafIndex });
|
|
4777
|
+
const firstPage = await fetchPage(1);
|
|
4778
|
+
const total = ctx.outcome.scanned + firstPage.pagination.total;
|
|
4779
|
+
const effectiveLimit = firstPage.pagination.limit || PAGE_SIZE;
|
|
4780
|
+
const totalPages = Math.max(Math.ceil(firstPage.pagination.total / effectiveLimit), 1);
|
|
4781
|
+
await runPrefetched(
|
|
4782
|
+
totalPages,
|
|
4783
|
+
(i) => i === 0 ? Promise.resolve(firstPage) : fetchPage(i + 1),
|
|
4784
|
+
ctx.signal,
|
|
4785
|
+
(page) => processHints(ctx, page.data, total)
|
|
4786
|
+
);
|
|
4787
|
+
}
|
|
4734
4788
|
async function collectScanEntries(params) {
|
|
4735
|
-
const { source,
|
|
4736
|
-
const { onBatchDone } = params;
|
|
4789
|
+
const { source, sinceLeafIndex, signal } = params;
|
|
4737
4790
|
if (signal?.aborted) throw scanAbortError();
|
|
4738
4791
|
const outcome = {
|
|
4739
4792
|
scanEntries: [],
|
|
@@ -4751,14 +4804,14 @@ async function collectScanEntries(params) {
|
|
|
4751
4804
|
};
|
|
4752
4805
|
const ctx = {
|
|
4753
4806
|
outcome,
|
|
4754
|
-
pool,
|
|
4755
|
-
keys,
|
|
4756
|
-
existingHexes,
|
|
4757
|
-
vaultHexes: params.vaultHexes ?? existingHexes,
|
|
4758
|
-
onProgress,
|
|
4807
|
+
pool: params.pool,
|
|
4808
|
+
keys: params.keys,
|
|
4809
|
+
existingHexes: params.existingHexes,
|
|
4810
|
+
vaultHexes: params.vaultHexes ?? params.existingHexes,
|
|
4811
|
+
onProgress: params.onProgress,
|
|
4759
4812
|
signal,
|
|
4760
|
-
onPage,
|
|
4761
|
-
onBatchDone
|
|
4813
|
+
onPage: params.onPage,
|
|
4814
|
+
onBatchDone: params.onBatchDone
|
|
4762
4815
|
};
|
|
4763
4816
|
let tailSince = sinceLeafIndex;
|
|
4764
4817
|
try {
|
|
@@ -4769,33 +4822,7 @@ async function collectScanEntries(params) {
|
|
|
4769
4822
|
params.onWarning?.("sealed-chunk path failed; falling back to paginated hints", err);
|
|
4770
4823
|
if (outcome.maxLeafIndex !== void 0) tailSince = outcome.maxLeafIndex + 1;
|
|
4771
4824
|
}
|
|
4772
|
-
|
|
4773
|
-
const firstPage = await fetchPage(1);
|
|
4774
|
-
const total = outcome.scanned + firstPage.pagination.total;
|
|
4775
|
-
const effectiveLimit = firstPage.pagination.limit || PAGE_SIZE;
|
|
4776
|
-
const totalPages = Math.ceil(firstPage.pagination.total / effectiveLimit);
|
|
4777
|
-
const PREFETCH = 3;
|
|
4778
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
4779
|
-
const ensureFetching = (page) => {
|
|
4780
|
-
if (page >= 2 && page <= totalPages && !inFlight.has(page)) {
|
|
4781
|
-
inFlight.set(page, fetchPage(page));
|
|
4782
|
-
}
|
|
4783
|
-
};
|
|
4784
|
-
try {
|
|
4785
|
-
for (let p = 2; p < 2 + PREFETCH; p++) ensureFetching(p);
|
|
4786
|
-
await processHints(ctx, firstPage.data, total);
|
|
4787
|
-
for (let page = 2; page <= totalPages; page++) {
|
|
4788
|
-
if (signal?.aborted) throw scanAbortError();
|
|
4789
|
-
ensureFetching(page);
|
|
4790
|
-
const current = await inFlight.get(page);
|
|
4791
|
-
inFlight.delete(page);
|
|
4792
|
-
for (let p = page + 1; p <= page + PREFETCH; p++) ensureFetching(p);
|
|
4793
|
-
await processHints(ctx, current.data, total);
|
|
4794
|
-
}
|
|
4795
|
-
} finally {
|
|
4796
|
-
for (const pending of inFlight.values()) pending.catch(() => {
|
|
4797
|
-
});
|
|
4798
|
-
}
|
|
4825
|
+
await processPaginatedTail(ctx, source, tailSince);
|
|
4799
4826
|
return outcome;
|
|
4800
4827
|
}
|
|
4801
4828
|
|
|
@@ -4996,7 +5023,7 @@ function gapMargin(windowSize) {
|
|
|
4996
5023
|
}
|
|
4997
5024
|
function resolveSelfEphCeiling(params) {
|
|
4998
5025
|
const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
|
|
4999
|
-
const windowSize = params.windowSize
|
|
5026
|
+
const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
|
|
5000
5027
|
if (scanMaxIndex === null) return null;
|
|
5001
5028
|
if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
|
|
5002
5029
|
const ownEphPks = /* @__PURE__ */ new Set();
|
|
@@ -5021,9 +5048,12 @@ function resolveSelfEphCeiling(params) {
|
|
|
5021
5048
|
}
|
|
5022
5049
|
return max;
|
|
5023
5050
|
}
|
|
5051
|
+
var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
|
|
5024
5052
|
function windowSizeForCounter(counter) {
|
|
5025
|
-
const
|
|
5026
|
-
|
|
5053
|
+
const used = Number.isFinite(counter) ? counter : 0;
|
|
5054
|
+
const needed = used + gapMargin(SELF_EPH_WINDOW);
|
|
5055
|
+
const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
|
|
5056
|
+
return Math.min(MAX_EPH_WINDOW, Math.max(SELF_EPH_WINDOW, rounded));
|
|
5027
5057
|
}
|
|
5028
5058
|
|
|
5029
5059
|
// src/wallet/scanner/phases/spentStatus.ts
|
|
@@ -5281,11 +5311,13 @@ async function buildZkNote(params, deps) {
|
|
|
5281
5311
|
} else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
|
|
5282
5312
|
try {
|
|
5283
5313
|
const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5314
|
+
if (index !== null) {
|
|
5315
|
+
const pairSecret = derivePairwiseSharedSecret(
|
|
5316
|
+
keys.viewingSecretKey,
|
|
5317
|
+
params.viewingPublicKey
|
|
5318
|
+
);
|
|
5319
|
+
ephSkOverride = derivePairwiseEphSk(pairSecret, index);
|
|
5320
|
+
}
|
|
5289
5321
|
} catch {
|
|
5290
5322
|
}
|
|
5291
5323
|
}
|