Przeglądaj źródła

feat(entropy-explorer): implement some feedback items

This commit implements the following items to address feedback:

- Use fortuna-staging for testnet chains
- Make "All Mainnet Chains" and "All Testnet Chains" separate options
- Use slugs instead of chain ids in query params
- Fix warning about table missing row header
- Clarify what the callback error message is
Connor Prussin 3 miesięcy temu
rodzic
commit
488ff7c8e3

+ 4 - 6
apps/entropy-explorer/src/components/Address/index.tsx

@@ -3,12 +3,12 @@ import { Link } from "@pythnetwork/component-library/Link";
 import { useMemo } from "react";
 
 import styles from "./index.module.scss";
-import { EntropyDeployments } from "../../entropy-deployments";
+import type { EntropyDeployment } from "../../entropy-deployments";
 import { truncate } from "../../truncate";
 
 type Props = {
   value: string;
-  chain: keyof typeof EntropyDeployments;
+  chain: EntropyDeployment;
   isAccount?: boolean | undefined;
 };
 
@@ -21,11 +21,9 @@ export const Transaction = (props: Omit<Props, "isAccount">) => (
 );
 
 const Address = ({ value, chain, isAccount }: Props) => {
-  const { explorerTxTemplate, explorerAccountTemplate } =
-    EntropyDeployments[chain];
   const explorerTemplate = isAccount
-    ? explorerAccountTemplate
-    : explorerTxTemplate;
+    ? chain.explorerAccountTemplate
+    : chain.explorerTxTemplate;
   const truncatedValue = useMemo(() => truncate(value), [value]);
   return (
     <div className={styles.address}>

+ 1 - 1
apps/entropy-explorer/src/components/Home/search-controls.module.scss → apps/entropy-explorer/src/components/Home/chain-tag.module.scss

@@ -1,6 +1,6 @@
 @use "@pythnetwork/component-library/theme";
 
-.chainSelectItem {
+.chainTag {
   display: grid;
   grid-template-columns: max-content 1fr;
   gap: theme.spacing(2);

+ 17 - 0
apps/entropy-explorer/src/components/Home/chain-tag.tsx

@@ -0,0 +1,17 @@
+import clsx from "clsx";
+import Image from "next/image";
+import type { ComponentProps } from "react";
+
+import styles from "./chain-tag.module.scss";
+import type { EntropyDeployment } from "../../entropy-deployments";
+
+export const ChainTag = ({
+  chain,
+  className,
+  ...props
+}: { chain: EntropyDeployment } & ComponentProps<"div">) => (
+  <div className={clsx(styles.chainTag, className)} {...props}>
+    <Image alt="" src={chain.icon} width={20} height={20} />
+    {chain.name}
+  </div>
+);

+ 4 - 0
apps/entropy-explorer/src/components/Home/index.module.scss

@@ -27,6 +27,10 @@
       width: 100%;
 
       @include theme.breakpoint("lg") {
+        width: theme.spacing(80);
+      }
+
+      @include theme.breakpoint("xl") {
         width: theme.spacing(100);
       }
     }

+ 2 - 7
apps/entropy-explorer/src/components/Home/index.tsx

@@ -12,7 +12,7 @@ import {
   ChainSelect,
   StatusSelect,
 } from "./search-controls";
-import { isValidDeployment } from "../../entropy-deployments";
+import { parseChainSlug } from "../../entropy-deployments";
 import type { Args } from "../../requests";
 import { getRequests, ResultType } from "../../requests";
 
@@ -94,12 +94,7 @@ const Results = async (props: Props) => {
               searchParams.search,
               searchParams.status,
             ].join(",")}
-            chain={
-              searchParams.chain !== undefined &&
-              isValidDeployment(searchParams.chain)
-                ? searchParams.chain
-                : undefined
-            }
+            chain={parseChainSlug(searchParams.chain)}
             search={searchParams.search}
             currentPage={results.currentPage}
             now={new Date()}

+ 14 - 8
apps/entropy-explorer/src/components/Home/request-drawer.tsx

@@ -13,7 +13,6 @@ import { useNumberFormatter } from "react-aria";
 import TimeAgo from "react-timeago";
 
 import styles from "./request-drawer.module.scss";
-import { EntropyDeployments } from "../../entropy-deployments";
 import { getErrorDetails } from "../../errors";
 import type {
   Request,
@@ -25,6 +24,7 @@ import { truncate } from "../../truncate";
 import { Account, Transaction } from "../Address";
 import { Status as StatusComponent } from "../Status";
 import { Timestamp } from "../Timestamp";
+import { ChainTag } from "./chain-tag";
 
 export const mkRequestDrawer = (
   request: Request,
@@ -97,6 +97,11 @@ const RequestDrawerBody = ({
           },
         ]}
         rows={[
+          {
+            id: "chain",
+            field: "Chain",
+            value: <ChainTag chain={request.chain} />,
+          },
           {
             id: "requestTimestamp",
             field: "Request Timestamp",
@@ -244,8 +249,7 @@ const RequestDrawerBody = ({
 };
 
 const CallbackErrorInfo = ({ request }: { request: CallbackErrorRequest }) => {
-  const deployment = EntropyDeployments[request.chain];
-  const retryCommand = `cast send ${deployment.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userContribution} ${request.randomNumber} -r ${deployment.rpc} --private-key <YOUR_PRIVATE_KEY>`;
+  const retryCommand = `cast send ${request.chain.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userContribution} ${request.randomNumber} -r ${request.chain.rpc} --private-key <YOUR_PRIVATE_KEY>`;
 
   return (
     <>
@@ -268,7 +272,7 @@ const CallbackErrorInfo = ({ request }: { request: CallbackErrorRequest }) => {
           Help
         </Button>
         <div className={styles.failureMessage}>
-          <CallbackFailureMessage reason={request.returnValue} />
+          <FailureMessage reason={request.returnValue} />
         </div>
       </InfoBox>
       <InfoBox
@@ -288,7 +292,7 @@ const CallbackErrorInfo = ({ request }: { request: CallbackErrorRequest }) => {
             marginTop: "16px",
           }}
         >
-          <CopyButton text={retryCommand}>Copy Forge Command</CopyButton>
+          <CopyButton text={retryCommand}>Copy Cast Command</CopyButton>
           <Button
             size="sm"
             variant="ghost"
@@ -327,7 +331,7 @@ const RevealFailedInfo = ({ request }: { request: FailedRequest }) => (
       Help
     </Button>
     <div className={styles.failureMessage}>
-      <CallbackFailureMessage reason={request.reason} />
+      <FailureMessage reason={request.reason} />
     </div>
   </InfoBox>
 );
@@ -340,7 +344,7 @@ const getHelpLink = (reason: string) => {
   );
 };
 
-const CallbackFailureMessage = ({ reason }: { reason: string }) => {
+const FailureMessage = ({ reason }: { reason: string }) => {
   const details = getErrorDetails(reason);
   return details ? (
     <>
@@ -350,6 +354,8 @@ const CallbackFailureMessage = ({ reason }: { reason: string }) => {
       </p>
     </>
   ) : (
-    reason
+    <>
+      <b>Error response:</b> {reason}
+    </>
   );
 };

+ 0 - 5
apps/entropy-explorer/src/components/Home/results.module.scss

@@ -22,11 +22,6 @@
 
 .chain {
   @include theme.text("sm", "medium");
-
-  display: flex;
-  flex-flow: row nowrap;
-  gap: theme.spacing(2);
-  align-items: center;
 }
 
 .timestamp {

+ 20 - 14
apps/entropy-explorer/src/components/Home/results.tsx

@@ -5,12 +5,13 @@ import { NoResults as NoResultsImpl } from "@pythnetwork/component-library/NoRes
 import type { RowConfig } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
-import Image from "next/image";
 import type { ComponentProps } from "react";
 import { useMemo } from "react";
 
+import { ChainTag } from "./chain-tag";
 import { mkRequestDrawer } from "./request-drawer";
 import styles from "./results.module.scss";
+import type { ChainSlug } from "../../entropy-deployments";
 import { EntropyDeployments } from "../../entropy-deployments";
 import type { Request } from "../../requests";
 import { Status } from "../../requests";
@@ -24,7 +25,7 @@ type Props = {
   search?: string | undefined;
   isUpdating?: boolean | undefined;
   now: Date;
-  chain?: keyof typeof EntropyDeployments | undefined;
+  chain: ChainSlug;
 };
 
 export const Results = ({
@@ -44,7 +45,7 @@ export const Results = ({
           drawer.open(mkRequestDrawer(request, now));
         },
         data: {
-          chain: <Chain chain={request.chain} />,
+          chain: <ChainTag className={styles.chain} chain={request.chain} />,
           timestamp: (
             <div className={styles.timestamp}>
               <Timestamp timestamp={request.requestTimestamp} now={now} />
@@ -86,7 +87,7 @@ type ResultsImplProps =
     }
   | {
       isLoading?: false | undefined;
-      chain?: keyof typeof EntropyDeployments | undefined;
+      chain: ChainSlug;
       rows: (RowConfig<(typeof defaultProps)["columns"][number]["id"]> & {
         textValue: string;
       })[];
@@ -132,7 +133,7 @@ const ResultsImpl = (props: ResultsImplProps) => (
 
 type NoResultsProps = {
   search?: string | undefined;
-  chain?: keyof typeof EntropyDeployments | undefined;
+  chain: ChainSlug;
 };
 
 const NoResults = ({ search, chain }: NoResultsProps) => {
@@ -143,7 +144,7 @@ const NoResults = ({ search, chain }: NoResultsProps) => {
         <>
           <p>
             We couldn{"'"}t find any results for your query on{" "}
-            {chain ? EntropyDeployments[chain].name : "any chain"}.
+            <ChainName chain={chain} />
           </p>
           <p>Would you like to try your search on a different chain?</p>
           <ChainSelect
@@ -159,14 +160,18 @@ const NoResults = ({ search, chain }: NoResultsProps) => {
   );
 };
 
-const Chain = ({ chain }: { chain: keyof typeof EntropyDeployments }) => {
-  const chainInfo = EntropyDeployments[chain];
-  return (
-    <div className={styles.chain}>
-      <Image alt="" src={chainInfo.icon} width={20} height={20} />
-      {chainInfo.name}
-    </div>
-  );
+const ChainName = ({ chain }: { chain: ChainSlug }) => {
+  switch (chain) {
+    case "all-mainnet": {
+      return "any mainnet chain";
+    }
+    case "all-testnet": {
+      return "any testnet chain";
+    }
+    default: {
+      return EntropyDeployments[chain].name;
+    }
+  }
 };
 
 const defaultProps = {
@@ -185,6 +190,7 @@ const defaultProps = {
       name: "SEQUENCE NUMBER",
       alignment: "center",
       width: 20,
+      isRowHeader: true,
     },
     {
       id: "timestamp" as const,

+ 30 - 44
apps/entropy-explorer/src/components/Home/search-controls.tsx

@@ -10,15 +10,19 @@ import type { ComponentProps } from "react";
 import { Suspense, useCallback, useMemo, useTransition } from "react";
 import { useCollator } from "react-aria";
 
-import {
-  EntropyDeployments,
-  isValidDeployment,
-} from "../../entropy-deployments";
+import type { ChainSlug } from "../../entropy-deployments";
 import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from "../../pages";
 import { Status, StatusParams } from "../../requests";
 import type { ConstrainedOmit } from "../../type-utils";
 import { Status as StatusComponent } from "../Status";
-import styles from "./search-controls.module.scss";
+import { ChainTag } from "./chain-tag";
+import {
+  EntropyDeployments,
+  CHAIN_LABELS,
+  getChainName,
+  isSpecialChainKey,
+  parseChainSlug,
+} from "../../entropy-deployments";
 
 export const SearchBar = (props: ComponentProps<typeof ResolvedSearchBar>) => (
   <Suspense fallback={<SearchInput isPending {...props} />}>
@@ -173,10 +177,6 @@ const parseStatus = (value: string) => {
 const serializeStatus = (value: ReturnType<typeof parseStatus>) =>
   value === "all" ? "" : StatusParams[value];
 
-type Deployment =
-  | ReturnType<typeof entropyDeploymentsByNetwork>[number]
-  | { id: "all" };
-
 export const ChainSelect = (
   props: ComponentProps<typeof ResolvedChainSelect>,
 ) => (
@@ -187,16 +187,16 @@ export const ChainSelect = (
 
 const ResolvedChainSelect = (
   props: ConstrainedOmit<
-    SelectProps<Deployment>,
+    SelectProps<{ id: ChainSlug }>,
     keyof ReturnType<typeof useChainSelect>
   >,
 ) => <Select {...useChainSelect()} {...props} />;
 
 const useChainSelect = () => {
-  const chain = useSearchParam({
+  const chain = useSearchParam<ChainSlug>({
     paramName: "chain",
-    parse: parseChain,
-    defaultValue: "all",
+    parse: parseChainSlug,
+    defaultValue: "all-mainnet",
     serialize: toString,
   });
   const collator = useCollator();
@@ -205,48 +205,43 @@ const useChainSelect = () => {
     selectedKey: chain.value,
     onSelectionChange: chain.onChange,
     isPending: chain.isTransitioning,
-    buttonLabel:
-      chain.value === "all"
-        ? "All Chains"
-        : EntropyDeployments[chain.value].name,
+    buttonLabel: getChainName(chain.value),
     optionGroups: useMemo(
       () => [
-        {
-          name: "ALL",
-          options: [{ id: "all" as const }],
-          hideLabel: true,
-        },
         {
           name: "MAINNET",
-          options: entropyDeploymentsByNetwork(collator, false),
+          options: [
+            { id: "all-mainnet" as const },
+            ...entropyDeploymentsByNetwork(collator, false),
+          ],
         },
         {
           name: "TESTNET",
-          options: entropyDeploymentsByNetwork(collator, true),
+          options: [
+            { id: "all-testnet" as const },
+            ...entropyDeploymentsByNetwork(collator, true),
+          ],
         },
       ],
       [collator],
     ),
     show: useCallback(
-      (chain: Deployment) =>
-        chain.id === "all" ? (
-          "All Chains"
+      (chain: { id: ChainSlug }) =>
+        isSpecialChainKey(chain.id) ? (
+          CHAIN_LABELS[chain.id]
         ) : (
-          <div className={styles.chainSelectItem}>
-            <Image alt={chain.name} src={chain.icon} width={20} height={20} />
-            {chain.name}
-          </div>
+          <ChainTag chain={EntropyDeployments[chain.id]} />
         ),
       [],
     ),
     textValue: useCallback(
-      (chain: Deployment) => (chain.id === "all" ? "All" : chain.name),
+      (chain: { id: ChainSlug }) => getChainName(chain.id),
       [],
     ),
-    ...(chain.value !== "all" && {
+    ...(!isSpecialChainKey(chain.value) && {
       icon: (
         <Image
-          alt={EntropyDeployments[chain.value].name}
+          alt=""
           src={EntropyDeployments[chain.value].icon}
           width={20}
           height={20}
@@ -264,7 +259,7 @@ const entropyDeploymentsByNetwork = (
     .map(([slug, chain]) => {
       return {
         ...chain,
-        id: Number.parseInt(slug, 10) as keyof typeof EntropyDeployments,
+        id: slug as keyof typeof EntropyDeployments,
       };
     })
     .filter((chain) => chain.isTestnet === isTestnet)
@@ -276,15 +271,6 @@ const parseInt = (value: string) => Number.parseInt(value, 10);
 const toString = <T extends { toString: () => string }>(value: T) =>
   value.toString();
 
-const parseChain = (value: string) => {
-  if (value === "all") {
-    return "all";
-  } else {
-    const parsedId = Number.parseInt(value, 10);
-    return isValidDeployment(parsedId) ? parsedId : "all";
-  }
-};
-
 const useSearchParam = <T,>({
   paramName,
   parse,

+ 136 - 49
apps/entropy-explorer/src/entropy-deployments.tsx

@@ -6,10 +6,11 @@ export type EntropyDeployment = {
   name: string;
   icon: string;
   isTestnet: boolean;
+  chainId: number;
 };
 
 export const EntropyDeployments = {
-  [10]: {
+  "op-mainnet": {
     address: "0xdF21D137Aadc95588205586636710ca2890538d5",
     name: "OP Mainnet",
     rpc: "https://optimism.llamarpc.com",
@@ -17,8 +18,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://optimistic.etherscan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 10,
   },
-  [130]: {
+  unichain: {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Unichain",
     rpc: "https://mainnet.unichain.org",
@@ -26,8 +28,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://unichain.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 130,
   },
-  [146]: {
+  "sonic-mainnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Sonic Mainnet",
     rpc: "https://rpc.soniclabs.com",
@@ -35,8 +38,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://sonicscan.org/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 146,
   },
-  [919]: {
+  "mode-testnet": {
     address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
     name: "Mode Testnet",
     rpc: "https://sepolia.mode.network/",
@@ -44,8 +48,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://testnet.modescan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_mode?w=20&h=20",
     isTestnet: true,
+    chainId: 919,
   },
-  [999]: {
+  hyperevm: {
     address: "0xfA25E653b44586dBbe27eE9d252192F0e4956683",
     name: "HyperEVM",
     rpc: "https://rpc.hyperliquid.xyz/evm",
@@ -54,8 +59,9 @@ export const EntropyDeployments = {
       "https://hyperliquid.cloud.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_hyperevm?w=20&h=20",
     isTestnet: false,
+    chainId: 999,
   },
-  [1001]: {
+  "kaia-kairos-testnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Kaia Kairos Testnet",
     rpc: "https://rpc.ankr.com/klaytn_testnet",
@@ -63,8 +69,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://kairos.kaiascan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_klaytn.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 1001,
   },
-  [1301]: {
+  "unichain-sepolia-testnet": {
     address: "0x8D254a21b3C86D32F7179855531CE99164721933",
     name: "Unichain Sepolia Testnet",
     rpc: "https://sepolia.unichain.org",
@@ -73,8 +80,9 @@ export const EntropyDeployments = {
       "https://unichain-sepolia.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 1301,
   },
-  [1315]: {
+  "story-aeneid-testnet": {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "Story Aeneid Testnet",
     rpc: "https://aeneid.storyrpc.io",
@@ -82,8 +90,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://aeneid.storyscan.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_story?w=20&h=20",
     isTestnet: true,
+    chainId: 1315,
   },
-  [1328]: {
+  "sei-testnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Sei Testnet",
     rpc: "https://evm-rpc-testnet.sei-apis.com",
@@ -92,8 +101,9 @@ export const EntropyDeployments = {
       "https://seitrace.com/address/$ADDRESS?chain=atlantic-2",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 1328,
   },
-  [1329]: {
+  "sei-network": {
     address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
     name: "Sei Network",
     rpc: "https://evm-rpc.sei-apis.com",
@@ -102,8 +112,9 @@ export const EntropyDeployments = {
       "https://seitrace.com/address/$ADDRESS?chain=pacific-1",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 1329,
   },
-  [1514]: {
+  story: {
     address: "0xdF21D137Aadc95588205586636710ca2890538d5",
     name: "Story",
     rpc: "https://mainnet.storyrpc.io",
@@ -111,8 +122,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://storyscan.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_story?w=20&h=20",
     isTestnet: false,
+    chainId: 1514,
   },
-  [1868]: {
+  soneium: {
     address: "0x0708325268dF9F66270F1401206434524814508b",
     name: "Soneium",
     rpc: "https://soneium.drpc.org",
@@ -120,8 +132,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://soneium.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_soneium.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 1868,
   },
-  [1890]: {
+  "lightlink-phoenix-mainnet": {
     address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
     name: "Lightlink Phoenix Mainnet",
     rpc: "https://replicator.phoenix.lightlink.io/rpc/v1",
@@ -129,8 +142,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://phoenix.lightlink.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_lightlink?w=20&h=20",
     isTestnet: false,
+    chainId: 1890,
   },
-  [1891]: {
+  "lightlink-pegasus-testnet": {
     address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a",
     name: "Lightlink Pegasus Testnet",
     rpc: "https://replicator.pegasus.lightlink.io/rpc/v1",
@@ -138,8 +152,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://pegasus.lightlink.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_lightlink?w=20&h=20",
     isTestnet: true,
+    chainId: 1891,
   },
-  [1946]: {
+  "soneium-testnet-minato": {
     address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
     name: "Soneium Testnet Minato",
     rpc: "https://rpc.minato.soneium.org/",
@@ -148,8 +163,9 @@ export const EntropyDeployments = {
       "https://explorer-testnet.soneium.org/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_soneium.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 1946,
   },
-  [1992]: {
+  "sanko-testnet": {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "Sanko Testnet",
     rpc: "https://sanko-arb-sepolia.rpc.caldera.xyz/http",
@@ -159,8 +175,9 @@ export const EntropyDeployments = {
       "https://sanko-arb-sepolia.explorer.caldera.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sanko.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 1992,
   },
-  [1993]: {
+  "b3-sepolia-testnet": {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "B3 Sepolia Testnet",
     rpc: "https://sepolia.b3.fun/http/",
@@ -168,8 +185,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://sepolia.explorer.b3.fun/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_b3?w=20&h=20",
     isTestnet: true,
+    chainId: 1993,
   },
-  [1996]: {
+  sanko: {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "Sanko",
     rpc: "https://mainnet.sanko.xyz",
@@ -177,8 +195,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://explorer.sanko.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sanko.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 1996,
   },
-  [2741]: {
+  abstract: {
     address: "0x5a4a369F4db5df2054994AF031b7b23949b98c0e",
     name: "Abstract",
     rpc: "https://api.mainnet.abs.xyz",
@@ -186,8 +205,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://abscan.org/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_abstract.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 2741,
   },
-  [4200]: {
+  "merlin-mainnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Merlin Mainnet",
     rpc: "https://rpc.merlinchain.io",
@@ -195,8 +215,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://scan.merlinchain.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_merlin.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 4200,
   },
-  [7000]: {
+  "zetachain-mainnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "ZetaChain Mainnet",
     rpc: "https://zetachain-evm.blockpi.network/v1/rpc/public",
@@ -205,8 +226,9 @@ export const EntropyDeployments = {
       "https://zetachain.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_zetachain.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 7000,
   },
-  [7001]: {
+  "zetachain-testnet": {
     address: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
     name: "ZetaChain Testnet",
     rpc: "https://zetachain-athens-evm.blockpi.network/v1/rpc/public",
@@ -214,8 +236,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://explorer.zetachain.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_zetachain.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 7001,
   },
-  [8217]: {
+  "kaia-mainnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Kaia Mainnet",
     rpc: "https://rpc.ankr.com/klaytn",
@@ -223,8 +246,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://kaiascan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_klaytn.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 8217,
   },
-  [8333]: {
+  b3: {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "B3",
     rpc: "https://mainnet-rpc.b3.fun/http",
@@ -232,8 +256,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://explorer.b3.fun/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_b3?w=20&h=20",
     isTestnet: false,
+    chainId: 8333,
   },
-  [8453]: {
+  base: {
     address: "0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb",
     name: "Base",
     rpc: "https://developer-access-mainnet.base.org/",
@@ -241,8 +266,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://basescan.org/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_base.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 8453,
   },
-  [9788]: {
+  "tabi-testnet-v2": {
     address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
     name: "Tabi Testnet v2",
     rpc: "https://rpc.testnetv2.tabichain.com",
@@ -250,8 +276,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://testnetv2.tabiscan.com/address/$ADDRESS",
     icon: "https://www.tabichain.com/images/new2/tabi.svg",
     isTestnet: true,
+    chainId: 9788,
   },
-  [10_143]: {
+  "monad-testnet": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Monad Testnet",
     rpc: "https://testnet-rpc.monad.xyz",
@@ -260,8 +287,9 @@ export const EntropyDeployments = {
       "https://testnet.monadexplorer.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_monad.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 10_143,
   },
-  [11_124]: {
+  "abstract-sepolia-testnet": {
     address: "0x858687fD592112f7046E394A3Bf10D0C11fF9e63",
     name: "Abstract Sepolia Testnet",
     rpc: "https://api.testnet.abs.xyz",
@@ -270,8 +298,9 @@ export const EntropyDeployments = {
       "https://explorer.testnet.abs.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_abstract.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 11_124,
   },
-  [33_111]: {
+  curtis: {
     address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
     name: "Curtis",
     rpc: "https://curtis.rpc.caldera.xyz/http",
@@ -280,8 +309,9 @@ export const EntropyDeployments = {
       "https://curtis.explorer.caldera.xyz/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_apechain.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 33_111,
   },
-  [33_139]: {
+  apechain: {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "ApeChain",
     rpc: "https://apechain.calderachain.xyz/http",
@@ -289,8 +319,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://apescan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_apechain.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 33_139,
   },
-  [34_443]: {
+  mode: {
     address: "0x8D254a21b3C86D32F7179855531CE99164721933",
     name: "Mode",
     rpc: "https://mainnet.mode.network/",
@@ -298,8 +329,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://explorer.mode.network/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_mode?w=20&h=20",
     isTestnet: false,
+    chainId: 34_443,
   },
-  [42_161]: {
+  "arbitrum-one": {
     address: "0x7698E925FfC29655576D0b361D75Af579e20AdAc",
     name: "Arbitrum One",
     rpc: "https://arb1.arbitrum.io/rpc",
@@ -307,8 +339,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://arbiscan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 42_161,
   },
-  [42_793]: {
+  "etherlink-mainnet": {
     address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
     name: "Etherlink Mainnet",
     rpc: "https://node.mainnet.etherlink.com/",
@@ -316,8 +349,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://explorer.etherlink.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_etherlink.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 42_793,
   },
-  [57_054]: {
+  "sonic-blaze-testnet": {
     address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
     name: "Sonic Blaze Testnet",
     rpc: "https://rpc.blaze.soniclabs.com",
@@ -325,8 +359,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://blaze.soniclabs.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 57_054,
   },
-  [80_069]: {
+  "berachain-bepolia": {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Berachain Bepolia",
     rpc: "https://bepolia.rpc.berachain.com",
@@ -334,8 +369,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://bepolia.beratrail.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_berachain?w=20&h=20",
     isTestnet: true,
+    chainId: 80_069,
   },
-  [80_094]: {
+  berachain: {
     address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
     name: "Berachain",
     rpc: "https://rpc.berachain.com/",
@@ -343,8 +379,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://berascan.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_berachain?w=20&h=20",
     isTestnet: false,
+    chainId: 80_094,
   },
-  [81_457]: {
+  blast: {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "Blast",
     rpc: "https://rpc.blast.io",
@@ -352,8 +389,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://blastscan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_blast.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 81_457,
   },
-  [84_532]: {
+  "base-sepolia-testnet": {
     address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
     name: "Base Sepolia Testnet",
     rpc: "https://sepolia.base.org",
@@ -362,8 +400,9 @@ export const EntropyDeployments = {
       "https://base-sepolia.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_base.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 84_532,
   },
-  [88_882]: {
+  "chiliz-spicy-testnet": {
     address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134",
     name: "Chiliz Spicy Testnet",
     rpc: "https://spicy-rpc.chiliz.com",
@@ -372,8 +411,9 @@ export const EntropyDeployments = {
       "https://spicy-explorer.chiliz.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_chiliz.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 88_882,
   },
-  [88_888]: {
+  "chiliz-chain": {
     address: "0x0708325268dF9F66270F1401206434524814508b",
     name: "Chiliz Chain",
     rpc: "https://rpc.ankr.com/chiliz",
@@ -381,8 +421,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://scan.chiliz.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_chiliz.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 88_888,
   },
-  [128_123]: {
+  "etherlink-testnet": {
     address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
     name: "Etherlink Testnet",
     rpc: "https://node.ghostnet.etherlink.com",
@@ -391,8 +432,9 @@ export const EntropyDeployments = {
       "https://testnet.explorer.etherlink.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_etherlink.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 128_123,
   },
-  [167_000]: {
+  "taiko-alethia": {
     address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
     name: "Taiko Alethia",
     rpc: "https://rpc.mainnet.taiko.xyz",
@@ -400,8 +442,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://taikoscan.network/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_taiko.jpg?w=20&h=20",
     isTestnet: false,
+    chainId: 167_000,
   },
-  [167_009]: {
+  "taiko-hekla": {
     address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
     name: "Taiko Hekla",
     rpc: "https://rpc.hekla.taiko.xyz/",
@@ -409,8 +452,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://hekla.taikoscan.network/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_taiko.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 167_009,
   },
-  [421_614]: {
+  "arbitrum-sepolia": {
     address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440",
     name: "Arbitrum Sepolia",
     rpc: "https://sepolia-rollup.arbitrum.io/rpc",
@@ -418,8 +462,9 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://sepolia.arbiscan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 421_614,
   },
-  [686_868]: {
+  "merlin-testnet": {
     address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
     name: "Merlin Testnet",
     rpc: "https://testnet-rpc.merlinchain.io/",
@@ -428,8 +473,9 @@ export const EntropyDeployments = {
       "https://testnet-scan.merlinchain.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_merlin?w=20&h=20",
     isTestnet: true,
+    chainId: 686_868,
   },
-  [11_155_420]: {
+  "op-sepolia-testnet": {
     address: "0x4821932D0CDd71225A6d914706A621e0389D7061",
     name: "OP Sepolia Testnet",
     rpc: "https://api.zan.top/opt-sepolia",
@@ -438,8 +484,9 @@ export const EntropyDeployments = {
       "https://optimism-sepolia.blockscout.com/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 11_155_420,
   },
-  [168_587_773]: {
+  "blast-sepolia-testnet": {
     address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
     name: "Blast Sepolia Testnet",
     rpc: "https://sepolia.blast.io",
@@ -447,10 +494,50 @@ export const EntropyDeployments = {
     explorerAccountTemplate: "https://sepolia.blastscan.io/address/$ADDRESS",
     icon: "https://icons.llamao.fi/icons/chains/rsz_blast.jpg?w=20&h=20",
     isTestnet: true,
+    chainId: 168_587_773,
   },
 } as const satisfies Record<string, EntropyDeployment>;
 
-export const isValidDeployment = (
-  name: number,
+export const isValidDeploymentSlug = (
+  name: string,
 ): name is keyof typeof EntropyDeployments =>
   Object.prototype.hasOwnProperty.call(EntropyDeployments, name);
+
+export type ChainSlug =
+  | keyof typeof EntropyDeployments
+  | "all-mainnet"
+  | "all-testnet";
+
+export const parseChainSlug = (value: string | undefined) => {
+  switch (value) {
+    case "all-mainnet":
+    case "all-testnet": {
+      return value;
+    }
+    default: {
+      return value !== undefined && isValidDeploymentSlug(value)
+        ? value
+        : "all-mainnet";
+    }
+  }
+};
+
+export const getChainName = (chainSlug: ChainSlug) =>
+  isSpecialChainKey(chainSlug)
+    ? CHAIN_LABELS[chainSlug]
+    : EntropyDeployments[chainSlug].name;
+
+export const getChainNetworkId = (chainSlug: ChainSlug) =>
+  chainSlug === "all-mainnet" || chainSlug === "all-testnet"
+    ? undefined
+    : EntropyDeployments[chainSlug].chainId;
+
+export const CHAIN_LABELS = {
+  "all-mainnet": "All Mainnet Chains",
+  "all-testnet": "All Testnet Chains",
+};
+
+export const isSpecialChainKey = (
+  key: unknown,
+): key is keyof typeof CHAIN_LABELS =>
+  Object.keys(CHAIN_LABELS).includes(key as keyof typeof CHAIN_LABELS);

+ 44 - 12
apps/entropy-explorer/src/requests.ts

@@ -1,14 +1,20 @@
 import { z } from "zod";
 
-import { EntropyDeployments, isValidDeployment } from "./entropy-deployments";
+import type { ChainSlug, EntropyDeployment } from "./entropy-deployments";
+import {
+  getChainNetworkId,
+  parseChainSlug,
+  EntropyDeployments,
+} from "./entropy-deployments";
 import type { PAGE_SIZE } from "./pages";
 import { DEFAULT_PAGE_SIZE } from "./pages";
 
-const FORTUNA_URL = "https://fortuna.dourolabs.app/";
+const FORTUNA_MAINNET_URL = "https://fortuna.dourolabs.app/";
+const FORTUNA_TESTNET_URL = "https://fortuna-staging.dourolabs.app/";
 
 export type Args = Partial<{
   search: string;
-  chain: number;
+  chain: string;
   status: string;
   pageSize: PAGE_SIZE;
   page: number;
@@ -21,7 +27,9 @@ export const getRequests = async ({
   pageSize = DEFAULT_PAGE_SIZE,
   page,
 }: Args): Promise<Result> => {
-  const url = new URL("/v1/logs", FORTUNA_URL);
+  const chainSlug = parseChainSlug(chain);
+  const networkId = getChainNetworkId(chainSlug);
+  const url = new URL("/v1/logs", getFortunaUrl(chainSlug));
   url.searchParams.set("min_timestamp", new Date("2023-10-01").toISOString());
   url.searchParams.set("max_timestamp", new Date("2033-10-01").toISOString());
   url.searchParams.set("limit", pageSize.toString());
@@ -31,8 +39,8 @@ export const getRequests = async ({
   if (search) {
     url.searchParams.set("query", search);
   }
-  if (chain) {
-    url.searchParams.set("network_id", chain.toString());
+  if (networkId) {
+    url.searchParams.set("network_id", networkId.toString());
   }
   const fortunaStatus = status ? toFortunaStatus(status) : undefined;
   if (fortunaStatus) {
@@ -55,7 +63,7 @@ export const getRequests = async ({
               numPages: Math.ceil(parsed.data.total_results / pageSize),
               currentPage: parsed.data.requests.map((request) => {
                 const common = {
-                  chain: request.network_id,
+                  chain: getChain(request.network_id),
                   gasLimit: request.gas_limit,
                   provider: request.provider,
                   requestTimestamp: request.created_at,
@@ -194,10 +202,7 @@ const fortunaSchema = z.strictObject({
       created_at: z.string().transform((value) => new Date(value)),
       gas_limit: z.number(),
       last_updated_at: z.string().transform((value) => new Date(value)),
-      network_id: z.number().refine(
-        (value) => isValidDeployment(value),
-        (value) => ({ message: `Unrecognized chain id: ${value.toString()}` }),
-      ),
+      network_id: z.number(),
       provider: hexStringSchema,
       request_block_number: z.number(),
       request_tx_hash: hexStringSchema,
@@ -217,7 +222,7 @@ export enum Status {
 }
 
 type BaseArgs = {
-  chain: keyof typeof EntropyDeployments;
+  chain: EntropyDeployment;
   sequenceNumber: number;
   provider: `0x${string}`;
   sender: `0x${string}`;
@@ -292,3 +297,30 @@ const toFortunaStatus = (status: string) => {
     }
   }
 };
+
+const getChain = (networkId: number) => {
+  const chain = Object.values(EntropyDeployments).find(
+    (deployment) => deployment.chainId === networkId,
+  );
+  if (chain) {
+    return chain;
+  } else {
+    throw new Error(`Invalid chain id: ${networkId.toString()}`);
+  }
+};
+
+const getFortunaUrl = (chainSlug: ChainSlug) => {
+  switch (chainSlug) {
+    case "all-mainnet": {
+      return FORTUNA_MAINNET_URL;
+    }
+    case "all-testnet": {
+      return FORTUNA_TESTNET_URL;
+    }
+    default: {
+      return EntropyDeployments[chainSlug].isTestnet
+        ? FORTUNA_TESTNET_URL
+        : FORTUNA_MAINNET_URL;
+    }
+  }
+};