@secondlayer/shared 13.14.0 → 14.0.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.
- package/README.md +1 -1
- package/dist/src/archive/key-registry.js +1 -1
- package/dist/src/archive/key-registry.js.map +1 -1
- package/dist/src/archive/reference.js +8 -6
- package/dist/src/archive/reference.js.map +2 -2
- package/dist/src/crypto/ed25519.js +1 -1
- package/dist/src/crypto/ed25519.js.map +1 -1
- package/dist/src/crypto/hmac.js +1 -1
- package/dist/src/crypto/hmac.js.map +1 -1
- package/dist/src/crypto/secondlayer-webhook.js +13 -11
- package/dist/src/crypto/secondlayer-webhook.js.map +2 -2
- package/dist/src/db/queries/webhooks.js +6 -4
- package/dist/src/db/queries/webhooks.js.map +2 -2
- package/dist/src/errors.js +2 -2
- package/dist/src/errors.js.map +2 -2
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +24 -20
- package/dist/src/index.js.map +3 -3
- package/dist/src/node/hiro-pg-client.d.ts +9 -1
- package/dist/src/node/hiro-pg-client.js +7 -4
- package/dist/src/node/hiro-pg-client.js.map +3 -3
- package/dist/src/node/local-client.js +8 -7
- package/dist/src/node/local-client.js.map +3 -3
- package/dist/src/streams-bulk-manifest.js +1 -1
- package/dist/src/streams-bulk-manifest.js.map +1 -1
- package/package.json +2 -6
- package/dist/src/node/archive-client.d.ts +0 -39
- package/dist/src/node/archive-client.js +0 -399
- package/dist/src/node/archive-client.js.map +0 -12
|
@@ -50,17 +50,18 @@ class LocalClient {
|
|
|
50
50
|
const vmRows = await db.selectFrom("vm_events").select(["tx_id", "ordinal", "type", "data"]).where("block_height", "=", height).orderBy("ordinal", "asc").execute();
|
|
51
51
|
const vm_events = reconstructVmEventsForReplay(vmRows);
|
|
52
52
|
const state_writes = await db.selectFrom("state_writes").select(["tx_index", "ordinal", "key", "value_hex"]).where("block_height", "=", height).orderBy("ordinal", "asc").execute();
|
|
53
|
+
const timestamp = Number(block.timestamp);
|
|
53
54
|
return {
|
|
54
55
|
block_hash: block.hash,
|
|
55
|
-
block_height: block.height,
|
|
56
|
-
index_block_hash: "",
|
|
56
|
+
block_height: Number(block.height),
|
|
57
|
+
index_block_hash: block.index_block_hash ?? "",
|
|
57
58
|
parent_block_hash: block.parent_hash,
|
|
58
59
|
parent_index_block_hash: "",
|
|
59
|
-
burn_block_hash: "",
|
|
60
|
-
burn_block_height: block.burn_block_height,
|
|
61
|
-
burn_block_timestamp:
|
|
60
|
+
burn_block_hash: block.burn_block_hash ?? "",
|
|
61
|
+
burn_block_height: Number(block.burn_block_height),
|
|
62
|
+
burn_block_timestamp: timestamp,
|
|
62
63
|
miner_txid: "",
|
|
63
|
-
timestamp
|
|
64
|
+
timestamp,
|
|
64
65
|
transactions: transactions.map((tx) => {
|
|
65
66
|
const entry = {
|
|
66
67
|
txid: tx.tx_id,
|
|
@@ -111,5 +112,5 @@ export {
|
|
|
111
112
|
reconstructVmEventsForReplay
|
|
112
113
|
};
|
|
113
114
|
|
|
114
|
-
//# debugId=
|
|
115
|
+
//# debugId=85374ECE2AE4234264756E2164756E21
|
|
115
116
|
//# sourceMappingURL=local-client.js.map
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../src/event-types.ts", "../src/node/local-client.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"// The decoded event-type vocabulary — canonical home is now\n// `@secondlayer/stacks/filters` (the LEAF of the dependency graph: shared\n// depends on stacks, never the reverse, so no package can drift a private\n// copy upward). Re-exported here so every existing `@secondlayer/shared`\n// import keeps working unchanged.\nimport {\n\tDECODED_EVENT_TYPES,\n\ttype VM_NODE_TO_STORED_TYPE,\n} from \"@secondlayer/stacks/filters\";\nimport type { DecodedEventType } from \"@secondlayer/stacks/filters\";\n\nexport {\n\tDECODED_EVENT_TYPES,\n\tVM_EVENT_TYPES,\n\tVM_NODE_TO_STORED_TYPE,\n\ttype DecodedEventType,\n\ttype VmEventType,\n\ttype VmNodeEventType,\n} from \"@secondlayer/stacks/filters\";\n\ntype VmStoredToNode = {\n\t[K in keyof typeof VM_NODE_TO_STORED_TYPE as (typeof VM_NODE_TO_STORED_TYPE)[K]]: K;\n};\n\n/** Inverse of {@link VM_NODE_TO_STORED_TYPE}. Replay reconstruction emits the\n * node-shaped `/new_block.vm_events` body, so ingest can re-parse it. */\nexport const VM_STORED_TO_NODE_TYPE: VmStoredToNode = {\n\tnested_contract_call: \"contract_call_event\",\n\tvar_set: \"var_set_event\",\n\tmap_set: \"map_set_event\",\n\tmap_insert: \"map_insert_event\",\n\tmap_delete: \"map_delete_event\",\n};\n\n/** Alias kept for the Streams surface (identical to {@link DECODED_EVENT_TYPES}).\n * Explicit type annotation required — isolatedDeclarations emits `unknown` for\n * a bare const-to-const alias. */\nexport const STREAMS_EVENT_TYPES: typeof DECODED_EVENT_TYPES =\n\tDECODED_EVENT_TYPES;\nexport type StreamsEventType = DecodedEventType;\n\n/**\n * Wire vocabulary for `chain=bitcoin` Streams events (plan 059) — Runes\n * activity, mirroring the Stacks `ft_*` family. Same four kinds as 058's\n * `RuneEventKind` (`packages/api/src/index/runes.ts`), duplicated here as the\n * canonical shared const rather than imported from the api package (shared\n * never depends on api); the DB `kind` column (`etch|mint|transfer|burn`) is\n * mapped at the edge in both readers.\n */\nexport const RUNE_EVENT_TYPES = [\n\t\"rune_etch\",\n\t\"rune_mint\",\n\t\"rune_transfer\",\n\t\"rune_burn\",\n] as const;\nexport type RuneEventType = (typeof RUNE_EVENT_TYPES)[number];\n",
|
|
6
|
-
"/**\n * Local replay client — reconstructs NewBlockPayload from our own Postgres.\n *\n * Used for re-orgs, reprocessing, and self-serve replay after genesis sync.\n * Eliminates need for self-hosted Hiro API for blocks already in our DB.\n */\n\nimport type { Kysely } from \"kysely\";\nimport type { Database } from \"../db/types.ts\";\nimport {\n\tVM_STORED_TO_NODE_TYPE,\n\ttype VmEventType,\n\ttype VmNodeEventType,\n} from \"../event-types.ts\";\n\n/** Matches the NewBlockPayload shape expected by the indexer's /new_block endpoint */\nexport interface ReplayBlockPayload {\n\tblock_hash: string;\n\tblock_height: number;\n\tindex_block_hash: string;\n\tparent_block_hash: string;\n\tparent_index_block_hash: string;\n\tburn_block_hash: string;\n\tburn_block_height: number;\n\tburn_block_timestamp: number;\n\tminer_txid: string;\n\ttimestamp: number;\n\ttransactions: ReplayTransactionPayload[];\n\tevents: ReplayEventPayload[];\n\t/** Node-shaped opt-in traces. Omitted when the height has none. */\n\tvm_events?: ReplayVmEventPayload[];\n\t/** Node-shaped storage-layer writes. Omitted when the height has none. */\n\tstate_writes?: ReplayStateWritePayload[];\n}\n\n/** Node-shaped `/new_block.state_writes[]` row; stored 1:1. */\nexport interface ReplayStateWritePayload {\n\ttx_index: number | null;\n\tordinal: number;\n\tkey: string;\n\tvalue_hex: string;\n}\n\ninterface ReplayTransactionPayload {\n\ttxid: string;\n\traw_tx: string;\n\tstatus: string;\n\ttx_index: number;\n\ttx_type?: string;\n\tsender_address?: string;\n\traw_result?: string | null;\n\tcontract_call?: { function_args: string[] };\n}\n\ninterface ReplayEventPayload {\n\ttxid: string;\n\tevent_index: number;\n\tcommitted: boolean;\n\ttype: string;\n\t[key: string]: unknown;\n}\n\n/** Node-shaped `/new_block.vm_events[]` row. Body lives under the node type\n * key so `parseVmEvent` can re-ingest it on fork restoration. Array order\n * is the index (sorted by stored `ordinal`). */\nexport interface ReplayVmEventPayload {\n\ttxid: string;\n\tcommitted: boolean;\n\ttype: VmNodeEventType;\n\t[key: string]: unknown;\n}\n\n/** Rebuild node-shaped vm traces from stored rows. Sorted so re-ingest\n * array order matches the stored second clock. */\nexport function reconstructVmEventsForReplay(\n\trows: ReadonlyArray<{\n\t\ttx_id: string;\n\t\tordinal: number | string;\n\t\ttype: string;\n\t\tdata: unknown;\n\t}>,\n): ReplayVmEventPayload[] {\n\tconst sorted = [...rows].sort(\n\t\t(a, b) => Number(a.ordinal) - Number(b.ordinal),\n\t);\n\tconst out: ReplayVmEventPayload[] = [];\n\tfor (const row of sorted) {\n\t\tconst nodeType =\n\t\t\trow.type in VM_STORED_TO_NODE_TYPE\n\t\t\t\t? VM_STORED_TO_NODE_TYPE[row.type as VmEventType]\n\t\t\t\t: undefined;\n\t\tif (!nodeType) continue;\n\t\tout.push({\n\t\t\ttxid: row.tx_id,\n\t\t\tcommitted: true,\n\t\t\ttype: nodeType,\n\t\t\t[nodeType]: row.data,\n\t\t});\n\t}\n\treturn out;\n}\n\nexport class LocalClient {\n\t/**\n\t * Reconstruct a NewBlockPayload from local DB for replay.\n\t * Returns null if block not found.\n\t */\n\tasync getBlockForReplay(\n\t\tdb: Kysely<Database>,\n\t\theight: number,\n\t): Promise<ReplayBlockPayload | null> {\n\t\tconst block = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.selectAll()\n\t\t\t.where(\"height\", \"=\", height)\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!block) return null;\n\n\t\tconst transactions = await db\n\t\t\t.selectFrom(\"transactions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"tx_index\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst events = await db\n\t\t\t.selectFrom(\"events\")\n\t\t\t.selectAll()\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"event_index\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst vmRows = await db\n\t\t\t.selectFrom(\"vm_events\")\n\t\t\t.select([\"tx_id\", \"ordinal\", \"type\", \"data\"])\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"ordinal\", \"asc\")\n\t\t\t.execute();\n\t\tconst vm_events = reconstructVmEventsForReplay(vmRows);\n\n\t\tconst state_writes: ReplayStateWritePayload[] = await db\n\t\t\t.selectFrom(\"state_writes\")\n\t\t\t.select([\"tx_index\", \"ordinal\", \"key\", \"value_hex\"])\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"ordinal\", \"asc\")\n\t\t\t.execute();\n\n\t\treturn {\n\t\t\tblock_hash: block.hash,\n\t\t\tblock_height: block.height,\n\t\t\t//
|
|
6
|
+
"/**\n * Local replay client — reconstructs NewBlockPayload from our own Postgres.\n *\n * Used for re-orgs, reprocessing, and self-serve replay after genesis sync.\n * Eliminates need for self-hosted Hiro API for blocks already in our DB.\n */\n\nimport type { Kysely } from \"kysely\";\nimport type { Database } from \"../db/types.ts\";\nimport {\n\tVM_STORED_TO_NODE_TYPE,\n\ttype VmEventType,\n\ttype VmNodeEventType,\n} from \"../event-types.ts\";\n\n/** Matches the NewBlockPayload shape expected by the indexer's /new_block endpoint */\nexport interface ReplayBlockPayload {\n\tblock_hash: string;\n\tblock_height: number;\n\tindex_block_hash: string;\n\tparent_block_hash: string;\n\tparent_index_block_hash: string;\n\tburn_block_hash: string;\n\tburn_block_height: number;\n\tburn_block_timestamp: number;\n\tminer_txid: string;\n\ttimestamp: number;\n\ttransactions: ReplayTransactionPayload[];\n\tevents: ReplayEventPayload[];\n\t/** Node-shaped opt-in traces. Omitted when the height has none. */\n\tvm_events?: ReplayVmEventPayload[];\n\t/** Node-shaped storage-layer writes. Omitted when the height has none. */\n\tstate_writes?: ReplayStateWritePayload[];\n}\n\n/** Node-shaped `/new_block.state_writes[]` row; stored 1:1. */\nexport interface ReplayStateWritePayload {\n\ttx_index: number | null;\n\tordinal: number;\n\tkey: string;\n\tvalue_hex: string;\n}\n\ninterface ReplayTransactionPayload {\n\ttxid: string;\n\traw_tx: string;\n\tstatus: string;\n\ttx_index: number;\n\ttx_type?: string;\n\tsender_address?: string;\n\traw_result?: string | null;\n\tcontract_call?: { function_args: string[] };\n}\n\ninterface ReplayEventPayload {\n\ttxid: string;\n\tevent_index: number;\n\tcommitted: boolean;\n\ttype: string;\n\t[key: string]: unknown;\n}\n\n/** Node-shaped `/new_block.vm_events[]` row. Body lives under the node type\n * key so `parseVmEvent` can re-ingest it on fork restoration. Array order\n * is the index (sorted by stored `ordinal`). */\nexport interface ReplayVmEventPayload {\n\ttxid: string;\n\tcommitted: boolean;\n\ttype: VmNodeEventType;\n\t[key: string]: unknown;\n}\n\n/** Rebuild node-shaped vm traces from stored rows. Sorted so re-ingest\n * array order matches the stored second clock. */\nexport function reconstructVmEventsForReplay(\n\trows: ReadonlyArray<{\n\t\ttx_id: string;\n\t\tordinal: number | string;\n\t\ttype: string;\n\t\tdata: unknown;\n\t}>,\n): ReplayVmEventPayload[] {\n\tconst sorted = [...rows].sort(\n\t\t(a, b) => Number(a.ordinal) - Number(b.ordinal),\n\t);\n\tconst out: ReplayVmEventPayload[] = [];\n\tfor (const row of sorted) {\n\t\tconst nodeType =\n\t\t\trow.type in VM_STORED_TO_NODE_TYPE\n\t\t\t\t? VM_STORED_TO_NODE_TYPE[row.type as VmEventType]\n\t\t\t\t: undefined;\n\t\tif (!nodeType) continue;\n\t\tout.push({\n\t\t\ttxid: row.tx_id,\n\t\t\tcommitted: true,\n\t\t\ttype: nodeType,\n\t\t\t[nodeType]: row.data,\n\t\t});\n\t}\n\treturn out;\n}\n\nexport class LocalClient {\n\t/**\n\t * Reconstruct a NewBlockPayload from local DB for replay.\n\t * Returns null if block not found.\n\t */\n\tasync getBlockForReplay(\n\t\tdb: Kysely<Database>,\n\t\theight: number,\n\t): Promise<ReplayBlockPayload | null> {\n\t\tconst block = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.selectAll()\n\t\t\t.where(\"height\", \"=\", height)\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!block) return null;\n\n\t\tconst transactions = await db\n\t\t\t.selectFrom(\"transactions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"tx_index\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst events = await db\n\t\t\t.selectFrom(\"events\")\n\t\t\t.selectAll()\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"event_index\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst vmRows = await db\n\t\t\t.selectFrom(\"vm_events\")\n\t\t\t.select([\"tx_id\", \"ordinal\", \"type\", \"data\"])\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"ordinal\", \"asc\")\n\t\t\t.execute();\n\t\tconst vm_events = reconstructVmEventsForReplay(vmRows);\n\n\t\tconst state_writes: ReplayStateWritePayload[] = await db\n\t\t\t.selectFrom(\"state_writes\")\n\t\t\t.select([\"tx_index\", \"ordinal\", \"key\", \"value_hex\"])\n\t\t\t.where(\"block_height\", \"=\", height)\n\t\t\t.orderBy(\"ordinal\", \"asc\")\n\t\t\t.execute();\n\n\t\t// height, burn_block_height and timestamp are bigint columns: postgres.js\n\t\t// returns them as strings, which the parser's numeric checks reject\n\t\t// (timestamp silently became 0 on every flip-back).\n\t\tconst timestamp = Number(block.timestamp);\n\t\treturn {\n\t\t\tblock_hash: block.hash,\n\t\t\tblock_height: Number(block.height),\n\t\t\t// Null on rows ingested before these were persisted; the parser\n\t\t\t// stores \"\" as NULL, so the restored row stays honest.\n\t\t\tindex_block_hash: block.index_block_hash ?? \"\",\n\t\t\tparent_block_hash: block.parent_hash,\n\t\t\t// Not stored in our DB — not needed by parser/deliveries\n\t\t\tparent_index_block_hash: \"\",\n\t\t\tburn_block_hash: block.burn_block_hash ?? \"\",\n\t\t\tburn_block_height: Number(block.burn_block_height),\n\t\t\tburn_block_timestamp: timestamp,\n\t\t\tminer_txid: \"\",\n\t\t\ttimestamp,\n\t\t\ttransactions: transactions.map((tx) => {\n\t\t\t\tconst entry: ReplayTransactionPayload = {\n\t\t\t\t\ttxid: tx.tx_id,\n\t\t\t\t\traw_tx: tx.raw_tx,\n\t\t\t\t\tstatus: tx.status,\n\t\t\t\t\ttx_index: tx.tx_index,\n\t\t\t\t\ttx_type: tx.type,\n\t\t\t\t\tsender_address: tx.sender,\n\t\t\t\t\traw_result: tx.raw_result ?? null,\n\t\t\t\t};\n\t\t\t\t// Include function_args if stored (for contract_call txs)\n\t\t\t\tif (tx.function_args) {\n\t\t\t\t\tconst args =\n\t\t\t\t\t\ttypeof tx.function_args === \"string\"\n\t\t\t\t\t\t\t? JSON.parse(tx.function_args)\n\t\t\t\t\t\t\t: tx.function_args;\n\t\t\t\t\tif (Array.isArray(args)) {\n\t\t\t\t\t\tentry.contract_call = { function_args: args };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn entry;\n\t\t\t}),\n\t\t\tevents: events.map((evt) => {\n\t\t\t\tconst data = (evt.data ?? {}) as Record<string, unknown>;\n\t\t\t\tconst eventType = evt.type;\n\n\t\t\t\t// Reconstruct the flat event structure the indexer expects:\n\t\t\t\t// { txid, event_index, committed, type, [type_key]: data }\n\t\t\t\tconst payload: ReplayEventPayload = {\n\t\t\t\t\ttxid: evt.tx_id,\n\t\t\t\t\tevent_index: evt.event_index,\n\t\t\t\t\tcommitted: true,\n\t\t\t\t\ttype: eventType,\n\t\t\t\t};\n\n\t\t\t\t// Attach event-specific data under the correct key\n\t\t\t\tif (eventType && data) {\n\t\t\t\t\tpayload[eventType] = data;\n\t\t\t\t}\n\n\t\t\t\treturn payload;\n\t\t\t}),\n\t\t\t...(vm_events.length > 0 ? { vm_events } : {}),\n\t\t\t...(state_writes.length > 0 ? { state_writes } : {}),\n\t\t};\n\t}\n\n\t/** Get highest block height in local DB */\n\tasync getChainTip(db: Kysely<Database>): Promise<number> {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.select((eb) => eb.fn.max(\"height\").as(\"max_height\"))\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(row?.max_height ?? 0);\n\t}\n\n\t/** Check if a specific block height exists in local DB */\n\tasync hasBlock(db: Kysely<Database>, height: number): Promise<boolean> {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"blocks\")\n\t\t\t.select(\"height\")\n\t\t\t.where(\"height\", \"=\", height)\n\t\t\t.where(\"canonical\", \"=\", true)\n\t\t\t.executeTakeFirst();\n\t\treturn !!row;\n\t}\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";AAKA;AAAA;AAAA;AAMA;AAAA,yBACC;AAAA;AAAA;AAAA;AAcM,IAAM,yBAAyC;AAAA,EACrD,sBAAsB;AAAA,EACtB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AACb;AAKO,IAAM,sBACZ;AAWM,IAAM,mBAAmB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;;;ACoBO,SAAS,4BAA4B,CAC3C,MAMyB;AAAA,EACzB,MAAM,SAAS,CAAC,GAAG,IAAI,EAAE,KACxB,CAAC,GAAG,MAAM,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,CAC/C;AAAA,EACA,MAAM,MAA8B,CAAC;AAAA,EACrC,WAAW,OAAO,QAAQ;AAAA,IACzB,MAAM,WACL,IAAI,QAAQ,yBACT,uBAAuB,IAAI,QAC3B;AAAA,IACJ,IAAI,CAAC;AAAA,MAAU;AAAA,IACf,IAAI,KAAK;AAAA,MACR,MAAM,IAAI;AAAA,MACV,WAAW;AAAA,MACX,MAAM;AAAA,OACL,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACF;AAAA,EACA,OAAO;AAAA;AAAA;AAGD,MAAM,YAAY;AAAA,OAKlB,kBAAiB,CACtB,IACA,QACqC;AAAA,IACrC,MAAM,QAAQ,MAAM,GAClB,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,UAAU,KAAK,MAAM,EAC3B,MAAM,aAAa,KAAK,IAAI,EAC5B,iBAAiB;AAAA,IAEnB,IAAI,CAAC;AAAA,MAAO,OAAO;AAAA,IAEnB,MAAM,eAAe,MAAM,GACzB,WAAW,cAAc,EACzB,UAAU,EACV,MAAM,gBAAgB,KAAK,MAAM,EACjC,QAAQ,YAAY,KAAK,EACzB,QAAQ;AAAA,IAEV,MAAM,SAAS,MAAM,GACnB,WAAW,QAAQ,EACnB,UAAU,EACV,MAAM,gBAAgB,KAAK,MAAM,EACjC,QAAQ,eAAe,KAAK,EAC5B,QAAQ;AAAA,IAEV,MAAM,SAAS,MAAM,GACnB,WAAW,WAAW,EACtB,OAAO,CAAC,SAAS,WAAW,QAAQ,MAAM,CAAC,EAC3C,MAAM,gBAAgB,KAAK,MAAM,EACjC,QAAQ,WAAW,KAAK,EACxB,QAAQ;AAAA,IACV,MAAM,YAAY,6BAA6B,MAAM;AAAA,IAErD,MAAM,eAA0C,MAAM,GACpD,WAAW,cAAc,EACzB,OAAO,CAAC,YAAY,WAAW,OAAO,WAAW,CAAC,EAClD,MAAM,gBAAgB,KAAK,MAAM,EACjC,QAAQ,WAAW,KAAK,EACxB,QAAQ;AAAA,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "85374ECE2AE4234264756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
"import {\n\ted25519KeyId,\n\tloadEd25519PrivateKey,\n\tloadEd25519PublicKey,\n\tpublicKeyPemFromPrivate,\n\tsignEd25519,\n\tverifyEd25519,\n} from \"./crypto/ed25519.ts\";\n\n/**\n * ed25519 signing for the Streams cold-bulk parquet manifest.\n *\n * The live Streams lane is ed25519-signed; the bulk manifest was plain JSON with\n * only per-file sha256, so a tampered manifest+file pair verified cleanly. This\n * signs the manifest itself with the same platform key, so a consumer can trust\n * the file hashes only after the manifest signature checks out — making the two\n * availability lanes symmetric.\n *\n * The signed bytes are the manifest's canonical JSON with the signature envelope\n * fields removed, so signer and verifier agree without a separate canonical\n * form: `signature`/`key_id` are appended last, so stripping them and\n * re-serializing reproduces the exact pre-sign bytes.\n */\ntype SignatureEnvelope = { signature?: string; key_id?: string };\n\n/** The exact bytes a manifest signature covers: the manifest JSON minus the\n * signature envelope fields. */\nexport function canonicalStreamsBulkManifestPayload(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n): string {\n\tconst { signature: _signature, key_id: _keyId, ...rest } = manifest;\n\treturn JSON.stringify(rest);\n}\n\nfunction normalizePem(pem: string): string {\n\t// Env transport often escapes newlines; restore real PEM line breaks.\n\treturn pem.includes(\"\\\\n\") ? pem.replace(/\\\\n/g, \"\\n\") : pem;\n}\n\n/**\n * Attach an ed25519 `signature` + `key_id` over the manifest's canonical bytes.\n * Returns a new manifest; re-signing one that already carries a signature signs\n * over its un-enveloped form (idempotent shape).\n */\nexport function signStreamsBulkManifest<\n\tT extends Record<string, unknown> & SignatureEnvelope,\n>(\n\tmanifest: T,\n\tprivateKeyPem: string,\n): T & { signature: string; key_id: string } {\n\tconst pem = normalizePem(privateKeyPem);\n\tconst privateKey = loadEd25519PrivateKey(pem);\n\tconst keyId = ed25519KeyId(publicKeyPemFromPrivate(pem));\n\tconst { signature: _signature, key_id: _keyId, ...base } = manifest;\n\tconst payload = JSON.stringify(base);\n\treturn {\n\t\t...(base as T),\n\t\tsignature: signEd25519(payload, privateKey),\n\t\tkey_id: keyId,\n\t};\n}\n\n/** Verify a manifest's ed25519 signature against the published public key. */\nexport function verifyStreamsBulkManifestSignature(\n\tmanifest: Record<string, unknown> & SignatureEnvelope,\n\tpublicKeyPem: string,\n): boolean {\n\tif (!manifest.signature) return false;\n\tconst payload = canonicalStreamsBulkManifestPayload(manifest);\n\treturn verifyEd25519(\n\t\tpayload,\n\t\tmanifest.signature,\n\t\tloadEd25519PublicKey(publicKeyPem),\n\t);\n}\n"
|
|
7
7
|
],
|
|
8
8
|
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMC;AAAA,YACA;AAAA;AAaM,SAAS,sBAAsB,GAGpC;AAAA,EACD,QAAQ,YAAY,cAAc,oBAAoB,SAAS;AAAA,EAC/D,OAAO;AAAA,IACN,eAAe,WACb,OAAO,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC,EACvC,SAAS;AAAA,IACX,cAAc,UAAU,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EAAE,SAAS;AAAA,EAC1E;AAAA;AAGM,SAAS,qBAAqB,CAAC,KAAwB;AAAA,EAC7D,OAAO,iBAAiB,GAAG;AAAA;AAGrB,SAAS,oBAAoB,CAAC,KAAwB;AAAA,EAC5D,OAAO,gBAAgB,GAAG;AAAA;AAGpB,SAAS,uBAAuB,CAAC,eAA+B;AAAA,EACtE,OAAO,gBAAgB,iBAAiB,aAAa,CAAC,EACpD,OAAO,EAAE,QAAQ,OAAO,MAAM,OAAO,CAAC,EACtC,SAAS;AAAA;AAIL,SAAS,YAAY,CAAC,cAA8B;AAAA,EAC1D,MAAM,MAAM,gBAAgB,YAAY,EAAE,OAAO;AAAA,IAChD,QAAQ;AAAA,IACR,MAAM;AAAA,EACP,CAAC;AAAA,EACD,OAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,EAAE,OAAO,WAAW,EAAE,MAAM,GAAG,EAAE;AAAA;AAGjE,SAAS,WAAW,CAAC,SAAiB,YAA+B;AAAA,EAC3E,OAAO,SAAS,MAAM,OAAO,KAAK,SAAS,MAAM,GAAG,UAAU,EAAE,SAC/D,QACD;AAAA;AAGM,SAAS,aAAa,CAC5B,SACA,iBACA,WACU;AAAA,EACV,IAAI;AAAA,IACH,OAAO,WACN,MACA,OAAO,KAAK,SAAS,MAAM,GAC3B,WACA,OAAO,KAAK,iBAAiB,QAAQ,CACtC;AAAA,IACC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;;;AChDF,SAAS,mCAAmC,CAClD,UACS;AAAA,EACT,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,OAAO,KAAK,UAAU,IAAI;AAAA;AAG3B,SAAS,YAAY,CAAC,KAAqB;AAAA,EAE1C,OAAO,IAAI,SAAS,KAAK,IAAI,IAAI,QAAQ,QAAQ;AAAA,CAAI,IAAI;AAAA;AAQnD,SAAS,uBAEf,CACA,UACA,eAC4C;AAAA,EAC5C,MAAM,MAAM,aAAa,aAAa;AAAA,EACtC,MAAM,aAAa,sBAAsB,GAAG;AAAA,EAC5C,MAAM,QAAQ,aAAa,wBAAwB,GAAG,CAAC;AAAA,EACvD,QAAQ,WAAW,YAAY,QAAQ,WAAW,SAAS;AAAA,EAC3D,MAAM,UAAU,KAAK,UAAU,IAAI;AAAA,EACnC,OAAO;AAAA,OACF;AAAA,IACJ,WAAW,YAAY,SAAS,UAAU;AAAA,IAC1C,QAAQ;AAAA,EACT;AAAA;AAIM,SAAS,kCAAkC,CACjD,UACA,cACU;AAAA,EACV,IAAI,CAAC,SAAS;AAAA,IAAW,OAAO;AAAA,EAChC,MAAM,UAAU,oCAAoC,QAAQ;AAAA,EAC5D,OAAO,cACN,SACA,SAAS,WACT,qBAAqB,YAAY,CAClC;AAAA;",
|
|
9
|
-
"debugId": "
|
|
9
|
+
"debugId": "095950E98D64233364756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secondlayer/shared",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.1",
|
|
4
4
|
"description": "Internal building blocks shared across Secondlayer packages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -237,10 +237,6 @@
|
|
|
237
237
|
"types": "./dist/src/node/client.d.ts",
|
|
238
238
|
"import": "./dist/src/node/client.js"
|
|
239
239
|
},
|
|
240
|
-
"./node/archive-client": {
|
|
241
|
-
"types": "./dist/src/node/archive-client.d.ts",
|
|
242
|
-
"import": "./dist/src/node/archive-client.js"
|
|
243
|
-
},
|
|
244
240
|
"./node/tx-summary": {
|
|
245
241
|
"types": "./dist/src/node/tx-summary.d.ts",
|
|
246
242
|
"import": "./dist/src/node/tx-summary.js"
|
|
@@ -277,7 +273,7 @@
|
|
|
277
273
|
"prepublishOnly": "bun run build"
|
|
278
274
|
},
|
|
279
275
|
"dependencies": {
|
|
280
|
-
"@secondlayer/stacks": "^6.
|
|
276
|
+
"@secondlayer/stacks": "^6.5.1",
|
|
281
277
|
"kysely": "0.28.17",
|
|
282
278
|
"kysely-postgres-js": "3.0.0",
|
|
283
279
|
"postgres": "^3.4.6",
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
interface ReplayResult {
|
|
2
|
-
replayed: number;
|
|
3
|
-
errors: number;
|
|
4
|
-
}
|
|
5
|
-
/** Options for replaying archive blocks, with a progress callback. */
|
|
6
|
-
interface ReplayOptions {
|
|
7
|
-
onProgress?: (count: number, height: number) => void;
|
|
8
|
-
}
|
|
9
|
-
declare class ArchiveReplayClient {
|
|
10
|
-
private archiveUrl;
|
|
11
|
-
private archiveDir;
|
|
12
|
-
constructor(opts?: {
|
|
13
|
-
archiveUrl?: string
|
|
14
|
-
archiveDir?: string
|
|
15
|
-
});
|
|
16
|
-
private get archivePath();
|
|
17
|
-
private get metaPath();
|
|
18
|
-
private get partialPath();
|
|
19
|
-
/** HEAD request to archive URL — verify reachable and has content */
|
|
20
|
-
isAvailable(): Promise<boolean>;
|
|
21
|
-
/**
|
|
22
|
-
* Download archive, stream-decompress, replay blocks matching gapHeights
|
|
23
|
-
* to the indexer's /new_block endpoint.
|
|
24
|
-
*/
|
|
25
|
-
replayGaps(gapHeights: Set<number>, indexerUrl: string, opts?: ReplayOptions): Promise<ReplayResult>;
|
|
26
|
-
/**
|
|
27
|
-
* Ensure a fresh-enough archive exists locally.
|
|
28
|
-
* Uses HTTP conditional requests to avoid redundant downloads.
|
|
29
|
-
*/
|
|
30
|
-
private ensureArchive;
|
|
31
|
-
/** Remove stale cache (> 24h) and orphaned partial files */
|
|
32
|
-
private cleanStaleFiles;
|
|
33
|
-
private readMeta;
|
|
34
|
-
private writeMeta;
|
|
35
|
-
private cleanupFile;
|
|
36
|
-
/** Download archive to disk with streaming. Returns Last-Modified header. */
|
|
37
|
-
private download;
|
|
38
|
-
}
|
|
39
|
-
export { ArchiveReplayClient, ReplayOptions, ReplayResult };
|
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
// src/node/archive-client.ts
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
renameSync,
|
|
6
|
-
unlinkSync,
|
|
7
|
-
writeFileSync
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
|
|
10
|
-
// src/env.ts
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
var networksSchema = z.string().transform((val) => {
|
|
13
|
-
const networks = val.split(",").map((n) => n.trim()).filter(Boolean);
|
|
14
|
-
const valid = ["mainnet", "testnet"];
|
|
15
|
-
for (const n of networks) {
|
|
16
|
-
if (!valid.includes(n)) {
|
|
17
|
-
throw new Error(`Invalid network: ${n}. Must be one of: ${valid.join(", ")}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return networks;
|
|
21
|
-
});
|
|
22
|
-
var envSchema = z.object({
|
|
23
|
-
DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
24
|
-
SOURCE_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
25
|
-
TARGET_DATABASE_URL: z.preprocess((val) => typeof val === "string" && val.length === 0 ? undefined : val, z.string().url().optional()),
|
|
26
|
-
NETWORK: z.enum(["mainnet", "testnet"]).optional(),
|
|
27
|
-
NETWORKS: networksSchema.optional(),
|
|
28
|
-
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
29
|
-
NODE_ENV: z.enum(["development", "production", "test"]).default("development")
|
|
30
|
-
});
|
|
31
|
-
var cachedEnv = null;
|
|
32
|
-
function getEnv() {
|
|
33
|
-
if (cachedEnv) {
|
|
34
|
-
return cachedEnv;
|
|
35
|
-
}
|
|
36
|
-
const result = envSchema.safeParse(process.env);
|
|
37
|
-
if (!result.success) {
|
|
38
|
-
console.error("❌ Invalid environment configuration:");
|
|
39
|
-
console.error(z.treeifyError(result.error));
|
|
40
|
-
throw new Error("Invalid environment configuration");
|
|
41
|
-
}
|
|
42
|
-
let enabledNetworks;
|
|
43
|
-
if (result.data.NETWORKS && result.data.NETWORKS.length > 0) {
|
|
44
|
-
enabledNetworks = result.data.NETWORKS;
|
|
45
|
-
} else if (result.data.NETWORK) {
|
|
46
|
-
enabledNetworks = [result.data.NETWORK];
|
|
47
|
-
} else {
|
|
48
|
-
enabledNetworks = ["mainnet"];
|
|
49
|
-
}
|
|
50
|
-
cachedEnv = { ...result.data, enabledNetworks };
|
|
51
|
-
return cachedEnv;
|
|
52
|
-
}
|
|
53
|
-
function isProductionEnv() {
|
|
54
|
-
return process.env["NODE_ENV"] === "production";
|
|
55
|
-
}
|
|
56
|
-
function isPox4DecoderEnabled(env = process.env) {
|
|
57
|
-
return env.POX4_DECODER_ENABLED !== "false";
|
|
58
|
-
}
|
|
59
|
-
function isPox5DecoderEnabled(env = process.env) {
|
|
60
|
-
return env.POX5_DECODER_ENABLED !== "false";
|
|
61
|
-
}
|
|
62
|
-
function isSbtcDecoderEnabled(env = process.env) {
|
|
63
|
-
return env.SBTC_DECODER_ENABLED !== "false";
|
|
64
|
-
}
|
|
65
|
-
function isBnsDecoderEnabled(env = process.env) {
|
|
66
|
-
return env.BNS_DECODER_ENABLED === "true";
|
|
67
|
-
}
|
|
68
|
-
// src/logger.ts
|
|
69
|
-
var LOG_LEVELS = {
|
|
70
|
-
debug: 0,
|
|
71
|
-
info: 1,
|
|
72
|
-
warn: 2,
|
|
73
|
-
error: 3
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
class Logger {
|
|
77
|
-
_level;
|
|
78
|
-
_isProduction;
|
|
79
|
-
_initialized = false;
|
|
80
|
-
init() {
|
|
81
|
-
if (this._initialized)
|
|
82
|
-
return;
|
|
83
|
-
this._initialized = true;
|
|
84
|
-
try {
|
|
85
|
-
const env = getEnv();
|
|
86
|
-
this._level = env.LOG_LEVEL;
|
|
87
|
-
this._isProduction = env.NODE_ENV === "production";
|
|
88
|
-
} catch {
|
|
89
|
-
this._level = "info";
|
|
90
|
-
this._isProduction = false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
get level() {
|
|
94
|
-
this.init();
|
|
95
|
-
return this._level;
|
|
96
|
-
}
|
|
97
|
-
get isProduction() {
|
|
98
|
-
this.init();
|
|
99
|
-
return this._isProduction;
|
|
100
|
-
}
|
|
101
|
-
shouldLog(level) {
|
|
102
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
|
|
103
|
-
}
|
|
104
|
-
formatMessage(level, message, meta) {
|
|
105
|
-
const timestamp = new Date().toISOString();
|
|
106
|
-
if (this.isProduction) {
|
|
107
|
-
return JSON.stringify({
|
|
108
|
-
timestamp,
|
|
109
|
-
level,
|
|
110
|
-
message,
|
|
111
|
-
...meta
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
115
|
-
return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;
|
|
116
|
-
}
|
|
117
|
-
debug(message, meta) {
|
|
118
|
-
if (this.shouldLog("debug")) {
|
|
119
|
-
console.debug(this.formatMessage("debug", message, meta));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
info(message, meta) {
|
|
123
|
-
if (this.shouldLog("info")) {
|
|
124
|
-
console.info(this.formatMessage("info", message, meta));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
warn(message, meta) {
|
|
128
|
-
if (this.shouldLog("warn")) {
|
|
129
|
-
console.warn(this.formatMessage("warn", message, meta));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
error(message, meta) {
|
|
133
|
-
if (this.shouldLog("error")) {
|
|
134
|
-
console.error(this.formatMessage("error", message, meta));
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
var logger = new Logger;
|
|
139
|
-
|
|
140
|
-
// src/node/archive-client.ts
|
|
141
|
-
var DEFAULT_ARCHIVE_URL = "https://archive.hiro.so/mainnet/stacks-blockchain-api/mainnet-stacks-blockchain-api-latest.zst";
|
|
142
|
-
var HEIGHT_REGEX = /"block_height":\s*(\d+)/;
|
|
143
|
-
var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
144
|
-
|
|
145
|
-
class ArchiveReplayClient {
|
|
146
|
-
archiveUrl;
|
|
147
|
-
archiveDir;
|
|
148
|
-
constructor(opts) {
|
|
149
|
-
this.archiveUrl = opts?.archiveUrl || process.env.ARCHIVE_URL || DEFAULT_ARCHIVE_URL;
|
|
150
|
-
this.archiveDir = opts?.archiveDir || process.env.ARCHIVE_DIR || "/tmp";
|
|
151
|
-
}
|
|
152
|
-
get archivePath() {
|
|
153
|
-
return `${this.archiveDir}/secondlayer-archive.zst`;
|
|
154
|
-
}
|
|
155
|
-
get metaPath() {
|
|
156
|
-
return `${this.archiveDir}/secondlayer-archive.meta.json`;
|
|
157
|
-
}
|
|
158
|
-
get partialPath() {
|
|
159
|
-
return `${this.archiveDir}/secondlayer-archive.zst.partial`;
|
|
160
|
-
}
|
|
161
|
-
async isAvailable() {
|
|
162
|
-
try {
|
|
163
|
-
const res = await fetch(this.archiveUrl, {
|
|
164
|
-
method: "HEAD",
|
|
165
|
-
signal: AbortSignal.timeout(15000)
|
|
166
|
-
});
|
|
167
|
-
const contentLength = Number(res.headers.get("content-length") || 0);
|
|
168
|
-
return res.ok && contentLength > 0;
|
|
169
|
-
} catch {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async replayGaps(gapHeights, indexerUrl, opts) {
|
|
174
|
-
if (gapHeights.size === 0)
|
|
175
|
-
return { replayed: 0, errors: 0 };
|
|
176
|
-
let maxHeight = 0;
|
|
177
|
-
for (const h of gapHeights)
|
|
178
|
-
if (h > maxHeight)
|
|
179
|
-
maxHeight = h;
|
|
180
|
-
let replayed = 0;
|
|
181
|
-
let errors = 0;
|
|
182
|
-
try {
|
|
183
|
-
await this.ensureArchive();
|
|
184
|
-
logger.info("Archive replay: starting decompression + replay", {
|
|
185
|
-
targetHeights: gapHeights.size,
|
|
186
|
-
maxHeight
|
|
187
|
-
});
|
|
188
|
-
const proc = Bun.spawn(["zstd", "-d", this.archivePath, "--stdout"], {
|
|
189
|
-
stdout: "pipe",
|
|
190
|
-
stderr: "ignore"
|
|
191
|
-
});
|
|
192
|
-
const reader = proc.stdout.getReader();
|
|
193
|
-
const decoder = new TextDecoder;
|
|
194
|
-
let buffer = "";
|
|
195
|
-
const remaining = new Set(gapHeights);
|
|
196
|
-
while (true) {
|
|
197
|
-
const { done, value } = await reader.read();
|
|
198
|
-
if (done)
|
|
199
|
-
break;
|
|
200
|
-
buffer += decoder.decode(value, { stream: true });
|
|
201
|
-
const lines = buffer.split(`
|
|
202
|
-
`);
|
|
203
|
-
buffer = lines.pop() || "";
|
|
204
|
-
for (const line of lines) {
|
|
205
|
-
if (remaining.size === 0)
|
|
206
|
-
break;
|
|
207
|
-
const match = HEIGHT_REGEX.exec(line);
|
|
208
|
-
if (!match)
|
|
209
|
-
continue;
|
|
210
|
-
const height = Number.parseInt(match[1]);
|
|
211
|
-
if (!remaining.has(height)) {
|
|
212
|
-
if (height > maxHeight)
|
|
213
|
-
break;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const tabIdx3 = nthIndex(line, "\t", 3);
|
|
217
|
-
if (tabIdx3 === -1)
|
|
218
|
-
continue;
|
|
219
|
-
const path = line.substring(nthIndex(line, "\t", 2) + 1, tabIdx3);
|
|
220
|
-
if (path !== "/new_block")
|
|
221
|
-
continue;
|
|
222
|
-
const payload = line.substring(tabIdx3 + 1);
|
|
223
|
-
try {
|
|
224
|
-
const res = await fetch(`${indexerUrl}/new_block`, {
|
|
225
|
-
method: "POST",
|
|
226
|
-
headers: {
|
|
227
|
-
"Content-Type": "application/json",
|
|
228
|
-
"X-Source": "archive-replay"
|
|
229
|
-
},
|
|
230
|
-
body: payload
|
|
231
|
-
});
|
|
232
|
-
if (res.ok) {
|
|
233
|
-
replayed++;
|
|
234
|
-
remaining.delete(height);
|
|
235
|
-
opts?.onProgress?.(replayed, height);
|
|
236
|
-
} else {
|
|
237
|
-
errors++;
|
|
238
|
-
logger.warn("Archive replay: indexer rejected block", {
|
|
239
|
-
height,
|
|
240
|
-
status: res.status
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
} catch (err) {
|
|
244
|
-
errors++;
|
|
245
|
-
logger.warn("Archive replay: POST failed", {
|
|
246
|
-
height,
|
|
247
|
-
error: String(err)
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (remaining.size === 0) {
|
|
252
|
-
proc.kill();
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
await proc.exited;
|
|
257
|
-
if (remaining.size > 0) {
|
|
258
|
-
const sample = [];
|
|
259
|
-
for (const h of remaining) {
|
|
260
|
-
sample.push(h);
|
|
261
|
-
if (sample.length === 5)
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
logger.warn("Archive replay: some heights not found in archive", {
|
|
265
|
-
missing: remaining.size,
|
|
266
|
-
sample
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
logger.info("Archive replay: complete", {
|
|
270
|
-
replayed,
|
|
271
|
-
errors,
|
|
272
|
-
missing: remaining.size
|
|
273
|
-
});
|
|
274
|
-
} catch (err) {
|
|
275
|
-
this.cleanupFile(this.archivePath);
|
|
276
|
-
this.cleanupFile(this.metaPath);
|
|
277
|
-
this.cleanupFile(this.partialPath);
|
|
278
|
-
throw err;
|
|
279
|
-
}
|
|
280
|
-
return { replayed, errors };
|
|
281
|
-
}
|
|
282
|
-
async ensureArchive() {
|
|
283
|
-
this.cleanStaleFiles();
|
|
284
|
-
const meta = this.readMeta();
|
|
285
|
-
const cached = existsSync(this.archivePath) && meta !== null;
|
|
286
|
-
if (cached) {
|
|
287
|
-
const age = Date.now() - new Date(meta.downloadedAt).getTime();
|
|
288
|
-
if (age < CACHE_MAX_AGE_MS) {
|
|
289
|
-
const headers = {};
|
|
290
|
-
if (meta.lastModified) {
|
|
291
|
-
headers["If-Modified-Since"] = meta.lastModified;
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
const res = await fetch(this.archiveUrl, {
|
|
295
|
-
method: "HEAD",
|
|
296
|
-
headers,
|
|
297
|
-
signal: AbortSignal.timeout(15000)
|
|
298
|
-
});
|
|
299
|
-
if (res.status === 304) {
|
|
300
|
-
logger.info("Archive replay: using cached archive", {
|
|
301
|
-
ageHrs: (age / 3600000).toFixed(1)
|
|
302
|
-
});
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
logger.info("Archive replay: remote archive is newer, re-downloading");
|
|
306
|
-
} catch {
|
|
307
|
-
logger.info("Archive replay: remote unreachable, using cached archive");
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
logger.info("Archive replay: cache expired, re-downloading");
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
logger.info("Archive replay: downloading archive", {
|
|
315
|
-
url: this.archiveUrl.split("/").pop()
|
|
316
|
-
});
|
|
317
|
-
const lastModified = await this.download(this.partialPath);
|
|
318
|
-
renameSync(this.partialPath, this.archivePath);
|
|
319
|
-
this.writeMeta({ lastModified, downloadedAt: new Date().toISOString() });
|
|
320
|
-
logger.info("Archive replay: download complete");
|
|
321
|
-
}
|
|
322
|
-
cleanStaleFiles() {
|
|
323
|
-
try {
|
|
324
|
-
if (existsSync(this.partialPath)) {
|
|
325
|
-
unlinkSync(this.partialPath);
|
|
326
|
-
}
|
|
327
|
-
const meta = this.readMeta();
|
|
328
|
-
if (meta) {
|
|
329
|
-
const age = Date.now() - new Date(meta.downloadedAt).getTime();
|
|
330
|
-
if (age > CACHE_MAX_AGE_MS) {
|
|
331
|
-
this.cleanupFile(this.archivePath);
|
|
332
|
-
this.cleanupFile(this.metaPath);
|
|
333
|
-
logger.info("Archive replay: cleaned stale cache");
|
|
334
|
-
}
|
|
335
|
-
} else if (existsSync(this.archivePath)) {
|
|
336
|
-
this.cleanupFile(this.archivePath);
|
|
337
|
-
}
|
|
338
|
-
} catch {}
|
|
339
|
-
}
|
|
340
|
-
readMeta() {
|
|
341
|
-
try {
|
|
342
|
-
if (!existsSync(this.metaPath))
|
|
343
|
-
return null;
|
|
344
|
-
return JSON.parse(readFileSync(this.metaPath, "utf-8"));
|
|
345
|
-
} catch {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
writeMeta(meta) {
|
|
350
|
-
try {
|
|
351
|
-
writeFileSync(this.metaPath, JSON.stringify(meta));
|
|
352
|
-
} catch {}
|
|
353
|
-
}
|
|
354
|
-
cleanupFile(path) {
|
|
355
|
-
try {
|
|
356
|
-
if (existsSync(path))
|
|
357
|
-
unlinkSync(path);
|
|
358
|
-
} catch {}
|
|
359
|
-
}
|
|
360
|
-
async download(destPath) {
|
|
361
|
-
const res = await fetch(this.archiveUrl, {
|
|
362
|
-
signal: AbortSignal.timeout(30 * 60 * 1000)
|
|
363
|
-
});
|
|
364
|
-
if (!res.ok || !res.body) {
|
|
365
|
-
throw new Error(`Archive download failed: HTTP ${res.status}`);
|
|
366
|
-
}
|
|
367
|
-
const lastModified = res.headers.get("last-modified");
|
|
368
|
-
const totalBytes = Number(res.headers.get("content-length") || 0);
|
|
369
|
-
const writer = Bun.file(destPath).writer();
|
|
370
|
-
let downloaded = 0;
|
|
371
|
-
let lastLog = 0;
|
|
372
|
-
for await (const chunk of res.body) {
|
|
373
|
-
writer.write(chunk);
|
|
374
|
-
downloaded += chunk.byteLength;
|
|
375
|
-
if (totalBytes > 0 && downloaded - lastLog > 5000000000) {
|
|
376
|
-
lastLog = downloaded;
|
|
377
|
-
const pct = (downloaded / totalBytes * 100).toFixed(0);
|
|
378
|
-
logger.info("Archive replay: downloading", { progress: `${pct}%` });
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
await writer.end();
|
|
382
|
-
return lastModified;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function nthIndex(str, char, n) {
|
|
386
|
-
let idx = -1;
|
|
387
|
-
for (let i = 0;i < n; i++) {
|
|
388
|
-
idx = str.indexOf(char, idx + 1);
|
|
389
|
-
if (idx === -1)
|
|
390
|
-
return -1;
|
|
391
|
-
}
|
|
392
|
-
return idx;
|
|
393
|
-
}
|
|
394
|
-
export {
|
|
395
|
-
ArchiveReplayClient
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
//# debugId=9E72CC1B9C56340B64756E2164756E21
|
|
399
|
-
//# sourceMappingURL=archive-client.js.map
|