@rhinestone/shared-configs 1.16.0 → 1.18.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/README.md +15 -0
- package/dist/configs/chains.json +303 -27
- package/dist/scripts/__tests__/validation.test.js +117 -0
- package/dist/scripts/__tests__/vendor-drift.test.d.ts +1 -0
- package/dist/scripts/__tests__/vendor-drift.test.js +411 -0
- package/dist/scripts/generate.js +15 -1
- package/dist/scripts/validation.d.ts +10 -0
- package/dist/scripts/validation.js +137 -0
- package/dist/scripts/vendor-drift/diff.d.ts +80 -0
- package/dist/scripts/vendor-drift/diff.js +569 -0
- package/dist/scripts/vendor-drift/identity.d.ts +58 -0
- package/dist/scripts/vendor-drift/identity.js +139 -0
- package/dist/scripts/vendor-drift/main.d.ts +10 -0
- package/dist/scripts/vendor-drift/main.js +77 -0
- package/dist/scripts/vendor-drift/report.d.ts +5 -0
- package/dist/scripts/vendor-drift/report.js +94 -0
- package/dist/scripts/vendor-drift/vendors.d.ts +34 -0
- package/dist/scripts/vendor-drift/vendors.js +193 -0
- package/dist/src/chains.d.ts +10 -1
- package/dist/src/chains.js +303 -27
- package/dist/src/generated/packageVersion.d.ts +1 -1
- package/dist/src/generated/packageVersion.js +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/types.d.ts +72 -1
- package/package.json +2 -1
|
@@ -40,6 +40,7 @@ exports.validateChainIconSlugs = validateChainIconSlugs;
|
|
|
40
40
|
exports.classifyChainPublicRpcUrls = classifyChainPublicRpcUrls;
|
|
41
41
|
exports.classifyChainStacks = classifyChainStacks;
|
|
42
42
|
exports.validateChainConfig = validateChainConfig;
|
|
43
|
+
exports.validateOftCoverage = validateOftCoverage;
|
|
43
44
|
exports.formatValidationIssues = formatValidationIssues;
|
|
44
45
|
const celo_1 = require("viem/celo");
|
|
45
46
|
const viemChains = __importStar(require("viem/chains"));
|
|
@@ -93,6 +94,87 @@ const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
|
93
94
|
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
94
95
|
const isNonNegativeInt = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
|
|
95
96
|
const isAbsentOr = (v, check) => v === undefined || check(v);
|
|
97
|
+
// Every vendor layer's REQUIRED payload, keyed by the SettlementLayer value
|
|
98
|
+
// that switches it on. This is what replaces the chain-id allowlists the
|
|
99
|
+
// jsonnet used to carry with "must match <map> in the orchestrator" comments —
|
|
100
|
+
// nothing enforced those, and they had drifted. A layer declared without the
|
|
101
|
+
// identifiers needed to address it is a route we advertise and then cannot
|
|
102
|
+
// take, so it fails the build rather than the fill.
|
|
103
|
+
const SETTLEMENT_CONFIG_SCHEMA = {
|
|
104
|
+
RHINO: { key: 'rhino', required: { chainName: isNonEmptyString } },
|
|
105
|
+
CCTP: { key: 'cctp', required: { domain: isNonNegativeInt } },
|
|
106
|
+
NEAR: { key: 'near', required: { prefix: isNonEmptyString } },
|
|
107
|
+
OFT: {
|
|
108
|
+
key: 'oft',
|
|
109
|
+
required: { adapter: isNonEmptyString, layerZeroEid: isNonNegativeInt },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
// Optional payload fields, shape-checked only when present. `bridgeContract` is
|
|
113
|
+
// absent for a destination-only chain, which nothing originates from.
|
|
114
|
+
const SETTLEMENT_CONFIG_OPTIONAL = {
|
|
115
|
+
rhino: { bridgeContract: isNonEmptyString },
|
|
116
|
+
};
|
|
117
|
+
const SETTLEMENT_CONFIG_KEYS = new Set(Object.values(SETTLEMENT_CONFIG_SCHEMA).map((entry) => entry.key));
|
|
118
|
+
/**
|
|
119
|
+
* Cross-checks `settlementConfig` against `settlementLayers`.
|
|
120
|
+
*
|
|
121
|
+
* Both directions matter. A layer with no payload is unaddressable; a payload
|
|
122
|
+
* for a layer that is off is a route someone believes is live — the producer
|
|
123
|
+
* emits neither, so either means the derivation broke.
|
|
124
|
+
*/
|
|
125
|
+
function settlementConfigProblems(chain) {
|
|
126
|
+
const problems = [];
|
|
127
|
+
const config = chain.settlementConfig;
|
|
128
|
+
if (config !== undefined && !isObject(config)) {
|
|
129
|
+
return ['settlementConfig is not an object'];
|
|
130
|
+
}
|
|
131
|
+
const cfg = (config ?? {});
|
|
132
|
+
const layers = new Set(Array.isArray(chain.settlementLayers)
|
|
133
|
+
? chain.settlementLayers.filter((l) => typeof l === 'string')
|
|
134
|
+
: []);
|
|
135
|
+
for (const key of Object.keys(cfg)) {
|
|
136
|
+
if (!SETTLEMENT_CONFIG_KEYS.has(key)) {
|
|
137
|
+
problems.push(`settlementConfig has unknown layer ${JSON.stringify(key)} — add it to SETTLEMENT_CONFIG_SCHEMA and to SettlementConfig in src/types.ts`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const [layer, { key, required }] of Object.entries(SETTLEMENT_CONFIG_SCHEMA)) {
|
|
141
|
+
const payload = cfg[key];
|
|
142
|
+
if (!layers.has(layer)) {
|
|
143
|
+
if (payload !== undefined) {
|
|
144
|
+
problems.push(`settlementConfig.${key} is present but ${layer} is not in settlementLayers`);
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!isObject(payload)) {
|
|
149
|
+
problems.push(`settlementLayers contains ${layer} but settlementConfig.${key} is missing`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const [field, check] of Object.entries(required)) {
|
|
153
|
+
if (!check(payload[field])) {
|
|
154
|
+
problems.push(`settlementConfig.${key}.${field} ${JSON.stringify(payload[field])} is invalid — ${layer} cannot be addressed without it`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const [field, check] of Object.entries(SETTLEMENT_CONFIG_OPTIONAL[key] ?? {})) {
|
|
158
|
+
if (!isAbsentOr(payload[field], check)) {
|
|
159
|
+
problems.push(`settlementConfig.${key}.${field} is invalid`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// A non-EVM chain's 1Click asset ids are not derivable from the on-chain
|
|
164
|
+
// address, so every token on one has to carry its own. Missing it is not a
|
|
165
|
+
// degraded route: `toNearAssetId` throws mid-plan.
|
|
166
|
+
if (layers.has('NEAR') &&
|
|
167
|
+
typeof chain.vmType === 'string' &&
|
|
168
|
+
chain.vmType !== 'evm' &&
|
|
169
|
+
Array.isArray(chain.tokens)) {
|
|
170
|
+
for (const token of chain.tokens) {
|
|
171
|
+
if (isObject(token) && !isNonEmptyString(token.nearAssetId)) {
|
|
172
|
+
problems.push(`token ${JSON.stringify(token.symbol)} has no nearAssetId, which a non-EVM NEAR chain cannot derive`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return problems;
|
|
177
|
+
}
|
|
96
178
|
/**
|
|
97
179
|
* Structural checks on one chain entry, returning human-readable problems.
|
|
98
180
|
*
|
|
@@ -196,6 +278,10 @@ function structuralProblems(chain) {
|
|
|
196
278
|
}
|
|
197
279
|
if (!isAbsentOr(token.priceSymbol, isNonEmptyString))
|
|
198
280
|
problems.push(`${at}.priceSymbol is not a string`);
|
|
281
|
+
if (!isAbsentOr(token.rhinoSymbol, isNonEmptyString))
|
|
282
|
+
problems.push(`${at}.rhinoSymbol is not a string`);
|
|
283
|
+
if (!isAbsentOr(token.nearAssetId, isNonEmptyString))
|
|
284
|
+
problems.push(`${at}.nearAssetId is not a string`);
|
|
199
285
|
if (!isAbsentOr(token.unpriced, (v) => typeof v === 'boolean'))
|
|
200
286
|
problems.push(`${at}.unpriced is not a boolean`);
|
|
201
287
|
if (!isAbsentOr(token.pegGroup, (v) => PEG_GROUPS.has(v))) {
|
|
@@ -218,6 +304,7 @@ function structuralProblems(chain) {
|
|
|
218
304
|
}
|
|
219
305
|
}
|
|
220
306
|
}
|
|
307
|
+
problems.push(...settlementConfigProblems(chain));
|
|
221
308
|
const quoterConfig = chain.swapQuoterConfig;
|
|
222
309
|
if (quoterConfig !== undefined) {
|
|
223
310
|
if (!isObject(quoterConfig)) {
|
|
@@ -885,6 +972,56 @@ function validateChainConfig(chains) {
|
|
|
885
972
|
hasErrors: issues.some((issue) => issue.severity === 'error'),
|
|
886
973
|
};
|
|
887
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Insists that every chain the OFT registry deploys on declares OFT.
|
|
977
|
+
*
|
|
978
|
+
* The chain block states the membership and reads the adapter and endpoint id
|
|
979
|
+
* back out of config/oft.jsonnet, so the two can only disagree one way: a
|
|
980
|
+
* deployment added to the OFT registry whose chain block was never given an
|
|
981
|
+
* `oft` entry. That chain silently stops advertising OFT, which is invisible in
|
|
982
|
+
* a diff of either file on its own — this is the check that sees both.
|
|
983
|
+
*/
|
|
984
|
+
function validateOftCoverage(chains, oft) {
|
|
985
|
+
const issues = [];
|
|
986
|
+
const expected = new Map();
|
|
987
|
+
for (const registry of Object.values(oft)) {
|
|
988
|
+
if (!isObject(registry) || !isObject(registry.adapters))
|
|
989
|
+
continue;
|
|
990
|
+
for (const [chainId, adapter] of Object.entries(registry.adapters)) {
|
|
991
|
+
if (typeof adapter === 'string')
|
|
992
|
+
expected.set(chainId, adapter);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const [chainId, adapter] of expected) {
|
|
996
|
+
const chain = chains[chainId];
|
|
997
|
+
if (!chain)
|
|
998
|
+
continue;
|
|
999
|
+
const chainName = isNonEmptyString(chain.name) ? chain.name : '<unnamed>';
|
|
1000
|
+
const config = isObject(chain.settlementConfig)
|
|
1001
|
+
? chain.settlementConfig.oft
|
|
1002
|
+
: undefined;
|
|
1003
|
+
if (!Array.isArray(chain.settlementLayers) || !chain.settlementLayers.includes('OFT')) {
|
|
1004
|
+
issues.push({
|
|
1005
|
+
severity: 'error',
|
|
1006
|
+
chainId,
|
|
1007
|
+
chainName,
|
|
1008
|
+
message: 'config/oft.jsonnet deploys an OFT adapter here, but the chain declares no OFT layer — add `oft: oftLayer.forChain(<chain_id>)` to its supported_settlement_layers',
|
|
1009
|
+
});
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (!isObject(config) ||
|
|
1013
|
+
typeof config.adapter !== 'string' ||
|
|
1014
|
+
config.adapter.toLowerCase() !== adapter.toLowerCase()) {
|
|
1015
|
+
issues.push({
|
|
1016
|
+
severity: 'error',
|
|
1017
|
+
chainId,
|
|
1018
|
+
chainName,
|
|
1019
|
+
message: `settlementConfig.oft.adapter ${JSON.stringify(config?.adapter)} does not match config/oft.jsonnet's ${JSON.stringify(adapter)}`,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return issues;
|
|
1024
|
+
}
|
|
888
1025
|
/**
|
|
889
1026
|
* Formats validation issues for console output.
|
|
890
1027
|
*
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure diff between what `configs/chains.json` claims and what each vendor
|
|
3
|
+
* serves. No I/O — `vendors.ts` collects the vendor side, `main.ts` wires them.
|
|
4
|
+
*/
|
|
5
|
+
import { type SettlementLayerName } from "./identity";
|
|
6
|
+
export type Severity = "drift" | "warning" | "info";
|
|
7
|
+
/**
|
|
8
|
+
* Which direction the disagreement runs. `overclaim` is the dangerous one — we
|
|
9
|
+
* advertise a route the vendor will refuse, and an offered-but-unserved route
|
|
10
|
+
* fails at fill, not at quote.
|
|
11
|
+
*/
|
|
12
|
+
export type Kind = "overclaim" | "underclaim" | "mismatch" | "note";
|
|
13
|
+
export type Finding = {
|
|
14
|
+
layer: SettlementLayerName | "REGISTRY";
|
|
15
|
+
severity: Severity;
|
|
16
|
+
kind: Kind;
|
|
17
|
+
code: string;
|
|
18
|
+
chainId?: number;
|
|
19
|
+
message: string;
|
|
20
|
+
};
|
|
21
|
+
export type RegistryToken = {
|
|
22
|
+
address: string;
|
|
23
|
+
symbol: string;
|
|
24
|
+
/** Authored override; when present it is what the consumer actually sends. */
|
|
25
|
+
nearAssetId?: string;
|
|
26
|
+
settlementLayers?: string[];
|
|
27
|
+
};
|
|
28
|
+
export type RegistryEntry = {
|
|
29
|
+
name?: string;
|
|
30
|
+
vmType?: string;
|
|
31
|
+
network?: string;
|
|
32
|
+
publicRpcUrl?: string;
|
|
33
|
+
virtual?: boolean;
|
|
34
|
+
settlementLayers?: string[];
|
|
35
|
+
tokens?: RegistryToken[];
|
|
36
|
+
settlementConfig?: Record<string, Record<string, unknown>>;
|
|
37
|
+
settlementLayerConfig?: Record<string, Record<string, unknown>>;
|
|
38
|
+
vendorConfig?: Record<string, Record<string, unknown>>;
|
|
39
|
+
};
|
|
40
|
+
export type Registry = Record<string, RegistryEntry>;
|
|
41
|
+
export type RhinoVendorEntry = {
|
|
42
|
+
name: string;
|
|
43
|
+
networkId: unknown;
|
|
44
|
+
status?: string;
|
|
45
|
+
contractAddress?: string;
|
|
46
|
+
};
|
|
47
|
+
export type OneClickToken = {
|
|
48
|
+
assetId: string;
|
|
49
|
+
blockchain: string;
|
|
50
|
+
symbol?: string;
|
|
51
|
+
contractAddress?: string;
|
|
52
|
+
};
|
|
53
|
+
export type CctpProbe = {
|
|
54
|
+
chainId: number;
|
|
55
|
+
reachable: true;
|
|
56
|
+
messenger: string;
|
|
57
|
+
deployed: boolean;
|
|
58
|
+
transmitter?: string;
|
|
59
|
+
domain?: number;
|
|
60
|
+
} | {
|
|
61
|
+
chainId: number;
|
|
62
|
+
reachable: false;
|
|
63
|
+
error: string;
|
|
64
|
+
};
|
|
65
|
+
export declare function diffRhino(registry: Registry, vendor: RhinoVendorEntry[]): Finding[];
|
|
66
|
+
export declare function nearAssetId(slug: string, chainId: number, address: string, registryConfig?: Record<string, unknown>): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Both directions, because a token is either claimed or not and each side has
|
|
69
|
+
* its own failure. A token we claim that 1Click will not serve is an
|
|
70
|
+
* **overclaim** — the route is offered and the quote fails `tokenOut is not
|
|
71
|
+
* valid`. A token 1Click does serve that we do not claim is an
|
|
72
|
+
* **underclaim** — capability left on the table, informational rather than
|
|
73
|
+
* drift because nothing breaks. Checking only the first would let coverage
|
|
74
|
+
* silently ossify; checking every token as a claim, which is what this did
|
|
75
|
+
* before per-token coverage existed, reports a deliberate exclusion as an
|
|
76
|
+
* overclaim.
|
|
77
|
+
*/
|
|
78
|
+
export declare function diffNear(registry: Registry, vendor: OneClickToken[]): Finding[];
|
|
79
|
+
export declare function diffCctp(registry: Registry, probes: CctpProbe[]): Finding[];
|
|
80
|
+
export declare function diffRegistryConsistency(registry: Registry): Finding[];
|