@sm-lab/merkle 1.2.0 → 1.3.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.
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +12 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{pipelines-Dxpjkr0j.mjs → pipelines-D7R4SJ5k.mjs} +33 -2
- package/dist/pipelines-D7R4SJ5k.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/pipelines-Dxpjkr0j.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { h as readJsonFile, m as readAddressFile, n as makeRewards, r as makeStrikes, t as makeAddresses } from "./pipelines-D7R4SJ5k.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import "dotenv/config";
|
|
5
5
|
import { Argument, Command } from "commander";
|
package/dist/index.d.mts
CHANGED
|
@@ -118,6 +118,17 @@ declare function hasCustomIpfsEndpoint(opts?: IpfsClientOptions): boolean;
|
|
|
118
118
|
* Use the CLI's `--no-upload` / `MakeOptions.noUpload` to explicitly skip pinning.
|
|
119
119
|
*/
|
|
120
120
|
declare function shouldAttemptPin(opts?: IpfsClientOptions): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Assert a pin can succeed, throwing actionable guidance otherwise. Three outcomes:
|
|
123
|
+
* - `IPFS_API_URL` points at real Pinata with no credentials → throw (can't authenticate).
|
|
124
|
+
* - Pinata with credentials → return (assume reachable; a token can't be cheaply verified).
|
|
125
|
+
* - local / custom endpoint → probe reachability; a thrown fetch (connection refused / DNS /
|
|
126
|
+
* timeout) means down → throw. Any HTTP response (even a 404) counts as reachable.
|
|
127
|
+
*
|
|
128
|
+
* `skipHint` is the caller's escape hatch (e.g. `'pass --cid <cid>'`), woven into both messages so
|
|
129
|
+
* the recipe can tell the user how to bypass pinning entirely. Call this before `pinJsonToIpfs`.
|
|
130
|
+
*/
|
|
131
|
+
declare function assertPinnable(skipHint?: string, opts?: IpfsClientOptions): Promise<void>;
|
|
121
132
|
/**
|
|
122
133
|
* Pin a JSON object and return its CID. POSTs the Pinata `pinJSONToIPFS` envelope
|
|
123
134
|
* (`{ pinataContent, pinataMetadata }`) so both real Pinata and `@sm-lab/ipfs` accept it.
|
|
@@ -174,5 +185,5 @@ declare function makeRewards(leaves: [bigint, bigint][], opts?: MakeOptions & {
|
|
|
174
185
|
log?: unknown;
|
|
175
186
|
}): Promise<MakeRewardsResult>;
|
|
176
187
|
//#endregion
|
|
177
|
-
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, type IpfsClientOptions, LOCAL_IPFS_API_URL, type MakeOptions, type MakeResult, type MakeRewardsResult, type PinResponse, REWARDS_LEAF_ENCODING, STRIKES_LEAF_ENCODING, type StrikesEntry, type TreeConfig, type TreeDump, buildAddressesTree, buildRewardsTree, buildStrikesTree, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, shouldAttemptPin, writeJsonFile };
|
|
188
|
+
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, type IpfsClientOptions, LOCAL_IPFS_API_URL, type MakeOptions, type MakeResult, type MakeRewardsResult, type PinResponse, REWARDS_LEAF_ENCODING, STRIKES_LEAF_ENCODING, type StrikesEntry, type TreeConfig, type TreeDump, assertPinnable, buildAddressesTree, buildRewardsTree, buildStrikesTree, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, shouldAttemptPin, writeJsonFile };
|
|
178
189
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/tree.ts","../src/io.ts","../src/ipfs.ts","../src/pipelines.ts"],"mappings":";;;;;AAaA;;;;AAA2D;AAG3D;;;cAHa,uBAAA;AAGmE;AAAA,cAAnE,qBAAA;;cAGA,qBAAA;;UAGI,YAAA;EACf,cAAA;EACA,MAAA;EACA,OAAA;AAAA;;KAIU,QAAA,GAAW,UAAU,CAAC,kBAAA;;iBAGlB,kBAAA,CAAmB,SAAA,aAAsB,kBAAkB;AAPlE;AAAA,iBAeO,gBAAA,CACd,OAAA,EAAS,YAAA,KACR,kBAAkB;;;;AAb+B;AAGpD;;;;iBAyBgB,gBAAA,CAAiB,MAAA,uBAA6B,kBAAkB;;;;;AA5ChF;;;UCHiB,UAAA;EACf,QAAA;EACA,OAAO;AAAA;;iBAIO,YAAA,IAAgB,QAAA,WAAmB,CAAC;ADA4B;AAGhF;;;AAHgF,iBCWhE,cAAA,CAAe,OAAe;ADRsB;AAAA,iBCoBpD,eAAA,CAAgB,QAAgB;;iBAQhC,eAAA,CAAgB,QAAA,WAAmB,YAAY;;iBAK/C,aAAA,CAAc,QAAA,UAAkB,IAAa;;;;;;ADvC7D;;;;AAA2D;AAG3D;;;;AAAgF;AAGhF;AAAA,cEJa,oBAAA;;cAGA,kBAAA;AAAA,UAEI,iBAAA;EFEA;EEAf,MAAA;;EAEA,MAAA;EFDA;EEGA,SAAA;EFDA;EEGA,GAAA;AAAA;AFCF;AAAA,UEGiB,WAAA;EACf,QAAA;EACA,OAAA;EACA,SAAA;AAAA;;;;AFHyE;iBEU3D,iBAAA,CAAkB,MAAe;;iBASjC,kBAAA,IAAsB,iBAAiB;;iBAUvC,oBAAA,CAAqB,IAA8C,GAAxC,iBAAwC;;;;AFnB9D;AAerB;iBEagB,qBAAA,CAAsB,IAA8C,GAAxC,iBAAwC;;;AFbJ;;;;iBEwBhE,gBAAA,CAAiB,IAA8C,GAAxC,iBAAwC;;;;ADrEtE;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/tree.ts","../src/io.ts","../src/ipfs.ts","../src/pipelines.ts"],"mappings":";;;;;AAaA;;;;AAA2D;AAG3D;;;cAHa,uBAAA;AAGmE;AAAA,cAAnE,qBAAA;;cAGA,qBAAA;;UAGI,YAAA;EACf,cAAA;EACA,MAAA;EACA,OAAA;AAAA;;KAIU,QAAA,GAAW,UAAU,CAAC,kBAAA;;iBAGlB,kBAAA,CAAmB,SAAA,aAAsB,kBAAkB;AAPlE;AAAA,iBAeO,gBAAA,CACd,OAAA,EAAS,YAAA,KACR,kBAAkB;;;;AAb+B;AAGpD;;;;iBAyBgB,gBAAA,CAAiB,MAAA,uBAA6B,kBAAkB;;;;;AA5ChF;;;UCHiB,UAAA;EACf,QAAA;EACA,OAAO;AAAA;;iBAIO,YAAA,IAAgB,QAAA,WAAmB,CAAC;ADA4B;AAGhF;;;AAHgF,iBCWhE,cAAA,CAAe,OAAe;ADRsB;AAAA,iBCoBpD,eAAA,CAAgB,QAAgB;;iBAQhC,eAAA,CAAgB,QAAA,WAAmB,YAAY;;iBAK/C,aAAA,CAAc,QAAA,UAAkB,IAAa;;;;;;ADvC7D;;;;AAA2D;AAG3D;;;;AAAgF;AAGhF;AAAA,cEJa,oBAAA;;cAGA,kBAAA;AAAA,UAEI,iBAAA;EFEA;EEAf,MAAA;;EAEA,MAAA;EFDA;EEGA,SAAA;EFDA;EEGA,GAAA;AAAA;AFCF;AAAA,UEGiB,WAAA;EACf,QAAA;EACA,OAAA;EACA,SAAA;AAAA;;;;AFHyE;iBEU3D,iBAAA,CAAkB,MAAe;;iBASjC,kBAAA,IAAsB,iBAAiB;;iBAUvC,oBAAA,CAAqB,IAA8C,GAAxC,iBAAwC;;;;AFnB9D;AAerB;iBEagB,qBAAA,CAAsB,IAA8C,GAAxC,iBAAwC;;;AFbJ;;;;iBEwBhE,gBAAA,CAAiB,IAA8C,GAAxC,iBAAwC;;;;ADrEtE;AAIT;;;;;;iBCkFsB,cAAA,CACpB,QAAA,WACA,IAAA,GAAM,iBAAA,GACL,OAAO;;ADrF0C;AAWpD;;iBCkIsB,aAAA,CACpB,IAAA,WACA,YAAA,UACA,IAAA,GAAM,iBAAA,GACL,OAAO;;;;;AFpJV;;;UGDiB,UAAA;EACf,QAAA;EHGW;EGDX,OAAA;;EAEA,UAAA;AAAA;AAAA,UAGe,iBAAA,SAA0B,UAAU;EACnD,MAAA;;;AHFkE;AAGpE;;EGKE,QAAA,EAAU,QAAA;AAAA;AAAA,UAGK,WAAA;EHNf;EGQA,QAAA;EHPO;EGSP,UAAU;AAAA;;;;AHLwC;AAGpD;;iBGkCsB,aAAA,CACpB,SAAA,YACA,IAAA,GAAM,WAAA,GACL,OAAA,CAAQ,UAAA;;iBAOW,WAAA,CACpB,WAAA,UACA,IAAA,GAAM,WAAA,GACL,OAAA,CAAQ,UAAA;AHvCX;;;;;;;;AAEqB;AAFrB,iBGyDsB,WAAA,CACpB,MAAA,sBACA,IAAA,GAAM,WAAA;EAAgB,GAAA;AAAA,IACrB,OAAA,CAAQ,iBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, LOCAL_IPFS_API_URL, REWARDS_LEAF_ENCODING, STRIKES_LEAF_ENCODING, buildAddressesTree, buildRewardsTree, buildStrikesTree, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, shouldAttemptPin, writeJsonFile };
|
|
1
|
+
import { C as buildStrikesTree, S as buildRewardsTree, _ as writeJsonFile, a as LOCAL_IPFS_API_URL, b as STRIKES_LEAF_ENCODING, c as hasPinataCredentials, d as resolveIpfsApiUrl, f as shouldAttemptPin, g as readStrikesFile, h as readJsonFile, i as DEFAULT_IPFS_API_URL, l as ipfsOptionsFromEnv, m as readAddressFile, n as makeRewards, o as assertPinnable, p as parseAddresses, r as makeStrikes, s as hasCustomIpfsEndpoint, t as makeAddresses, u as pinJsonToIpfs, v as ADDRESSES_LEAF_ENCODING, x as buildAddressesTree, y as REWARDS_LEAF_ENCODING } from "./pipelines-D7R4SJ5k.mjs";
|
|
2
|
+
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, LOCAL_IPFS_API_URL, REWARDS_LEAF_ENCODING, STRIKES_LEAF_ENCODING, assertPinnable, buildAddressesTree, buildRewardsTree, buildStrikesTree, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, shouldAttemptPin, writeJsonFile };
|
|
@@ -135,6 +135,37 @@ function shouldAttemptPin(opts = ipfsOptionsFromEnv()) {
|
|
|
135
135
|
if ((opts.apiUrl ?? process.env.IPFS_API_URL ?? "").replace(/\/+$/, "") === "https://api.pinata.cloud" && !hasPinataCredentials(opts)) return false;
|
|
136
136
|
return true;
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Assert a pin can succeed, throwing actionable guidance otherwise. Three outcomes:
|
|
140
|
+
* - `IPFS_API_URL` points at real Pinata with no credentials → throw (can't authenticate).
|
|
141
|
+
* - Pinata with credentials → return (assume reachable; a token can't be cheaply verified).
|
|
142
|
+
* - local / custom endpoint → probe reachability; a thrown fetch (connection refused / DNS /
|
|
143
|
+
* timeout) means down → throw. Any HTTP response (even a 404) counts as reachable.
|
|
144
|
+
*
|
|
145
|
+
* `skipHint` is the caller's escape hatch (e.g. `'pass --cid <cid>'`), woven into both messages so
|
|
146
|
+
* the recipe can tell the user how to bypass pinning entirely. Call this before `pinJsonToIpfs`.
|
|
147
|
+
*/
|
|
148
|
+
async function assertPinnable(skipHint = "supply a precomputed CID", opts = ipfsOptionsFromEnv()) {
|
|
149
|
+
const target = resolveIpfsApiUrl(opts.apiUrl);
|
|
150
|
+
if (!shouldAttemptPin(opts)) throw new Error(`@sm-lab/merkle: IPFS_API_URL points at Pinata (${target}) but no credentials are set.\nDo one of:\n • set PINATA_JWT (or PINATA_API_KEY + PINATA_API_SECRET)\n • unset IPFS_API_URL to use the local mock: npx @sm-lab/ipfs serve\n • ${skipHint}`);
|
|
151
|
+
if (hasPinataCredentials(opts)) return;
|
|
152
|
+
if (await isReachable(target)) return;
|
|
153
|
+
throw new Error(`@sm-lab/merkle: cannot reach the IPFS pinning service at ${target}.\nDo one of:\n • start the local mock: npx @sm-lab/ipfs serve (or: pnpm stack:up)\n • use Pinata: set PINATA_JWT (or PINATA_API_KEY + PINATA_API_SECRET)\n • point elsewhere: set IPFS_API_URL=<url>\n • ${skipHint}`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* True when `url` answers any HTTP response within `timeoutMs`. A 404/405 still counts —
|
|
157
|
+
* reachability ≠ correctness; we only need proof something is listening. A thrown fetch
|
|
158
|
+
* (connection refused / DNS failure / timeout) is the "down" signal. Inlined here (single
|
|
159
|
+
* consumer) rather than promoted to @sm-lab/core — YAGNI until a second caller needs it.
|
|
160
|
+
*/
|
|
161
|
+
async function isReachable(url, timeoutMs = 2e3) {
|
|
162
|
+
try {
|
|
163
|
+
await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
138
169
|
function buildAuthHeaders(opts) {
|
|
139
170
|
if (opts.jwt) return { Authorization: `Bearer ${opts.jwt}` };
|
|
140
171
|
if (opts.apiKey && opts.apiSecret) return {
|
|
@@ -233,6 +264,6 @@ async function makeRewards(leaves, opts = {}) {
|
|
|
233
264
|
};
|
|
234
265
|
}
|
|
235
266
|
//#endregion
|
|
236
|
-
export { buildStrikesTree as S,
|
|
267
|
+
export { buildStrikesTree as C, buildRewardsTree as S, writeJsonFile as _, LOCAL_IPFS_API_URL as a, STRIKES_LEAF_ENCODING as b, hasPinataCredentials as c, resolveIpfsApiUrl as d, shouldAttemptPin as f, readStrikesFile as g, readJsonFile as h, DEFAULT_IPFS_API_URL as i, ipfsOptionsFromEnv as l, readAddressFile as m, makeRewards as n, assertPinnable as o, parseAddresses as p, makeStrikes as r, hasCustomIpfsEndpoint as s, makeAddresses as t, pinJsonToIpfs as u, ADDRESSES_LEAF_ENCODING as v, buildAddressesTree as x, REWARDS_LEAF_ENCODING as y };
|
|
237
268
|
|
|
238
|
-
//# sourceMappingURL=pipelines-
|
|
269
|
+
//# sourceMappingURL=pipelines-D7R4SJ5k.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipelines-D7R4SJ5k.mjs","names":[],"sources":["../src/tree.ts","../src/io.ts","../src/ipfs.ts","../src/pipelines.ts"],"sourcesContent":["import { StandardMerkleTree } from '@openzeppelin/merkle-tree';\n\n/**\n * Pure, deterministic tree construction. No I/O, no network — given the same inputs these\n * functions always produce the same root, which is exactly what the Vitest suite pins.\n *\n * Three OZ StandardMerkleTree shapes, one per CSM pipeline:\n * - addresses (vetted gate): [\"address\"] → VettedGate.setTreeParams\n * - strikes: [\"uint256\",\"string\",\"uint256[]\"] → CSStrikes.processOracleReport\n * - rewards: [\"uint256\",\"uint256\"] → FeeDistributor cumulative tree\n */\n\n/** Leaf encoding for the addresses (vetted gate) tree: one address per leaf. */\nexport const ADDRESSES_LEAF_ENCODING = ['address'] as const;\n\n/** Leaf encoding for the strikes tree: (nodeOperatorId, pubkey, strikes[]). */\nexport const STRIKES_LEAF_ENCODING = ['uint256', 'string', 'uint256[]'] as const;\n\n/** Leaf encoding for the rewards tree: (nodeOperatorId, cumulativeShares). */\nexport const REWARDS_LEAF_ENCODING = ['uint256', 'uint256'] as const;\n\n/** A single node-operator strikes record, as stored in strikes.json. */\nexport interface StrikesEntry {\n nodeOperatorId: number;\n pubkey: string;\n strikes: number[];\n}\n\n/** The OZ tree dump shape we pin to IPFS (kept structural to avoid leaking OZ internals). */\nexport type TreeDump = ReturnType<StandardMerkleTree<unknown[]>['dump']>;\n\n/** Build the addresses (vetted gate) Merkle tree from a list of whitelisted addresses. */\nexport function buildAddressesTree(addresses: string[]): StandardMerkleTree<[string]> {\n return StandardMerkleTree.of(\n addresses.map((address) => [address] as [string]),\n [...ADDRESSES_LEAF_ENCODING],\n );\n}\n\n/** Build the strikes Merkle tree from node-operator strike records. */\nexport function buildStrikesTree(\n entries: StrikesEntry[],\n): StandardMerkleTree<[number, string, number[]]> {\n return StandardMerkleTree.of(\n entries.map(({ nodeOperatorId, pubkey, strikes }) => [nodeOperatorId, pubkey, strikes]),\n [...STRIKES_LEAF_ENCODING],\n );\n}\n\n/**\n * Build the cumulative rewards tree: one [nodeOperatorId, cumulativeShares] leaf per operator.\n *\n * Leaf values are `bigint` (not `number` like buildStrikesTree) because cumulative reward shares\n * are wei amounts that routinely overflow `Number.MAX_SAFE_INTEGER`. OZ serializes them as decimal\n * strings in `dump()` (JSON-safe). Callers shape the leaves — e.g. the FeeDistributor non-empty-proof\n * pad leaf is appended by the caller, not here (same division of labor as the addresses/strikes builders).\n */\nexport function buildRewardsTree(leaves: [bigint, bigint][]): StandardMerkleTree<[bigint, bigint]> {\n return StandardMerkleTree.of(leaves, [...REWARDS_LEAF_ENCODING]);\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { StrikesEntry } from './tree';\n\n/**\n * Pure file I/O helpers. Kept small and side-effect-explicit so the deterministic parsing\n * (address-file, strikes.json) can be unit-tested against fixtures.\n */\n\n/** The `{ treeRoot, treeCid }` shape written by the optional `-o` handoff file. */\nexport interface TreeConfig {\n treeRoot: string;\n treeCid: string;\n}\n\n/** Read+parse a JSON file as `T`. Throws (with the path) if missing. */\nexport function readJsonFile<T>(filePath: string): T {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found at ${filePath}`);\n }\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T;\n}\n\n/**\n * Parse an address list. Accepts either a JSON array (`[\"0x..\", ...]`) or a newline-delimited\n * text file (blank lines and `#` comments ignored) — the original tool supported both shapes.\n */\nexport function parseAddresses(content: string): string[] {\n const trimmed = content.trim();\n if (trimmed.startsWith('[')) {\n return JSON.parse(trimmed) as string[];\n }\n return trimmed\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#'));\n}\n\n/** Read and parse an address file (JSON array or newline-delimited text). */\nexport function readAddressFile(filePath: string): string[] {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found at ${filePath}`);\n }\n return parseAddresses(fs.readFileSync(filePath, 'utf-8'));\n}\n\n/** Read and parse a strikes.json file. */\nexport function readStrikesFile(filePath: string): StrikesEntry[] {\n return readJsonFile<StrikesEntry[]>(filePath);\n}\n\n/** Write a JSON file, creating parent directories as needed. */\nexport function writeJsonFile(filePath: string, data: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2));\n}\n","/**\n * IPFS pinning client — Pinata-compatible, with an env-switchable endpoint.\n *\n * Why not `@pinata/sdk` directly? The installed v2 SDK hardcodes\n * `baseUrl = 'https://api.pinata.cloud'` (see its `src/constants.ts`); its `PinataConfig`\n * exposes only API keys / JWT, no host override. To target `@sm-lab/ipfs` locally we\n * need a configurable base URL, so this is a thin `fetch` client hitting the exact same\n * `/pinning/pinJSONToIPFS` route the mock implements. Point it at real Pinata in test-infra\n * by supplying PINATA_* credentials (no need to set IPFS_API_URL when using Pinata).\n *\n * Default resolution (local-first):\n * explicit apiUrl → IPFS_API_URL env → Pinata (if PINATA_* set) → local @sm-lab/ipfs\n */\n\n/** Real Pinata host. Used when PINATA_* credentials are set but IPFS_API_URL is unset. */\nexport const DEFAULT_IPFS_API_URL = 'https://api.pinata.cloud';\n\n/** Local @sm-lab/ipfs default — the fallback when no IPFS_API_URL or Pinata creds are set. */\nexport const LOCAL_IPFS_API_URL = 'http://127.0.0.1:5001';\n\nexport interface IpfsClientOptions {\n /** Base URL of the pinning service. Defaults to `IPFS_API_URL` env, then real Pinata. */\n apiUrl?: string;\n /** Pinata API key (header `pinata_api_key`). Mocks may ignore it. */\n apiKey?: string;\n /** Pinata API secret (header `pinata_secret_api_key`). Mocks may ignore it. */\n apiSecret?: string;\n /** Pinata JWT (header `Authorization: Bearer …`). Alternative to key/secret. */\n jwt?: string;\n}\n\n/** Shape of a Pinata `pinJSONToIPFS` success response. */\nexport interface PinResponse {\n IpfsHash: string;\n PinSize: number;\n Timestamp: string;\n}\n\n/**\n * Resolve the pinning base URL: explicit apiUrl → IPFS_API_URL env → Pinata (if PINATA_* set)\n * → local @sm-lab/ipfs (http://127.0.0.1:5001).\n */\nexport function resolveIpfsApiUrl(apiUrl?: string): string {\n // Treat empty string (e.g. `IPFS_API_URL=`) as unset so it falls through to the defaults.\n const explicit = apiUrl || process.env.IPFS_API_URL;\n if (explicit) return explicit.replace(/\\/+$/, '');\n if (hasPinataCredentials()) return DEFAULT_IPFS_API_URL;\n return LOCAL_IPFS_API_URL;\n}\n\n/** Read pinning config from the environment (credentials + endpoint switch). */\nexport function ipfsOptionsFromEnv(): IpfsClientOptions {\n return {\n apiUrl: process.env.IPFS_API_URL,\n apiKey: process.env.PINATA_API_KEY,\n apiSecret: process.env.PINATA_API_SECRET,\n jwt: process.env.PINATA_JWT,\n };\n}\n\n/** True when enough credentials are present to talk to real Pinata. */\nexport function hasPinataCredentials(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n return Boolean(opts.jwt || (opts.apiKey && opts.apiSecret));\n}\n\n/**\n * True when a non-default pinning endpoint is configured — i.e. `IPFS_API_URL` points\n * somewhere other than real Pinata (typically a local `@sm-lab/ipfs`). Such endpoints\n * accept unauthenticated pins, so credentials are not required to upload to them.\n */\nexport function hasCustomIpfsEndpoint(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n const raw = (opts.apiUrl ?? '').replace(/\\/+$/, '');\n return raw.length > 0 && raw !== DEFAULT_IPFS_API_URL;\n}\n\n/**\n * Whether `make`/`tree` should attempt to pin. With the local-first default there is always a\n * usable target, so this returns `true` unless an explicit `IPFS_API_URL` points directly at\n * real Pinata without credentials (that edge case cannot pin, so we still return `false` there).\n * Use the CLI's `--no-upload` / `MakeOptions.noUpload` to explicitly skip pinning.\n */\nexport function shouldAttemptPin(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n const raw = (opts.apiUrl ?? process.env.IPFS_API_URL ?? '').replace(/\\/+$/, '');\n if (raw === DEFAULT_IPFS_API_URL && !hasPinataCredentials(opts)) return false;\n // All other cases: custom endpoint (including no env, which falls through to LOCAL) or Pinata creds.\n return true;\n}\n\n/**\n * Assert a pin can succeed, throwing actionable guidance otherwise. Three outcomes:\n * - `IPFS_API_URL` points at real Pinata with no credentials → throw (can't authenticate).\n * - Pinata with credentials → return (assume reachable; a token can't be cheaply verified).\n * - local / custom endpoint → probe reachability; a thrown fetch (connection refused / DNS /\n * timeout) means down → throw. Any HTTP response (even a 404) counts as reachable.\n *\n * `skipHint` is the caller's escape hatch (e.g. `'pass --cid <cid>'`), woven into both messages so\n * the recipe can tell the user how to bypass pinning entirely. Call this before `pinJsonToIpfs`.\n */\nexport async function assertPinnable(\n skipHint = 'supply a precomputed CID',\n opts: IpfsClientOptions = ipfsOptionsFromEnv(),\n): Promise<void> {\n const target = resolveIpfsApiUrl(opts.apiUrl);\n if (!shouldAttemptPin(opts)) {\n // shouldAttemptPin rejects exactly one case: IPFS_API_URL == real Pinata, no credentials.\n throw new Error(\n `@sm-lab/merkle: IPFS_API_URL points at Pinata (${target}) but no credentials are set.\\n` +\n `Do one of:\\n` +\n ` • set PINATA_JWT (or PINATA_API_KEY + PINATA_API_SECRET)\\n` +\n ` • unset IPFS_API_URL to use the local mock: npx @sm-lab/ipfs serve\\n` +\n ` • ${skipHint}`,\n );\n }\n if (hasPinataCredentials(opts)) return; // Pinata with creds — assume reachable.\n if (await isReachable(target)) return;\n throw new Error(\n `@sm-lab/merkle: cannot reach the IPFS pinning service at ${target}.\\n` +\n `Do one of:\\n` +\n ` • start the local mock: npx @sm-lab/ipfs serve (or: pnpm stack:up)\\n` +\n ` • use Pinata: set PINATA_JWT (or PINATA_API_KEY + PINATA_API_SECRET)\\n` +\n ` • point elsewhere: set IPFS_API_URL=<url>\\n` +\n ` • ${skipHint}`,\n );\n}\n\n/**\n * True when `url` answers any HTTP response within `timeoutMs`. A 404/405 still counts —\n * reachability ≠ correctness; we only need proof something is listening. A thrown fetch\n * (connection refused / DNS failure / timeout) is the \"down\" signal. Inlined here (single\n * consumer) rather than promoted to @sm-lab/core — YAGNI until a second caller needs it.\n */\nasync function isReachable(url: string, timeoutMs = 2000): Promise<boolean> {\n try {\n await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });\n return true;\n } catch {\n return false;\n }\n}\n\nfunction buildAuthHeaders(opts: IpfsClientOptions): Record<string, string> {\n if (opts.jwt) {\n return { Authorization: `Bearer ${opts.jwt}` };\n }\n if (opts.apiKey && opts.apiSecret) {\n return {\n pinata_api_key: opts.apiKey,\n pinata_secret_api_key: opts.apiSecret,\n };\n }\n return {};\n}\n\n/**\n * Pin a JSON object and return its CID. POSTs the Pinata `pinJSONToIPFS` envelope\n * (`{ pinataContent, pinataMetadata }`) so both real Pinata and `@sm-lab/ipfs` accept it.\n */\nexport async function pinJsonToIpfs(\n data: unknown,\n metadataName: string,\n opts: IpfsClientOptions = ipfsOptionsFromEnv(),\n): Promise<string> {\n const url = `${resolveIpfsApiUrl(opts.apiUrl)}/pinning/pinJSONToIPFS`;\n const body = JSON.stringify({\n pinataContent: data,\n pinataMetadata: { name: metadataName },\n });\n\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...buildAuthHeaders(opts) },\n body,\n });\n\n if (!res.ok) {\n const detail = await res.text().catch(() => '');\n throw new Error(\n `pinJSONToIPFS failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,\n );\n }\n\n const json = (await res.json()) as Partial<PinResponse>;\n if (!json.IpfsHash) {\n throw new Error(`pinJSONToIPFS: response missing IpfsHash (${JSON.stringify(json)})`);\n }\n return json.IpfsHash;\n}\n","import { buildAddressesTree, buildRewardsTree, buildStrikesTree } from './tree';\nimport { readStrikesFile, writeJsonFile } from './io';\nimport { pinJsonToIpfs, shouldAttemptPin } from './ipfs';\nimport type { TreeConfig } from './io';\nimport type { TreeDump } from './tree';\n\n/**\n * merkle's single job: build a Merkle tree from input, pin it to IPFS, and return the\n * root + CID. Pushing those on-chain (and resolving deploy addresses) is out of scope —\n * that belongs to `@sm-lab/receipts`. No `cast`, no `DEPLOY_JSON_PATH` here.\n */\n\nexport interface MakeResult {\n treeRoot: string;\n /** undefined when IPFS upload was skipped (--no-upload, or nothing configured). */\n treeCid?: string;\n /** set only when `configPath` was provided and a `{ treeRoot, treeCid }` file was written. */\n configPath?: string;\n}\n\nexport interface MakeRewardsResult extends MakeResult {\n logCid?: string;\n /**\n * JSON-safe tree dump (bigint leaf values serialized as decimal strings).\n * Identical shape to OZ `StandardMerkleTree.dump()` but with string values, so\n * `JSON.stringify(treeDump)` is safe without a custom replacer.\n */\n treeDump: TreeDump;\n}\n\nexport interface MakeOptions {\n /** Skip pinning to IPFS (build + return root only). */\n noUpload?: boolean;\n /** When set, also write `{ treeRoot, treeCid }` JSON here (a handoff seam for receipts/CI). */\n configPath?: string;\n}\n\nasync function maybePin(\n data: unknown,\n name: string,\n noUpload?: boolean,\n): Promise<string | undefined> {\n if (noUpload) return undefined;\n if (!shouldAttemptPin()) {\n console.warn(\n 'Warning: IPFS upload skipped — set IPFS_API_URL pointing at real Pinata and supply PINATA_API_KEY/SECRET or PINATA_JWT. Unset IPFS_API_URL pins to local @sm-lab/ipfs (http://127.0.0.1:5001).',\n );\n return undefined;\n }\n return pinJsonToIpfs(data, name);\n}\n\nfunction finish(treeRoot: string, treeCid: string | undefined, configPath?: string): MakeResult {\n if (configPath) {\n const config: TreeConfig = { treeRoot, treeCid: treeCid ?? '' };\n writeJsonFile(configPath, config);\n }\n return { treeRoot, treeCid, configPath };\n}\n\n/**\n * Addresses (vetted gate): build the address tree from a resolved address list, pin it, return\n * `{ treeRoot, treeCid }`. The CLI resolves file/inline/flag inputs to `string[]` before calling\n * this — keeping this function pure so it can be called directly from TS consumers without\n * touching the filesystem.\n */\nexport async function makeAddresses(\n addresses: string[],\n opts: MakeOptions = {},\n): Promise<MakeResult> {\n const tree = buildAddressesTree(addresses);\n const treeCid = await maybePin(tree.dump(), 'merkle-tree-addresses', opts.noUpload);\n return finish(tree.root, treeCid, opts.configPath);\n}\n\n/** Strikes: build the strikes tree, pin it, return `{ treeRoot, treeCid }`. */\nexport async function makeStrikes(\n strikesPath: string,\n opts: MakeOptions = {},\n): Promise<MakeResult> {\n const tree = buildStrikesTree(readStrikesFile(strikesPath));\n const treeCid = await maybePin(tree.dump(), 'merkle-tree-strikes', opts.noUpload);\n return finish(tree.root, treeCid, opts.configPath);\n}\n\nconst bigintReplacer = (_k: string, v: unknown): unknown =>\n typeof v === 'bigint' ? v.toString() : v;\n\n/**\n * Rewards: build the cumulative rewards tree from `[nodeOperatorId, cumulativeShares]` leaves\n * (bigint values), optionally pin the tree dump and a log object, and return\n * `{ treeRoot, treeCid, logCid?, treeDump }`.\n *\n * Bigints in the tree dump and `opts.log` are serialized to decimal strings before pinning\n * (OZ `dump()` returns leaf values verbatim, which are bigints here). The returned `treeDump`\n * uses the same serialization so it is JSON-safe (string values, no BigInt).\n */\nexport async function makeRewards(\n leaves: [bigint, bigint][],\n opts: MakeOptions & { log?: unknown } = {},\n): Promise<MakeRewardsResult> {\n const tree = buildRewardsTree(leaves);\n // OZ dump() returns leaf values as bigints; JSON.stringify(dump) would throw without this.\n const treeDump: TreeDump = JSON.parse(JSON.stringify(tree.dump(), bigintReplacer));\n const treeCid = await maybePin(treeDump, 'merkle-tree-rewards', opts.noUpload);\n const logCid =\n opts.log !== undefined && !opts.noUpload\n ? await maybePin(JSON.parse(JSON.stringify(opts.log, bigintReplacer)), 'merkle-rewards-log')\n : undefined;\n const base = finish(tree.root, treeCid, opts.configPath);\n return logCid !== undefined ? { ...base, logCid, treeDump } : { ...base, treeDump };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sm-lab/merkle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Merkle tree builder for Lido SM (Staking Modules) — addresses (vetted gate), strikes, and rewards pipelines",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typescript": "^6.0.3",
|
|
42
42
|
"vitest": "^4.1.9",
|
|
43
43
|
"@types/node": "^26.1.0",
|
|
44
|
-
"@sm-lab/
|
|
45
|
-
"@sm-lab/
|
|
44
|
+
"@sm-lab/core": "0.0.0",
|
|
45
|
+
"@sm-lab/config": "0.0.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=24"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pipelines-Dxpjkr0j.mjs","names":[],"sources":["../src/tree.ts","../src/io.ts","../src/ipfs.ts","../src/pipelines.ts"],"sourcesContent":["import { StandardMerkleTree } from '@openzeppelin/merkle-tree';\n\n/**\n * Pure, deterministic tree construction. No I/O, no network — given the same inputs these\n * functions always produce the same root, which is exactly what the Vitest suite pins.\n *\n * Three OZ StandardMerkleTree shapes, one per CSM pipeline:\n * - addresses (vetted gate): [\"address\"] → VettedGate.setTreeParams\n * - strikes: [\"uint256\",\"string\",\"uint256[]\"] → CSStrikes.processOracleReport\n * - rewards: [\"uint256\",\"uint256\"] → FeeDistributor cumulative tree\n */\n\n/** Leaf encoding for the addresses (vetted gate) tree: one address per leaf. */\nexport const ADDRESSES_LEAF_ENCODING = ['address'] as const;\n\n/** Leaf encoding for the strikes tree: (nodeOperatorId, pubkey, strikes[]). */\nexport const STRIKES_LEAF_ENCODING = ['uint256', 'string', 'uint256[]'] as const;\n\n/** Leaf encoding for the rewards tree: (nodeOperatorId, cumulativeShares). */\nexport const REWARDS_LEAF_ENCODING = ['uint256', 'uint256'] as const;\n\n/** A single node-operator strikes record, as stored in strikes.json. */\nexport interface StrikesEntry {\n nodeOperatorId: number;\n pubkey: string;\n strikes: number[];\n}\n\n/** The OZ tree dump shape we pin to IPFS (kept structural to avoid leaking OZ internals). */\nexport type TreeDump = ReturnType<StandardMerkleTree<unknown[]>['dump']>;\n\n/** Build the addresses (vetted gate) Merkle tree from a list of whitelisted addresses. */\nexport function buildAddressesTree(addresses: string[]): StandardMerkleTree<[string]> {\n return StandardMerkleTree.of(\n addresses.map((address) => [address] as [string]),\n [...ADDRESSES_LEAF_ENCODING],\n );\n}\n\n/** Build the strikes Merkle tree from node-operator strike records. */\nexport function buildStrikesTree(\n entries: StrikesEntry[],\n): StandardMerkleTree<[number, string, number[]]> {\n return StandardMerkleTree.of(\n entries.map(({ nodeOperatorId, pubkey, strikes }) => [nodeOperatorId, pubkey, strikes]),\n [...STRIKES_LEAF_ENCODING],\n );\n}\n\n/**\n * Build the cumulative rewards tree: one [nodeOperatorId, cumulativeShares] leaf per operator.\n *\n * Leaf values are `bigint` (not `number` like buildStrikesTree) because cumulative reward shares\n * are wei amounts that routinely overflow `Number.MAX_SAFE_INTEGER`. OZ serializes them as decimal\n * strings in `dump()` (JSON-safe). Callers shape the leaves — e.g. the FeeDistributor non-empty-proof\n * pad leaf is appended by the caller, not here (same division of labor as the addresses/strikes builders).\n */\nexport function buildRewardsTree(leaves: [bigint, bigint][]): StandardMerkleTree<[bigint, bigint]> {\n return StandardMerkleTree.of(leaves, [...REWARDS_LEAF_ENCODING]);\n}\n","import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { StrikesEntry } from './tree';\n\n/**\n * Pure file I/O helpers. Kept small and side-effect-explicit so the deterministic parsing\n * (address-file, strikes.json) can be unit-tested against fixtures.\n */\n\n/** The `{ treeRoot, treeCid }` shape written by the optional `-o` handoff file. */\nexport interface TreeConfig {\n treeRoot: string;\n treeCid: string;\n}\n\n/** Read+parse a JSON file as `T`. Throws (with the path) if missing. */\nexport function readJsonFile<T>(filePath: string): T {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found at ${filePath}`);\n }\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T;\n}\n\n/**\n * Parse an address list. Accepts either a JSON array (`[\"0x..\", ...]`) or a newline-delimited\n * text file (blank lines and `#` comments ignored) — the original tool supported both shapes.\n */\nexport function parseAddresses(content: string): string[] {\n const trimmed = content.trim();\n if (trimmed.startsWith('[')) {\n return JSON.parse(trimmed) as string[];\n }\n return trimmed\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#'));\n}\n\n/** Read and parse an address file (JSON array or newline-delimited text). */\nexport function readAddressFile(filePath: string): string[] {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found at ${filePath}`);\n }\n return parseAddresses(fs.readFileSync(filePath, 'utf-8'));\n}\n\n/** Read and parse a strikes.json file. */\nexport function readStrikesFile(filePath: string): StrikesEntry[] {\n return readJsonFile<StrikesEntry[]>(filePath);\n}\n\n/** Write a JSON file, creating parent directories as needed. */\nexport function writeJsonFile(filePath: string, data: unknown): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2));\n}\n","/**\n * IPFS pinning client — Pinata-compatible, with an env-switchable endpoint.\n *\n * Why not `@pinata/sdk` directly? The installed v2 SDK hardcodes\n * `baseUrl = 'https://api.pinata.cloud'` (see its `src/constants.ts`); its `PinataConfig`\n * exposes only API keys / JWT, no host override. To target `@sm-lab/ipfs` locally we\n * need a configurable base URL, so this is a thin `fetch` client hitting the exact same\n * `/pinning/pinJSONToIPFS` route the mock implements. Point it at real Pinata in test-infra\n * by supplying PINATA_* credentials (no need to set IPFS_API_URL when using Pinata).\n *\n * Default resolution (local-first):\n * explicit apiUrl → IPFS_API_URL env → Pinata (if PINATA_* set) → local @sm-lab/ipfs\n */\n\n/** Real Pinata host. Used when PINATA_* credentials are set but IPFS_API_URL is unset. */\nexport const DEFAULT_IPFS_API_URL = 'https://api.pinata.cloud';\n\n/** Local @sm-lab/ipfs default — the fallback when no IPFS_API_URL or Pinata creds are set. */\nexport const LOCAL_IPFS_API_URL = 'http://127.0.0.1:5001';\n\nexport interface IpfsClientOptions {\n /** Base URL of the pinning service. Defaults to `IPFS_API_URL` env, then real Pinata. */\n apiUrl?: string;\n /** Pinata API key (header `pinata_api_key`). Mocks may ignore it. */\n apiKey?: string;\n /** Pinata API secret (header `pinata_secret_api_key`). Mocks may ignore it. */\n apiSecret?: string;\n /** Pinata JWT (header `Authorization: Bearer …`). Alternative to key/secret. */\n jwt?: string;\n}\n\n/** Shape of a Pinata `pinJSONToIPFS` success response. */\nexport interface PinResponse {\n IpfsHash: string;\n PinSize: number;\n Timestamp: string;\n}\n\n/**\n * Resolve the pinning base URL: explicit apiUrl → IPFS_API_URL env → Pinata (if PINATA_* set)\n * → local @sm-lab/ipfs (http://127.0.0.1:5001).\n */\nexport function resolveIpfsApiUrl(apiUrl?: string): string {\n // Treat empty string (e.g. `IPFS_API_URL=`) as unset so it falls through to the defaults.\n const explicit = apiUrl || process.env.IPFS_API_URL;\n if (explicit) return explicit.replace(/\\/+$/, '');\n if (hasPinataCredentials()) return DEFAULT_IPFS_API_URL;\n return LOCAL_IPFS_API_URL;\n}\n\n/** Read pinning config from the environment (credentials + endpoint switch). */\nexport function ipfsOptionsFromEnv(): IpfsClientOptions {\n return {\n apiUrl: process.env.IPFS_API_URL,\n apiKey: process.env.PINATA_API_KEY,\n apiSecret: process.env.PINATA_API_SECRET,\n jwt: process.env.PINATA_JWT,\n };\n}\n\n/** True when enough credentials are present to talk to real Pinata. */\nexport function hasPinataCredentials(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n return Boolean(opts.jwt || (opts.apiKey && opts.apiSecret));\n}\n\n/**\n * True when a non-default pinning endpoint is configured — i.e. `IPFS_API_URL` points\n * somewhere other than real Pinata (typically a local `@sm-lab/ipfs`). Such endpoints\n * accept unauthenticated pins, so credentials are not required to upload to them.\n */\nexport function hasCustomIpfsEndpoint(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n const raw = (opts.apiUrl ?? '').replace(/\\/+$/, '');\n return raw.length > 0 && raw !== DEFAULT_IPFS_API_URL;\n}\n\n/**\n * Whether `make`/`tree` should attempt to pin. With the local-first default there is always a\n * usable target, so this returns `true` unless an explicit `IPFS_API_URL` points directly at\n * real Pinata without credentials (that edge case cannot pin, so we still return `false` there).\n * Use the CLI's `--no-upload` / `MakeOptions.noUpload` to explicitly skip pinning.\n */\nexport function shouldAttemptPin(opts: IpfsClientOptions = ipfsOptionsFromEnv()): boolean {\n const raw = (opts.apiUrl ?? process.env.IPFS_API_URL ?? '').replace(/\\/+$/, '');\n if (raw === DEFAULT_IPFS_API_URL && !hasPinataCredentials(opts)) return false;\n // All other cases: custom endpoint (including no env, which falls through to LOCAL) or Pinata creds.\n return true;\n}\n\nfunction buildAuthHeaders(opts: IpfsClientOptions): Record<string, string> {\n if (opts.jwt) {\n return { Authorization: `Bearer ${opts.jwt}` };\n }\n if (opts.apiKey && opts.apiSecret) {\n return {\n pinata_api_key: opts.apiKey,\n pinata_secret_api_key: opts.apiSecret,\n };\n }\n return {};\n}\n\n/**\n * Pin a JSON object and return its CID. POSTs the Pinata `pinJSONToIPFS` envelope\n * (`{ pinataContent, pinataMetadata }`) so both real Pinata and `@sm-lab/ipfs` accept it.\n */\nexport async function pinJsonToIpfs(\n data: unknown,\n metadataName: string,\n opts: IpfsClientOptions = ipfsOptionsFromEnv(),\n): Promise<string> {\n const url = `${resolveIpfsApiUrl(opts.apiUrl)}/pinning/pinJSONToIPFS`;\n const body = JSON.stringify({\n pinataContent: data,\n pinataMetadata: { name: metadataName },\n });\n\n const res = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...buildAuthHeaders(opts) },\n body,\n });\n\n if (!res.ok) {\n const detail = await res.text().catch(() => '');\n throw new Error(\n `pinJSONToIPFS failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,\n );\n }\n\n const json = (await res.json()) as Partial<PinResponse>;\n if (!json.IpfsHash) {\n throw new Error(`pinJSONToIPFS: response missing IpfsHash (${JSON.stringify(json)})`);\n }\n return json.IpfsHash;\n}\n","import { buildAddressesTree, buildRewardsTree, buildStrikesTree } from './tree';\nimport { readStrikesFile, writeJsonFile } from './io';\nimport { pinJsonToIpfs, shouldAttemptPin } from './ipfs';\nimport type { TreeConfig } from './io';\nimport type { TreeDump } from './tree';\n\n/**\n * merkle's single job: build a Merkle tree from input, pin it to IPFS, and return the\n * root + CID. Pushing those on-chain (and resolving deploy addresses) is out of scope —\n * that belongs to `@sm-lab/receipts`. No `cast`, no `DEPLOY_JSON_PATH` here.\n */\n\nexport interface MakeResult {\n treeRoot: string;\n /** undefined when IPFS upload was skipped (--no-upload, or nothing configured). */\n treeCid?: string;\n /** set only when `configPath` was provided and a `{ treeRoot, treeCid }` file was written. */\n configPath?: string;\n}\n\nexport interface MakeRewardsResult extends MakeResult {\n logCid?: string;\n /**\n * JSON-safe tree dump (bigint leaf values serialized as decimal strings).\n * Identical shape to OZ `StandardMerkleTree.dump()` but with string values, so\n * `JSON.stringify(treeDump)` is safe without a custom replacer.\n */\n treeDump: TreeDump;\n}\n\nexport interface MakeOptions {\n /** Skip pinning to IPFS (build + return root only). */\n noUpload?: boolean;\n /** When set, also write `{ treeRoot, treeCid }` JSON here (a handoff seam for receipts/CI). */\n configPath?: string;\n}\n\nasync function maybePin(\n data: unknown,\n name: string,\n noUpload?: boolean,\n): Promise<string | undefined> {\n if (noUpload) return undefined;\n if (!shouldAttemptPin()) {\n console.warn(\n 'Warning: IPFS upload skipped — set IPFS_API_URL pointing at real Pinata and supply PINATA_API_KEY/SECRET or PINATA_JWT. Unset IPFS_API_URL pins to local @sm-lab/ipfs (http://127.0.0.1:5001).',\n );\n return undefined;\n }\n return pinJsonToIpfs(data, name);\n}\n\nfunction finish(treeRoot: string, treeCid: string | undefined, configPath?: string): MakeResult {\n if (configPath) {\n const config: TreeConfig = { treeRoot, treeCid: treeCid ?? '' };\n writeJsonFile(configPath, config);\n }\n return { treeRoot, treeCid, configPath };\n}\n\n/**\n * Addresses (vetted gate): build the address tree from a resolved address list, pin it, return\n * `{ treeRoot, treeCid }`. The CLI resolves file/inline/flag inputs to `string[]` before calling\n * this — keeping this function pure so it can be called directly from TS consumers without\n * touching the filesystem.\n */\nexport async function makeAddresses(\n addresses: string[],\n opts: MakeOptions = {},\n): Promise<MakeResult> {\n const tree = buildAddressesTree(addresses);\n const treeCid = await maybePin(tree.dump(), 'merkle-tree-addresses', opts.noUpload);\n return finish(tree.root, treeCid, opts.configPath);\n}\n\n/** Strikes: build the strikes tree, pin it, return `{ treeRoot, treeCid }`. */\nexport async function makeStrikes(\n strikesPath: string,\n opts: MakeOptions = {},\n): Promise<MakeResult> {\n const tree = buildStrikesTree(readStrikesFile(strikesPath));\n const treeCid = await maybePin(tree.dump(), 'merkle-tree-strikes', opts.noUpload);\n return finish(tree.root, treeCid, opts.configPath);\n}\n\nconst bigintReplacer = (_k: string, v: unknown): unknown =>\n typeof v === 'bigint' ? v.toString() : v;\n\n/**\n * Rewards: build the cumulative rewards tree from `[nodeOperatorId, cumulativeShares]` leaves\n * (bigint values), optionally pin the tree dump and a log object, and return\n * `{ treeRoot, treeCid, logCid?, treeDump }`.\n *\n * Bigints in the tree dump and `opts.log` are serialized to decimal strings before pinning\n * (OZ `dump()` returns leaf values verbatim, which are bigints here). The returned `treeDump`\n * uses the same serialization so it is JSON-safe (string values, no BigInt).\n */\nexport async function makeRewards(\n leaves: [bigint, bigint][],\n opts: MakeOptions & { log?: unknown } = {},\n): Promise<MakeRewardsResult> {\n const tree = buildRewardsTree(leaves);\n // OZ dump() returns leaf values as bigints; JSON.stringify(dump) would throw without this.\n const treeDump: TreeDump = JSON.parse(JSON.stringify(tree.dump(), bigintReplacer));\n const treeCid = await maybePin(treeDump, 'merkle-tree-rewards', opts.noUpload);\n const logCid =\n opts.log !== undefined && !opts.noUpload\n ? await maybePin(JSON.parse(JSON.stringify(opts.log, bigintReplacer)), 'merkle-rewards-log')\n : undefined;\n const base = finish(tree.root, treeCid, opts.configPath);\n return logCid !== undefined ? { ...base, logCid, treeDump } : { ...base, treeDump };\n}\n"],"mappings":"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"}
|