@artblocks/abx-sdk 0.1.0-alpha.15 → 0.1.0-alpha.17

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/ops.d.ts CHANGED
@@ -698,7 +698,7 @@ export declare function prepareLockScript(args: {
698
698
  }): PreparedTx;
699
699
  /** Set a CONTRACT-scope param to a literal `bytes32` (the raw owner setter — Params base).
700
700
  * Only for schema-less keys (a schema'd key closes the raw path: `SchemaGoverned`). This is
701
- * the write behind well-known contract params like `display.gateway`. The key joins the
701
+ * the write behind well-known contract params like `display.animation`. The key joins the
702
702
  * contract's on-chain key list automatically (`contractParamKeys`), so it is enumerated by
703
703
  * the generator and the renderer with no separate declaration. Signer must be the owner. */
704
704
  export declare function prepareSetContractParam(args: {
package/dist/ops.js CHANGED
@@ -1343,7 +1343,7 @@ export function prepareLockScript(args) {
1343
1343
  }
1344
1344
  /** Set a CONTRACT-scope param to a literal `bytes32` (the raw owner setter — Params base).
1345
1345
  * Only for schema-less keys (a schema'd key closes the raw path: `SchemaGoverned`). This is
1346
- * the write behind well-known contract params like `display.gateway`. The key joins the
1346
+ * the write behind well-known contract params like `display.animation`. The key joins the
1347
1347
  * contract's on-chain key list automatically (`contractParamKeys`), so it is enumerated by
1348
1348
  * the generator and the renderer with no separate declaration. Signer must be the owner. */
1349
1349
  export function prepareSetContractParam(args) {
package/dist/probe.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type PublicClient } from 'viem';
1
2
  /**
2
3
  * Capability probe for RPC endpoints. Reconstruction is `eth_getLogs`-heavy and
3
4
  * needs *archive* depth (to read a project's deploy block) plus a usable block
@@ -27,4 +28,32 @@ export declare function probeRpcEndpoints(opts?: {
27
28
  chainKey?: string;
28
29
  urls?: string[];
29
30
  }): Promise<RpcProbe[]>;
31
+ /**
32
+ * Can this endpoint still serve LOG HISTORY at `blockNumber`?
33
+ *
34
+ * The check this replaced was `getLogs({address: zeroAddress, fromBlock: old, toBlock: old})` and
35
+ * "any non-throw counts as archive access" — which cannot fail on a pruning node. `zeroAddress`
36
+ * never emits logs, so an empty result is what a *perfect* archive node returns too: the assertion
37
+ * had no discriminating power and only caught endpoints that *error* on old blocks. A node that
38
+ * silently answers `[]` for pruned history was graded `best`, and `abx add` then ground through
39
+ * half a million blocks and reconstructed nothing, because a successful empty response is not an
40
+ * error and never fails over to the next endpoint.
41
+ *
42
+ * So assert on something that MUST come back. Blocks are retained by everyone; receipts and the log
43
+ * index are what get pruned. We read the probe block, ask for one of its transactions' receipts, and
44
+ * — when that receipt carries a log — require `getLogs` to return that same log. Measured on two
45
+ * Base Sepolia endpoints at 500k blocks back, same transaction: one returns a receipt with 1 log,
46
+ * the other returns null. That is the discriminator, and it needs no hardcoded address or block.
47
+ *
48
+ * Fails SAFE, in the direction of the old behaviour: when the probe block has no transactions, or
49
+ * the receipt has no logs, there is nothing to assert on, so we fall back to the lenient
50
+ * "getLogs didn't throw" signal rather than manufacturing a failure.
51
+ *
52
+ * Exported for tests — `probeOne` builds its own transport per URL, so the discriminating logic is
53
+ * what gets asserted directly.
54
+ */
55
+ export declare function probeHistoryAt(client: PublicClient, blockNumber: bigint): Promise<{
56
+ retained: boolean;
57
+ reason?: string;
58
+ }>;
30
59
  //# sourceMappingURL=probe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAExD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,wBAAsB,iBAAiB,CACrC,IAAI,GAAE;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAM,GAC9C,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIrB"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6E,KAAK,YAAY,EAAC,MAAM,MAAM,CAAC;AAInH;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAExD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAsB,iBAAiB,CACrC,IAAI,GAAE;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAM,GAC9C,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIrB;AAyDD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,YAAY,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC,CAgD/C"}
package/dist/probe.js CHANGED
@@ -2,7 +2,17 @@ import { createPublicClient, http, zeroAddress } from 'viem';
2
2
  import { resolveChain, resolveRpcUrls, redactRpcUrl } from './chains.js';
3
3
  import { isGetLogsRangeError } from './reconstruct.js';
4
4
  const RANGE_PROBE = 2000n; // "wide" recent range to test the getLogs window
5
- const ARCHIVE_DEPTH = 100000n; // deep enough that non-archive nodes refuse
5
+ /**
6
+ * How far back "archive depth" means. Deliberately deep: reconstruction starts at a project's
7
+ * DEPLOY block, so the bar is "can this endpoint still serve a project that is not new?". At 500k
8
+ * blocks that is ~11 days on Base Sepolia (2s blocks) and ~10 weeks on Ethereum Sepolia (12s) —
9
+ * comfortably past the retention window of the pruning public endpoints, and far short of any
10
+ * genuinely archival node. It was 100_000, which sat *inside* the retained window of at least one
11
+ * default endpoint, so the probe passed on a node that could not reconstruct anything older.
12
+ */
13
+ const ARCHIVE_DEPTH = 500000n;
14
+ /** How many of a probe block's transactions to try a receipt for before giving up. */
15
+ const RECEIPT_TRIES = 3;
6
16
  export async function probeRpcEndpoints(opts = {}) {
7
17
  const chain = resolveChain(opts.chainKey);
8
18
  const urls = opts.urls ?? resolveRpcUrls(opts.chainKey);
@@ -46,14 +56,10 @@ async function probeOne(chain, url) {
46
56
  }
47
57
  // archive depth — one block, deep in the past, to isolate archive from range
48
58
  const old = head > ARCHIVE_DEPTH ? head - ARCHIVE_DEPTH : 0n;
49
- try {
50
- await client.getLogs({ address: zeroAddress, fromBlock: old, toBlock: old });
51
- probe.archive = true;
52
- }
53
- catch (err) {
54
- if (!probe.reason)
55
- probe.reason = firstLine(err);
56
- }
59
+ const retention = await probeHistoryAt(client, old);
60
+ probe.archive = retention.retained;
61
+ if (!probe.archive && !probe.reason)
62
+ probe.reason = retention.reason;
57
63
  if (!probe.archive) {
58
64
  probe.verdict = 'unusable';
59
65
  probe.reason = probe.reason ?? 'no archive access — can’t read a project’s deploy block';
@@ -68,6 +74,95 @@ async function probeOne(chain, url) {
68
74
  }
69
75
  return probe;
70
76
  }
77
+ /**
78
+ * Can this endpoint still serve LOG HISTORY at `blockNumber`?
79
+ *
80
+ * The check this replaced was `getLogs({address: zeroAddress, fromBlock: old, toBlock: old})` and
81
+ * "any non-throw counts as archive access" — which cannot fail on a pruning node. `zeroAddress`
82
+ * never emits logs, so an empty result is what a *perfect* archive node returns too: the assertion
83
+ * had no discriminating power and only caught endpoints that *error* on old blocks. A node that
84
+ * silently answers `[]` for pruned history was graded `best`, and `abx add` then ground through
85
+ * half a million blocks and reconstructed nothing, because a successful empty response is not an
86
+ * error and never fails over to the next endpoint.
87
+ *
88
+ * So assert on something that MUST come back. Blocks are retained by everyone; receipts and the log
89
+ * index are what get pruned. We read the probe block, ask for one of its transactions' receipts, and
90
+ * — when that receipt carries a log — require `getLogs` to return that same log. Measured on two
91
+ * Base Sepolia endpoints at 500k blocks back, same transaction: one returns a receipt with 1 log,
92
+ * the other returns null. That is the discriminator, and it needs no hardcoded address or block.
93
+ *
94
+ * Fails SAFE, in the direction of the old behaviour: when the probe block has no transactions, or
95
+ * the receipt has no logs, there is nothing to assert on, so we fall back to the lenient
96
+ * "getLogs didn't throw" signal rather than manufacturing a failure.
97
+ *
98
+ * Exported for tests — `probeOne` builds its own transport per URL, so the discriminating logic is
99
+ * what gets asserted directly.
100
+ */
101
+ export async function probeHistoryAt(client, blockNumber) {
102
+ const depth = `~${ARCHIVE_DEPTH} blocks back`; // the FAULT only — each caller words its own remedy
103
+ let txHashes = [];
104
+ try {
105
+ const block = await client.getBlock({ blockNumber, includeTransactions: false });
106
+ txHashes = block.transactions.slice(0, RECEIPT_TRIES);
107
+ }
108
+ catch (err) {
109
+ return { retained: false, reason: `can't read a block ${depth} — ${firstLine(err)}` };
110
+ }
111
+ for (const hash of txHashes) {
112
+ let logAddress;
113
+ try {
114
+ const receipt = await client.getTransactionReceipt({ hash });
115
+ logAddress = receipt.logs[0]?.address;
116
+ }
117
+ catch (err) {
118
+ // Only a genuine "no such receipt" proves pruning. A rate-limit or a transport blip must NOT
119
+ // become a "history pruned" claim about an endpoint that is actually fine — so anything else
120
+ // falls through to the lenient check below, which is what this function did before.
121
+ if (!looksLikeReceiptMissing(err))
122
+ break;
123
+ // The node served the block but not the receipt of a transaction inside it: its history is
124
+ // pruned at this depth. This is exactly what a "recent logs only" endpoint looks like.
125
+ return {
126
+ retained: false,
127
+ reason: `history pruned ${depth} — it serves the block but not the receipts of transactions inside it`,
128
+ };
129
+ }
130
+ if (!logAddress)
131
+ continue; // nothing to assert on in this tx — try the next
132
+ try {
133
+ const logs = await client.getLogs({ address: logAddress, fromBlock: blockNumber, toBlock: blockNumber });
134
+ if (logs.length > 0)
135
+ return { retained: true };
136
+ return {
137
+ retained: false,
138
+ reason: `log index pruned ${depth} — eth_getLogs returns EMPTY for a block whose receipts still hold logs`,
139
+ };
140
+ }
141
+ catch (err) {
142
+ return { retained: false, reason: `eth_getLogs failed ${depth} — ${firstLine(err)}` };
143
+ }
144
+ }
145
+ // Inconclusive (no transactions at the probe block, or none of them logged anything): keep the
146
+ // old lenient signal rather than inventing a verdict.
147
+ try {
148
+ await client.getLogs({ address: zeroAddress, fromBlock: blockNumber, toBlock: blockNumber });
149
+ return { retained: true };
150
+ }
151
+ catch (err) {
152
+ return { retained: false, reason: firstLine(err) };
153
+ }
154
+ }
155
+ /**
156
+ * Does this error mean "that receipt does not exist here" (viem's `TransactionReceiptNotFoundError`,
157
+ * i.e. the node answered `null`) as opposed to "I could not ask" (rate limit, timeout, 5xx)? Only the
158
+ * first is evidence about retention; treating the second as evidence would libel a healthy endpoint.
159
+ */
160
+ function looksLikeReceiptMissing(err) {
161
+ const e = err;
162
+ if (e?.name === 'TransactionReceiptNotFoundError')
163
+ return true;
164
+ return /receipt.*(not be found|not found)|could not be found/i.test(e?.message ?? '');
165
+ }
71
166
  function firstLine(err) {
72
167
  return (err?.message ?? String(err)).split('\n')[0].slice(0, 100);
73
168
  }
package/dist/probe.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type Address, type Hex, type PublicClient } from 'viem';
1
+ import { type Address, type Hex, type PublicClient, type Log } from 'viem';
2
2
  import { AbxSdkError } from './errors.js';
3
3
  import type { DependencyInfo, ExtensionInfo, MetadataField, ParamHooks, ParamSchema, ParamValue, ProjectState, SpineEvent, TokenState } from './types.js';
4
4
  export interface ReconstructOptions {
@@ -46,6 +46,21 @@ export declare function reconstructIncremental(client: PublicClient, prior: Proj
46
46
  getLogsRange?: bigint;
47
47
  readUriDocuments?: boolean;
48
48
  }): Promise<ProjectState>;
49
+ /**
50
+ * The fold / head-read half of {@link reconstructIncremental}, given logs the caller already
51
+ * fetched. A multi-address watch loop (`getLogs({address: Address[]})`) holds each touched
52
+ * project's delta; this consumes it so that loop is the only `eth_getLogs`. Folding stays here.
53
+ *
54
+ * `toBlock` is the inclusive scan head those logs cover — required even when `logs` is empty,
55
+ * so a quiet window still advances the checkpoint to the range the caller scanned, and so a
56
+ * caller that scanned a capped window (not chain head) cannot stamp `toBlock` past the last
57
+ * log they actually held.
58
+ */
59
+ export declare function reconstructFromLogs(client: PublicClient, prior: ProjectState, logs: readonly Log[], opts: {
60
+ factory?: Address;
61
+ readUriDocuments?: boolean;
62
+ toBlock: bigint;
63
+ }): Promise<ProjectState>;
49
64
  /** The structural projection folded from the spine (everything that isn't a head read).
50
65
  * Exported for tests and advanced consumers; the shape may evolve with the spine. */
51
66
  export interface Fold {
@@ -113,6 +128,34 @@ export declare class GetLogsScanTooLargeError extends AbxSdkError {
113
128
  * contract has no code at block 0 (true for every ABX clone — deployed long after genesis).
114
129
  */
115
130
  export declare function discoverDeployBlock(client: PublicClient, address: Address, toBlock?: bigint): Promise<bigint | null>;
131
+ /**
132
+ * Ordinary RPC tiers cap how many addresses one `eth_getLogs` filter may carry.
133
+ * This is an RPC-tier opinion, not a protocol constant — callers with a dedicated
134
+ * node can raise it; `0` disables splitting (one filter for the whole list).
135
+ */
136
+ export declare const DEFAULT_GETLOGS_ADDRESS_BATCH = 1000;
137
+ export interface GetLogsAdaptiveOptions {
138
+ maxChunks?: number;
139
+ range?: bigint;
140
+ /** Split `address[]` filters into this many addresses per request. Default
141
+ * {@link DEFAULT_GETLOGS_ADDRESS_BATCH}. `0` = no split. */
142
+ addressBatch?: number;
143
+ }
144
+ /**
145
+ * Pull logs over a block range, adapting to RPC range caps — and, when `address` is
146
+ * a list, splitting that list so a registered-set watch scan stays one code path for
147
+ * self-host and hosted. Providers cap `eth_getLogs` very differently and the numbers
148
+ * change, so we don't hard-code a window: try the whole range, and when the RPC
149
+ * rejects it, halve the window and sweep. `ABX_GETLOGS_RANGE` overrides the starting
150
+ * window. Address-list chunking is a separate cap (`addressBatch`); 5000-block
151
+ * per-tick windows belong to the caller (the watcher), not here.
152
+ *
153
+ * Once the working window is known we estimate the whole job *before* grinding it: if
154
+ * it would exceed the cap, we stop early (only the probe calls spent) and throw
155
+ * {@link GetLogsScanTooLargeError} so the caller can offer the real fix (a higher-range
156
+ * RPC) or an explicit opt-in to chunk through it.
157
+ */
158
+ export declare function getLogsAdaptive(client: PublicClient, address: Address | readonly Address[], fromBlock: bigint, toBlock: bigint, opts?: GetLogsAdaptiveOptions): Promise<Log[]>;
116
159
  /**
117
160
  * Multicall in chunks, tolerating per-leg failure — and, when a whole chunk comes back failed,
118
161
  * retrying its legs individually before believing it.
@@ -1 +1 @@
1
- {"version":3,"file":"reconstruct.d.ts","sourceRoot":"","sources":["../src/reconstruct.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,OAAO,EACZ,KAAK,GAAG,EACR,KAAK,YAAY,EAElB,MAAM,MAAM,CAAC;AAWd,OAAO,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AAExC,OAAO,KAAK,EAEV,cAAc,EACd,aAAa,EACb,aAAa,EACb,UAAU,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,UAAU,EACV,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAWvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,YAAY,EACnB,IAAI,GAAE;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAM,GACpG,OAAO,CAAC,YAAY,CAAC,CAgCvB;AAID;sFACsF;AACtF,MAAM,WAAW,IAAI;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,GAAG,IAAI,CAAC;IACjD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACpC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACrD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtC,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC7C,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAE9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAE7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACvC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB,EAAE,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAC7C,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAC;IACnC,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AA2ED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CA+NpD;AAuID,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAED,yFAAyF;AACzF,qBAAa,wBAAyB,SAAQ,WAAW;IAErD,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,iBAAiB,EAAE,MAAM;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAHX,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM;CASvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;AAkED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAkCzB"}
1
+ {"version":3,"file":"reconstruct.d.ts","sourceRoot":"","sources":["../src/reconstruct.ts"],"names":[],"mappings":"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"}
@@ -38,14 +38,29 @@ export async function reconstructIncremental(client, prior, opts = {}) {
38
38
  const priorTo = BigInt(prior.toBlock);
39
39
  const origin = BigInt(prior.deployBlock ?? prior.fromBlock);
40
40
  const resumeFrom = priorTo + 1n > origin ? priorTo + 1n : origin;
41
- let fresh = [];
41
+ let rawLogs = [];
42
42
  if (head >= resumeFrom) {
43
- const rawLogs = await getLogsAdaptive(client, address, resumeFrom, head, {
43
+ rawLogs = await getLogsAdaptive(client, address, resumeFrom, head, {
44
44
  maxChunks: opts.maxChunks,
45
45
  range: opts.getLogsRange,
46
46
  });
47
- fresh = decodedToSpineEvents(parseEventLogs({ abi: spineEventAbi, logs: rawLogs }));
48
47
  }
48
+ const toBlock = head >= priorTo ? head : priorTo;
49
+ return reconstructFromLogs(client, prior, rawLogs, { factory, readUriDocuments: opts.readUriDocuments, toBlock });
50
+ }
51
+ /**
52
+ * The fold / head-read half of {@link reconstructIncremental}, given logs the caller already
53
+ * fetched. A multi-address watch loop (`getLogs({address: Address[]})`) holds each touched
54
+ * project's delta; this consumes it so that loop is the only `eth_getLogs`. Folding stays here.
55
+ *
56
+ * `toBlock` is the inclusive scan head those logs cover — required even when `logs` is empty,
57
+ * so a quiet window still advances the checkpoint to the range the caller scanned, and so a
58
+ * caller that scanned a capped window (not chain head) cannot stamp `toBlock` past the last
59
+ * log they actually held.
60
+ */
61
+ export async function reconstructFromLogs(client, prior, logs, opts) {
62
+ const factory = opts.factory ?? prior.factory ?? undefined;
63
+ const fresh = decodedToSpineEvents(parseEventLogs({ abi: spineEventAbi, logs: logs }));
49
64
  // Merge the prior log with the freshly-fetched tail, de-duping by position so an
50
65
  // overlapping/duplicate re-run can't double-count, then fold the complete spine.
51
66
  const seen = new Set();
@@ -58,8 +73,13 @@ export async function reconstructIncremental(client, prior, opts = {}) {
58
73
  all.push(ev);
59
74
  }
60
75
  all.sort(byOrder);
61
- const toBlock = head >= priorTo ? head : priorTo;
62
- return assembleState(client, address, all, foldSpine(all), BigInt(prior.fromBlock), toBlock, factory, !!opts.readUriDocuments);
76
+ // Chunk content has no log (too large); `ScriptUpdated` is the ping. A Transfer currently
77
+ // used to re-download every chunk. Keep the prior digest unless that ping is in `fresh`.
78
+ const scriptTouched = fresh.some((e) => e.name === 'ScriptUpdated');
79
+ const reuseScript = !scriptTouched && prior.script?.digest != null ? prior.script : undefined;
80
+ const priorTo = BigInt(prior.toBlock);
81
+ const toBlock = opts.toBlock >= priorTo ? opts.toBlock : priorTo;
82
+ return assembleState(client, prior.address, all, foldSpine(all), BigInt(prior.fromBlock), toBlock, factory, !!opts.readUriDocuments, reuseScript);
63
83
  }
64
84
  /** Turn decoded viem logs into the canonical, serializable {@link SpineEvent} list. */
65
85
  function decodedToSpineEvents(decoded) {
@@ -365,7 +385,7 @@ export function foldSpine(events) {
365
385
  return fold;
366
386
  }
367
387
  /** Build the full {@link ProjectState} from a fold + provenance, then overlay head reads. */
368
- async function assembleState(client, address, events, fold, fromBlock, toBlock, factory, readUriDocuments = false) {
388
+ async function assembleState(client, address, events, fold, fromBlock, toBlock, factory, readUriDocuments = false, reuseScript) {
369
389
  // ensure a TokenState exists for every token referenced by on-chain fields/locks/params
370
390
  // (a deployed-but-unminted token can carry fields before any Transfer)
371
391
  for (const id of fold.tokenFields.keys())
@@ -477,7 +497,7 @@ async function assembleState(client, address, events, fold, fromBlock, toBlock,
477
497
  reconstructedAt: new Date().toISOString(),
478
498
  rpcUrl: client.transport?.url,
479
499
  };
480
- await applyHeadReads(client, state, factory, readUriDocuments);
500
+ await applyHeadReads(client, state, factory, readUriDocuments, reuseScript);
481
501
  return state;
482
502
  }
483
503
  /** Default safety cap on chunk count; raise via the `maxChunks` opt (or ABX_GETLOGS_MAX_CHUNKS —
@@ -550,49 +570,68 @@ export async function discoverDeployBlock(client, address, toBlock) {
550
570
  }
551
571
  }
552
572
  /**
553
- * Pull a contract's logs over a block range, adapting to RPC range caps. Providers
554
- * cap `eth_getLogs` very differently and the numbers change, so we don't hard-code a
555
- * window: try the whole range, and when the RPC rejects it, halve the window and
556
- * sweep — adopting the smaller window for the rest of the run. So reconstruction is a
557
- * single request on a generous RPC, still completes on a constrained one, and (for a
558
- * 1/1, whose events cluster at the deploy block) stays cheap. `ABX_GETLOGS_RANGE`
559
- * overrides the starting window.
573
+ * Ordinary RPC tiers cap how many addresses one `eth_getLogs` filter may carry.
574
+ * This is an RPC-tier opinion, not a protocol constant — callers with a dedicated
575
+ * node can raise it; `0` disables splitting (one filter for the whole list).
576
+ */
577
+ export const DEFAULT_GETLOGS_ADDRESS_BATCH = 1000;
578
+ /**
579
+ * Pull logs over a block range, adapting to RPC range caps — and, when `address` is
580
+ * a list, splitting that list so a registered-set watch scan stays one code path for
581
+ * self-host and hosted. Providers cap `eth_getLogs` very differently and the numbers
582
+ * change, so we don't hard-code a window: try the whole range, and when the RPC
583
+ * rejects it, halve the window and sweep. `ABX_GETLOGS_RANGE` overrides the starting
584
+ * window. Address-list chunking is a separate cap (`addressBatch`); 5000-block
585
+ * per-tick windows belong to the caller (the watcher), not here.
560
586
  *
561
587
  * Once the working window is known we estimate the whole job *before* grinding it: if
562
588
  * it would exceed the cap, we stop early (only the probe calls spent) and throw
563
589
  * {@link GetLogsScanTooLargeError} so the caller can offer the real fix (a higher-range
564
590
  * RPC) or an explicit opt-in to chunk through it.
565
591
  */
566
- async function getLogsAdaptive(client, address, fromBlock, toBlock, opts = {}) {
592
+ export async function getLogsAdaptive(client, address, fromBlock, toBlock, opts = {}) {
593
+ const addrs = (Array.isArray(address) ? address : [address]);
594
+ if (addrs.length === 0)
595
+ return [];
596
+ const batchSize = opts.addressBatch === 0 ? addrs.length : Math.max(1, opts.addressBatch ?? DEFAULT_GETLOGS_ADDRESS_BATCH);
597
+ const batches = [];
598
+ for (let i = 0; i < addrs.length; i += batchSize)
599
+ batches.push(addrs.slice(i, i + batchSize));
567
600
  const span = toBlock - fromBlock + 1n;
568
601
  const envRange = readEnv('ABX_GETLOGS_RANGE');
569
- let window = opts.range && opts.range > 0n ? opts.range : envRange && BigInt(envRange) > 0n ? BigInt(envRange) : span;
602
+ const startWindow = opts.range && opts.range > 0n ? opts.range : envRange && BigInt(envRange) > 0n ? BigInt(envRange) : span;
570
603
  const out = [];
571
- let start = fromBlock;
572
604
  let estimated = false;
573
- while (start <= toBlock) {
574
- const end = start + window - 1n < toBlock ? start + window - 1n : toBlock;
575
- let logs;
576
- try {
577
- logs = (await client.getLogs({ address, fromBlock: start, toBlock: end }));
578
- }
579
- catch (err) {
580
- if (isGetLogsRangeError(err) && window > 1n) {
581
- window = window > 2n ? window / 2n : 1n; // RPC rejected this window — shrink and retry
582
- continue;
605
+ for (const batch of batches) {
606
+ const filter = batch.length === 1 ? batch[0] : batch;
607
+ let window = startWindow;
608
+ let start = fromBlock;
609
+ while (start <= toBlock) {
610
+ const end = start + window - 1n < toBlock ? start + window - 1n : toBlock;
611
+ let logs;
612
+ try {
613
+ logs = (await client.getLogs({ address: filter, fromBlock: start, toBlock: end }));
614
+ }
615
+ catch (err) {
616
+ if (isGetLogsRangeError(err) && window > 1n) {
617
+ window = window > 2n ? window / 2n : 1n; // RPC rejected this window — shrink and retry
618
+ continue;
619
+ }
620
+ throw err; // genuine failure, or already at one block per call
621
+ }
622
+ out.push(...logs);
623
+ start = end + 1n;
624
+ // Window now proven. Estimate the whole job once (range chunks × address batches)
625
+ // and bail *before* grinding if absurd.
626
+ if (!estimated) {
627
+ estimated = true;
628
+ const remaining = toBlock - start + 1n;
629
+ const perBatch = (remaining > 0n ? Number(remaining / window) + 1 : 0) + 1; // +1 for the chunk just done
630
+ const need = perBatch * batches.length;
631
+ const cap = resolveMaxChunks(opts.maxChunks);
632
+ if (need > cap)
633
+ throw new GetLogsScanTooLargeError(span, window, need, cap);
583
634
  }
584
- throw err; // genuine failure, or already at one block per call
585
- }
586
- out.push(...logs);
587
- start = end + 1n;
588
- // Window now proven. Estimate the whole job once and bail *before* grinding if absurd.
589
- if (!estimated) {
590
- estimated = true;
591
- const remaining = toBlock - start + 1n;
592
- const need = (remaining > 0n ? Number(remaining / window) + 1 : 0) + 1; // +1 for the chunk just done
593
- const cap = resolveMaxChunks(opts.maxChunks);
594
- if (need > cap)
595
- throw new GetLogsScanTooLargeError(span, window, need, cap);
596
635
  }
597
636
  }
598
637
  return out;
@@ -702,7 +741,7 @@ export async function multicallChunked(client, contracts, chunkSize) {
702
741
  * actually wants to display it (`nothing served depends on it`, the one exception being a fresh
703
742
  * `abx demo` read-back, which asks for it explicitly).
704
743
  */
705
- async function applyHeadReads(client, state, factory, readUriDocuments = false) {
744
+ async function applyHeadReads(client, state, factory, readUriDocuments = false, reuseScript) {
706
745
  const token = { address: state.address, abi: oneOfOneImageAbi };
707
746
  const tokenIds = state.tokens.map((t) => t.tokenId);
708
747
  // The edition family (ERC-1155) shares every cheap fixed-return name with the 721 superset ABI
@@ -730,8 +769,11 @@ async function applyHeadReads(client, state, factory, readUriDocuments = false)
730
769
  cheap.push({ address: factory, abi: oneOfOneImageFactoryAbi, functionName: 'isAbxClone', args: [state.address] }, { address: factory, abi: oneOfOneImageFactoryAbi, functionName: 'implementation' });
731
770
  }
732
771
  // On-chain script: chunk content is too large to log, so the count is a head read.
772
+ // Skip when the caller already holds a digest and `fresh` had no `ScriptUpdated` —
773
+ // a Transfer must not re-download every chunk.
774
+ const keepPriorScript = !!(state.script && reuseScript?.digest != null);
733
775
  const scriptIdx = cheap.length;
734
- if (state.script) {
776
+ if (state.script && !keepPriorScript) {
735
777
  cheap.push({ address: state.address, abi: seriesCodeAbi, functionName: 'scriptChunkCount' });
736
778
  }
737
779
  // ── unbounded returns: each one may assemble a whole document on-chain ──
@@ -774,7 +816,11 @@ async function applyHeadReads(client, state, factory, readUriDocuments = false)
774
816
  tok.tokenURI = heavyVals[1 + k] ?? null;
775
817
  });
776
818
  }
777
- if (state.script) {
819
+ if (state.script && keepPriorScript) {
820
+ state.script.chunkCount = reuseScript.chunkCount;
821
+ state.script.digest = reuseScript.digest;
822
+ }
823
+ else if (state.script) {
778
824
  const count = cheapVals[scriptIdx] ?? null;
779
825
  state.script.chunkCount = count === null ? null : Number(count);
780
826
  // digest = keccak over the concatenated chunks — the content half of effect inputsHash