@unlink-xyz/core 0.1.3-canary.fd5dddf → 0.1.4
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 +9 -0
- package/dist/account/account.d.ts +31 -2
- package/dist/account/account.d.ts.map +1 -1
- package/dist/account/accounts.d.ts +42 -0
- package/dist/account/accounts.d.ts.map +1 -0
- package/dist/account/seed.d.ts +45 -0
- package/dist/account/seed.d.ts.map +1 -0
- package/dist/account/serialization.d.ts +6 -0
- package/dist/account/serialization.d.ts.map +1 -0
- package/dist/browser/index.js +34424 -86406
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/wallet/index.js +55942 -0
- package/dist/browser/wallet/index.js.map +1 -0
- package/dist/clients/broadcaster.d.ts +1 -0
- package/dist/clients/broadcaster.d.ts.map +1 -1
- package/dist/clients/indexer.d.ts +5 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/config.d.ts +6 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/crypto/adapters/index.d.ts +17 -0
- package/dist/crypto/adapters/index.d.ts.map +1 -0
- package/dist/crypto/adapters/polyfills.d.ts +5 -0
- package/dist/crypto/adapters/polyfills.d.ts.map +1 -0
- package/dist/crypto/encrypt.d.ts +33 -0
- package/dist/crypto/encrypt.d.ts.map +1 -0
- package/dist/crypto/secure-memory.d.ts.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6721 -23
- package/dist/index.js.map +1 -0
- package/dist/keys/derive.d.ts +2 -2
- package/dist/keys/derive.d.ts.map +1 -1
- package/dist/keys/hex.d.ts +1 -4
- package/dist/keys/hex.d.ts.map +1 -1
- package/dist/keys/mnemonic.d.ts +0 -2
- package/dist/keys/mnemonic.d.ts.map +1 -1
- package/dist/keys.d.ts +1 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/prover/config.d.ts +54 -9
- package/dist/prover/config.d.ts.map +1 -1
- package/dist/prover/integrity.d.ts +20 -0
- package/dist/prover/integrity.d.ts.map +1 -0
- package/dist/prover/prover.d.ts +16 -31
- package/dist/prover/prover.d.ts.map +1 -1
- package/dist/state/merkle/hydrator.d.ts +21 -19
- package/dist/state/merkle/hydrator.d.ts.map +1 -1
- package/dist/state/merkle/index.d.ts +1 -1
- package/dist/state/merkle/index.d.ts.map +1 -1
- package/dist/state/store/ciphertext-store.d.ts +7 -0
- package/dist/state/store/ciphertext-store.d.ts.map +1 -1
- package/dist/state/store/index.d.ts +1 -1
- package/dist/state/store/index.d.ts.map +1 -1
- package/dist/state/store/job-store.d.ts.map +1 -1
- package/dist/state/store/jobs.d.ts +14 -16
- package/dist/state/store/jobs.d.ts.map +1 -1
- package/dist/state/store/leaf-store.d.ts +4 -0
- package/dist/state/store/leaf-store.d.ts.map +1 -1
- package/dist/state/store/nullifier-store.d.ts.map +1 -1
- package/dist/state/store/records.d.ts +8 -0
- package/dist/state/store/records.d.ts.map +1 -1
- package/dist/state/store/store.d.ts +18 -0
- package/dist/state/store/store.d.ts.map +1 -1
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/transactions/adapter.d.ts +31 -0
- package/dist/transactions/adapter.d.ts.map +1 -0
- package/dist/transactions/deposit.d.ts +1 -1
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/index.d.ts +4 -2
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/note-sync.d.ts +3 -3
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/reconcile.d.ts +1 -1
- package/dist/transactions/reconcile.d.ts.map +1 -1
- package/dist/transactions/transact.d.ts +21 -2
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transaction-planner.d.ts +1 -1
- package/dist/transactions/transaction-planner.d.ts.map +1 -1
- package/dist/transactions/transfer-planner.d.ts +2 -1
- package/dist/transactions/transfer-planner.d.ts.map +1 -1
- package/dist/transactions/types/deposit.d.ts +3 -3
- package/dist/transactions/types/deposit.d.ts.map +1 -1
- package/dist/transactions/types/domain.d.ts +3 -0
- package/dist/transactions/types/domain.d.ts.map +1 -1
- package/dist/transactions/types/options.d.ts +14 -5
- package/dist/transactions/types/options.d.ts.map +1 -1
- package/dist/transactions/types/planning.d.ts +2 -0
- package/dist/transactions/types/planning.d.ts.map +1 -1
- package/dist/transactions/types/state-stores.d.ts +53 -5
- package/dist/transactions/types/state-stores.d.ts.map +1 -1
- package/dist/transactions/types/transact.d.ts +10 -3
- package/dist/transactions/types/transact.d.ts.map +1 -1
- package/dist/transactions/withdrawal-planner.d.ts +1 -1
- package/dist/transactions/withdrawal-planner.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsup.config.d.ts +8 -0
- package/dist/tsup.config.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/amounts.d.ts +0 -13
- package/dist/utils/amounts.d.ts.map +1 -1
- package/dist/utils/async.js +37 -34
- package/dist/utils/async.js.map +1 -0
- package/dist/utils/bigint.d.ts +0 -2
- package/dist/utils/bigint.d.ts.map +1 -1
- package/dist/utils/random.d.ts +5 -0
- package/dist/utils/random.d.ts.map +1 -1
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/wallet/adapter.d.ts +21 -0
- package/dist/wallet/adapter.d.ts.map +1 -0
- package/dist/wallet/burner/service.d.ts +32 -0
- package/dist/wallet/burner/service.d.ts.map +1 -0
- package/dist/wallet/burner/types.d.ts +47 -0
- package/dist/wallet/burner/types.d.ts.map +1 -0
- package/dist/wallet/index.d.ts +20 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +6462 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/sdk.d.ts +48 -0
- package/dist/wallet/sdk.d.ts.map +1 -0
- package/dist/wallet/types.d.ts +457 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/unlink-wallet.d.ts +187 -0
- package/dist/wallet/unlink-wallet.d.ts.map +1 -0
- package/package.json +16 -6
- package/dist/account/account.js +0 -142
- package/dist/circuits.json +0 -74
- package/dist/clients/broadcaster.js +0 -30
- package/dist/clients/http.js +0 -72
- package/dist/clients/indexer.js +0 -94
- package/dist/config.js +0 -36
- package/dist/constants.js +0 -5
- package/dist/core.js +0 -15
- package/dist/crypto-adapters/auto-init.d.ts +0 -2
- package/dist/crypto-adapters/auto-init.d.ts.map +0 -1
- package/dist/crypto-adapters/auto-init.js +0 -7
- package/dist/crypto-adapters/index.d.ts +0 -22
- package/dist/crypto-adapters/index.d.ts.map +0 -1
- package/dist/crypto-adapters/index.js +0 -47
- package/dist/crypto-adapters/polyfills.d.ts +0 -5
- package/dist/crypto-adapters/polyfills.d.ts.map +0 -1
- package/dist/crypto-adapters/polyfills.js +0 -8
- package/dist/errors.js +0 -36
- package/dist/history/index.js +0 -2
- package/dist/history/service.js +0 -354
- package/dist/history/types.js +0 -1
- package/dist/keys/address.js +0 -55
- package/dist/keys/derive.js +0 -112
- package/dist/keys/hex.js +0 -66
- package/dist/keys/index.js +0 -4
- package/dist/keys/mnemonic.js +0 -23
- package/dist/keys.js +0 -45
- package/dist/prover/config.js +0 -70
- package/dist/prover/index.js +0 -1
- package/dist/prover/prover.js +0 -291
- package/dist/prover/registry.js +0 -18
- package/dist/schema.js +0 -14
- package/dist/state/index.js +0 -2
- package/dist/state/merkle/hydrator.js +0 -37
- package/dist/state/merkle/index.js +0 -2
- package/dist/state/merkle/merkle-tree.js +0 -113
- package/dist/state/store/ciphertext-store.js +0 -37
- package/dist/state/store/history-store.js +0 -53
- package/dist/state/store/index.js +0 -9
- package/dist/state/store/job-store.js +0 -144
- package/dist/state/store/jobs.js +0 -1
- package/dist/state/store/leaf-store.js +0 -32
- package/dist/state/store/note-store.js +0 -146
- package/dist/state/store/nullifier-store.js +0 -60
- package/dist/state/store/records.js +0 -1
- package/dist/state/store/root-store.js +0 -26
- package/dist/state/store/store.js +0 -113
- package/dist/storage/index.js +0 -2
- package/dist/storage/indexeddb.js +0 -205
- package/dist/storage/memory.js +0 -91
- package/dist/transactions/deposit.js +0 -220
- package/dist/transactions/index.js +0 -9
- package/dist/transactions/note-selection.js +0 -201
- package/dist/transactions/note-sync.js +0 -485
- package/dist/transactions/reconcile.js +0 -85
- package/dist/transactions/transact.js +0 -450
- package/dist/transactions/transaction-planner.js +0 -116
- package/dist/transactions/transfer-planner.js +0 -85
- package/dist/transactions/types/deposit.js +0 -1
- package/dist/transactions/types/domain.js +0 -4
- package/dist/transactions/types/index.js +0 -17
- package/dist/transactions/types/options.js +0 -1
- package/dist/transactions/types/planning.js +0 -1
- package/dist/transactions/types/state-stores.js +0 -1
- package/dist/transactions/types/transact.js +0 -1
- package/dist/transactions/withdrawal-planner.js +0 -128
- package/dist/tsup.browser.config.js +0 -34
- package/dist/types.js +0 -1
- package/dist/utils/amounts.js +0 -89
- package/dist/utils/bigint.js +0 -29
- package/dist/utils/crypto.d.ts +0 -18
- package/dist/utils/crypto.d.ts.map +0 -1
- package/dist/utils/crypto.js +0 -45
- package/dist/utils/format.js +0 -33
- package/dist/utils/json-codec.js +0 -25
- package/dist/utils/notes.js +0 -14
- package/dist/utils/polling.js +0 -11
- package/dist/utils/random.js +0 -27
- package/dist/utils/secure-memory.d.ts.map +0 -1
- package/dist/utils/secure-memory.js +0 -28
- package/dist/utils/signature.js +0 -14
- package/dist/utils/validators.js +0 -96
- package/dist/vitest.config.js +0 -13
- /package/dist/{utils → crypto}/secure-memory.d.ts +0 -0
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { ValidationError } from "../errors.js";
|
|
2
|
-
import { CIRCUIT_REGISTRY } from "../prover/registry.js";
|
|
3
|
-
/** Maximum inputs supported by available circuits */
|
|
4
|
-
const MAX_CIRCUIT_INPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.inputs));
|
|
5
|
-
/** Maximum outputs supported by available circuits */
|
|
6
|
-
const MAX_CIRCUIT_OUTPUTS = Math.max(...Object.values(CIRCUIT_REGISTRY).map((c) => c.outputs));
|
|
7
|
-
/**
|
|
8
|
-
* Get the value of a note as bigint.
|
|
9
|
-
* Handles both bigint and string values for NoteRecord compatibility.
|
|
10
|
-
*/
|
|
11
|
-
function getNoteValue(note) {
|
|
12
|
-
return typeof note.value === "bigint" ? note.value : BigInt(note.value);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Select notes for a transaction with consolidation-first strategy.
|
|
16
|
-
*
|
|
17
|
-
* Priorities:
|
|
18
|
-
* 1. Consolidation - prefer spending more small notes
|
|
19
|
-
* 2. Minimize change - find combinations with less overshoot
|
|
20
|
-
* 3. Respect maxInputs constraint
|
|
21
|
-
*
|
|
22
|
-
* @param notes - Available unspent notes (caller filters by token)
|
|
23
|
-
* @param totalNeeded - Total amount needed for all recipients
|
|
24
|
-
* @param options - Selection options
|
|
25
|
-
* @returns Selection result with notes and amounts
|
|
26
|
-
* @throws ValidationError if insufficient balance or constraints can't be met
|
|
27
|
-
*/
|
|
28
|
-
export function selectNotesForAmount(notes, totalNeeded, options) {
|
|
29
|
-
const maxInputs = options?.maxInputs ?? MAX_CIRCUIT_INPUTS;
|
|
30
|
-
const maxOutputs = options?.maxOutputs ?? MAX_CIRCUIT_OUTPUTS;
|
|
31
|
-
const recipientCount = options?.recipientCount ?? 1;
|
|
32
|
-
// Validate inputs
|
|
33
|
-
if (totalNeeded <= 0n) {
|
|
34
|
-
throw new ValidationError("Amount must be positive");
|
|
35
|
-
}
|
|
36
|
-
if (!notes.length) {
|
|
37
|
-
throw new ValidationError("No notes available");
|
|
38
|
-
}
|
|
39
|
-
// Validate output constraints: recipients + potential change <= maxOutputs
|
|
40
|
-
// Change is needed when selected sum > totalNeeded (almost always)
|
|
41
|
-
// So we conservatively assume change will be needed
|
|
42
|
-
if (recipientCount + 1 > maxOutputs) {
|
|
43
|
-
throw new ValidationError(`Too many recipients: ${recipientCount} recipients + change exceeds maxOutputs (${maxOutputs})`);
|
|
44
|
-
}
|
|
45
|
-
// Check total balance
|
|
46
|
-
const totalAvailable = notes.reduce((sum, n) => sum + getNoteValue(n), 0n);
|
|
47
|
-
if (totalAvailable < totalNeeded) {
|
|
48
|
-
throw new ValidationError("Insufficient balance");
|
|
49
|
-
}
|
|
50
|
-
// Sort ascending for consolidation preference (smallest first)
|
|
51
|
-
const sortedAsc = [...notes].sort((a, b) => {
|
|
52
|
-
const aVal = getNoteValue(a);
|
|
53
|
-
const bVal = getNoteValue(b);
|
|
54
|
-
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
55
|
-
});
|
|
56
|
-
// Phase 1: Try greedy smallest-first
|
|
57
|
-
let baseline = greedySelect(sortedAsc, totalNeeded, maxInputs);
|
|
58
|
-
// Phase 2: If greedy smallest-first didn't work, try largest-first as fallback
|
|
59
|
-
if (baseline.sum < totalNeeded) {
|
|
60
|
-
const sortedDesc = [...sortedAsc].reverse();
|
|
61
|
-
baseline = greedySelect(sortedDesc, totalNeeded, maxInputs);
|
|
62
|
-
}
|
|
63
|
-
// Phase 3: If we have a valid baseline, use branch-and-bound to optimize
|
|
64
|
-
// Only run B&B on a subset of notes to keep it fast
|
|
65
|
-
let optimized;
|
|
66
|
-
if (baseline.sum >= totalNeeded) {
|
|
67
|
-
// Limit B&B to reasonable subset for performance (30 smallest notes)
|
|
68
|
-
const maxNotesForBnB = Math.min(sortedAsc.length, 30);
|
|
69
|
-
const notesForBnB = sortedAsc.slice(0, maxNotesForBnB);
|
|
70
|
-
optimized = findOptimalSelection(notesForBnB, totalNeeded, maxInputs, baseline);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
optimized = baseline;
|
|
74
|
-
}
|
|
75
|
-
// If no valid solution found
|
|
76
|
-
if (optimized.sum < totalNeeded) {
|
|
77
|
-
throw new ValidationError("Cannot meet amount within maxInputs constraint");
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
selected: optimized.notes,
|
|
81
|
-
totalInput: optimized.sum,
|
|
82
|
-
totalNeeded,
|
|
83
|
-
change: optimized.sum - totalNeeded,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Greedy selection: accumulate notes until threshold met.
|
|
88
|
-
* Returns a partial solution if it can't meet the target.
|
|
89
|
-
*/
|
|
90
|
-
function greedySelect(sorted, target, maxInputs) {
|
|
91
|
-
const selected = [];
|
|
92
|
-
let sum = 0n;
|
|
93
|
-
for (const note of sorted) {
|
|
94
|
-
if (sum >= target)
|
|
95
|
-
break;
|
|
96
|
-
if (selected.length >= maxInputs)
|
|
97
|
-
break;
|
|
98
|
-
selected.push(note);
|
|
99
|
-
sum += getNoteValue(note);
|
|
100
|
-
}
|
|
101
|
-
return { notes: selected, sum };
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Compare two solutions.
|
|
105
|
-
* Returns true if (candidate) is better than (best).
|
|
106
|
-
*
|
|
107
|
-
* Priority 0: Prefer valid solutions (sum >= target)
|
|
108
|
-
* Priority 1: Prefer more notes (consolidation)
|
|
109
|
-
* Priority 2: Prefer less change
|
|
110
|
-
*/
|
|
111
|
-
function isBetterSolution(candidate, best, target) {
|
|
112
|
-
const candidateValid = candidate.sum >= target;
|
|
113
|
-
const bestValid = best.sum >= target;
|
|
114
|
-
// Priority 0: Prefer valid solutions
|
|
115
|
-
if (candidateValid && !bestValid)
|
|
116
|
-
return true;
|
|
117
|
-
if (!candidateValid && bestValid)
|
|
118
|
-
return false;
|
|
119
|
-
if (!candidateValid && !bestValid) {
|
|
120
|
-
// Neither valid - prefer higher sum (closer to target)
|
|
121
|
-
return candidate.sum > best.sum;
|
|
122
|
-
}
|
|
123
|
-
// Both valid - apply consolidation + change minimization
|
|
124
|
-
const candidateChange = candidate.sum - target;
|
|
125
|
-
const bestChange = best.sum - target;
|
|
126
|
-
// Priority 1: Prefer more notes (consolidation)
|
|
127
|
-
if (candidate.notes.length > best.notes.length)
|
|
128
|
-
return true;
|
|
129
|
-
if (candidate.notes.length < best.notes.length)
|
|
130
|
-
return false;
|
|
131
|
-
// Priority 2: Prefer less change
|
|
132
|
-
return candidateChange < bestChange;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Branch-and-bound search for optimal selection.
|
|
136
|
-
*
|
|
137
|
-
* Explores the solution space with aggressive pruning:
|
|
138
|
-
* - Stop when sum >= target (don't over-select beyond what's needed)
|
|
139
|
-
* - Stop when remaining notes can't reach target
|
|
140
|
-
* - Stop when maxInputs exceeded
|
|
141
|
-
* - Early termination when we find a solution with maxInputs notes and exact match
|
|
142
|
-
*/
|
|
143
|
-
function findOptimalSelection(sorted, target, maxInputs, baseline) {
|
|
144
|
-
let best = baseline;
|
|
145
|
-
let foundOptimal = false;
|
|
146
|
-
// Precompute remaining sums for pruning
|
|
147
|
-
const remainingSums = new Array(sorted.length + 1).fill(0n);
|
|
148
|
-
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
149
|
-
const nextSum = remainingSums[i + 1] ?? 0n;
|
|
150
|
-
const note = sorted[i];
|
|
151
|
-
const noteValue = note ? getNoteValue(note) : 0n;
|
|
152
|
-
remainingSums[i] = nextSum + noteValue;
|
|
153
|
-
}
|
|
154
|
-
function search(index, selected, sum) {
|
|
155
|
-
// Early exit if we found optimal solution
|
|
156
|
-
if (foundOptimal)
|
|
157
|
-
return;
|
|
158
|
-
// Pruning: too many notes selected
|
|
159
|
-
if (selected.length > maxInputs)
|
|
160
|
-
return;
|
|
161
|
-
// Found valid solution, now comparing with best
|
|
162
|
-
if (sum >= target) {
|
|
163
|
-
const candidate = { notes: selected, sum };
|
|
164
|
-
if (isBetterSolution(candidate, best, target)) {
|
|
165
|
-
best = { notes: [...selected], sum };
|
|
166
|
-
// Check if this is optimal (maxInputs notes with exact match)
|
|
167
|
-
if (selected.length === maxInputs && sum === target) {
|
|
168
|
-
foundOptimal = true;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// Don't add more notes once target is met
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Pruning: can't reach target with remaining notes
|
|
175
|
-
const remaining = remainingSums[index] ?? 0n;
|
|
176
|
-
if (sum + remaining < target)
|
|
177
|
-
return;
|
|
178
|
-
// Pruning: reached end of notes
|
|
179
|
-
if (index >= sorted.length)
|
|
180
|
-
return;
|
|
181
|
-
// Pruning: can't possibly beat best's note count
|
|
182
|
-
const remainingSlots = maxInputs - selected.length;
|
|
183
|
-
const remainingNotes = sorted.length - index;
|
|
184
|
-
if (selected.length + Math.min(remainingSlots, remainingNotes) <
|
|
185
|
-
best.notes.length) {
|
|
186
|
-
// Even selecting all remaining notes won't beat best's count
|
|
187
|
-
// Only skip if best is already valid
|
|
188
|
-
if (best.sum >= target)
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const currentNote = sorted[index];
|
|
192
|
-
if (!currentNote)
|
|
193
|
-
return;
|
|
194
|
-
// Try including current note (explore this branch first for consolidation)
|
|
195
|
-
search(index + 1, [...selected, currentNote], sum + getNoteValue(currentNote));
|
|
196
|
-
// Try excluding current note
|
|
197
|
-
search(index + 1, selected, sum);
|
|
198
|
-
}
|
|
199
|
-
search(0, [], 0n);
|
|
200
|
-
return best;
|
|
201
|
-
}
|
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import { createIndexerClient } from "../clients/indexer.js";
|
|
2
|
-
import { createServiceConfig } from "../config.js";
|
|
3
|
-
import { poseidon } from "../crypto-adapters/index.js";
|
|
4
|
-
import { CoreError, InitializationError } from "../errors.js";
|
|
5
|
-
import { FieldSize, Hex } from "../keys/hex.js";
|
|
6
|
-
import { createMerkleTrees, rebuildTreeFromStore, } from "../state/index.js";
|
|
7
|
-
import { formatUint256, parseHexToBigInt } from "../utils/bigint.js";
|
|
8
|
-
import { decryptNote } from "../utils/crypto.js";
|
|
9
|
-
import { ensureChainId } from "../utils/validators.js";
|
|
10
|
-
const DEFAULT_BATCH_SIZE = 256;
|
|
11
|
-
function buildCiphertext(bytes) {
|
|
12
|
-
return {
|
|
13
|
-
data: [
|
|
14
|
-
parseHexToBigInt(bytes[0]),
|
|
15
|
-
parseHexToBigInt(bytes[1]),
|
|
16
|
-
parseHexToBigInt(bytes[2]),
|
|
17
|
-
],
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function encodeCiphertext(ciphertext) {
|
|
21
|
-
const payload = new Uint8Array(FieldSize.SCALAR * ciphertext.data.length);
|
|
22
|
-
ciphertext.data.forEach((value, index) => {
|
|
23
|
-
const start = index * FieldSize.SCALAR;
|
|
24
|
-
payload.set(Hex.toBytes(formatUint256(value)), start);
|
|
25
|
-
});
|
|
26
|
-
return payload;
|
|
27
|
-
}
|
|
28
|
-
export function createNoteSyncService(stateStore, options = {}) {
|
|
29
|
-
const trees = options.merkleTrees ?? createMerkleTrees();
|
|
30
|
-
const limit = options.limit ?? DEFAULT_BATCH_SIZE;
|
|
31
|
-
const status = new Map();
|
|
32
|
-
// Initialize indexer client
|
|
33
|
-
let indexerClient = options.indexerClient;
|
|
34
|
-
if (!indexerClient) {
|
|
35
|
-
const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch : undefined);
|
|
36
|
-
if (!fetchImpl) {
|
|
37
|
-
throw new InitializationError("fetch dependency is required to sync indexer commitments");
|
|
38
|
-
}
|
|
39
|
-
if (!options.rpcUrl) {
|
|
40
|
-
throw new InitializationError("rpcUrl is required to sync indexer commitments");
|
|
41
|
-
}
|
|
42
|
-
const serviceConfig = createServiceConfig(options.rpcUrl);
|
|
43
|
-
indexerClient = createIndexerClient(serviceConfig.indexerBaseUrl, {
|
|
44
|
-
fetch: fetchImpl,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function createNoteRecord(params) {
|
|
48
|
-
const { chainId, index, commitment, token, amount, npk, mpk, random, nullifier, createdTxHash, createdEventType, createdAt, spentAt, spentTxHash, } = params;
|
|
49
|
-
const base = {
|
|
50
|
-
chainId,
|
|
51
|
-
index,
|
|
52
|
-
token,
|
|
53
|
-
value: amount.toString(),
|
|
54
|
-
commitment,
|
|
55
|
-
npk: formatUint256(npk),
|
|
56
|
-
mpk: formatUint256(mpk),
|
|
57
|
-
random: formatUint256(random),
|
|
58
|
-
nullifier,
|
|
59
|
-
createdTxHash,
|
|
60
|
-
createdEventType,
|
|
61
|
-
createdAt,
|
|
62
|
-
spentTxHash,
|
|
63
|
-
};
|
|
64
|
-
return spentAt === undefined ? base : { ...base, spentAt };
|
|
65
|
-
}
|
|
66
|
-
// Try to decrypt a note. Returns the note record if successful, null otherwise.
|
|
67
|
-
function tryDecryptNoteRecord(chainId, record, account) {
|
|
68
|
-
const ciphertext = buildCiphertext(record.ciphertext);
|
|
69
|
-
let note;
|
|
70
|
-
try {
|
|
71
|
-
note = decryptNote(ciphertext, account.masterPublicKey);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
if (err instanceof CoreError) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
const random = note.random;
|
|
80
|
-
const amount = note.amount;
|
|
81
|
-
const token = note.token;
|
|
82
|
-
const npk = poseidon([account.masterPublicKey, random]);
|
|
83
|
-
const commitment = poseidon([npk, BigInt(token), amount]);
|
|
84
|
-
if (commitment !== parseHexToBigInt(record.commitment)) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const nullifier = poseidon([account.nullifyingKey, BigInt(record.index)]);
|
|
88
|
-
const nullifierHex = formatUint256(nullifier);
|
|
89
|
-
return createNoteRecord({
|
|
90
|
-
chainId,
|
|
91
|
-
index: record.index,
|
|
92
|
-
commitment: record.commitment,
|
|
93
|
-
token,
|
|
94
|
-
amount,
|
|
95
|
-
npk,
|
|
96
|
-
mpk: account.masterPublicKey,
|
|
97
|
-
random,
|
|
98
|
-
nullifier: nullifierHex,
|
|
99
|
-
createdTxHash: record.txHash,
|
|
100
|
-
createdEventType: record.eventType,
|
|
101
|
-
createdAt: record.insertedAt != null ? record.insertedAt * 1000 : undefined,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
// Pulls commitments from the indexer, updating local tree and notes atomically.
|
|
105
|
-
// Uses batch nullifier check to avoid individual 404-generating queries.
|
|
106
|
-
async function ingestFrom(chainId, start, account) {
|
|
107
|
-
let cursor = start;
|
|
108
|
-
for (;;) {
|
|
109
|
-
const batch = await indexerClient.fetchCommitmentBatch({
|
|
110
|
-
chainId,
|
|
111
|
-
start: cursor,
|
|
112
|
-
limit,
|
|
113
|
-
});
|
|
114
|
-
if (batch.commitments.length === 0) {
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
// Phase 1: Ingest commitments into tree + store
|
|
118
|
-
const decryptedNotes = [];
|
|
119
|
-
for (const record of batch.commitments) {
|
|
120
|
-
const expectedIndex = trees.getLeafCount(chainId);
|
|
121
|
-
if (record.index !== expectedIndex) {
|
|
122
|
-
throw new CoreError(`indexed commitments out of order: expected index ${expectedIndex}, got ${record.index} (cursor=${cursor})`);
|
|
123
|
-
}
|
|
124
|
-
const value = Hex.toBigInt(record.commitment);
|
|
125
|
-
const { index } = trees.addLeaf(chainId, value);
|
|
126
|
-
if (index !== record.index) {
|
|
127
|
-
throw new CoreError("local merkle tree desynchronized");
|
|
128
|
-
}
|
|
129
|
-
const noteRecord = tryDecryptNoteRecord(chainId, record, account);
|
|
130
|
-
const ciphertext = buildCiphertext(record.ciphertext);
|
|
131
|
-
const ciphertextBytes = encodeCiphertext(ciphertext);
|
|
132
|
-
await stateStore.syncCommitment({
|
|
133
|
-
leaf: {
|
|
134
|
-
chainId,
|
|
135
|
-
index: record.index,
|
|
136
|
-
commitment: record.commitment,
|
|
137
|
-
txHash: record.txHash,
|
|
138
|
-
eventType: record.eventType,
|
|
139
|
-
insertedAt: record.insertedAt,
|
|
140
|
-
},
|
|
141
|
-
ciphertext: {
|
|
142
|
-
chainId,
|
|
143
|
-
index: record.index,
|
|
144
|
-
payload: ciphertextBytes,
|
|
145
|
-
},
|
|
146
|
-
root: { chainId, root: record.root },
|
|
147
|
-
note: noteRecord ?? undefined,
|
|
148
|
-
});
|
|
149
|
-
if (noteRecord) {
|
|
150
|
-
decryptedNotes.push(noteRecord);
|
|
151
|
-
}
|
|
152
|
-
cursor = record.index + 1;
|
|
153
|
-
}
|
|
154
|
-
// Phase 2: Batch nullifier check for decrypted notes
|
|
155
|
-
if (decryptedNotes.length > 0) {
|
|
156
|
-
const notesToCheck = [];
|
|
157
|
-
for (const note of decryptedNotes) {
|
|
158
|
-
const existing = await stateStore.getNote(chainId, note.index);
|
|
159
|
-
if (existing?.spentAt !== undefined) {
|
|
160
|
-
await stateStore.putNullifier({
|
|
161
|
-
chainId,
|
|
162
|
-
nullifier: note.nullifier,
|
|
163
|
-
noteIndex: note.index,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
notesToCheck.push(note);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (notesToCheck.length > 0) {
|
|
171
|
-
const spentNullifiers = await indexerClient.checkNullifiers({
|
|
172
|
-
chainId,
|
|
173
|
-
nullifiers: notesToCheck.map((n) => n.nullifier),
|
|
174
|
-
});
|
|
175
|
-
const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
|
|
176
|
-
for (const note of notesToCheck) {
|
|
177
|
-
const spent = spentMap.get(note.nullifier);
|
|
178
|
-
if (spent) {
|
|
179
|
-
const spentAtMs = spent.spentAt * 1000;
|
|
180
|
-
await stateStore.putNullifier({
|
|
181
|
-
chainId,
|
|
182
|
-
nullifier: note.nullifier,
|
|
183
|
-
noteIndex: note.index,
|
|
184
|
-
});
|
|
185
|
-
await stateStore.markNoteSpent(chainId, note.index, spentAtMs, spent.txHash);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async function diagnoseChainState(chainId, localCount) {
|
|
193
|
-
// Tier 1: compare first commitment
|
|
194
|
-
const firstBatch = await indexerClient.fetchCommitmentBatch({
|
|
195
|
-
chainId,
|
|
196
|
-
start: 0,
|
|
197
|
-
limit: 1,
|
|
198
|
-
});
|
|
199
|
-
const localFirst = await stateStore.getLeaf(chainId, 0);
|
|
200
|
-
if (firstBatch.commitments.length === 0 || !localFirst) {
|
|
201
|
-
return { kind: "full-divergence" };
|
|
202
|
-
}
|
|
203
|
-
if (firstBatch.commitments[0].commitment !== localFirst.commitment) {
|
|
204
|
-
return { kind: "full-divergence" };
|
|
205
|
-
}
|
|
206
|
-
// Single-leaf shortcut: first matches and there's only one local leaf.
|
|
207
|
-
if (localCount === 1) {
|
|
208
|
-
return { kind: "consistent" };
|
|
209
|
-
}
|
|
210
|
-
// Compare last local commitment
|
|
211
|
-
const lastBatch = await indexerClient.fetchCommitmentBatch({
|
|
212
|
-
chainId,
|
|
213
|
-
start: localCount - 1,
|
|
214
|
-
limit: 1,
|
|
215
|
-
});
|
|
216
|
-
const localLast = await stateStore.getLeaf(chainId, localCount - 1);
|
|
217
|
-
if (lastBatch.commitments.length > 0 &&
|
|
218
|
-
localLast &&
|
|
219
|
-
lastBatch.commitments[0].commitment === localLast.commitment) {
|
|
220
|
-
return { kind: "consistent" };
|
|
221
|
-
}
|
|
222
|
-
// Tier 2: binary search for divergence point
|
|
223
|
-
const divergePoint = await findDivergencePoint(chainId, 1, localCount - 1);
|
|
224
|
-
return { kind: "partial-divergence", validPrefix: divergePoint };
|
|
225
|
-
}
|
|
226
|
-
async function findDivergencePoint(chainId, low, high) {
|
|
227
|
-
while (low < high) {
|
|
228
|
-
const mid = Math.floor((low + high) / 2);
|
|
229
|
-
const batch = await indexerClient.fetchCommitmentBatch({
|
|
230
|
-
chainId,
|
|
231
|
-
start: mid,
|
|
232
|
-
limit: 1,
|
|
233
|
-
});
|
|
234
|
-
const localLeaf = await stateStore.getLeaf(chainId, mid);
|
|
235
|
-
if (batch.commitments.length > 0 &&
|
|
236
|
-
localLeaf &&
|
|
237
|
-
batch.commitments[0].commitment === localLeaf.commitment) {
|
|
238
|
-
low = mid + 1;
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
high = mid;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return low;
|
|
245
|
-
}
|
|
246
|
-
// Clears stale tail data and re-ingests from the divergence point.
|
|
247
|
-
async function partialResync(chainId, fromIndex, account) {
|
|
248
|
-
await Promise.all([
|
|
249
|
-
stateStore.clearChainDataFromIndex(chainId, fromIndex),
|
|
250
|
-
stateStore.clearNullifiersFromIndex(chainId, fromIndex),
|
|
251
|
-
]);
|
|
252
|
-
trees.reset(chainId);
|
|
253
|
-
await rebuildTreeFromStore({
|
|
254
|
-
chainId,
|
|
255
|
-
trees,
|
|
256
|
-
loadLeaf: stateStore.getLeaf.bind(stateStore),
|
|
257
|
-
});
|
|
258
|
-
await ingestFrom(chainId, fromIndex, account);
|
|
259
|
-
}
|
|
260
|
-
// Clears local state for a chain and rehydrates fully from the indexer.
|
|
261
|
-
async function fullResync(chainId, account) {
|
|
262
|
-
trees.reset(chainId);
|
|
263
|
-
await Promise.all([
|
|
264
|
-
stateStore.clearLeaves(chainId),
|
|
265
|
-
stateStore.clearNotes(chainId),
|
|
266
|
-
stateStore.clearCiphertexts(chainId),
|
|
267
|
-
stateStore.clearNullifiers(chainId),
|
|
268
|
-
]);
|
|
269
|
-
await ingestFrom(chainId, 0, account);
|
|
270
|
-
}
|
|
271
|
-
// Decode a stored ciphertext (3 x 32-byte values) into a Ciphertext object
|
|
272
|
-
function decodeCiphertext(payload) {
|
|
273
|
-
const data = [];
|
|
274
|
-
for (let i = 0; i < 3; i++) {
|
|
275
|
-
const start = i * FieldSize.SCALAR;
|
|
276
|
-
const end = start + FieldSize.SCALAR;
|
|
277
|
-
const slice = payload.slice(start, end);
|
|
278
|
-
data.push(Hex.toBigInt(Hex.fromBytes(slice)));
|
|
279
|
-
}
|
|
280
|
-
return { data: data };
|
|
281
|
-
}
|
|
282
|
-
// Re-scan stored ciphertexts for a new account, using batch nullifier check.
|
|
283
|
-
async function rescanCiphertextsForAccount(chainId, account, leafCount) {
|
|
284
|
-
const accountMpk = formatUint256(account.masterPublicKey);
|
|
285
|
-
const pendingNotes = [];
|
|
286
|
-
for (let index = 0; index < leafCount; index++) {
|
|
287
|
-
const existingNote = await stateStore.getNote(chainId, index);
|
|
288
|
-
if (existingNote && existingNote.mpk === accountMpk) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const ciphertextBytes = await stateStore.getCiphertext(chainId, index);
|
|
292
|
-
if (!ciphertextBytes)
|
|
293
|
-
continue;
|
|
294
|
-
const leaf = await stateStore.getLeaf(chainId, index);
|
|
295
|
-
if (!leaf)
|
|
296
|
-
continue;
|
|
297
|
-
const ciphertext = decodeCiphertext(ciphertextBytes);
|
|
298
|
-
let note;
|
|
299
|
-
try {
|
|
300
|
-
note = decryptNote(ciphertext, account.masterPublicKey);
|
|
301
|
-
}
|
|
302
|
-
catch (err) {
|
|
303
|
-
if (err instanceof CoreError)
|
|
304
|
-
continue;
|
|
305
|
-
throw err;
|
|
306
|
-
}
|
|
307
|
-
const npk = poseidon([account.masterPublicKey, note.random]);
|
|
308
|
-
const commitment = poseidon([npk, BigInt(note.token), note.amount]);
|
|
309
|
-
if (commitment !== parseHexToBigInt(leaf.commitment))
|
|
310
|
-
continue;
|
|
311
|
-
const nullifier = poseidon([account.nullifyingKey, BigInt(index)]);
|
|
312
|
-
const nullifierHex = formatUint256(nullifier);
|
|
313
|
-
const baseNoteParams = {
|
|
314
|
-
chainId,
|
|
315
|
-
index,
|
|
316
|
-
commitment: leaf.commitment,
|
|
317
|
-
token: note.token,
|
|
318
|
-
amount: note.amount,
|
|
319
|
-
npk,
|
|
320
|
-
mpk: account.masterPublicKey,
|
|
321
|
-
random: note.random,
|
|
322
|
-
nullifier: nullifierHex,
|
|
323
|
-
createdTxHash: leaf.txHash,
|
|
324
|
-
createdEventType: leaf.eventType,
|
|
325
|
-
createdAt: leaf.insertedAt != null ? leaf.insertedAt * 1000 : undefined,
|
|
326
|
-
};
|
|
327
|
-
pendingNotes.push({
|
|
328
|
-
note: createNoteRecord(baseNoteParams),
|
|
329
|
-
baseParams: baseNoteParams,
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
if (pendingNotes.length > 0) {
|
|
333
|
-
const spentNullifiers = await indexerClient.checkNullifiers({
|
|
334
|
-
chainId,
|
|
335
|
-
nullifiers: pendingNotes.map((p) => p.note.nullifier),
|
|
336
|
-
});
|
|
337
|
-
const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
|
|
338
|
-
for (const { note, baseParams } of pendingNotes) {
|
|
339
|
-
const spent = spentMap.get(note.nullifier);
|
|
340
|
-
if (spent) {
|
|
341
|
-
await stateStore.putNullifier({
|
|
342
|
-
chainId,
|
|
343
|
-
nullifier: note.nullifier,
|
|
344
|
-
noteIndex: note.index,
|
|
345
|
-
});
|
|
346
|
-
await stateStore.putNote(createNoteRecord({
|
|
347
|
-
...baseParams,
|
|
348
|
-
spentAt: spent.spentAt * 1000,
|
|
349
|
-
spentTxHash: spent.txHash,
|
|
350
|
-
}));
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
await stateStore.putNote(note);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// Check existing unspent notes for nullification using batch check.
|
|
359
|
-
async function checkExistingNotesForNullification(chainId, account) {
|
|
360
|
-
const accountMpk = formatUint256(account.masterPublicKey);
|
|
361
|
-
const unspentNotes = await stateStore.listNotes({
|
|
362
|
-
chainId,
|
|
363
|
-
mpk: accountMpk,
|
|
364
|
-
includeSpent: false,
|
|
365
|
-
});
|
|
366
|
-
if (unspentNotes.length === 0)
|
|
367
|
-
return;
|
|
368
|
-
const spentNullifiers = await indexerClient.checkNullifiers({
|
|
369
|
-
chainId,
|
|
370
|
-
nullifiers: unspentNotes.map((n) => n.nullifier),
|
|
371
|
-
});
|
|
372
|
-
const spentMap = new Map(spentNullifiers.map((n) => [n.nullifier, n]));
|
|
373
|
-
for (const note of unspentNotes) {
|
|
374
|
-
const spent = spentMap.get(note.nullifier);
|
|
375
|
-
if (spent) {
|
|
376
|
-
await stateStore.putNullifier({
|
|
377
|
-
chainId,
|
|
378
|
-
nullifier: note.nullifier,
|
|
379
|
-
noteIndex: note.index,
|
|
380
|
-
});
|
|
381
|
-
await stateStore.markNoteSpent(chainId, note.index, spent.spentAt * 1000, spent.txHash);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Syncs a single chain into local state, optionally forcing a full rebuild.
|
|
387
|
-
*/
|
|
388
|
-
async function syncChain(chainId, account, opts = {}) {
|
|
389
|
-
ensureChainId(chainId);
|
|
390
|
-
const current = status.get(chainId) ?? {
|
|
391
|
-
inFlight: false,
|
|
392
|
-
lastSuccess: null,
|
|
393
|
-
};
|
|
394
|
-
if (current.inFlight)
|
|
395
|
-
return;
|
|
396
|
-
status.set(chainId, { ...current, inFlight: true, lastError: undefined });
|
|
397
|
-
try {
|
|
398
|
-
const start = opts.forceFullResync
|
|
399
|
-
? 0
|
|
400
|
-
: await rebuildTreeFromStore({
|
|
401
|
-
chainId,
|
|
402
|
-
trees,
|
|
403
|
-
loadLeaf: stateStore.getLeaf.bind(stateStore),
|
|
404
|
-
});
|
|
405
|
-
if (opts.forceFullResync) {
|
|
406
|
-
await fullResync(chainId, account);
|
|
407
|
-
}
|
|
408
|
-
else if (start > 0) {
|
|
409
|
-
try {
|
|
410
|
-
const diagnosis = await diagnoseChainState(chainId, start);
|
|
411
|
-
switch (diagnosis.kind) {
|
|
412
|
-
case "consistent":
|
|
413
|
-
try {
|
|
414
|
-
await ingestFrom(chainId, start, account);
|
|
415
|
-
}
|
|
416
|
-
catch (err) {
|
|
417
|
-
console.warn(`[note-sync] incremental ingest failed for chain ${chainId}, falling back to full resync:`, err);
|
|
418
|
-
await fullResync(chainId, account);
|
|
419
|
-
}
|
|
420
|
-
break;
|
|
421
|
-
case "partial-divergence":
|
|
422
|
-
await partialResync(chainId, diagnosis.validPrefix, account);
|
|
423
|
-
break;
|
|
424
|
-
case "full-divergence":
|
|
425
|
-
await fullResync(chainId, account);
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
catch (err) {
|
|
430
|
-
console.warn(`[note-sync] chain diagnosis failed for chain ${chainId}, falling back to full resync:`, err);
|
|
431
|
-
await fullResync(chainId, account);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
// start === 0, fresh first sync
|
|
436
|
-
try {
|
|
437
|
-
await ingestFrom(chainId, 0, account);
|
|
438
|
-
}
|
|
439
|
-
catch (err) {
|
|
440
|
-
console.warn(`[note-sync] initial ingest failed for chain ${chainId}, falling back to full resync:`, err);
|
|
441
|
-
await fullResync(chainId, account);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Rescan ciphertexts for multi-account support
|
|
445
|
-
const accountMpk = formatUint256(account.masterPublicKey);
|
|
446
|
-
const leafCount = trees.getLeafCount(chainId);
|
|
447
|
-
if (leafCount > 0 && !opts.forceFullResync) {
|
|
448
|
-
const accountNotes = await stateStore.listNotes({
|
|
449
|
-
chainId,
|
|
450
|
-
mpk: accountMpk,
|
|
451
|
-
});
|
|
452
|
-
if (accountNotes.length < leafCount) {
|
|
453
|
-
await rescanCiphertextsForAccount(chainId, account, leafCount);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// Check if any existing unspent notes have been nullified
|
|
457
|
-
await checkExistingNotesForNullification(chainId, account);
|
|
458
|
-
status.set(chainId, { inFlight: false, lastSuccess: Date.now() });
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
status.set(chainId, {
|
|
462
|
-
inFlight: false,
|
|
463
|
-
lastSuccess: current.lastSuccess ?? null,
|
|
464
|
-
lastError: err instanceof Error ? err.message : String(err),
|
|
465
|
-
});
|
|
466
|
-
throw err;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Syncs multiple chains sequentially.
|
|
471
|
-
*/
|
|
472
|
-
async function syncChains(chainIds, account, opts) {
|
|
473
|
-
for (const chainId of chainIds) {
|
|
474
|
-
await syncChain(chainId, account, opts);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function getStatus() {
|
|
478
|
-
return status;
|
|
479
|
-
}
|
|
480
|
-
return {
|
|
481
|
-
syncChain,
|
|
482
|
-
syncChains,
|
|
483
|
-
getStatus,
|
|
484
|
-
};
|
|
485
|
-
}
|