@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.
- package/dist/configs/chains.json +50 -1
- package/dist/scripts/__tests__/validation.test.js +215 -10
- package/dist/scripts/generate.js +31 -5
- package/dist/scripts/validation.d.ts +78 -2
- package/dist/scripts/validation.d.ts.map +1 -1
- package/dist/scripts/validation.js +439 -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 +5 -1
- package/dist/src/chains.d.ts.map +1 -1
- package/dist/src/chains.js +50 -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/networks.d.ts +1281 -1281
- package/dist/src/generated/networks.d.ts.map +1 -1
- 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 +37 -1
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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":"
|
|
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
|
-
*
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"chainFactsClient.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/chainFactsClient.test.ts"],"names":[],"mappings":""}
|