@rhinestone/shared-configs 1.17.0 → 1.19.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.
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ /**
3
+ * Pure diff between what `configs/chains.json` claims and what each vendor
4
+ * serves. No I/O — `vendors.ts` collects the vendor side, `main.ts` wires them.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.diffRhino = diffRhino;
8
+ exports.nearAssetId = nearAssetId;
9
+ exports.diffNear = diffNear;
10
+ exports.diffCctp = diffCctp;
11
+ exports.diffRegistryConsistency = diffRegistryConsistency;
12
+ const identity_1 = require("./identity");
13
+ const NATIVE_ADDRESSES = new Set([
14
+ "0x0000000000000000000000000000000000000000",
15
+ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
16
+ ]);
17
+ /**
18
+ * Every placeholder this registry uses for a chain's gas token. The EVM/Tron
19
+ * zero address plus Solana's System Program id, which `NATIVE_ADDRESSES` must
20
+ * not carry — that set drives the EVM asset-id derivation, where an SVM address
21
+ * has no meaning.
22
+ */
23
+ const NATIVE_PLACEHOLDERS = new Set([
24
+ ...NATIVE_ADDRESSES,
25
+ "11111111111111111111111111111111",
26
+ ]);
27
+ function chainIds(registry) {
28
+ return Object.keys(registry)
29
+ .map(Number)
30
+ .filter((id) => Number.isFinite(id))
31
+ .sort((a, b) => a - b);
32
+ }
33
+ function claims(registry, layer) {
34
+ return chainIds(registry).filter((id) => (registry[String(id)]?.settlementLayers ?? []).includes(layer));
35
+ }
36
+ function label(registry, chainId) {
37
+ const name = registry[String(chainId)]?.name;
38
+ return name ? `${name} (${chainId})` : String(chainId);
39
+ }
40
+ /**
41
+ * Per-layer vendor config for one chain, read from the registry's
42
+ * `settlementConfig` — which keys layers lowercase, unlike the uppercase
43
+ * `settlementLayers` list these call sites use.
44
+ *
45
+ * The aliases and the case-insensitive lookup are what stop this check from
46
+ * silently falling back to the baseline tables if the emitted shape is ever
47
+ * renamed: a fallback that cannot be distinguished from a real read would make
48
+ * the whole check report on hardcoded values instead of the registry.
49
+ */
50
+ function readLayerConfig(entry, layer) {
51
+ const blocks = entry?.settlementConfig ??
52
+ entry?.settlementLayerConfig ??
53
+ entry?.vendorConfig;
54
+ if (!blocks)
55
+ return undefined;
56
+ return blocks[layer] ?? blocks[layer.toLowerCase()];
57
+ }
58
+ function pickString(config, ...keys) {
59
+ for (const key of keys) {
60
+ const value = config?.[key];
61
+ if (typeof value === "string" && value.length > 0)
62
+ return value;
63
+ }
64
+ return undefined;
65
+ }
66
+ function pickNumber(config, ...keys) {
67
+ for (const key of keys) {
68
+ const value = config?.[key];
69
+ if (typeof value === "number" && Number.isFinite(value))
70
+ return value;
71
+ }
72
+ return undefined;
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Rhino — GET https://api.rhino.fi/bridge/configs
76
+ // ---------------------------------------------------------------------------
77
+ function diffRhino(registry, vendor) {
78
+ const findings = [];
79
+ const byName = new Map(vendor.map((entry) => [entry.name, entry]));
80
+ const nameToChainId = new Map(Object.entries(identity_1.RHINO_CHAIN_NAMES).map(([id, name]) => [name, Number(id)]));
81
+ const claimed = new Set(claims(registry, "RHINO"));
82
+ for (const chainId of claimed) {
83
+ const entry = registry[String(chainId)];
84
+ const config = readLayerConfig(entry, "RHINO");
85
+ const vendorName = pickString(config, "chainName", "vendorChainName", "name") ??
86
+ identity_1.RHINO_CHAIN_NAMES[chainId];
87
+ if (!vendorName) {
88
+ findings.push({
89
+ layer: "RHINO",
90
+ severity: "drift",
91
+ kind: "mismatch",
92
+ code: "rhino_no_chain_name",
93
+ chainId,
94
+ message: `${label(registry, chainId)} claims RHINO but no Rhino chain name is known for it — the quote API cannot be called.`,
95
+ });
96
+ continue;
97
+ }
98
+ const served = byName.get(vendorName);
99
+ if (!served) {
100
+ findings.push({
101
+ layer: "RHINO",
102
+ severity: "drift",
103
+ kind: "overclaim",
104
+ code: "rhino_chain_dropped",
105
+ chainId,
106
+ message: `${label(registry, chainId)} claims RHINO but ${vendorName} is not in /bridge/configs at all.`,
107
+ });
108
+ continue;
109
+ }
110
+ if (served.status !== "enabled") {
111
+ findings.push({
112
+ layer: "RHINO",
113
+ severity: "drift",
114
+ kind: "overclaim",
115
+ code: "rhino_chain_disabled",
116
+ chainId,
117
+ message: `${label(registry, chainId)} claims RHINO but ${vendorName} is status="${served.status}".`,
118
+ });
119
+ }
120
+ // Rhino returns every networkId as a string, EVM chains included, so a
121
+ // typeof check compares nothing on the real payload. Non-numeric ids are
122
+ // the non-EVM chains (BITCOIN, SN_MAIN, TON) and have no chain id to
123
+ // disagree with.
124
+ const servedChainId = typeof served.networkId === "number"
125
+ ? served.networkId
126
+ : /^\d+$/.test(String(served.networkId))
127
+ ? Number(served.networkId)
128
+ : undefined;
129
+ if (entry?.vmType === "evm" &&
130
+ servedChainId !== undefined &&
131
+ servedChainId !== chainId) {
132
+ findings.push({
133
+ layer: "RHINO",
134
+ severity: "drift",
135
+ kind: "mismatch",
136
+ code: "rhino_network_id_mismatch",
137
+ chainId,
138
+ message: `${vendorName} maps to chain ${chainId} here but Rhino reports networkId=${served.networkId}.`,
139
+ });
140
+ }
141
+ const expected = pickString(config, "bridgeContract", "contractAddress", "depositContract") ?? identity_1.BASELINE_RHINO_BRIDGE_CONTRACTS[chainId];
142
+ const actual = served.contractAddress;
143
+ if (!expected) {
144
+ if (entry?.vmType === "evm") {
145
+ findings.push({
146
+ layer: "RHINO",
147
+ severity: "warning",
148
+ kind: "note",
149
+ code: "rhino_bridge_contract_unknown",
150
+ chainId,
151
+ message: `${label(registry, chainId)} claims RHINO but no bridge contract is recorded — Rhino serves ${actual ?? "none"}, unchecked.`,
152
+ });
153
+ }
154
+ }
155
+ else if (!actual) {
156
+ // The bridge contract is the fund-routing address this checker exists to
157
+ // verify, so Rhino omitting or renaming the field must surface as an
158
+ // unchecked address rather than pass as agreement.
159
+ findings.push({
160
+ layer: "RHINO",
161
+ severity: "warning",
162
+ kind: "note",
163
+ code: "rhino_bridge_contract_unserved",
164
+ chainId,
165
+ message: `${label(registry, chainId)} holds bridge contract ${expected} but Rhino's entry carries no contract address — left unverified.`,
166
+ });
167
+ }
168
+ else if (actual.toLowerCase() !== expected.toLowerCase()) {
169
+ findings.push({
170
+ layer: "RHINO",
171
+ severity: "drift",
172
+ kind: "mismatch",
173
+ code: "rhino_bridge_contract_changed",
174
+ chainId,
175
+ message: `${label(registry, chainId)} bridge contract disagrees — we hold ${expected}, Rhino serves ${actual}.`,
176
+ });
177
+ }
178
+ }
179
+ for (const served of vendor) {
180
+ const chainId = nameToChainId.get(served.name);
181
+ if (chainId === undefined) {
182
+ findings.push({
183
+ layer: "RHINO",
184
+ severity: "info",
185
+ kind: "note",
186
+ code: "rhino_chain_unmapped",
187
+ message: `Rhino serves ${served.name} (networkId=${String(served.networkId)}, ${served.status}) — no chain of ours maps to it.`,
188
+ });
189
+ continue;
190
+ }
191
+ if (claimed.has(chainId))
192
+ continue;
193
+ if (served.status !== "enabled")
194
+ continue;
195
+ if (!registry[String(chainId)]) {
196
+ findings.push({
197
+ layer: "RHINO",
198
+ severity: "info",
199
+ kind: "note",
200
+ code: "rhino_chain_unregistered",
201
+ chainId,
202
+ message: `Rhino serves ${served.name} which maps to chain ${chainId} — not in the registry.`,
203
+ });
204
+ continue;
205
+ }
206
+ findings.push({
207
+ layer: "RHINO",
208
+ severity: "drift",
209
+ kind: "underclaim",
210
+ code: "rhino_chain_available",
211
+ chainId,
212
+ message: `Rhino serves ${served.name} on ${label(registry, chainId)} but the registry does not claim RHINO there.`,
213
+ });
214
+ }
215
+ return findings;
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ // NEAR / 1Click — GET https://1click.chaindefuser.com/v0/tokens
219
+ // ---------------------------------------------------------------------------
220
+ function nearAssetId(slug, chainId, address, registryConfig) {
221
+ const overrides = registryConfig?.["assetIds"];
222
+ if (overrides && typeof overrides === "object") {
223
+ const hit = overrides[address];
224
+ if (typeof hit === "string")
225
+ return hit;
226
+ }
227
+ const nonEvm = identity_1.NEAR_NON_EVM_ASSET_IDS[chainId];
228
+ if (nonEvm)
229
+ return nonEvm[address];
230
+ const lower = address.toLowerCase();
231
+ if (NATIVE_ADDRESSES.has(lower))
232
+ return `nep141:${slug}.omft.near`;
233
+ return `nep141:${slug}-${lower}.omft.near`;
234
+ }
235
+ function declaresNear(token) {
236
+ return (Array.isArray(token.settlementLayers) &&
237
+ token.settlementLayers.includes("NEAR"));
238
+ }
239
+ /**
240
+ * Whether the registry expresses NEAR coverage per token at all.
241
+ *
242
+ * Mirrors what the consumer does, which is what makes the two branches below
243
+ * mean anything: a registry declaring NEAR on no token predates the field, and
244
+ * the orchestrator then advertises every token on a NEAR-enabled chain — so
245
+ * every token is a claim and has to be checked as one. Once ANY token declares
246
+ * NEAR the field is being authored, and a token without it is a decision not to
247
+ * route, not an absence.
248
+ */
249
+ function registryDeclaresNearCoverage(registry) {
250
+ for (const entry of Object.values(registry)) {
251
+ for (const token of entry.tokens ?? []) {
252
+ if (declaresNear(token))
253
+ return true;
254
+ }
255
+ }
256
+ return false;
257
+ }
258
+ /**
259
+ * Both directions, because a token is either claimed or not and each side has
260
+ * its own failure. A token we claim that 1Click will not serve is an
261
+ * **overclaim** — the route is offered and the quote fails `tokenOut is not
262
+ * valid`. A token 1Click does serve that we do not claim is an
263
+ * **underclaim** — capability left on the table, informational rather than
264
+ * drift because nothing breaks. Checking only the first would let coverage
265
+ * silently ossify; checking every token as a claim, which is what this did
266
+ * before per-token coverage existed, reports a deliberate exclusion as an
267
+ * overclaim.
268
+ */
269
+ function diffNear(registry, vendor) {
270
+ const findings = [];
271
+ const perTokenCoverage = registryDeclaresNearCoverage(registry);
272
+ const servedSlugs = new Set(vendor.map((token) => token.blockchain));
273
+ const servedAssetIds = new Set(vendor.map((token) => token.assetId));
274
+ // 1Click returns NO `contractAddress` for a chain's gas token, while this
275
+ // registry stores one as a placeholder — so keying both sides on the raw
276
+ // address never matches a native, and `actual` reads as undefined for every
277
+ // one of them. That is invisible while the asset id resolves (the id check
278
+ // short-circuits first) and wrong the moment it stops: a native whose id has
279
+ // gone stale reports `near_token_dropped` — "1Click does not list it" — when
280
+ // 1Click does list it under a new id, and the message omits the very id
281
+ // needed to fix it. Both sides normalise to the same empty-address key.
282
+ const addressKey = (slug, address) => NATIVE_PLACEHOLDERS.has(address.toLowerCase())
283
+ ? `${slug}:`
284
+ : `${slug}:${address.toLowerCase()}`;
285
+ const bySlugAndAddress = new Map();
286
+ for (const token of vendor) {
287
+ const key = `${token.blockchain}:${(token.contractAddress ?? "").toLowerCase()}`;
288
+ if (!bySlugAndAddress.has(key))
289
+ bySlugAndAddress.set(key, token);
290
+ }
291
+ const slugToChainId = new Map(Object.entries(identity_1.NEAR_CHAIN_SLUGS).map(([id, slug]) => [slug, Number(id)]));
292
+ const claimed = new Set(claims(registry, "NEAR"));
293
+ for (const chainId of claimed) {
294
+ const entry = registry[String(chainId)];
295
+ const config = readLayerConfig(entry, "NEAR");
296
+ const slug = pickString(config, "prefix", "slug", "blockchain") ??
297
+ identity_1.NEAR_CHAIN_SLUGS[chainId];
298
+ if (!slug) {
299
+ findings.push({
300
+ layer: "NEAR",
301
+ severity: "drift",
302
+ kind: "mismatch",
303
+ code: "near_no_slug",
304
+ chainId,
305
+ message: `${label(registry, chainId)} claims NEAR but no 1Click blockchain slug is known for it — asset ids cannot be derived.`,
306
+ });
307
+ continue;
308
+ }
309
+ if (!servedSlugs.has(slug)) {
310
+ findings.push({
311
+ layer: "NEAR",
312
+ severity: "drift",
313
+ kind: "overclaim",
314
+ code: "near_blockchain_dropped",
315
+ chainId,
316
+ message: `${label(registry, chainId)} claims NEAR but 1Click lists no tokens for blockchain "${slug}".`,
317
+ });
318
+ continue;
319
+ }
320
+ for (const token of entry?.tokens ?? []) {
321
+ // The authored id is what the consumer sends, so it is what must be
322
+ // checked. Deriving here regardless would report drift on a token we
323
+ // have already corrected, and — worse — would never notice an authored
324
+ // id going stale, which is the case this check exists for.
325
+ const expected = token.nearAssetId ?? nearAssetId(slug, chainId, token.address, config);
326
+ const actual = bySlugAndAddress.get(addressKey(slug, token.address));
327
+ if (perTokenCoverage && !declaresNear(token)) {
328
+ // Scoped to tokens the registry already lists: a 1Click asset we carry
329
+ // no entry for at all is a token-registry question, not NEAR coverage.
330
+ if (actual === undefined && !(expected && servedAssetIds.has(expected)))
331
+ continue;
332
+ findings.push({
333
+ layer: "NEAR",
334
+ severity: "info",
335
+ kind: "underclaim",
336
+ code: "near_token_available",
337
+ chainId,
338
+ message: `1Click serves ${label(registry, chainId)} ${token.symbol} as ${actual?.assetId ?? expected}, but the registry does not declare NEAR for it.`,
339
+ });
340
+ continue;
341
+ }
342
+ if (!expected) {
343
+ findings.push({
344
+ layer: "NEAR",
345
+ severity: "drift",
346
+ kind: "mismatch",
347
+ code: "near_asset_id_unmapped",
348
+ chainId,
349
+ message: `${label(registry, chainId)} ${token.symbol} has no 1Click asset id mapping — a NEAR quote for it throws.`,
350
+ });
351
+ continue;
352
+ }
353
+ if (servedAssetIds.has(expected))
354
+ continue;
355
+ findings.push({
356
+ layer: "NEAR",
357
+ severity: "drift",
358
+ kind: actual ? "mismatch" : "overclaim",
359
+ code: actual ? "near_asset_id_changed" : "near_token_dropped",
360
+ chainId,
361
+ message: actual
362
+ ? `${label(registry, chainId)} ${token.symbol} resolves to ${expected} but 1Click serves it as ${actual.assetId}.`
363
+ : `${label(registry, chainId)} ${token.symbol} resolves to ${expected}, which 1Click does not list.`,
364
+ });
365
+ }
366
+ }
367
+ const unmappedSlugs = new Set();
368
+ for (const slug of servedSlugs) {
369
+ const chainId = slugToChainId.get(slug);
370
+ if (chainId === undefined) {
371
+ unmappedSlugs.add(slug);
372
+ continue;
373
+ }
374
+ if (claimed.has(chainId))
375
+ continue;
376
+ if (!registry[String(chainId)])
377
+ continue;
378
+ findings.push({
379
+ layer: "NEAR",
380
+ severity: "drift",
381
+ kind: "underclaim",
382
+ code: "near_chain_available",
383
+ chainId,
384
+ message: `1Click serves blockchain "${slug}" on ${label(registry, chainId)} but the registry does not claim NEAR there.`,
385
+ });
386
+ }
387
+ if (unmappedSlugs.size > 0) {
388
+ findings.push({
389
+ layer: "NEAR",
390
+ severity: "info",
391
+ kind: "note",
392
+ code: "near_slugs_unmapped",
393
+ message: `1Click serves ${unmappedSlugs.size} blockchains no chain of ours maps to: ${[...unmappedSlugs].sort().join(", ")}.`,
394
+ });
395
+ }
396
+ return findings;
397
+ }
398
+ // ---------------------------------------------------------------------------
399
+ // CCTP — on-chain, Circle publishes no enumeration endpoint
400
+ // ---------------------------------------------------------------------------
401
+ function diffCctp(registry, probes) {
402
+ const findings = [];
403
+ const claimed = new Set(claims(registry, "CCTP"));
404
+ for (const probe of probes) {
405
+ const { chainId } = probe;
406
+ const entry = registry[String(chainId)];
407
+ const config = readLayerConfig(entry, "CCTP");
408
+ const expectedDomain = pickNumber(config, "domainId", "domain") ?? identity_1.BASELINE_CCTP_DOMAINS[chainId];
409
+ if (!probe.reachable) {
410
+ findings.push({
411
+ layer: "CCTP",
412
+ severity: "warning",
413
+ kind: "note",
414
+ code: "cctp_rpc_unreachable",
415
+ chainId,
416
+ message: `${label(registry, chainId)} not probed — ${probe.error}.`,
417
+ });
418
+ continue;
419
+ }
420
+ if (!probe.deployed) {
421
+ if (claimed.has(chainId)) {
422
+ findings.push({
423
+ layer: "CCTP",
424
+ severity: "drift",
425
+ kind: "overclaim",
426
+ code: "cctp_not_deployed",
427
+ chainId,
428
+ message: `${label(registry, chainId)} claims CCTP but TokenMessengerV2 ${probe.messenger} has no code there.`,
429
+ });
430
+ }
431
+ continue;
432
+ }
433
+ // Checked before the claim branch: code at the TokenMessenger address is
434
+ // only evidence of a Circle deployment if the contract points at the
435
+ // MessageTransmitter every other chain reports. Without this an unrelated
436
+ // contract that happens to sit at that address reads as a new CCTP chain.
437
+ const expectedTransmitter = entry?.network === "testnet"
438
+ ? identity_1.CCTP_CONTRACTS.testnet.messageTransmitter
439
+ : identity_1.CCTP_CONTRACTS.mainnet.messageTransmitter;
440
+ if (!probe.transmitter) {
441
+ // An unread transmitter is not a confirmed deployment. Falling through
442
+ // would let an RPC or ABI failure report `cctp_chain_available` on the
443
+ // strength of code existing at the address, which is exactly what this
444
+ // check refuses to accept on its own.
445
+ findings.push({
446
+ layer: "CCTP",
447
+ severity: "warning",
448
+ kind: "note",
449
+ code: "cctp_transmitter_unreadable",
450
+ chainId,
451
+ message: `${label(registry, chainId)} has code at TokenMessengerV2 ${probe.messenger} but localMessageTransmitter() could not be read — deployment unconfirmed, neither claimed nor unclaimed reported.`,
452
+ });
453
+ continue;
454
+ }
455
+ if (probe.transmitter.toLowerCase() !== expectedTransmitter.toLowerCase()) {
456
+ findings.push({
457
+ layer: "CCTP",
458
+ severity: "drift",
459
+ kind: "mismatch",
460
+ code: "cctp_transmitter_changed",
461
+ chainId,
462
+ message: `${label(registry, chainId)} localMessageTransmitter() is ${probe.transmitter}, not the ${expectedTransmitter} every other chain reports.`,
463
+ });
464
+ continue;
465
+ }
466
+ if (!claimed.has(chainId)) {
467
+ findings.push({
468
+ layer: "CCTP",
469
+ severity: "drift",
470
+ kind: "underclaim",
471
+ code: "cctp_chain_available",
472
+ chainId,
473
+ message: `${label(registry, chainId)} runs TokenMessengerV2 ${probe.messenger} pointing at the canonical transmitter, domain ${probe.domain ?? "?"} — the registry does not claim CCTP there.`,
474
+ });
475
+ continue;
476
+ }
477
+ if (probe.domain === undefined) {
478
+ findings.push({
479
+ layer: "CCTP",
480
+ severity: "warning",
481
+ kind: "note",
482
+ code: "cctp_domain_unreadable",
483
+ chainId,
484
+ message: `${label(registry, chainId)} has TokenMessengerV2 but localDomain() could not be read.`,
485
+ });
486
+ }
487
+ else if (expectedDomain === undefined) {
488
+ findings.push({
489
+ layer: "CCTP",
490
+ severity: "drift",
491
+ kind: "mismatch",
492
+ code: "cctp_domain_missing",
493
+ chainId,
494
+ message: `${label(registry, chainId)} claims CCTP but no domain id is recorded — on-chain says ${probe.domain}.`,
495
+ });
496
+ }
497
+ else if (expectedDomain !== probe.domain) {
498
+ findings.push({
499
+ layer: "CCTP",
500
+ severity: "drift",
501
+ kind: "mismatch",
502
+ code: "cctp_domain_mismatch",
503
+ chainId,
504
+ message: `${label(registry, chainId)} domain disagrees — we hold ${expectedDomain}, on-chain says ${probe.domain}.`,
505
+ });
506
+ }
507
+ }
508
+ const probed = new Set(probes.map((probe) => probe.chainId));
509
+ for (const chainId of claimed) {
510
+ if (probed.has(chainId))
511
+ continue;
512
+ findings.push({
513
+ layer: "CCTP",
514
+ severity: "warning",
515
+ kind: "note",
516
+ code: "cctp_not_probed",
517
+ chainId,
518
+ message: `${label(registry, chainId)} claims CCTP but was not probed (non-EVM chains are out of scope for the on-chain sweep).`,
519
+ });
520
+ }
521
+ return findings;
522
+ }
523
+ // ---------------------------------------------------------------------------
524
+ // Registry self-consistency — chain-level settlementLayers vs per-layer config
525
+ // ---------------------------------------------------------------------------
526
+ const BASELINE_LAYER_CHAINS = {
527
+ RHINO: identity_1.BASELINE_RHINO_CHAIN_IDS,
528
+ NEAR: identity_1.BASELINE_NEAR_CHAIN_IDS,
529
+ CCTP: Object.keys(identity_1.BASELINE_CCTP_DOMAINS).map(Number),
530
+ };
531
+ function diffRegistryConsistency(registry) {
532
+ const findings = [];
533
+ for (const layer of ["RHINO", "NEAR", "CCTP"]) {
534
+ const claimed = new Set(claims(registry, layer));
535
+ const registryConfigured = chainIds(registry).filter((id) => readLayerConfig(registry[String(id)], layer) !== undefined);
536
+ const configured = new Set(registryConfigured.length > 0
537
+ ? registryConfigured
538
+ : BASELINE_LAYER_CHAINS[layer]);
539
+ for (const chainId of claimed) {
540
+ if (configured.has(chainId))
541
+ continue;
542
+ findings.push({
543
+ layer: "REGISTRY",
544
+ severity: "drift",
545
+ kind: "mismatch",
546
+ code: "layer_claimed_without_config",
547
+ chainId,
548
+ message: `${label(registry, chainId)} lists ${layer} in settlementLayers but the ${layer} config has no entry for it.`,
549
+ });
550
+ }
551
+ for (const chainId of configured) {
552
+ if (claimed.has(chainId))
553
+ continue;
554
+ findings.push({
555
+ layer: "REGISTRY",
556
+ severity: "drift",
557
+ kind: "mismatch",
558
+ code: registry[String(chainId)]
559
+ ? "layer_config_without_claim"
560
+ : "layer_config_for_unknown_chain",
561
+ chainId,
562
+ message: registry[String(chainId)]
563
+ ? `${label(registry, chainId)} has ${layer} config but does not list ${layer} in settlementLayers.`
564
+ : `${layer} config carries chain ${chainId}, which is not in the registry at all.`,
565
+ });
566
+ }
567
+ }
568
+ return findings;
569
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Chain identity across three settlement-layer vendors, plus the pre-migration
3
+ * fallback for vendor detail `configs/chains.json` does not carry yet.
4
+ *
5
+ * Identity is not derivable and not published by the vendor: Rhino names chains
6
+ * (`MATIC_POS`) and its `networkId` is not a chain id off the EVM set (SOLANA is
7
+ * `900`, and `BITCOIN`/`SN_MAIN`/`TON` are strings); 1Click uses its own slug
8
+ * (`pol`, `avax`). Both tables are deliberately wider than what we claim — a
9
+ * vendor chain with no entry here can only be reported as unmapped, never as a
10
+ * chain we could newly serve.
11
+ */
12
+ export type SettlementLayerName = "RHINO" | "NEAR" | "CCTP";
13
+ export declare const RHINO_CHAIN_NAMES: Record<number, string>;
14
+ export declare const NEAR_CHAIN_SLUGS: Record<number, string>;
15
+ /**
16
+ * Per-token 1Click asset ids for non-EVM chains. 1Click wraps each non-EVM
17
+ * token under a NEAR-internal hex id that the EVM
18
+ * `nep141:{slug}-{address}.omft.near` derivation cannot produce, so these
19
+ * cannot be computed from the registry address.
20
+ *
21
+ * Keyed by the chain-native address as `chains.json` stores it — base58 is
22
+ * case-sensitive, so these keys are not lowercased.
23
+ */
24
+ export declare const NEAR_NON_EVM_ASSET_IDS: Record<number, Record<string, string>>;
25
+ /**
26
+ * Vendor detail that `configs/chains.json` does not carry yet — the per-layer
27
+ * maps the orchestrator holds in `src/settlement/layer/{rhino,cctp}/config.ts`
28
+ * and `near/httpClient.ts`.
29
+ *
30
+ * Read only where the registry entry has no `settlementLayerConfig` for the
31
+ * layer, so it becomes dead the moment that config lands in the registry.
32
+ * Deleting an entry here silently narrows what the check compares — do not
33
+ * prune it to "fix" a finding.
34
+ */
35
+ export declare const BASELINE_RHINO_BRIDGE_CONTRACTS: Record<number, string>;
36
+ /** Chains the orchestrator's own Rhino map covers, contract or not. */
37
+ export declare const BASELINE_RHINO_CHAIN_IDS: number[];
38
+ /** Chains the orchestrator's own NEAR prefix map covers. */
39
+ export declare const BASELINE_NEAR_CHAIN_IDS: number[];
40
+ export declare const BASELINE_CCTP_DOMAINS: Record<number, number>;
41
+ /**
42
+ * TokenMessengerV2 and MessageTransmitterV2 are each deployed at one address
43
+ * across every EVM chain, but mainnet and testnet use different ones —
44
+ * probing a testnet with the mainnet address reads "not deployed" on four
45
+ * chains that are fine.
46
+ */
47
+ export declare const CCTP_CONTRACTS: {
48
+ readonly mainnet: {
49
+ readonly tokenMessenger: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d";
50
+ readonly messageTransmitter: "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64";
51
+ };
52
+ readonly testnet: {
53
+ readonly tokenMessenger: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
54
+ readonly messageTransmitter: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
55
+ };
56
+ };
57
+ export declare const RHINO_CONFIGS_URL = "https://api.rhino.fi/bridge/configs";
58
+ export declare const ONECLICK_TOKENS_URL = "https://1click.chaindefuser.com/v0/tokens";