smail-sdk 0.1.0 → 0.1.1

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.
@@ -1,5 +1,5 @@
1
1
  import { type Address, type Hex } from "viem";
2
- import { type LogWindowOptions } from "./events.js";
2
+ import type { LogWindowOptions } from "./events.js";
3
3
  import { type MerklePath, type PoolDeposit } from "./private.js";
4
4
  import type { SmailReadClient } from "./read.js";
5
5
  /**
@@ -23,6 +23,13 @@ import type { SmailReadClient } from "./read.js";
23
23
  * claimant rehashes its own chunk against the stored chunk root, and the
24
24
  * root the path reaches must be one the pool itself knows.
25
25
  */
26
+ /**
27
+ * Blocks asked for in one `eth_getLogs` while reading a pool. Wide on purpose:
28
+ * a pool's log is sparse, and a node that refuses a range is asked for each
29
+ * half instead, so a strict node costs a few extra calls and a generous one
30
+ * answers the pool's whole life in a handful.
31
+ */
32
+ export declare const POOL_LOG_WINDOW = 1000000;
26
33
  export declare const CHUNK_LEVELS = 10;
27
34
  export declare const CHUNK_SIZE: number;
28
35
  export interface SpentEntry {
@@ -71,6 +78,8 @@ export declare function emptyPoolIndex(pool: Address, firstBlock: bigint): PoolI
71
78
  */
72
79
  export declare function syncPoolIndex(client: Pick<SmailReadClient, "getLogs" | "getBlockNumber">, base: PoolIndex, options?: LogWindowOptions & {
73
80
  onProgress?: (done: bigint, total: bigint) => void;
81
+ /** Windows read at once. One by default; a terminal with a long way to catch up asks for more. */
82
+ concurrency?: number;
74
83
  }): Promise<PoolIndex>;
75
84
  /**
76
85
  * The index as a header and two flat binary tables, the same shape for the
@@ -1,7 +1,6 @@
1
1
  import { poseidon2 } from "poseidon-lite";
2
2
  import { getAbiItem } from "viem";
3
3
  import { privateSnailAbi } from "./abi/index.js";
4
- import { LOG_WINDOW, walkWindows } from "./events.js";
5
4
  import { PRIVATE_LEVELS, ZERO_VALUE } from "./private.js";
6
5
  /**
7
6
  * A private pool's full public history, kept so a claim never has to re-read
@@ -24,6 +23,13 @@ import { PRIVATE_LEVELS, ZERO_VALUE } from "./private.js";
24
23
  * claimant rehashes its own chunk against the stored chunk root, and the
25
24
  * root the path reaches must be one the pool itself knows.
26
25
  */
26
+ /**
27
+ * Blocks asked for in one `eth_getLogs` while reading a pool. Wide on purpose:
28
+ * a pool's log is sparse, and a node that refuses a range is asked for each
29
+ * half instead, so a strict node costs a few extra calls and a generous one
30
+ * answers the pool's whole life in a handful.
31
+ */
32
+ export const POOL_LOG_WINDOW = 1_000_000;
27
33
  export const CHUNK_LEVELS = 10;
28
34
  export const CHUNK_SIZE = 1 << CHUNK_LEVELS;
29
35
  const DEPOSIT = getAbiItem({ abi: privateSnailAbi, name: "Deposit" });
@@ -123,11 +129,23 @@ export async function syncPoolIndex(client, base, options = {}) {
123
129
  const from = base.toBlock + 1n;
124
130
  const added = [];
125
131
  const spent = new Map(base.spent);
126
- await walkWindows(from, to, options.window ?? LOG_WINDOW, false, async (start, end) => {
127
- const [deposits, withdrawals] = await Promise.all([
128
- client.getLogs({ address: base.pool, event: DEPOSIT, fromBlock: start, toBlock: end }),
129
- client.getLogs({ address: base.pool, event: WITHDRAWAL, fromBlock: start, toBlock: end }),
130
- ]);
132
+ const read = async (start, end) => {
133
+ let deposits, withdrawals;
134
+ try {
135
+ [deposits, withdrawals] = await Promise.all([
136
+ client.getLogs({ address: base.pool, event: DEPOSIT, fromBlock: start, toBlock: end }),
137
+ client.getLogs({ address: base.pool, event: WITHDRAWAL, fromBlock: start, toBlock: end }),
138
+ ]);
139
+ }
140
+ catch (cause) {
141
+ // A node that refuses the range is asked for each half instead.
142
+ if (start === end)
143
+ throw cause;
144
+ const middle = start + (end - start) / 2n;
145
+ await read(start, middle);
146
+ await read(middle + 1n, end);
147
+ return;
148
+ }
131
149
  for (const log of deposits) {
132
150
  const args = log.args;
133
151
  added.push({
@@ -144,9 +162,24 @@ export async function syncPoolIndex(client, base, options = {}) {
144
162
  const args = log.args;
145
163
  spent.set(BigInt(args.nullifierHash), { recipient: args.recipient, transactionHash: log.transactionHash ?? "0x" });
146
164
  }
147
- options.onProgress?.(end - from + 1n, to - from + 1n);
148
- return false;
149
- });
165
+ };
166
+ // The range in windows, read `concurrency` at a time. Order does not matter:
167
+ // deposits are sorted by leaf index below, and a nullifier is spent once.
168
+ const width = BigInt(options.window ?? POOL_LOG_WINDOW);
169
+ const windows = [];
170
+ for (let start = from; start <= to; start += width)
171
+ windows.push([start, start + width - 1n < to ? start + width - 1n : to]);
172
+ let next = 0;
173
+ let done = 0n;
174
+ const worker = async () => {
175
+ while (next < windows.length) {
176
+ const [start, end] = windows[next++];
177
+ await read(start, end);
178
+ done += end - start + 1n;
179
+ options.onProgress?.(done, to - from + 1n);
180
+ }
181
+ };
182
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(options.concurrency ?? 1, windows.length)) }, worker));
150
183
  added.sort((a, b) => a.leafIndex - b.leafIndex);
151
184
  const deposits = base.deposits.concat(added);
152
185
  for (let i = base.deposits.length; i < deposits.length; i++) {
@@ -154,14 +187,14 @@ export async function syncPoolIndex(client, base, options = {}) {
154
187
  throw new PoolIndexMismatch(`Deposit ${i} is missing; the tree cannot be rebuilt.`);
155
188
  }
156
189
  const chunkRoots = base.chunkRoots.slice();
157
- const next = { pool: base.pool, toBlock: to, deposits, spent, chunkRoots };
190
+ const synced = { pool: base.pool, toBlock: to, deposits, spent, chunkRoots };
158
191
  if (added.length) {
159
192
  const first = Math.floor(base.deposits.length / CHUNK_SIZE);
160
193
  const last = Math.floor((deposits.length - 1) / CHUNK_SIZE);
161
194
  for (let chunk = first; chunk <= last; chunk++)
162
- chunkRoots[chunk] = chunkRootOf(chunkLeaves(next, chunk));
195
+ chunkRoots[chunk] = chunkRootOf(chunkLeaves(synced, chunk));
163
196
  }
164
- return next;
197
+ return synced;
165
198
  }
166
199
  const DEPOSIT_BYTES = 66;
167
200
  const SPENT_BYTES = 84;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smail-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Send ETH or USDG as a claim link on Robinhood Chain: keys, links, signatures and contract calls",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://smail.cash",