@rhinestone/shared-configs 1.7.16 → 1.8.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.
@@ -1,4 +1,17 @@
1
- export type SettlementLayer = 'ACROSS' | 'ECO' | 'RELAY' | 'OFT';
1
+ export type SettlementLayer = 'ACROSS' | 'ECO' | 'RELAY' | 'OFT' | 'NEAR' | 'RHINO' | 'CCTP';
2
+ export type ChainNetwork = 'mainnet' | 'testnet';
3
+ /**
4
+ * Which execution stack a chain runs, for consumers that must build a viem
5
+ * `Chain` object themselves.
6
+ *
7
+ * The value set is deliberately "the stacks viem ships a `chainConfig` for",
8
+ * not a taxonomy of L2 architectures: the only thing a consumer does with this
9
+ * is pick the matching `chainConfig` for its formatters/serializers. Arbitrum
10
+ * Orbit is absent for that reason — viem's own `arbitrum` carries no
11
+ * formatters, fees or serializers, so an Orbit chain is correctly served as
12
+ * `vanilla` and an `orbit` value would promise behaviour viem does not have.
13
+ */
14
+ export type ChainStack = 'op-stack' | 'zksync' | 'celo' | 'vanilla';
2
15
  export interface ChainData {
3
16
  name: string;
4
17
  settlementLayers: SettlementLayer[];
@@ -6,6 +19,11 @@ export interface ChainData {
6
19
  symbol: string;
7
20
  [key: string]: unknown;
8
21
  }>;
22
+ /** Whether this chain is a mainnet or a testnet. Stamped by
23
+ * `classifyChainNetworks`, not authored in the jsonnet. */
24
+ network?: ChainNetwork;
25
+ /** Execution stack. Stamped by `classifyChainStacks`. */
26
+ stack?: ChainStack;
9
27
  /** Virtual chains (e.g. HyperCore) settle on another chain and have no
10
28
  * direct settlement layers of their own — excluded from reachability checks. */
11
29
  virtual?: boolean;
@@ -24,9 +42,67 @@ export interface ValidationResult {
24
42
  hasErrors: boolean;
25
43
  }
26
44
  /**
27
- * Validates a chain registry for configuration problems.
45
+ * Stamps every chain with its mainnet/testnet classification, derived from which
46
+ * network map its chain id appears in.
47
+ *
48
+ * Why this exists: the split is otherwise implicit in *which file* an id lands in
49
+ * (configs/mainnets.json vs configs/testnets.json). The package surfaces it as
50
+ * `mainnetChains` / `MainnetNetwork`, but configs/chains.json — the registry the
51
+ * facts artifact publishes and the read-client bundles — carried no trace of it.
52
+ * So a consumer reading facts at runtime could not tell which environment a chain
53
+ * belonged to, which is exactly what the orchestrator's mainnet/testnet registry
54
+ * gating keys off: a CDN-added chain would arrive unclassifiable.
55
+ *
56
+ * Derived rather than authored per chain in the jsonnet, so it cannot drift from
57
+ * the membership it describes — "is this a testnet" keeps one source, the network
58
+ * maps.
59
+ *
60
+ * Lives in this module rather than generate.ts because the partition it relies on
61
+ * is a build-time invariant that has to fail the build, and generate.ts runs its
62
+ * generator on import, so nothing declared there is testable.
63
+ */
64
+ export declare function classifyChainNetworks(chains: ChainRegistry, mainnets: Record<string, unknown>, testnets: Record<string, unknown>): {
65
+ chains: ChainRegistry;
66
+ issues: ValidationIssue[];
67
+ };
68
+ /** The subset of a generated network entry that stack classification needs. */
69
+ export interface StackNetworkEntry {
70
+ id: number;
71
+ /** Absent for non-EVM and virtual chains, and for any chain viem does not ship. */
72
+ viemChain?: string;
73
+ /** Authored in config/shared-configs.jsonnet. Only meaningful — and only
74
+ * permitted to decide the outcome — when there is no `viemChain`. */
75
+ stack?: string;
76
+ }
77
+ /**
78
+ * Stamps `stack` onto every registry entry.
79
+ *
80
+ * Derived from viem wherever viem defines the chain, which is every EVM chain
81
+ * today, so the common case is authored nowhere and cannot drift. The jsonnet's
82
+ * `stack` is the escape hatch for the case that motivates the field at all: a
83
+ * chain viem has never heard of, which a consumer has to synthesise with
84
+ * `defineChain` and therefore needs told which `chainConfig` to apply.
85
+ *
86
+ * For such a chain the field is **required, not defaulted**. Defaulting to
87
+ * `vanilla` would make the single most likely new chain — an OP-stack L2 —
88
+ * synthesise without OP-stack formatters, so its L1 fee fields would be missing
89
+ * and its gas silently mispriced. Today's behaviour for an unknown chain is a
90
+ * loud skip; a silent misprice would be strictly worse, so the decision is
91
+ * forced here, where a human is adding the chain anyway.
92
+ */
93
+ export declare function classifyChainStacks(chains: ChainRegistry, networks: {
94
+ mainnets: StackNetworkEntry[];
95
+ testnets: StackNetworkEntry[];
96
+ }): {
97
+ chains: ChainRegistry;
98
+ issues: ValidationIssue[];
99
+ };
100
+ /**
101
+ * Validates a chain registry for structural and configuration problems.
28
102
  *
29
103
  * Checks performed:
104
+ * - Structure: chain-id keys, and every field of every entry against the
105
+ * shapes and unions declared in src/types.ts (error)
30
106
  * - Orphaned chains: chains with no settlement layers (error)
31
107
  * - Single-layer fragility: chains with exactly one settlement layer (warning)
32
108
  */
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../scripts/validation.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,KAAK,CAAC;AAEjE,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;oFACgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,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;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,gBAAgB,CAkC3E;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":"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,23 +1,461 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.classifyChainNetworks = classifyChainNetworks;
37
+ exports.classifyChainStacks = classifyChainStacks;
3
38
  exports.validateChainConfig = validateChainConfig;
4
39
  exports.formatValidationIssues = formatValidationIssues;
40
+ const celo_1 = require("viem/celo");
41
+ const viemChains = __importStar(require("viem/chains"));
42
+ const op_stack_1 = require("viem/op-stack");
43
+ const zksync_1 = require("viem/zksync");
44
+ // Runtime mirrors of the string unions in src/types.ts. Keeping these in sync
45
+ // is a build-time concern only: an unrecognised value here means the jsonnet
46
+ // introduced a layer/quoter/vm the TS unions don't know about yet, which SHOULD
47
+ // fail the build — consumers switch on these, so the union has to be updated in
48
+ // the same change. (Read-side clients deliberately do NOT check enum values:
49
+ // there, an unknown value means an older consumer is reading a newer artifact,
50
+ // which must keep working.)
51
+ const SETTLEMENT_LAYERS = new Set([
52
+ 'ACROSS',
53
+ 'ECO',
54
+ 'RELAY',
55
+ 'OFT',
56
+ 'NEAR',
57
+ 'RHINO',
58
+ 'CCTP',
59
+ ]);
60
+ const SWAP_QUOTERS = new Set([
61
+ '1inch',
62
+ '0x',
63
+ 'velora',
64
+ 'kyberswap',
65
+ 'fynd',
66
+ ]);
67
+ const VM_TYPES = new Set(['evm', 'svm', 'tvm']);
68
+ const PEG_GROUPS = new Set(['USD', 'ETH']);
69
+ const NETWORKS = new Set(['mainnet', 'testnet']);
70
+ const STACKS = new Set(['op-stack', 'zksync', 'celo', 'vanilla']);
71
+ // Registry keys are chain ids consumers index by directly, so they must be
72
+ // canonical positive integers — no leading zeros, which would let "01" and "1"
73
+ // both address chain 1.
74
+ const CHAIN_ID_KEY = /^[1-9][0-9]*$/;
75
+ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
76
+ const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
77
+ const isNonNegativeInt = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
78
+ const isAbsentOr = (v, check) => v === undefined || check(v);
79
+ /**
80
+ * Structural checks on one chain entry, returning human-readable problems.
81
+ *
82
+ * This is the exhaustive validation: it runs once, here, against data we just
83
+ * generated and against unions we ship in the same version. Read-side clients
84
+ * only sanity-check the envelope, so this is the single place a malformed
85
+ * registry gets caught before it reaches anyone.
86
+ */
87
+ function structuralProblems(chain) {
88
+ const problems = [];
89
+ if (!isNonEmptyString(chain.name))
90
+ problems.push('name is not a non-empty string');
91
+ if (!VM_TYPES.has(chain.vmType)) {
92
+ problems.push(`unknown vmType ${JSON.stringify(chain.vmType)}`);
93
+ }
94
+ // Required, and the only field here the jsonnet does not author — it is
95
+ // stamped by classifyChainNetworks. Checked anyway, because this is the gate
96
+ // the published artifact passes through: consumers gate their mainnet vs
97
+ // testnet registry on it, so an entry reaching the CDN without it would be
98
+ // unclassifiable at runtime with no way to recover.
99
+ if (!NETWORKS.has(chain.network)) {
100
+ problems.push(`network ${JSON.stringify(chain.network)} is not "mainnet" or "testnet"`);
101
+ }
102
+ // Same deal as `network`: stamped by classifyChainStacks, not authored, and
103
+ // checked here anyway because this is the gate the published artifact passes
104
+ // through. A consumer synthesising a chain object reads this to pick viem's
105
+ // matching chainConfig, so an entry reaching the CDN without it would be
106
+ // built with vanilla formatters — silently mispriced rather than absent.
107
+ if (!STACKS.has(chain.stack)) {
108
+ problems.push(`stack ${JSON.stringify(chain.stack)} is not one of ${[...STACKS].join(', ')}`);
109
+ }
110
+ if (!isAbsentOr(chain.caip2, isNonEmptyString))
111
+ problems.push('caip2 is not a string');
112
+ if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
113
+ problems.push('virtual is not a boolean');
114
+ }
115
+ for (const field of ['nativeToken', 'wrappedNativeToken']) {
116
+ const token = chain[field];
117
+ if (!isObject(token)) {
118
+ problems.push(`${field} is not an object`);
119
+ continue;
120
+ }
121
+ if (typeof token.address !== 'string')
122
+ problems.push(`${field}.address is not a string`);
123
+ if (!isNonEmptyString(token.symbol))
124
+ problems.push(`${field}.symbol is not a non-empty string`);
125
+ if (!isNonNegativeInt(token.decimals))
126
+ problems.push(`${field}.decimals is not a non-negative integer`);
127
+ }
128
+ if (!Array.isArray(chain.tokens)) {
129
+ problems.push('tokens is not an array');
130
+ }
131
+ else {
132
+ chain.tokens.forEach((token, i) => {
133
+ const at = `tokens[${i}]`;
134
+ if (!isObject(token)) {
135
+ problems.push(`${at} is not an object`);
136
+ return;
137
+ }
138
+ if (typeof token.address !== 'string')
139
+ problems.push(`${at}.address is not a string`);
140
+ if (!isNonEmptyString(token.symbol))
141
+ problems.push(`${at}.symbol is not a non-empty string`);
142
+ if (!isNonNegativeInt(token.decimals))
143
+ problems.push(`${at}.decimals is not a non-negative integer`);
144
+ // Nullable, but the key must be present — consumers read it directly.
145
+ for (const slot of ['balanceSlot', 'approvalSlot']) {
146
+ if (!(token[slot] === null || isNonNegativeInt(token[slot]))) {
147
+ problems.push(`${at}.${slot} is not a non-negative integer or null`);
148
+ }
149
+ }
150
+ if (!isAbsentOr(token.priceSymbol, isNonEmptyString))
151
+ problems.push(`${at}.priceSymbol is not a string`);
152
+ if (!isAbsentOr(token.unpriced, (v) => typeof v === 'boolean'))
153
+ problems.push(`${at}.unpriced is not a boolean`);
154
+ if (!isAbsentOr(token.pegGroup, (v) => PEG_GROUPS.has(v))) {
155
+ problems.push(`${at}.pegGroup ${JSON.stringify(token.pegGroup)} is not a known peg group`);
156
+ }
157
+ });
158
+ }
159
+ for (const [field, allowed] of [
160
+ ['settlementLayers', SETTLEMENT_LAYERS],
161
+ ['swapQuoters', SWAP_QUOTERS],
162
+ ]) {
163
+ const values = chain[field];
164
+ if (!Array.isArray(values)) {
165
+ problems.push(`${field} is not an array`);
166
+ continue;
167
+ }
168
+ for (const value of values) {
169
+ if (!allowed.has(value)) {
170
+ problems.push(`${field} contains unknown value ${JSON.stringify(value)} — add it to the union in src/types.ts and to validation.ts`);
171
+ }
172
+ }
173
+ }
174
+ const quoterConfig = chain.swapQuoterConfig;
175
+ if (quoterConfig !== undefined) {
176
+ if (!isObject(quoterConfig)) {
177
+ problems.push('swapQuoterConfig is not an object');
178
+ }
179
+ else {
180
+ for (const [quoter, config] of Object.entries(quoterConfig)) {
181
+ if (!SWAP_QUOTERS.has(quoter)) {
182
+ problems.push(`swapQuoterConfig has unknown quoter ${JSON.stringify(quoter)}`);
183
+ }
184
+ if (!isObject(config)) {
185
+ problems.push(`swapQuoterConfig.${quoter} is not an object`);
186
+ }
187
+ else if (!isAbsentOr(config.slug, isNonEmptyString)) {
188
+ problems.push(`swapQuoterConfig.${quoter}.slug is not a string`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+ return problems;
194
+ }
5
195
  /**
6
- * Validates a chain registry for configuration problems.
196
+ * Reinserts an entry's keys in sorted order.
197
+ *
198
+ * jsonnet emits its objects key-sorted, so appending a field would leave the one
199
+ * generated file that gets signed and published non-canonical — the same logical
200
+ * registry serialising differently depending on how the generator happened to
201
+ * build the object. Only the top level needs it; nested objects come through
202
+ * untouched and already sorted.
203
+ */
204
+ function withSortedKeys(entry) {
205
+ return Object.fromEntries(Object.entries(entry).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
206
+ }
207
+ /**
208
+ * Stamps every chain with its mainnet/testnet classification, derived from which
209
+ * network map its chain id appears in.
210
+ *
211
+ * Why this exists: the split is otherwise implicit in *which file* an id lands in
212
+ * (configs/mainnets.json vs configs/testnets.json). The package surfaces it as
213
+ * `mainnetChains` / `MainnetNetwork`, but configs/chains.json — the registry the
214
+ * facts artifact publishes and the read-client bundles — carried no trace of it.
215
+ * So a consumer reading facts at runtime could not tell which environment a chain
216
+ * belonged to, which is exactly what the orchestrator's mainnet/testnet registry
217
+ * gating keys off: a CDN-added chain would arrive unclassifiable.
218
+ *
219
+ * Derived rather than authored per chain in the jsonnet, so it cannot drift from
220
+ * the membership it describes — "is this a testnet" keeps one source, the network
221
+ * maps.
222
+ *
223
+ * Lives in this module rather than generate.ts because the partition it relies on
224
+ * is a build-time invariant that has to fail the build, and generate.ts runs its
225
+ * generator on import, so nothing declared there is testable.
226
+ */
227
+ function classifyChainNetworks(chains, mainnets, testnets) {
228
+ const issues = [];
229
+ const classified = {};
230
+ for (const [chainId, chain] of Object.entries(chains)) {
231
+ const chainName = isNonEmptyString(chain?.name)
232
+ ? chain.name
233
+ : '<unnamed>';
234
+ const inMainnets = Object.hasOwn(mainnets, chainId);
235
+ const inTestnets = Object.hasOwn(testnets, chainId);
236
+ // Anything the input already carries under `network` is dropped rather than
237
+ // trusted: the field is present if and only if it was derived here, from the
238
+ // maps. That holds on the failure paths too, which is what keeps the
239
+ // structural check in validateChainConfig an independent second gate — it
240
+ // rejects an unclassified chain regardless of what the jsonnet emitted.
241
+ const base = { ...chain };
242
+ delete base.network;
243
+ if (inMainnets && inTestnets) {
244
+ // Nothing downstream can resolve this, and the pre-existing provider merge
245
+ // (`{...mainnets, ...testnets}`) would silently settle it as a testnet.
246
+ issues.push({
247
+ severity: 'error',
248
+ chainId,
249
+ chainName,
250
+ message: 'listed in both configs/mainnets.json and configs/testnets.json — cannot be classified',
251
+ });
252
+ classified[chainId] = withSortedKeys(base);
253
+ continue;
254
+ }
255
+ if (!inMainnets && !inTestnets) {
256
+ issues.push({
257
+ severity: 'error',
258
+ chainId,
259
+ chainName,
260
+ message: 'listed in neither configs/mainnets.json nor configs/testnets.json — cannot be classified as mainnet or testnet',
261
+ });
262
+ classified[chainId] = withSortedKeys(base);
263
+ continue;
264
+ }
265
+ classified[chainId] = withSortedKeys({
266
+ ...base,
267
+ network: inMainnets ? 'mainnet' : 'testnet',
268
+ });
269
+ }
270
+ // The reverse direction: providers configured for a chain the registry does not
271
+ // define. Harmless today (every consumer iterates the registry, not the network
272
+ // maps) but it is the same jsonnet drift that makes a chain silently absent, so
273
+ // it is worth saying out loud rather than leaving to be discovered.
274
+ for (const [mapName, map] of [
275
+ ['configs/mainnets.json', mainnets],
276
+ ['configs/testnets.json', testnets],
277
+ ]) {
278
+ for (const chainId of Object.keys(map)) {
279
+ if (!Object.hasOwn(chains, chainId)) {
280
+ issues.push({
281
+ severity: 'warning',
282
+ chainId,
283
+ chainName: '<not in registry>',
284
+ message: `listed in ${mapName} but absent from the chain registry`,
285
+ });
286
+ }
287
+ }
288
+ }
289
+ return { chains: classified, issues };
290
+ }
291
+ const isChainStack = (v) => typeof v === 'string' && STACKS.has(v);
292
+ const STACK_FORMATTERS = [
293
+ ['op-stack', op_stack_1.chainConfig.formatters],
294
+ ['zksync', zksync_1.chainConfig.formatters],
295
+ ['celo', celo_1.chainConfig.formatters],
296
+ ];
297
+ /**
298
+ * Reads the stack straight off viem's own chain definition.
299
+ *
300
+ * viem builds a stack-specific chain by spreading its `chainConfig`, so the
301
+ * `formatters` object is shared by reference with the config it came from —
302
+ * comparing identity is exact, and needs no list of chain names to maintain.
303
+ */
304
+ function stackFromViem(viemChainName) {
305
+ const chain = viemChains[viemChainName];
306
+ if (!isObject(chain)) {
307
+ return { problem: `viem/chains has no export "${viemChainName}"` };
308
+ }
309
+ const formatters = chain.formatters;
310
+ if (!formatters)
311
+ return { stack: 'vanilla' };
312
+ for (const [stack, reference] of STACK_FORMATTERS) {
313
+ if (formatters === reference)
314
+ return { stack };
315
+ }
316
+ // viem gave this chain custom formatters from a config this generator does not
317
+ // know about — a new stack in a viem upgrade. Failing here is the point: the
318
+ // alternative is publishing it as `vanilla`, which reads as "no special
319
+ // handling needed" when the opposite was just established.
320
+ return {
321
+ problem: `viem's "${viemChainName}" carries formatters from a chainConfig this generator ` +
322
+ 'does not recognise — add it to STACK_FORMATTERS and to the ChainStack union',
323
+ };
324
+ }
325
+ /**
326
+ * Stamps `stack` onto every registry entry.
327
+ *
328
+ * Derived from viem wherever viem defines the chain, which is every EVM chain
329
+ * today, so the common case is authored nowhere and cannot drift. The jsonnet's
330
+ * `stack` is the escape hatch for the case that motivates the field at all: a
331
+ * chain viem has never heard of, which a consumer has to synthesise with
332
+ * `defineChain` and therefore needs told which `chainConfig` to apply.
333
+ *
334
+ * For such a chain the field is **required, not defaulted**. Defaulting to
335
+ * `vanilla` would make the single most likely new chain — an OP-stack L2 —
336
+ * synthesise without OP-stack formatters, so its L1 fee fields would be missing
337
+ * and its gas silently mispriced. Today's behaviour for an unknown chain is a
338
+ * loud skip; a silent misprice would be strictly worse, so the decision is
339
+ * forced here, where a human is adding the chain anyway.
340
+ */
341
+ function classifyChainStacks(chains, networks) {
342
+ const issues = [];
343
+ const stamped = {};
344
+ const entriesById = new Map();
345
+ for (const entry of [...networks.mainnets, ...networks.testnets]) {
346
+ entriesById.set(String(entry.id), entry);
347
+ }
348
+ for (const [chainId, chain] of Object.entries(chains)) {
349
+ const chainName = isNonEmptyString(chain?.name)
350
+ ? chain.name
351
+ : '<unnamed>';
352
+ // Same rule as `network`: present if and only if derived here, including on
353
+ // the failure paths, so the structural check stays an independent gate.
354
+ const base = { ...chain };
355
+ delete base.stack;
356
+ const entry = entriesById.get(chainId);
357
+ const fail = (message) => {
358
+ issues.push({ severity: 'error', chainId, chainName, message });
359
+ stamped[chainId] = withSortedKeys(base);
360
+ };
361
+ let authored;
362
+ if (entry?.stack !== undefined) {
363
+ if (!isChainStack(entry.stack)) {
364
+ fail(`stack ${JSON.stringify(entry.stack)} is not one of ${[...STACKS].join(', ')}`);
365
+ continue;
366
+ }
367
+ authored = entry.stack;
368
+ }
369
+ if (entry?.viemChain) {
370
+ const { stack, problem } = stackFromViem(entry.viemChain);
371
+ if (problem || !stack) {
372
+ fail(problem ?? 'stack could not be derived from viem');
373
+ continue;
374
+ }
375
+ // Authoring is allowed to agree, never to override: viem's definition is
376
+ // what the consumer actually uses for a chain viem ships.
377
+ if (authored !== undefined && authored !== stack) {
378
+ fail(`stack ${JSON.stringify(authored)} contradicts viem's own "${entry.viemChain}", ` +
379
+ `which is ${JSON.stringify(stack)} — drop the override or fix it`);
380
+ continue;
381
+ }
382
+ stamped[chainId] = withSortedKeys({ ...base, stack });
383
+ continue;
384
+ }
385
+ const isEvm = chain.vmType === undefined || chain.vmType === 'evm';
386
+ const isVirtual = chain.virtual === true;
387
+ if (isEvm && !isVirtual) {
388
+ if (authored === undefined) {
389
+ fail('is an EVM chain with no viem chain entry, so consumers must synthesise it — ' +
390
+ `author \`stack\` for it in config/shared-configs.jsonnet (one of ${[...STACKS].join(', ')}). ` +
391
+ 'It is not defaulted: publishing an OP-stack chain as "vanilla" would drop its ' +
392
+ 'L1 fee fields and misprice gas with no error anywhere');
393
+ continue;
394
+ }
395
+ stamped[chainId] = withSortedKeys({ ...base, stack: authored });
396
+ continue;
397
+ }
398
+ // Non-EVM (Solana, Tron) and virtual (HyperCore) chains are never built as
399
+ // viem chains, so the value is inert. Stamped anyway to keep the field
400
+ // required for every entry rather than optional-in-practice.
401
+ if (authored !== undefined) {
402
+ issues.push({
403
+ severity: 'warning',
404
+ chainId,
405
+ chainName,
406
+ message: `has an authored stack ${JSON.stringify(authored)} but is non-EVM or virtual, ` +
407
+ 'so it is never synthesised as a viem chain — the value is ignored',
408
+ });
409
+ }
410
+ stamped[chainId] = withSortedKeys({ ...base, stack: 'vanilla' });
411
+ }
412
+ return { chains: stamped, issues };
413
+ }
414
+ /**
415
+ * Validates a chain registry for structural and configuration problems.
7
416
  *
8
417
  * Checks performed:
418
+ * - Structure: chain-id keys, and every field of every entry against the
419
+ * shapes and unions declared in src/types.ts (error)
9
420
  * - Orphaned chains: chains with no settlement layers (error)
10
421
  * - Single-layer fragility: chains with exactly one settlement layer (warning)
11
422
  */
12
423
  function validateChainConfig(chains) {
13
424
  const issues = [];
14
425
  for (const [chainId, chain] of Object.entries(chains)) {
426
+ const chainName = isNonEmptyString(chain?.name)
427
+ ? chain.name
428
+ : '<unnamed>';
429
+ if (!CHAIN_ID_KEY.test(chainId)) {
430
+ issues.push({
431
+ severity: 'error',
432
+ chainId,
433
+ chainName,
434
+ message: 'key is not a canonical chain id (positive integer, no leading zeros)',
435
+ });
436
+ }
437
+ if (!isObject(chain)) {
438
+ issues.push({
439
+ severity: 'error',
440
+ chainId,
441
+ chainName,
442
+ message: 'entry is not an object',
443
+ });
444
+ continue;
445
+ }
446
+ for (const problem of structuralProblems(chain)) {
447
+ issues.push({ severity: 'error', chainId, chainName, message: problem });
448
+ }
15
449
  // Virtual chains (HyperCore) are reachable via their settlement chain, not
16
450
  // a direct settlement layer — the orphaned-chain check doesn't apply.
17
451
  if (chain.virtual) {
18
452
  continue;
19
453
  }
20
454
  const layers = chain.settlementLayers;
455
+ if (!Array.isArray(layers)) {
456
+ // Already reported structurally; skip the reachability checks below.
457
+ continue;
458
+ }
21
459
  if (layers.length === 0) {
22
460
  issues.push({
23
461
  severity: 'error',
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chainFactsClient.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chainFactsClient.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/chainFactsClient.test.ts"],"names":[],"mappings":""}