@rhinestone/shared-configs 1.11.1 → 1.12.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.
@@ -6,6 +6,7 @@
6
6
  "addressPath": "/address/",
7
7
  "txPath": "/tx/"
8
8
  },
9
+ "iconSlug": "mainnet",
9
10
  "name": "Ethereum",
10
11
  "nativeToken": {
11
12
  "address": "0x0000000000000000000000000000000000000000",
@@ -13,6 +14,7 @@
13
14
  "symbol": "ETH"
14
15
  },
15
16
  "network": "mainnet",
17
+ "publicRpcUrl": "https://eth.merkle.io",
16
18
  "settlementLayers": [
17
19
  "ACROSS",
18
20
  "ECO",
@@ -96,6 +98,7 @@
96
98
  "addressPath": "/address/",
97
99
  "txPath": "/tx/"
98
100
  },
101
+ "iconSlug": "optimism",
99
102
  "name": "OP Mainnet",
100
103
  "nativeToken": {
101
104
  "address": "0x0000000000000000000000000000000000000000",
@@ -103,6 +106,7 @@
103
106
  "symbol": "ETH"
104
107
  },
105
108
  "network": "mainnet",
109
+ "publicRpcUrl": "https://mainnet.optimism.io",
106
110
  "settlementLayers": [
107
111
  "ACROSS",
108
112
  "ECO",
@@ -197,6 +201,7 @@
197
201
  "addressPath": "/address/",
198
202
  "txPath": "/tx/"
199
203
  },
204
+ "iconSlug": "bsc",
200
205
  "name": "BNB Smart Chain",
201
206
  "nativeToken": {
202
207
  "address": "0x0000000000000000000000000000000000000000",
@@ -204,6 +209,7 @@
204
209
  "symbol": "BNB"
205
210
  },
206
211
  "network": "mainnet",
212
+ "publicRpcUrl": "https://56.rpc.thirdweb.com",
207
213
  "settlementLayers": [
208
214
  "ACROSS",
209
215
  "ECO",
@@ -271,6 +277,7 @@
271
277
  "addressPath": "/address/",
272
278
  "txPath": "/tx/"
273
279
  },
280
+ "iconSlug": "gnosis",
274
281
  "name": "Gnosis",
275
282
  "nativeToken": {
276
283
  "address": "0x0000000000000000000000000000000000000000",
@@ -278,6 +285,7 @@
278
285
  "symbol": "XDAI"
279
286
  },
280
287
  "network": "mainnet",
288
+ "publicRpcUrl": "https://rpc.gnosischain.com",
281
289
  "settlementLayers": [
282
290
  "RELAY",
283
291
  "NEAR",
@@ -312,6 +320,7 @@
312
320
  "addressPath": "/address/",
313
321
  "txPath": "/tx/"
314
322
  },
323
+ "iconSlug": "unichain",
315
324
  "name": "Unichain",
316
325
  "nativeToken": {
317
326
  "address": "0x0000000000000000000000000000000000000000",
@@ -319,6 +328,7 @@
319
328
  "symbol": "ETH"
320
329
  },
321
330
  "network": "mainnet",
331
+ "publicRpcUrl": "https://mainnet.unichain.org",
322
332
  "settlementLayers": [
323
333
  "ACROSS",
324
334
  "ECO",
@@ -401,6 +411,7 @@
401
411
  "addressPath": "/address/",
402
412
  "txPath": "/tx/"
403
413
  },
414
+ "iconSlug": "polygon",
404
415
  "name": "Polygon",
405
416
  "nativeToken": {
406
417
  "address": "0x0000000000000000000000000000000000000000",
@@ -408,6 +419,7 @@
408
419
  "symbol": "POL"
409
420
  },
410
421
  "network": "mainnet",
422
+ "publicRpcUrl": "https://polygon.drpc.org",
411
423
  "settlementLayers": [
412
424
  "ACROSS",
413
425
  "ECO",
@@ -478,6 +490,7 @@
478
490
  "addressPath": "/address/",
479
491
  "txPath": "/tx/"
480
492
  },
493
+ "iconSlug": "monad",
481
494
  "name": "Monad",
482
495
  "nativeToken": {
483
496
  "address": "0x0000000000000000000000000000000000000000",
@@ -485,6 +498,7 @@
485
498
  "symbol": "MON"
486
499
  },
487
500
  "network": "mainnet",
501
+ "publicRpcUrl": "https://rpc.monad.xyz",
488
502
  "settlementLayers": [
489
503
  "ACROSS",
490
504
  "RELAY",
@@ -553,6 +567,7 @@
553
567
  "addressPath": "/address/",
554
568
  "txPath": "/tx/"
555
569
  },
570
+ "iconSlug": "sonic",
556
571
  "name": "Sonic",
557
572
  "nativeToken": {
558
573
  "address": "0x0000000000000000000000000000000000000000",
@@ -560,6 +575,7 @@
560
575
  "symbol": "S"
561
576
  },
562
577
  "network": "mainnet",
578
+ "publicRpcUrl": "https://rpc.soniclabs.com",
563
579
  "settlementLayers": [
564
580
  "ECO",
565
581
  "RELAY",
@@ -600,6 +616,7 @@
600
616
  "addressPath": "/address/",
601
617
  "txPath": "/tx/"
602
618
  },
619
+ "iconSlug": "hyperevm",
603
620
  "name": "HyperEVM",
604
621
  "nativeToken": {
605
622
  "address": "0x0000000000000000000000000000000000000000",
@@ -607,6 +624,7 @@
607
624
  "symbol": "HYPE"
608
625
  },
609
626
  "network": "mainnet",
627
+ "publicRpcUrl": "https://rpc.hyperliquid.xyz/evm",
610
628
  "settlementLayers": [
611
629
  "ACROSS",
612
630
  "ECO",
@@ -658,6 +676,7 @@
658
676
  "txPath": "/explorer/tx/",
659
677
  "url": "https://app.hyperliquid.xyz"
660
678
  },
679
+ "iconSlug": "hyperevm",
661
680
  "name": "HyperCore",
662
681
  "nativeToken": {
663
682
  "address": "0x0000000000000000000000000000000000000000",
@@ -697,6 +716,7 @@
697
716
  "addressPath": "/address/",
698
717
  "txPath": "/tx/"
699
718
  },
719
+ "iconSlug": "soneium",
700
720
  "name": "Soneium Mainnet",
701
721
  "nativeToken": {
702
722
  "address": "0x0000000000000000000000000000000000000000",
@@ -704,6 +724,7 @@
704
724
  "symbol": "ETH"
705
725
  },
706
726
  "network": "mainnet",
727
+ "publicRpcUrl": "https://rpc.soneium.org",
707
728
  "settlementLayers": [
708
729
  "ACROSS",
709
730
  "RELAY"
@@ -771,6 +792,7 @@
771
792
  "addressPath": "/address/",
772
793
  "txPath": "/tx/"
773
794
  },
795
+ "iconSlug": "robinhood",
774
796
  "name": "Robinhood Chain",
775
797
  "nativeToken": {
776
798
  "address": "0x0000000000000000000000000000000000000000",
@@ -778,6 +800,7 @@
778
800
  "symbol": "ETH"
779
801
  },
780
802
  "network": "mainnet",
803
+ "publicRpcUrl": "https://rpc.mainnet.chain.robinhood.com",
781
804
  "settlementLayers": [
782
805
  "ACROSS",
783
806
  "RELAY"
@@ -834,6 +857,7 @@
834
857
  "addressPath": "/address/",
835
858
  "txPath": "/tx/"
836
859
  },
860
+ "iconSlug": "base",
837
861
  "name": "Base",
838
862
  "nativeToken": {
839
863
  "address": "0x0000000000000000000000000000000000000000",
@@ -841,6 +865,7 @@
841
865
  "symbol": "ETH"
842
866
  },
843
867
  "network": "mainnet",
868
+ "publicRpcUrl": "https://mainnet.base.org",
844
869
  "settlementLayers": [
845
870
  "ACROSS",
846
871
  "ECO",
@@ -909,6 +934,7 @@
909
934
  "addressPath": "/address/",
910
935
  "txPath": "/tx/"
911
936
  },
937
+ "iconSlug": "plasma",
912
938
  "name": "Plasma",
913
939
  "nativeToken": {
914
940
  "address": "0x0000000000000000000000000000000000000000",
@@ -916,6 +942,7 @@
916
942
  "symbol": "XPL"
917
943
  },
918
944
  "network": "mainnet",
945
+ "publicRpcUrl": "https://rpc.plasma.to",
919
946
  "settlementLayers": [
920
947
  "ACROSS",
921
948
  "ECO",
@@ -958,6 +985,7 @@
958
985
  "addressPath": "/address/",
959
986
  "txPath": "/tx/"
960
987
  },
988
+ "iconSlug": "plasma",
961
989
  "name": "Plasma Testnet",
962
990
  "nativeToken": {
963
991
  "address": "0x0000000000000000000000000000000000000000",
@@ -965,6 +993,7 @@
965
993
  "symbol": "XPL"
966
994
  },
967
995
  "network": "testnet",
996
+ "publicRpcUrl": "https://testnet-rpc.plasma.to",
968
997
  "settlementLayers": [
969
998
  "ECO"
970
999
  ],
@@ -1009,6 +1038,7 @@
1009
1038
  "addressPath": "/address/",
1010
1039
  "txPath": "/tx/"
1011
1040
  },
1041
+ "iconSlug": "arbitrum",
1012
1042
  "name": "Arbitrum One",
1013
1043
  "nativeToken": {
1014
1044
  "address": "0x0000000000000000000000000000000000000000",
@@ -1016,6 +1046,7 @@
1016
1046
  "symbol": "ETH"
1017
1047
  },
1018
1048
  "network": "mainnet",
1049
+ "publicRpcUrl": "https://arb1.arbitrum.io/rpc",
1019
1050
  "settlementLayers": [
1020
1051
  "ACROSS",
1021
1052
  "ECO",
@@ -1099,6 +1130,7 @@
1099
1130
  "addressPath": "/address/",
1100
1131
  "txPath": "/tx/"
1101
1132
  },
1133
+ "iconSlug": "avalanche",
1102
1134
  "name": "Avalanche",
1103
1135
  "nativeToken": {
1104
1136
  "address": "0x0000000000000000000000000000000000000000",
@@ -1106,6 +1138,7 @@
1106
1138
  "symbol": "AVAX"
1107
1139
  },
1108
1140
  "network": "mainnet",
1141
+ "publicRpcUrl": "https://api.avax.network/ext/bc/C/rpc",
1109
1142
  "settlementLayers": [
1110
1143
  "ACROSS",
1111
1144
  "RELAY",
@@ -1182,6 +1215,7 @@
1182
1215
  "addressPath": "/address/",
1183
1216
  "txPath": "/tx/"
1184
1217
  },
1218
+ "iconSlug": "base",
1185
1219
  "name": "Base Sepolia",
1186
1220
  "nativeToken": {
1187
1221
  "address": "0x0000000000000000000000000000000000000000",
@@ -1189,6 +1223,7 @@
1189
1223
  "symbol": "ETH"
1190
1224
  },
1191
1225
  "network": "testnet",
1226
+ "publicRpcUrl": "https://sepolia.base.org",
1192
1227
  "settlementLayers": [
1193
1228
  "ACROSS",
1194
1229
  "ECO",
@@ -1263,6 +1298,7 @@
1263
1298
  "addressPath": "/address/",
1264
1299
  "txPath": "/tx/"
1265
1300
  },
1301
+ "iconSlug": "arbitrum",
1266
1302
  "name": "Arbitrum Sepolia",
1267
1303
  "nativeToken": {
1268
1304
  "address": "0x0000000000000000000000000000000000000000",
@@ -1270,6 +1306,7 @@
1270
1306
  "symbol": "ETH"
1271
1307
  },
1272
1308
  "network": "testnet",
1309
+ "publicRpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
1273
1310
  "settlementLayers": [
1274
1311
  "ACROSS",
1275
1312
  "ECO",
@@ -1330,6 +1367,7 @@
1330
1367
  "addressPath": "/address/",
1331
1368
  "txPath": "/tx/"
1332
1369
  },
1370
+ "iconSlug": "katana",
1333
1371
  "name": "Katana",
1334
1372
  "nativeToken": {
1335
1373
  "address": "0x0000000000000000000000000000000000000000",
@@ -1337,6 +1375,7 @@
1337
1375
  "symbol": "ETH"
1338
1376
  },
1339
1377
  "network": "mainnet",
1378
+ "publicRpcUrl": "https://rpc.katana.network",
1340
1379
  "settlementLayers": [
1341
1380
  "RELAY",
1342
1381
  "RHINO"
@@ -1394,6 +1433,7 @@
1394
1433
  "txPath": "/explorer/tx/",
1395
1434
  "url": "https://app.hyperliquid.xyz"
1396
1435
  },
1436
+ "iconSlug": "hyperevm",
1397
1437
  "name": "HyperCore Spot",
1398
1438
  "nativeToken": {
1399
1439
  "address": "0x0000000000000000000000000000000000000000",
@@ -1433,6 +1473,7 @@
1433
1473
  "txPath": "/explorer/tx/",
1434
1474
  "url": "https://app.hyperliquid.xyz"
1435
1475
  },
1476
+ "iconSlug": "hyperevm",
1436
1477
  "name": "HyperCore Perp",
1437
1478
  "nativeToken": {
1438
1479
  "address": "0x0000000000000000000000000000000000000000",
@@ -1472,6 +1513,7 @@
1472
1513
  "addressPath": "/address/",
1473
1514
  "txPath": "/tx/"
1474
1515
  },
1516
+ "iconSlug": "mainnet",
1475
1517
  "name": "Sepolia",
1476
1518
  "nativeToken": {
1477
1519
  "address": "0x0000000000000000000000000000000000000000",
@@ -1479,6 +1521,7 @@
1479
1521
  "symbol": "ETH"
1480
1522
  },
1481
1523
  "network": "testnet",
1524
+ "publicRpcUrl": "https://11155111.rpc.thirdweb.com",
1482
1525
  "settlementLayers": [
1483
1526
  "ACROSS",
1484
1527
  "ECO",
@@ -1549,6 +1592,7 @@
1549
1592
  "addressPath": "/address/",
1550
1593
  "txPath": "/tx/"
1551
1594
  },
1595
+ "iconSlug": "optimism",
1552
1596
  "name": "OP Sepolia",
1553
1597
  "nativeToken": {
1554
1598
  "address": "0x0000000000000000000000000000000000000000",
@@ -1556,6 +1600,7 @@
1556
1600
  "symbol": "ETH"
1557
1601
  },
1558
1602
  "network": "testnet",
1603
+ "publicRpcUrl": "https://sepolia.optimism.io",
1559
1604
  "settlementLayers": [
1560
1605
  "ACROSS",
1561
1606
  "ECO",
@@ -1616,6 +1661,7 @@
1616
1661
  "txPath": "/#/transaction/",
1617
1662
  "url": "https://tronscan.org"
1618
1663
  },
1664
+ "iconSlug": "tron",
1619
1665
  "name": "Tron",
1620
1666
  "nativeToken": {
1621
1667
  "address": "0x0000000000000000000000000000000000000000",
@@ -1662,6 +1708,7 @@
1662
1708
  "txPath": "/tx/",
1663
1709
  "url": "https://solscan.io"
1664
1710
  },
1711
+ "iconSlug": "solana",
1665
1712
  "name": "Solana",
1666
1713
  "nativeToken": {
1667
1714
  "address": "11111111111111111111111111111111",
@@ -73,6 +73,12 @@ function errorsFor(chain) {
73
73
  ["an explorer url with a trailing slash", {
74
74
  explorer: { url: "https://etherscan.io/", addressPath: "/address/", txPath: "/tx/" },
75
75
  }],
76
+ // A stem, not a path or a URL — the whole point of publishing a slug is that
77
+ // the artifact says nothing about where the file is served from.
78
+ ["an iconSlug that is a full URL", { iconSlug: "https://s3.rhinestone.dev/chains/base.svg" }],
79
+ ["an iconSlug carrying its extension", { iconSlug: "base.svg" }],
80
+ ["an iconSlug carrying a directory", { iconSlug: "chains/base" }],
81
+ ["an iconSlug that is not a string", { iconSlug: 42 }],
76
82
  ])("rejects %s", (_label, overrides) => {
77
83
  (0, bun_test_1.expect)(errorsFor(validChain(overrides)).length).toBeGreaterThan(0);
78
84
  });
@@ -387,6 +393,145 @@ function errorsFor(chain) {
387
393
  }
388
394
  });
389
395
  });
396
+ (0, bun_test_1.describe)("classifyChainPublicRpcUrls", () => {
397
+ const nets = (mainnets, testnets = []) => ({ mainnets, testnets });
398
+ (0, bun_test_1.it)("derives the endpoint from viem's own chain definition", () => {
399
+ const { chains, issues } = (0, validation_1.classifyChainPublicRpcUrls)({ "8453": validChain({ name: "Base" }) }, nets([{ id: 8453, viemChain: "base" }]));
400
+ (0, bun_test_1.expect)(chains["8453"].publicRpcUrl).toBe("https://mainnet.base.org");
401
+ (0, bun_test_1.expect)(issues).toEqual([]);
402
+ });
403
+ // The regression this exists for, and the reason it is worse here than for
404
+ // explorers: viem's id 999 is claimed by hyperEvm, hyperliquid, wanchainTestnet
405
+ // and zoraTestnet. An id lookup would publish a live endpoint for the wrong
406
+ // chain, which fails at connect time rather than at render time.
407
+ (0, bun_test_1.it)("resolves by declared viemChain name, never by chain id", () => {
408
+ const { chains } = (0, validation_1.classifyChainPublicRpcUrls)({ "999": validChain({ name: "HyperEVM", caip2: "eip155:999" }) }, nets([{ id: 999, viemChain: "hyperEvm" }]));
409
+ (0, bun_test_1.expect)(chains["999"].publicRpcUrl).toBe("https://rpc.hyperliquid.xyz/evm");
410
+ });
411
+ (0, bun_test_1.it)("lets an authored endpoint win, for a chain viem does not ship", () => {
412
+ const { chains, issues } = (0, validation_1.classifyChainPublicRpcUrls)({
413
+ "424242": validChain({
414
+ name: "Newchain",
415
+ publicRpcUrl: "https://rpc.newchain.example",
416
+ }),
417
+ }, nets([{ id: 424242 }]));
418
+ (0, bun_test_1.expect)(chains["424242"].publicRpcUrl).toBe("https://rpc.newchain.example");
419
+ (0, bun_test_1.expect)(issues).toEqual([]);
420
+ });
421
+ // A browser on an https page cannot call an http endpoint at all, so an
422
+ // authored one is a URL that never connects rather than a working fallback.
423
+ (0, bun_test_1.it)("errors on an authored endpoint that is not https", () => {
424
+ const { issues } = (0, validation_1.classifyChainPublicRpcUrls)({ "1": validChain({ publicRpcUrl: "http://rpc.example" }) }, nets([{ id: 1, viemChain: "mainnet" }]));
425
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
426
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
427
+ });
428
+ // Absent is a real answer — the chain is display-only — so a real EVM chain
429
+ // with nothing to derive warns rather than failing the build.
430
+ (0, bun_test_1.it)("warns and leaves the field absent when nothing can supply one", () => {
431
+ const { chains, issues } = (0, validation_1.classifyChainPublicRpcUrls)({ "424242": validChain({ name: "Newchain" }) }, nets([{ id: 424242 }]));
432
+ (0, bun_test_1.expect)("publicRpcUrl" in chains["424242"]).toBe(false);
433
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
434
+ (0, bun_test_1.expect)(issues[0].severity).toBe("warning");
435
+ });
436
+ // Non-EVM and virtual chains are never synthesised as viem chains, so there
437
+ // is nothing for them to be missing and nothing worth reporting.
438
+ (0, bun_test_1.it)("stays silent for chains that are never built as viem chains", () => {
439
+ const { issues } = (0, validation_1.classifyChainPublicRpcUrls)({
440
+ "792703809": validChain({ name: "Solana", vmType: "svm" }),
441
+ "1337": validChain({ name: "HyperCore", virtual: true }),
442
+ }, nets([{ id: 792703809 }, { id: 1337 }]));
443
+ (0, bun_test_1.expect)(issues).toEqual([]);
444
+ });
445
+ // Asserted against the shipped registry rather than by re-running the
446
+ // classifier, so this covers what actually reaches the artifact.
447
+ (0, bun_test_1.it)("gives every real EVM chain a well-formed https endpoint", () => {
448
+ for (const [id, entry] of Object.entries(index_1.chainRegistry)) {
449
+ const e = entry;
450
+ const isSynthesisable = e.vmType === "evm" && e.virtual !== true;
451
+ if (!isSynthesisable) {
452
+ (0, bun_test_1.expect)("publicRpcUrl" in e, id).toBe(false);
453
+ continue;
454
+ }
455
+ (0, bun_test_1.expect)(e.publicRpcUrl, id).toMatch(/^https:\/\/\S+$/);
456
+ (0, bun_test_1.expect)(e.publicRpcUrl.endsWith("/"), id).toBe(false);
457
+ }
458
+ });
459
+ });
460
+ (0, bun_test_1.describe)("validateChainIconSlugs", () => {
461
+ (0, bun_test_1.it)("passes a chain that carries a mark", () => {
462
+ (0, bun_test_1.expect)((0, validation_1.validateChainIconSlugs)({ "8453": validChain({ name: "Base", iconSlug: "base" }) })).toEqual([]);
463
+ });
464
+ // The whole point of the gate. `iconSlug` is authored by hand, so a chain
465
+ // added without one produces a blank tile in production and nothing anywhere
466
+ // reports it — no type, no test, no log. This turns that into a build failure
467
+ // while someone is already editing the registry.
468
+ (0, bun_test_1.it)("errors when a chain has neither a mark nor an exemption", () => {
469
+ const issues = (0, validation_1.validateChainIconSlugs)({
470
+ "424242": validChain({ name: "Newchain" }),
471
+ });
472
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
473
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
474
+ (0, bun_test_1.expect)(issues[0].chainId).toBe("424242");
475
+ // Both remedies are named, because which one is right is a product call.
476
+ (0, bun_test_1.expect)(issues[0].message).toContain("s3-public");
477
+ (0, bun_test_1.expect)(issues[0].message).toContain("CHAINS_WITHOUT_ICON");
478
+ });
479
+ // Absence is allowed, but only as a decision written down in code. The real
480
+ // exemption map is empty, so these two inject one rather than requiring some
481
+ // chain be left un-iconed purely to have something to assert against.
482
+ const exempt = new Map([["424242", "Newchain — no mark published"]]);
483
+ (0, bun_test_1.it)("accepts an absent mark for a chain listed in CHAINS_WITHOUT_ICON", () => {
484
+ (0, bun_test_1.expect)((0, validation_1.validateChainIconSlugs)({ "424242": validChain({ name: "Newchain" }) }, exempt)).toEqual([]);
485
+ });
486
+ // A stale exemption permanently excuses the chain it names, so the next person
487
+ // to drop that slug gets no warning at all.
488
+ (0, bun_test_1.it)("errors when an exempted chain gains a mark, so the exemption cannot rot", () => {
489
+ const issues = (0, validation_1.validateChainIconSlugs)({ "424242": validChain({ name: "Newchain", iconSlug: "newchain" }) }, exempt);
490
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
491
+ (0, bun_test_1.expect)(issues[0].severity).toBe("error");
492
+ (0, bun_test_1.expect)(issues[0].message).toContain("remove the exemption");
493
+ });
494
+ bun_test_1.it.each([
495
+ ["a full URL", "https://s3.rhinestone.dev/chains/base.svg"],
496
+ ["an extension", "base.svg"],
497
+ ["a directory", "chains/base"],
498
+ ["uppercase", "Base"],
499
+ ])("errors on a slug carrying %s", (_label, iconSlug) => {
500
+ const issues = (0, validation_1.validateChainIconSlugs)({
501
+ "8453": validChain({ name: "Base", iconSlug }),
502
+ });
503
+ (0, bun_test_1.expect)(issues).toHaveLength(1);
504
+ (0, bun_test_1.expect)(issues[0].message).toContain("malformed");
505
+ });
506
+ (0, bun_test_1.it)("passes the real generated registry", () => {
507
+ (0, bun_test_1.expect)((0, validation_1.validateChainIconSlugs)(index_1.chainRegistry)).toEqual([]);
508
+ });
509
+ // Asserted against the shipped registry rather than by re-running the check,
510
+ // so this covers what actually reaches the artifact.
511
+ (0, bun_test_1.it)("gives every shipped chain a mark", () => {
512
+ const withoutIcon = Object.entries(index_1.chainRegistry)
513
+ .filter(([, e]) => e.iconSlug === undefined)
514
+ .map(([id]) => id);
515
+ (0, bun_test_1.expect)(withoutIcon).toEqual([]);
516
+ });
517
+ // Two reuse rules that would otherwise look like copy-paste errors in the
518
+ // jsonnet. A testnet is visually the same chain as its mainnet, and all three
519
+ // HyperCore ids are Hyperliquid — the same branding as HyperEVM.
520
+ bun_test_1.it.each([
521
+ ["11155111", "1"],
522
+ ["11155420", "10"],
523
+ ["84532", "8453"],
524
+ ["421614", "42161"],
525
+ ["9746", "9745"],
526
+ ["1337", "999"],
527
+ ["1337001", "999"],
528
+ ["1337002", "999"],
529
+ ])("reuses chain %s's glyph from chain %s", (child, parent) => {
530
+ const slug = index_1.chainRegistry[parent].iconSlug;
531
+ (0, bun_test_1.expect)(slug).toBeDefined();
532
+ (0, bun_test_1.expect)(index_1.chainRegistry[child].iconSlug).toBe(slug);
533
+ });
534
+ });
390
535
  (0, bun_test_1.describe)("classifyChainStacks", () => {
391
536
  const nets = (mainnets, testnets = []) => ({ mainnets, testnets });
392
537
  // The common case: nothing is authored anywhere. viem already knows what Base
@@ -132,6 +132,14 @@ function renderChains(chainRegistry, mainnets, testnets) {
132
132
  " caip2: string;",
133
133
  " /** Block explorer, or null for a chain that has none (render plain text). */",
134
134
  " explorer: Explorer | null;",
135
+ " /** Filename stem of this chain's mark in s3-public's `chains/` directory,",
136
+ " * served today at `https://s3.rhinestone.dev/chains/<slug>.svg`. A slug and",
137
+ " * not a URL so the CDN stays a consumer's concern. Absent means no mark —",
138
+ " * render a coloured initial chip, never an error. */",
139
+ " iconSlug?: string;",
140
+ " /** Public, unauthenticated https JSON-RPC endpoint — what a browser needs to",
141
+ " * build this chain with `defineChain`. Absent means display-only. */",
142
+ " publicRpcUrl?: string;",
135
143
  " /** True for virtual chains (e.g. HyperCore) that settle on another chain. */",
136
144
  " virtual?: boolean;",
137
145
  " /** Which directions of transfer the chain supports; absent means both.",
@@ -394,12 +402,22 @@ async function generate() {
394
402
  const { chains: caip2Classified, issues: caip2Issues } = (0, validation_1.classifyChainCaip2)(stackClassified);
395
403
  // Then the block explorer, so a consumer rendering a link for a chain added
396
404
  // after its release does not need a hand-maintained table of its own.
397
- const { chains: classifiedChains, issues: explorerIssues } = (0, validation_1.classifyChainExplorers)(caip2Classified, output.networks);
405
+ const { chains: explorerClassified, issues: explorerIssues } = (0, validation_1.classifyChainExplorers)(caip2Classified, output.networks);
406
+ // Then the public RPC endpoint, the last thing a browser consumer needs to
407
+ // build a viem `Chain` for a chain it was never compiled against — and the
408
+ // one that decides whether that chain is connectable or only displayable.
409
+ const { chains: classifiedChains, issues: publicRpcUrlIssues } = (0, validation_1.classifyChainPublicRpcUrls)(explorerClassified, output.networks);
410
+ // `iconSlug` is authored, not derived, so there is nothing to classify — only
411
+ // to insist on. A chain added without one would otherwise reach production as
412
+ // a blank tile, which nothing anywhere reports.
413
+ const iconSlugIssues = (0, validation_1.validateChainIconSlugs)(classifiedChains);
398
414
  const classificationIssues = [
399
415
  ...networkIssues,
400
416
  ...stackIssues,
401
417
  ...caip2Issues,
402
418
  ...explorerIssues,
419
+ ...publicRpcUrlIssues,
420
+ ...iconSlugIssues,
403
421
  ];
404
422
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "chains.json"), `${JSON.stringify(classifiedChains, null, 2)}\n`);
405
423
  await (0, promises_1.writeFile)(node_path_1.default.join(configsDir, "mainnets.json"), `${JSON.stringify(output.mainnets, null, 2)}\n`);
@@ -122,6 +122,57 @@ export declare function classifyChainExplorers(chains: ChainRegistry, networks:
122
122
  chains: ChainRegistry;
123
123
  issues: ValidationIssue[];
124
124
  };
125
+ /**
126
+ * Fails the build for a chain that has neither a mark nor an explicit exemption.
127
+ *
128
+ * Why an error and not a warning: `iconSlug` is authored by hand, so the failure
129
+ * mode of a new chain is silence — nobody forgets a field they were never
130
+ * prompted for, and the result is a blank tile in production that no test, type
131
+ * or log names. The gate turns that into a build failure at the moment someone
132
+ * is already editing the registry, which is the only point where the decision is
133
+ * cheap. CHAINS_WITHOUT_ICON is how "this chain genuinely has no mark" is said
134
+ * out loud, in code review, rather than being indistinguishable from an oversight.
135
+ *
136
+ * Deliberately offline. Whether `chains/<slug>.svg` actually exists on the CDN is
137
+ * not checked and must not be: the build would then fail on a CDN blip or in a
138
+ * sandboxed CI runner, which is a far more frequent failure than a typo'd slug
139
+ * and produces a red build with nothing wrong in the diff. The cost of a bad slug
140
+ * is one missing image; the cost of a network-dependent build gate is every build.
141
+ */
142
+ export declare function validateChainIconSlugs(chains: ChainRegistry, exemptions?: ReadonlyMap<string, string>): ValidationIssue[];
143
+ /**
144
+ * Stamps every chain with a public JSON-RPC endpoint, derived from the chain's
145
+ * viem definition where it has one and authored in the jsonnet where it does
146
+ * not.
147
+ *
148
+ * Why this exists: a browser consumer building a viem `Chain` with
149
+ * `defineChain` — for a chain newer than its pinned viem — already gets id,
150
+ * name, native currency, `explorer` and `stack` from this registry. The RPC
151
+ * endpoint was the only missing piece, and it is the piece that decides whether
152
+ * a newly added chain is a first-class wallet-connect target or display-only.
153
+ * It is the PUBLIC endpoint on purpose: the rpc-proxy path needs an API key,
154
+ * which is the one thing a browser cannot have.
155
+ *
156
+ * Resolved by the entry's declared `viemChain` **name**, never by chain id, for
157
+ * the same reason as `classifyChainExplorers` — only worse here, because the
158
+ * consequence is a live endpoint for the wrong chain rather than a dead link.
159
+ * viem 2.55 has 23 chain ids claimed by more than one export: id 999 is
160
+ * `hyperEvm`, `hyperliquid`, `wanchainTestnet` and `zoraTestnet`, so an id
161
+ * lookup for HyperEVM can hand out a Zora testnet RPC.
162
+ *
163
+ * A missing endpoint is not an error — it resolves to the field being absent,
164
+ * and a consumer treats that chain as display-only. It is worth a warning for a
165
+ * real EVM chain, where the usual cause is a chain viem does not ship yet; for
166
+ * a non-EVM or virtual chain there is nothing to warn about, since none of them
167
+ * is ever built as a viem chain.
168
+ */
169
+ export declare function classifyChainPublicRpcUrls(chains: ChainRegistry, networks: {
170
+ mainnets: StackNetworkEntry[];
171
+ testnets: StackNetworkEntry[];
172
+ }): {
173
+ chains: ChainRegistry;
174
+ issues: ValidationIssue[];
175
+ };
125
176
  /** The subset of a generated network entry that stack classification needs. */
126
177
  export interface StackNetworkEntry {
127
178
  id: number;