@rhinestone/shared-configs 1.7.15 → 1.7.17

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.
Files changed (34) hide show
  1. package/dist/configs/chains.json +27 -1
  2. package/dist/scripts/__tests__/validation.test.js +132 -10
  3. package/dist/scripts/generate.js +25 -5
  4. package/dist/scripts/validation.d.ts +32 -2
  5. package/dist/scripts/validation.d.ts.map +1 -1
  6. package/dist/scripts/validation.js +269 -1
  7. package/dist/src/__tests__/chainFactsClient.test.d.ts +2 -0
  8. package/dist/src/__tests__/chainFactsClient.test.d.ts.map +1 -0
  9. package/dist/src/__tests__/chainFactsClient.test.js +460 -0
  10. package/dist/src/chainFactsClient.d.ts +57 -0
  11. package/dist/src/chainFactsClient.d.ts.map +1 -0
  12. package/dist/src/chainFactsClient.js +295 -0
  13. package/dist/src/chains.d.ts +3 -1
  14. package/dist/src/chains.d.ts.map +1 -1
  15. package/dist/src/chains.js +27 -1
  16. package/dist/src/generated/abis.d.ts +4 -0
  17. package/dist/src/generated/abis.d.ts.map +1 -1
  18. package/dist/src/generated/abis.js +82 -0
  19. package/dist/src/generated/chainFactsKey.d.ts +5 -0
  20. package/dist/src/generated/chainFactsKey.d.ts.map +1 -0
  21. package/dist/src/generated/chainFactsKey.js +19 -0
  22. package/dist/src/generated/contracts.d.ts.map +1 -1
  23. package/dist/src/generated/contracts.dev.d.ts.map +1 -1
  24. package/dist/src/generated/contracts.dev.js +5 -0
  25. package/dist/src/generated/contracts.js +5 -0
  26. package/dist/src/generated/packageVersion.d.ts +3 -0
  27. package/dist/src/generated/packageVersion.d.ts.map +1 -0
  28. package/dist/src/generated/packageVersion.js +6 -0
  29. package/dist/src/index.d.ts +6 -3
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/index.js +6 -1
  32. package/dist/src/types.d.ts +13 -1
  33. package/dist/src/types.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -1,23 +1,291 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyChainNetworks = classifyChainNetworks;
3
4
  exports.validateChainConfig = validateChainConfig;
4
5
  exports.formatValidationIssues = formatValidationIssues;
6
+ // Runtime mirrors of the string unions in src/types.ts. Keeping these in sync
7
+ // is a build-time concern only: an unrecognised value here means the jsonnet
8
+ // introduced a layer/quoter/vm the TS unions don't know about yet, which SHOULD
9
+ // fail the build — consumers switch on these, so the union has to be updated in
10
+ // the same change. (Read-side clients deliberately do NOT check enum values:
11
+ // there, an unknown value means an older consumer is reading a newer artifact,
12
+ // which must keep working.)
13
+ const SETTLEMENT_LAYERS = new Set([
14
+ 'ACROSS',
15
+ 'ECO',
16
+ 'RELAY',
17
+ 'OFT',
18
+ 'NEAR',
19
+ 'RHINO',
20
+ 'CCTP',
21
+ ]);
22
+ const SWAP_QUOTERS = new Set([
23
+ '1inch',
24
+ '0x',
25
+ 'velora',
26
+ 'kyberswap',
27
+ 'fynd',
28
+ ]);
29
+ const VM_TYPES = new Set(['evm', 'svm', 'tvm']);
30
+ const PEG_GROUPS = new Set(['USD', 'ETH']);
31
+ const NETWORKS = new Set(['mainnet', 'testnet']);
32
+ // Registry keys are chain ids consumers index by directly, so they must be
33
+ // canonical positive integers — no leading zeros, which would let "01" and "1"
34
+ // both address chain 1.
35
+ const CHAIN_ID_KEY = /^[1-9][0-9]*$/;
36
+ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
37
+ const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
38
+ const isNonNegativeInt = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
39
+ const isAbsentOr = (v, check) => v === undefined || check(v);
5
40
  /**
6
- * Validates a chain registry for configuration problems.
41
+ * Structural checks on one chain entry, returning human-readable problems.
42
+ *
43
+ * This is the exhaustive validation: it runs once, here, against data we just
44
+ * generated and against unions we ship in the same version. Read-side clients
45
+ * only sanity-check the envelope, so this is the single place a malformed
46
+ * registry gets caught before it reaches anyone.
47
+ */
48
+ function structuralProblems(chain) {
49
+ const problems = [];
50
+ if (!isNonEmptyString(chain.name))
51
+ problems.push('name is not a non-empty string');
52
+ if (!VM_TYPES.has(chain.vmType)) {
53
+ problems.push(`unknown vmType ${JSON.stringify(chain.vmType)}`);
54
+ }
55
+ // Required, and the only field here the jsonnet does not author — it is
56
+ // stamped by classifyChainNetworks. Checked anyway, because this is the gate
57
+ // the published artifact passes through: consumers gate their mainnet vs
58
+ // testnet registry on it, so an entry reaching the CDN without it would be
59
+ // unclassifiable at runtime with no way to recover.
60
+ if (!NETWORKS.has(chain.network)) {
61
+ problems.push(`network ${JSON.stringify(chain.network)} is not "mainnet" or "testnet"`);
62
+ }
63
+ if (!isAbsentOr(chain.caip2, isNonEmptyString))
64
+ problems.push('caip2 is not a string');
65
+ if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
66
+ problems.push('virtual is not a boolean');
67
+ }
68
+ for (const field of ['nativeToken', 'wrappedNativeToken']) {
69
+ const token = chain[field];
70
+ if (!isObject(token)) {
71
+ problems.push(`${field} is not an object`);
72
+ continue;
73
+ }
74
+ if (typeof token.address !== 'string')
75
+ problems.push(`${field}.address is not a string`);
76
+ if (!isNonEmptyString(token.symbol))
77
+ problems.push(`${field}.symbol is not a non-empty string`);
78
+ if (!isNonNegativeInt(token.decimals))
79
+ problems.push(`${field}.decimals is not a non-negative integer`);
80
+ }
81
+ if (!Array.isArray(chain.tokens)) {
82
+ problems.push('tokens is not an array');
83
+ }
84
+ else {
85
+ chain.tokens.forEach((token, i) => {
86
+ const at = `tokens[${i}]`;
87
+ if (!isObject(token)) {
88
+ problems.push(`${at} is not an object`);
89
+ return;
90
+ }
91
+ if (typeof token.address !== 'string')
92
+ problems.push(`${at}.address is not a string`);
93
+ if (!isNonEmptyString(token.symbol))
94
+ problems.push(`${at}.symbol is not a non-empty string`);
95
+ if (!isNonNegativeInt(token.decimals))
96
+ problems.push(`${at}.decimals is not a non-negative integer`);
97
+ // Nullable, but the key must be present — consumers read it directly.
98
+ for (const slot of ['balanceSlot', 'approvalSlot']) {
99
+ if (!(token[slot] === null || isNonNegativeInt(token[slot]))) {
100
+ problems.push(`${at}.${slot} is not a non-negative integer or null`);
101
+ }
102
+ }
103
+ if (!isAbsentOr(token.priceSymbol, isNonEmptyString))
104
+ problems.push(`${at}.priceSymbol is not a string`);
105
+ if (!isAbsentOr(token.unpriced, (v) => typeof v === 'boolean'))
106
+ problems.push(`${at}.unpriced is not a boolean`);
107
+ if (!isAbsentOr(token.pegGroup, (v) => PEG_GROUPS.has(v))) {
108
+ problems.push(`${at}.pegGroup ${JSON.stringify(token.pegGroup)} is not a known peg group`);
109
+ }
110
+ });
111
+ }
112
+ for (const [field, allowed] of [
113
+ ['settlementLayers', SETTLEMENT_LAYERS],
114
+ ['swapQuoters', SWAP_QUOTERS],
115
+ ]) {
116
+ const values = chain[field];
117
+ if (!Array.isArray(values)) {
118
+ problems.push(`${field} is not an array`);
119
+ continue;
120
+ }
121
+ for (const value of values) {
122
+ if (!allowed.has(value)) {
123
+ problems.push(`${field} contains unknown value ${JSON.stringify(value)} — add it to the union in src/types.ts and to validation.ts`);
124
+ }
125
+ }
126
+ }
127
+ const quoterConfig = chain.swapQuoterConfig;
128
+ if (quoterConfig !== undefined) {
129
+ if (!isObject(quoterConfig)) {
130
+ problems.push('swapQuoterConfig is not an object');
131
+ }
132
+ else {
133
+ for (const [quoter, config] of Object.entries(quoterConfig)) {
134
+ if (!SWAP_QUOTERS.has(quoter)) {
135
+ problems.push(`swapQuoterConfig has unknown quoter ${JSON.stringify(quoter)}`);
136
+ }
137
+ if (!isObject(config)) {
138
+ problems.push(`swapQuoterConfig.${quoter} is not an object`);
139
+ }
140
+ else if (!isAbsentOr(config.slug, isNonEmptyString)) {
141
+ problems.push(`swapQuoterConfig.${quoter}.slug is not a string`);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return problems;
147
+ }
148
+ /**
149
+ * Reinserts an entry's keys in sorted order.
150
+ *
151
+ * jsonnet emits its objects key-sorted, so appending a field would leave the one
152
+ * generated file that gets signed and published non-canonical — the same logical
153
+ * registry serialising differently depending on how the generator happened to
154
+ * build the object. Only the top level needs it; nested objects come through
155
+ * untouched and already sorted.
156
+ */
157
+ function withSortedKeys(entry) {
158
+ return Object.fromEntries(Object.entries(entry).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
159
+ }
160
+ /**
161
+ * Stamps every chain with its mainnet/testnet classification, derived from which
162
+ * network map its chain id appears in.
163
+ *
164
+ * Why this exists: the split is otherwise implicit in *which file* an id lands in
165
+ * (configs/mainnets.json vs configs/testnets.json). The package surfaces it as
166
+ * `mainnetChains` / `MainnetNetwork`, but configs/chains.json — the registry the
167
+ * facts artifact publishes and the read-client bundles — carried no trace of it.
168
+ * So a consumer reading facts at runtime could not tell which environment a chain
169
+ * belonged to, which is exactly what the orchestrator's mainnet/testnet registry
170
+ * gating keys off: a CDN-added chain would arrive unclassifiable.
171
+ *
172
+ * Derived rather than authored per chain in the jsonnet, so it cannot drift from
173
+ * the membership it describes — "is this a testnet" keeps one source, the network
174
+ * maps.
175
+ *
176
+ * Lives in this module rather than generate.ts because the partition it relies on
177
+ * is a build-time invariant that has to fail the build, and generate.ts runs its
178
+ * generator on import, so nothing declared there is testable.
179
+ */
180
+ function classifyChainNetworks(chains, mainnets, testnets) {
181
+ const issues = [];
182
+ const classified = {};
183
+ for (const [chainId, chain] of Object.entries(chains)) {
184
+ const chainName = isNonEmptyString(chain?.name)
185
+ ? chain.name
186
+ : '<unnamed>';
187
+ const inMainnets = Object.hasOwn(mainnets, chainId);
188
+ const inTestnets = Object.hasOwn(testnets, chainId);
189
+ // Anything the input already carries under `network` is dropped rather than
190
+ // trusted: the field is present if and only if it was derived here, from the
191
+ // maps. That holds on the failure paths too, which is what keeps the
192
+ // structural check in validateChainConfig an independent second gate — it
193
+ // rejects an unclassified chain regardless of what the jsonnet emitted.
194
+ const base = { ...chain };
195
+ delete base.network;
196
+ if (inMainnets && inTestnets) {
197
+ // Nothing downstream can resolve this, and the pre-existing provider merge
198
+ // (`{...mainnets, ...testnets}`) would silently settle it as a testnet.
199
+ issues.push({
200
+ severity: 'error',
201
+ chainId,
202
+ chainName,
203
+ message: 'listed in both configs/mainnets.json and configs/testnets.json — cannot be classified',
204
+ });
205
+ classified[chainId] = withSortedKeys(base);
206
+ continue;
207
+ }
208
+ if (!inMainnets && !inTestnets) {
209
+ issues.push({
210
+ severity: 'error',
211
+ chainId,
212
+ chainName,
213
+ message: 'listed in neither configs/mainnets.json nor configs/testnets.json — cannot be classified as mainnet or testnet',
214
+ });
215
+ classified[chainId] = withSortedKeys(base);
216
+ continue;
217
+ }
218
+ classified[chainId] = withSortedKeys({
219
+ ...base,
220
+ network: inMainnets ? 'mainnet' : 'testnet',
221
+ });
222
+ }
223
+ // The reverse direction: providers configured for a chain the registry does not
224
+ // define. Harmless today (every consumer iterates the registry, not the network
225
+ // maps) but it is the same jsonnet drift that makes a chain silently absent, so
226
+ // it is worth saying out loud rather than leaving to be discovered.
227
+ for (const [mapName, map] of [
228
+ ['configs/mainnets.json', mainnets],
229
+ ['configs/testnets.json', testnets],
230
+ ]) {
231
+ for (const chainId of Object.keys(map)) {
232
+ if (!Object.hasOwn(chains, chainId)) {
233
+ issues.push({
234
+ severity: 'warning',
235
+ chainId,
236
+ chainName: '<not in registry>',
237
+ message: `listed in ${mapName} but absent from the chain registry`,
238
+ });
239
+ }
240
+ }
241
+ }
242
+ return { chains: classified, issues };
243
+ }
244
+ /**
245
+ * Validates a chain registry for structural and configuration problems.
7
246
  *
8
247
  * Checks performed:
248
+ * - Structure: chain-id keys, and every field of every entry against the
249
+ * shapes and unions declared in src/types.ts (error)
9
250
  * - Orphaned chains: chains with no settlement layers (error)
10
251
  * - Single-layer fragility: chains with exactly one settlement layer (warning)
11
252
  */
12
253
  function validateChainConfig(chains) {
13
254
  const issues = [];
14
255
  for (const [chainId, chain] of Object.entries(chains)) {
256
+ const chainName = isNonEmptyString(chain?.name)
257
+ ? chain.name
258
+ : '<unnamed>';
259
+ if (!CHAIN_ID_KEY.test(chainId)) {
260
+ issues.push({
261
+ severity: 'error',
262
+ chainId,
263
+ chainName,
264
+ message: 'key is not a canonical chain id (positive integer, no leading zeros)',
265
+ });
266
+ }
267
+ if (!isObject(chain)) {
268
+ issues.push({
269
+ severity: 'error',
270
+ chainId,
271
+ chainName,
272
+ message: 'entry is not an object',
273
+ });
274
+ continue;
275
+ }
276
+ for (const problem of structuralProblems(chain)) {
277
+ issues.push({ severity: 'error', chainId, chainName, message: problem });
278
+ }
15
279
  // Virtual chains (HyperCore) are reachable via their settlement chain, not
16
280
  // a direct settlement layer — the orphaned-chain check doesn't apply.
17
281
  if (chain.virtual) {
18
282
  continue;
19
283
  }
20
284
  const layers = chain.settlementLayers;
285
+ if (!Array.isArray(layers)) {
286
+ // Already reported structurally; skip the reachability checks below.
287
+ continue;
288
+ }
21
289
  if (layers.length === 0) {
22
290
  issues.push({
23
291
  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":""}