@rhinestone/shared-configs 1.15.0 → 1.17.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,8 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const bun_test_1 = require("bun:test");
4
7
  const validation_1 = require("../validation");
5
8
  const index_1 = require("../../src/index");
9
+ const oft_json_1 = __importDefault(require("../../configs/oft.json"));
6
10
  const nativeToken = { address: "0x0", symbol: "ETH", decimals: 18 };
7
11
  /** A structurally complete entry, so semantic tests don't trip structural checks. */
8
12
  function validChain(overrides = {}) {
@@ -147,6 +151,119 @@ function errorsFor(chain) {
147
151
  (0, bun_test_1.expect)(result.hasErrors).toBe(false);
148
152
  });
149
153
  });
154
+ // These replace the "must match <map> in the orchestrator" comments the jsonnet
155
+ // used to carry over four flat chain-id allowlists. A layer advertised without
156
+ // the identifiers that address it is a route offered and then not taken, so the
157
+ // build is the only place left to catch it.
158
+ (0, bun_test_1.describe)("validateChainConfig — settlementConfig", () => {
159
+ const withLayer = (layer, config) => validChain({
160
+ settlementLayers: ["RELAY", layer],
161
+ settlementConfig: config,
162
+ });
163
+ bun_test_1.it.each([
164
+ ["RHINO", "rhino", { chainName: "ETHEREUM" }],
165
+ ["CCTP", "cctp", { domain: 0 }],
166
+ ["NEAR", "near", { prefix: "eth" }],
167
+ ["OFT", "oft", { adapter: "0xadapter", layerZeroEid: 30101 }],
168
+ ])("accepts %s with its payload", (layer, key, payload) => {
169
+ (0, bun_test_1.expect)(errorsFor(withLayer(layer, { [key]: payload }))).toEqual([]);
170
+ });
171
+ bun_test_1.it.each([
172
+ ["RHINO", "rhino"],
173
+ ["CCTP", "cctp"],
174
+ ["NEAR", "near"],
175
+ ["OFT", "oft"],
176
+ ])("rejects %s declared with no payload", (layer, key) => {
177
+ const errors = errorsFor(withLayer(layer, {}));
178
+ (0, bun_test_1.expect)(errors).toHaveLength(1);
179
+ (0, bun_test_1.expect)(errors[0].message).toContain(`settlementConfig.${key} is missing`);
180
+ });
181
+ bun_test_1.it.each([
182
+ ["RHINO", { rhino: {} }, "rhino.chainName"],
183
+ ["CCTP", { cctp: { domain: "0" } }, "cctp.domain"],
184
+ ["NEAR", { near: { prefix: "" } }, "near.prefix"],
185
+ ["OFT", { oft: { adapter: "0xa" } }, "oft.layerZeroEid"],
186
+ ])("rejects %s whose payload is incomplete", (layer, config, field) => {
187
+ const errors = errorsFor(withLayer(layer, config));
188
+ (0, bun_test_1.expect)(errors.some((e) => e.message.includes(field))).toBe(true);
189
+ });
190
+ // Domain 0 is Ethereum, so a truthiness check would drop a real route.
191
+ (0, bun_test_1.it)("accepts CCTP domain 0", () => {
192
+ (0, bun_test_1.expect)(errorsFor(withLayer("CCTP", { cctp: { domain: 0 } }))).toEqual([]);
193
+ });
194
+ // Absent for a destination-only chain, which nothing originates from.
195
+ (0, bun_test_1.it)("accepts a rhino entry with no bridgeContract", () => {
196
+ (0, bun_test_1.expect)(errorsFor(withLayer("RHINO", { rhino: { chainName: "SOLANA" } }))).toEqual([]);
197
+ });
198
+ (0, bun_test_1.it)("rejects a payload for a layer that is not enabled", () => {
199
+ const errors = errorsFor(validChain({ settlementConfig: { cctp: { domain: 6 } } }));
200
+ (0, bun_test_1.expect)(errors).toHaveLength(1);
201
+ (0, bun_test_1.expect)(errors[0].message).toContain("CCTP is not in settlementLayers");
202
+ });
203
+ (0, bun_test_1.it)("rejects an unknown layer key", () => {
204
+ const errors = errorsFor(validChain({ settlementConfig: { hop: {} } }));
205
+ (0, bun_test_1.expect)(errors).toHaveLength(1);
206
+ (0, bun_test_1.expect)(errors[0].message).toContain("unknown layer");
207
+ });
208
+ // 1Click's non-EVM ids are NEAR-internal; deriving one produces a string it
209
+ // rejects, so a missing id throws mid-plan rather than degrading.
210
+ (0, bun_test_1.it)("rejects a non-EVM NEAR chain whose token has no nearAssetId", () => {
211
+ const errors = errorsFor(validChain({
212
+ vmType: "svm",
213
+ settlementLayers: ["RELAY", "NEAR"],
214
+ settlementConfig: { near: { prefix: "sol" } },
215
+ tokens: [{ ...nativeToken, balanceSlot: null, approvalSlot: null }],
216
+ }));
217
+ (0, bun_test_1.expect)(errors).toHaveLength(1);
218
+ (0, bun_test_1.expect)(errors[0].message).toContain("nearAssetId");
219
+ });
220
+ (0, bun_test_1.it)("does not require nearAssetId on an EVM NEAR chain", () => {
221
+ (0, bun_test_1.expect)(errorsFor(validChain({
222
+ settlementLayers: ["RELAY", "NEAR"],
223
+ settlementConfig: { near: { prefix: "eth" } },
224
+ }))).toEqual([]);
225
+ });
226
+ });
227
+ // The chain block states OFT membership and reads its addresses back out of
228
+ // config/oft.jsonnet, so the one way the two can disagree is an adapter added
229
+ // there whose chain block was never given an `oft` entry — invisible in a diff
230
+ // of either file alone.
231
+ (0, bun_test_1.describe)("validateOftCoverage", () => {
232
+ (0, bun_test_1.it)("accepts the real generated registry", () => {
233
+ (0, bun_test_1.expect)((0, validation_1.validateOftCoverage)(index_1.chainRegistry, oft_json_1.default)).toEqual([]);
234
+ });
235
+ (0, bun_test_1.it)("reports a chain with an adapter but no OFT layer", () => {
236
+ const issues = (0, validation_1.validateOftCoverage)({ "1": validChain() }, {
237
+ USDT0: { adapters: { "1": "0xadapter" } },
238
+ });
239
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
240
+ (0, bun_test_1.expect)(issues[0].message).toContain("declares no OFT layer");
241
+ });
242
+ (0, bun_test_1.it)("reports an adapter the chain entry disagrees with", () => {
243
+ const chain = validChain({
244
+ settlementLayers: ["RELAY", "OFT"],
245
+ settlementConfig: { oft: { adapter: "0xstale", layerZeroEid: 30101 } },
246
+ });
247
+ const issues = (0, validation_1.validateOftCoverage)({ "1": chain }, {
248
+ USDT0: { adapters: { "1": "0xadapter" } },
249
+ });
250
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
251
+ (0, bun_test_1.expect)(issues[0].message).toContain("does not match");
252
+ });
253
+ // The registry is authored in checksummed hex; the chain block is not.
254
+ (0, bun_test_1.it)("compares adapters case-insensitively", () => {
255
+ const chain = validChain({
256
+ settlementLayers: ["RELAY", "OFT"],
257
+ settlementConfig: { oft: { adapter: "0xABC", layerZeroEid: 30101 } },
258
+ });
259
+ (0, bun_test_1.expect)((0, validation_1.validateOftCoverage)({ "1": chain }, {
260
+ USDT0: { adapters: { "1": "0xabc" } },
261
+ })).toEqual([]);
262
+ });
263
+ (0, bun_test_1.it)("ignores an OFT chain that is not in the registry", () => {
264
+ (0, bun_test_1.expect)((0, validation_1.validateOftCoverage)({}, { USDT0: { adapters: { "1": "0xa" } } })).toEqual([]);
265
+ });
266
+ });
150
267
  (0, bun_test_1.describe)("formatValidationIssues", () => {
151
268
  (0, bun_test_1.it)("returns empty string for no issues", () => {
152
269
  (0, bun_test_1.expect)((0, validation_1.formatValidationIssues)({ issues: [], hasErrors: false })).toBe("");
@@ -89,7 +89,7 @@ function renderChains(chainRegistry, mainnets, testnets) {
89
89
  }
90
90
  return [
91
91
  "// Auto-generated by scripts/generate.ts. Do not edit manually.",
92
- 'import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";',
92
+ 'import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementConfig, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";',
93
93
  "",
94
94
  "interface Token {",
95
95
  " /** EVM: 0x-prefixed hex. SVM: base58 SPL mint. TVM: base58 (T-prefixed). */",
@@ -102,6 +102,11 @@ function renderChains(chainRegistry, mainnets, testnets) {
102
102
  " approvalSlot: number | null;",
103
103
  " /** Bridge layers that can carry this token on this chain, when known. */",
104
104
  " settlementLayers?: SettlementLayer[];",
105
+ " /** Rhino's own symbol for this token, where it differs from ours. */",
106
+ " rhinoSymbol?: string;",
107
+ " /** NEAR 1Click asset id, for tokens whose id 1Click does not derive from",
108
+ " * the on-chain address (non-EVM chains). */",
109
+ " nearAssetId?: string;",
105
110
  " unpriced?: boolean;",
106
111
  "}",
107
112
  "",
@@ -150,6 +155,10 @@ function renderChains(chainRegistry, mainnets, testnets) {
150
155
  " wrappedNativeToken: NativeToken;",
151
156
  " tokens: Token[];",
152
157
  " settlementLayers: SettlementLayer[];",
158
+ " /** Per-vendor addressing for the enabled layers that need it (Rhino chain",
159
+ " * name, CCTP domain, NEAR prefix, OFT adapter). Third-party addresses —",
160
+ " * never the contracts block, which is read as Rhinestone-deployed. */",
161
+ " settlementConfig?: SettlementConfig;",
153
162
  " providers: ProviderName[];",
154
163
  " swapQuoters: SwapQuoter[];",
155
164
  " swapQuoterConfig?: Partial<Record<SwapQuoter, SwapQuoterConfig>>;",
@@ -412,6 +421,10 @@ async function generate() {
412
421
  // to insist on. A chain added without one would otherwise reach production as
413
422
  // a blank tile, which nothing anywhere reports.
414
423
  const iconSlugIssues = (0, validation_1.validateChainIconSlugs)(classifiedChains);
424
+ // The one check that needs both files: a chain block declares OFT membership
425
+ // and reads its addresses back out of the OFT registry, so only an adapter
426
+ // added there without the matching chain entry can drift.
427
+ const oftCoverageIssues = (0, validation_1.validateOftCoverage)(classifiedChains, output.oft);
415
428
  const classificationIssues = [
416
429
  ...networkIssues,
417
430
  ...stackIssues,
@@ -419,6 +432,7 @@ async function generate() {
419
432
  ...explorerIssues,
420
433
  ...publicRpcUrlIssues,
421
434
  ...iconSlugIssues,
435
+ ...oftCoverageIssues,
422
436
  ];
423
437
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "chains.json"), `${JSON.stringify(classifiedChains, null, 2)}\n`);
424
438
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "mainnets.json"), `${JSON.stringify(output.mainnets, null, 2)}\n`);
@@ -215,6 +215,16 @@ export declare function classifyChainStacks(chains: ChainRegistry, networks: {
215
215
  * - Single-layer fragility: chains with exactly one settlement layer (warning)
216
216
  */
217
217
  export declare function validateChainConfig(chains: ChainRegistry): ValidationResult;
218
+ /**
219
+ * Insists that every chain the OFT registry deploys on declares OFT.
220
+ *
221
+ * The chain block states the membership and reads the adapter and endpoint id
222
+ * back out of config/oft.jsonnet, so the two can only disagree one way: a
223
+ * deployment added to the OFT registry whose chain block was never given an
224
+ * `oft` entry. That chain silently stops advertising OFT, which is invisible in
225
+ * a diff of either file on its own — this is the check that sees both.
226
+ */
227
+ export declare function validateOftCoverage(chains: ChainRegistry, oft: Record<string, unknown>): ValidationIssue[];
218
228
  /**
219
229
  * Formats validation issues for console output.
220
230
  *
@@ -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
  *
@@ -1,4 +1,4 @@
1
- import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";
1
+ import type { ChainNetwork, ChainStack, PegGroup, ProviderName, SettlementConfig, SettlementLayer, SupportedChain, SwapQuoter, SwapQuoterConfig, VmType } from "./types";
2
2
  interface Token {
3
3
  /** EVM: 0x-prefixed hex. SVM: base58 SPL mint. TVM: base58 (T-prefixed). */
4
4
  address: string;
@@ -10,6 +10,11 @@ interface Token {
10
10
  approvalSlot: number | null;
11
11
  /** Bridge layers that can carry this token on this chain, when known. */
12
12
  settlementLayers?: SettlementLayer[];
13
+ /** Rhino's own symbol for this token, where it differs from ours. */
14
+ rhinoSymbol?: string;
15
+ /** NEAR 1Click asset id, for tokens whose id 1Click does not derive from
16
+ * the on-chain address (non-EVM chains). */
17
+ nearAssetId?: string;
13
18
  unpriced?: boolean;
14
19
  }
15
20
  interface NativeToken {
@@ -58,6 +63,10 @@ interface Chain {
58
63
  wrappedNativeToken: NativeToken;
59
64
  tokens: Token[];
60
65
  settlementLayers: SettlementLayer[];
66
+ /** Per-vendor addressing for the enabled layers that need it (Rhino chain
67
+ * name, CCTP domain, NEAR prefix, OFT adapter). Third-party addresses —
68
+ * never the contracts block, which is read as Rhinestone-deployed. */
69
+ settlementConfig?: SettlementConfig;
61
70
  providers: ProviderName[];
62
71
  swapQuoters: SwapQuoter[];
63
72
  swapQuoterConfig?: Partial<Record<SwapQuoter, SwapQuoterConfig>>;