@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.
- package/dist/configs/chains.json +27 -1
- package/dist/scripts/__tests__/validation.test.js +132 -10
- package/dist/scripts/generate.js +25 -5
- package/dist/scripts/validation.d.ts +32 -2
- package/dist/scripts/validation.d.ts.map +1 -1
- package/dist/scripts/validation.js +269 -1
- package/dist/src/__tests__/chainFactsClient.test.d.ts +2 -0
- package/dist/src/__tests__/chainFactsClient.test.d.ts.map +1 -0
- package/dist/src/__tests__/chainFactsClient.test.js +460 -0
- package/dist/src/chainFactsClient.d.ts +57 -0
- package/dist/src/chainFactsClient.d.ts.map +1 -0
- package/dist/src/chainFactsClient.js +295 -0
- package/dist/src/chains.d.ts +3 -1
- package/dist/src/chains.d.ts.map +1 -1
- package/dist/src/chains.js +27 -1
- package/dist/src/generated/abis.d.ts +4 -0
- package/dist/src/generated/abis.d.ts.map +1 -1
- package/dist/src/generated/abis.js +82 -0
- package/dist/src/generated/chainFactsKey.d.ts +5 -0
- package/dist/src/generated/chainFactsKey.d.ts.map +1 -0
- package/dist/src/generated/chainFactsKey.js +19 -0
- package/dist/src/generated/contracts.d.ts.map +1 -1
- package/dist/src/generated/contracts.dev.d.ts.map +1 -1
- package/dist/src/generated/contracts.dev.js +5 -0
- package/dist/src/generated/contracts.js +5 -0
- package/dist/src/generated/packageVersion.d.ts +3 -0
- package/dist/src/generated/packageVersion.d.ts.map +1 -0
- package/dist/src/generated/packageVersion.js +6 -0
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/types.d.ts +13 -1
- package/dist/src/types.d.ts.map +1 -1
- 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
|
-
*
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"chainFactsClient.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/chainFactsClient.test.ts"],"names":[],"mappings":""}
|