@secondlayer/shared 7.7.0 → 7.9.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.
Files changed (57) hide show
  1. package/dist/src/archive/derived-stage-report.d.ts +99 -0
  2. package/dist/src/archive/derived-stage-report.js +64 -0
  3. package/dist/src/archive/derived-stage-report.js.map +10 -0
  4. package/dist/src/archive/disk-guard.d.ts +46 -0
  5. package/dist/src/archive/disk-guard.js +69 -0
  6. package/dist/src/archive/disk-guard.js.map +10 -0
  7. package/dist/src/archive/key-registry.d.ts +52 -0
  8. package/dist/src/archive/key-registry.js +168 -0
  9. package/dist/src/archive/key-registry.js.map +11 -0
  10. package/dist/src/archive/per-height-digest.d.ts +99 -0
  11. package/dist/src/archive/per-height-digest.js +194 -0
  12. package/dist/src/archive/per-height-digest.js.map +11 -0
  13. package/dist/src/archive/progress.d.ts +36 -0
  14. package/dist/src/archive/progress.js +75 -0
  15. package/dist/src/archive/progress.js.map +10 -0
  16. package/dist/src/archive/range-digest.d.ts +7 -0
  17. package/dist/src/archive/semantic-digest-builder.d.ts +1094 -0
  18. package/dist/src/archive/semantic-digest-builder.js +302 -0
  19. package/dist/src/archive/semantic-digest-builder.js.map +11 -0
  20. package/dist/src/archive/semantic-digest.d.ts +131 -0
  21. package/dist/src/archive/semantic-digest.js +143 -0
  22. package/dist/src/archive/semantic-digest.js.map +10 -0
  23. package/dist/src/archive/status.d.ts +92 -0
  24. package/dist/src/archive/status.js +106 -0
  25. package/dist/src/archive/status.js.map +10 -0
  26. package/dist/src/db/index.d.ts +11 -1
  27. package/dist/src/db/index.js +26 -1
  28. package/dist/src/db/index.js.map +6 -5
  29. package/dist/src/db/queries/chain-reorgs.d.ts +7 -0
  30. package/dist/src/db/queries/chain-reorgs.js +25 -1
  31. package/dist/src/db/queries/chain-reorgs.js.map +6 -5
  32. package/dist/src/db/queries/contracts.d.ts +7 -0
  33. package/dist/src/db/queries/instance.d.ts +1038 -0
  34. package/dist/src/db/queries/instance.js +92 -0
  35. package/dist/src/db/queries/instance.js.map +10 -0
  36. package/dist/src/db/queries/integrity.d.ts +7 -0
  37. package/dist/src/db/queries/subgraph-gaps.d.ts +7 -0
  38. package/dist/src/db/queries/subgraph-operations.d.ts +7 -0
  39. package/dist/src/db/queries/subgraphs.d.ts +10 -1
  40. package/dist/src/db/queries/subgraphs.js +47 -23
  41. package/dist/src/db/queries/subgraphs.js.map +7 -7
  42. package/dist/src/db/queries/subscriptions.d.ts +7 -0
  43. package/dist/src/db/queries/subscriptions.js +47 -31
  44. package/dist/src/db/queries/subscriptions.js.map +5 -5
  45. package/dist/src/db/schema.d.ts +8 -1
  46. package/dist/src/index.d.ts +11 -1
  47. package/dist/src/index.js +26 -1
  48. package/dist/src/index.js.map +6 -5
  49. package/dist/src/node/client.d.ts +11 -1
  50. package/dist/src/node/client.js +42 -4
  51. package/dist/src/node/client.js.map +3 -3
  52. package/dist/src/node/local-client.d.ts +7 -0
  53. package/dist/src/node/nakamoto.d.ts +22 -7
  54. package/dist/src/node/nakamoto.js +46 -5
  55. package/dist/src/node/nakamoto.js.map +3 -3
  56. package/migrations/0115_instances.ts +33 -0
  57. package/package.json +38 -2
@@ -0,0 +1,99 @@
1
+ /**
2
+ * v1 canonical byte encoding for chain identity — the shape that lets
3
+ * independently-run runtimes agree on the same block.
4
+ *
5
+ * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one
6
+ * source: replay the same bytes, get the same rows. It cannot prove two nodes
7
+ * observed the same chain, because their Parquet encoders legally differ in
8
+ * column padding, compression, JSON key order, and null placement — two
9
+ * operators processing the same block can ship byte-different Parquets that
10
+ * carry identical content.
11
+ *
12
+ * The semantic digest is the shape that lets independent runtimes agree. Every
13
+ * value on a block/transaction/event is normalized into a canonical byte string
14
+ * by rules that MUST NOT change without a version bump. Two v1 computations
15
+ * over the same range agree, or one is wrong about the chain — never about
16
+ * formatting.
17
+ *
18
+ * Encoding rules (v1):
19
+ * - Fields join with `\x1f` (ASCII US, unit separator — cannot legally
20
+ * appear in any chain value; a printable delimiter risks collision with
21
+ * contract-emitted strings).
22
+ * - Null encodes as `\x00` (single NUL). Distinct from empty string `""`.
23
+ * - Integers encode as their base-10 string. bigint → decimal string.
24
+ * - JSON payload columns (function_args, event data) encode as canonical
25
+ * JSON: object keys sorted lexicographically, no whitespace, bigint →
26
+ * quoted decimal string, non-finite numbers rejected.
27
+ * - Field order per record type is fixed and never reordered.
28
+ * - Digest = sha256(bytes), lowercase hex.
29
+ *
30
+ * Rollup rules (v1) — order MUST match the archive's stream order or an
31
+ * honest re-will disagree with the manifest:
32
+ * - blocks: (height asc)
33
+ * - transactions: (block_height asc, tx_index asc, tx_id asc)
34
+ * - events: (block_height asc, event_index asc, tx_id asc)
35
+ * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
36
+ *
37
+ * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
38
+ * pin the encoding. Any change to bytes must bump the spec version.
39
+ */
40
+ declare const SEMANTIC_DIGEST_SPEC_V1: "sha256:semantic-v1";
41
+ /**
42
+ * Per-height semantic digest sidecars — enable "verify one block" without
43
+ * downloading a whole 50k-block partition.
44
+ *
45
+ * The partition-level semantic digest is the shape a byte-level comparator
46
+ * wants: one string per partition, cheap to compare in bulk. But a caller who
47
+ * only cares about block 8_500_017 has to pull an entire Parquet file to check
48
+ * one row. The per-height sidecar closes that gap: one 32-byte block digest and
49
+ * two roll-up digests per height (transactions and events), addressable by
50
+ * height without touching the raw datasets.
51
+ *
52
+ * Rollup rules (v1) — MUST match the exporter's stream order or an honest
53
+ * re-derivation will disagree:
54
+ * - transactions_rollup: sha256(concat(tx_digest_bytes, tx_index asc, tx_id asc))
55
+ * `null` when the height has no transactions.
56
+ * - events_rollup: sha256(concat(event_digest_bytes, event_index asc, tx_id asc))
57
+ * `null` when the height has no events.
58
+ * - block_digest: the semantic-v1 block digest itself (one row per
59
+ * height, so no rollup needed).
60
+ *
61
+ * `null` is deliberate: an empty rollup is not the sha256 of nothing (which
62
+ * would collide across two empty datasets); an absent-here-but-present-elsewhere
63
+ * dataset must never compare equal to a truly-empty one.
64
+ */
65
+ interface PerHeightDigestRow {
66
+ height: number;
67
+ block_digest: string;
68
+ transactions_rollup: string | null;
69
+ events_rollup: string | null;
70
+ }
71
+ declare class PerHeightDigestAccumulator {
72
+ private readonly rows;
73
+ setBlockDigest(height: number, blockDigestHex: string): void;
74
+ appendTransactionDigest(height: number, digestHex: string): void;
75
+ appendEventDigest(height: number, digestHex: string): void;
76
+ /**
77
+ * Drain the accumulator into height-ascending rows. Heights with no block
78
+ * digest are dropped with a thrown error — a canonical partition MUST have
79
+ * a block at every height it covers, and silently emitting a row without
80
+ * one would hide the missing block from downstream verifiers.
81
+ */
82
+ drain(fromBlock: number, toBlock: number): PerHeightDigestRow[];
83
+ spec(): typeof SEMANTIC_DIGEST_SPEC_V1;
84
+ private ensure;
85
+ }
86
+ /**
87
+ * Registered in the snapshot manifest under `digest_index[]`. One entry per
88
+ * partition, addressable at height granularity without downloading raw data.
89
+ */
90
+ interface DigestIndexPartition {
91
+ from_block: number;
92
+ to_block: number;
93
+ path: string;
94
+ row_count: number;
95
+ byte_size: number;
96
+ sha256: string;
97
+ digest_spec: typeof SEMANTIC_DIGEST_SPEC_V1;
98
+ }
99
+ export { PerHeightDigestRow, PerHeightDigestAccumulator, DigestIndexPartition };
@@ -0,0 +1,194 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/semantic-digest.ts
18
+ import { createHash } from "node:crypto";
19
+ var SEMANTIC_DIGEST_SPEC_V1 = "sha256:semantic-v1";
20
+ var FIELD_SEP = "\x1F";
21
+ var NULL_MARKER = "\x00";
22
+ var textEncoder = new TextEncoder;
23
+ function canonicalJson(value) {
24
+ if (value === null)
25
+ return "null";
26
+ if (typeof value === "string" || typeof value === "boolean") {
27
+ return JSON.stringify(value);
28
+ }
29
+ if (typeof value === "number") {
30
+ if (!Number.isFinite(value)) {
31
+ throw new Error("canonicalJson: non-finite number");
32
+ }
33
+ return JSON.stringify(value);
34
+ }
35
+ if (typeof value === "bigint")
36
+ return JSON.stringify(value.toString());
37
+ if (Array.isArray(value)) {
38
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
39
+ }
40
+ if (typeof value === "object") {
41
+ const object = value;
42
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
43
+ }
44
+ return "null";
45
+ }
46
+ function scalar(value) {
47
+ if (value === null || value === undefined)
48
+ return NULL_MARKER;
49
+ if (typeof value === "number") {
50
+ if (!Number.isFinite(value)) {
51
+ throw new Error("semanticDigest: non-finite number");
52
+ }
53
+ if (!Number.isInteger(value)) {
54
+ throw new Error(`semanticDigest: non-integer scalar ${value}`);
55
+ }
56
+ return String(value);
57
+ }
58
+ if (typeof value === "bigint")
59
+ return value.toString();
60
+ return value;
61
+ }
62
+ function jsonScalar(value) {
63
+ return value === null || value === undefined ? NULL_MARKER : canonicalJson(value);
64
+ }
65
+ function sha256Hex(input) {
66
+ return createHash("sha256").update(input).digest("hex");
67
+ }
68
+ function encodeBlock(row) {
69
+ return [
70
+ scalar(row.height),
71
+ scalar(row.hash),
72
+ scalar(row.parent_hash),
73
+ scalar(row.burn_block_height),
74
+ scalar(row.burn_block_hash),
75
+ scalar(row.index_block_hash),
76
+ scalar(row.timestamp)
77
+ ].join(FIELD_SEP);
78
+ }
79
+ function encodeTransaction(row) {
80
+ return [
81
+ scalar(row.tx_id),
82
+ scalar(row.block_height),
83
+ scalar(row.tx_index),
84
+ scalar(row.type),
85
+ scalar(row.sender),
86
+ scalar(row.status),
87
+ scalar(row.contract_id),
88
+ scalar(row.function_name),
89
+ jsonScalar(row.function_args),
90
+ scalar(row.raw_result),
91
+ scalar(row.raw_tx)
92
+ ].join(FIELD_SEP);
93
+ }
94
+ function encodeEvent(row) {
95
+ return [
96
+ scalar(row.tx_id),
97
+ scalar(row.block_height),
98
+ scalar(row.event_index),
99
+ scalar(row.type),
100
+ jsonScalar(row.data)
101
+ ].join(FIELD_SEP);
102
+ }
103
+ var semanticDigest = {
104
+ v1: {
105
+ spec: SEMANTIC_DIGEST_SPEC_V1,
106
+ encodeBlock,
107
+ encodeTransaction,
108
+ encodeEvent,
109
+ block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
110
+ transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
111
+ event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
112
+ }
113
+ };
114
+
115
+ class SemanticDigestRollup {
116
+ hasher = createHash("sha256");
117
+ count = 0;
118
+ static forDataset(_dataset) {
119
+ return new SemanticDigestRollup;
120
+ }
121
+ appendRowDigest(rowDigestHex) {
122
+ this.hasher.update(Buffer.from(rowDigestHex, "hex"));
123
+ this.count += 1;
124
+ }
125
+ rowCount() {
126
+ return this.count;
127
+ }
128
+ digest() {
129
+ return this.count === 0 ? null : this.hasher.copy().digest("hex");
130
+ }
131
+ spec() {
132
+ return SEMANTIC_DIGEST_SPEC_V1;
133
+ }
134
+ }
135
+
136
+ // src/archive/per-height-digest.ts
137
+ import { createHash as createHash2 } from "node:crypto";
138
+ class PerHeightDigestAccumulator {
139
+ rows = new Map;
140
+ setBlockDigest(height, blockDigestHex) {
141
+ this.ensure(height).block_digest = blockDigestHex;
142
+ }
143
+ appendTransactionDigest(height, digestHex) {
144
+ const a = this.ensure(height);
145
+ a.tx.update(Buffer.from(digestHex, "hex"));
146
+ a.tx_count += 1;
147
+ }
148
+ appendEventDigest(height, digestHex) {
149
+ const a = this.ensure(height);
150
+ a.event.update(Buffer.from(digestHex, "hex"));
151
+ a.event_count += 1;
152
+ }
153
+ drain(fromBlock, toBlock) {
154
+ const rows = [];
155
+ for (let h = fromBlock;h <= toBlock; h++) {
156
+ const a = this.rows.get(h);
157
+ if (!a)
158
+ continue;
159
+ if (!a.block_digest) {
160
+ throw new Error(`per-height digest at ${h} has tx/event rollups but no block digest`);
161
+ }
162
+ rows.push({
163
+ height: h,
164
+ block_digest: a.block_digest,
165
+ transactions_rollup: a.tx_count === 0 ? null : a.tx.copy().digest("hex"),
166
+ events_rollup: a.event_count === 0 ? null : a.event.copy().digest("hex")
167
+ });
168
+ }
169
+ return rows;
170
+ }
171
+ spec() {
172
+ return SEMANTIC_DIGEST_SPEC_V1;
173
+ }
174
+ ensure(height) {
175
+ let a = this.rows.get(height);
176
+ if (!a) {
177
+ a = {
178
+ block_digest: null,
179
+ tx: createHash2("sha256"),
180
+ tx_count: 0,
181
+ event: createHash2("sha256"),
182
+ event_count: 0
183
+ };
184
+ this.rows.set(height, a);
185
+ }
186
+ return a;
187
+ }
188
+ }
189
+ export {
190
+ PerHeightDigestAccumulator
191
+ };
192
+
193
+ //# debugId=2ABEAEFAD6B6D8C464756E2164756E21
194
+ //# sourceMappingURL=per-height-digest.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/semantic-digest.ts", "../src/archive/per-height-digest.ts"],
4
+ "sourcesContent": [
5
+ "import { createHash } from \"node:crypto\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(\n\t\t_dataset: \"blocks\" | \"transactions\" | \"events\",\n\t): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: \"blocks\" | \"transactions\" | \"events\";\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n",
6
+ "import { createHash } from \"node:crypto\";\nimport { SEMANTIC_DIGEST_SPEC_V1 } from \"./semantic-digest.ts\";\n\n/**\n * Per-height semantic digest sidecars — enable \"verify one block\" without\n * downloading a whole 50k-block partition.\n *\n * The partition-level semantic digest is the shape a byte-level comparator\n * wants: one string per partition, cheap to compare in bulk. But a caller who\n * only cares about block 8_500_017 has to pull an entire Parquet file to check\n * one row. The per-height sidecar closes that gap: one 32-byte block digest and\n * two roll-up digests per height (transactions and events), addressable by\n * height without touching the raw datasets.\n *\n * Rollup rules (v1) — MUST match the exporter's stream order or an honest\n * re-derivation will disagree:\n * - transactions_rollup: sha256(concat(tx_digest_bytes, tx_index asc, tx_id asc))\n * `null` when the height has no transactions.\n * - events_rollup: sha256(concat(event_digest_bytes, event_index asc, tx_id asc))\n * `null` when the height has no events.\n * - block_digest: the semantic-v1 block digest itself (one row per\n * height, so no rollup needed).\n *\n * `null` is deliberate: an empty rollup is not the sha256 of nothing (which\n * would collide across two empty datasets); an absent-here-but-present-elsewhere\n * dataset must never compare equal to a truly-empty one.\n */\n\nexport interface PerHeightDigestRow {\n\theight: number;\n\tblock_digest: string;\n\ttransactions_rollup: string | null;\n\tevents_rollup: string | null;\n}\n\ninterface Accumulator {\n\tblock_digest: string | null;\n\ttx: ReturnType<typeof createHash>;\n\ttx_count: number;\n\tevent: ReturnType<typeof createHash>;\n\tevent_count: number;\n}\n\nexport class PerHeightDigestAccumulator {\n\tprivate readonly rows = new Map<number, Accumulator>();\n\n\tsetBlockDigest(height: number, blockDigestHex: string): void {\n\t\tthis.ensure(height).block_digest = blockDigestHex;\n\t}\n\n\tappendTransactionDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.tx.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.tx_count += 1;\n\t}\n\n\tappendEventDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.event.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.event_count += 1;\n\t}\n\n\t/**\n\t * Drain the accumulator into height-ascending rows. Heights with no block\n\t * digest are dropped with a thrown error — a canonical partition MUST have\n\t * a block at every height it covers, and silently emitting a row without\n\t * one would hide the missing block from downstream verifiers.\n\t */\n\tdrain(fromBlock: number, toBlock: number): PerHeightDigestRow[] {\n\t\tconst rows: PerHeightDigestRow[] = [];\n\t\tfor (let h = fromBlock; h <= toBlock; h++) {\n\t\t\tconst a = this.rows.get(h);\n\t\t\tif (!a) continue; // Missing entirely — surfaced by canonical audit, not here.\n\t\t\tif (!a.block_digest) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`per-height digest at ${h} has tx/event rollups but no block digest`,\n\t\t\t\t);\n\t\t\t}\n\t\t\trows.push({\n\t\t\t\theight: h,\n\t\t\t\tblock_digest: a.block_digest,\n\t\t\t\ttransactions_rollup:\n\t\t\t\t\ta.tx_count === 0 ? null : a.tx.copy().digest(\"hex\"),\n\t\t\t\tevents_rollup:\n\t\t\t\t\ta.event_count === 0 ? null : a.event.copy().digest(\"hex\"),\n\t\t\t});\n\t\t}\n\t\treturn rows;\n\t}\n\n\tspec(): typeof SEMANTIC_DIGEST_SPEC_V1 {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n\n\tprivate ensure(height: number): Accumulator {\n\t\tlet a = this.rows.get(height);\n\t\tif (!a) {\n\t\t\ta = {\n\t\t\t\tblock_digest: null,\n\t\t\t\ttx: createHash(\"sha256\"),\n\t\t\t\ttx_count: 0,\n\t\t\t\tevent: createHash(\"sha256\"),\n\t\t\t\tevent_count: 0,\n\t\t\t};\n\t\t\tthis.rows.set(height, a);\n\t\t}\n\t\treturn a;\n\t}\n}\n\n/**\n * Registered in the snapshot manifest under `digest_index[]`. One entry per\n * partition, addressable at height granularity without downloading raw data.\n */\nexport interface DigestIndexPartition {\n\tfrom_block: number;\n\tto_block: number;\n\tpath: string;\n\trow_count: number;\n\tbyte_size: number;\n\tsha256: string;\n\tdigest_spec: typeof SEMANTIC_DIGEST_SPEC_V1;\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "2ABEAEFAD6B6D8C464756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Progress reporting for multi-hour archive jobs.
3
+ *
4
+ * A job that prints nothing until it finishes is indistinguishable from a hung
5
+ * one. That is not a cosmetic problem: on 2026-08-12 an ran silently for
6
+ * 2.5 hours and progress had to be inferred by listing partition filenames on
7
+ * disk, and a killed upload left a log whose single line could not distinguish
8
+ * "died immediately" from "died at 90%".
9
+ *
10
+ * Two rules make the output trustworthy:
11
+ *
12
+ * - **Time-based, not count-based.** Reporting every N items goes quiet for
13
+ * an hour when items are large and floods when they are small. Reporting
14
+ * every N seconds behaves the same for a sparse range and a dense one, so
15
+ * silence always means the same thing: something is wrong.
16
+ * - **stderr only.** Progress is chrome. Putting it on stdout would corrupt
17
+ * `--json` output for every caller that pipes it.
18
+ */
19
+ type ProgressReporter = {
20
+ /** Record cumulative progress. Emits at most once per interval. */
21
+ tick: (completed: number, detail?: string) => void
22
+ /** Emit a final line regardless of interval. */
23
+ finish: (detail?: string) => void
24
+ };
25
+ type ProgressOptions = {
26
+ label: string
27
+ /** Total units, when known. Enables percentage and ETA. */
28
+ total?: number
29
+ intervalMs?: number
30
+ /** Defaults to stderr. Injectable so tests capture without spying. */
31
+ write?: (line: string) => void
32
+ now?: () => number
33
+ };
34
+ declare function formatDuration(seconds: number): string;
35
+ declare function createProgressReporter(options: ProgressOptions): ProgressReporter;
36
+ export { formatDuration, createProgressReporter, ProgressReporter, ProgressOptions };
@@ -0,0 +1,75 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/progress.ts
18
+ var DEFAULT_INTERVAL_MS = 30000;
19
+ function formatDuration(seconds) {
20
+ if (!Number.isFinite(seconds) || seconds < 0)
21
+ return "unknown";
22
+ if (seconds < 90)
23
+ return `${Math.round(seconds)}s`;
24
+ if (seconds < 5400)
25
+ return `${Math.round(seconds / 60)}m`;
26
+ if (seconds < 172800)
27
+ return `${(seconds / 3600).toFixed(1)}h`;
28
+ return `${(seconds / 86400).toFixed(1)}d`;
29
+ }
30
+ function createProgressReporter(options) {
31
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
32
+ const now = options.now ?? (() => Date.now());
33
+ const write = options.write ?? ((line2) => process.stderr.write(`${line2}
34
+ `));
35
+ const startedAt = now();
36
+ let lastEmit = startedAt;
37
+ function line(completed, detail) {
38
+ const elapsed = (now() - startedAt) / 1000;
39
+ const parts = [`${options.label}:`];
40
+ if (options.total !== undefined && options.total > 0) {
41
+ const pct = Math.floor(completed / options.total * 100);
42
+ parts.push(`${completed}/${options.total} (${pct}%)`);
43
+ if (completed > 0 && elapsed > 0) {
44
+ const remaining = (options.total - completed) / completed * elapsed;
45
+ parts.push(`eta ${formatDuration(remaining)}`);
46
+ }
47
+ } else {
48
+ parts.push(String(completed));
49
+ }
50
+ parts.push(`elapsed ${formatDuration(elapsed)}`);
51
+ if (detail)
52
+ parts.push(`· ${detail}`);
53
+ return ` ${parts.join(" ")}`;
54
+ }
55
+ return {
56
+ tick(completed, detail) {
57
+ const current = now();
58
+ if (current - lastEmit < intervalMs)
59
+ return;
60
+ lastEmit = current;
61
+ write(line(completed, detail));
62
+ },
63
+ finish(detail) {
64
+ lastEmit = now();
65
+ write(line(options.total ?? 0, detail));
66
+ }
67
+ };
68
+ }
69
+ export {
70
+ formatDuration,
71
+ createProgressReporter
72
+ };
73
+
74
+ //# debugId=C1E6E800BC29361764756E2164756E21
75
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/progress.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Progress reporting for multi-hour archive jobs.\n *\n * A job that prints nothing until it finishes is indistinguishable from a hung\n * one. That is not a cosmetic problem: on 2026-08-12 an export ran silently for\n * 2.5 hours and progress had to be inferred by listing partition filenames on\n * disk, and a killed upload left a log whose single line could not distinguish\n * \"died immediately\" from \"died at 90%\".\n *\n * Two rules make the output trustworthy:\n *\n * - **Time-based, not count-based.** Reporting every N items goes quiet for\n * an hour when items are large and floods when they are small. Reporting\n * every N seconds behaves the same for a sparse range and a dense one, so\n * silence always means the same thing: something is wrong.\n * - **stderr only.** Progress is chrome. Putting it on stdout would corrupt\n * `--json` output for every caller that pipes it.\n */\n\nexport type ProgressReporter = {\n\t/** Record cumulative progress. Emits at most once per interval. */\n\ttick: (completed: number, detail?: string) => void;\n\t/** Emit a final line regardless of interval. */\n\tfinish: (detail?: string) => void;\n};\n\nexport type ProgressOptions = {\n\tlabel: string;\n\t/** Total units, when known. Enables percentage and ETA. */\n\ttotal?: number;\n\tintervalMs?: number;\n\t/** Defaults to stderr. Injectable so tests capture without spying. */\n\twrite?: (line: string) => void;\n\tnow?: () => number;\n};\n\nconst DEFAULT_INTERVAL_MS = 30_000;\n\nexport function formatDuration(seconds: number): string {\n\tif (!Number.isFinite(seconds) || seconds < 0) return \"unknown\";\n\tif (seconds < 90) return `${Math.round(seconds)}s`;\n\tif (seconds < 5_400) return `${Math.round(seconds / 60)}m`;\n\tif (seconds < 172_800) return `${(seconds / 3_600).toFixed(1)}h`;\n\treturn `${(seconds / 86_400).toFixed(1)}d`;\n}\n\nexport function createProgressReporter(\n\toptions: ProgressOptions,\n): ProgressReporter {\n\tconst intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n\tconst now = options.now ?? (() => Date.now());\n\tconst write =\n\t\toptions.write ?? ((line: string) => process.stderr.write(`${line}\\n`));\n\n\tconst startedAt = now();\n\tlet lastEmit = startedAt;\n\n\tfunction line(completed: number, detail?: string): string {\n\t\tconst elapsed = (now() - startedAt) / 1000;\n\t\tconst parts = [`${options.label}:`];\n\n\t\tif (options.total !== undefined && options.total > 0) {\n\t\t\tconst pct = Math.floor((completed / options.total) * 100);\n\t\t\tparts.push(`${completed}/${options.total} (${pct}%)`);\n\t\t\t// ETA only once there is a rate to extrapolate from; a prediction made\n\t\t\t// from zero progress is noise dressed as information.\n\t\t\tif (completed > 0 && elapsed > 0) {\n\t\t\t\tconst remaining = ((options.total - completed) / completed) * elapsed;\n\t\t\t\tparts.push(`eta ${formatDuration(remaining)}`);\n\t\t\t}\n\t\t} else {\n\t\t\tparts.push(String(completed));\n\t\t}\n\n\t\tparts.push(`elapsed ${formatDuration(elapsed)}`);\n\t\tif (detail) parts.push(`· ${detail}`);\n\t\treturn ` ${parts.join(\" \")}`;\n\t}\n\n\treturn {\n\t\ttick(completed, detail) {\n\t\t\tconst current = now();\n\t\t\tif (current - lastEmit < intervalMs) return;\n\t\t\tlastEmit = current;\n\t\t\twrite(line(completed, detail));\n\t\t},\n\t\tfinish(detail) {\n\t\t\tlastEmit = now();\n\t\t\twrite(line(options.total ?? 0, detail));\n\t\t},\n\t};\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAoCA,IAAM,sBAAsB;AAErB,SAAS,cAAc,CAAC,SAAyB;AAAA,EACvD,IAAI,CAAC,OAAO,SAAS,OAAO,KAAK,UAAU;AAAA,IAAG,OAAO;AAAA,EACrD,IAAI,UAAU;AAAA,IAAI,OAAO,GAAG,KAAK,MAAM,OAAO;AAAA,EAC9C,IAAI,UAAU;AAAA,IAAO,OAAO,GAAG,KAAK,MAAM,UAAU,EAAE;AAAA,EACtD,IAAI,UAAU;AAAA,IAAS,OAAO,IAAI,UAAU,MAAO,QAAQ,CAAC;AAAA,EAC5D,OAAO,IAAI,UAAU,OAAQ,QAAQ,CAAC;AAAA;AAGhC,SAAS,sBAAsB,CACrC,SACmB;AAAA,EACnB,MAAM,aAAa,QAAQ,cAAc;AAAA,EACzC,MAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAAA,EAC3C,MAAM,QACL,QAAQ,UAAU,CAAC,UAAiB,QAAQ,OAAO,MAAM,GAAG;AAAA,CAAQ;AAAA,EAErE,MAAM,YAAY,IAAI;AAAA,EACtB,IAAI,WAAW;AAAA,EAEf,SAAS,IAAI,CAAC,WAAmB,QAAyB;AAAA,IACzD,MAAM,WAAW,IAAI,IAAI,aAAa;AAAA,IACtC,MAAM,QAAQ,CAAC,GAAG,QAAQ,QAAQ;AAAA,IAElC,IAAI,QAAQ,UAAU,aAAa,QAAQ,QAAQ,GAAG;AAAA,MACrD,MAAM,MAAM,KAAK,MAAO,YAAY,QAAQ,QAAS,GAAG;AAAA,MACxD,MAAM,KAAK,GAAG,aAAa,QAAQ,UAAU,OAAO;AAAA,MAGpD,IAAI,YAAY,KAAK,UAAU,GAAG;AAAA,QACjC,MAAM,aAAc,QAAQ,QAAQ,aAAa,YAAa;AAAA,QAC9D,MAAM,KAAK,OAAO,eAAe,SAAS,GAAG;AAAA,MAC9C;AAAA,IACD,EAAO;AAAA,MACN,MAAM,KAAK,OAAO,SAAS,CAAC;AAAA;AAAA,IAG7B,MAAM,KAAK,WAAW,eAAe,OAAO,GAAG;AAAA,IAC/C,IAAI;AAAA,MAAQ,MAAM,KAAK,KAAI,QAAQ;AAAA,IACnC,OAAO,KAAK,MAAM,KAAK,GAAG;AAAA;AAAA,EAG3B,OAAO;AAAA,IACN,IAAI,CAAC,WAAW,QAAQ;AAAA,MACvB,MAAM,UAAU,IAAI;AAAA,MACpB,IAAI,UAAU,WAAW;AAAA,QAAY;AAAA,MACrC,WAAW;AAAA,MACX,MAAM,KAAK,WAAW,MAAM,CAAC;AAAA;AAAA,IAE9B,MAAM,CAAC,QAAQ;AAAA,MACd,WAAW,IAAI;AAAA,MACf,MAAM,KAAK,QAAQ,SAAS,GAAG,MAAM,CAAC;AAAA;AAAA,EAExC;AAAA;",
8
+ "debugId": "C1E6E800BC29361764756E2164756E21",
9
+ "names": []
10
+ }
@@ -190,6 +190,12 @@ interface ApiKeysTable {
190
190
  revoked_at: Date | null;
191
191
  created_at: Generated<Date>;
192
192
  }
193
+ /** Singleton self-host / publisher identity. Network is immutable. */
194
+ interface InstancesTable {
195
+ id: Generated<string>;
196
+ network: string;
197
+ created_at: Generated<Date>;
198
+ }
193
199
  interface AccountsTable {
194
200
  id: Generated<string>;
195
201
  /** NULL for unclaimed ghost accounts (anonymous self-serve keys). */
@@ -732,6 +738,7 @@ interface Database {
732
738
  contracts: ContractsTable;
733
739
  subgraphs: SubgraphsTable;
734
740
  api_keys: ApiKeysTable;
741
+ instances: InstancesTable;
735
742
  accounts: AccountsTable;
736
743
  sessions: SessionsTable;
737
744
  magic_links: MagicLinksTable;