@rhinestone/shared-configs 1.11.2 → 1.13.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",
@@ -333,7 +343,8 @@
333
343
  "1inch",
334
344
  "0x",
335
345
  "velora",
336
- "fynd"
346
+ "fynd",
347
+ "fynd-hosted"
337
348
  ],
338
349
  "tokens": [
339
350
  {
@@ -401,6 +412,7 @@
401
412
  "addressPath": "/address/",
402
413
  "txPath": "/tx/"
403
414
  },
415
+ "iconSlug": "polygon",
404
416
  "name": "Polygon",
405
417
  "nativeToken": {
406
418
  "address": "0x0000000000000000000000000000000000000000",
@@ -408,6 +420,7 @@
408
420
  "symbol": "POL"
409
421
  },
410
422
  "network": "mainnet",
423
+ "publicRpcUrl": "https://polygon.drpc.org",
411
424
  "settlementLayers": [
412
425
  "ACROSS",
413
426
  "ECO",
@@ -478,6 +491,7 @@
478
491
  "addressPath": "/address/",
479
492
  "txPath": "/tx/"
480
493
  },
494
+ "iconSlug": "monad",
481
495
  "name": "Monad",
482
496
  "nativeToken": {
483
497
  "address": "0x0000000000000000000000000000000000000000",
@@ -485,6 +499,7 @@
485
499
  "symbol": "MON"
486
500
  },
487
501
  "network": "mainnet",
502
+ "publicRpcUrl": "https://rpc.monad.xyz",
488
503
  "settlementLayers": [
489
504
  "ACROSS",
490
505
  "RELAY",
@@ -553,6 +568,7 @@
553
568
  "addressPath": "/address/",
554
569
  "txPath": "/tx/"
555
570
  },
571
+ "iconSlug": "sonic",
556
572
  "name": "Sonic",
557
573
  "nativeToken": {
558
574
  "address": "0x0000000000000000000000000000000000000000",
@@ -560,6 +576,7 @@
560
576
  "symbol": "S"
561
577
  },
562
578
  "network": "mainnet",
579
+ "publicRpcUrl": "https://rpc.soniclabs.com",
563
580
  "settlementLayers": [
564
581
  "ECO",
565
582
  "RELAY",
@@ -600,6 +617,7 @@
600
617
  "addressPath": "/address/",
601
618
  "txPath": "/tx/"
602
619
  },
620
+ "iconSlug": "hyperevm",
603
621
  "name": "HyperEVM",
604
622
  "nativeToken": {
605
623
  "address": "0x0000000000000000000000000000000000000000",
@@ -607,6 +625,7 @@
607
625
  "symbol": "HYPE"
608
626
  },
609
627
  "network": "mainnet",
628
+ "publicRpcUrl": "https://rpc.hyperliquid.xyz/evm",
610
629
  "settlementLayers": [
611
630
  "ACROSS",
612
631
  "ECO",
@@ -658,6 +677,7 @@
658
677
  "txPath": "/explorer/tx/",
659
678
  "url": "https://app.hyperliquid.xyz"
660
679
  },
680
+ "iconSlug": "hyperevm",
661
681
  "name": "HyperCore",
662
682
  "nativeToken": {
663
683
  "address": "0x0000000000000000000000000000000000000000",
@@ -697,6 +717,7 @@
697
717
  "addressPath": "/address/",
698
718
  "txPath": "/tx/"
699
719
  },
720
+ "iconSlug": "soneium",
700
721
  "name": "Soneium Mainnet",
701
722
  "nativeToken": {
702
723
  "address": "0x0000000000000000000000000000000000000000",
@@ -704,6 +725,7 @@
704
725
  "symbol": "ETH"
705
726
  },
706
727
  "network": "mainnet",
728
+ "publicRpcUrl": "https://rpc.soneium.org",
707
729
  "settlementLayers": [
708
730
  "ACROSS",
709
731
  "RELAY"
@@ -771,6 +793,7 @@
771
793
  "addressPath": "/address/",
772
794
  "txPath": "/tx/"
773
795
  },
796
+ "iconSlug": "robinhood",
774
797
  "name": "Robinhood Chain",
775
798
  "nativeToken": {
776
799
  "address": "0x0000000000000000000000000000000000000000",
@@ -778,6 +801,7 @@
778
801
  "symbol": "ETH"
779
802
  },
780
803
  "network": "mainnet",
804
+ "publicRpcUrl": "https://rpc.mainnet.chain.robinhood.com",
781
805
  "settlementLayers": [
782
806
  "ACROSS",
783
807
  "RELAY"
@@ -834,6 +858,7 @@
834
858
  "addressPath": "/address/",
835
859
  "txPath": "/tx/"
836
860
  },
861
+ "iconSlug": "base",
837
862
  "name": "Base",
838
863
  "nativeToken": {
839
864
  "address": "0x0000000000000000000000000000000000000000",
@@ -841,6 +866,7 @@
841
866
  "symbol": "ETH"
842
867
  },
843
868
  "network": "mainnet",
869
+ "publicRpcUrl": "https://mainnet.base.org",
844
870
  "settlementLayers": [
845
871
  "ACROSS",
846
872
  "ECO",
@@ -909,6 +935,7 @@
909
935
  "addressPath": "/address/",
910
936
  "txPath": "/tx/"
911
937
  },
938
+ "iconSlug": "plasma",
912
939
  "name": "Plasma",
913
940
  "nativeToken": {
914
941
  "address": "0x0000000000000000000000000000000000000000",
@@ -916,6 +943,7 @@
916
943
  "symbol": "XPL"
917
944
  },
918
945
  "network": "mainnet",
946
+ "publicRpcUrl": "https://rpc.plasma.to",
919
947
  "settlementLayers": [
920
948
  "ACROSS",
921
949
  "ECO",
@@ -958,6 +986,7 @@
958
986
  "addressPath": "/address/",
959
987
  "txPath": "/tx/"
960
988
  },
989
+ "iconSlug": "plasma",
961
990
  "name": "Plasma Testnet",
962
991
  "nativeToken": {
963
992
  "address": "0x0000000000000000000000000000000000000000",
@@ -965,6 +994,7 @@
965
994
  "symbol": "XPL"
966
995
  },
967
996
  "network": "testnet",
997
+ "publicRpcUrl": "https://testnet-rpc.plasma.to",
968
998
  "settlementLayers": [
969
999
  "ECO"
970
1000
  ],
@@ -1009,6 +1039,7 @@
1009
1039
  "addressPath": "/address/",
1010
1040
  "txPath": "/tx/"
1011
1041
  },
1042
+ "iconSlug": "arbitrum",
1012
1043
  "name": "Arbitrum One",
1013
1044
  "nativeToken": {
1014
1045
  "address": "0x0000000000000000000000000000000000000000",
@@ -1016,6 +1047,7 @@
1016
1047
  "symbol": "ETH"
1017
1048
  },
1018
1049
  "network": "mainnet",
1050
+ "publicRpcUrl": "https://arb1.arbitrum.io/rpc",
1019
1051
  "settlementLayers": [
1020
1052
  "ACROSS",
1021
1053
  "ECO",
@@ -1099,6 +1131,7 @@
1099
1131
  "addressPath": "/address/",
1100
1132
  "txPath": "/tx/"
1101
1133
  },
1134
+ "iconSlug": "avalanche",
1102
1135
  "name": "Avalanche",
1103
1136
  "nativeToken": {
1104
1137
  "address": "0x0000000000000000000000000000000000000000",
@@ -1106,6 +1139,7 @@
1106
1139
  "symbol": "AVAX"
1107
1140
  },
1108
1141
  "network": "mainnet",
1142
+ "publicRpcUrl": "https://api.avax.network/ext/bc/C/rpc",
1109
1143
  "settlementLayers": [
1110
1144
  "ACROSS",
1111
1145
  "RELAY",
@@ -1182,6 +1216,7 @@
1182
1216
  "addressPath": "/address/",
1183
1217
  "txPath": "/tx/"
1184
1218
  },
1219
+ "iconSlug": "base",
1185
1220
  "name": "Base Sepolia",
1186
1221
  "nativeToken": {
1187
1222
  "address": "0x0000000000000000000000000000000000000000",
@@ -1189,6 +1224,7 @@
1189
1224
  "symbol": "ETH"
1190
1225
  },
1191
1226
  "network": "testnet",
1227
+ "publicRpcUrl": "https://sepolia.base.org",
1192
1228
  "settlementLayers": [
1193
1229
  "ACROSS",
1194
1230
  "ECO",
@@ -1263,6 +1299,7 @@
1263
1299
  "addressPath": "/address/",
1264
1300
  "txPath": "/tx/"
1265
1301
  },
1302
+ "iconSlug": "arbitrum",
1266
1303
  "name": "Arbitrum Sepolia",
1267
1304
  "nativeToken": {
1268
1305
  "address": "0x0000000000000000000000000000000000000000",
@@ -1270,6 +1307,7 @@
1270
1307
  "symbol": "ETH"
1271
1308
  },
1272
1309
  "network": "testnet",
1310
+ "publicRpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
1273
1311
  "settlementLayers": [
1274
1312
  "ACROSS",
1275
1313
  "ECO",
@@ -1330,6 +1368,7 @@
1330
1368
  "addressPath": "/address/",
1331
1369
  "txPath": "/tx/"
1332
1370
  },
1371
+ "iconSlug": "katana",
1333
1372
  "name": "Katana",
1334
1373
  "nativeToken": {
1335
1374
  "address": "0x0000000000000000000000000000000000000000",
@@ -1337,6 +1376,7 @@
1337
1376
  "symbol": "ETH"
1338
1377
  },
1339
1378
  "network": "mainnet",
1379
+ "publicRpcUrl": "https://rpc.katana.network",
1340
1380
  "settlementLayers": [
1341
1381
  "RELAY",
1342
1382
  "RHINO"
@@ -1394,6 +1434,7 @@
1394
1434
  "txPath": "/explorer/tx/",
1395
1435
  "url": "https://app.hyperliquid.xyz"
1396
1436
  },
1437
+ "iconSlug": "hyperevm",
1397
1438
  "name": "HyperCore Spot",
1398
1439
  "nativeToken": {
1399
1440
  "address": "0x0000000000000000000000000000000000000000",
@@ -1433,6 +1474,7 @@
1433
1474
  "txPath": "/explorer/tx/",
1434
1475
  "url": "https://app.hyperliquid.xyz"
1435
1476
  },
1477
+ "iconSlug": "hyperevm",
1436
1478
  "name": "HyperCore Perp",
1437
1479
  "nativeToken": {
1438
1480
  "address": "0x0000000000000000000000000000000000000000",
@@ -1472,6 +1514,7 @@
1472
1514
  "addressPath": "/address/",
1473
1515
  "txPath": "/tx/"
1474
1516
  },
1517
+ "iconSlug": "mainnet",
1475
1518
  "name": "Sepolia",
1476
1519
  "nativeToken": {
1477
1520
  "address": "0x0000000000000000000000000000000000000000",
@@ -1479,6 +1522,7 @@
1479
1522
  "symbol": "ETH"
1480
1523
  },
1481
1524
  "network": "testnet",
1525
+ "publicRpcUrl": "https://11155111.rpc.thirdweb.com",
1482
1526
  "settlementLayers": [
1483
1527
  "ACROSS",
1484
1528
  "ECO",
@@ -1549,6 +1593,7 @@
1549
1593
  "addressPath": "/address/",
1550
1594
  "txPath": "/tx/"
1551
1595
  },
1596
+ "iconSlug": "optimism",
1552
1597
  "name": "OP Sepolia",
1553
1598
  "nativeToken": {
1554
1599
  "address": "0x0000000000000000000000000000000000000000",
@@ -1556,6 +1601,7 @@
1556
1601
  "symbol": "ETH"
1557
1602
  },
1558
1603
  "network": "testnet",
1604
+ "publicRpcUrl": "https://sepolia.optimism.io",
1559
1605
  "settlementLayers": [
1560
1606
  "ACROSS",
1561
1607
  "ECO",
@@ -1616,6 +1662,7 @@
1616
1662
  "txPath": "/#/transaction/",
1617
1663
  "url": "https://tronscan.org"
1618
1664
  },
1665
+ "iconSlug": "tron",
1619
1666
  "name": "Tron",
1620
1667
  "nativeToken": {
1621
1668
  "address": "0x0000000000000000000000000000000000000000",
@@ -1662,6 +1709,7 @@
1662
1709
  "txPath": "/tx/",
1663
1710
  "url": "https://solscan.io"
1664
1711
  },
1712
+ "iconSlug": "solana",
1665
1713
  "name": "Solana",
1666
1714
  "nativeToken": {
1667
1715
  "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;
@@ -65,6 +67,7 @@ const SWAP_QUOTERS = new Set([
65
67
  'velora',
66
68
  'kyberswap',
67
69
  'fynd',
70
+ 'fynd-hosted',
68
71
  ]);
69
72
  const VM_TYPES = new Set(['evm', 'svm', 'tvm']);
70
73
  const PEG_GROUPS = new Set(['USD', 'ETH']);
@@ -141,6 +144,18 @@ function structuralProblems(chain) {
141
144
  else if (chain.explorer !== null && !isChainExplorer(chain.explorer)) {
142
145
  problems.push(`explorer ${JSON.stringify(chain.explorer)} is not null or { url (no trailing slash), addressPath, txPath (both slash-wrapped) }`);
143
146
  }
147
+ // Optional, like publicRpcUrl below: a chain with no mark renders as a
148
+ // coloured initial chip. Only the shape is checked here — whether a chain is
149
+ // ALLOWED to have none is validateChainIconSlugs' job.
150
+ if (!isAbsentOr(chain.iconSlug, isIconSlug)) {
151
+ problems.push(`iconSlug ${JSON.stringify(chain.iconSlug)} is not a bare filename stem (lowercase letters, digits and hyphens)`);
152
+ }
153
+ // Optional, unlike the four above: a chain with no known public endpoint is
154
+ // display-only, which is a real answer. Only its shape is checked, and only
155
+ // when present — see classifyChainPublicRpcUrls for why it must be https.
156
+ if (!isAbsentOr(chain.publicRpcUrl, isPublicRpcUrl)) {
157
+ problems.push(`publicRpcUrl ${JSON.stringify(chain.publicRpcUrl)} is not an https:// URL`);
158
+ }
144
159
  if (!isAbsentOr(chain.virtual, (v) => typeof v === 'boolean')) {
145
160
  problems.push('virtual is not a boolean');
146
161
  }
@@ -499,6 +514,186 @@ function classifyChainExplorers(chains, networks) {
499
514
  }
500
515
  return { chains: stamped, issues };
501
516
  }
517
+ /**
518
+ * A bare filename stem: no directory, no extension, no scheme.
519
+ *
520
+ * The shape is what keeps the field a chain fact rather than a URL. A value
521
+ * carrying `https://`, a `chains/` prefix or a `.svg` suffix would have baked
522
+ * this CDN's layout into a signed artifact every service reads — the exact
523
+ * coupling the field is a slug to avoid.
524
+ */
525
+ const ICON_SLUG = /^[a-z0-9][a-z0-9-]*$/;
526
+ const isIconSlug = (value) => typeof value === 'string' && ICON_SLUG.test(value);
527
+ /**
528
+ * Chain ids that are allowed to ship with no `iconSlug`.
529
+ *
530
+ * An entry here is a decision that this chain renders as a coloured initial
531
+ * chip. Every id needs a reason next to it, and the reason should stop being
532
+ * true eventually — when a mark is published, the entry goes.
533
+ */
534
+ // Empty, and worth keeping that way: every chain in the registry has a mark.
535
+ // An entry here permanently excuses a chain from the check below, so it is for
536
+ // a chain whose mark genuinely cannot be obtained — not for one nobody has got
537
+ // round to yet.
538
+ const CHAINS_WITHOUT_ICON = new Map();
539
+ /**
540
+ * Fails the build for a chain that has neither a mark nor an explicit exemption.
541
+ *
542
+ * Why an error and not a warning: `iconSlug` is authored by hand, so the failure
543
+ * mode of a new chain is silence — nobody forgets a field they were never
544
+ * prompted for, and the result is a blank tile in production that no test, type
545
+ * or log names. The gate turns that into a build failure at the moment someone
546
+ * is already editing the registry, which is the only point where the decision is
547
+ * cheap. CHAINS_WITHOUT_ICON is how "this chain genuinely has no mark" is said
548
+ * out loud, in code review, rather than being indistinguishable from an oversight.
549
+ *
550
+ * Deliberately offline. Whether `chains/<slug>.svg` actually exists on the CDN is
551
+ * not checked and must not be: the build would then fail on a CDN blip or in a
552
+ * sandboxed CI runner, which is a far more frequent failure than a typo'd slug
553
+ * and produces a red build with nothing wrong in the diff. The cost of a bad slug
554
+ * is one missing image; the cost of a network-dependent build gate is every build.
555
+ */
556
+ function validateChainIconSlugs(chains,
557
+ // Injectable so the exemption paths stay covered while the real map is empty.
558
+ // Testing them against a live entry would mean keeping a chain un-iconed just
559
+ // to have something to assert on.
560
+ exemptions = CHAINS_WITHOUT_ICON) {
561
+ const issues = [];
562
+ for (const [chainId, chain] of Object.entries(chains)) {
563
+ const chainName = isNonEmptyString(chain?.name)
564
+ ? chain.name
565
+ : '<unnamed>';
566
+ const slug = chain?.iconSlug;
567
+ const exemption = exemptions.get(chainId);
568
+ if (slug === undefined) {
569
+ if (exemption === undefined) {
570
+ issues.push({
571
+ severity: 'error',
572
+ chainId,
573
+ chainName,
574
+ message: 'has no iconSlug — add the filename stem of its mark in rhinestonewtf/s3-public ' +
575
+ "(`chains/<slug>.svg`) to this chain's chainMeta entry in " +
576
+ 'config/shared-configs.jsonnet, or, if it genuinely has none, add its id to ' +
577
+ 'CHAINS_WITHOUT_ICON in scripts/validation.ts with the reason. It is not ' +
578
+ 'defaulted: a chain with no mark renders as a blank tile with nothing logged',
579
+ });
580
+ }
581
+ continue;
582
+ }
583
+ // A stale exemption is worse than no exemption: it permanently excuses the
584
+ // one chain it names, so the next person to drop the slug gets no warning.
585
+ if (exemption !== undefined) {
586
+ issues.push({
587
+ severity: 'error',
588
+ chainId,
589
+ chainName,
590
+ message: `has an iconSlug ${JSON.stringify(slug)} but is still listed in CHAINS_WITHOUT_ICON (${exemption}) — remove the exemption`,
591
+ });
592
+ }
593
+ // Shape is also checked structurally; repeated here so this function is a
594
+ // complete gate on its own and a malformed slug cannot pass as "present".
595
+ if (!isIconSlug(slug)) {
596
+ issues.push({
597
+ severity: 'error',
598
+ chainId,
599
+ chainName,
600
+ message: `declares a malformed iconSlug ${JSON.stringify(slug)} — expected a bare filename stem, with no directory, extension or scheme`,
601
+ });
602
+ }
603
+ }
604
+ return issues;
605
+ }
606
+ /**
607
+ * `https://` only, and deliberately so. The field exists for a browser, which
608
+ * cannot hold an API key and — on an https page — cannot call an `http://`
609
+ * endpoint at all: the request is blocked as mixed content. Publishing one
610
+ * would hand consumers a URL that never connects, which is strictly worse than
611
+ * publishing nothing and letting the chain fall back to display-only.
612
+ */
613
+ const isPublicRpcUrl = (value) => typeof value === 'string' && /^https:\/\/\S+$/.test(value);
614
+ /**
615
+ * Stamps every chain with a public JSON-RPC endpoint, derived from the chain's
616
+ * viem definition where it has one and authored in the jsonnet where it does
617
+ * not.
618
+ *
619
+ * Why this exists: a browser consumer building a viem `Chain` with
620
+ * `defineChain` — for a chain newer than its pinned viem — already gets id,
621
+ * name, native currency, `explorer` and `stack` from this registry. The RPC
622
+ * endpoint was the only missing piece, and it is the piece that decides whether
623
+ * a newly added chain is a first-class wallet-connect target or display-only.
624
+ * It is the PUBLIC endpoint on purpose: the rpc-proxy path needs an API key,
625
+ * which is the one thing a browser cannot have.
626
+ *
627
+ * Resolved by the entry's declared `viemChain` **name**, never by chain id, for
628
+ * the same reason as `classifyChainExplorers` — only worse here, because the
629
+ * consequence is a live endpoint for the wrong chain rather than a dead link.
630
+ * viem 2.55 has 23 chain ids claimed by more than one export: id 999 is
631
+ * `hyperEvm`, `hyperliquid`, `wanchainTestnet` and `zoraTestnet`, so an id
632
+ * lookup for HyperEVM can hand out a Zora testnet RPC.
633
+ *
634
+ * A missing endpoint is not an error — it resolves to the field being absent,
635
+ * and a consumer treats that chain as display-only. It is worth a warning for a
636
+ * real EVM chain, where the usual cause is a chain viem does not ship yet; for
637
+ * a non-EVM or virtual chain there is nothing to warn about, since none of them
638
+ * is ever built as a viem chain.
639
+ */
640
+ function classifyChainPublicRpcUrls(chains, networks) {
641
+ const issues = [];
642
+ const stamped = {};
643
+ const entriesById = new Map();
644
+ for (const entry of [...networks.mainnets, ...networks.testnets]) {
645
+ entriesById.set(String(entry.id), entry);
646
+ }
647
+ for (const [chainId, chain] of Object.entries(chains)) {
648
+ const chainName = isNonEmptyString(chain?.name)
649
+ ? chain.name
650
+ : '<unnamed>';
651
+ const authored = chain?.publicRpcUrl;
652
+ // Authoring wins outright, as with `explorer`: the jsonnet is the only place
653
+ // an endpoint can be given for a chain viem does not ship, or a better one
654
+ // preferred over viem's default.
655
+ if (authored !== undefined) {
656
+ if (!isPublicRpcUrl(authored)) {
657
+ issues.push({
658
+ severity: 'error',
659
+ chainId,
660
+ chainName,
661
+ message: `declares a malformed publicRpcUrl ${JSON.stringify(authored)} — expected an https:// URL`,
662
+ });
663
+ stamped[chainId] = withSortedKeys({ ...chain });
664
+ continue;
665
+ }
666
+ stamped[chainId] = withSortedKeys({ ...chain, publicRpcUrl: authored });
667
+ continue;
668
+ }
669
+ const viemChainName = entriesById.get(chainId)?.viemChain;
670
+ const url = viemChainName
671
+ ? viemChains[viemChainName]?.rpcUrls?.default?.http?.[0]
672
+ : undefined;
673
+ // viem's own value carries a trailing slash on some chains (`unichain`) and
674
+ // not others. Both address the same endpoint, so it is normalised here
675
+ // rather than published as an inconsistency every consumer has to absorb.
676
+ const normalised = typeof url === 'string' ? url.replace(/\/+$/, '') : undefined;
677
+ if (!isPublicRpcUrl(normalised)) {
678
+ // A chain that is never synthesised as a viem chain has nothing to be
679
+ // missing — only a real EVM chain is worth reporting.
680
+ if (chain.vmType === 'evm' && chain.virtual !== true) {
681
+ issues.push({
682
+ severity: 'warning',
683
+ chainId,
684
+ chainName,
685
+ message: viemChainName
686
+ ? `viem chain "${viemChainName}" ships no usable https rpc url — the chain stays display-only for consumers that synthesise it until one is authored`
687
+ : 'has no viemChain to derive a public rpc url from and authors none — the chain stays display-only for consumers that synthesise it',
688
+ });
689
+ }
690
+ stamped[chainId] = withSortedKeys({ ...chain });
691
+ continue;
692
+ }
693
+ stamped[chainId] = withSortedKeys({ ...chain, publicRpcUrl: normalised });
694
+ }
695
+ return { chains: stamped, issues };
696
+ }
502
697
  const isChainStack = (v) => typeof v === 'string' && STACKS.has(v);
503
698
  const STACK_FORMATTERS = [
504
699
  ['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",
@@ -353,7 +363,8 @@ const chains = {
353
363
  "1inch",
354
364
  "0x",
355
365
  "velora",
356
- "fynd"
366
+ "fynd",
367
+ "fynd-hosted"
357
368
  ],
358
369
  "tokens": [
359
370
  {
@@ -425,6 +436,7 @@ const chains = {
425
436
  "addressPath": "/address/",
426
437
  "txPath": "/tx/"
427
438
  },
439
+ "iconSlug": "polygon",
428
440
  "name": "Polygon",
429
441
  "nativeToken": {
430
442
  "address": "0x0000000000000000000000000000000000000000",
@@ -432,6 +444,7 @@ const chains = {
432
444
  "symbol": "POL"
433
445
  },
434
446
  "network": "mainnet",
447
+ "publicRpcUrl": "https://polygon.drpc.org",
435
448
  "settlementLayers": [
436
449
  "ACROSS",
437
450
  "ECO",
@@ -506,6 +519,7 @@ const chains = {
506
519
  "addressPath": "/address/",
507
520
  "txPath": "/tx/"
508
521
  },
522
+ "iconSlug": "monad",
509
523
  "name": "Monad",
510
524
  "nativeToken": {
511
525
  "address": "0x0000000000000000000000000000000000000000",
@@ -513,6 +527,7 @@ const chains = {
513
527
  "symbol": "MON"
514
528
  },
515
529
  "network": "mainnet",
530
+ "publicRpcUrl": "https://rpc.monad.xyz",
516
531
  "settlementLayers": [
517
532
  "ACROSS",
518
533
  "RELAY",
@@ -585,6 +600,7 @@ const chains = {
585
600
  "addressPath": "/address/",
586
601
  "txPath": "/tx/"
587
602
  },
603
+ "iconSlug": "sonic",
588
604
  "name": "Sonic",
589
605
  "nativeToken": {
590
606
  "address": "0x0000000000000000000000000000000000000000",
@@ -592,6 +608,7 @@ const chains = {
592
608
  "symbol": "S"
593
609
  },
594
610
  "network": "mainnet",
611
+ "publicRpcUrl": "https://rpc.soniclabs.com",
595
612
  "settlementLayers": [
596
613
  "ECO",
597
614
  "RELAY",
@@ -636,6 +653,7 @@ const chains = {
636
653
  "addressPath": "/address/",
637
654
  "txPath": "/tx/"
638
655
  },
656
+ "iconSlug": "hyperevm",
639
657
  "name": "HyperEVM",
640
658
  "nativeToken": {
641
659
  "address": "0x0000000000000000000000000000000000000000",
@@ -643,6 +661,7 @@ const chains = {
643
661
  "symbol": "HYPE"
644
662
  },
645
663
  "network": "mainnet",
664
+ "publicRpcUrl": "https://rpc.hyperliquid.xyz/evm",
646
665
  "settlementLayers": [
647
666
  "ACROSS",
648
667
  "ECO",
@@ -699,6 +718,7 @@ const chains = {
699
718
  "txPath": "/explorer/tx/",
700
719
  "url": "https://app.hyperliquid.xyz"
701
720
  },
721
+ "iconSlug": "hyperevm",
702
722
  "name": "HyperCore",
703
723
  "nativeToken": {
704
724
  "address": "0x0000000000000000000000000000000000000000",
@@ -739,6 +759,7 @@ const chains = {
739
759
  "addressPath": "/address/",
740
760
  "txPath": "/tx/"
741
761
  },
762
+ "iconSlug": "soneium",
742
763
  "name": "Soneium Mainnet",
743
764
  "nativeToken": {
744
765
  "address": "0x0000000000000000000000000000000000000000",
@@ -746,6 +767,7 @@ const chains = {
746
767
  "symbol": "ETH"
747
768
  },
748
769
  "network": "mainnet",
770
+ "publicRpcUrl": "https://rpc.soneium.org",
749
771
  "settlementLayers": [
750
772
  "ACROSS",
751
773
  "RELAY"
@@ -817,6 +839,7 @@ const chains = {
817
839
  "addressPath": "/address/",
818
840
  "txPath": "/tx/"
819
841
  },
842
+ "iconSlug": "robinhood",
820
843
  "name": "Robinhood Chain",
821
844
  "nativeToken": {
822
845
  "address": "0x0000000000000000000000000000000000000000",
@@ -824,6 +847,7 @@ const chains = {
824
847
  "symbol": "ETH"
825
848
  },
826
849
  "network": "mainnet",
850
+ "publicRpcUrl": "https://rpc.mainnet.chain.robinhood.com",
827
851
  "settlementLayers": [
828
852
  "ACROSS",
829
853
  "RELAY"
@@ -884,6 +908,7 @@ const chains = {
884
908
  "addressPath": "/address/",
885
909
  "txPath": "/tx/"
886
910
  },
911
+ "iconSlug": "base",
887
912
  "name": "Base",
888
913
  "nativeToken": {
889
914
  "address": "0x0000000000000000000000000000000000000000",
@@ -891,6 +916,7 @@ const chains = {
891
916
  "symbol": "ETH"
892
917
  },
893
918
  "network": "mainnet",
919
+ "publicRpcUrl": "https://mainnet.base.org",
894
920
  "settlementLayers": [
895
921
  "ACROSS",
896
922
  "ECO",
@@ -963,6 +989,7 @@ const chains = {
963
989
  "addressPath": "/address/",
964
990
  "txPath": "/tx/"
965
991
  },
992
+ "iconSlug": "plasma",
966
993
  "name": "Plasma",
967
994
  "nativeToken": {
968
995
  "address": "0x0000000000000000000000000000000000000000",
@@ -970,6 +997,7 @@ const chains = {
970
997
  "symbol": "XPL"
971
998
  },
972
999
  "network": "mainnet",
1000
+ "publicRpcUrl": "https://rpc.plasma.to",
973
1001
  "settlementLayers": [
974
1002
  "ACROSS",
975
1003
  "ECO",
@@ -1017,6 +1045,7 @@ const chains = {
1017
1045
  "addressPath": "/address/",
1018
1046
  "txPath": "/tx/"
1019
1047
  },
1048
+ "iconSlug": "plasma",
1020
1049
  "name": "Plasma Testnet",
1021
1050
  "nativeToken": {
1022
1051
  "address": "0x0000000000000000000000000000000000000000",
@@ -1024,6 +1053,7 @@ const chains = {
1024
1053
  "symbol": "XPL"
1025
1054
  },
1026
1055
  "network": "testnet",
1056
+ "publicRpcUrl": "https://testnet-rpc.plasma.to",
1027
1057
  "settlementLayers": [
1028
1058
  "ECO"
1029
1059
  ],
@@ -1073,6 +1103,7 @@ const chains = {
1073
1103
  "addressPath": "/address/",
1074
1104
  "txPath": "/tx/"
1075
1105
  },
1106
+ "iconSlug": "arbitrum",
1076
1107
  "name": "Arbitrum One",
1077
1108
  "nativeToken": {
1078
1109
  "address": "0x0000000000000000000000000000000000000000",
@@ -1080,6 +1111,7 @@ const chains = {
1080
1111
  "symbol": "ETH"
1081
1112
  },
1082
1113
  "network": "mainnet",
1114
+ "publicRpcUrl": "https://arb1.arbitrum.io/rpc",
1083
1115
  "settlementLayers": [
1084
1116
  "ACROSS",
1085
1117
  "ECO",
@@ -1167,6 +1199,7 @@ const chains = {
1167
1199
  "addressPath": "/address/",
1168
1200
  "txPath": "/tx/"
1169
1201
  },
1202
+ "iconSlug": "avalanche",
1170
1203
  "name": "Avalanche",
1171
1204
  "nativeToken": {
1172
1205
  "address": "0x0000000000000000000000000000000000000000",
@@ -1174,6 +1207,7 @@ const chains = {
1174
1207
  "symbol": "AVAX"
1175
1208
  },
1176
1209
  "network": "mainnet",
1210
+ "publicRpcUrl": "https://api.avax.network/ext/bc/C/rpc",
1177
1211
  "settlementLayers": [
1178
1212
  "ACROSS",
1179
1213
  "RELAY",
@@ -1254,6 +1288,7 @@ const chains = {
1254
1288
  "addressPath": "/address/",
1255
1289
  "txPath": "/tx/"
1256
1290
  },
1291
+ "iconSlug": "base",
1257
1292
  "name": "Base Sepolia",
1258
1293
  "nativeToken": {
1259
1294
  "address": "0x0000000000000000000000000000000000000000",
@@ -1261,6 +1296,7 @@ const chains = {
1261
1296
  "symbol": "ETH"
1262
1297
  },
1263
1298
  "network": "testnet",
1299
+ "publicRpcUrl": "https://sepolia.base.org",
1264
1300
  "settlementLayers": [
1265
1301
  "ACROSS",
1266
1302
  "ECO",
@@ -1339,6 +1375,7 @@ const chains = {
1339
1375
  "addressPath": "/address/",
1340
1376
  "txPath": "/tx/"
1341
1377
  },
1378
+ "iconSlug": "arbitrum",
1342
1379
  "name": "Arbitrum Sepolia",
1343
1380
  "nativeToken": {
1344
1381
  "address": "0x0000000000000000000000000000000000000000",
@@ -1346,6 +1383,7 @@ const chains = {
1346
1383
  "symbol": "ETH"
1347
1384
  },
1348
1385
  "network": "testnet",
1386
+ "publicRpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
1349
1387
  "settlementLayers": [
1350
1388
  "ACROSS",
1351
1389
  "ECO",
@@ -1410,6 +1448,7 @@ const chains = {
1410
1448
  "addressPath": "/address/",
1411
1449
  "txPath": "/tx/"
1412
1450
  },
1451
+ "iconSlug": "katana",
1413
1452
  "name": "Katana",
1414
1453
  "nativeToken": {
1415
1454
  "address": "0x0000000000000000000000000000000000000000",
@@ -1417,6 +1456,7 @@ const chains = {
1417
1456
  "symbol": "ETH"
1418
1457
  },
1419
1458
  "network": "mainnet",
1459
+ "publicRpcUrl": "https://rpc.katana.network",
1420
1460
  "settlementLayers": [
1421
1461
  "RELAY",
1422
1462
  "RHINO"
@@ -1478,6 +1518,7 @@ const chains = {
1478
1518
  "txPath": "/explorer/tx/",
1479
1519
  "url": "https://app.hyperliquid.xyz"
1480
1520
  },
1521
+ "iconSlug": "hyperevm",
1481
1522
  "name": "HyperCore Spot",
1482
1523
  "nativeToken": {
1483
1524
  "address": "0x0000000000000000000000000000000000000000",
@@ -1518,6 +1559,7 @@ const chains = {
1518
1559
  "txPath": "/explorer/tx/",
1519
1560
  "url": "https://app.hyperliquid.xyz"
1520
1561
  },
1562
+ "iconSlug": "hyperevm",
1521
1563
  "name": "HyperCore Perp",
1522
1564
  "nativeToken": {
1523
1565
  "address": "0x0000000000000000000000000000000000000000",
@@ -1558,6 +1600,7 @@ const chains = {
1558
1600
  "addressPath": "/address/",
1559
1601
  "txPath": "/tx/"
1560
1602
  },
1603
+ "iconSlug": "mainnet",
1561
1604
  "name": "Sepolia",
1562
1605
  "nativeToken": {
1563
1606
  "address": "0x0000000000000000000000000000000000000000",
@@ -1565,6 +1608,7 @@ const chains = {
1565
1608
  "symbol": "ETH"
1566
1609
  },
1567
1610
  "network": "testnet",
1611
+ "publicRpcUrl": "https://11155111.rpc.thirdweb.com",
1568
1612
  "settlementLayers": [
1569
1613
  "ACROSS",
1570
1614
  "ECO",
@@ -1639,6 +1683,7 @@ const chains = {
1639
1683
  "addressPath": "/address/",
1640
1684
  "txPath": "/tx/"
1641
1685
  },
1686
+ "iconSlug": "optimism",
1642
1687
  "name": "OP Sepolia",
1643
1688
  "nativeToken": {
1644
1689
  "address": "0x0000000000000000000000000000000000000000",
@@ -1646,6 +1691,7 @@ const chains = {
1646
1691
  "symbol": "ETH"
1647
1692
  },
1648
1693
  "network": "testnet",
1694
+ "publicRpcUrl": "https://sepolia.optimism.io",
1649
1695
  "settlementLayers": [
1650
1696
  "ACROSS",
1651
1697
  "ECO",
@@ -1710,6 +1756,7 @@ const chains = {
1710
1756
  "txPath": "/#/transaction/",
1711
1757
  "url": "https://tronscan.org"
1712
1758
  },
1759
+ "iconSlug": "tron",
1713
1760
  "name": "Tron",
1714
1761
  "nativeToken": {
1715
1762
  "address": "0x0000000000000000000000000000000000000000",
@@ -1757,6 +1804,7 @@ const chains = {
1757
1804
  "txPath": "/tx/",
1758
1805
  "url": "https://solscan.io"
1759
1806
  },
1807
+ "iconSlug": "solana",
1760
1808
  "name": "Solana",
1761
1809
  "nativeToken": {
1762
1810
  "address": "11111111111111111111111111111111",
@@ -1,2 +1,2 @@
1
- declare const PACKAGE_VERSION = "1.11.2";
1
+ declare const PACKAGE_VERSION = "1.13.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.13.0";
6
6
  exports.PACKAGE_VERSION = PACKAGE_VERSION;
@@ -8,7 +8,7 @@ type SupportedOPStackMainnet = Extract<EnumNumberValues<typeof OPStackChains>, S
8
8
  type SupportedOPStackTestnet = Extract<EnumNumberValues<typeof OPStackChains>, SupportedTestnet>;
9
9
  type SupportedChain = SupportedMainnet | SupportedTestnet;
10
10
  type SettlementLayer = 'ACROSS' | 'ECO' | 'RELAY' | 'OFT' | 'NEAR' | 'RHINO' | 'CCTP';
11
- type SwapQuoter = '1inch' | '0x' | 'velora' | 'kyberswap' | 'fynd';
11
+ type SwapQuoter = '1inch' | '0x' | 'velora' | 'kyberswap' | 'fynd' | 'fynd-hosted';
12
12
  type PegGroup = 'USD' | 'ETH';
13
13
  type VmType = 'evm' | 'svm' | 'tvm';
14
14
  type ChainNetwork = 'mainnet' | 'testnet';
@@ -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.13.0",
4
4
  "description": "Reusable configuration files for Rhinestone services",
5
5
  "author": {
6
6
  "name": "Rhinestone",