Browse Source

Merge pull request #2766 from pyth-network/cprussin/entropy-explorer-live

feat(entropy-explorer): bind to live apis
Connor Prussin 3 months ago
parent
commit
b011aee221
35 changed files with 1626 additions and 1221 deletions
  1. 54 0
      apps/entropy-explorer/NOTES.org
  2. 7 0
      apps/entropy-explorer/next.config.js
  3. 0 3
      apps/entropy-explorer/package.json
  4. 0 16
      apps/entropy-explorer/src/components/Address/index.module.scss
  5. 18 10
      apps/entropy-explorer/src/components/Address/index.tsx
  6. 0 126
      apps/entropy-explorer/src/components/Home/chain-select.tsx
  7. 9 0
      apps/entropy-explorer/src/components/Home/index.module.scss
  8. 114 8
      apps/entropy-explorer/src/components/Home/index.tsx
  9. 12 0
      apps/entropy-explorer/src/components/Home/request-drawer.module.scss
  10. 134 72
      apps/entropy-explorer/src/components/Home/request-drawer.tsx
  11. 5 3
      apps/entropy-explorer/src/components/Home/results.module.scss
  12. 94 128
      apps/entropy-explorer/src/components/Home/results.tsx
  13. 0 37
      apps/entropy-explorer/src/components/Home/search-bar.tsx
  14. 0 7
      apps/entropy-explorer/src/components/Home/search-controls.module.scss
  15. 344 0
      apps/entropy-explorer/src/components/Home/search-controls.tsx
  16. 0 95
      apps/entropy-explorer/src/components/Home/status-select.tsx
  17. 0 87
      apps/entropy-explorer/src/components/Home/use-query.ts
  18. 0 21
      apps/entropy-explorer/src/components/Root/evm-provider.tsx
  19. 1 2
      apps/entropy-explorer/src/components/Root/index.tsx
  20. 12 6
      apps/entropy-explorer/src/components/Status/index.tsx
  21. 14 2
      apps/entropy-explorer/src/components/Timestamp/index.tsx
  22. 0 471
      apps/entropy-explorer/src/entropy-deployments.ts
  23. 456 0
      apps/entropy-explorer/src/entropy-deployments.tsx
  24. 32 5
      apps/entropy-explorer/src/errors.ts
  25. 3 0
      apps/entropy-explorer/src/pages.ts
  26. 242 99
      apps/entropy-explorer/src/requests.ts
  27. 1 1
      apps/insights/src/components/Publisher/performance.tsx
  28. 2 0
      packages/component-library/src/ErrorPage/index.tsx
  29. 12 6
      packages/component-library/src/Header/index.tsx
  30. 10 5
      packages/component-library/src/NoResults/index.tsx
  31. 7 0
      packages/component-library/src/Paginator/index.module.scss
  32. 19 0
      packages/component-library/src/Paginator/index.tsx
  33. 10 0
      packages/component-library/src/Select/index.module.scss
  34. 13 1
      packages/component-library/src/Select/index.tsx
  35. 1 10
      pnpm-lock.yaml

+ 54 - 0
apps/entropy-explorer/NOTES.org

@@ -0,0 +1,54 @@
+* TODO Lock timestamp to when page loaded & add sync button
+* TODO Add TODO items in requests.ts
+- providerContribution
+- gasUsed + randomNumber in errored state
+
+* TODO Connect the wallet to execute the transaction.
+* TODO Have a blinking yellow-coloured pending status that can be turned Green using an event listener.
+* TODO A dashboard on the home page to display reference material, such as current fees and usage.
+* TODO Many people use the Block number to search in explorers as well. We can think about it.
+
+* DONE Removing badges from the sequence number + making it bold
+CLOSED: [2025-05-09 Fri 20:06]
+* DONE Rename Caller -> From or Sender
+CLOSED: [2025-05-09 Fri 20:08]
+* DONE Option to filter Callback Status
+CLOSED: [2025-05-09 Fri 20:08]
+* DONE Reorder Failed Tx Fields.
+CLOSED: [2025-05-09 Fri 20:11]
+** Request Timestamp
+** Callback Timestamp
+** Request Tx +Hash+
+** From / Sender
+** Callback Tx +Hash+
+** Provider
+** +User Random Number+ User Contribution
+** Gas Used
+* DONE Put the cast command in the code background with syntax colouring/highlighting.
+CLOSED: [2025-05-09 Fri 20:11]
+* DONE Decoding [[https://docs.pyth.network/entropy/error-codes][Error codes]].
+CLOSED: [2025-05-12 Mon 13:53]
+* DONE The timestamp should be shown in the following format ~9 secs ago (May-09-2025 12:47:11 PM +utc:)~
+CLOSED: [2025-05-12 Mon 14:30]
+* DONE Tooltips
+CLOSED: [2025-05-12 Mon 22:37]
+** Request Tx
+** Callback Tx
+** User Random Number / User Contribution
+** Provider Random Number / User Contribution
+* DONE Show delay/latency (delta between callbackTimestamp and requestTimestamp) somehow
+CLOSED: [2025-05-12 Mon 22:45]
+* DONE As @Tejas mentioned above, a tooltip button for the code can be used, or it can redirect them [[https://docs.pyth.network/entropy/debug-callback-failures][here]].
+CLOSED: [2025-05-12 Mon 22:48]
+* DONE Finish paginator
+CLOSED: [2025-06-06 Fri 11:10]
+* DONE Improve use-search-params
+CLOSED: [2025-06-06 Fri 19:55]
+* DONE Fix callback error page for unknown callback errors (i.e. when fortuna sends a string that isn't an error code)
+CLOSED: [2025-06-06 Fri 20:07]
+* DONE Improve not found message, especially for invalid search
+CLOSED: [2025-06-10 Tue 12:24]
+* DONE Improve ErrorResult handling
+CLOSED: [2025-06-10 Tue 12:24]
+* DONE Add all chains + icons
+CLOSED: [2025-06-10 Tue 16:12]

+ 7 - 0
apps/entropy-explorer/next.config.js

@@ -7,6 +7,13 @@ const config = {
 
 
   pageExtensions: ["ts", "tsx", "mdx"],
   pageExtensions: ["ts", "tsx", "mdx"],
 
 
+  images: {
+    remotePatterns: [
+      new URL("https://icons.llamao.fi/icons/chains/*?w=20&h=20"),
+      new URL("https://www.tabichain.com/images/new2/tabi.svg"),
+    ],
+  },
+
   logging: {
   logging: {
     fetches: {
     fetches: {
       fullUrl: true,
       fullUrl: true,

+ 0 - 3
apps/entropy-explorer/package.json

@@ -23,15 +23,12 @@
     "@phosphor-icons/react": "catalog:",
     "@phosphor-icons/react": "catalog:",
     "@pythnetwork/component-library": "workspace:*",
     "@pythnetwork/component-library": "workspace:*",
     "clsx": "catalog:",
     "clsx": "catalog:",
-    "connectkit": "catalog:",
     "next": "catalog:",
     "next": "catalog:",
     "nuqs": "catalog:",
     "nuqs": "catalog:",
     "react": "catalog:",
     "react": "catalog:",
     "react-aria": "catalog:",
     "react-aria": "catalog:",
     "react-dom": "catalog:",
     "react-dom": "catalog:",
     "react-timeago": "catalog:",
     "react-timeago": "catalog:",
-    "viem": "catalog:",
-    "wagmi": "catalog:",
     "zod": "catalog:"
     "zod": "catalog:"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 0 - 16
apps/entropy-explorer/src/components/Address/index.module.scss

@@ -5,20 +5,4 @@
   flex-flow: row nowrap;
   flex-flow: row nowrap;
   gap: theme.spacing(2);
   gap: theme.spacing(2);
   font-size: theme.font-size("sm");
   font-size: theme.font-size("sm");
-
-  .full {
-    display: none;
-  }
-
-  &:not([data-always-truncate]) {
-    @include theme.breakpoint("xl") {
-      .truncated {
-        display: none;
-      }
-
-      .full {
-        display: unset;
-      }
-    }
-  }
 }
 }

+ 18 - 10
apps/entropy-explorer/src/components/Address/index.tsx

@@ -9,24 +9,32 @@ import { truncate } from "../../truncate";
 type Props = {
 type Props = {
   value: string;
   value: string;
   chain: keyof typeof EntropyDeployments;
   chain: keyof typeof EntropyDeployments;
-  alwaysTruncate?: boolean | undefined;
+  isAccount?: boolean | undefined;
 };
 };
 
 
-export const Address = ({ value, chain, alwaysTruncate }: Props) => {
-  const { explorer } = EntropyDeployments[chain];
+export const Account = (props: Omit<Props, "isAccount">) => (
+  <Address {...props} isAccount />
+);
+
+export const Transaction = (props: Omit<Props, "isAccount">) => (
+  <Address {...props} />
+);
+
+const Address = ({ value, chain, isAccount }: Props) => {
+  const { explorerTxTemplate, explorerAccountTemplate } =
+    EntropyDeployments[chain];
+  const explorerTemplate = isAccount
+    ? explorerAccountTemplate
+    : explorerTxTemplate;
   const truncatedValue = useMemo(() => truncate(value), [value]);
   const truncatedValue = useMemo(() => truncate(value), [value]);
   return (
   return (
-    <div
-      data-always-truncate={alwaysTruncate ? "" : undefined}
-      className={styles.address}
-    >
+    <div className={styles.address}>
       <Link
       <Link
-        href={explorer.replace("$ADDRESS", value)}
+        href={explorerTemplate.replace("$ADDRESS", value)}
         target="_blank"
         target="_blank"
         rel="noreferrer"
         rel="noreferrer"
       >
       >
-        <code className={styles.truncated}>{truncatedValue}</code>
-        <code className={styles.full}>{value}</code>
+        <code>{truncatedValue}</code>
       </Link>
       </Link>
       <CopyButton text={value} iconOnly />
       <CopyButton text={value} iconOnly />
     </div>
     </div>

+ 0 - 126
apps/entropy-explorer/src/components/Home/chain-select.tsx

@@ -1,126 +0,0 @@
-"use client";
-
-import type { Props as SelectProps } from "@pythnetwork/component-library/Select";
-import { Select } from "@pythnetwork/component-library/Select";
-import { ChainIcon } from "connectkit";
-import type { ComponentProps } from "react";
-import { Suspense, useCallback, useMemo } from "react";
-import { useCollator } from "react-aria";
-import * as viemChains from "viem/chains";
-
-import styles from "./chain-select.module.scss";
-import { useQuery } from "./use-query";
-import { EntropyDeployments } from "../../entropy-deployments";
-import type { ConstrainedOmit } from "../../type-utils";
-
-export const ChainSelect = (
-  props: ComponentProps<typeof ResolvedChainSelect>,
-) => (
-  <Suspense
-    fallback={
-      <Select
-        {...defaultProps}
-        {...props}
-        isPending
-        options={[]}
-        defaultSelectedKey={undefined}
-      />
-    }
-  >
-    <ResolvedChainSelect {...props} />
-  </Suspense>
-);
-
-type Deployment =
-  | ReturnType<typeof entropyDeploymentsByNetwork>[number]
-  | { id: "all" };
-
-const ResolvedChainSelect = (
-  props: ConstrainedOmit<
-    SelectProps<Deployment>,
-    keyof typeof defaultProps | keyof ReturnType<typeof useResolvedProps>
-  >,
-) => {
-  const resolvedProps = useResolvedProps();
-
-  return <Select {...defaultProps} {...resolvedProps} {...props} />;
-};
-
-const useResolvedProps = () => {
-  const collator = useCollator();
-  const { chain, setChain } = useQuery();
-  const chains = useMemo(
-    () => [
-      {
-        name: "ALL",
-        options: [{ id: "all" as const }],
-        hideLabel: true,
-      },
-      {
-        name: "MAINNET",
-        options: entropyDeploymentsByNetwork("mainnet", collator),
-      },
-      {
-        name: "TESTNET",
-        options: entropyDeploymentsByNetwork("testnet", collator),
-      },
-    ],
-    [collator],
-  );
-
-  const showChain = useCallback(
-    (chain: Deployment) =>
-      chain.id === "all" ? (
-        "All"
-      ) : (
-        <div className={styles.chainSelectItem}>
-          <ChainIcon id={chain.chainId} />
-          {chain.name}
-        </div>
-      ),
-    [],
-  );
-
-  const chainTextValue = useCallback(
-    (chain: Deployment) => (chain.id === "all" ? "All" : chain.name),
-    [],
-  );
-  // eslint-disable-next-line import/namespace
-  const viemChain = chain ? viemChains[chain] : undefined;
-
-  return {
-    selectedKey: chain ?? ("all" as const),
-    onSelectionChange: setChain,
-    optionGroups: chains,
-    show: showChain,
-    textValue: chainTextValue,
-    buttonLabel: viemChain?.name ?? "Chain",
-    ...(viemChain && {
-      icon: <ChainIcon id={viemChain.id} />,
-    }),
-  };
-};
-
-const defaultProps = {
-  label: "Chain",
-  hideLabel: true,
-  defaultButtonLabel: "Chain",
-} as const;
-
-const entropyDeploymentsByNetwork = (
-  network: "mainnet" | "testnet",
-  collator: ReturnType<typeof useCollator>,
-) =>
-  Object.entries(EntropyDeployments)
-    .map(([slug, chain]) => {
-      // eslint-disable-next-line import/namespace
-      const viemChain = viemChains[slug as keyof typeof EntropyDeployments];
-      return {
-        ...chain,
-        name: viemChain.name,
-        chainId: viemChain.id,
-        id: slug as keyof typeof EntropyDeployments,
-      };
-    })
-    .filter((chain) => chain.network === network)
-    .toSorted((a, b) => collator.compare(a.name, b.name));

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

@@ -19,6 +19,10 @@
   .body {
   .body {
     @include theme.max-width;
     @include theme.max-width;
 
 
+    .statusSelect {
+      width: theme.spacing(44);
+    }
+
     .searchBar {
     .searchBar {
       width: 100%;
       width: 100%;
 
 
@@ -26,5 +30,10 @@
         width: theme.spacing(100);
         width: theme.spacing(100);
       }
       }
     }
     }
+
+    .cardBody {
+      background: theme.color("background", "primary");
+      border-radius: theme.border-radius("xl");
+    }
   }
   }
 }
 }

+ 114 - 8
apps/entropy-explorer/src/components/Home/index.tsx

@@ -1,13 +1,26 @@
 import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes";
 import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes";
 import { Card } from "@pythnetwork/component-library/Card";
 import { Card } from "@pythnetwork/component-library/Card";
+import { ErrorPage } from "@pythnetwork/component-library/ErrorPage";
+import { NoResults } from "@pythnetwork/component-library/NoResults";
+import { Suspense } from "react";
 
 
-import { ChainSelect } from "./chain-select";
 import styles from "./index.module.scss";
 import styles from "./index.module.scss";
-import { Results } from "./results";
-import { SearchBar } from "./search-bar";
-import { StatusSelect } from "./status-select";
+import { Results as ResultsImpl, ResultsLoading } from "./results";
+import {
+  SearchBar,
+  Paginator as PaginatorImpl,
+  ChainSelect,
+  StatusSelect,
+} from "./search-controls";
+import { isValidDeployment } from "../../entropy-deployments";
+import type { Args } from "../../requests";
+import { getRequests, ResultType } from "../../requests";
 
 
-export const Home = () => (
+type Props = {
+  searchParams: Promise<Args>;
+};
+
+export const Home = (props: Props) => (
   <div className={styles.home}>
   <div className={styles.home}>
     <h1 className={styles.header}>Requests</h1>
     <h1 className={styles.header}>Requests</h1>
     <div className={styles.body}>
     <div className={styles.body}>
@@ -16,18 +29,111 @@ export const Home = () => (
         icon={<ListDashes />}
         icon={<ListDashes />}
         toolbar={
         toolbar={
           <>
           <>
-            <ChainSelect variant="outline" size="sm" placement="bottom right" />
+            <ChainSelect
+              label="Chain"
+              hideLabel
+              variant="outline"
+              size="sm"
+              placement="bottom right"
+            />
             <StatusSelect
             <StatusSelect
+              label="Status"
+              hideLabel
+              defaultButtonLabel="Status"
+              hideGroupLabel
               variant="outline"
               variant="outline"
               size="sm"
               size="sm"
               placement="bottom right"
               placement="bottom right"
+              className={styles.statusSelect ?? ""}
+            />
+            <SearchBar
+              size="sm"
+              placeholder="Sequence number, provider, sender or tx hash"
+              className={styles.searchBar ?? ""}
             />
             />
-            <SearchBar className={styles.searchBar ?? ""} />
           </>
           </>
         }
         }
+        footer={
+          <Suspense>
+            <Paginator {...props} />
+          </Suspense>
+        }
       >
       >
-        <Results />
+        <div className={styles.cardBody}>
+          <Suspense fallback={<ResultsLoading />}>
+            <Results {...props} />
+          </Suspense>
+        </div>
       </Card>
       </Card>
     </div>
     </div>
   </div>
   </div>
 );
 );
+
+const Results = async (props: Props) => {
+  try {
+    const searchParams = await props.searchParams;
+    const results = await getRequests(searchParams);
+    switch (results.type) {
+      case ResultType.BadSearch: {
+        return (
+          <NoResults
+            header="Invalid Search"
+            body="Your search query is not a valid transaction hash, sequence number, or sender."
+            query={results.search}
+          />
+        );
+      }
+      case ResultType.ErrorResult: {
+        return <ErrorPage error={results.error} />;
+      }
+      case ResultType.Success: {
+        return (
+          <ResultsImpl
+            key={[
+              searchParams.chain,
+              searchParams.search,
+              searchParams.status,
+            ].join(",")}
+            chain={
+              searchParams.chain !== undefined &&
+              isValidDeployment(searchParams.chain)
+                ? searchParams.chain
+                : undefined
+            }
+            search={searchParams.search}
+            currentPage={results.currentPage}
+            now={new Date()}
+          />
+        );
+      }
+    }
+  } catch (error) {
+    if (error instanceof Error) {
+      return <ErrorPage error={error} />;
+    } else {
+      const err = new Error("Unknown Error");
+      err.cause = error;
+      return <ErrorPage error={err} />;
+    }
+  }
+};
+
+const Paginator = async (props: Props) => {
+  try {
+    const searchParams = await props.searchParams;
+    const results = await getRequests(searchParams);
+    switch (results.type) {
+      case ResultType.Success: {
+        return <PaginatorImpl numPages={results.numPages} />;
+      }
+      case ResultType.BadSearch: {
+        return <PaginatorImpl numPages={0} />;
+      }
+      case ResultType.ErrorResult: {
+        return <></>;
+      }
+    }
+  } catch {
+    return <></>;
+  }
+};

+ 12 - 0
apps/entropy-explorer/src/components/Home/request-drawer.module.scss

@@ -36,6 +36,18 @@
     margin-right: theme.spacing(4);
     margin-right: theme.spacing(4);
     position: relative;
     position: relative;
 
 
+    .helpButton {
+      position: absolute;
+      top: theme.spacing(2);
+      right: theme.spacing(2);
+    }
+
+    .failureMessage {
+      overflow: auto;
+      width: 100%;
+      word-break: break-word;
+    }
+
     p {
     p {
       margin: 0;
       margin: 0;
 
 

+ 134 - 72
apps/entropy-explorer/src/components/Home/request-drawer.tsx

@@ -15,22 +15,35 @@ import TimeAgo from "react-timeago";
 import styles from "./request-drawer.module.scss";
 import styles from "./request-drawer.module.scss";
 import { EntropyDeployments } from "../../entropy-deployments";
 import { EntropyDeployments } from "../../entropy-deployments";
 import { getErrorDetails } from "../../errors";
 import { getErrorDetails } from "../../errors";
-import type { Request, CallbackErrorRequest } from "../../requests";
+import type {
+  Request,
+  CallbackErrorRequest,
+  FailedRequest,
+} from "../../requests";
 import { Status } from "../../requests";
 import { Status } from "../../requests";
 import { truncate } from "../../truncate";
 import { truncate } from "../../truncate";
-import { Address } from "../Address";
+import { Account, Transaction } from "../Address";
 import { Status as StatusComponent } from "../Status";
 import { Status as StatusComponent } from "../Status";
 import { Timestamp } from "../Timestamp";
 import { Timestamp } from "../Timestamp";
 
 
-export const mkRequestDrawer = (request: Request): OpenDrawerArgs => ({
+export const mkRequestDrawer = (
+  request: Request,
+  now: Date,
+): OpenDrawerArgs => ({
   title: `Request ${truncate(request.requestTxHash)}`,
   title: `Request ${truncate(request.requestTxHash)}`,
-  headingExtra: <StatusComponent prefix="CALLBACK " status={request.status} />,
+  headingExtra: <StatusComponent status={request.status} />,
   bodyClassName: styles.requestDrawer ?? "",
   bodyClassName: styles.requestDrawer ?? "",
   fill: true,
   fill: true,
-  contents: <RequestDrawerBody request={request} />,
+  contents: <RequestDrawerBody request={request} now={now} />,
 });
 });
 
 
-const RequestDrawerBody = ({ request }: { request: Request }) => {
+const RequestDrawerBody = ({
+  request,
+  now,
+}: {
+  request: Request;
+  now: Date;
+}) => {
   const gasFormatter = useNumberFormatter({ maximumFractionDigits: 3 });
   const gasFormatter = useNumberFormatter({ maximumFractionDigits: 3 });
 
 
   return (
   return (
@@ -42,12 +55,12 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
           small
           small
           variant="primary"
           variant="primary"
           stat={
           stat={
-            request.status === Status.Pending ? (
-              <StatusComponent prefix="CALLBACK " status={Status.Pending} />
-            ) : (
+            "randomNumber" in request ? (
               <CopyButton text={request.randomNumber}>
               <CopyButton text={request.randomNumber}>
                 <code>{truncate(request.randomNumber)}</code>
                 <code>{truncate(request.randomNumber)}</code>
               </CopyButton>
               </CopyButton>
+            ) : (
+              <StatusComponent status={request.status} />
             )
             )
           }
           }
         />
         />
@@ -59,7 +72,10 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
         />
         />
       </div>
       </div>
       {request.status === Status.CallbackError && (
       {request.status === Status.CallbackError && (
-        <CallbackFailedInfo request={request} />
+        <CallbackErrorInfo request={request} />
+      )}
+      {request.status === Status.Failed && (
+        <RevealFailedInfo request={request} />
       )}
       )}
       <Table
       <Table
         label="Details"
         label="Details"
@@ -84,15 +100,19 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
           {
           {
             id: "requestTimestamp",
             id: "requestTimestamp",
             field: "Request Timestamp",
             field: "Request Timestamp",
-            value: <Timestamp timestamp={request.requestTimestamp} />,
+            value: <Timestamp timestamp={request.requestTimestamp} now={now} />,
           },
           },
-          ...(request.status === Status.Pending
-            ? []
-            : [
+          ...("callbackTimestamp" in request
+            ? [
                 {
                 {
                   id: "callbackTimestamp",
                   id: "callbackTimestamp",
                   field: "Callback Timestamp",
                   field: "Callback Timestamp",
-                  value: <Timestamp timestamp={request.callbackTimestamp} />,
+                  value: (
+                    <Timestamp
+                      timestamp={request.callbackTimestamp}
+                      now={now}
+                    />
+                  ),
                 },
                 },
                 {
                 {
                   id: "duration",
                   id: "duration",
@@ -113,7 +133,8 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
                     />
                     />
                   ),
                   ),
                 },
                 },
-              ]),
+              ]
+            : []),
           {
           {
             id: "requestTx",
             id: "requestTx",
             field: (
             field: (
@@ -123,17 +144,19 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
               </Term>
               </Term>
             ),
             ),
             value: (
             value: (
-              <Address chain={request.chain} value={request.requestTxHash} />
+              <Transaction
+                chain={request.chain}
+                value={request.requestTxHash}
+              />
             ),
             ),
           },
           },
           {
           {
             id: "sender",
             id: "sender",
             field: "Sender",
             field: "Sender",
-            value: <Address chain={request.chain} value={request.sender} />,
+            value: <Account chain={request.chain} value={request.sender} />,
           },
           },
-          ...(request.status === Status.Pending
-            ? []
-            : [
+          ...(request.status === Status.Complete
+            ? [
                 {
                 {
                   id: "callbackTx",
                   id: "callbackTx",
                   field: (
                   field: (
@@ -143,17 +166,18 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
                     </Term>
                     </Term>
                   ),
                   ),
                   value: (
                   value: (
-                    <Address
+                    <Transaction
                       chain={request.chain}
                       chain={request.chain}
                       value={request.callbackTxHash}
                       value={request.callbackTxHash}
                     />
                     />
                   ),
                   ),
                 },
                 },
-              ]),
+              ]
+            : []),
           {
           {
             id: "provider",
             id: "provider",
             field: "Provider",
             field: "Provider",
-            value: <Address chain={request.chain} value={request.provider} />,
+            value: <Account chain={request.chain} value={request.provider} />,
           },
           },
           {
           {
             id: "userContribution",
             id: "userContribution",
@@ -163,32 +187,34 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
               </Term>
               </Term>
             ),
             ),
             value: (
             value: (
-              <CopyButton text={request.userRandomNumber}>
-                <code>{truncate(request.userRandomNumber)}</code>
-              </CopyButton>
-            ),
-          },
-          {
-            id: "providerContribution",
-            field: (
-              <Term term="Provider Contribution">
-                Provider-submitted randomness used to calculate the random
-                number.
-              </Term>
-            ),
-            value: (
-              <CopyButton text={request.userRandomNumber}>
-                <code>{truncate(request.userRandomNumber)}</code>
+              <CopyButton text={request.userContribution}>
+                <code>{truncate(request.userContribution)}</code>
               </CopyButton>
               </CopyButton>
             ),
             ),
           },
           },
+          ...("providerContribution" in request
+            ? [
+                {
+                  id: "providerContribution",
+                  field: (
+                    <Term term="Provider Contribution">
+                      Provider-submitted randomness used to calculate the random
+                      number.
+                    </Term>
+                  ),
+                  value: (
+                    <CopyButton text={request.providerContribution}>
+                      <code>{truncate(request.providerContribution)}</code>
+                    </CopyButton>
+                  ),
+                },
+              ]
+            : []),
           {
           {
             id: "gas",
             id: "gas",
             field: "Gas",
             field: "Gas",
             value:
             value:
-              request.status === Status.Pending ? (
-                `${gasFormatter.format(request.gasLimit)} max`
-              ) : (
+              "gasUsed" in request ? (
                 <Meter
                 <Meter
                   label="Gas"
                   label="Gas"
                   value={request.gasUsed}
                   value={request.gasUsed}
@@ -201,6 +227,8 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
                     request.gasUsed > request.gasLimit ? "error" : "default"
                     request.gasUsed > request.gasLimit ? "error" : "default"
                   }
                   }
                 />
                 />
+              ) : (
+                `${gasFormatter.format(request.gasLimit)} max`
               ),
               ),
           },
           },
         ].map((data) => ({
         ].map((data) => ({
@@ -215,9 +243,9 @@ const RequestDrawerBody = ({ request }: { request: Request }) => {
   );
   );
 };
 };
 
 
-const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
+const CallbackErrorInfo = ({ request }: { request: CallbackErrorRequest }) => {
   const deployment = EntropyDeployments[request.chain];
   const deployment = EntropyDeployments[request.chain];
-  const retryCommand = `cast send ${deployment.address} 'revealWithCallback(address, uint64, bytes32, bytes32)' ${request.provider} ${request.sequenceNumber.toString()} ${request.userRandomNumber} ${request.randomNumber} -r ${deployment.rpc} --private-key <YOUR_PRIVATE_KEY>`;
+  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>`;
 
 
   return (
   return (
     <>
     <>
@@ -227,7 +255,21 @@ const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
         className={styles.message}
         className={styles.message}
         variant="warning"
         variant="warning"
       >
       >
-        <CallbackFailureMessage request={request} />
+        <Button
+          hideText
+          beforeIcon={<Question />}
+          rounded
+          size="sm"
+          variant="ghost"
+          className={styles.helpButton ?? ""}
+          href={getHelpLink(request.returnValue)}
+          target="_blank"
+        >
+          Help
+        </Button>
+        <div className={styles.failureMessage}>
+          <CallbackFailureMessage reason={request.returnValue} />
+        </div>
       </InfoBox>
       </InfoBox>
       <InfoBox
       <InfoBox
         header="Retry the callback yourself"
         header="Retry the callback yourself"
@@ -247,9 +289,6 @@ const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
           }}
           }}
         >
         >
           <CopyButton text={retryCommand}>Copy Forge Command</CopyButton>
           <CopyButton text={retryCommand}>Copy Forge Command</CopyButton>
-          <Button size="sm" variant="outline">
-            Connect Wallet
-          </Button>
           <Button
           <Button
             size="sm"
             size="sm"
             variant="ghost"
             variant="ghost"
@@ -258,6 +297,7 @@ const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
             hideText
             hideText
             href="https://docs.pyth.network/entropy/debug-callback-failures"
             href="https://docs.pyth.network/entropy/debug-callback-failures"
             target="_blank"
             target="_blank"
+            className={styles.helpButton ?? ""}
           >
           >
             Help
             Help
           </Button>
           </Button>
@@ -267,27 +307,49 @@ const CallbackFailedInfo = ({ request }: { request: CallbackErrorRequest }) => {
   );
   );
 };
 };
 
 
-const CallbackFailureMessage = ({
-  request,
-}: {
-  request: CallbackErrorRequest;
-}) => {
-  if (request.returnValue === "" && request.gasUsed > request.gasLimit) {
-    return "The callback used more gas than the gas limit.";
-  } else {
-    const details = getErrorDetails(request.returnValue);
-    return details ? (
-      <>
-        <p>The callback encountered the following error:</p>
-        <p className={styles.details}>
-          <b>{details[0]}</b> (<code>{request.returnValue}</code>): {details[1]}
-        </p>
-      </>
-    ) : (
-      <>
-        The callback encountered an unknown error:{" "}
-        <code>{request.returnValue}</code>
-      </>
-    );
-  }
+const RevealFailedInfo = ({ request }: { request: FailedRequest }) => (
+  <InfoBox
+    header="Reveal failed!"
+    icon={<Warning />}
+    className={styles.message}
+    variant="warning"
+  >
+    <Button
+      hideText
+      beforeIcon={<Question />}
+      rounded
+      size="sm"
+      variant="ghost"
+      className={styles.helpButton ?? ""}
+      href={getHelpLink(request.reason)}
+      target="_blank"
+    >
+      Help
+    </Button>
+    <div className={styles.failureMessage}>
+      <CallbackFailureMessage reason={request.reason} />
+    </div>
+  </InfoBox>
+);
+
+const getHelpLink = (reason: string) => {
+  const details = getErrorDetails(reason);
+  return (
+    details?.[2] ??
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures"
+  );
+};
+
+const CallbackFailureMessage = ({ reason }: { reason: string }) => {
+  const details = getErrorDetails(reason);
+  return details ? (
+    <>
+      <p>The callback encountered the following error:</p>
+      <p className={styles.details}>
+        <b>{details[0]}</b> (<code>{reason}</code>): {details[1]}
+      </p>
+    </>
+  ) : (
+    reason
+  );
 };
 };

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

@@ -15,9 +15,6 @@
 }
 }
 
 
 .entityList {
 .entityList {
-  background: theme.color("background", "primary");
-  border-radius: theme.border-radius("xl");
-
   @include theme.breakpoint("xl") {
   @include theme.breakpoint("xl") {
     display: none;
     display: none;
   }
   }
@@ -39,3 +36,8 @@
     @include theme.text("base", "medium");
     @include theme.text("base", "medium");
   }
   }
 }
 }
+
+.noResultsChainSelect {
+  margin-top: theme.spacing(4);
+  justify-self: center;
+}

+ 94 - 128
apps/entropy-explorer/src/components/Home/results.tsx

@@ -1,154 +1,104 @@
 "use client";
 "use client";
 
 
-import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
 import { EntityList } from "@pythnetwork/component-library/EntityList";
 import { EntityList } from "@pythnetwork/component-library/EntityList";
-import { NoResults } from "@pythnetwork/component-library/NoResults";
+import { NoResults as NoResultsImpl } from "@pythnetwork/component-library/NoResults";
 import type { RowConfig } from "@pythnetwork/component-library/Table";
 import type { RowConfig } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
 import { Table } from "@pythnetwork/component-library/Table";
-import { StateType, useData } from "@pythnetwork/component-library/useData";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
 import { useDrawer } from "@pythnetwork/component-library/useDrawer";
-import { ChainIcon } from "connectkit";
+import Image from "next/image";
 import type { ComponentProps } from "react";
 import type { ComponentProps } from "react";
-import { Suspense, useMemo } from "react";
-import { useFilter } from "react-aria";
-import * as viemChains from "viem/chains";
+import { useMemo } from "react";
 
 
 import { mkRequestDrawer } from "./request-drawer";
 import { mkRequestDrawer } from "./request-drawer";
 import styles from "./results.module.scss";
 import styles from "./results.module.scss";
-import { useQuery } from "./use-query";
 import { EntropyDeployments } from "../../entropy-deployments";
 import { EntropyDeployments } from "../../entropy-deployments";
-import { Status, getRequests } from "../../requests";
-import { Address } from "../Address";
+import type { Request } from "../../requests";
+import { Status } from "../../requests";
+import { Account, Transaction } from "../Address";
 import { Status as StatusComponent } from "../Status";
 import { Status as StatusComponent } from "../Status";
 import { Timestamp } from "../Timestamp";
 import { Timestamp } from "../Timestamp";
+import { ChainSelect } from "./search-controls";
 
 
-export const Results = () => (
-  <Suspense fallback={<ResultsImpl isLoading />}>
-    <MountedResults />
-  </Suspense>
-);
-
-const MountedResults = () => {
-  const results = useData(["requests"], getRequests, {
-    refreshInterval: 0,
-    revalidateIfStale: false,
-    revalidateOnFocus: false,
-    revalidateOnReconnect: false,
-  });
-  switch (results.type) {
-    case StateType.Error: {
-      return (
-        <Empty
-          icon={<Warning />}
-          header="Uh oh, we hit an error"
-          body={results.error.message}
-          variant="error"
-        />
-      );
-    }
-    case StateType.NotLoaded:
-    case StateType.Loading: {
-      return <ResultsImpl isLoading />;
-    }
-    case StateType.Loaded: {
-      return (
-        <ResolvedResults
-          data={results.data}
-          isUpdating={results.isValidating}
-        />
-      );
-    }
-  }
-};
-
-type ResolvedResultsProps = {
-  data: Awaited<ReturnType<typeof getRequests>>;
+type Props = {
+  currentPage: Request[];
+  search?: string | undefined;
   isUpdating?: boolean | undefined;
   isUpdating?: boolean | undefined;
+  now: Date;
+  chain?: keyof typeof EntropyDeployments | undefined;
 };
 };
 
 
-const ResolvedResults = ({ data, isUpdating }: ResolvedResultsProps) => {
+export const Results = ({
+  currentPage,
+  isUpdating,
+  search,
+  now,
+  chain,
+}: Props) => {
   const drawer = useDrawer();
   const drawer = useDrawer();
-  const { search, chain, status } = useQuery();
-  const filter = useFilter({ sensitivity: "base", usage: "search" });
   const rows = useMemo(
   const rows = useMemo(
     () =>
     () =>
-      data
-        .filter(
-          (request) =>
-            (status === null || status === request.status) &&
-            (chain === null || chain === request.chain) &&
-            (filter.contains(request.requestTxHash, search) ||
-              (request.status !== Status.Pending &&
-                filter.contains(request.callbackTxHash, search)) ||
-              filter.contains(request.sender, search) ||
-              filter.contains(request.sequenceNumber.toString(), search)),
-        )
-        .map((request) => ({
-          id: request.sequenceNumber.toString(),
-          textValue: request.requestTxHash,
-          onAction: () => {
-            drawer.open(mkRequestDrawer(request));
-          },
-          data: {
-            chain: <Chain chain={request.chain} />,
-            timestamp: (
-              <div className={styles.timestamp}>
-                <Timestamp timestamp={request.requestTimestamp} />
-              </div>
-            ),
-            sequenceNumber: (
-              <div className={styles.sequenceNumber}>
-                {request.sequenceNumber}
-              </div>
-            ),
-            sender: (
-              <Address
-                alwaysTruncate
-                chain={request.chain}
-                value={request.sender}
-              />
-            ),
-            requestTxHash: (
-              <Address
-                alwaysTruncate
-                chain={request.chain}
-                value={request.requestTxHash}
-              />
-            ),
-            callbackTxHash: request.status !== Status.Pending && (
-              <Address
-                alwaysTruncate
-                chain={request.chain}
-                value={request.callbackTxHash}
-              />
-            ),
-            status: <StatusComponent status={request.status} />,
-          },
-        })),
-    [data, search, drawer, filter, chain, status],
+      currentPage.map((request) => ({
+        id: request.sequenceNumber.toString(),
+        textValue: request.requestTxHash,
+        onAction: () => {
+          drawer.open(mkRequestDrawer(request, now));
+        },
+        data: {
+          chain: <Chain chain={request.chain} />,
+          timestamp: (
+            <div className={styles.timestamp}>
+              <Timestamp timestamp={request.requestTimestamp} now={now} />
+            </div>
+          ),
+          sequenceNumber: (
+            <div className={styles.sequenceNumber}>
+              {request.sequenceNumber}
+            </div>
+          ),
+          sender: <Account chain={request.chain} value={request.sender} />,
+          requestTxHash: (
+            <Transaction chain={request.chain} value={request.requestTxHash} />
+          ),
+          callbackTxHash: request.status === Status.Complete && (
+            <Transaction chain={request.chain} value={request.callbackTxHash} />
+          ),
+          status: <StatusComponent status={request.status} size="xs" />,
+        },
+      })),
+    [currentPage, drawer, now],
   );
   );
 
 
-  return <ResultsImpl rows={rows} isUpdating={isUpdating} search={search} />;
+  return (
+    <ResultsImpl
+      rows={rows}
+      search={search}
+      chain={chain}
+      isUpdating={isUpdating}
+    />
+  );
 };
 };
 
 
+export const ResultsLoading = () => <ResultsImpl isLoading />;
+
 type ResultsImplProps =
 type ResultsImplProps =
   | {
   | {
       isLoading: true;
       isLoading: true;
     }
     }
   | {
   | {
       isLoading?: false | undefined;
       isLoading?: false | undefined;
+      chain?: keyof typeof EntropyDeployments | undefined;
       rows: (RowConfig<(typeof defaultProps)["columns"][number]["id"]> & {
       rows: (RowConfig<(typeof defaultProps)["columns"][number]["id"]> & {
         textValue: string;
         textValue: string;
       })[];
       })[];
       isUpdating?: boolean | undefined;
       isUpdating?: boolean | undefined;
-      search: string;
+      search?: string | undefined;
     };
     };
 
 
 const ResultsImpl = (props: ResultsImplProps) => (
 const ResultsImpl = (props: ResultsImplProps) => (
   <>
   <>
     <div className={styles.entityList}>
     <div className={styles.entityList}>
       {!props.isLoading && props.rows.length === 0 ? (
       {!props.isLoading && props.rows.length === 0 ? (
-        <NoResults query={props.search} />
+        <NoResults search={props.search} chain={props.chain} />
       ) : (
       ) : (
         <EntityList
         <EntityList
           label={defaultProps.label}
           label={defaultProps.label}
@@ -173,32 +123,48 @@ const ResultsImpl = (props: ResultsImplProps) => (
         : {
         : {
             rows: props.rows,
             rows: props.rows,
             isUpdating: props.isUpdating,
             isUpdating: props.isUpdating,
-            emptyState: <NoResults query={props.search} />,
+            emptyState: <NoResults search={props.search} chain={props.chain} />,
             className: styles.table ?? "",
             className: styles.table ?? "",
           })}
           })}
     />
     />
   </>
   </>
 );
 );
 
 
-const Empty = (props: ComponentProps<typeof NoResults>) => (
-  <>
-    <NoResults className={styles.entityList} {...props} />
-    <Table
-      className={styles.table ?? ""}
-      rows={[]}
-      emptyState={<NoResults {...props} />}
-      {...defaultProps}
+type NoResultsProps = {
+  search?: string | undefined;
+  chain?: keyof typeof EntropyDeployments | undefined;
+};
+
+const NoResults = ({ search, chain }: NoResultsProps) => {
+  return (
+    <NoResultsImpl
+      query={search ?? ""}
+      body={
+        <>
+          <p>
+            We couldn{"'"}t find any results for your query on{" "}
+            {chain ? EntropyDeployments[chain].name : "any chain"}.
+          </p>
+          <p>Would you like to try your search on a different chain?</p>
+          <ChainSelect
+            className={styles.noResultsChainSelect ?? ""}
+            label="Chain"
+            hideLabel
+            variant="outline"
+            size="sm"
+          />
+        </>
+      }
     />
     />
-  </>
-);
+  );
+};
 
 
 const Chain = ({ chain }: { chain: keyof typeof EntropyDeployments }) => {
 const Chain = ({ chain }: { chain: keyof typeof EntropyDeployments }) => {
-  // eslint-disable-next-line import/namespace
-  const viemChain = viemChains[chain];
+  const chainInfo = EntropyDeployments[chain];
   return (
   return (
     <div className={styles.chain}>
     <div className={styles.chain}>
-      <ChainIcon id={viemChain.id} />
-      {viemChain.name}
+      <Image alt="" src={chainInfo.icon} width={20} height={20} />
+      {chainInfo.name}
     </div>
     </div>
   );
   );
 };
 };
@@ -241,9 +207,9 @@ const defaultProps = {
     },
     },
     {
     {
       id: "status" as const,
       id: "status" as const,
-      name: "CALLBACK STATUS",
+      name: "STATUS",
       alignment: "center",
       alignment: "center",
-      width: 25,
+      width: 32,
     },
     },
   ],
   ],
 } satisfies Partial<ComponentProps<typeof Table<string>>>;
 } satisfies Partial<ComponentProps<typeof Table<string>>>;

+ 0 - 37
apps/entropy-explorer/src/components/Home/search-bar.tsx

@@ -1,37 +0,0 @@
-"use client";
-
-import { SearchInput } from "@pythnetwork/component-library/SearchInput";
-import type { ComponentProps } from "react";
-import { Suspense } from "react";
-
-import { useQuery } from "./use-query";
-import type { ConstrainedOmit } from "../../type-utils";
-
-export const SearchBar = (props: ComponentProps<typeof ResolvedSearchBar>) => (
-  <Suspense fallback={<SearchInput isPending {...defaultProps} {...props} />}>
-    <ResolvedSearchBar {...props} />
-  </Suspense>
-);
-
-const ResolvedSearchBar = (
-  props: ConstrainedOmit<
-    ComponentProps<typeof SearchInput>,
-    keyof typeof defaultProps | "value" | "onChange"
-  >,
-) => {
-  const { search, setSearch } = useQuery();
-
-  return (
-    <SearchInput
-      {...defaultProps}
-      {...props}
-      value={search}
-      onChange={setSearch}
-    />
-  );
-};
-
-const defaultProps = {
-  size: "sm",
-  placeholder: "Sequence number, provider, sender or tx hash",
-} as const;

+ 0 - 7
apps/entropy-explorer/src/components/Home/chain-select.module.scss → apps/entropy-explorer/src/components/Home/search-controls.module.scss

@@ -1,12 +1,5 @@
 @use "@pythnetwork/component-library/theme";
 @use "@pythnetwork/component-library/theme";
 
 
-.searchBar {
-  display: grid;
-  grid-template-columns: max-content 1fr;
-  gap: theme.spacing(2);
-  width: 100%;
-}
-
 .chainSelectItem {
 .chainSelectItem {
   display: grid;
   display: grid;
   grid-template-columns: max-content 1fr;
   grid-template-columns: max-content 1fr;

+ 344 - 0
apps/entropy-explorer/src/components/Home/search-controls.tsx

@@ -0,0 +1,344 @@
+"use client";
+
+import { Paginator as PaginatorImpl } from "@pythnetwork/component-library/Paginator";
+import { SearchInput } from "@pythnetwork/component-library/SearchInput";
+import type { Props as SelectProps } from "@pythnetwork/component-library/Select";
+import { Select } from "@pythnetwork/component-library/Select";
+import Image from "next/image";
+import { usePathname, useSearchParams, useRouter } from "next/navigation";
+import type { ComponentProps } from "react";
+import { Suspense, useCallback, useMemo, useTransition } from "react";
+import { useCollator } from "react-aria";
+
+import {
+  EntropyDeployments,
+  isValidDeployment,
+} 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";
+
+export const SearchBar = (props: ComponentProps<typeof ResolvedSearchBar>) => (
+  <Suspense fallback={<SearchInput isPending {...props} />}>
+    <ResolvedSearchBar {...props} />
+  </Suspense>
+);
+
+const ResolvedSearchBar = (
+  props: ConstrainedOmit<
+    ComponentProps<typeof SearchInput>,
+    keyof ReturnType<typeof useSearchBar>
+  >,
+) => <SearchInput {...useSearchBar()} {...props} />;
+
+const useSearchBar = () => {
+  const search = useSearchParam({
+    paramName: "search",
+    parse: id,
+    defaultValue: "",
+    serialize: id,
+  });
+
+  return {
+    onChange: search.onChange,
+    defaultValue: search.value,
+    isPending: search.isTransitioning,
+  };
+};
+
+export const Paginator = (props: ComponentProps<typeof ResolvedPaginator>) => (
+  <Suspense fallback={<Paginator {...props} />}>
+    <ResolvedPaginator {...props} />
+  </Suspense>
+);
+
+const ResolvedPaginator = (
+  props: ConstrainedOmit<
+    ComponentProps<typeof PaginatorImpl>,
+    keyof ReturnType<typeof usePaginator>
+  >,
+) => <PaginatorImpl {...usePaginator()} {...props} />;
+
+const usePaginator = () => {
+  const pageSize = useSearchParam({
+    paramName: "pageSize",
+    parse: parseInt,
+    defaultValue: DEFAULT_PAGE_SIZE,
+    serialize: toString,
+  });
+  const page = useSearchParam({
+    paramName: "page",
+    parse: parseInt,
+    defaultValue: 1,
+    preservePageOnChange: true,
+    serialize: toString,
+  });
+
+  return {
+    currentPage: page.value,
+    onPageChange: page.onChange,
+    isPageTransitioning: page.isTransitioning,
+    onPageSizeChange: pageSize.onChange,
+    isPageSizeTransitioning: pageSize.isTransitioning,
+    pageSize: pageSize.value,
+    pageSizeOptions: PAGE_SIZES as unknown as number[],
+  };
+};
+
+export const StatusSelect = (
+  props: ComponentProps<typeof ResolvedStatusSelect>,
+) => (
+  <Suspense fallback={<Select isPending options={[]} {...props} />}>
+    <ResolvedStatusSelect {...props} />
+  </Suspense>
+);
+
+const ResolvedStatusSelect = (
+  props: ConstrainedOmit<
+    SelectProps<{ id: Status | "all" }>,
+    keyof ReturnType<typeof useStatusSelect>
+  >,
+) => <Select {...useStatusSelect()} {...props} />;
+
+const useStatusSelect = () => {
+  const status = useSearchParam({
+    paramName: "status",
+    parse: parseStatus,
+    defaultValue: "all",
+    serialize: serializeStatus,
+  });
+
+  return {
+    selectedKey: status.value,
+    onSelectionChange: status.onChange,
+    isPending: status.isTransitioning,
+    optionGroups: useMemo(
+      () => [
+        {
+          name: "All",
+          options: [{ id: "all" as const }],
+        },
+        {
+          name: "Statuses",
+          options: [
+            { id: Status.Complete },
+            { id: Status.Pending },
+            { id: Status.Failed },
+            { id: Status.CallbackError },
+          ],
+        },
+      ],
+      [],
+    ),
+    show: useCallback(
+      (status: { id: Status | "all" }) =>
+        status.id === "all" ? (
+          "All"
+        ) : (
+          <StatusComponent size="xs" status={status.id} />
+        ),
+      [],
+    ),
+    buttonLabel:
+      status.value === "all" ? (
+        "Status"
+      ) : (
+        <StatusComponent size="xs" status={status.value} />
+      ),
+  };
+};
+
+const parseStatus = (value: string) => {
+  switch (value) {
+    case StatusParams[Status.Pending]: {
+      return Status.Pending;
+    }
+    case StatusParams[Status.CallbackError]: {
+      return Status.CallbackError;
+    }
+    case StatusParams[Status.Complete]: {
+      return Status.Complete;
+    }
+    case StatusParams[Status.Failed]: {
+      return Status.Failed;
+    }
+    default: {
+      return "all";
+    }
+  }
+};
+
+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>,
+) => (
+  <Suspense fallback={<Select isPending options={[]} {...props} />}>
+    <ResolvedChainSelect {...props} />
+  </Suspense>
+);
+
+const ResolvedChainSelect = (
+  props: ConstrainedOmit<
+    SelectProps<Deployment>,
+    keyof ReturnType<typeof useChainSelect>
+  >,
+) => <Select {...useChainSelect()} {...props} />;
+
+const useChainSelect = () => {
+  const chain = useSearchParam({
+    paramName: "chain",
+    parse: parseChain,
+    defaultValue: "all",
+    serialize: toString,
+  });
+  const collator = useCollator();
+
+  return {
+    selectedKey: chain.value,
+    onSelectionChange: chain.onChange,
+    isPending: chain.isTransitioning,
+    buttonLabel:
+      chain.value === "all"
+        ? "All Chains"
+        : EntropyDeployments[chain.value].name,
+    optionGroups: useMemo(
+      () => [
+        {
+          name: "ALL",
+          options: [{ id: "all" as const }],
+          hideLabel: true,
+        },
+        {
+          name: "MAINNET",
+          options: entropyDeploymentsByNetwork(collator, false),
+        },
+        {
+          name: "TESTNET",
+          options: entropyDeploymentsByNetwork(collator, true),
+        },
+      ],
+      [collator],
+    ),
+    show: useCallback(
+      (chain: Deployment) =>
+        chain.id === "all" ? (
+          "All Chains"
+        ) : (
+          <div className={styles.chainSelectItem}>
+            <Image alt={chain.name} src={chain.icon} width={20} height={20} />
+            {chain.name}
+          </div>
+        ),
+      [],
+    ),
+    textValue: useCallback(
+      (chain: Deployment) => (chain.id === "all" ? "All" : chain.name),
+      [],
+    ),
+    ...(chain.value !== "all" && {
+      icon: (
+        <Image
+          alt={EntropyDeployments[chain.value].name}
+          src={EntropyDeployments[chain.value].icon}
+          width={20}
+          height={20}
+        />
+      ),
+    }),
+  };
+};
+
+const entropyDeploymentsByNetwork = (
+  collator: ReturnType<typeof useCollator>,
+  isTestnet: boolean,
+) =>
+  Object.entries(EntropyDeployments)
+    .map(([slug, chain]) => {
+      return {
+        ...chain,
+        id: Number.parseInt(slug, 10) as keyof typeof EntropyDeployments,
+      };
+    })
+    .filter((chain) => chain.isTestnet === isTestnet)
+    .toSorted((a, b) => collator.compare(a.name, b.name));
+
+const id = <T,>(value: T) => value;
+const parseInt = (value: string) => Number.parseInt(value, 10);
+// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+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,
+  defaultValue,
+  preservePageOnChange,
+  serialize,
+}: {
+  paramName: string;
+  parse: (value: string) => T;
+  defaultValue: T;
+  preservePageOnChange?: boolean | undefined;
+  serialize: (value: T) => string;
+}) => {
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const pathname = usePathname();
+  const [isTransitioning, startTransition] = useTransition();
+
+  const value = useMemo(() => {
+    const paramValue = searchParams.get(paramName);
+    return paramValue ? parse(paramValue) : defaultValue;
+  }, [searchParams, paramName, parse, defaultValue]);
+
+  return {
+    value,
+    isTransitioning,
+    onChange: useCallback(
+      (newValue: T) => {
+        if (newValue !== value) {
+          startTransition(() => {
+            const params = new URLSearchParams(searchParams);
+            if (newValue === defaultValue) {
+              params.delete(paramName);
+            } else {
+              params.set(paramName, serialize(newValue));
+              if (!preservePageOnChange) {
+                params.delete("page");
+              }
+            }
+            router.replace(`${pathname}?${params.toString()}`);
+          });
+        }
+      },
+      [
+        searchParams,
+        pathname,
+        router,
+        value,
+        startTransition,
+        defaultValue,
+        paramName,
+        preservePageOnChange,
+        serialize,
+      ],
+    ),
+  };
+};

+ 0 - 95
apps/entropy-explorer/src/components/Home/status-select.tsx

@@ -1,95 +0,0 @@
-"use client";
-
-import type { Props as SelectProps } from "@pythnetwork/component-library/Select";
-import { Select } from "@pythnetwork/component-library/Select";
-import type { ComponentProps } from "react";
-import { Suspense, useCallback, useMemo } from "react";
-
-import { useQuery } from "./use-query";
-import { Status } from "../../requests";
-import type { ConstrainedOmit } from "../../type-utils";
-import { Status as StatusComponent } from "../Status";
-
-export const StatusSelect = (
-  props: ComponentProps<typeof ResolvedStatusSelect>,
-) => (
-  <Suspense
-    fallback={
-      <Select
-        {...defaultProps}
-        {...props}
-        isPending
-        options={[]}
-        defaultSelectedKey={undefined}
-      />
-    }
-  >
-    <ResolvedStatusSelect {...props} />
-  </Suspense>
-);
-
-const ResolvedStatusSelect = (
-  props: ConstrainedOmit<
-    SelectProps<
-      ReturnType<
-        typeof useResolvedProps
-      >["optionGroups"][number]["options"][number]
-    >,
-    keyof typeof defaultProps | keyof ReturnType<typeof useResolvedProps>
-  >,
-) => {
-  const resolvedProps = useResolvedProps();
-
-  return <Select {...defaultProps} {...resolvedProps} {...props} />;
-};
-
-const useResolvedProps = () => {
-  const { status, setStatus } = useQuery();
-  const chains = useMemo(
-    () => [
-      {
-        name: "All",
-        options: [{ id: "all" as const }],
-      },
-      {
-        name: "Statuses",
-        options: [
-          { id: Status.Complete },
-          { id: Status.Pending },
-          { id: Status.CallbackError },
-        ],
-      },
-    ],
-    [],
-  );
-
-  const showStatus = useCallback(
-    (status: (typeof chains)[number]["options"][number]) =>
-      status.id === "all" ? (
-        "All"
-      ) : (
-        <StatusComponent size="xs" status={status.id} />
-      ),
-    [],
-  );
-
-  return {
-    selectedKey: status ?? ("all" as const),
-    onSelectionChange: setStatus,
-    optionGroups: chains,
-    show: showStatus,
-    buttonLabel:
-      status === null ? (
-        "Status"
-      ) : (
-        <StatusComponent size="xs" status={status} />
-      ),
-  };
-};
-
-const defaultProps = {
-  label: "Status",
-  hideLabel: true,
-  defaultButtonLabel: "Status",
-  hideGroupLabel: true,
-} as const;

+ 0 - 87
apps/entropy-explorer/src/components/Home/use-query.ts

@@ -1,87 +0,0 @@
-import { useLogger } from "@pythnetwork/component-library/useLogger";
-import { useQueryStates, parseAsString, parseAsStringEnum } from "nuqs";
-import { useCallback, useMemo } from "react";
-
-import { EntropyDeployments } from "../../entropy-deployments";
-import { Status } from "../../requests";
-
-const StatusParams = {
-  [Status.Pending]: "pending",
-  [Status.Complete]: "complete",
-  [Status.CallbackError]: "callback-error",
-} as const;
-
-const queryParams = {
-  status: parseAsStringEnum<(typeof StatusParams)[Status]>(
-    Object.values(StatusParams),
-  ),
-  search: parseAsString.withDefault(""),
-  chain: parseAsStringEnum<keyof typeof EntropyDeployments>(
-    Object.keys(EntropyDeployments) as (keyof typeof EntropyDeployments)[],
-  ),
-};
-
-export const useQuery = () => {
-  const logger = useLogger();
-  const [{ search, chain, status }, setQuery] = useQueryStates(queryParams);
-
-  const updateQuery = useCallback(
-    (newQuery: Parameters<typeof setQuery>[0]) => {
-      setQuery(newQuery).catch((error: unknown) => {
-        logger.error("Failed to update query", error);
-      });
-    },
-    [setQuery, logger],
-  );
-
-  const setSearch = useCallback(
-    (newSearch: string) => {
-      updateQuery({ search: newSearch });
-    },
-    [updateQuery],
-  );
-
-  const setChain = useCallback(
-    (newChain: keyof typeof EntropyDeployments | "all") => {
-      // eslint-disable-next-line unicorn/no-null
-      updateQuery({ chain: newChain === "all" ? null : newChain });
-    },
-    [updateQuery],
-  );
-
-  const setStatus = useCallback(
-    (newStatus: Status | "all") => {
-      updateQuery({
-        // eslint-disable-next-line unicorn/no-null
-        status: newStatus === "all" ? null : StatusParams[newStatus],
-      });
-    },
-    [updateQuery],
-  );
-
-  return {
-    search,
-    chain,
-    status: useMemo(() => {
-      switch (status) {
-        case "pending": {
-          return Status.Pending;
-        }
-        case "callback-error": {
-          return Status.CallbackError;
-        }
-        case "complete": {
-          return Status.Complete;
-        }
-        // eslint-disable-next-line unicorn/no-null
-        case null: {
-          // eslint-disable-next-line unicorn/no-null
-          return null;
-        }
-      }
-    }, [status]),
-    setSearch,
-    setChain,
-    setStatus,
-  };
-};

+ 0 - 21
apps/entropy-explorer/src/components/Root/evm-provider.tsx

@@ -1,21 +0,0 @@
-"use client";
-
-import type { ReactNode } from "react";
-import { mainnet } from "viem/chains";
-import { WagmiProvider, createConfig, http } from "wagmi";
-
-// We only use wagmi because we use connectkit to get chain icons, and
-// connectkit blows up if there isn't a wagmi context initialized.  However, the
-// wagmi config isn't actually used when fetching chain icons.  But wagmi
-// requires at least one chain to create a config, so we'll just inject mainnet
-// here to make everyone happy.
-export const EvmProvider = ({ children }: { children: ReactNode }) => (
-  <WagmiProvider
-    config={createConfig({
-      chains: [mainnet],
-      transports: { [mainnet.id]: http() },
-    })}
-  >
-    {children}
-  </WagmiProvider>
-);

+ 1 - 2
apps/entropy-explorer/src/components/Root/index.tsx

@@ -2,7 +2,6 @@ import { AppShell } from "@pythnetwork/component-library/AppShell";
 import { NuqsAdapter } from "nuqs/adapters/next/app";
 import { NuqsAdapter } from "nuqs/adapters/next/app";
 import type { ReactNode } from "react";
 import type { ReactNode } from "react";
 
 
-import { EvmProvider } from "./evm-provider";
 import {
 import {
   ENABLE_ACCESSIBILITY_REPORTING,
   ENABLE_ACCESSIBILITY_REPORTING,
   GOOGLE_ANALYTICS_ID,
   GOOGLE_ANALYTICS_ID,
@@ -23,7 +22,7 @@ export const Root = ({ children }: Props) => (
       label: "Entropy Docs",
       label: "Entropy Docs",
       href: "https://docs.pyth.network/entropy",
       href: "https://docs.pyth.network/entropy",
     }}
     }}
-    providers={[EvmProvider, NuqsAdapter]}
+    providers={[NuqsAdapter]}
   >
   >
     {children}
     {children}
   </AppShell>
   </AppShell>

+ 12 - 6
apps/entropy-explorer/src/components/Status/index.tsx

@@ -5,29 +5,35 @@ import { Status as StatusType } from "../../requests";
 
 
 type Props = Omit<ComponentProps<typeof StatusImpl>, "variant" | "style"> & {
 type Props = Omit<ComponentProps<typeof StatusImpl>, "variant" | "style"> & {
   status: StatusType;
   status: StatusType;
-  prefix?: string | undefined;
 };
 };
 
 
-export const Status = ({ status, prefix, ...props }: Props) => {
+export const Status = ({ status, ...props }: Props) => {
   switch (status) {
   switch (status) {
     case StatusType.Complete: {
     case StatusType.Complete: {
       return (
       return (
         <StatusImpl variant="success" {...props}>
         <StatusImpl variant="success" {...props}>
-          {prefix}COMPLETE
+          COMPLETE
         </StatusImpl>
         </StatusImpl>
       );
       );
     }
     }
-    case StatusType.CallbackError: {
+    case StatusType.Failed: {
       return (
       return (
         <StatusImpl variant="error" {...props}>
         <StatusImpl variant="error" {...props}>
-          {prefix}ERROR
+          REVEAL ERROR
+        </StatusImpl>
+      );
+    }
+    case StatusType.CallbackError: {
+      return (
+        <StatusImpl variant="warning" {...props}>
+          CALLBACK FAILED
         </StatusImpl>
         </StatusImpl>
       );
       );
     }
     }
     case StatusType.Pending: {
     case StatusType.Pending: {
       return (
       return (
         <StatusImpl variant="disabled" style="outline" {...props}>
         <StatusImpl variant="disabled" style="outline" {...props}>
-          {prefix}PENDING
+          PENDING
         </StatusImpl>
         </StatusImpl>
       );
       );
     }
     }

+ 14 - 2
apps/entropy-explorer/src/components/Timestamp/index.tsx

@@ -1,11 +1,19 @@
 import { Clock } from "@phosphor-icons/react/dist/ssr/Clock";
 import { Clock } from "@phosphor-icons/react/dist/ssr/Clock";
 import { Button } from "@pythnetwork/component-library/unstyled/Button";
 import { Button } from "@pythnetwork/component-library/unstyled/Button";
 import { useState } from "react";
 import { useState } from "react";
+import { useIsSSR } from "react-aria";
 import TimeAgo from "react-timeago";
 import TimeAgo from "react-timeago";
 
 
 import styles from "./index.module.scss";
 import styles from "./index.module.scss";
 
 
-export const Timestamp = ({ timestamp }: { timestamp: Date }) => {
+export const Timestamp = ({
+  timestamp,
+  now,
+}: {
+  timestamp: Date;
+  now: Date;
+}) => {
+  const isSSR = useIsSSR();
   const [showRelative, setShowRelative] = useState(true);
   const [showRelative, setShowRelative] = useState(true);
   const month = timestamp.toLocaleString("default", {
   const month = timestamp.toLocaleString("default", {
     month: "long",
     month: "long",
@@ -16,6 +24,7 @@ export const Timestamp = ({ timestamp }: { timestamp: Date }) => {
   const hour = timestamp.getUTCHours().toString().padStart(2, "0");
   const hour = timestamp.getUTCHours().toString().padStart(2, "0");
   const minute = timestamp.getUTCMinutes().toString().padStart(2, "0");
   const minute = timestamp.getUTCMinutes().toString().padStart(2, "0");
   const seconds = timestamp.getUTCSeconds().toString().padStart(2, "0");
   const seconds = timestamp.getUTCSeconds().toString().padStart(2, "0");
+
   return (
   return (
     <Button
     <Button
       onPress={() => {
       onPress={() => {
@@ -26,7 +35,10 @@ export const Timestamp = ({ timestamp }: { timestamp: Date }) => {
     >
     >
       <Clock className={styles.clock} />
       <Clock className={styles.clock} />
       <span className={styles.relative}>
       <span className={styles.relative}>
-        <TimeAgo date={timestamp} />
+        <TimeAgo
+          date={timestamp}
+          {...(isSSR && { now: () => now.getTime() })}
+        />
       </span>
       </span>
       <span className={styles.absolute}>
       <span className={styles.absolute}>
         {month}-{day}-{year} {hour}:{minute}:{seconds} +UTC
         {month}-{day}-{year} {hour}:{minute}:{seconds} +UTC

+ 0 - 471
apps/entropy-explorer/src/entropy-deployments.ts

@@ -1,471 +0,0 @@
-import type * as viemChains from "viem/chains";
-
-export type EntropyDeployment = {
-  address: string;
-  network: "mainnet" | "testnet";
-  explorer: string;
-  delay: string;
-  gasLimit: string;
-  rpc?: string;
-  nativeCurrency: string;
-};
-
-export const EntropyDeployments = {
-  berachain: {
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    network: "mainnet",
-    explorer: "https://berascan.com/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "2.5M",
-    rpc: "https://rpc.berachain.com",
-    nativeCurrency: "BERA",
-  },
-  blast: {
-    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-    network: "mainnet",
-    explorer: "https://blastscan.io/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    rpc: "https://rpc.blast.io",
-    nativeCurrency: "ETH",
-  },
-  lightlinkPhoenix: {
-    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-    network: "mainnet",
-    explorer: "https://phoenix.lightlink.io/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    rpc: "https://replicator.phoenix.lightlink.io/rpc/v1",
-    nativeCurrency: "ETH",
-  },
-  chiliz: {
-    address: "0x0708325268dF9F66270F1401206434524814508b",
-    network: "mainnet",
-    explorer: "https://scan.chiliz.com/address/$ADDRESS",
-    delay: "12 blocks",
-    gasLimit: "500K",
-    rpc: "https://rpc.ankr.com/chiliz",
-    nativeCurrency: "CHZ",
-  },
-  arbitrum: {
-    address: "0x7698E925FfC29655576D0b361D75Af579e20AdAc",
-    network: "mainnet",
-    explorer: "https://arbiscan.io/address/$ADDRESS",
-    delay: "6 blocks",
-    gasLimit: "2.5M",
-    rpc: "https://arb1.arbitrum.io/rpc",
-    nativeCurrency: "ETH",
-  },
-  optimism: {
-    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
-    network: "mainnet",
-    explorer: "https://optimistic.etherscan.io/address/$ADDRESS",
-    delay: "2 blocks",
-    gasLimit: "500K",
-    rpc: "https://optimism.llamarpc.com",
-    nativeCurrency: "ETH",
-  },
-  mode: {
-    address: "0x8D254a21b3C86D32F7179855531CE99164721933",
-    network: "mainnet",
-    explorer: "https://explorer.mode.network/address/$ADDRESS",
-    delay: "2 blocks",
-    gasLimit: "500K",
-    rpc: "https://mainnet.mode.network/",
-    nativeCurrency: "ETH",
-  },
-  zetachain: {
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    network: "mainnet",
-    explorer: "https://zetachain.blockscout.com/address/$ADDRESS",
-    delay: "0 block",
-    gasLimit: "500K",
-    rpc: "https://zetachain-evm.blockpi.network/v1/rpc/public",
-    nativeCurrency: "ZETA",
-  },
-  base: {
-    address: "0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb",
-    network: "mainnet",
-    explorer: "https://basescan.org/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    rpc: "https://developer-access-mainnet.base.org/",
-    nativeCurrency: "ETH",
-  },
-  lightlinkPegasus: {
-    rpc: "https://replicator.pegasus.lightlink.io/rpc/v1",
-    network: "testnet",
-    delay: "",
-    address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a",
-    explorer: "https://pegasus.lightlink.io/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  spicy: {
-    rpc: "https://spicy-rpc.chiliz.com",
-    network: "testnet",
-    delay: "",
-    address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134",
-    explorer: "https://spicy-explorer.chiliz.com/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "CHZ",
-  },
-  confluxESpaceTestnet: {
-    rpc: "https://evmtestnet.confluxrpc.com",
-    network: "testnet",
-    delay: "",
-    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
-    explorer: "https://evmtestnet.confluxscan.org/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "CFX",
-  },
-  modeTestnet: {
-    rpc: "https://sepolia.mode.network/",
-    network: "testnet",
-    delay: "",
-    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-    explorer: "https://sepolia.explorer.mode.network/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  seiTestnet: {
-    rpc: "https://evm-rpc-testnet.sei-apis.com",
-    network: "testnet",
-    delay: "",
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    explorer: "https://seitrace.com/address/$ADDRESS?chain=atlantic-2",
-    gasLimit: "500K",
-    nativeCurrency: "SEI",
-  },
-  arbitrumSepolia: {
-    rpc: "https://sepolia-rollup.arbitrum.io/rpc",
-    network: "testnet",
-    delay: "",
-    address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440",
-    explorer: "https://sepolia.arbiscan.io/address/$ADDRESS",
-    gasLimit: "2.5M",
-    nativeCurrency: "ETH",
-  },
-  blastSepolia: {
-    rpc: "https://sepolia.blast.io",
-    network: "testnet",
-    delay: "",
-    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-    explorer: "https://testnet.blastscan.io/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  optimismSepolia: {
-    rpc: "https://api.zan.top/opt-sepolia",
-    network: "testnet",
-    delay: "",
-    address: "0x4821932D0CDd71225A6d914706A621e0389D7061",
-    explorer: "https://optimism-sepolia.blockscout.com/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  baseSepolia: {
-    rpc: "https://sepolia.base.org",
-    network: "testnet",
-    delay: "",
-    address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
-    explorer: "https://base-sepolia.blockscout.com/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  berachainTestnetbArtio: {
-    rpc: "https://evm-rpc-bera.rhino-apis.com/",
-    network: "testnet",
-    delay: "",
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    explorer: "https://bartio.beratrail.io/address/$ADDRESS",
-    gasLimit: "2.5M",
-    nativeCurrency: "BERA",
-  },
-  // coreTestnet1: {
-  //   rpc: "https://rpc.test.btcs.network",
-  //   network: "testnet",
-  //   delay: "",
-  //   address: "0xf0a1b566B55e0A0CB5BeF52Eb2a57142617Bee67",
-  //   explorer: "https://scan.test.btcs.network/address/$ADDRESS",
-  //   gasLimit: "500K",
-  //   nativeCurrency: "tCORE",
-  // },
-  zetachainAthensTestnet: {
-    rpc: "https://zetachain-athens-evm.blockpi.network/v1/rpc/public",
-    network: "testnet",
-    delay: "",
-    address: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
-    explorer: "https://explorer.zetachain.com/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ZETA",
-  },
-  taikoHekla: {
-    rpc: "https://rpc.hekla.taiko.xyz/",
-    network: "testnet",
-    delay: "",
-    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-    explorer: "https://hekla.taikoscan.network/address/$ADDRESS",
-    gasLimit: "500K",
-    nativeCurrency: "ETH",
-  },
-  // orangeTestnet: {
-  //   address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-  //   explorer: "https://subnets-test.avax.network/orangetest/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://subnets.avax.network/orangetest/testnet/rpc",
-  //   nativeCurrency: "JUICE",
-  // },
-  sei: {
-    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-    explorer: "https://seitrace.com/address/$ADDRESS?chain=pacific-1",
-    delay: "1 block",
-    gasLimit: "500K",
-    network: "mainnet",
-    rpc: "https://evm-rpc.sei-apis.com",
-    nativeCurrency: "SEI",
-  },
-  merlin: {
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    explorer: "https://scan.merlinchain.io/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    network: "mainnet",
-    rpc: "https://rpc.merlinchain.io",
-    nativeCurrency: "BTC",
-  },
-  // merlinTestnet: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://testnet-scan.merlinchain.io/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://testnet-rpc.merlinchain.io/",
-  //   nativeCurrency: "BTC",
-  // },
-  taiko: {
-    address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
-    explorer: "https://taikoscan.io/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    network: "mainnet",
-    rpc: "https://rpc.mainnet.taiko.xyz",
-    nativeCurrency: "ETH",
-  },
-  etherlinkTestnet: {
-    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-    explorer: "https://testnet.explorer.etherlink.com/address/$ADDRESS",
-    delay: "",
-    gasLimit: "15M",
-    network: "testnet",
-    rpc: "https://node.ghostnet.etherlink.com",
-    nativeCurrency: "XTZ",
-  },
-  etherlink: {
-    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-    explorer: "https://explorer.etherlink.com/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "15M",
-    network: "mainnet",
-    rpc: "https://node.mainnet.etherlink.com/",
-    nativeCurrency: "XTZ",
-  },
-  kaia: {
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    explorer: "https://kaiascan.io/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    network: "mainnet",
-    rpc: "https://rpc.ankr.com/klaytn",
-    nativeCurrency: "KLAY",
-  },
-  kairos: {
-    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-    explorer: "https://kairos.kaiascan.io/address/$ADDRESS",
-    delay: "",
-    gasLimit: "500K",
-    network: "testnet",
-    rpc: "https://rpc.ankr.com/klaytn_testnet",
-    nativeCurrency: "KLAY",
-  },
-  // tabiTestnet: {
-  //   address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
-  //   explorer: "https://testnetv2.tabiscan.com/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://rpc.testnetv2.tabichain.com",
-  //   nativeCurrency: "TABI",
-  // },
-  // b3Testnet: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://sepolia.explorer.b3.fun/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://sepolia.b3.fun/http/",
-  //   nativeCurrency: "ETH",
-  // },
-  // b3: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://explorer.b3.fun/address/$ADDRESS",
-  //   delay: "1 block",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://mainnet-rpc.b3.fun/http",
-  //   nativeCurrency: "ETH",
-  // },
-  // apechainTestnet: {
-  //   address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  //   explorer: "https://curtis.explorer.caldera.xyz/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://curtis.rpc.caldera.xyz/http",
-  //   nativeCurrency: "APE",
-  // },
-  // soneiumMinatoTestnet: {
-  //   address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  //   explorer: "https://explorer-testnet.soneium.org/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://rpc.minato.soneium.org/",
-  //   nativeCurrency: "ETH",
-  // },
-  // sanko: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://explorer.sanko.xyz/address/$ADDRESS",
-  //   delay: "1 block",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://mainnet.sanko.xyz",
-  //   nativeCurrency: "DMT",
-  // },
-  // sankoTestnet: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://sanko-arb-sepolia.explorer.caldera.xyz/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://sanko-arb-sepolia.rpc.caldera.xyz/http",
-  //   nativeCurrency: "DMT",
-  // },
-  // apechain: {
-  //   address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-  //   explorer: "https://apechain.calderaexplorer.xyz/address/$ADDRESS",
-  //   delay: "1 block",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://apechain.calderachain.xyz/http",
-  //   nativeCurrency: "APE",
-  // },
-  // abstractTestnet: {
-  //   address: "0x858687fD592112f7046E394A3Bf10D0C11fF9e63",
-  //   explorer: "https://explorer.testnet.abs.xyz/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://api.testnet.abs.xyz",
-  //   nativeCurrency: "ETH",
-  // },
-  // sonicBlazeTestnet: {
-  //   address: "0xebe57e8045f2f230872523bbff7374986e45c486",
-  //   explorer: "https://blaze.soniclabs.com/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://rpc.blaze.soniclabs.com",
-  //   nativeCurrency: "S",
-  // },
-  // unichainSepolia: {
-  //   address: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  //   explorer: "https://unichain-sepolia.blockscout.com/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://sepolia.unichain.org",
-  //   nativeCurrency: "ETH",
-  // },
-  // sonic: {
-  //   address: "0x36825bf3fbdf5a29e2d5148bfe7dcf7b5639e320",
-  //   explorer: "https://sonicscan.org/address/$ADDRESS",
-  //   delay: "1 block",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://rpc.soniclabs.com",
-  //   nativeCurrency: "S",
-  // },
-  // storyTestnet: {
-  //   address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  //   explorer: "https://aeneid.storyscan.xyz/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://aeneid.storyrpc.io",
-  //   nativeCurrency: "IP",
-  // },
-  // monadTestnet: {
-  //   address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-  //   explorer: "https://testnet.monadexplorer.com/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://testnet-rpc.monad.xyz",
-  //   nativeCurrency: "MON",
-  // },
-  // abstract: {
-  //   address: "0x5a4a369F4db5df2054994AF031b7b23949b98c0e",
-  //   explorer: "https://abscan.org/address/$ADDRESS",
-  //   delay: "1 block",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://api.mainnet.abs.xyz",
-  //   nativeCurrency: "ETH",
-  // },
-  // story: {
-  //   address: "0xdF21D137Aadc95588205586636710ca2890538d5",
-  //   explorer: "https://storyscan.xyz/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://mainnet.storyrpc.io",
-  //   nativeCurrency: "IP",
-  // },
-  // berachainBepolia: {
-  //   address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-  //   explorer: "https://bepolia.beratrail.io/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "testnet",
-  //   rpc: "https://bepolia.rpc.berachain.com",
-  //   nativeCurrency: "BERA",
-  // },
-  // hyperevm: {
-  //   address: "0xfA25E653b44586dBbe27eE9d252192F0e4956683",
-  //   explorer: "https://hyperliquid.cloud.blockscout.com/address/$ADDRESS",
-  //   delay: "",
-  //   gasLimit: "500K",
-  //   network: "mainnet",
-  //   rpc: "https://rpc.hyperliquid.xyz/evm",
-  //   nativeCurrency: "HYPE",
-  // },
-  soneium: {
-    address: "0x0708325268dF9F66270F1401206434524814508b",
-    explorer: "https://soneium.blockscout.com/address/$ADDRESS",
-    delay: "1 block",
-    gasLimit: "500K",
-    network: "mainnet",
-    rpc: "https://soneium.drpc.org",
-    nativeCurrency: "ETH",
-  },
-} as const satisfies Partial<
-  Record<keyof typeof viemChains, EntropyDeployment>
->;
-
-export const isValidDeployment = (
-  name: string,
-): name is keyof typeof EntropyDeployments =>
-  Object.prototype.hasOwnProperty.call(EntropyDeployments, name);

+ 456 - 0
apps/entropy-explorer/src/entropy-deployments.tsx

@@ -0,0 +1,456 @@
+export type EntropyDeployment = {
+  address: string;
+  rpc: string;
+  explorerTxTemplate: string;
+  explorerAccountTemplate: string;
+  name: string;
+  icon: string;
+  isTestnet: boolean;
+};
+
+export const EntropyDeployments = {
+  [10]: {
+    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
+    name: "OP Mainnet",
+    rpc: "https://optimism.llamarpc.com",
+    explorerTxTemplate: "https://optimistic.etherscan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://optimistic.etherscan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [130]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Unichain",
+    rpc: "https://mainnet.unichain.org",
+    explorerTxTemplate: "https://unichain.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://unichain.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [146]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Sonic Mainnet",
+    rpc: "https://rpc.soniclabs.com",
+    explorerTxTemplate: "https://sonicscan.org/tx/$ADDRESS",
+    explorerAccountTemplate: "https://sonicscan.org/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [919]: {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    name: "Mode Testnet",
+    rpc: "https://sepolia.mode.network/",
+    explorerTxTemplate: "https://testnet.modescan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://testnet.modescan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_mode?w=20&h=20",
+    isTestnet: true,
+  },
+  [999]: {
+    address: "0xfA25E653b44586dBbe27eE9d252192F0e4956683",
+    name: "HyperEVM",
+    rpc: "https://rpc.hyperliquid.xyz/evm",
+    explorerTxTemplate: "https://hyperliquid.cloud.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://hyperliquid.cloud.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_hyperevm?w=20&h=20",
+    isTestnet: false,
+  },
+  [1001]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Kaia Kairos Testnet",
+    rpc: "https://rpc.ankr.com/klaytn_testnet",
+    explorerTxTemplate: "https://kairos.kaiascan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://kairos.kaiascan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_klaytn.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [1301]: {
+    address: "0x8D254a21b3C86D32F7179855531CE99164721933",
+    name: "Unichain Sepolia Testnet",
+    rpc: "https://sepolia.unichain.org",
+    explorerTxTemplate: "https://unichain-sepolia.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://unichain-sepolia.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_unichain.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [1315]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "Story Aeneid Testnet",
+    rpc: "https://aeneid.storyrpc.io",
+    explorerTxTemplate: "https://aeneid.storyscan.xyz/tx/$ADDRESS",
+    explorerAccountTemplate: "https://aeneid.storyscan.xyz/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_story?w=20&h=20",
+    isTestnet: true,
+  },
+  [1328]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Sei Testnet",
+    rpc: "https://evm-rpc-testnet.sei-apis.com",
+    explorerTxTemplate: "https://seitrace.com/tx/$ADDRESS?chain=atlantic-2",
+    explorerAccountTemplate:
+      "https://seitrace.com/address/$ADDRESS?chain=atlantic-2",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [1329]: {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    name: "Sei Network",
+    rpc: "https://evm-rpc.sei-apis.com",
+    explorerTxTemplate: "https://seitrace.com/tx/$ADDRESS?chain=pacific-1",
+    explorerAccountTemplate:
+      "https://seitrace.com/address/$ADDRESS?chain=pacific-1",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_sei.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [1514]: {
+    address: "0xdF21D137Aadc95588205586636710ca2890538d5",
+    name: "Story",
+    rpc: "https://mainnet.storyrpc.io",
+    explorerTxTemplate: "https://storyscan.xyz/tx/$ADDRESS",
+    explorerAccountTemplate: "https://storyscan.xyz/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_story?w=20&h=20",
+    isTestnet: false,
+  },
+  [1868]: {
+    address: "0x0708325268dF9F66270F1401206434524814508b",
+    name: "Soneium",
+    rpc: "https://soneium.drpc.org",
+    explorerTxTemplate: "https://soneium.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://soneium.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_soneium.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [1890]: {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    name: "Lightlink Phoenix Mainnet",
+    rpc: "https://replicator.phoenix.lightlink.io/rpc/v1",
+    explorerTxTemplate: "https://phoenix.lightlink.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://phoenix.lightlink.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_lightlink?w=20&h=20",
+    isTestnet: false,
+  },
+  [1891]: {
+    address: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a",
+    name: "Lightlink Pegasus Testnet",
+    rpc: "https://replicator.pegasus.lightlink.io/rpc/v1",
+    explorerTxTemplate: "https://pegasus.lightlink.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://pegasus.lightlink.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_lightlink?w=20&h=20",
+    isTestnet: true,
+  },
+  [1946]: {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    name: "Soneium Testnet Minato",
+    rpc: "https://rpc.minato.soneium.org/",
+    explorerTxTemplate: "https://explorer-testnet.soneium.org/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://explorer-testnet.soneium.org/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_soneium.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [1992]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "Sanko Testnet",
+    rpc: "https://sanko-arb-sepolia.rpc.caldera.xyz/http",
+    explorerTxTemplate:
+      "https://sanko-arb-sepolia.explorer.caldera.xyz/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "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,
+  },
+  [1993]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "B3 Sepolia Testnet",
+    rpc: "https://sepolia.b3.fun/http/",
+    explorerTxTemplate: "https://sepolia.explorer.b3.fun/tx/$ADDRESS",
+    explorerAccountTemplate: "https://sepolia.explorer.b3.fun/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_b3?w=20&h=20",
+    isTestnet: true,
+  },
+  [1996]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "Sanko",
+    rpc: "https://mainnet.sanko.xyz",
+    explorerTxTemplate: "https://explorer.sanko.xyz/tx/$ADDRESS",
+    explorerAccountTemplate: "https://explorer.sanko.xyz/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_sanko.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [2741]: {
+    address: "0x5a4a369F4db5df2054994AF031b7b23949b98c0e",
+    name: "Abstract",
+    rpc: "https://api.mainnet.abs.xyz",
+    explorerTxTemplate: "https://abscan.org/tx/$ADDRESS",
+    explorerAccountTemplate: "https://abscan.org/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_abstract.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [4200]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Merlin Mainnet",
+    rpc: "https://rpc.merlinchain.io",
+    explorerTxTemplate: "https://scan.merlinchain.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://scan.merlinchain.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_merlin.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [7000]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "ZetaChain Mainnet",
+    rpc: "https://zetachain-evm.blockpi.network/v1/rpc/public",
+    explorerTxTemplate: "https://zetachain.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://zetachain.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_zetachain.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [7001]: {
+    address: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
+    name: "ZetaChain Testnet",
+    rpc: "https://zetachain-athens-evm.blockpi.network/v1/rpc/public",
+    explorerTxTemplate: "https://explorer.zetachain.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://explorer.zetachain.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_zetachain.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [8217]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Kaia Mainnet",
+    rpc: "https://rpc.ankr.com/klaytn",
+    explorerTxTemplate: "https://kaiascan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://kaiascan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_klaytn.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [8333]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "B3",
+    rpc: "https://mainnet-rpc.b3.fun/http",
+    explorerTxTemplate: "https://explorer.b3.fun/tx/$ADDRESS",
+    explorerAccountTemplate: "https://explorer.b3.fun/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_b3?w=20&h=20",
+    isTestnet: false,
+  },
+  [8453]: {
+    address: "0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb",
+    name: "Base",
+    rpc: "https://developer-access-mainnet.base.org/",
+    explorerTxTemplate: "https://basescan.org/tx/$ADDRESS",
+    explorerAccountTemplate: "https://basescan.org/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_base.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [9788]: {
+    address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
+    name: "Tabi Testnet v2",
+    rpc: "https://rpc.testnetv2.tabichain.com",
+    explorerTxTemplate: "https://testnetv2.tabiscan.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://testnetv2.tabiscan.com/address/$ADDRESS",
+    icon: "https://www.tabichain.com/images/new2/tabi.svg",
+    isTestnet: true,
+  },
+  [10_143]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Monad Testnet",
+    rpc: "https://testnet-rpc.monad.xyz",
+    explorerTxTemplate: "https://testnet.monadexplorer.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://testnet.monadexplorer.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_monad.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [11_124]: {
+    address: "0x858687fD592112f7046E394A3Bf10D0C11fF9e63",
+    name: "Abstract Sepolia Testnet",
+    rpc: "https://api.testnet.abs.xyz",
+    explorerTxTemplate: "https://explorer.testnet.abs.xyz/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://explorer.testnet.abs.xyz/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_abstract.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [33_111]: {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    name: "Curtis",
+    rpc: "https://curtis.rpc.caldera.xyz/http",
+    explorerTxTemplate: "https://curtis.explorer.caldera.xyz/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://curtis.explorer.caldera.xyz/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_apechain.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [33_139]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "ApeChain",
+    rpc: "https://apechain.calderachain.xyz/http",
+    explorerTxTemplate: "https://apescan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://apescan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_apechain.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [34_443]: {
+    address: "0x8D254a21b3C86D32F7179855531CE99164721933",
+    name: "Mode",
+    rpc: "https://mainnet.mode.network/",
+    explorerTxTemplate: "https://explorer.mode.network/tx/$ADDRESS",
+    explorerAccountTemplate: "https://explorer.mode.network/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_mode?w=20&h=20",
+    isTestnet: false,
+  },
+  [42_161]: {
+    address: "0x7698E925FfC29655576D0b361D75Af579e20AdAc",
+    name: "Arbitrum One",
+    rpc: "https://arb1.arbitrum.io/rpc",
+    explorerTxTemplate: "https://arbiscan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://arbiscan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [42_793]: {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    name: "Etherlink Mainnet",
+    rpc: "https://node.mainnet.etherlink.com/",
+    explorerTxTemplate: "https://explorer.etherlink.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://explorer.etherlink.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_etherlink.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [57_054]: {
+    address: "0xEbe57e8045F2F230872523bbff7374986E45C486",
+    name: "Sonic Blaze Testnet",
+    rpc: "https://rpc.blaze.soniclabs.com",
+    explorerTxTemplate: "https://blaze.soniclabs.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://blaze.soniclabs.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_sonic.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [80_069]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Berachain Bepolia",
+    rpc: "https://bepolia.rpc.berachain.com",
+    explorerTxTemplate: "https://bepolia.beratrail.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://bepolia.beratrail.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_berachain?w=20&h=20",
+    isTestnet: true,
+  },
+  [80_094]: {
+    address: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
+    name: "Berachain",
+    rpc: "https://rpc.berachain.com/",
+    explorerTxTemplate: "https://berascan.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://berascan.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_berachain?w=20&h=20",
+    isTestnet: false,
+  },
+  [81_457]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "Blast",
+    rpc: "https://rpc.blast.io",
+    explorerTxTemplate: "https://blastscan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://blastscan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_blast.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [84_532]: {
+    address: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
+    name: "Base Sepolia Testnet",
+    rpc: "https://sepolia.base.org",
+    explorerTxTemplate: "https://base-sepolia.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://base-sepolia.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_base.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [88_882]: {
+    address: "0xD458261E832415CFd3BAE5E416FdF3230ce6F134",
+    name: "Chiliz Spicy Testnet",
+    rpc: "https://spicy-rpc.chiliz.com",
+    explorerTxTemplate: "https://spicy-explorer.chiliz.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://spicy-explorer.chiliz.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_chiliz.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [88_888]: {
+    address: "0x0708325268dF9F66270F1401206434524814508b",
+    name: "Chiliz Chain",
+    rpc: "https://rpc.ankr.com/chiliz",
+    explorerTxTemplate: "https://scan.chiliz.com/tx/$ADDRESS",
+    explorerAccountTemplate: "https://scan.chiliz.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_chiliz.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [128_123]: {
+    address: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
+    name: "Etherlink Testnet",
+    rpc: "https://node.ghostnet.etherlink.com",
+    explorerTxTemplate: "https://testnet.explorer.etherlink.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://testnet.explorer.etherlink.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_etherlink.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [167_000]: {
+    address: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
+    name: "Taiko Alethia",
+    rpc: "https://rpc.mainnet.taiko.xyz",
+    explorerTxTemplate: "https://taikoscan.network/tx/$ADDRESS",
+    explorerAccountTemplate: "https://taikoscan.network/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_taiko.jpg?w=20&h=20",
+    isTestnet: false,
+  },
+  [167_009]: {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    name: "Taiko Hekla",
+    rpc: "https://rpc.hekla.taiko.xyz/",
+    explorerTxTemplate: "https://hekla.taikoscan.network/tx/$ADDRESS",
+    explorerAccountTemplate: "https://hekla.taikoscan.network/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_taiko.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [421_614]: {
+    address: "0x549Ebba8036Ab746611B4fFA1423eb0A4Df61440",
+    name: "Arbitrum Sepolia",
+    rpc: "https://sepolia-rollup.arbitrum.io/rpc",
+    explorerTxTemplate: "https://sepolia.arbiscan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://sepolia.arbiscan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_arbitrum.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [686_868]: {
+    address: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
+    name: "Merlin Testnet",
+    rpc: "https://testnet-rpc.merlinchain.io/",
+    explorerTxTemplate: "https://testnet-scan.merlinchain.io/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://testnet-scan.merlinchain.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_merlin?w=20&h=20",
+    isTestnet: true,
+  },
+  [11_155_420]: {
+    address: "0x4821932D0CDd71225A6d914706A621e0389D7061",
+    name: "OP Sepolia Testnet",
+    rpc: "https://api.zan.top/opt-sepolia",
+    explorerTxTemplate: "https://optimism-sepolia.blockscout.com/tx/$ADDRESS",
+    explorerAccountTemplate:
+      "https://optimism-sepolia.blockscout.com/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_optimism.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+  [168_587_773]: {
+    address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
+    name: "Blast Sepolia Testnet",
+    rpc: "https://sepolia.blast.io",
+    explorerTxTemplate: "https://sepolia.blastscan.io/tx/$ADDRESS",
+    explorerAccountTemplate: "https://sepolia.blastscan.io/address/$ADDRESS",
+    icon: "https://icons.llamao.fi/icons/chains/rsz_blast.jpg?w=20&h=20",
+    isTestnet: true,
+  },
+} as const satisfies Record<string, EntropyDeployment>;
+
+export const isValidDeployment = (
+  name: number,
+): name is keyof typeof EntropyDeployments =>
+  Object.prototype.hasOwnProperty.call(EntropyDeployments, name);

+ 32 - 5
apps/entropy-explorer/src/errors.ts

@@ -2,52 +2,79 @@ export const ERROR_DETAILS = {
   "0xd82dd966": [
   "0xd82dd966": [
     "AssertionFailure",
     "AssertionFailure",
     "An invariant of the contract failed to hold. This error indicates a software logic bug.",
     "An invariant of the contract failed to hold. This error indicates a software logic bug.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0xda041bdf": [
   "0xda041bdf": [
     "ProviderAlreadyRegistered",
     "ProviderAlreadyRegistered",
     "The provider being registered has already registered",
     "The provider being registered has already registered",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
+  ],
+  "0xdf51c431": [
+    "NoSuchProvider",
+    "The requested provider does not exist.",
+    "https://docs.pyth.network/entropy/contract-addresses",
+  ],
+  "0xc4237352": [
+    "NoSuchRequest",
+    "The specified request does not exist.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
-  "0xdf51c431": ["NoSuchProvider", "The requested provider does not exist."],
-  "0xc4237352": ["NoSuchRequest", "The specified request does not exist."],
   "0x3e515085": [
   "0x3e515085": [
     "OutOfRandomness",
     "OutOfRandomness",
     "The randomness provider is out of commited random numbers. The provider needs to rotate their on-chain commitment to resolve this error.",
     "The randomness provider is out of commited random numbers. The provider needs to rotate their on-chain commitment to resolve this error.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
+  ],
+  "0x025dbdd4": [
+    "InsufficientFee",
+    "The transaction fee was not sufficient",
+    "https://docs.pyth.network/entropy/current-fees",
   ],
   ],
-  "0x025dbdd4": ["InsufficientFee", "The transaction fee was not sufficient"],
   "0xb8be1a8d": [
   "0xb8be1a8d": [
     "IncorrectRevelation",
     "IncorrectRevelation",
     "Either the user's or the provider's revealed random values did not match their commitment.",
     "Either the user's or the provider's revealed random values did not match their commitment.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0xb463ce7a": [
   "0xb463ce7a": [
     "InvalidUpgradeMagic",
     "InvalidUpgradeMagic",
     "Governance message is invalid (e.g., deserialization error).",
     "Governance message is invalid (e.g., deserialization error).",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0x82b42900": [
   "0x82b42900": [
     "Unauthorized",
     "Unauthorized",
     "The msg.sender is not allowed to invoke this call.",
     "The msg.sender is not allowed to invoke this call.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
+  ],
+  "0x92555c0e": [
+    "BlockhashUnavailable",
+    "The blockhash is 0.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
-  "0x92555c0e": ["BlockhashUnavailable", "The blockhash is 0."],
   "0x50f0dc92": [
   "0x50f0dc92": [
     "InvalidRevealCall",
     "InvalidRevealCall",
     "if a request was made using `requestWithCallback`, request should be fulfilled using `revealWithCallback` else if a request was made using `request`, request should be fulfilled using `reveal`",
     "if a request was made using `requestWithCallback`, request should be fulfilled using `revealWithCallback` else if a request was made using `request`, request should be fulfilled using `reveal`",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0xb28d9c76": [
   "0xb28d9c76": [
     "LastRevealedTooOld",
     "LastRevealedTooOld",
     "The last random number revealed from the provider is too old. Therefore, too many hashes are required for any new reveal. Please update the currentCommitment before making more requests.",
     "The last random number revealed from the provider is too old. Therefore, too many hashes are required for any new reveal. Please update the currentCommitment before making more requests.",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0x5e5b3f1b": [
   "0x5e5b3f1b": [
     "UpdateTooOld",
     "UpdateTooOld",
     "A more recent commitment is already revealed on-chain",
     "A more recent commitment is already revealed on-chain",
+    "https://docs.pyth.network/entropy/best-practices#handling-callback-failures",
   ],
   ],
   "0x1c26714c": [
   "0x1c26714c": [
     "InsufficientGas",
     "InsufficientGas",
     "Not enough gas was provided to the function to execute the callback with the desired amount of gas.",
     "Not enough gas was provided to the function to execute the callback with the desired amount of gas.",
+    "https://docs.pyth.network/entropy/best-practices#limit-gas-usage-on-the-callback",
   ],
   ],
   "0x9376b93b": [
   "0x9376b93b": [
     "MaxGasLimitExceeded",
     "MaxGasLimitExceeded",
     "A gas limit value was provided that was greater than the maximum possible limit of 655,350,000",
     "A gas limit value was provided that was greater than the maximum possible limit of 655,350,000",
+    "https://docs.pyth.network/entropy/best-practices#limit-gas-usage-on-the-callback",
   ],
   ],
 } as const;
 } as const;
 
 
 export const getErrorDetails = (error: string) =>
 export const getErrorDetails = (error: string) =>
-  (ERROR_DETAILS as Record<string, readonly [string, string]>)[error];
+  (ERROR_DETAILS as Record<string, readonly [string, string, string]>)[error];

+ 3 - 0
apps/entropy-explorer/src/pages.ts

@@ -0,0 +1,3 @@
+export const PAGE_SIZES = [5, 10, 25, 50, 75, 100] as const;
+export type PAGE_SIZE = (typeof PAGE_SIZES)[number];
+export const DEFAULT_PAGE_SIZE: PAGE_SIZE = 50;

+ 242 - 99
apps/entropy-explorer/src/requests.ts

@@ -1,110 +1,217 @@
 import { z } from "zod";
 import { z } from "zod";
 
 
-import type { EntropyDeployments } from "./entropy-deployments";
-import { ERROR_DETAILS } from "./errors";
-
-const MOCK_DATA_SIZE = 20;
-
-export const getRequests = async (): Promise<Request[]> => {
-  await new Promise((resolve) => setTimeout(resolve, 1000));
-
-  return requestsSchema.parse(
-    range(MOCK_DATA_SIZE).map(() => {
-      const completed = randomBoolean();
-      const gasLimit = randomBetween(10_000, 1_000_000);
-      const gasUsed = randomBetween(1000, 500_000);
-      const fail = gasUsed > gasLimit || randomBoolean();
-
-      return {
-        chain: randomElem(chains),
-        sequenceNumber: Math.floor(randomBetween(10_000, 100_100)),
-        provider: `0x${randomHex(42)}`,
-        sender: `0x${randomHex(42)}`,
-        requestTxHash: `0x${randomHex(42)}`,
-        gasLimit,
-        requestTimestamp: new Date(),
-        hasCallbackCompleted: completed,
-        userRandomNumber: `0x${randomHex(42)}`,
-        ...(completed && {
-          callbackTxHash: `0x${randomHex(42)}`,
-          callbackFailed: fail,
-          randomNumber: `0x${randomHex(10)}`,
-          returnValue:
-            !fail || gasUsed > gasLimit
-              ? ""
-              : randomElem(Object.keys(ERROR_DETAILS)),
-          gasUsed,
-          callbackTimestamp: new Date(),
-        }),
-      };
-    }),
-  );
-};
+import { EntropyDeployments, isValidDeployment } from "./entropy-deployments";
+import type { PAGE_SIZE } from "./pages";
+import { DEFAULT_PAGE_SIZE } from "./pages";
 
 
-const range = (i: number) => [...Array.from({ length: i }).keys()];
+const FORTUNA_URL = "https://fortuna-staging.dourolabs.app/";
 
 
-const randomBetween = (min: number, max: number) =>
-  Math.random() * (max - min) + min;
+export type Args = Partial<{
+  search: string;
+  chain: number;
+  status: string;
+  pageSize: PAGE_SIZE;
+  page: number;
+}>;
+
+export const getRequests = async ({
+  search,
+  chain,
+  status,
+  pageSize = DEFAULT_PAGE_SIZE,
+  page,
+}: Args): Promise<Result> => {
+  const url = new URL("/v1/logs", FORTUNA_URL);
+  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());
+  if (page) {
+    url.searchParams.set("offset", ((page - 1) * pageSize).toString());
+  }
+  if (search) {
+    url.searchParams.set("query", search);
+  }
+  if (chain) {
+    url.searchParams.set("network_id", chain.toString());
+  }
+  const fortunaStatus = status ? toFortunaStatus(status) : undefined;
+  if (fortunaStatus) {
+    url.searchParams.set("state", fortunaStatus);
+  }
+  try {
+    const response = await fetch(url);
+    if (response.status === 400) {
+      const text = await response.text();
+      return text ===
+        "The query string is not parsable to a transaction hash, address, or sequence number" &&
+        search !== undefined
+        ? Result.BadSearch(search)
+        : Result.ErrorResult(toError(text));
+    } else if (response.status === 200) {
+      try {
+        const parsed = fortunaSchema.safeParse(await response.json());
+        return parsed.success
+          ? Result.Success({
+              numPages: Math.ceil(parsed.data.total_results / pageSize),
+              currentPage: parsed.data.requests.map((request) => {
+                const common = {
+                  chain: request.network_id,
+                  gasLimit: request.gas_limit,
+                  provider: request.provider,
+                  requestTimestamp: request.created_at,
+                  requestTxHash: request.request_tx_hash,
+                  sender: request.sender,
+                  sequenceNumber: request.sequence,
+                  userContribution: request.user_random_number,
+                };
+                switch (request.state.state) {
+                  case "completed": {
+                    const completedCommon = {
+                      ...common,
+                      callbackTxHash: request.state.reveal_tx_hash,
+                      callbackTimestamp: request.last_updated_at,
+                      gasUsed: request.state.gas_used,
+                      randomNumber: request.state.combined_random_number,
+                      providerContribution:
+                        request.state.provider_random_number,
+                    };
+                    return request.state.callback_failed
+                      ? Request.CallbackErrored({
+                          ...completedCommon,
+                          returnValue: request.state.callback_return_value,
+                        })
+                      : Request.Complete({
+                          ...completedCommon,
+                        });
+                  }
+                  case "pending": {
+                    return Request.Pending(common);
+                  }
+                  case "failed": {
+                    return Request.Failed({
+                      ...common,
+                      reason: request.state.reason.replace(/^Reverted: /, ""),
+                      providerContribution:
+                        request.state.provider_random_number,
+                    });
+                  }
+                }
+              }),
+            })
+          : Result.ErrorResult(parsed.error);
+      } catch (error) {
+        return Result.ErrorResult(toError(error));
+      }
+    } else {
+      return Result.ErrorResult(new NotOKError(response));
+    }
+  } catch (error) {
+    return Result.ErrorResult(toError(error));
+  }
+};
 
 
-const randomBoolean = (): boolean => Math.random() < 0.5;
+class NotOKError extends Error {
+  constructor(result: Response) {
+    super(`Received a ${result.status.toString()} response for ${result.url}`);
+    this.cause = result;
+    this.name = "NotOKError";
+  }
+}
+
+const toError = (e: unknown) => {
+  if (e instanceof Error) {
+    return e;
+  } else if (typeof e === "string") {
+    return new UnknownError(e);
+  } else {
+    return new UnknownError("Unknown Error");
+  }
+};
 
 
-const randomHex = (length: number) =>
-  Array.from({ length })
-    .map(() => Math.floor(Math.random() * 16).toString(16))
-    .join("");
+class UnknownError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = "UnknownError";
+  }
+}
 
 
-const randomElem = <T>(arr: T[] | readonly T[]) =>
-  arr[Math.floor(randomBetween(0, arr.length))];
+export enum ResultType {
+  Success,
+  BadSearch,
+  ErrorResult,
+}
 
 
-const chains = [
-  "arbitrum",
-  "base",
-  "optimism",
-  "baseSepolia",
-  "optimismSepolia",
-] as const;
+const Result = {
+  Success: ({
+    numPages,
+    currentPage,
+  }: {
+    numPages: number;
+    currentPage: Request[];
+  }) => ({ type: ResultType.Success as const, numPages, currentPage }),
+  BadSearch: (search: string) => ({
+    type: ResultType.BadSearch as const,
+    search,
+  }),
+  ErrorResult: (error: Error) => ({
+    type: ResultType.ErrorResult as const,
+    error,
+  }),
+};
+type Result = ReturnType<(typeof Result)[keyof typeof Result]>;
 
 
 const hexStringSchema = z.custom<`0x${string}`>(
 const hexStringSchema = z.custom<`0x${string}`>(
   (val) => typeof val === "string" && val.startsWith("0x"),
   (val) => typeof val === "string" && val.startsWith("0x"),
 );
 );
-const schemaBase = z.strictObject({
-  chain: z.enum(chains),
-  sequenceNumber: z.number(),
-  provider: hexStringSchema,
-  sender: hexStringSchema,
-  requestTxHash: hexStringSchema,
-  gasLimit: z.number(),
-  userRandomNumber: hexStringSchema,
-  requestTimestamp: z.date(),
+
+const completedStateSchema = z.strictObject({
+  combined_random_number: z.string(),
+  gas_used: z.string().transform((value) => Number.parseInt(value, 10)),
+  provider_random_number: z.string(),
+  reveal_block_number: z.number(),
+  reveal_tx_hash: hexStringSchema,
+  state: z.literal("completed"),
+  callback_failed: z.boolean(),
+  callback_return_value: z.string(),
+  callback_gas_used: z.string(),
+});
+
+const pendingStateSchema = z.strictObject({
+  state: z.literal("pending"),
+});
+
+const failedSchema = z.strictObject({
+  provider_random_number: z.string(),
+  reason: z.string(),
+  state: z.literal("failed"),
+});
+
+const fortunaSchema = z.strictObject({
+  total_results: z.number(),
+  requests: z.array(
+    z.strictObject({
+      chain_id: z.string(),
+      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()}` }),
+      ),
+      provider: hexStringSchema,
+      request_block_number: z.number(),
+      request_tx_hash: hexStringSchema,
+      sender: hexStringSchema,
+      sequence: z.number(),
+      user_random_number: z.string(),
+      state: z.union([completedStateSchema, pendingStateSchema, failedSchema]),
+    }),
+  ),
 });
 });
-const inProgressRequestScehma = schemaBase
-  .extend({
-    hasCallbackCompleted: z.literal(false),
-  })
-  .transform((args) => Request.Pending(args));
-const completedRequestSchema = schemaBase
-  .extend({
-    hasCallbackCompleted: z.literal(true),
-    callbackTxHash: hexStringSchema,
-    callbackFailed: z.boolean(),
-    randomNumber: hexStringSchema,
-    returnValue: z.union([hexStringSchema, z.literal("")]),
-    gasUsed: z.number(),
-    callbackTimestamp: z.date(),
-  })
-  .transform((args) =>
-    args.callbackFailed
-      ? Request.CallbackErrored(args)
-      : Request.Complete(args),
-  );
-const requestSchema = z.union([
-  inProgressRequestScehma,
-  completedRequestSchema,
-]);
-const requestsSchema = z.array(requestSchema);
 
 
 export enum Status {
 export enum Status {
   Pending,
   Pending,
+  Failed,
   CallbackError,
   CallbackError,
   Complete,
   Complete,
 }
 }
@@ -117,35 +224,71 @@ type BaseArgs = {
   requestTxHash: `0x${string}`;
   requestTxHash: `0x${string}`;
   gasLimit: number;
   gasLimit: number;
   requestTimestamp: Date;
   requestTimestamp: Date;
-  userRandomNumber: `0x${string}`;
+  userContribution: string;
 };
 };
 type PendingArgs = BaseArgs;
 type PendingArgs = BaseArgs;
-type RevealedBaseArgs = BaseArgs & {
-  callbackTxHash: `0x${string}`;
-  randomNumber: `0x${string}`;
+type FailedArgs = BaseArgs & {
+  reason: string;
+  providerContribution: string;
+};
+type CompletedArgs = BaseArgs & {
+  randomNumber: string;
   gasUsed: number;
   gasUsed: number;
   callbackTimestamp: Date;
   callbackTimestamp: Date;
+  providerContribution: string;
+  callbackTxHash: `0x${string}`;
 };
 };
-type CallbackErrorArgs = RevealedBaseArgs & {
-  returnValue: "" | `0x${string}`;
+type CallbackErrorArgs = CompletedArgs & {
+  returnValue: string;
 };
 };
-type CompleteArgs = RevealedBaseArgs;
 
 
 const Request = {
 const Request = {
   Pending: (args: PendingArgs) => ({
   Pending: (args: PendingArgs) => ({
     status: Status.Pending as const,
     status: Status.Pending as const,
     ...args,
     ...args,
   }),
   }),
+  Failed: (args: FailedArgs) => ({
+    status: Status.Failed as const,
+    ...args,
+  }),
   CallbackErrored: (args: CallbackErrorArgs) => ({
   CallbackErrored: (args: CallbackErrorArgs) => ({
     status: Status.CallbackError as const,
     status: Status.CallbackError as const,
     ...args,
     ...args,
   }),
   }),
-  Complete: (args: CompleteArgs) => ({
+  Complete: (args: CompletedArgs) => ({
     status: Status.Complete as const,
     status: Status.Complete as const,
     ...args,
     ...args,
   }),
   }),
 };
 };
 export type Request = ReturnType<(typeof Request)[keyof typeof Request]>;
 export type Request = ReturnType<(typeof Request)[keyof typeof Request]>;
 export type PendingRequest = ReturnType<typeof Request.Pending>;
 export type PendingRequest = ReturnType<typeof Request.Pending>;
+export type FailedRequest = ReturnType<typeof Request.Failed>;
 export type CallbackErrorRequest = ReturnType<typeof Request.CallbackErrored>;
 export type CallbackErrorRequest = ReturnType<typeof Request.CallbackErrored>;
 export type CompleteRequest = ReturnType<typeof Request.Complete>;
 export type CompleteRequest = ReturnType<typeof Request.Complete>;
+
+export const StatusParams = {
+  [Status.Pending]: "pending",
+  [Status.Failed]: "failed",
+  [Status.Complete]: "complete",
+  [Status.CallbackError]: "callback-error",
+} as const;
+
+const toFortunaStatus = (status: string) => {
+  switch (status) {
+    case StatusParams[Status.Pending]: {
+      return "Pending";
+    }
+    case StatusParams[Status.Failed]: {
+      return "Failed";
+    }
+    case StatusParams[Status.Complete]: {
+      return "Completed";
+    }
+    case StatusParams[Status.CallbackError]: {
+      return "CallbackErrored";
+    }
+    default: {
+      return;
+    }
+  }
+};

+ 1 - 1
apps/insights/src/components/Publisher/performance.tsx

@@ -378,7 +378,7 @@ const TopFeedsCard = ({
       <NoResults
       <NoResults
         icon={emptyIcon}
         icon={emptyIcon}
         header={emptyHeader}
         header={emptyHeader}
-        body={emptyBody}
+        body={<p>{emptyBody}</p>}
         variant={emptyVariant}
         variant={emptyVariant}
       />
       />
     )}
     )}

+ 2 - 0
packages/component-library/src/ErrorPage/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
 import { Warning } from "@phosphor-icons/react/dist/ssr/Warning";
 import { useEffect } from "react";
 import { useEffect } from "react";
 
 

+ 12 - 6
packages/component-library/src/Header/index.tsx

@@ -64,7 +64,7 @@ export const Header = ({
           Support
           Support
         </Button>
         </Button>
         {extraCta}
         {extraCta}
-        <MobileMenu className={styles.mobileMenu} />
+        <MobileMenu className={styles.mobileMenu} mainCta={mainCta} />
         <Button
         <Button
           href={mainCta?.href ?? "https://docs.pyth.network"}
           href={mainCta?.href ?? "https://docs.pyth.network"}
           size="sm"
           size="sm"
@@ -80,7 +80,13 @@ export const Header = ({
   </header>
   </header>
 );
 );
 
 
-const MobileMenu = ({ className }: { className?: string | undefined }) => (
+const MobileMenu = ({
+  className,
+  mainCta,
+}: {
+  className?: string | undefined;
+  mainCta: Props["mainCta"];
+}) => (
   <Button
   <Button
     className={className ?? ""}
     className={className ?? ""}
     beforeIcon={<List />}
     beforeIcon={<List />}
@@ -91,14 +97,14 @@ const MobileMenu = ({ className }: { className?: string | undefined }) => (
     drawer={{
     drawer={{
       hideHeading: true,
       hideHeading: true,
       title: "Menu",
       title: "Menu",
-      contents: <MobileMenuContents />,
+      contents: <MobileMenuContents mainCta={mainCta} />,
     }}
     }}
   >
   >
     Menu
     Menu
   </Button>
   </Button>
 );
 );
 
 
-const MobileMenuContents = () => (
+const MobileMenuContents = ({ mainCta }: { mainCta: Props["mainCta"] }) => (
   <div className={styles.mobileMenuContents}>
   <div className={styles.mobileMenuContents}>
     <div className={styles.buttons}>
     <div className={styles.buttons}>
       <Button
       <Button
@@ -111,12 +117,12 @@ const MobileMenuContents = () => (
         Support
         Support
       </Button>
       </Button>
       <Button
       <Button
-        href="https://docs.pyth.network"
+        href={mainCta?.href ?? "https://docs.pyth.network"}
         size="md"
         size="md"
         rounded
         rounded
         target="_blank"
         target="_blank"
       >
       >
-        Dev Docs
+        {mainCta?.label ?? "Dev Docs"}
       </Button>
       </Button>
     </div>
     </div>
     <div className={styles.theme}>
     <div className={styles.theme}>

+ 10 - 5
packages/component-library/src/NoResults/index.tsx

@@ -34,11 +34,16 @@ export const NoResults = ({ className, onClearSearch, ...props }: Props) => (
       <h3 className={styles.header}>
       <h3 className={styles.header}>
         {"header" in props ? props.header : "No results found"}
         {"header" in props ? props.header : "No results found"}
       </h3>
       </h3>
-      <p className={styles.body}>
-        {"body" in props
-          ? props.body
-          : `We couldn't find any results for ${props.query === "" ? "your query" : `"${props.query}"`}.`}
-      </p>
+      <div className={styles.body}>
+        {"body" in props ? (
+          props.body
+        ) : (
+          <p>
+            We couldn{"'"}t find any results for{" "}
+            {props.query === "" ? "your query" : `"${props.query}"`}.
+          </p>
+        )}
+      </div>
     </div>
     </div>
     {onClearSearch && (
     {onClearSearch && (
       <Button variant="outline" size="sm" onPress={onClearSearch}>
       <Button variant="outline" size="sm" onPress={onClearSearch}>

+ 7 - 0
packages/component-library/src/Paginator/index.module.scss

@@ -37,7 +37,14 @@
   .paginatorToolbar {
   .paginatorToolbar {
     display: flex;
     display: flex;
     flex-flow: row nowrap;
     flex-flow: row nowrap;
+    align-items: center;
     gap: theme.spacing(1);
     gap: theme.spacing(1);
+    position: relative;
+
+    .spinner {
+      position: absolute;
+      left: -#{theme.spacing(8)};
+    }
 
 
     .selectedPage {
     .selectedPage {
       cursor: text;
       cursor: text;

+ 19 - 0
packages/component-library/src/Paginator/index.tsx

@@ -10,15 +10,18 @@ import type { Props as ButtonProps } from "../Button/index.jsx";
 import { Button } from "../Button/index.jsx";
 import { Button } from "../Button/index.jsx";
 import buttonStyles from "../Button/index.module.scss";
 import buttonStyles from "../Button/index.module.scss";
 import { Select } from "../Select/index.jsx";
 import { Select } from "../Select/index.jsx";
+import { Spinner } from "../Spinner/index.jsx";
 import { Toolbar } from "../unstyled/Toolbar/index.jsx";
 import { Toolbar } from "../unstyled/Toolbar/index.jsx";
 
 
 type Props = {
 type Props = {
   numPages: number;
   numPages: number;
   currentPage: number;
   currentPage: number;
   onPageChange: (newPage: number) => void;
   onPageChange: (newPage: number) => void;
+  isPageTransitioning?: boolean | undefined;
   pageSize: number;
   pageSize: number;
   pageSizeOptions: number[];
   pageSizeOptions: number[];
   onPageSizeChange: (newPageSize: number) => void;
   onPageSizeChange: (newPageSize: number) => void;
+  isPageSizeTransitioning?: boolean | undefined;
   mkPageLink?: ((page: number) => string) | undefined;
   mkPageLink?: ((page: number) => string) | undefined;
   className?: string | undefined;
   className?: string | undefined;
 };
 };
@@ -26,10 +29,12 @@ type Props = {
 export const Paginator = ({
 export const Paginator = ({
   numPages,
   numPages,
   currentPage,
   currentPage,
+  isPageTransitioning,
   pageSize,
   pageSize,
   pageSizeOptions,
   pageSizeOptions,
   onPageChange,
   onPageChange,
   onPageSizeChange,
   onPageSizeChange,
+  isPageSizeTransitioning,
   mkPageLink,
   mkPageLink,
   className,
   className,
 }: Props) => (
 }: Props) => (
@@ -38,6 +43,7 @@ export const Paginator = ({
       pageSize={pageSize}
       pageSize={pageSize}
       pageSizeOptions={pageSizeOptions}
       pageSizeOptions={pageSizeOptions}
       onPageSizeChange={onPageSizeChange}
       onPageSizeChange={onPageSizeChange}
+      isPending={isPageSizeTransitioning}
     />
     />
     {numPages > 1 && (
     {numPages > 1 && (
       <PaginatorToolbar
       <PaginatorToolbar
@@ -45,6 +51,7 @@ export const Paginator = ({
         numPages={numPages}
         numPages={numPages}
         onPageChange={onPageChange}
         onPageChange={onPageChange}
         mkPageLink={mkPageLink}
         mkPageLink={mkPageLink}
+        isPending={isPageTransitioning}
       />
       />
     )}
     )}
   </div>
   </div>
@@ -54,12 +61,14 @@ type PageSizeSelectProps = {
   pageSize: number;
   pageSize: number;
   pageSizeOptions: number[];
   pageSizeOptions: number[];
   onPageSizeChange: (newPageSize: number) => void;
   onPageSizeChange: (newPageSize: number) => void;
+  isPending?: boolean | undefined;
 };
 };
 
 
 const PageSizeSelect = ({
 const PageSizeSelect = ({
   pageSize,
   pageSize,
   onPageSizeChange,
   onPageSizeChange,
   pageSizeOptions,
   pageSizeOptions,
+  isPending,
 }: PageSizeSelectProps) => (
 }: PageSizeSelectProps) => (
   <Select
   <Select
     className={styles.pageSizeSelect ?? ""}
     className={styles.pageSizeSelect ?? ""}
@@ -71,6 +80,7 @@ const PageSizeSelect = ({
     show={(value) => `${value.id.toString()} per page`}
     show={(value) => `${value.id.toString()} per page`}
     variant="ghost"
     variant="ghost"
     size="sm"
     size="sm"
+    isPending={isPending ?? false}
   />
   />
 );
 );
 
 
@@ -79,6 +89,7 @@ type PaginatorProps = {
   currentPage: number;
   currentPage: number;
   onPageChange: (newPage: number) => void;
   onPageChange: (newPage: number) => void;
   mkPageLink: ((page: number) => string) | undefined;
   mkPageLink: ((page: number) => string) | undefined;
+  isPending?: boolean | undefined;
 };
 };
 
 
 const PaginatorToolbar = ({
 const PaginatorToolbar = ({
@@ -86,6 +97,7 @@ const PaginatorToolbar = ({
   currentPage,
   currentPage,
   onPageChange,
   onPageChange,
   mkPageLink,
   mkPageLink,
+  isPending,
 }: PaginatorProps) => {
 }: PaginatorProps) => {
   const first = useMemo(
   const first = useMemo(
     () =>
     () =>
@@ -105,6 +117,13 @@ const PaginatorToolbar = ({
 
 
   return (
   return (
     <Toolbar aria-label="Page" className={styles.paginatorToolbar ?? ""}>
     <Toolbar aria-label="Page" className={styles.paginatorToolbar ?? ""}>
+      {isPending && (
+        <Spinner
+          isIndeterminate
+          label="Loading page..."
+          className={styles.spinner ?? ""}
+        />
+      )}
       <PageSelector
       <PageSelector
         hideText
         hideText
         beforeIcon={<CaretLeft />}
         beforeIcon={<CaretLeft />}

+ 10 - 0
packages/component-library/src/Select/index.module.scss

@@ -1,6 +1,16 @@
 @use "../theme";
 @use "../theme";
 
 
 .select {
 .select {
+  .trigger {
+    width: 100%;
+    justify-content: space-between;
+  }
+
+  .spinner {
+    width: 1em;
+    height: 1em;
+  }
+
   .caret {
   .caret {
     transition: transform 300ms ease;
     transition: transform 300ms ease;
   }
   }

+ 13 - 1
packages/component-library/src/Select/index.tsx

@@ -17,6 +17,7 @@ import styles from "./index.module.scss";
 import type { Props as ButtonProps } from "../Button/index.jsx";
 import type { Props as ButtonProps } from "../Button/index.jsx";
 import { Button } from "../Button/index.jsx";
 import { Button } from "../Button/index.jsx";
 import { DropdownCaretDown } from "../DropdownCaretDown/index.jsx";
 import { DropdownCaretDown } from "../DropdownCaretDown/index.jsx";
+import { Spinner } from "../Spinner/index.jsx";
 import {
 import {
   ListBox,
   ListBox,
   ListBoxItem,
   ListBoxItem,
@@ -88,7 +89,18 @@ export const Select = <T extends { id: string | number }>({
   >
   >
     <Label className={styles.label}>{label}</Label>
     <Label className={styles.label}>{label}</Label>
     <Button
     <Button
-      afterIcon={<DropdownCaretDown className={styles.caret} />}
+      className={styles.trigger ?? ""}
+      afterIcon={
+        isPending ? (
+          <Spinner
+            label="Loading..."
+            isIndeterminate
+            className={styles.spinner ?? ""}
+          />
+        ) : (
+          <DropdownCaretDown className={styles.caret} />
+        )
+      }
       variant={variant}
       variant={variant}
       size={size}
       size={size}
       rounded={rounded}
       rounded={rounded}

+ 1 - 10
pnpm-lock.yaml

@@ -709,9 +709,6 @@ importers:
       clsx:
       clsx:
         specifier: 'catalog:'
         specifier: 'catalog:'
         version: 2.1.1
         version: 2.1.1
-      connectkit:
-        specifier: 'catalog:'
-        version: 1.9.0(@babel/core@7.27.1)(@tanstack/react-query@5.71.5(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react-is@18.3.1)(react@19.1.0)(viem@2.24.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.4))(wagmi@2.14.16(@react-native-async-storage/async-storage@1.24.0(react-native@0.78.2(@babel/core@7.27.1)(@babel/preset-env@7.26.9(@babel/core@7.27.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.71.5)(@tanstack/react-query@5.71.5(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(encoding@0.1.13)(immer@9.0.21)(react@19.1.0)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.24.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.4))(zod@3.24.4))
       next:
       next:
         specifier: 'catalog:'
         specifier: 'catalog:'
         version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)
         version: 15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1)
@@ -730,12 +727,6 @@ importers:
       react-timeago:
       react-timeago:
         specifier: 'catalog:'
         specifier: 'catalog:'
         version: 8.2.0(react@19.1.0)
         version: 8.2.0(react@19.1.0)
-      viem:
-        specifier: 'catalog:'
-        version: 2.24.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.4)
-      wagmi:
-        specifier: 'catalog:'
-        version: 2.14.16(@react-native-async-storage/async-storage@1.24.0(react-native@0.78.2(@babel/core@7.27.1)(@babel/preset-env@7.26.9(@babel/core@7.27.1))(@types/react@19.1.0)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.71.5)(@tanstack/react-query@5.71.5(react@19.1.0))(@types/react@19.1.0)(bufferutil@4.0.9)(encoding@0.1.13)(immer@9.0.21)(react@19.1.0)(typescript@5.8.2)(utf-8-validate@5.0.10)(viem@2.24.3(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.4))(zod@3.24.4)
       zod:
       zod:
         specifier: 'catalog:'
         specifier: 'catalog:'
         version: 3.24.4
         version: 3.24.4
@@ -745,7 +736,7 @@ importers:
         version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2)
         version: 4.0.2(@testing-library/dom@10.4.0)(@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2)))(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2))(turbo@2.4.4)(typescript@5.8.2)
       '@cprussin/jest-config':
       '@cprussin/jest-config':
         specifier: 'catalog:'
         specifier: 'catalog:'
-        version: 2.0.2(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(bufferutil@4.0.9)(eslint@9.23.0(jiti@2.4.2))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@5.0.10)
+        version: 2.0.2(@babel/core@7.27.1)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.1))(bufferutil@4.0.9)(esbuild@0.25.4)(eslint@9.23.0(jiti@2.4.2))(jest@29.7.0(@types/node@22.14.0)(ts-node@10.9.2(@swc/core@1.13.2)(@types/node@22.14.0)(typescript@5.8.2)))(next@15.3.2(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.86.1))(prettier@3.5.3)(typescript@5.8.2)(utf-8-validate@6.0.3)
       '@cprussin/prettier-config':
       '@cprussin/prettier-config':
         specifier: 'catalog:'
         specifier: 'catalog:'
         version: 2.2.2(prettier@3.5.3)
         version: 2.2.2(prettier@3.5.3)