@rhinestone/shared-configs 1.11.2 → 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;
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.classifyChainNetworks = classifyChainNetworks;
37
37
  exports.classifyChainCaip2 = classifyChainCaip2;
38
38
  exports.classifyChainExplorers = classifyChainExplorers;
39
+ exports.validateChainIconSlugs = validateChainIconSlugs;
40
+ exports.classifyChainPublicRpcUrls = classifyChainPublicRpcUrls;
39
41
  exports.classifyChainStacks = classifyChainStacks;
40
42
  exports.validateChainConfig = validateChainConfig;
41
43
  exports.formatValidationIssues = formatValidationIssues;
@@ -141,6 +143,18 @@ function structuralProblems(chain) {
141
143
  else if (chain.explorer !== null && !isChainExplorer(chain.explorer)) {
142
144
  problems.push(`explorer ${JSON.stringify(chain.explorer)} is not null or { url (no trailing slash), addressPath, txPath (both slash-wrapped) }`);
143
145
  }
146
+ // Optional, like publicRpcUrl below: a chain with no mark renders as a
147
+ // coloured initial chip. Only the shape is checked here — whether a chain is
148
+ // ALLOWED to have none is validateChainIconSlugs' job.
149
+ if (!isAbsentOr(chain.iconSlug, isIconSlug)) {
150
+ problems.push(`iconSlug ${JSON.stringify(chain.iconSlug)} is not a bare filename stem (lowercase letters, digits and hyphens)`);
151
+ }
152
+ // Optional, unlike the four above: a chain with no known public endpoint is
153
+ // display-only, which is a real answer. Only its shape is checked, and only
154
+ // when present — see classifyChainPublicRpcUrls for why it must be https.
155
+ if (!isAbsentOr(chain.publicRpcUrl, isPublicRpcUrl)) {
156
+ problems.push(`publicRpcUrl ${JSON.stringify(chain.publicRpcUrl)} is not an https:// URL`);
157
+ }
144
158
  if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
145
159
  problems.push('virtual is not a boolean');
146
160
  }
@@ -499,6 +513,186 @@ function classifyChainExplorers(chains, networks) {
499
513
  }
500
514
  return { chains: stamped, issues };
501
515
  }
516
+ /**
517
+ * A bare filename stem: no directory, no extension, no scheme.
518
+ *
519
+ * The shape is what keeps the field a chain fact rather than a URL. A value
520
+ * carrying `https://`, a `chains/` prefix or a `.svg` suffix would have baked
521
+ * this CDN's layout into a signed artifact every service reads — the exact
522
+ * coupling the field is a slug to avoid.
523
+ */
524
+ const ICON_SLUG = /^[a-z0-9][a-z0-9-]*$/;
525
+ const isIconSlug = (value) => typeof value === 'string' && ICON_SLUG.test(value);
526
+ /**
527
+ * Chain ids that are allowed to ship with no `iconSlug`.
528
+ *
529
+ * An entry here is a decision that this chain renders as a coloured initial
530
+ * chip. Every id needs a reason next to it, and the reason should stop being
531
+ * true eventually — when a mark is published, the entry goes.
532
+ */
533
+ // Empty, and worth keeping that way: every chain in the registry has a mark.
534
+ // An entry here permanently excuses a chain from the check below, so it is for
535
+ // a chain whose mark genuinely cannot be obtained — not for one nobody has got
536
+ // round to yet.
537
+ const CHAINS_WITHOUT_ICON = new Map();
538
+ /**
539
+ * Fails the build for a chain that has neither a mark nor an explicit exemption.
540
+ *
541
+ * Why an error and not a warning: `iconSlug` is authored by hand, so the failure
542
+ * mode of a new chain is silence — nobody forgets a field they were never
543
+ * prompted for, and the result is a blank tile in production that no test, type
544
+ * or log names. The gate turns that into a build failure at the moment someone
545
+ * is already editing the registry, which is the only point where the decision is
546
+ * cheap. CHAINS_WITHOUT_ICON is how "this chain genuinely has no mark" is said
547
+ * out loud, in code review, rather than being indistinguishable from an oversight.
548
+ *
549
+ * Deliberately offline. Whether `chains/<slug>.svg` actually exists on the CDN is
550
+ * not checked and must not be: the build would then fail on a CDN blip or in a
551
+ * sandboxed CI runner, which is a far more frequent failure than a typo'd slug
552
+ * and produces a red build with nothing wrong in the diff. The cost of a bad slug
553
+ * is one missing image; the cost of a network-dependent build gate is every build.
554
+ */
555
+ function validateChainIconSlugs(chains,
556
+ // Injectable so the exemption paths stay covered while the real map is empty.
557
+ // Testing them against a live entry would mean keeping a chain un-iconed just
558
+ // to have something to assert on.
559
+ exemptions = CHAINS_WITHOUT_ICON) {
560
+ const issues = [];
561
+ for (const [chainId, chain] of Object.entries(chains)) {
562
+ const chainName = isNonEmptyString(chain?.name)
563
+ ? chain.name
564
+ : '<unnamed>';
565
+ const slug = chain?.iconSlug;
566
+ const exemption = exemptions.get(chainId);
567
+ if (slug === undefined) {
568
+ if (exemption === undefined) {
569
+ issues.push({
570
+ severity: 'error',
571
+ chainId,
572
+ chainName,
573
+ message: 'has no iconSlug — add the filename stem of its mark in rhinestonewtf/s3-public ' +
574
+ "(`chains/<slug>.svg`) to this chain's chainMeta entry in " +
575
+ 'config/shared-configs.jsonnet, or, if it genuinely has none, add its id to ' +
576
+ 'CHAINS_WITHOUT_ICON in scripts/validation.ts with the reason. It is not ' +
577
+ 'defaulted: a chain with no mark renders as a blank tile with nothing logged',
578
+ });
579
+ }
580
+ continue;
581
+ }
582
+ // A stale exemption is worse than no exemption: it permanently excuses the
583
+ // one chain it names, so the next person to drop the slug gets no warning.
584
+ if (exemption !== undefined) {
585
+ issues.push({
586
+ severity: 'error',
587
+ chainId,
588
+ chainName,
589
+ message: `has an iconSlug ${JSON.stringify(slug)} but is still listed in CHAINS_WITHOUT_ICON (${exemption}) — remove the exemption`,
590
+ });
591
+ }
592
+ // Shape is also checked structurally; repeated here so this function is a
593
+ // complete gate on its own and a malformed slug cannot pass as "present".
594
+ if (!isIconSlug(slug)) {
595
+ issues.push({
596
+ severity: 'error',
597
+ chainId,
598
+ chainName,
599
+ message: `declares a malformed iconSlug ${JSON.stringify(slug)} — expected a bare filename stem, with no directory, extension or scheme`,
600
+ });
601
+ }
602
+ }
603
+ return issues;
604
+ }
605
+ /**
606
+ * `https://` only, and deliberately so. The field exists for a browser, which
607
+ * cannot hold an API key and — on an https page — cannot call an `http://`
608
+ * endpoint at all: the request is blocked as mixed content. Publishing one
609
+ * would hand consumers a URL that never connects, which is strictly worse than
610
+ * publishing nothing and letting the chain fall back to display-only.
611
+ */
612
+ const isPublicRpcUrl = (value) => typeof value === 'string' && /^https:\/\/\S+$/.test(value);
613
+ /**
614
+ * Stamps every chain with a public JSON-RPC endpoint, derived from the chain's
615
+ * viem definition where it has one and authored in the jsonnet where it does
616
+ * not.
617
+ *
618
+ * Why this exists: a browser consumer building a viem `Chain` with
619
+ * `defineChain` — for a chain newer than its pinned viem — already gets id,
620
+ * name, native currency, `explorer` and `stack` from this registry. The RPC
621
+ * endpoint was the only missing piece, and it is the piece that decides whether
622
+ * a newly added chain is a first-class wallet-connect target or display-only.
623
+ * It is the PUBLIC endpoint on purpose: the rpc-proxy path needs an API key,
624
+ * which is the one thing a browser cannot have.
625
+ *
626
+ * Resolved by the entry's declared `viemChain` **name**, never by chain id, for
627
+ * the same reason as `classifyChainExplorers` — only worse here, because the
628
+ * consequence is a live endpoint for the wrong chain rather than a dead link.
629
+ * viem 2.55 has 23 chain ids claimed by more than one export: id 999 is
630
+ * `hyperEvm`, `hyperliquid`, `wanchainTestnet` and `zoraTestnet`, so an id
631
+ * lookup for HyperEVM can hand out a Zora testnet RPC.
632
+ *
633
+ * A missing endpoint is not an error — it resolves to the field being absent,
634
+ * and a consumer treats that chain as display-only. It is worth a warning for a
635
+ * real EVM chain, where the usual cause is a chain viem does not ship yet; for
636
+ * a non-EVM or virtual chain there is nothing to warn about, since none of them
637
+ * is ever built as a viem chain.
638
+ */
639
+ function classifyChainPublicRpcUrls(chains, networks) {
640
+ const issues = [];
641
+ const stamped = {};
642
+ const entriesById = new Map();
643
+ for (const entry of [...networks.mainnets, ...networks.testnets]) {
644
+ entriesById.set(String(entry.id), entry);
645
+ }
646
+ for (const [chainId, chain] of Object.entries(chains)) {
647
+ const chainName = isNonEmptyString(chain?.name)
648
+ ? chain.name
649
+ : '<unnamed>';
650
+ const authored = chain?.publicRpcUrl;
651
+ // Authoring wins outright, as with `explorer`: the jsonnet is the only place
652
+ // an endpoint can be given for a chain viem does not ship, or a better one
653
+ // preferred over viem's default.
654
+ if (authored !== undefined) {
655
+ if (!isPublicRpcUrl(authored)) {
656
+ issues.push({
657
+ severity: 'error',
658
+ chainId,
659
+ chainName,
660
+ message: `declares a malformed publicRpcUrl ${JSON.stringify(authored)} — expected an https:// URL`,
661
+ });
662
+ stamped[chainId] = withSortedKeys({ ...chain });
663
+ continue;
664
+ }
665
+ stamped[chainId] = withSortedKeys({ ...chain, publicRpcUrl: authored });
666
+ continue;
667
+ }
668
+ const viemChainName = entriesById.get(chainId)?.viemChain;
669
+ const url = viemChainName
670
+ ? viemChains[viemChainName]?.rpcUrls?.default?.http?.[0]
671
+ : undefined;
672
+ // viem's own value carries a trailing slash on some chains (`unichain`) and
673
+ // not others. Both address the same endpoint, so it is normalised here
674
+ // rather than published as an inconsistency every consumer has to absorb.
675
+ const normalised = typeof url === 'string' ? url.replace(/\/+$/, '') : undefined;
676
+ if (!isPublicRpcUrl(normalised)) {
677
+ // A chain that is never synthesised as a viem chain has nothing to be
678
+ // missing — only a real EVM chain is worth reporting.
679
+ if (chain.vmType === 'evm' && chain.virtual !== true) {
680
+ issues.push({
681
+ severity: 'warning',
682
+ chainId,
683
+ chainName,
684
+ message: viemChainName
685
+ ? `viem chain "${viemChainName}" ships no usable https rpc url — the chain stays display-only for consumers that synthesise it until one is authored`
686
+ : 'has no viemChain to derive a public rpc url from and authors none — the chain stays display-only for consumers that synthesise it',
687
+ });
688
+ }
689
+ stamped[chainId] = withSortedKeys({ ...chain });
690
+ continue;
691
+ }
692
+ stamped[chainId] = withSortedKeys({ ...chain, publicRpcUrl: normalised });
693
+ }
694
+ return { chains: stamped, issues };
695
+ }
502
696
  const isChainStack = (v) => typeof v === 'string' && STACKS.has(v);
503
697
  const STACK_FORMATTERS = [
504
698
  ['op-stack', op_stack_1.chainConfig.formatters],
@@ -38,6 +38,14 @@ interface Chain {
38
38
  caip2: string;
39
39
  /** Block explorer, or null for a chain that has none (render plain text). */
40
40
  explorer: Explorer | null;
41
+ /** Filename stem of this chain's mark in s3-public's `chains/` directory,
42
+ * served today at `https://s3.rhinestone.dev/chains/<slug>.svg`. A slug and
43
+ * not a URL so the CDN stays a consumer's concern. Absent means no mark —
44
+ * render a coloured initial chip, never an error. */
45
+ iconSlug?: string;
46
+ /** Public, unauthenticated https JSON-RPC endpoint — what a browser needs to
47
+ * build this chain with `defineChain`. Absent means display-only. */
48
+ publicRpcUrl?: string;
41
49
  /** True for virtual chains (e.g. HyperCore) that settle on another chain. */
42
50
  virtual?: boolean;
43
51
  /** Which directions of transfer the chain supports; absent means both.
@@ -9,6 +9,7 @@ const chains = {
9
9
  "addressPath": "/address/",
10
10
  "txPath": "/tx/"
11
11
  },
12
+ "iconSlug": "mainnet",
12
13
  "name": "Ethereum",
13
14
  "nativeToken": {
14
15
  "address": "0x0000000000000000000000000000000000000000",
@@ -16,6 +17,7 @@ const chains = {
16
17
  "symbol": "ETH"
17
18
  },
18
19
  "network": "mainnet",
20
+ "publicRpcUrl": "https://eth.merkle.io",
19
21
  "settlementLayers": [
20
22
  "ACROSS",
21
23
  "ECO",
@@ -103,6 +105,7 @@ const chains = {
103
105
  "addressPath": "/address/",
104
106
  "txPath": "/tx/"
105
107
  },
108
+ "iconSlug": "optimism",
106
109
  "name": "OP Mainnet",
107
110
  "nativeToken": {
108
111
  "address": "0x0000000000000000000000000000000000000000",
@@ -110,6 +113,7 @@ const chains = {
110
113
  "symbol": "ETH"
111
114
  },
112
115
  "network": "mainnet",
116
+ "publicRpcUrl": "https://mainnet.optimism.io",
113
117
  "settlementLayers": [
114
118
  "ACROSS",
115
119
  "ECO",
@@ -208,6 +212,7 @@ const chains = {
208
212
  "addressPath": "/address/",
209
213
  "txPath": "/tx/"
210
214
  },
215
+ "iconSlug": "bsc",
211
216
  "name": "BNB Smart Chain",
212
217
  "nativeToken": {
213
218
  "address": "0x0000000000000000000000000000000000000000",
@@ -215,6 +220,7 @@ const chains = {
215
220
  "symbol": "BNB"
216
221
  },
217
222
  "network": "mainnet",
223
+ "publicRpcUrl": "https://56.rpc.thirdweb.com",
218
224
  "settlementLayers": [
219
225
  "ACROSS",
220
226
  "ECO",
@@ -286,6 +292,7 @@ const chains = {
286
292
  "addressPath": "/address/",
287
293
  "txPath": "/tx/"
288
294
  },
295
+ "iconSlug": "gnosis",
289
296
  "name": "Gnosis",
290
297
  "nativeToken": {
291
298
  "address": "0x0000000000000000000000000000000000000000",
@@ -293,6 +300,7 @@ const chains = {
293
300
  "symbol": "XDAI"
294
301
  },
295
302
  "network": "mainnet",
303
+ "publicRpcUrl": "https://rpc.gnosischain.com",
296
304
  "settlementLayers": [
297
305
  "RELAY",
298
306
  "NEAR",
@@ -332,6 +340,7 @@ const chains = {
332
340
  "addressPath": "/address/",
333
341
  "txPath": "/tx/"
334
342
  },
343
+ "iconSlug": "unichain",
335
344
  "name": "Unichain",
336
345
  "nativeToken": {
337
346
  "address": "0x0000000000000000000000000000000000000000",
@@ -339,6 +348,7 @@ const chains = {
339
348
  "symbol": "ETH"
340
349
  },
341
350
  "network": "mainnet",
351
+ "publicRpcUrl": "https://mainnet.unichain.org",
342
352
  "settlementLayers": [
343
353
  "ACROSS",
344
354
  "ECO",
@@ -425,6 +435,7 @@ const chains = {
425
435
  "addressPath": "/address/",
426
436
  "txPath": "/tx/"
427
437
  },
438
+ "iconSlug": "polygon",
428
439
  "name": "Polygon",
429
440
  "nativeToken": {
430
441
  "address": "0x0000000000000000000000000000000000000000",
@@ -432,6 +443,7 @@ const chains = {
432
443
  "symbol": "POL"
433
444
  },
434
445
  "network": "mainnet",
446
+ "publicRpcUrl": "https://polygon.drpc.org",
435
447
  "settlementLayers": [
436
448
  "ACROSS",
437
449
  "ECO",
@@ -506,6 +518,7 @@ const chains = {
506
518
  "addressPath": "/address/",
507
519
  "txPath": "/tx/"
508
520
  },
521
+ "iconSlug": "monad",
509
522
  "name": "Monad",
510
523
  "nativeToken": {
511
524
  "address": "0x0000000000000000000000000000000000000000",
@@ -513,6 +526,7 @@ const chains = {
513
526
  "symbol": "MON"
514
527
  },
515
528
  "network": "mainnet",
529
+ "publicRpcUrl": "https://rpc.monad.xyz",
516
530
  "settlementLayers": [
517
531
  "ACROSS",
518
532
  "RELAY",
@@ -585,6 +599,7 @@ const chains = {
585
599
  "addressPath": "/address/",
586
600
  "txPath": "/tx/"
587
601
  },
602
+ "iconSlug": "sonic",
588
603
  "name": "Sonic",
589
604
  "nativeToken": {
590
605
  "address": "0x0000000000000000000000000000000000000000",
@@ -592,6 +607,7 @@ const chains = {
592
607
  "symbol": "S"
593
608
  },
594
609
  "network": "mainnet",
610
+ "publicRpcUrl": "https://rpc.soniclabs.com",
595
611
  "settlementLayers": [
596
612
  "ECO",
597
613
  "RELAY",
@@ -636,6 +652,7 @@ const chains = {
636
652
  "addressPath": "/address/",
637
653
  "txPath": "/tx/"
638
654
  },
655
+ "iconSlug": "hyperevm",
639
656
  "name": "HyperEVM",
640
657
  "nativeToken": {
641
658
  "address": "0x0000000000000000000000000000000000000000",
@@ -643,6 +660,7 @@ const chains = {
643
660
  "symbol": "HYPE"
644
661
  },
645
662
  "network": "mainnet",
663
+ "publicRpcUrl": "https://rpc.hyperliquid.xyz/evm",
646
664
  "settlementLayers": [
647
665
  "ACROSS",
648
666
  "ECO",
@@ -699,6 +717,7 @@ const chains = {
699
717
  "txPath": "/explorer/tx/",
700
718
  "url": "https://app.hyperliquid.xyz"
701
719
  },
720
+ "iconSlug": "hyperevm",
702
721
  "name": "HyperCore",
703
722
  "nativeToken": {
704
723
  "address": "0x0000000000000000000000000000000000000000",
@@ -739,6 +758,7 @@ const chains = {
739
758
  "addressPath": "/address/",
740
759
  "txPath": "/tx/"
741
760
  },
761
+ "iconSlug": "soneium",
742
762
  "name": "Soneium Mainnet",
743
763
  "nativeToken": {
744
764
  "address": "0x0000000000000000000000000000000000000000",
@@ -746,6 +766,7 @@ const chains = {
746
766
  "symbol": "ETH"
747
767
  },
748
768
  "network": "mainnet",
769
+ "publicRpcUrl": "https://rpc.soneium.org",
749
770
  "settlementLayers": [
750
771
  "ACROSS",
751
772
  "RELAY"
@@ -817,6 +838,7 @@ const chains = {
817
838
  "addressPath": "/address/",
818
839
  "txPath": "/tx/"
819
840
  },
841
+ "iconSlug": "robinhood",
820
842
  "name": "Robinhood Chain",
821
843
  "nativeToken": {
822
844
  "address": "0x0000000000000000000000000000000000000000",
@@ -824,6 +846,7 @@ const chains = {
824
846
  "symbol": "ETH"
825
847
  },
826
848
  "network": "mainnet",
849
+ "publicRpcUrl": "https://rpc.mainnet.chain.robinhood.com",
827
850
  "settlementLayers": [
828
851
  "ACROSS",
829
852
  "RELAY"
@@ -884,6 +907,7 @@ const chains = {
884
907
  "addressPath": "/address/",
885
908
  "txPath": "/tx/"
886
909
  },
910
+ "iconSlug": "base",
887
911
  "name": "Base",
888
912
  "nativeToken": {
889
913
  "address": "0x0000000000000000000000000000000000000000",
@@ -891,6 +915,7 @@ const chains = {
891
915
  "symbol": "ETH"
892
916
  },
893
917
  "network": "mainnet",
918
+ "publicRpcUrl": "https://mainnet.base.org",
894
919
  "settlementLayers": [
895
920
  "ACROSS",
896
921
  "ECO",
@@ -963,6 +988,7 @@ const chains = {
963
988
  "addressPath": "/address/",
964
989
  "txPath": "/tx/"
965
990
  },
991
+ "iconSlug": "plasma",
966
992
  "name": "Plasma",
967
993
  "nativeToken": {
968
994
  "address": "0x0000000000000000000000000000000000000000",
@@ -970,6 +996,7 @@ const chains = {
970
996
  "symbol": "XPL"
971
997
  },
972
998
  "network": "mainnet",
999
+ "publicRpcUrl": "https://rpc.plasma.to",
973
1000
  "settlementLayers": [
974
1001
  "ACROSS",
975
1002
  "ECO",
@@ -1017,6 +1044,7 @@ const chains = {
1017
1044
  "addressPath": "/address/",
1018
1045
  "txPath": "/tx/"
1019
1046
  },
1047
+ "iconSlug": "plasma",
1020
1048
  "name": "Plasma Testnet",
1021
1049
  "nativeToken": {
1022
1050
  "address": "0x0000000000000000000000000000000000000000",
@@ -1024,6 +1052,7 @@ const chains = {
1024
1052
  "symbol": "XPL"
1025
1053
  },
1026
1054
  "network": "testnet",
1055
+ "publicRpcUrl": "https://testnet-rpc.plasma.to",
1027
1056
  "settlementLayers": [
1028
1057
  "ECO"
1029
1058
  ],
@@ -1073,6 +1102,7 @@ const chains = {
1073
1102
  "addressPath": "/address/",
1074
1103
  "txPath": "/tx/"
1075
1104
  },
1105
+ "iconSlug": "arbitrum",
1076
1106
  "name": "Arbitrum One",
1077
1107
  "nativeToken": {
1078
1108
  "address": "0x0000000000000000000000000000000000000000",
@@ -1080,6 +1110,7 @@ const chains = {
1080
1110
  "symbol": "ETH"
1081
1111
  },
1082
1112
  "network": "mainnet",
1113
+ "publicRpcUrl": "https://arb1.arbitrum.io/rpc",
1083
1114
  "settlementLayers": [
1084
1115
  "ACROSS",
1085
1116
  "ECO",
@@ -1167,6 +1198,7 @@ const chains = {
1167
1198
  "addressPath": "/address/",
1168
1199
  "txPath": "/tx/"
1169
1200
  },
1201
+ "iconSlug": "avalanche",
1170
1202
  "name": "Avalanche",
1171
1203
  "nativeToken": {
1172
1204
  "address": "0x0000000000000000000000000000000000000000",
@@ -1174,6 +1206,7 @@ const chains = {
1174
1206
  "symbol": "AVAX"
1175
1207
  },
1176
1208
  "network": "mainnet",
1209
+ "publicRpcUrl": "https://api.avax.network/ext/bc/C/rpc",
1177
1210
  "settlementLayers": [
1178
1211
  "ACROSS",
1179
1212
  "RELAY",
@@ -1254,6 +1287,7 @@ const chains = {
1254
1287
  "addressPath": "/address/",
1255
1288
  "txPath": "/tx/"
1256
1289
  },
1290
+ "iconSlug": "base",
1257
1291
  "name": "Base Sepolia",
1258
1292
  "nativeToken": {
1259
1293
  "address": "0x0000000000000000000000000000000000000000",
@@ -1261,6 +1295,7 @@ const chains = {
1261
1295
  "symbol": "ETH"
1262
1296
  },
1263
1297
  "network": "testnet",
1298
+ "publicRpcUrl": "https://sepolia.base.org",
1264
1299
  "settlementLayers": [
1265
1300
  "ACROSS",
1266
1301
  "ECO",
@@ -1339,6 +1374,7 @@ const chains = {
1339
1374
  "addressPath": "/address/",
1340
1375
  "txPath": "/tx/"
1341
1376
  },
1377
+ "iconSlug": "arbitrum",
1342
1378
  "name": "Arbitrum Sepolia",
1343
1379
  "nativeToken": {
1344
1380
  "address": "0x0000000000000000000000000000000000000000",
@@ -1346,6 +1382,7 @@ const chains = {
1346
1382
  "symbol": "ETH"
1347
1383
  },
1348
1384
  "network": "testnet",
1385
+ "publicRpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
1349
1386
  "settlementLayers": [
1350
1387
  "ACROSS",
1351
1388
  "ECO",
@@ -1410,6 +1447,7 @@ const chains = {
1410
1447
  "addressPath": "/address/",
1411
1448
  "txPath": "/tx/"
1412
1449
  },
1450
+ "iconSlug": "katana",
1413
1451
  "name": "Katana",
1414
1452
  "nativeToken": {
1415
1453
  "address": "0x0000000000000000000000000000000000000000",
@@ -1417,6 +1455,7 @@ const chains = {
1417
1455
  "symbol": "ETH"
1418
1456
  },
1419
1457
  "network": "mainnet",
1458
+ "publicRpcUrl": "https://rpc.katana.network",
1420
1459
  "settlementLayers": [
1421
1460
  "RELAY",
1422
1461
  "RHINO"
@@ -1478,6 +1517,7 @@ const chains = {
1478
1517
  "txPath": "/explorer/tx/",
1479
1518
  "url": "https://app.hyperliquid.xyz"
1480
1519
  },
1520
+ "iconSlug": "hyperevm",
1481
1521
  "name": "HyperCore Spot",
1482
1522
  "nativeToken": {
1483
1523
  "address": "0x0000000000000000000000000000000000000000",
@@ -1518,6 +1558,7 @@ const chains = {
1518
1558
  "txPath": "/explorer/tx/",
1519
1559
  "url": "https://app.hyperliquid.xyz"
1520
1560
  },
1561
+ "iconSlug": "hyperevm",
1521
1562
  "name": "HyperCore Perp",
1522
1563
  "nativeToken": {
1523
1564
  "address": "0x0000000000000000000000000000000000000000",
@@ -1558,6 +1599,7 @@ const chains = {
1558
1599
  "addressPath": "/address/",
1559
1600
  "txPath": "/tx/"
1560
1601
  },
1602
+ "iconSlug": "mainnet",
1561
1603
  "name": "Sepolia",
1562
1604
  "nativeToken": {
1563
1605
  "address": "0x0000000000000000000000000000000000000000",
@@ -1565,6 +1607,7 @@ const chains = {
1565
1607
  "symbol": "ETH"
1566
1608
  },
1567
1609
  "network": "testnet",
1610
+ "publicRpcUrl": "https://11155111.rpc.thirdweb.com",
1568
1611
  "settlementLayers": [
1569
1612
  "ACROSS",
1570
1613
  "ECO",
@@ -1639,6 +1682,7 @@ const chains = {
1639
1682
  "addressPath": "/address/",
1640
1683
  "txPath": "/tx/"
1641
1684
  },
1685
+ "iconSlug": "optimism",
1642
1686
  "name": "OP Sepolia",
1643
1687
  "nativeToken": {
1644
1688
  "address": "0x0000000000000000000000000000000000000000",
@@ -1646,6 +1690,7 @@ const chains = {
1646
1690
  "symbol": "ETH"
1647
1691
  },
1648
1692
  "network": "testnet",
1693
+ "publicRpcUrl": "https://sepolia.optimism.io",
1649
1694
  "settlementLayers": [
1650
1695
  "ACROSS",
1651
1696
  "ECO",
@@ -1710,6 +1755,7 @@ const chains = {
1710
1755
  "txPath": "/#/transaction/",
1711
1756
  "url": "https://tronscan.org"
1712
1757
  },
1758
+ "iconSlug": "tron",
1713
1759
  "name": "Tron",
1714
1760
  "nativeToken": {
1715
1761
  "address": "0x0000000000000000000000000000000000000000",
@@ -1757,6 +1803,7 @@ const chains = {
1757
1803
  "txPath": "/tx/",
1758
1804
  "url": "https://solscan.io"
1759
1805
  },
1806
+ "iconSlug": "solana",
1760
1807
  "name": "Solana",
1761
1808
  "nativeToken": {
1762
1809
  "address": "11111111111111111111111111111111",
@@ -1,2 +1,2 @@
1
- declare const PACKAGE_VERSION = "1.11.2";
1
+ declare const PACKAGE_VERSION = "1.12.0";
2
2
  export { PACKAGE_VERSION };
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PACKAGE_VERSION = void 0;
4
4
  // Auto-generated by scripts/generate.ts. Do not edit manually.
5
- const PACKAGE_VERSION = "1.11.2";
5
+ const PACKAGE_VERSION = "1.12.0";
6
6
  exports.PACKAGE_VERSION = PACKAGE_VERSION;
@@ -128,6 +128,46 @@ interface ChainEntry {
128
128
  * jsonnet where it does not — see `classifyChainExplorers`.
129
129
  */
130
130
  explorer: ChainExplorer | null;
131
+ /**
132
+ * Filename stem of this chain's mark in the `chains/` directory of
133
+ * rhinestonewtf/s3-public — today `https://s3.rhinestone.dev/chains/<slug>.svg`.
134
+ *
135
+ * The other half of the same problem `publicRpcUrl` solves: a frontend can
136
+ * already name, link and connect to a chain it was never compiled against,
137
+ * but it could not draw it. Every consumer instead carried a hand-maintained
138
+ * chain-id → asset table, which is the kind of table that is always one chain
139
+ * behind the registry.
140
+ *
141
+ * Deliberately a SLUG, not a URL. This artifact describes chain identity; the
142
+ * CDN it happens to be served from is deployment, and it is the consumer (the
143
+ * deposit-processor) that composes the URL. Publishing full URLs would put the
144
+ * host in a signed artifact read by every service, so moving the CDN would
145
+ * mean re-publishing facts and rolling the fleet rather than changing one
146
+ * template string.
147
+ *
148
+ * Optional, and absence is a normal answer — a chain with no mark renders as a
149
+ * coloured initial chip. A consumer must never treat it as an error. Which
150
+ * chains may be absent is decided at build time, not here: see
151
+ * `validateChainIconSlugs`.
152
+ */
153
+ iconSlug?: string;
154
+ /**
155
+ * A public, unauthenticated JSON-RPC endpoint over HTTPS, when one is known.
156
+ *
157
+ * The one thing a browser consumer cannot get from anywhere else in this
158
+ * artifact. Building a viem `Chain` with `defineChain` — which the deposit
159
+ * modal does at runtime for chains it was never compiled against — needs the
160
+ * id, name, native currency, explorer and stack, all of which are above, plus
161
+ * an RPC endpoint. That single field is what decides whether a newly added
162
+ * chain is a first-class wallet-connect target or display-only.
163
+ *
164
+ * Deliberately the PUBLIC endpoint, never the rpc-proxy/eRPC one: that path
165
+ * requires an API key, and a browser cannot hold one.
166
+ *
167
+ * Optional. A chain without one degrades to display-only, which is not an
168
+ * error — non-EVM chains are never built as viem chains and never carry it.
169
+ */
170
+ publicRpcUrl?: string;
131
171
  /**
132
172
  * True for virtual chains (e.g. HyperCore, chain id 1337) that are addressed
133
173
  * as a distinct destination but have no RPC / providers / contracts / gas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/shared-configs",
3
- "version": "1.11.2",
3
+ "version": "1.12.0",
4
4
  "description": "Reusable configuration files for Rhinestone services",
5
5
  "author": {
6
6
  "name": "Rhinestone",