@sm-lab/merkle 1.3.0 → 1.4.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 +34 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{pipelines-D7R4SJ5k.mjs → pipelines-CxHaVRGi.mjs} +52 -2
- package/dist/pipelines-CxHaVRGi.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/pipelines-D7R4SJ5k.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { _ as readAddressFile, n as makeRewards, r as makeStrikes, t as makeAddresses, v as readJsonFile } from "./pipelines-CxHaVRGi.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
|
@@ -37,6 +37,17 @@ declare function buildStrikesTree(entries: StrikesEntry[]): StandardMerkleTree<[
|
|
|
37
37
|
* pad leaf is appended by the caller, not here (same division of labor as the addresses/strikes builders).
|
|
38
38
|
*/
|
|
39
39
|
declare function buildRewardsTree(leaves: [bigint, bigint][]): StandardMerkleTree<[bigint, bigint]>;
|
|
40
|
+
/**
|
|
41
|
+
* Recover the address list from an addresses-tree dump — the inverse of
|
|
42
|
+
* `buildAddressesTree(...).dump()`. Loads the OZ tree (which validates the dump shape) and reads
|
|
43
|
+
* each single-value `['address']` leaf. Pure, no I/O. Used by recipes' `add-gate` to merge new
|
|
44
|
+
* addresses into a gate's existing whitelist.
|
|
45
|
+
*
|
|
46
|
+
* Short-circuits on an empty dump: OZ's `StandardMerkleTree` rejects zero-leaf trees in both
|
|
47
|
+
* `.of()` and `.load()` (`isValidMerkleTree` requires a non-empty tree), so a gate whose whitelist
|
|
48
|
+
* is currently empty can't be round-tripped through `StandardMerkleTree.load` at all.
|
|
49
|
+
*/
|
|
50
|
+
declare function addressesFromDump(dump: TreeDump): string[];
|
|
40
51
|
//#endregion
|
|
41
52
|
//#region src/io.d.ts
|
|
42
53
|
/**
|
|
@@ -80,6 +91,8 @@ declare function writeJsonFile(filePath: string, data: unknown): void;
|
|
|
80
91
|
declare const DEFAULT_IPFS_API_URL = "https://api.pinata.cloud";
|
|
81
92
|
/** Local @sm-lab/ipfs default — the fallback when no IPFS_API_URL or Pinata creds are set. */
|
|
82
93
|
declare const LOCAL_IPFS_API_URL = "http://127.0.0.1:5001";
|
|
94
|
+
/** Real Pinata public gateway — the read fallback when the pin origin is the Pinata API host. */
|
|
95
|
+
declare const DEFAULT_IPFS_GATEWAY_URL = "https://gateway.pinata.cloud";
|
|
83
96
|
interface IpfsClientOptions {
|
|
84
97
|
/** Base URL of the pinning service. Defaults to `IPFS_API_URL` env, then real Pinata. */
|
|
85
98
|
apiUrl?: string;
|
|
@@ -101,6 +114,26 @@ interface PinResponse {
|
|
|
101
114
|
* → local @sm-lab/ipfs (http://127.0.0.1:5001).
|
|
102
115
|
*/
|
|
103
116
|
declare function resolveIpfsApiUrl(apiUrl?: string): string;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the base URL for IPFS *reads* (`GET /ipfs/:cid`): explicit `gatewayUrl` →
|
|
119
|
+
* `IPFS_GATEWAY_URL` env → the pin origin (`resolveIpfsApiUrl` — exactly right for the local
|
|
120
|
+
* `@sm-lab/ipfs` mock, which serves pinning AND `/ipfs/:cid` on one port) → the public Pinata
|
|
121
|
+
* gateway when the pin origin is the Pinata API host (`api.pinata.cloud` does NOT serve `/ipfs`).
|
|
122
|
+
*/
|
|
123
|
+
declare function resolveIpfsGatewayUrl(gatewayUrl?: string): string;
|
|
124
|
+
interface FetchIpfsOptions {
|
|
125
|
+
/** Gateway base URL. Defaults per {@link resolveIpfsGatewayUrl}. */
|
|
126
|
+
gatewayUrl?: string;
|
|
127
|
+
/** Caller's bypass hint woven into the unreachable error (e.g. `'pass --from-cid <cid>'`). */
|
|
128
|
+
skipHint?: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Fetch + JSON-parse a pinned object by CID via `GET {gateway}/ipfs/{cid}`. The read counterpart
|
|
132
|
+
* of {@link pinJsonToIpfs}, mirroring its discipline: trailing-slash-stripped join, explicit
|
|
133
|
+
* `Response` typing (no DOM lib), and an actionable throw. A thrown fetch (connection refused /
|
|
134
|
+
* DNS / timeout) or a non-2xx surfaces an error naming the gateway + the caller's `skipHint`.
|
|
135
|
+
*/
|
|
136
|
+
declare function fetchIpfsJson(cid: string, opts?: FetchIpfsOptions): Promise<unknown>;
|
|
104
137
|
/** Read pinning config from the environment (credentials + endpoint switch). */
|
|
105
138
|
declare function ipfsOptionsFromEnv(): IpfsClientOptions;
|
|
106
139
|
/** True when enough credentials are present to talk to real Pinata. */
|
|
@@ -185,5 +218,5 @@ declare function makeRewards(leaves: [bigint, bigint][], opts?: MakeOptions & {
|
|
|
185
218
|
log?: unknown;
|
|
186
219
|
}): Promise<MakeRewardsResult>;
|
|
187
220
|
//#endregion
|
|
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 };
|
|
221
|
+
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, DEFAULT_IPFS_GATEWAY_URL, type FetchIpfsOptions, 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, addressesFromDump, assertPinnable, buildAddressesTree, buildRewardsTree, buildStrikesTree, fetchIpfsJson, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, resolveIpfsGatewayUrl, shouldAttemptPin, writeJsonFile };
|
|
189
222
|
//# 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;;;;;
|
|
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;AAjBhF;;;;;;;;AAEqB;AAerB;AAjBA,iBA+BgB,iBAAA,CAAkB,IAAc,EAAR,QAAQ;;;;;AA1DhD;;;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;AFCuD;AAAA,cEEvD,wBAAA;AAAA,UAEI,iBAAA;;EAEf,MAAA;EFFA;EEIA,MAAA;EFFA;EEIA,SAAA;EFJO;EEMP,GAAA;AAAA;;UAIe,WAAA;EACf,QAAA;EACA,OAAA;EACA,SAAA;AAAA;;AFNyE;AAQ3E;;iBEKgB,iBAAA,CAAkB,MAAe;;;;;;AFH5B;iBEiBL,qBAAA,CAAsB,UAAmB;AAAA,UAOxC,gBAAA;;EAEf,UAAA;EFX8E;EEa9E,QAAQ;AAAA;;;AFCsC;;;;iBEQ1B,aAAA,CAAc,GAAA,UAAa,IAAA,GAAM,gBAAA,GAAwB,OAAO;;iBAuBtE,kBAAA,IAAsB,iBAAiB;;iBAUvC,oBAAA,CAAqB,IAA8C,GAAxC,iBAAwC;ADhGnF;;;;;AAAA,iBCyGgB,qBAAA,CAAsB,IAA8C,GAAxC,iBAAwC;;;ADzGhC;AAWpD;;;iBCyGgB,gBAAA,CAAiB,IAA8C,GAAxC,iBAAwC;ADzGjC;AAY9C;;;;AAAgD;AAQhD;;;;AApB8C,iBC0HxB,cAAA,CACpB,QAAA,WACA,IAAA,GAAM,iBAAA,GACL,OAAO;ADpGV;;;;AAAA,iBC4JsB,aAAA,CACpB,IAAA,WACA,YAAA,UACA,IAAA,GAAM,iBAAA,GACL,OAAO;;;;;AFvMV;;;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 { C as buildStrikesTree,
|
|
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 };
|
|
1
|
+
import { C as STRIKES_LEAF_ENCODING, D as buildStrikesTree, E as buildRewardsTree, S as REWARDS_LEAF_ENCODING, T as buildAddressesTree, _ as readAddressFile, a as DEFAULT_IPFS_GATEWAY_URL, b as writeJsonFile, c as fetchIpfsJson, d as ipfsOptionsFromEnv, f as pinJsonToIpfs, g as parseAddresses, h as shouldAttemptPin, i as DEFAULT_IPFS_API_URL, l as hasCustomIpfsEndpoint, m as resolveIpfsGatewayUrl, n as makeRewards, o as LOCAL_IPFS_API_URL, p as resolveIpfsApiUrl, r as makeStrikes, s as assertPinnable, t as makeAddresses, u as hasPinataCredentials, v as readJsonFile, w as addressesFromDump, x as ADDRESSES_LEAF_ENCODING, y as readStrikesFile } from "./pipelines-CxHaVRGi.mjs";
|
|
2
|
+
export { ADDRESSES_LEAF_ENCODING, DEFAULT_IPFS_API_URL, DEFAULT_IPFS_GATEWAY_URL, LOCAL_IPFS_API_URL, REWARDS_LEAF_ENCODING, STRIKES_LEAF_ENCODING, addressesFromDump, assertPinnable, buildAddressesTree, buildRewardsTree, buildStrikesTree, fetchIpfsJson, hasCustomIpfsEndpoint, hasPinataCredentials, ipfsOptionsFromEnv, makeAddresses, makeRewards, makeStrikes, parseAddresses, pinJsonToIpfs, readAddressFile, readJsonFile, readStrikesFile, resolveIpfsApiUrl, resolveIpfsGatewayUrl, shouldAttemptPin, writeJsonFile };
|
|
@@ -44,6 +44,23 @@ function buildStrikesTree(entries) {
|
|
|
44
44
|
function buildRewardsTree(leaves) {
|
|
45
45
|
return StandardMerkleTree.of(leaves, [...REWARDS_LEAF_ENCODING]);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Recover the address list from an addresses-tree dump — the inverse of
|
|
49
|
+
* `buildAddressesTree(...).dump()`. Loads the OZ tree (which validates the dump shape) and reads
|
|
50
|
+
* each single-value `['address']` leaf. Pure, no I/O. Used by recipes' `add-gate` to merge new
|
|
51
|
+
* addresses into a gate's existing whitelist.
|
|
52
|
+
*
|
|
53
|
+
* Short-circuits on an empty dump: OZ's `StandardMerkleTree` rejects zero-leaf trees in both
|
|
54
|
+
* `.of()` and `.load()` (`isValidMerkleTree` requires a non-empty tree), so a gate whose whitelist
|
|
55
|
+
* is currently empty can't be round-tripped through `StandardMerkleTree.load` at all.
|
|
56
|
+
*/
|
|
57
|
+
function addressesFromDump(dump) {
|
|
58
|
+
if (dump.values.length === 0) return [];
|
|
59
|
+
const tree = StandardMerkleTree.load(dump);
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const [, leaf] of tree.entries()) out.push(leaf[0]);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
47
64
|
//#endregion
|
|
48
65
|
//#region src/io.ts
|
|
49
66
|
/** Read+parse a JSON file as `T`. Throws (with the path) if missing. */
|
|
@@ -93,6 +110,8 @@ function writeJsonFile(filePath, data) {
|
|
|
93
110
|
const DEFAULT_IPFS_API_URL = "https://api.pinata.cloud";
|
|
94
111
|
/** Local @sm-lab/ipfs default — the fallback when no IPFS_API_URL or Pinata creds are set. */
|
|
95
112
|
const LOCAL_IPFS_API_URL = "http://127.0.0.1:5001";
|
|
113
|
+
/** Real Pinata public gateway — the read fallback when the pin origin is the Pinata API host. */
|
|
114
|
+
const DEFAULT_IPFS_GATEWAY_URL = "https://gateway.pinata.cloud";
|
|
96
115
|
/**
|
|
97
116
|
* Resolve the pinning base URL: explicit apiUrl → IPFS_API_URL env → Pinata (if PINATA_* set)
|
|
98
117
|
* → local @sm-lab/ipfs (http://127.0.0.1:5001).
|
|
@@ -103,6 +122,37 @@ function resolveIpfsApiUrl(apiUrl) {
|
|
|
103
122
|
if (hasPinataCredentials()) return DEFAULT_IPFS_API_URL;
|
|
104
123
|
return LOCAL_IPFS_API_URL;
|
|
105
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the base URL for IPFS *reads* (`GET /ipfs/:cid`): explicit `gatewayUrl` →
|
|
127
|
+
* `IPFS_GATEWAY_URL` env → the pin origin (`resolveIpfsApiUrl` — exactly right for the local
|
|
128
|
+
* `@sm-lab/ipfs` mock, which serves pinning AND `/ipfs/:cid` on one port) → the public Pinata
|
|
129
|
+
* gateway when the pin origin is the Pinata API host (`api.pinata.cloud` does NOT serve `/ipfs`).
|
|
130
|
+
*/
|
|
131
|
+
function resolveIpfsGatewayUrl(gatewayUrl) {
|
|
132
|
+
const explicit = gatewayUrl || process.env.IPFS_GATEWAY_URL;
|
|
133
|
+
if (explicit) return explicit.replace(/\/+$/, "");
|
|
134
|
+
const pin = resolveIpfsApiUrl();
|
|
135
|
+
return pin === "https://api.pinata.cloud" ? DEFAULT_IPFS_GATEWAY_URL : pin;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fetch + JSON-parse a pinned object by CID via `GET {gateway}/ipfs/{cid}`. The read counterpart
|
|
139
|
+
* of {@link pinJsonToIpfs}, mirroring its discipline: trailing-slash-stripped join, explicit
|
|
140
|
+
* `Response` typing (no DOM lib), and an actionable throw. A thrown fetch (connection refused /
|
|
141
|
+
* DNS / timeout) or a non-2xx surfaces an error naming the gateway + the caller's `skipHint`.
|
|
142
|
+
*/
|
|
143
|
+
async function fetchIpfsJson(cid, opts = {}) {
|
|
144
|
+
const base = resolveIpfsGatewayUrl(opts.gatewayUrl);
|
|
145
|
+
const url = `${base}/ipfs/${cid}`;
|
|
146
|
+
const hint = opts.skipHint ?? "supply the addresses another way";
|
|
147
|
+
let res;
|
|
148
|
+
try {
|
|
149
|
+
res = await fetch(url);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new Error(`@sm-lab/merkle: cannot reach the IPFS gateway at ${base} to read ${cid}.\nDo one of:\n • start the local mock: npx @sm-lab/ipfs serve (or: pnpm stack:up)\n • point elsewhere: set IPFS_GATEWAY_URL=<url>\n • ${hint}`);
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) throw new Error(`@sm-lab/merkle: GET ${url} failed: ${res.status} ${res.statusText}`);
|
|
154
|
+
return await res.json();
|
|
155
|
+
}
|
|
106
156
|
/** Read pinning config from the environment (credentials + endpoint switch). */
|
|
107
157
|
function ipfsOptionsFromEnv() {
|
|
108
158
|
return {
|
|
@@ -264,6 +314,6 @@ async function makeRewards(leaves, opts = {}) {
|
|
|
264
314
|
};
|
|
265
315
|
}
|
|
266
316
|
//#endregion
|
|
267
|
-
export {
|
|
317
|
+
export { STRIKES_LEAF_ENCODING as C, buildStrikesTree as D, buildRewardsTree as E, REWARDS_LEAF_ENCODING as S, buildAddressesTree as T, readAddressFile as _, DEFAULT_IPFS_GATEWAY_URL as a, writeJsonFile as b, fetchIpfsJson as c, ipfsOptionsFromEnv as d, pinJsonToIpfs as f, parseAddresses as g, shouldAttemptPin as h, DEFAULT_IPFS_API_URL as i, hasCustomIpfsEndpoint as l, resolveIpfsGatewayUrl as m, makeRewards as n, LOCAL_IPFS_API_URL as o, resolveIpfsApiUrl as p, makeStrikes as r, assertPinnable as s, makeAddresses as t, hasPinataCredentials as u, readJsonFile as v, addressesFromDump as w, ADDRESSES_LEAF_ENCODING as x, readStrikesFile as y };
|
|
268
318
|
|
|
269
|
-
//# sourceMappingURL=pipelines-
|
|
319
|
+
//# sourceMappingURL=pipelines-CxHaVRGi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipelines-CxHaVRGi.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\n/**\n * Recover the address list from an addresses-tree dump — the inverse of\n * `buildAddressesTree(...).dump()`. Loads the OZ tree (which validates the dump shape) and reads\n * each single-value `['address']` leaf. Pure, no I/O. Used by recipes' `add-gate` to merge new\n * addresses into a gate's existing whitelist.\n *\n * Short-circuits on an empty dump: OZ's `StandardMerkleTree` rejects zero-leaf trees in both\n * `.of()` and `.load()` (`isValidMerkleTree` requires a non-empty tree), so a gate whose whitelist\n * is currently empty can't be round-tripped through `StandardMerkleTree.load` at all.\n */\nexport function addressesFromDump(dump: TreeDump): string[] {\n if (dump.values.length === 0) return [];\n const tree = StandardMerkleTree.load(dump);\n const out: string[] = [];\n for (const [, leaf] of tree.entries()) {\n out.push((leaf as [string])[0]);\n }\n return out;\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\n/** Real Pinata public gateway — the read fallback when the pin origin is the Pinata API host. */\nexport const DEFAULT_IPFS_GATEWAY_URL = 'https://gateway.pinata.cloud';\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/**\n * Resolve the base URL for IPFS *reads* (`GET /ipfs/:cid`): explicit `gatewayUrl` →\n * `IPFS_GATEWAY_URL` env → the pin origin (`resolveIpfsApiUrl` — exactly right for the local\n * `@sm-lab/ipfs` mock, which serves pinning AND `/ipfs/:cid` on one port) → the public Pinata\n * gateway when the pin origin is the Pinata API host (`api.pinata.cloud` does NOT serve `/ipfs`).\n */\nexport function resolveIpfsGatewayUrl(gatewayUrl?: string): string {\n const explicit = gatewayUrl || process.env.IPFS_GATEWAY_URL;\n if (explicit) return explicit.replace(/\\/+$/, '');\n const pin = resolveIpfsApiUrl();\n return pin === DEFAULT_IPFS_API_URL ? DEFAULT_IPFS_GATEWAY_URL : pin;\n}\n\nexport interface FetchIpfsOptions {\n /** Gateway base URL. Defaults per {@link resolveIpfsGatewayUrl}. */\n gatewayUrl?: string;\n /** Caller's bypass hint woven into the unreachable error (e.g. `'pass --from-cid <cid>'`). */\n skipHint?: string;\n}\n\n/**\n * Fetch + JSON-parse a pinned object by CID via `GET {gateway}/ipfs/{cid}`. The read counterpart\n * of {@link pinJsonToIpfs}, mirroring its discipline: trailing-slash-stripped join, explicit\n * `Response` typing (no DOM lib), and an actionable throw. A thrown fetch (connection refused /\n * DNS / timeout) or a non-2xx surfaces an error naming the gateway + the caller's `skipHint`.\n */\nexport async function fetchIpfsJson(cid: string, opts: FetchIpfsOptions = {}): Promise<unknown> {\n const base = resolveIpfsGatewayUrl(opts.gatewayUrl);\n const url = `${base}/ipfs/${cid}`;\n const hint = opts.skipHint ?? 'supply the addresses another way';\n let res: Response;\n try {\n res = await fetch(url);\n } catch {\n throw new Error(\n `@sm-lab/merkle: cannot reach the IPFS gateway at ${base} to read ${cid}.\\n` +\n `Do one of:\\n` +\n ` • start the local mock: npx @sm-lab/ipfs serve (or: pnpm stack:up)\\n` +\n ` • point elsewhere: set IPFS_GATEWAY_URL=<url>\\n` +\n ` • ${hint}`,\n );\n }\n if (!res.ok) {\n throw new Error(`@sm-lab/merkle: GET ${url} failed: ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as unknown;\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 +0,0 @@
|
|
|
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":";;;;;;;;;;;;;;AAaA,MAAa,0BAA0B,CAAC,SAAS;;AAGjD,MAAa,wBAAwB;CAAC;CAAW;CAAU;AAAW;;AAGtE,MAAa,wBAAwB,CAAC,WAAW,SAAS;;AAa1D,SAAgB,mBAAmB,WAAmD;CACpF,OAAO,mBAAmB,GACxB,UAAU,KAAK,YAAY,CAAC,OAAO,CAAa,GAChD,CAAC,GAAG,uBAAuB,CAC7B;AACF;;AAGA,SAAgB,iBACd,SACgD;CAChD,OAAO,mBAAmB,GACxB,QAAQ,KAAK,EAAE,gBAAgB,QAAQ,cAAc;EAAC;EAAgB;EAAQ;CAAO,CAAC,GACtF,CAAC,GAAG,qBAAqB,CAC3B;AACF;;;;;;;;;AAUA,SAAgB,iBAAiB,QAAkE;CACjG,OAAO,mBAAmB,GAAG,QAAQ,CAAC,GAAG,qBAAqB,CAAC;AACjE;;;;AC3CA,SAAgB,aAAgB,UAAqB;CACnD,IAAI,CAAC,GAAG,WAAW,QAAQ,GACzB,MAAM,IAAI,MAAM,qBAAqB,UAAU;CAEjD,OAAO,KAAK,MAAM,GAAG,aAAa,UAAU,OAAO,CAAC;AACtD;;;;;AAMA,SAAgB,eAAe,SAA2B;CACxD,MAAM,UAAU,QAAQ,KAAK;CAC7B,IAAI,QAAQ,WAAW,GAAG,GACxB,OAAO,KAAK,MAAM,OAAO;CAE3B,OAAO,QACJ,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,QAAQ,CAAC,KAAK,WAAW,GAAG,CAAC;AACnD;;AAGA,SAAgB,gBAAgB,UAA4B;CAC1D,IAAI,CAAC,GAAG,WAAW,QAAQ,GACzB,MAAM,IAAI,MAAM,qBAAqB,UAAU;CAEjD,OAAO,eAAe,GAAG,aAAa,UAAU,OAAO,CAAC;AAC1D;;AAGA,SAAgB,gBAAgB,UAAkC;CAChE,OAAO,aAA6B,QAAQ;AAC9C;;AAGA,SAAgB,cAAc,UAAkB,MAAqB;CACnE,GAAG,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;CACxD,GAAG,cAAc,UAAU,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAC1D;;;;;;;;;;;;;;;;;ACxCA,MAAa,uBAAuB;;AAGpC,MAAa,qBAAqB;;;;;AAwBlC,SAAgB,kBAAkB,QAAyB;CAEzD,MAAM,WAAW,UAAU,QAAQ,IAAI;CACvC,IAAI,UAAU,OAAO,SAAS,QAAQ,QAAQ,EAAE;CAChD,IAAI,qBAAqB,GAAG,OAAO;CACnC,OAAO;AACT;;AAGA,SAAgB,qBAAwC;CACtD,OAAO;EACL,QAAQ,QAAQ,IAAI;EACpB,QAAQ,QAAQ,IAAI;EACpB,WAAW,QAAQ,IAAI;EACvB,KAAK,QAAQ,IAAI;CACnB;AACF;;AAGA,SAAgB,qBAAqB,OAA0B,mBAAmB,GAAY;CAC5F,OAAO,QAAQ,KAAK,OAAQ,KAAK,UAAU,KAAK,SAAU;AAC5D;;;;;;AAOA,SAAgB,sBAAsB,OAA0B,mBAAmB,GAAY;CAC7F,MAAM,OAAO,KAAK,UAAU,GAAA,CAAI,QAAQ,QAAQ,EAAE;CAClD,OAAO,IAAI,SAAS,KAAK,QAAA;AAC3B;;;;;;;AAQA,SAAgB,iBAAiB,OAA0B,mBAAmB,GAAY;CAExF,KADa,KAAK,UAAU,QAAQ,IAAI,gBAAgB,GAAA,CAAI,QAAQ,QAAQ,EACtE,MAAA,8BAA8B,CAAC,qBAAqB,IAAI,GAAG,OAAO;CAExE,OAAO;AACT;;;;;;;;;;;AAYA,eAAsB,eACpB,WAAW,4BACX,OAA0B,mBAAmB,GAC9B;CACf,MAAM,SAAS,kBAAkB,KAAK,MAAM;CAC5C,IAAI,CAAC,iBAAiB,IAAI,GAExB,MAAM,IAAI,MACR,kDAAkD,OAAO,sLAIhD,UACX;CAEF,IAAI,qBAAqB,IAAI,GAAG;CAChC,IAAI,MAAM,YAAY,MAAM,GAAG;CAC/B,MAAM,IAAI,MACR,4DAA4D,OAAO,wOAK1D,UACX;AACF;;;;;;;AAQA,eAAe,YAAY,KAAa,YAAY,KAAwB;CAC1E,IAAI;EACF,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,SAAS,EAAE,CAAC;EAC3D,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,iBAAiB,MAAiD;CACzE,IAAI,KAAK,KACP,OAAO,EAAE,eAAe,UAAU,KAAK,MAAM;CAE/C,IAAI,KAAK,UAAU,KAAK,WACtB,OAAO;EACL,gBAAgB,KAAK;EACrB,uBAAuB,KAAK;CAC9B;CAEF,OAAO,CAAC;AACV;;;;;AAMA,eAAsB,cACpB,MACA,cACA,OAA0B,mBAAmB,GAC5B;CACjB,MAAM,MAAM,GAAG,kBAAkB,KAAK,MAAM,EAAE;CAC9C,MAAM,OAAO,KAAK,UAAU;EAC1B,eAAe;EACf,gBAAgB,EAAE,MAAM,aAAa;CACvC,CAAC;CAED,MAAM,MAAM,MAAM,MAAM,KAAK;EAC3B,QAAQ;EACR,SAAS;GAAE,gBAAgB;GAAoB,GAAG,iBAAiB,IAAI;EAAE;EACzE;CACF,CAAC;CAED,IAAI,CAAC,IAAI,IAAI;EACX,MAAM,SAAS,MAAM,IAAI,KAAK,CAAC,CAAC,YAAY,EAAE;EAC9C,MAAM,IAAI,MACR,yBAAyB,IAAI,OAAO,GAAG,IAAI,aAAa,SAAS,MAAM,WAAW,IACpF;CACF;CAEA,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,6CAA6C,KAAK,UAAU,IAAI,EAAE,EAAE;CAEtF,OAAO,KAAK;AACd;;;ACrJA,eAAe,SACb,MACA,MACA,UAC6B;CAC7B,IAAI,UAAU,OAAO,KAAA;CACrB,IAAI,CAAC,iBAAiB,GAAG;EACvB,QAAQ,KACN,gMACF;EACA;CACF;CACA,OAAO,cAAc,MAAM,IAAI;AACjC;AAEA,SAAS,OAAO,UAAkB,SAA6B,YAAiC;CAC9F,IAAI,YAEF,cAAc,YAAY;EADG;EAAU,SAAS,WAAW;CAC5B,CAAC;CAElC,OAAO;EAAE;EAAU;EAAS;CAAW;AACzC;;;;;;;AAQA,eAAsB,cACpB,WACA,OAAoB,CAAC,GACA;CACrB,MAAM,OAAO,mBAAmB,SAAS;CACzC,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK,GAAG,yBAAyB,KAAK,QAAQ;CAClF,OAAO,OAAO,KAAK,MAAM,SAAS,KAAK,UAAU;AACnD;;AAGA,eAAsB,YACpB,aACA,OAAoB,CAAC,GACA;CACrB,MAAM,OAAO,iBAAiB,gBAAgB,WAAW,CAAC;CAC1D,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK,GAAG,uBAAuB,KAAK,QAAQ;CAChF,OAAO,OAAO,KAAK,MAAM,SAAS,KAAK,UAAU;AACnD;AAEA,MAAM,kBAAkB,IAAY,MAClC,OAAO,MAAM,WAAW,EAAE,SAAS,IAAI;;;;;;;;;;AAWzC,eAAsB,YACpB,QACA,OAAwC,CAAC,GACb;CAC5B,MAAM,OAAO,iBAAiB,MAAM;CAEpC,MAAM,WAAqB,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,GAAG,cAAc,CAAC;CACjF,MAAM,UAAU,MAAM,SAAS,UAAU,uBAAuB,KAAK,QAAQ;CAC7E,MAAM,SACJ,KAAK,QAAQ,KAAA,KAAa,CAAC,KAAK,WAC5B,MAAM,SAAS,KAAK,MAAM,KAAK,UAAU,KAAK,KAAK,cAAc,CAAC,GAAG,oBAAoB,IACzF,KAAA;CACN,MAAM,OAAO,OAAO,KAAK,MAAM,SAAS,KAAK,UAAU;CACvD,OAAO,WAAW,KAAA,IAAY;EAAE,GAAG;EAAM;EAAQ;CAAS,IAAI;EAAE,GAAG;EAAM;CAAS;AACpF"}
|