@rhinestone/shared-configs 1.8.0 → 1.10.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.
@@ -11,6 +11,12 @@ function validChain(overrides = {}) {
11
11
  vmType: "evm",
12
12
  network: "mainnet",
13
13
  stack: "vanilla",
14
+ caip2: "eip155:1",
15
+ explorer: {
16
+ url: "https://etherscan.io",
17
+ addressPath: "/address/",
18
+ txPath: "/tx/",
19
+ },
14
20
  nativeToken,
15
21
  wrappedNativeToken: { ...nativeToken, symbol: "WETH" },
16
22
  tokens: [{ ...nativeToken, balanceSlot: null, approvalSlot: null }],
@@ -53,9 +59,26 @@ function errorsFor(chain) {
53
59
  ["a non-boolean virtual flag", { virtual: "true" }],
54
60
  ["a missing network", { network: undefined }],
55
61
  ["an unknown network", { network: "devnet" }],
62
+ // `explorer` is required (nullable) in the public ChainEntry, so the build
63
+ // gate has to reject what the type says cannot happen.
64
+ ["a missing explorer", { explorer: undefined }],
65
+ ["an explorer that is not an object", { explorer: "https://etherscan.io" }],
66
+ ["an explorer missing txPath", {
67
+ explorer: { url: "https://etherscan.io", addressPath: "/address/" },
68
+ }],
69
+ // Links are built by concatenation, so the slash convention is load-bearing.
70
+ ["an explorer path with no leading slash", {
71
+ explorer: { url: "https://etherscan.io", addressPath: "address/", txPath: "/tx/" },
72
+ }],
73
+ ["an explorer url with a trailing slash", {
74
+ explorer: { url: "https://etherscan.io/", addressPath: "/address/", txPath: "/tx/" },
75
+ }],
56
76
  ])("rejects %s", (_label, overrides) => {
57
77
  (0, bun_test_1.expect)(errorsFor(validChain(overrides)).length).toBeGreaterThan(0);
58
78
  });
79
+ (0, bun_test_1.it)("accepts a null explorer, the answer for a chain that has none", () => {
80
+ (0, bun_test_1.expect)(errorsFor(validChain({ explorer: null }))).toEqual([]);
81
+ });
59
82
  (0, bun_test_1.it)("names the offending value so the build failure is actionable", () => {
60
83
  const [error] = errorsFor(validChain({ settlementLayers: ["TELEPORT"] }));
61
84
  (0, bun_test_1.expect)(error.message).toContain("TELEPORT");
@@ -214,6 +237,149 @@ function errorsFor(chain) {
214
237
  (0, bun_test_1.expect)(Object.keys(chains)).toHaveLength(Object.keys(index_1.chainRegistry).length);
215
238
  });
216
239
  });
240
+ (0, bun_test_1.describe)("classifyChainCaip2", () => {
241
+ (0, bun_test_1.it)("derives eip155:<chainId> for a chain that declares none", () => {
242
+ const { chains, issues } = (0, validation_1.classifyChainCaip2)({
243
+ "8453": validChain({ name: "Base", caip2: undefined }),
244
+ });
245
+ (0, bun_test_1.expect)(chains["8453"].caip2).toBe("eip155:8453");
246
+ (0, bun_test_1.expect)(issues).toEqual([]);
247
+ });
248
+ (0, bun_test_1.it)("keeps a declared id for a chain whose key is not an eip155 one", () => {
249
+ const { chains, issues } = (0, validation_1.classifyChainCaip2)({
250
+ "792703809": validChain({ name: "Solana", vmType: "svm", caip2: "solana:abc" }),
251
+ });
252
+ (0, bun_test_1.expect)(chains["792703809"].caip2).toBe("solana:abc");
253
+ (0, bun_test_1.expect)(issues).toEqual([]);
254
+ });
255
+ // The whole reason the field is authored rather than derived: HyperCore's
256
+ // registry key is 1337, but `eip155:1337` is the Hardhat local chain id.
257
+ (0, bun_test_1.it)("keeps a virtual EVM chain's declared id instead of deriving from its key", () => {
258
+ const { chains, issues } = (0, validation_1.classifyChainCaip2)({
259
+ "1337": validChain({
260
+ name: "HyperCore",
261
+ virtual: true,
262
+ caip2: "hypercore:mainnet",
263
+ }),
264
+ });
265
+ (0, bun_test_1.expect)(chains["1337"].caip2).toBe("hypercore:mainnet");
266
+ (0, bun_test_1.expect)(issues).toEqual([]);
267
+ });
268
+ // Deriving here would hand the entry an `eip155:` id that is confidently
269
+ // wrong — HyperCore's `eip155:1337` failure, reintroduced by the mechanism
270
+ // meant to prevent it.
271
+ bun_test_1.it.each([
272
+ ["a non-EVM chain", { vmType: "svm" }],
273
+ ["a virtual chain", { virtual: true }],
274
+ ])("errors when %s declares no caip2, rather than deriving one", (_label, overrides) => {
275
+ const { issues } = (0, validation_1.classifyChainCaip2)({
276
+ "424242": validChain({ ...overrides, caip2: undefined }),
277
+ });
278
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
279
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
280
+ (0, bun_test_1.expect)(issues[0].message).toContain("must author its caip2");
281
+ });
282
+ (0, bun_test_1.it)("errors when a real EVM chain declares an id that isn't its own", () => {
283
+ const { issues } = (0, validation_1.classifyChainCaip2)({
284
+ "8453": validChain({ name: "Base", caip2: "eip155:84532" }),
285
+ });
286
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
287
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
288
+ (0, bun_test_1.expect)(issues[0].message).toContain("eip155:8453");
289
+ });
290
+ (0, bun_test_1.it)("errors on a malformed declaration rather than falling back to the derived id", () => {
291
+ const { chains, issues } = (0, validation_1.classifyChainCaip2)({
292
+ "1337": validChain({ name: "HyperCore", virtual: true, caip2: "hypercore" }),
293
+ });
294
+ // Left as authored, so the structural check rejects it too — silently
295
+ // publishing eip155:1337 here is the exact failure being guarded against.
296
+ (0, bun_test_1.expect)(chains["1337"].caip2).toBe("hypercore");
297
+ (0, bun_test_1.expect)(issues.some((i) => i.message.includes("malformed"))).toBe(true);
298
+ });
299
+ // chainIdFromCaip2 is a reverse lookup over this field, so a collision makes
300
+ // it silently return whichever entry was iterated first.
301
+ (0, bun_test_1.it)("errors when two chains resolve to the same id", () => {
302
+ const { issues } = (0, validation_1.classifyChainCaip2)({
303
+ "1": validChain(),
304
+ "1337": validChain({ name: "Impostor", virtual: true, caip2: "eip155:1" }),
305
+ });
306
+ (0, bun_test_1.expect)(issues.some((i) => i.message.includes("already uses"))).toBe(true);
307
+ });
308
+ (0, bun_test_1.it)("leaves the real generated registry issue-free", () => {
309
+ const { issues } = (0, validation_1.classifyChainCaip2)(index_1.chainRegistry);
310
+ (0, bun_test_1.expect)(issues).toEqual([]);
311
+ });
312
+ });
313
+ (0, bun_test_1.describe)("classifyChainExplorers", () => {
314
+ const nets = (mainnets, testnets = []) => ({ mainnets, testnets });
315
+ // The classifier's input is pre-stamp, i.e. whatever the jsonnet authored —
316
+ // which for most chains is nothing. `validChain` carries an explorer so the
317
+ // structural checks pass, so strip it to exercise the derivation path.
318
+ const unauthored = (overrides = {}) => validChain({ ...overrides, explorer: undefined });
319
+ (0, bun_test_1.it)("derives the explorer from viem's own chain definition", () => {
320
+ const { chains, issues } = (0, validation_1.classifyChainExplorers)({ "8453": unauthored({ name: "Base" }) }, nets([{ id: 8453, viemChain: "base" }]));
321
+ (0, bun_test_1.expect)(chains["8453"].explorer).toEqual({
322
+ url: "https://basescan.org",
323
+ addressPath: "/address/",
324
+ txPath: "/tx/",
325
+ });
326
+ (0, bun_test_1.expect)(issues).toEqual([]);
327
+ });
328
+ // The regression this exists for: viem's chain 1337 is `localhost`, and
329
+ // HyperCore's registry key is 1337. Resolving by id would hand HyperCore
330
+ // Localhost's metadata — the `eip155:1337` mistake from the other direction.
331
+ (0, bun_test_1.it)("resolves by declared viemChain name, never by chain id", () => {
332
+ const { chains } = (0, validation_1.classifyChainExplorers)({ "1337": unauthored({ name: "HyperCore", virtual: true }) },
333
+ // No viemChain declared, exactly as the registry has it.
334
+ nets([{ id: 1337 }]));
335
+ (0, bun_test_1.expect)(chains["1337"].explorer).toBeNull();
336
+ });
337
+ (0, bun_test_1.it)("lets an authored explorer win, for paths viem cannot express", () => {
338
+ const tronscan = {
339
+ url: "https://tronscan.org",
340
+ addressPath: "/#/address/",
341
+ txPath: "/#/transaction/",
342
+ };
343
+ const { chains, issues } = (0, validation_1.classifyChainExplorers)({
344
+ "728126428": validChain({
345
+ name: "Tron",
346
+ vmType: "tvm",
347
+ explorer: tronscan,
348
+ }),
349
+ }, nets([{ id: 728126428 }]));
350
+ (0, bun_test_1.expect)(chains["728126428"].explorer).toEqual(tronscan);
351
+ (0, bun_test_1.expect)(issues).toEqual([]);
352
+ });
353
+ (0, bun_test_1.it)("errors on a malformed authored explorer", () => {
354
+ const { issues } = (0, validation_1.classifyChainExplorers)({ "1": validChain({ explorer: { url: "https://etherscan.io" } }) }, nets([{ id: 1, viemChain: "mainnet" }]));
355
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
356
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
357
+ });
358
+ // A missing explorer costs a hyperlink, not a wrong answer, so it warns
359
+ // rather than failing the build.
360
+ (0, bun_test_1.it)("warns rather than errors when nothing can supply one", () => {
361
+ const { chains, issues } = (0, validation_1.classifyChainExplorers)({ "424242": unauthored({ name: "Newchain" }) }, nets([{ id: 424242 }]));
362
+ (0, bun_test_1.expect)(chains["424242"].explorer).toBeNull();
363
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
364
+ (0, bun_test_1.expect)(issues[0].severity).toBe("warning");
365
+ });
366
+ // Asserted against the shipped registry rather than by re-running the
367
+ // classifier, so this covers what actually reaches the artifact.
368
+ (0, bun_test_1.it)("leaves the shipped registry with an explorer on every chain but HyperCore", () => {
369
+ const withoutExplorer = Object.entries(index_1.chainRegistry)
370
+ .filter(([, e]) => e.explorer == null)
371
+ .map(([id]) => id);
372
+ (0, bun_test_1.expect)(withoutExplorer).toEqual(["1337"]);
373
+ for (const [id, entry] of Object.entries(index_1.chainRegistry)) {
374
+ const explorer = entry.explorer;
375
+ if (explorer === null)
376
+ continue;
377
+ (0, bun_test_1.expect)(explorer.url, id).toMatch(/^https:\/\/[^/]+$/);
378
+ (0, bun_test_1.expect)(explorer.addressPath, id).toMatch(/^\/.*\/$/);
379
+ (0, bun_test_1.expect)(explorer.txPath, id).toMatch(/^\/.*\/$/);
380
+ }
381
+ });
382
+ });
217
383
  (0, bun_test_1.describe)("classifyChainStacks", () => {
218
384
  const nets = (mainnets, testnets = []) => ({ mainnets, testnets });
219
385
  // The common case: nothing is authored anywhere. viem already knows what Base
@@ -99,6 +99,8 @@ function renderChains(chainRegistry, mainnets, testnets) {
99
99
  " pegGroup?: PegGroup;",
100
100
  " balanceSlot: number | null;",
101
101
  " approvalSlot: number | null;",
102
+ " /** Bridge layers that can carry this token on this chain, when known. */",
103
+ " settlementLayers?: SettlementLayer[];",
102
104
  " unpriced?: boolean;",
103
105
  "}",
104
106
  "",
@@ -109,6 +111,15 @@ function renderChains(chainRegistry, mainnets, testnets) {
109
111
  " decimals: number;",
110
112
  "}",
111
113
  "",
114
+ "interface Explorer {",
115
+ " /** Origin, no trailing slash. */",
116
+ " url: string;",
117
+ " /** Path to an address page, with both slashes (`/address/`). */",
118
+ " addressPath: string;",
119
+ " /** Path to a transaction page, with both slashes (`/tx/`). */",
120
+ " txPath: string;",
121
+ "}",
122
+ "",
112
123
  "interface Chain {",
113
124
  " name: string;",
114
125
  " vmType: VmType;",
@@ -116,8 +127,11 @@ function renderChains(chainRegistry, mainnets, testnets) {
116
127
  " network: ChainNetwork;",
117
128
  " /** Execution stack, for consumers that must synthesise a viem `Chain`. */",
118
129
  " stack: ChainStack;",
119
- " /** CAIP-2 chain identifier; populated for non-eip155 chains only. */",
120
- " caip2?: string;",
130
+ " /** CAIP-2 chain identifier. Present on every chain: authored for the",
131
+ " * non-eip155 ones, derived as `eip155:<chainId>` for the rest. */",
132
+ " caip2: string;",
133
+ " /** Block explorer, or null for a chain that has none (render plain text). */",
134
+ " explorer: Explorer | null;",
121
135
  " /** True for virtual chains (e.g. HyperCore) that settle on another chain. */",
122
136
  " virtual?: boolean;",
123
137
  " nativeToken: NativeToken;",
@@ -132,7 +146,7 @@ function renderChains(chainRegistry, mainnets, testnets) {
132
146
  `const chains: Record<SupportedChain, Chain> = ${JSON.stringify(chains, null, 2)};`,
133
147
  "",
134
148
  "export { chains };",
135
- "export type { Chain, Token, NativeToken };",
149
+ "export type { Chain, Token, NativeToken, Explorer };",
136
150
  ].join("\n");
137
151
  }
138
152
  function runJsonnet(yeetRoot) {
@@ -365,8 +379,19 @@ async function generate() {
365
379
  const { chains: networkClassified, issues: networkIssues } = (0, validation_1.classifyChainNetworks)(output.chains, output.mainnets, output.testnets);
366
380
  // Then the execution stack, for consumers that have to build a viem `Chain`
367
381
  // themselves because the chain postdates their pinned viem.
368
- const { chains: classifiedChains, issues: stackIssues } = (0, validation_1.classifyChainStacks)(networkClassified, output.networks);
369
- const classificationIssues = [...networkIssues, ...stackIssues];
382
+ const { chains: stackClassified, issues: stackIssues } = (0, validation_1.classifyChainStacks)(networkClassified, output.networks);
383
+ // Then the canonical CAIP-2 id, so consumers key a chain by the id the wire
384
+ // uses instead of each deriving `eip155:<id>` for themselves.
385
+ const { chains: caip2Classified, issues: caip2Issues } = (0, validation_1.classifyChainCaip2)(stackClassified);
386
+ // Then the block explorer, so a consumer rendering a link for a chain added
387
+ // after its release does not need a hand-maintained table of its own.
388
+ const { chains: classifiedChains, issues: explorerIssues } = (0, validation_1.classifyChainExplorers)(caip2Classified, output.networks);
389
+ const classificationIssues = [
390
+ ...networkIssues,
391
+ ...stackIssues,
392
+ ...caip2Issues,
393
+ ...explorerIssues,
394
+ ];
370
395
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "chains.json"), `${JSON.stringify(classifiedChains, null, 2)}\n`);
371
396
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "mainnets.json"), `${JSON.stringify(output.mainnets, null, 2)}\n`);
372
397
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "testnets.json"), `${JSON.stringify(output.testnets, null, 2)}\n`);
@@ -65,6 +65,63 @@ export declare function classifyChainNetworks(chains: ChainRegistry, mainnets: R
65
65
  chains: ChainRegistry;
66
66
  issues: ValidationIssue[];
67
67
  };
68
+ /**
69
+ * Stamps every chain with its canonical CAIP-2 id, deriving `eip155:<chainId>`
70
+ * for the chains that do not declare one.
71
+ *
72
+ * Why this exists: `caip2` was authored only for the chains whose id is not an
73
+ * EIP-155 one (Solana, Tron, HyperCore), so every consumer that wanted a CAIP-2
74
+ * id had to carry the same `entry.caip2 ?? \`eip155:${id}\`` fallback — the
75
+ * orchestrator, deposit-service, the SDK and the indexers each had their own.
76
+ * A fallback repeated in five places is a rule with five chances to drift, and
77
+ * the one place it is genuinely load-bearing (HyperCore, whose `eip155:1337`
78
+ * spelling is wrong) is exactly where getting it wrong is silent.
79
+ *
80
+ * Derived rather than authored per chain for the same reason `network` is: an
81
+ * EVM chain's CAIP-2 id is a restatement of its chain id, so authoring it would
82
+ * only create a second copy that could disagree with the first.
83
+ *
84
+ * Uniqueness is checked because `chainIdFromCaip2` is a reverse lookup over this
85
+ * field — two chains sharing an id makes that lookup silently return whichever
86
+ * came first.
87
+ */
88
+ export declare function classifyChainCaip2(chains: ChainRegistry): {
89
+ chains: ChainRegistry;
90
+ issues: ValidationIssue[];
91
+ };
92
+ /**
93
+ * Where a chain's transactions and addresses can be looked at by a human.
94
+ * `url` has no trailing slash; the paths carry their own leading and trailing
95
+ * ones, so a link is `${url}${addressPath}${value}`.
96
+ */
97
+ export interface ChainExplorer {
98
+ url: string;
99
+ addressPath: string;
100
+ txPath: string;
101
+ }
102
+ /**
103
+ * Stamps every chain with its block explorer, derived from the chain's viem
104
+ * definition where it has one and authored in the jsonnet where it does not.
105
+ *
106
+ * Resolved by the entry's declared `viemChain` **name**, never by chain id. A
107
+ * lookup by id is actively wrong here: viem's chain 1337 is `localhost`, so
108
+ * HyperCore — whose registry key is 1337 — would silently inherit Localhost's
109
+ * metadata. That is the same mistake as addressing HyperCore by `eip155:1337`,
110
+ * arrived at from the other direction.
111
+ *
112
+ * A missing explorer is not an error. It resolves to `null`, which consumers
113
+ * render as plain text instead of a link; the cost of not knowing one is a
114
+ * missing hyperlink, not a wrong answer. It is still worth a warning, because
115
+ * the usual cause is a chain added without one rather than a chain that
116
+ * genuinely has none.
117
+ */
118
+ export declare function classifyChainExplorers(chains: ChainRegistry, networks: {
119
+ mainnets: StackNetworkEntry[];
120
+ testnets: StackNetworkEntry[];
121
+ }): {
122
+ chains: ChainRegistry;
123
+ issues: ValidationIssue[];
124
+ };
68
125
  /** The subset of a generated network entry that stack classification needs. */
69
126
  export interface StackNetworkEntry {
70
127
  id: number;
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../scripts/validation.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,KAAK,GACL,OAAO,GACP,KAAK,GACL,MAAM,GACN,OAAO,GACP,MAAM,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAC1D;+DAC2D;IAC3D,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,yDAAyD;IACzD,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;oFACgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgKD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEtD,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAiBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAwEtD;AAED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6CD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE;IACR,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,QAAQ,EAAE,iBAAiB,EAAE,CAAC;CAC/B,GACA;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAA;CAAE,CAyFtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,gBAAgB,CAiE3E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAWvE"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../scripts/validation.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.classifyChainNetworks = classifyChainNetworks;
37
+ exports.classifyChainCaip2 = classifyChainCaip2;
38
+ exports.classifyChainExplorers = classifyChainExplorers;
37
39
  exports.classifyChainStacks = classifyChainStacks;
38
40
  exports.validateChainConfig = validateChainConfig;
39
41
  exports.formatValidationIssues = formatValidationIssues;
@@ -72,6 +74,18 @@ const STACKS = new Set(['op-stack', 'zksync', 'celo', 'vanilla']);
72
74
  // canonical positive integers — no leading zeros, which would let "01" and "1"
73
75
  // both address chain 1.
74
76
  const CHAIN_ID_KEY = /^[1-9][0-9]*$/;
77
+ // CAIP-2 (https://chainagnostic.org/CAIPs/caip-2): `<namespace>:<reference>`.
78
+ //
79
+ // Deliberately shape-only — the namespace set is not ours to close, and a chain
80
+ // arriving under a namespace this build has never heard of is a new chain, not
81
+ // a malformed one.
82
+ //
83
+ // Deliberately looser than the spec on namespace length too. CAIP-2 caps it at
84
+ // 8 characters; `hypercore` is 9, and that spelling is canonical for us across
85
+ // the orchestrator, the SDK and deposit-service. Tightening this to the spec
86
+ // would fail the build on our own established id, so the bound is the org's,
87
+ // not the spec's.
88
+ const CAIP2 = /^[-a-z0-9]{3,16}:[-_a-zA-Z0-9]{1,32}$/;
75
89
  const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
76
90
  const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
77
91
  const isNonNegativeInt = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
@@ -107,8 +121,26 @@ function structuralProblems(chain) {
107
121
  if (!STACKS.has(chain.stack)) {
108
122
  problems.push(`stack ${JSON.stringify(chain.stack)} is not one of ${[...STACKS].join(', ')}`);
109
123
  }
110
- if (!isAbsentOr(chain.caip2, isNonEmptyString))
111
- problems.push('caip2 is not a string');
124
+ // Same deal as `network` and `stack` for EVM chains, where it is derived
125
+ // rather than authored — but unlike those two it is authored in the jsonnet
126
+ // for every chain whose id is not an EIP-155 one. Required either way: it is
127
+ // the id consumers key a chain by on the wire, and until it was always
128
+ // present each of them re-derived `eip155:<id>` for itself.
129
+ if (!CAIP2.test(chain.caip2)) {
130
+ problems.push(`caip2 ${JSON.stringify(chain.caip2)} is not a "<namespace>:<reference>" identifier`);
131
+ }
132
+ // Nullable, but the key has to be present: `null` is the answer for a chain
133
+ // with no explorer, and absent is not an answer at all. Stamped by
134
+ // classifyChainExplorers rather than authored for most chains, and checked
135
+ // here for the same reason as `network` and `stack` — this is the gate the
136
+ // published artifact passes through, and `ChainEntry` types consumers as
137
+ // though the key is always there.
138
+ if (!('explorer' in chain)) {
139
+ problems.push('explorer is missing (use null for a chain with no explorer)');
140
+ }
141
+ else if (chain.explorer !== null && !isChainExplorer(chain.explorer)) {
142
+ problems.push(`explorer ${JSON.stringify(chain.explorer)} is not null or { url (no trailing slash), addressPath, txPath (both slash-wrapped) }`);
143
+ }
112
144
  if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
113
145
  problems.push('virtual is not a boolean');
114
146
  }
@@ -288,6 +320,185 @@ function classifyChainNetworks(chains, mainnets, testnets) {
288
320
  }
289
321
  return { chains: classified, issues };
290
322
  }
323
+ /**
324
+ * Stamps every chain with its canonical CAIP-2 id, deriving `eip155:<chainId>`
325
+ * for the chains that do not declare one.
326
+ *
327
+ * Why this exists: `caip2` was authored only for the chains whose id is not an
328
+ * EIP-155 one (Solana, Tron, HyperCore), so every consumer that wanted a CAIP-2
329
+ * id had to carry the same `entry.caip2 ?? \`eip155:${id}\`` fallback — the
330
+ * orchestrator, deposit-service, the SDK and the indexers each had their own.
331
+ * A fallback repeated in five places is a rule with five chances to drift, and
332
+ * the one place it is genuinely load-bearing (HyperCore, whose `eip155:1337`
333
+ * spelling is wrong) is exactly where getting it wrong is silent.
334
+ *
335
+ * Derived rather than authored per chain for the same reason `network` is: an
336
+ * EVM chain's CAIP-2 id is a restatement of its chain id, so authoring it would
337
+ * only create a second copy that could disagree with the first.
338
+ *
339
+ * Uniqueness is checked because `chainIdFromCaip2` is a reverse lookup over this
340
+ * field — two chains sharing an id makes that lookup silently return whichever
341
+ * came first.
342
+ */
343
+ function classifyChainCaip2(chains) {
344
+ const issues = [];
345
+ const classified = {};
346
+ const seen = new Map();
347
+ for (const [chainId, chain] of Object.entries(chains)) {
348
+ const chainName = isNonEmptyString(chain?.name)
349
+ ? chain.name
350
+ : '<unnamed>';
351
+ const declared = chain?.caip2;
352
+ // A malformed declaration is left exactly as authored rather than replaced
353
+ // with the derived form: quietly publishing `eip155:1337` for HyperCore is
354
+ // the precise mistake this field exists to prevent, so the structural check
355
+ // reports the real value instead.
356
+ if (declared !== undefined && !(typeof declared === 'string' && CAIP2.test(declared))) {
357
+ issues.push({
358
+ severity: 'error',
359
+ chainId,
360
+ chainName,
361
+ message: `declares a malformed caip2 ${JSON.stringify(declared)} — expected "<namespace>:<reference>"`,
362
+ });
363
+ classified[chainId] = withSortedKeys({ ...chain });
364
+ continue;
365
+ }
366
+ // A real EVM chain — and only a real EVM chain — has an id that is
367
+ // definitionally `eip155:<chainId>`. Everything else is addressed by an id
368
+ // the registry key cannot express, so its caip2 has to be authored.
369
+ const derivable = chain.vmType === 'evm' && chain.virtual !== true;
370
+ const derived = `eip155:${chainId}`;
371
+ // Deriving for a non-EVM or virtual entry would hand it an `eip155:` id that
372
+ // is confidently wrong — the same failure as HyperCore's `eip155:1337`,
373
+ // reintroduced by the very thing meant to prevent it. So the next svm/tvm or
374
+ // virtual chain added without an authored caip2 fails the build here rather
375
+ // than reaching the artifact under a wire id nothing uses.
376
+ if (declared === undefined && !derivable) {
377
+ issues.push({
378
+ severity: 'error',
379
+ chainId,
380
+ chainName,
381
+ message: `is ${chain.virtual === true ? 'a virtual chain' : `vmType ${JSON.stringify(chain.vmType)}`} and must author its caip2 — only a non-virtual EVM chain can derive ${JSON.stringify(derived)} from its registry key`,
382
+ });
383
+ classified[chainId] = withSortedKeys({ ...chain });
384
+ continue;
385
+ }
386
+ const caip2 = declared === undefined ? derived : declared;
387
+ // Conversely, a declaration that disagrees with the derivable form is a typo
388
+ // rather than an override.
389
+ if (declared !== undefined && derivable && declared !== derived) {
390
+ issues.push({
391
+ severity: 'error',
392
+ chainId,
393
+ chainName,
394
+ message: `declares caip2 ${JSON.stringify(declared)} but is a non-virtual EVM chain, whose canonical id is ${JSON.stringify(derived)}`,
395
+ });
396
+ }
397
+ const collision = seen.get(caip2);
398
+ if (collision !== undefined) {
399
+ issues.push({
400
+ severity: 'error',
401
+ chainId,
402
+ chainName,
403
+ message: `resolves to caip2 ${JSON.stringify(caip2)}, which chain ${collision} already uses — a reverse lookup cannot tell them apart`,
404
+ });
405
+ }
406
+ else {
407
+ seen.set(caip2, chainId);
408
+ }
409
+ classified[chainId] = withSortedKeys({ ...chain, caip2 });
410
+ }
411
+ return { chains: classified, issues };
412
+ }
413
+ // Near-universal across EVM explorers (Etherscan, Blockscout and the -scan
414
+ // forks all use these). Tronscan is the exception, and authors its own.
415
+ const DEFAULT_ADDRESS_PATH = '/address/';
416
+ const DEFAULT_TX_PATH = '/tx/';
417
+ // The slash convention is enforced, not assumed: a link is built by plain
418
+ // concatenation, so a path missing its leading slash or a url carrying a
419
+ // trailing one produces a subtly wrong URL rather than an obviously broken one.
420
+ function isChainExplorer(value) {
421
+ const hasSlashes = (v) => typeof v === 'string' && /^\/.*\/$/.test(v);
422
+ return (isObject(value) &&
423
+ typeof value.url === 'string' &&
424
+ value.url.length > 0 &&
425
+ !value.url.endsWith('/') &&
426
+ hasSlashes(value.addressPath) &&
427
+ hasSlashes(value.txPath));
428
+ }
429
+ /**
430
+ * Stamps every chain with its block explorer, derived from the chain's viem
431
+ * definition where it has one and authored in the jsonnet where it does not.
432
+ *
433
+ * Resolved by the entry's declared `viemChain` **name**, never by chain id. A
434
+ * lookup by id is actively wrong here: viem's chain 1337 is `localhost`, so
435
+ * HyperCore — whose registry key is 1337 — would silently inherit Localhost's
436
+ * metadata. That is the same mistake as addressing HyperCore by `eip155:1337`,
437
+ * arrived at from the other direction.
438
+ *
439
+ * A missing explorer is not an error. It resolves to `null`, which consumers
440
+ * render as plain text instead of a link; the cost of not knowing one is a
441
+ * missing hyperlink, not a wrong answer. It is still worth a warning, because
442
+ * the usual cause is a chain added without one rather than a chain that
443
+ * genuinely has none.
444
+ */
445
+ function classifyChainExplorers(chains, networks) {
446
+ const issues = [];
447
+ const stamped = {};
448
+ const entriesById = new Map();
449
+ for (const entry of [...networks.mainnets, ...networks.testnets]) {
450
+ entriesById.set(String(entry.id), entry);
451
+ }
452
+ for (const [chainId, chain] of Object.entries(chains)) {
453
+ const chainName = isNonEmptyString(chain?.name)
454
+ ? chain.name
455
+ : '<unnamed>';
456
+ const authored = chain?.explorer;
457
+ // Authoring wins outright, unlike `stack` where viem's definition is what
458
+ // the consumer actually uses. An explorer is presentation: the jsonnet is
459
+ // the only place a non-default path (Tronscan's `/#/address/`) or a
460
+ // preferred explorer among several can be expressed.
461
+ if (authored !== undefined) {
462
+ if (!isChainExplorer(authored)) {
463
+ issues.push({
464
+ severity: 'error',
465
+ chainId,
466
+ chainName,
467
+ message: `declares a malformed explorer ${JSON.stringify(authored)} — expected { url, addressPath, txPath }`,
468
+ });
469
+ stamped[chainId] = withSortedKeys({ ...chain });
470
+ continue;
471
+ }
472
+ stamped[chainId] = withSortedKeys({ ...chain, explorer: authored });
473
+ continue;
474
+ }
475
+ const viemChainName = entriesById.get(chainId)?.viemChain;
476
+ const url = viemChainName
477
+ ? viemChains[viemChainName]?.blockExplorers?.default?.url
478
+ : undefined;
479
+ if (typeof url !== 'string' || url.length === 0) {
480
+ issues.push({
481
+ severity: 'warning',
482
+ chainId,
483
+ chainName,
484
+ message: viemChainName
485
+ ? `viem chain "${viemChainName}" ships no block explorer — links will render as plain text until one is authored`
486
+ : 'has no viemChain to derive a block explorer from and authors none — links will render as plain text',
487
+ });
488
+ stamped[chainId] = withSortedKeys({ ...chain, explorer: null });
489
+ continue;
490
+ }
491
+ stamped[chainId] = withSortedKeys({
492
+ ...chain,
493
+ explorer: {
494
+ url: url.replace(/\/+$/, ''),
495
+ addressPath: DEFAULT_ADDRESS_PATH,
496
+ txPath: DEFAULT_TX_PATH,
497
+ },
498
+ });
499
+ }
500
+ return { chains: stamped, issues };
501
+ }
291
502
  const isChainStack = (v) => typeof v === 'string' && STACKS.has(v);
292
503
  const STACK_FORMATTERS = [
293
504
  ['op-stack', op_stack_1.chainConfig.formatters],
@@ -49,9 +49,19 @@ function bumpedVersion() {
49
49
  const [major, minor, patch] = packageVersion_1.PACKAGE_VERSION.split("-")[0].split(".");
50
50
  return `${major}.${minor}.${Number(patch) + 1}`;
51
51
  }
52
+ // Must produce a genuinely older version for every PACKAGE_VERSION, including
53
+ // the `x.y.0` releases where decrementing the patch has nowhere to go — clamping
54
+ // to 0 there returned PACKAGE_VERSION itself, which the client accepts as the
55
+ // steady state, so the staleness tests asserted the opposite of what they read.
52
56
  function olderVersion() {
53
- const [major, minor, patch] = packageVersion_1.PACKAGE_VERSION.split("-")[0].split(".");
54
- return `${major}.${minor}.${Math.max(0, Number(patch) - 1)}`;
57
+ const [major, minor, patch] = packageVersion_1.PACKAGE_VERSION.split("-")[0]
58
+ .split(".")
59
+ .map(Number);
60
+ if (patch > 0)
61
+ return `${major}.${minor}.${patch - 1}`;
62
+ if (minor > 0)
63
+ return `${major}.${minor - 1}.0`;
64
+ return `${major - 1}.0.0`;
55
65
  }
56
66
  function withEthereum(overrides) {
57
67
  return {
@@ -30,10 +30,16 @@ const TRON_CAIP2 = "tron:mainnet";
30
30
  (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)(SOLANA_CAIP2)).toBe(index_1.MainnetNetwork.SOLANA);
31
31
  (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)(TRON_CAIP2)).toBe(index_1.MainnetNetwork.TRON);
32
32
  });
33
- (0, bun_test_1.it)("returns undefined for unknown / eip155 caip2 strings", () => {
34
- // eip155 references are not registry-backed; callers parse them numerically.
35
- (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("eip155:1")).toBeUndefined();
33
+ (0, bun_test_1.it)("resolves eip155 caip2 strings, now that every entry carries one", () => {
34
+ (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("eip155:1")).toBe(index_1.MainnetNetwork.ETHEREUM);
35
+ (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("eip155:999")).toBe(index_1.MainnetNetwork.HYPEREVM);
36
+ });
37
+ (0, bun_test_1.it)("returns undefined for ids no chain uses", () => {
38
+ // 1337 is HyperCore's *registry key*, but its caip2 is `hypercore:mainnet`
39
+ // — so the eip155 spelling resolves to nothing, which is the point of
40
+ // reading the field instead of deriving it.
36
41
  (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("eip155:1337")).toBeUndefined();
42
+ (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("eip155:999999999")).toBeUndefined();
37
43
  (0, bun_test_1.expect)((0, index_1.chainIdFromCaip2)("hypercore:testnet")).toBeUndefined();
38
44
  });
39
45
  });
@@ -9,17 +9,19 @@ export declare function isNonEvmChainId(chainId: number): boolean;
9
9
  */
10
10
  export declare function getNonEvmChainIds(): number[];
11
11
  /**
12
- * Canonical CAIP-2 string for a chain id. Returns the registry-declared
13
- * `caip2` when present (non-EVM entries today), otherwise falls back to the
14
- * `eip155:<chainId>` form. Throws nothing returns the fallback for any
15
- * unknown id so callers don't need to special-case absent entries.
12
+ * Canonical CAIP-2 string for a chain id, read straight off the registry
13
+ * every entry carries one (see `classifyChainCaip2`). Falls back to the
14
+ * `eip155:<chainId>` form for an id the registry does not know, so callers
15
+ * don't need to special-case an absent entry.
16
16
  */
17
17
  export declare function getCaip2(chainId: number): string;
18
18
  /**
19
- * Reverse lookup — find the numeric chain id whose registry-declared
20
- * `caip2` matches the input, or `undefined` if no entry has it. EVM chains
21
- * (which omit `caip2` in the registry) are NOT covered by this lookup;
22
- * callers parse `eip155:N` references on their own.
19
+ * Reverse lookup — the numeric chain id addressed by a CAIP-2 string, or
20
+ * `undefined` if no registry entry uses it.
21
+ *
22
+ * Covers every chain, EVM included: `caip2` is present on all entries, and the
23
+ * generator rejects two chains resolving to the same one, so this is
24
+ * unambiguous.
23
25
  */
24
26
  export declare function chainIdFromCaip2(caip2: string): number | undefined;
25
27
  //# sourceMappingURL=chainVirtualization.d.ts.map