소스 검색

Fix staking app styles:

- Use better formatting for large numbers of tokens
- Improve loading and error states designs
- Add support for small screen sizes / mobile
Connor Prussin 1 년 전
부모
커밋
b28b7a3490
34개의 변경된 파일1079개의 추가작업 그리고 607개의 파일을 삭제
  1. 4 0
      apps/staking/package.json
  2. 19 66
      apps/staking/src/components/AccountHistory/index.tsx
  3. 21 21
      apps/staking/src/components/AccountSummary/index.tsx
  4. 2 14
      apps/staking/src/components/Amplitude/index.tsx
  5. 1 1
      apps/staking/src/components/Button/index.tsx
  6. 8 3
      apps/staking/src/components/Dashboard/index.tsx
  7. 28 8
      apps/staking/src/components/Error/index.tsx
  8. 2 2
      apps/staking/src/components/Footer/index.tsx
  9. 11 14
      apps/staking/src/components/Governance/index.tsx
  10. 3 3
      apps/staking/src/components/Header/index.tsx
  11. 42 81
      apps/staking/src/components/Home/index.tsx
  12. 7 0
      apps/staking/src/components/Loading/index.tsx
  13. 0 5
      apps/staking/src/components/LoadingSpinner/index.tsx
  14. 1 1
      apps/staking/src/components/MaxWidth/index.tsx
  15. 1 1
      apps/staking/src/components/Modal/index.tsx
  16. 5 3
      apps/staking/src/components/NotFound/index.tsx
  17. 166 81
      apps/staking/src/components/OracleIntegrityStaking/index.tsx
  18. 0 232
      apps/staking/src/components/PositionFlowchart/index.tsx
  19. 203 15
      apps/staking/src/components/ProgramSection/index.tsx
  20. 42 16
      apps/staking/src/components/Tokens/index.tsx
  21. 25 0
      apps/staking/src/components/Tooltip/index.tsx
  22. 4 2
      apps/staking/src/components/TransferButton/index.tsx
  23. 9 6
      apps/staking/src/components/WalletButton/index.tsx
  24. 10 2
      apps/staking/src/components/WalletProvider/index.tsx
  25. 56 0
      apps/staking/src/hooks/use-account-history.ts
  26. 17 0
      apps/staking/src/hooks/use-amplitude.ts
  27. 73 0
      apps/staking/src/hooks/use-dashboard-data.ts
  28. 0 11
      apps/staking/src/hooks/use-is-mounted.ts
  29. 1 3
      apps/staking/src/hooks/use-logger.tsx
  30. 19 3
      apps/staking/src/hooks/use-stake-account.tsx
  31. 4 2
      apps/staking/src/hooks/use-transfer.ts
  32. 1 1
      apps/staking/src/tokens.ts
  33. 3 1
      apps/staking/tailwind.config.ts
  34. 291 9
      pnpm-lock.yaml

+ 4 - 0
apps/staking/package.json

@@ -34,10 +34,12 @@
     "@solana/wallet-adapter-wallets": "0.19.10",
     "@solana/web3.js": "^1.95.2",
     "clsx": "^2.1.1",
+    "dnum": "^2.13.1",
     "next": "^14.2.5",
     "pino": "^9.3.2",
     "react": "^18.3.1",
     "react-aria": "^3.34.3",
+    "react-aria-components": "^1.3.3",
     "react-dom": "^18.3.1",
     "recharts": "^2.12.7",
     "swr": "^2.2.5",
@@ -61,6 +63,8 @@
     "postcss": "^8.4.40",
     "prettier": "^3.3.2",
     "tailwindcss": "^3.4.7",
+    "tailwindcss-animate": "^1.0.7",
+    "tailwindcss-react-aria-components": "^1.1.5",
     "typescript": "^5.5.4",
     "vercel": "^35.2.2"
   }

+ 19 - 66
apps/staking/src/components/AccountHistory/index.tsx

@@ -1,32 +1,26 @@
-import useSWR from "swr";
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
 
 import {
   type AccountHistoryAction,
   type StakeDetails,
   AccountHistoryItemType,
   StakeType,
-  loadAccountHistory,
 } from "../../api";
-import { useApiContext } from "../../hooks/use-api-context";
-import { LoadingSpinner } from "../LoadingSpinner";
+import { StateType, useAccountHistory } from "../../hooks/use-account-history";
 import { Tokens } from "../Tokens";
 
-const ONE_SECOND_IN_MS = 1000;
-const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
-const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
-
 export const AccountHistory = () => {
-  const history = useAccountHistoryData();
+  const history = useAccountHistory();
 
   switch (history.type) {
-    case DataStateType.NotLoaded:
-    case DataStateType.Loading: {
-      return <LoadingSpinner />;
+    case StateType.NotLoaded:
+    case StateType.Loading: {
+      return <ArrowPathIcon className="size-6 animate-spin" />;
     }
-    case DataStateType.Error: {
+    case StateType.Error: {
       return <p>Uh oh, an error occured!</p>;
     }
-    case DataStateType.Loaded: {
+    case StateType.Loaded: {
       return (
         <table className="text-sm">
           <thead className="font-medium">
@@ -56,7 +50,9 @@ export const AccountHistory = () => {
               ) => (
                 <tr key={i}>
                   <td className="pr-4">{timestamp.toLocaleString()}</td>
-                  <td className="pr-4">{mkDescription(action)}</td>
+                  <td className="pr-4">
+                    <Description>{action}</Description>
+                  </td>
                   <td className="pr-4">
                     <Tokens>{amount}</Tokens>
                   </td>
@@ -82,8 +78,8 @@ export const AccountHistory = () => {
   }
 };
 
-const mkDescription = (action: AccountHistoryAction): string => {
-  switch (action.type) {
+const Description = ({ children }: { children: AccountHistoryAction }) => {
+  switch (children.type) {
     case AccountHistoryItemType.Claim: {
       return "Rewards claimed";
     }
@@ -91,28 +87,28 @@ const mkDescription = (action: AccountHistoryAction): string => {
       return "Tokens added";
     }
     case AccountHistoryItemType.LockedDeposit: {
-      return `Locked tokens deposited, unlocking ${action.unlockDate.toLocaleString()}`;
+      return `Locked tokens deposited, unlocking ${children.unlockDate.toLocaleString()}`;
     }
     case AccountHistoryItemType.RewardsCredited: {
       return "Rewards credited";
     }
     case AccountHistoryItemType.Slash: {
-      return `Staked tokens slashed from ${action.publisherName}`;
+      return `Staked tokens slashed from ${children.publisherName}`;
     }
     case AccountHistoryItemType.StakeCreated: {
-      return `Created stake position for ${getStakeDetails(action.details)}`;
+      return `Created stake position for ${getStakeDetails(children.details)}`;
     }
     case AccountHistoryItemType.StakeFinishedWarmup: {
-      return `Warmup complete for position for ${getStakeDetails(action.details)}`;
+      return `Warmup complete for position for ${getStakeDetails(children.details)}`;
     }
     case AccountHistoryItemType.Unlock: {
       return "Locked tokens unlocked";
     }
     case AccountHistoryItemType.UnstakeCreated: {
-      return `Requested unstake for position for ${getStakeDetails(action.details)}`;
+      return `Requested unstake for position for ${getStakeDetails(children.details)}`;
     }
     case AccountHistoryItemType.UnstakeExitedCooldown: {
-      return `Cooldown completed for ${getStakeDetails(action.details)}`;
+      return `Cooldown completed for ${getStakeDetails(children.details)}`;
     }
     case AccountHistoryItemType.Withdrawal: {
       return "Tokens withdrawn to wallet";
@@ -130,46 +126,3 @@ const getStakeDetails = (details: StakeDetails): string => {
     }
   }
 };
-
-const useAccountHistoryData = () => {
-  const apiContext = useApiContext();
-
-  const { data, isLoading, ...rest } = useSWR(
-    `${apiContext.stakeAccount.address.toBase58()}/history`,
-    () => loadAccountHistory(apiContext),
-    {
-      refreshInterval: REFRESH_INTERVAL,
-    },
-  );
-  const error = rest.error as unknown;
-
-  if (error) {
-    return DataState.ErrorState(error);
-  } else if (isLoading) {
-    return DataState.Loading();
-  } else if (data) {
-    return DataState.Loaded(data);
-  } else {
-    return DataState.NotLoaded();
-  }
-};
-
-enum DataStateType {
-  NotLoaded,
-  Loading,
-  Loaded,
-  Error,
-}
-const DataState = {
-  NotLoaded: () => ({ type: DataStateType.NotLoaded as const }),
-  Loading: () => ({ type: DataStateType.Loading as const }),
-  Loaded: (data: Awaited<ReturnType<typeof loadAccountHistory>>) => ({
-    type: DataStateType.Loaded as const,
-    data,
-  }),
-  ErrorState: (error: unknown) => ({
-    type: DataStateType.Error as const,
-    error,
-  }),
-};
-type DataState = ReturnType<(typeof DataState)[keyof typeof DataState]>;

+ 21 - 21
apps/staking/src/components/AccountSummary/index.tsx

@@ -47,16 +47,16 @@ export const AccountSummary = ({
     <Image
       src={background}
       alt=""
-      className="absolute -right-40 h-full object-right [mask-image:linear-gradient(to_right,_transparent,_black_50%)]"
+      className="absolute -right-40 hidden h-full object-cover object-right [mask-image:linear-gradient(to_right,_transparent,_black_50%)] md:block"
     />
-    <div className="relative flex flex-col items-start justify-between gap-16 px-12 py-20 md:flex-row md:items-center">
+    <div className="relative flex flex-col items-start justify-between gap-8 px-6 py-10 sm:gap-16 sm:px-12 sm:py-20 lg:flex-row lg:items-center">
       <div>
-        <div className="mb-4 inline-block border border-neutral-600/50 bg-neutral-900 px-4 py-1 text-xs text-neutral-400">
+        <div className="mb-2 inline-block border border-neutral-600/50 bg-neutral-900 px-4 py-1 text-xs text-neutral-400 sm:mb-4">
           Total Balance
         </div>
         <div className="flex flex-row items-center gap-8">
           <span>
-            <Tokens className="text-6xl font-light">{total}</Tokens>
+            <Tokens className="text-4xl font-light sm:text-6xl">{total}</Tokens>
           </span>
           {lastSlash && (
             <p className="max-w-48 text-sm text-red-600">
@@ -65,19 +65,11 @@ export const AccountSummary = ({
             </p>
           )}
         </div>
-        <div className="mt-8 flex flex-row items-center gap-4">
-          <TransferButton
-            actionDescription="Add funds to your balance"
-            actionName="Add Tokens"
-            max={walletAmount}
-            transfer={deposit}
-          />
-        </div>
         {locked > 0n && (
           <>
-            <div className="mt-6 flex flex-row items-center gap-1 text-xl text-pythpurple-100/50">
+            <div className="mt-3 flex flex-row items-center gap-1 text-pythpurple-100/50 sm:mt-6 sm:text-xl">
               <Tokens>{locked}</Tokens>
-              <div>locked</div>
+              <div>locked included</div>
             </div>
             <Modal>
               <ModalButton
@@ -90,7 +82,7 @@ export const AccountSummary = ({
                 title="Unlock Schedule"
                 description="Your tokens will become available for withdrawal and for participation in Integrity Staking according to this schedule"
               >
-                <div className="border border-neutral-600/50 bg-pythpurple-100/10 px-8 py-6">
+                <div className="border border-neutral-600/50 bg-pythpurple-100/10 p-4 sm:px-8 sm:py-6">
                   <table>
                     <thead className="font-medium">
                       <tr>
@@ -101,7 +93,7 @@ export const AccountSummary = ({
                     <tbody>
                       {unlockSchedule.map((unlock, i) => (
                         <tr key={i}>
-                          <td className="pr-12 text-sm opacity-80">
+                          <td className="pr-12 text-xs opacity-80 sm:text-sm">
                             {unlock.date.toLocaleString()}
                           </td>
                           <td>
@@ -116,12 +108,20 @@ export const AccountSummary = ({
             </Modal>
           </>
         )}
+        <div className="mt-3 flex flex-row items-center gap-4 sm:mt-8">
+          <TransferButton
+            actionDescription="Add funds to your balance"
+            actionName="Add Tokens"
+            max={walletAmount}
+            transfer={deposit}
+          />
+        </div>
       </div>
-      <div className="flex flex-col items-stretch gap-4 xl:flex-row">
+      <div className="flex w-full flex-col items-stretch gap-4 lg:w-auto xl:flex-row">
         <BalanceCategory
-          name="Available for Withdrawal"
+          name="Unlocked & Unstaked"
           amount={availableToWithdraw}
-          description="The lesser of the amount you have available to stake in governance & integrity staking"
+          description="The amount of unlocked tokens that are not staked in either program"
           action={
             <TransferButton
               small
@@ -137,7 +137,7 @@ export const AccountSummary = ({
         <BalanceCategory
           name="Available Rewards"
           amount={availableRewards}
-          description="Rewards you have earned but not yet claimed from the Integrity Staking program"
+          description="Rewards you have earned from OIS"
           action={<ClaimButton disabled={availableRewards === 0n} />}
           {...(expiringRewards !== undefined &&
             expiringRewards.amount > 0n && {
@@ -169,7 +169,7 @@ const BalanceCategory = ({
   action,
   warning,
 }: BalanceCategoryProps) => (
-  <div className="flex flex-col justify-between border border-neutral-600/50 bg-pythpurple-800/60 p-6 backdrop-blur">
+  <div className="flex w-full flex-col justify-between border border-neutral-600/50 bg-pythpurple-800/60 p-6 backdrop-blur lg:w-96">
     <div>
       <div className="mb-4 inline-block border border-neutral-600/50 bg-neutral-900 px-4 py-1 text-xs text-neutral-400">
         {name}

+ 2 - 14
apps/staking/src/components/Amplitude/index.tsx

@@ -1,25 +1,13 @@
 "use client";
 
-import * as amplitude from "@amplitude/analytics-browser";
-import { autocapturePlugin } from "@amplitude/plugin-autocapture-browser";
-import { useEffect, useRef } from "react";
+import { useAmplitude } from "../../hooks/use-amplitude";
 
 type Props = {
   apiKey: string | undefined;
 };
 
 export const Amplitude = ({ apiKey }: Props) => {
-  const amplitudeInitialized = useRef(false);
-
-  useEffect(() => {
-    if (!amplitudeInitialized.current && apiKey) {
-      amplitude.add(autocapturePlugin());
-      amplitude.init(apiKey, {
-        defaultTracking: true,
-      });
-      amplitudeInitialized.current = true;
-    }
-  }, [apiKey]);
+  useAmplitude(apiKey);
 
   // eslint-disable-next-line unicorn/no-null
   return null;

+ 1 - 1
apps/staking/src/components/Button/index.tsx

@@ -29,5 +29,5 @@ const ButtonBase = ({
 
 export const Button = Styled(
   ButtonBase,
-  "border border-pythpurple-600 bg-pythpurple-600/50 data-[small]:text-sm data-[small]:px-6 data-[small]:py-1 data-[secondary]:bg-pythpurple-600/20 px-8 py-2 data-[nopad]:px-0 data-[nopad]:py-0 disabled:cursor-not-allowed disabled:bg-neutral-50/10 disabled:border-neutral-50/10 disabled:text-white/60 disabled:data-[loading]:cursor-wait hover:bg-pythpurple-600/60 data-[secondary]:hover:bg-pythpurple-600/60 data-[secondary]:disabled:bg-neutral-50/10 focus-visible:ring-1 focus-visible:ring-pythpurple-400 focus:outline-none justify-center",
+  "border border-pythpurple-600 bg-pythpurple-600/50 data-[small]:text-sm data-[small]:px-6 data-[small]:py-1 data-[secondary]:bg-pythpurple-600/20 px-2 sm:px-4 md:px-8 py-2 data-[nopad]:px-0 data-[nopad]:py-0 disabled:cursor-not-allowed disabled:bg-neutral-50/10 disabled:border-neutral-50/10 disabled:text-white/60 disabled:data-[loading]:cursor-wait hover:bg-pythpurple-600/60 data-[secondary]:hover:bg-pythpurple-600/60 data-[secondary]:disabled:bg-neutral-50/10 focus-visible:ring-1 focus-visible:ring-pythpurple-400 focus:outline-none",
 );

+ 8 - 3
apps/staking/src/components/Dashboard/index.tsx

@@ -118,10 +118,15 @@ export const Dashboard = ({
         expiringRewards={expiringRewards}
       />
       <TabGroup as="section">
-        <TabList className="flex w-full flex-row font-medium">
+        <TabList className="flex w-full flex-row text-sm font-medium sm:text-base">
           <DashboardTab>Overview</DashboardTab>
           <DashboardTab>Governance</DashboardTab>
-          <DashboardTab>Oracle Integrity Staking</DashboardTab>
+          <DashboardTab>
+            <span className="sm:hidden">Integrity Staking</span>
+            <span className="hidden sm:inline">
+              Oracle Integrity Staking (OIS)
+            </span>
+          </DashboardTab>
         </TabList>
         <TabPanels className="mt-8">
           <DashboardTabPanel>
@@ -159,7 +164,7 @@ export const Dashboard = ({
 
 const DashboardTab = Styled(
   Tab,
-  "grow border-b border-neutral-600/50 px-4 py-2 focus-visible:outline-none data-[selected]:cursor-default data-[selected]:border-pythpurple-400 data-[selected]:data-[hover]:bg-transparent data-[hover]:text-pythpurple-400 data-[selected]:text-pythpurple-400 data-[focus]:outline-none data-[focus]:ring-1 data-[focus]:ring-pythpurple-400",
+  "grow basis-0 border-b border-neutral-600/50 px-4 py-2 focus-visible:outline-none data-[selected]:cursor-default data-[selected]:border-pythpurple-400 data-[selected]:data-[hover]:bg-transparent data-[hover]:text-pythpurple-400 data-[selected]:text-pythpurple-400 data-[focus]:outline-none data-[focus]:ring-1 data-[focus]:ring-pythpurple-400",
 );
 
 const DashboardTabPanel = Styled(

+ 28 - 8
apps/staking/src/components/Error/index.tsx

@@ -1,13 +1,33 @@
+import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
+import { useEffect } from "react";
+
+import { useLogger } from "../../hooks/use-logger";
+import { Button } from "../Button";
+
 type Props = {
   error: Error & { digest?: string };
   reset?: () => void;
 };
 
-export const Error = ({ error, reset }: Props) => (
-  <main>
-    <h1>Uh oh!</h1>
-    <h2>Something went wrong</h2>
-    <p>Error Code: {error.digest}</p>
-    {reset && <button onClick={reset}>Reset</button>}
-  </main>
-);
+export const Error = ({ error, reset }: Props) => {
+  const logger = useLogger();
+
+  useEffect(() => {
+    logger.error(error);
+  }, [error, logger]);
+
+  return (
+    <main className="flex size-full flex-col items-center justify-center gap-2 text-center">
+      <div className="mb-10 flex flex-row items-center gap-6">
+        <ExclamationTriangleIcon className="size-16 text-red-700" />
+        <h1 className="text-3xl font-light">Uh oh!</h1>
+      </div>
+      <h2 className="mb-4 text-xl opacity-80">Something went wrong</h2>
+      <div className="text-sm opacity-50">Error Details:</div>
+      <strong className="mb-8 border border-pythpurple-400/20 bg-pythpurple-600/50 px-1 py-0.5 text-sm opacity-50">
+        {error.digest ?? error.message}
+      </strong>
+      {reset && <Button onClick={reset}>Reset</Button>}
+    </main>
+  );
+};

+ 2 - 2
apps/staking/src/components/Footer/index.tsx

@@ -42,7 +42,7 @@ export const Footer = ({
     {...props}
   >
     <div className="border-x border-t border-neutral-600/50 bg-pythpurple-800">
-      <MaxWidth className="-mx-4 flex h-16 items-center justify-between">
+      <MaxWidth className="flex h-16 items-center justify-between sm:-mx-4">
         <div>© 2024 Pyth Data Association</div>
         <div className="relative -right-3 flex h-full items-center">
           {SOCIAL_LINKS.map(({ name, icon: Icon, href }) => (
@@ -50,7 +50,7 @@ export const Footer = ({
               target="_blank"
               href={href}
               key={name}
-              className="grid h-full place-content-center px-3 hover:text-pythpurple-400"
+              className="grid h-full place-content-center px-2 hover:text-pythpurple-400 sm:px-3"
               rel="noreferrer"
             >
               <Icon className="size-4" />

+ 11 - 14
apps/staking/src/components/Governance/index.tsx

@@ -23,19 +23,16 @@ export const Governance = ({
   <ProgramSection
     name="Governance"
     description="Vote and Influence the Network"
-    positions={{
-      available: availableToStake,
-      warmup,
-      staked,
-      cooldown,
-      cooldown2,
-      stake: stakeGovernance,
-      stakeDescription: "Stake funds to participate in governance votes",
-      cancelWarmup: cancelWarmupGovernance,
-      cancelWarmupDescription:
-        "Cancel staking tokens for governance that are currently in warmup",
-      unstake: unstakeGovernance,
-      unstakeDescription: "Unstake tokens from the Governance program",
-    }}
+    available={availableToStake}
+    warmup={warmup}
+    staked={staked}
+    cooldown={cooldown}
+    cooldown2={cooldown2}
+    stake={stakeGovernance}
+    stakeDescription="Stake funds to participate in governance votes"
+    cancelWarmup={cancelWarmupGovernance}
+    cancelWarmupDescription="Cancel staking tokens for governance that are currently in warmup"
+    unstake={unstakeGovernance}
+    unstakeDescription="Unstake tokens from the Governance program"
   />
 );

+ 3 - 3
apps/staking/src/components/Header/index.tsx

@@ -16,9 +16,9 @@ export const Header = ({
     {...props}
   >
     <div className="border-x border-b border-neutral-600/50 bg-pythpurple-800">
-      <MaxWidth className="-mx-4 flex h-16 items-center justify-between">
-        <Logo className="h-full py-4 text-pythpurple-100" />
-        <WalletButton />
+      <MaxWidth className="flex h-16 items-center justify-between gap-8 sm:-mx-4">
+        <Logo className="max-h-full py-4 text-pythpurple-100" />
+        <WalletButton className="flex-none" />
       </MaxWidth>
     </div>
   </header>

+ 42 - 81
apps/staking/src/components/Home/index.tsx

@@ -3,53 +3,53 @@
 import { useWalletModal } from "@solana/wallet-adapter-react-ui";
 import { useCallback } from "react";
 import { useIsSSR } from "react-aria";
-import useSWR from "swr";
 
-import { loadData } from "../../api";
-import { useApiContext } from "../../hooks/use-api-context";
-import { useLogger } from "../../hooks/use-logger";
-import { StateType, useStakeAccount } from "../../hooks/use-stake-account";
+import {
+  StateType as DashboardDataStateType,
+  useDashboardData,
+} from "../../hooks/use-dashboard-data";
+import {
+  StateType as StakeAccountStateType,
+  useStakeAccount,
+} from "../../hooks/use-stake-account";
 import { Button } from "../Button";
 import { Dashboard } from "../Dashboard";
-import { LoadingSpinner } from "../LoadingSpinner";
-
-const ONE_SECOND_IN_MS = 1000;
-const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
-const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+import { Error as ErrorPage } from "../Error";
+import { Loading } from "../Loading";
 
 export const Home = () => {
   const isSSR = useIsSSR();
 
-  return (
-    <main className="mx-4 my-6">
-      {isSSR ? <LoadingSpinner /> : <MountedHome />}
-    </main>
-  );
+  return isSSR ? <Loading /> : <MountedHome />;
 };
 
 const MountedHome = () => {
   const stakeAccountState = useStakeAccount();
 
   switch (stakeAccountState.type) {
-    case StateType.Initialized:
-    case StateType.Loading: {
-      return <LoadingSpinner />;
+    case StakeAccountStateType.Initialized:
+    case StakeAccountStateType.Loading: {
+      return <Loading />;
     }
-    case StateType.NoAccounts: {
-      return <p>No stake account found for your wallet!</p>;
+    case StakeAccountStateType.NoAccounts: {
+      return (
+        <main className="my-20">
+          <p>No stake account found for your wallet!</p>
+        </main>
+      );
     }
-    case StateType.NoWallet: {
+    case StakeAccountStateType.NoWallet: {
       return <NoWalletHome />;
     }
-    case StateType.Error: {
+    case StakeAccountStateType.Error: {
       return (
-        <p>
-          Uh oh, an error occurred while loading stake accounts. Please refresh
-          and try again
-        </p>
+        <ErrorPage
+          error={stakeAccountState.error}
+          reset={stakeAccountState.reset}
+        />
       );
     }
-    case StateType.Loaded: {
+    case StakeAccountStateType.Loaded: {
       return <StakeAccountLoadedHome />;
     }
   }
@@ -62,7 +62,7 @@ const NoWalletHome = () => {
   }, [modal]);
 
   return (
-    <>
+    <main className="my-20">
       <h1 className="mb-8 mt-16 text-center text-4xl font-semibold text-pythpurple-400">
         Staking & Delegating
       </h1>
@@ -74,68 +74,29 @@ const NoWalletHome = () => {
       <div className="grid w-full place-content-center">
         <Button onClick={showModal}>Connect your wallet to participate</Button>
       </div>
-    </>
+    </main>
   );
 };
 
 const StakeAccountLoadedHome = () => {
   const data = useDashboardData();
-  const logger = useLogger();
 
   switch (data.type) {
-    case DataStateType.NotLoaded:
-    case DataStateType.Loading: {
-      return <LoadingSpinner />;
-    }
-    case DataStateType.Error: {
-      logger.error(data.error);
-      return <p>Uh oh, an error occured!</p>;
+    case DashboardDataStateType.NotLoaded:
+    case DashboardDataStateType.Loading: {
+      return <Loading />;
     }
-    case DataStateType.Loaded: {
-      return <Dashboard {...data.data} />;
-    }
-  }
-};
-
-const useDashboardData = () => {
-  const apiContext = useApiContext();
 
-  const { data, isLoading, ...rest } = useSWR(
-    apiContext.stakeAccount.address.toBase58(),
-    () => loadData(apiContext),
-    {
-      refreshInterval: REFRESH_INTERVAL,
-    },
-  );
-  const error = rest.error as unknown;
+    case DashboardDataStateType.Error: {
+      return <ErrorPage error={data.error} reset={data.reset} />;
+    }
 
-  if (error) {
-    return DataState.ErrorState(error);
-  } else if (isLoading) {
-    return DataState.Loading();
-  } else if (data) {
-    return DataState.Loaded(data);
-  } else {
-    return DataState.NotLoaded();
+    case DashboardDataStateType.Loaded: {
+      return (
+        <main className="mx-4 my-6">
+          <Dashboard {...data.data} />
+        </main>
+      );
+    }
   }
 };
-
-enum DataStateType {
-  NotLoaded,
-  Loading,
-  Loaded,
-  Error,
-}
-const DataState = {
-  NotLoaded: () => ({ type: DataStateType.NotLoaded as const }),
-  Loading: () => ({ type: DataStateType.Loading as const }),
-  Loaded: (data: Awaited<ReturnType<typeof loadData>>) => ({
-    type: DataStateType.Loaded as const,
-    data,
-  }),
-  ErrorState: (error: unknown) => ({
-    type: DataStateType.Error as const,
-    error,
-  }),
-};
-type DataState = ReturnType<(typeof DataState)[keyof typeof DataState]>;

+ 7 - 0
apps/staking/src/components/Loading/index.tsx

@@ -0,0 +1,7 @@
+export const Loading = () => (
+  <main className="flex size-full animate-pulse flex-row items-center justify-center gap-8">
+    <div className="size-6 animate-bounce border border-pythpurple-400/70 bg-pythpurple-950" />
+    <div className="size-6 animate-bounce border border-pythpurple-400/70 bg-pythpurple-950 delay-150" />
+    <div className="size-6 animate-bounce border border-pythpurple-400/70 bg-pythpurple-950 delay-300" />
+  </main>
+);

+ 0 - 5
apps/staking/src/components/LoadingSpinner/index.tsx

@@ -1,5 +0,0 @@
-import { ArrowPathIcon } from "@heroicons/react/24/outline";
-
-import { Styled } from "../Styled";
-
-export const LoadingSpinner = Styled(ArrowPathIcon, "size-6 animate-spin");

+ 1 - 1
apps/staking/src/components/MaxWidth/index.tsx

@@ -1,3 +1,3 @@
 import { Styled } from "../Styled";
 
-export const MaxWidth = Styled("div", "px-12");
+export const MaxWidth = Styled("div", "w-full px-6 sm:px-12 overflow-hidden");

+ 1 - 1
apps/staking/src/components/Modal/index.tsx

@@ -118,7 +118,7 @@ export const RawModal = ({
         <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
           <DialogPanel
             transition
-            className="relative border border-neutral-600/50 bg-[#100E21] px-10 py-12 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
+            className="relative border border-neutral-600/50 bg-[#100E21] px-6 pb-8 pt-12 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:px-10 sm:pb-12"
           >
             <DialogTitle as="h2" className="text-3xl font-light leading-6">
               {title}

+ 5 - 3
apps/staking/src/components/NotFound/index.tsx

@@ -1,15 +1,17 @@
 import Link from "next/link";
 
+import { Button } from "../Button";
+
 export const NotFound = () => (
   <main className="grid size-full place-content-center text-center">
-    <h1 className="mb-8 text-6xl font-semibold text-pythpurple-600 dark:text-pythpurple-400">
+    <h1 className="mb-8 text-6xl font-semibold text-pythpurple-400">
       Not Found
     </h1>
     <p className="mb-20 text-lg font-medium">
       {"The page you're looking for isn't here"}
     </p>
-    <Link className="place-self-center px-24 py-3" href="/">
+    <Button as={Link} className="place-self-center px-24 py-3" href="/">
       Go Home
-    </Link>
+    </Button>
   </main>
 );

+ 166 - 81
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -8,7 +8,6 @@ import {
   unstakeIntegrityStaking,
   calculateApy,
 } from "../../api";
-import { PositionFlowchart } from "../PositionFlowchart";
 import { ProgramSection } from "../ProgramSection";
 import { SparkChart } from "../SparkChart";
 import { StakingTimeline } from "../StakingTimeline";
@@ -47,80 +46,96 @@ export const OracleIntegrityStaking = ({
 
   return (
     <ProgramSection
-      name="Oracle Integrity Staking"
-      description="Protect DeFi, Earn Yield"
-      className="pb-0"
-      positions={{
-        locked,
-        available: availableToStake,
-        warmup,
-        staked,
-        cooldown,
-        cooldown2,
-        className: "mb-8",
-      }}
+      name="Oracle Integrity Staking (OIS)"
+      description="Protect DeFi"
+      className="pb-0 sm:pb-0"
+      available={availableToStake}
+      warmup={warmup}
+      staked={staked}
+      cooldown={cooldown}
+      cooldown2={cooldown2}
+      {...(locked > 0n && {
+        availableToStakeDetails: (
+          <div className="mt-2 text-xs text-red-600">
+            <Tokens>{locked}</Tokens> are locked and cannot be staked in OIS
+          </div>
+        ),
+      })}
     >
       {self && (
-        <div className="-mx-10 border-t border-neutral-600/50 py-16">
-          <table className="mx-auto border border-neutral-600/50 text-sm">
-            <caption className="mb-4 ml-10 text-2xl font-light">
+        <div className="relative -mx-4 mt-6 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 sm:mt-10">
+          <div className="relative w-full overflow-x-auto">
+            <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
               You ({self.name})
-            </caption>
-            <thead className="bg-pythpurple-400/30 font-light">
+            </h3>
+
+            <table className="mx-auto border border-neutral-600/50 text-sm">
+              <thead className="bg-pythpurple-400/30 font-light">
+                <tr>
+                  <PublisherTableHeader>Pool</PublisherTableHeader>
+                  <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
+                  <PublisherTableHeader>Historical APY</PublisherTableHeader>
+                  <PublisherTableHeader>Number of feeds</PublisherTableHeader>
+                  <PublisherTableHeader>Quality ranking</PublisherTableHeader>
+                  {availableToStake > 0n && <PublisherTableHeader />}
+                </tr>
+              </thead>
+              <tbody className="bg-pythpurple-400/10">
+                <Publisher
+                  isSelf
+                  availableToStake={availableToStake}
+                  publisher={self}
+                  totalStaked={staked}
+                />
+              </tbody>
+            </table>
+          </div>
+        </div>
+      )}
+      <div
+        className={clsx(
+          "relative -mx-4 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 lg:mt-10",
+          { "mt-6": self === undefined },
+        )}
+      >
+        <div className="relative w-full overflow-x-auto">
+          <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
+            {self ? "Other Publishers" : "Publishers"}
+          </h3>
+
+          <table className="min-w-full text-sm">
+            <thead className="bg-pythpurple-100/30 font-light">
               <tr>
+                <PublisherTableHeader className="pl-4 text-left sm:pl-10">
+                  Publisher
+                </PublisherTableHeader>
+                <PublisherTableHeader>Self stake</PublisherTableHeader>
                 <PublisherTableHeader>Pool</PublisherTableHeader>
-                <PublisherTableHeader>APY</PublisherTableHeader>
+                <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
                 <PublisherTableHeader>Historical APY</PublisherTableHeader>
                 <PublisherTableHeader>Number of feeds</PublisherTableHeader>
-                <PublisherTableHeader>Quality ranking</PublisherTableHeader>
-                {availableToStake > 0n && <PublisherTableHeader />}
+                <PublisherTableHeader
+                  className={clsx({ "pr-4 sm:pr-10": availableToStake <= 0n })}
+                >
+                  Quality ranking
+                </PublisherTableHeader>
+                {availableToStake > 0n && (
+                  <PublisherTableHeader className="pr-4 sm:pr-10" />
+                )}
               </tr>
             </thead>
-            <tbody className="bg-pythpurple-400/10">
-              <Publisher
-                isSelf
-                availableToStake={availableToStake}
-                publisher={self}
-              />
+            <tbody className="bg-white/5">
+              {otherPublishers.map((publisher) => (
+                <Publisher
+                  key={publisher.publicKey}
+                  availableToStake={availableToStake}
+                  publisher={publisher}
+                  totalStaked={staked}
+                />
+              ))}
             </tbody>
           </table>
         </div>
-      )}
-      <div className="-mx-10 border-t border-neutral-600/50 pt-4">
-        <table className="w-full text-sm">
-          <caption className="mb-4 ml-10 text-left text-2xl font-light">
-            {self ? "Other Publishers" : "Publishers"}
-          </caption>
-          <thead className="bg-pythpurple-100/30 font-light">
-            <tr>
-              <PublisherTableHeader className="pl-10 text-left">
-                Publisher
-              </PublisherTableHeader>
-              <PublisherTableHeader>Self stake</PublisherTableHeader>
-              <PublisherTableHeader>Pool</PublisherTableHeader>
-              <PublisherTableHeader>APY</PublisherTableHeader>
-              <PublisherTableHeader>Historical APY</PublisherTableHeader>
-              <PublisherTableHeader>Number of feeds</PublisherTableHeader>
-              <PublisherTableHeader
-                className={clsx({ "pr-10": availableToStake <= 0n })}
-              >
-                Quality ranking
-              </PublisherTableHeader>
-              {availableToStake > 0n && (
-                <PublisherTableHeader className="pr-10" />
-              )}
-            </tr>
-          </thead>
-          <tbody className="bg-white/5">
-            {otherPublishers.map((publisher) => (
-              <Publisher
-                key={publisher.publicKey}
-                availableToStake={availableToStake}
-                publisher={publisher}
-              />
-            ))}
-          </tbody>
-        </table>
       </div>
     </ProgramSection>
   );
@@ -133,6 +148,7 @@ const PublisherTableHeader = Styled(
 
 type PublisherProps = {
   availableToStake: bigint;
+  totalStaked: bigint;
   isSelf?: boolean;
   publisher: {
     name: string;
@@ -155,7 +171,29 @@ type PublisherProps = {
   };
 };
 
-const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => {
+const Publisher = ({
+  publisher,
+  availableToStake,
+  totalStaked,
+  isSelf,
+}: PublisherProps) => {
+  const warmup = useMemo(
+    () =>
+      publisher.positions?.warmup !== undefined &&
+      publisher.positions.warmup > 0n
+        ? publisher.positions.warmup
+        : undefined,
+    [publisher.positions?.warmup],
+  );
+  const staked = useMemo(
+    () =>
+      publisher.positions?.staked !== undefined &&
+      publisher.positions.staked > 0n
+        ? publisher.positions.staked
+        : undefined,
+    [publisher.positions?.staked],
+  );
+
   const cancelWarmup = useTransferActionForPublisher(
     cancelWarmupIntegrityStaking,
     publisher.publicKey,
@@ -174,7 +212,7 @@ const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => {
       <tr className="border-t border-neutral-600/50 first:border-0">
         {!isSelf && (
           <>
-            <PublisherTableCell className="py-4 pl-10 font-medium">
+            <PublisherTableCell className="py-4 pl-4 font-medium sm:pl-10">
               {publisher.name}
             </PublisherTableCell>
             <PublisherTableCell className="text-center">
@@ -239,14 +277,14 @@ const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => {
         </PublisherTableCell>
         <PublisherTableCell
           className={clsx("text-center", {
-            "pr-10": availableToStake <= 0n && !isSelf,
+            "pr-4 sm:pr-10": availableToStake <= 0n && !isSelf,
           })}
         >
           {publisher.qualityRanking}
         </PublisherTableCell>
         {availableToStake > 0 && (
           <PublisherTableCell
-            className={clsx("text-right", { "pr-10": !isSelf })}
+            className={clsx("text-right", { "pr-4 sm:pr-10": !isSelf })}
           >
             <StakeToPublisherButton
               availableToStake={availableToStake}
@@ -259,22 +297,69 @@ const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => {
           </PublisherTableCell>
         )}
       </tr>
-      {publisher.positions && (
+      {(warmup !== undefined || staked !== undefined) && (
         <tr>
           <td colSpan={8} className="border-separate border-spacing-8">
-            <div className="mx-auto w-full px-20 pb-8">
-              <PositionFlowchart
-                small
-                className="mx-auto w-[56rem]"
-                warmup={publisher.positions.warmup ?? 0n}
-                staked={publisher.positions.staked ?? 0n}
-                cooldown={publisher.positions.cooldown ?? 0n}
-                cooldown2={publisher.positions.cooldown2 ?? 0n}
-                cancelWarmup={cancelWarmup}
-                cancelWarmupDescription={`Cancel tokens that are in warmup for staking to ${publisher.name}`}
-                unstake={unstake}
-                unstakeDescription={`Unstake tokens from ${publisher.name}`}
-              />
+            <div className="mx-auto mb-8 mt-4 w-[30rem] border border-neutral-600/50 bg-pythpurple-800 px-8 py-6">
+              <table className="w-full">
+                <caption className="mb-2 text-left text-lg font-light">
+                  Your Positions
+                </caption>
+                <tbody>
+                  {warmup !== undefined && (
+                    <tr>
+                      <td className="opacity-80">Warmup</td>
+                      <td className="px-4">
+                        <Tokens>{warmup}</Tokens>
+                      </td>
+                      <td
+                        className={clsx("text-right", {
+                          "pb-2": staked !== undefined,
+                        })}
+                      >
+                        <TransferButton
+                          small
+                          secondary
+                          className="w-28"
+                          actionDescription={`Cancel tokens that are in warmup for staking to ${publisher.name}`}
+                          actionName="Cancel"
+                          submitButtonText="Cancel Warmup"
+                          title="Cancel Warmup"
+                          max={warmup}
+                          transfer={cancelWarmup}
+                        />
+                      </td>
+                    </tr>
+                  )}
+                  {staked !== undefined && (
+                    <tr>
+                      <td className="opacity-80">Staked</td>
+                      <td className="px-4">
+                        <div className="flex items-center gap-2">
+                          <Tokens>{staked}</Tokens>
+                          <div className="text-xs opacity-60">
+                            ({Number((100n * staked) / totalStaked)}% of your
+                            staked tokens)
+                          </div>
+                        </div>
+                      </td>
+                      <td className="py-0.5 text-right">
+                        <TransferButton
+                          small
+                          secondary
+                          className="w-28"
+                          actionDescription={`Unstake tokens from ${publisher.name}`}
+                          actionName="Unstake"
+                          max={staked}
+                          transfer={unstake}
+                        >
+                          <StakingTimeline cooldownOnly />
+                        </TransferButton>
+                      </td>
+                    </tr>
+                  )}
+                </tbody>
+              </table>
             </div>
           </td>
         </tr>

+ 0 - 232
apps/staking/src/components/PositionFlowchart/index.tsx

@@ -1,232 +0,0 @@
-import { ArrowLongRightIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-import type { HTMLAttributes, ReactNode, ComponentProps } from "react";
-
-import { getUpcomingEpoch, getNextFullEpoch } from "../../api";
-import { StakingTimeline } from "../StakingTimeline";
-import { Tokens } from "../Tokens";
-import { TransferButton } from "../TransferButton";
-
-type Props = HTMLAttributes<HTMLDivElement> & {
-  locked?: bigint | undefined;
-  warmup: bigint;
-  staked: bigint;
-  cooldown: bigint;
-  cooldown2: bigint;
-  small?: boolean | undefined;
-} & (
-    | {
-        stake?: never;
-        stakeDescription?: never;
-        available?: bigint | undefined;
-      }
-    | {
-        available: bigint;
-        stake: ComponentProps<typeof TransferButton>["transfer"] | undefined;
-        stakeDescription: string;
-      }
-  ) &
-  (
-    | { cancelWarmup?: never; cancelWarmupDescription?: never }
-    | {
-        cancelWarmup:
-          | ComponentProps<typeof TransferButton>["transfer"]
-          | undefined;
-        cancelWarmupDescription: string;
-      }
-  ) &
-  (
-    | { unstake?: never; unstakeDescription?: never }
-    | {
-        unstake: ComponentProps<typeof TransferButton>["transfer"] | undefined;
-        unstakeDescription: string;
-      }
-  );
-
-export const PositionFlowchart = ({
-  className,
-  small,
-  locked,
-  available,
-  warmup,
-  staked,
-  cooldown,
-  cooldown2,
-  stake,
-  stakeDescription,
-  cancelWarmup,
-  cancelWarmupDescription,
-  unstake,
-  unstakeDescription,
-  ...props
-}: Props) => (
-  <div
-    className={clsx("flex flex-row items-stretch justify-center", className)}
-    {...props}
-  >
-    {locked !== undefined && (
-      <Position
-        name="Locked"
-        className="mr-12"
-        small={small}
-        nameClassName="bg-red-950"
-      >
-        {locked}
-      </Position>
-    )}
-    {available !== undefined && (
-      <>
-        <Position
-          name="Available to Stake"
-          small={small}
-          nameClassName="bg-[rgba(43,_129,_167,_0.25)]"
-          {...(stake !== undefined &&
-            available > 0n && {
-              actions: (
-                <TransferButton
-                  small
-                  actionDescription={stakeDescription}
-                  actionName="Stake"
-                  max={available}
-                  transfer={stake}
-                >
-                  <StakingTimeline />
-                </TransferButton>
-              ),
-            })}
-        >
-          {available}
-        </Position>
-        <Arrow />
-      </>
-    )}
-    <Position
-      name="Warmup"
-      small={small}
-      nameClassName="bg-[rgba(206,_153,_247,_0.25)]"
-      {...(warmup > 0n && {
-        details: (
-          <div className="mt-2 text-xs text-neutral-500">
-            Staking {getUpcomingEpoch().toLocaleString()}
-          </div>
-        ),
-        ...(cancelWarmup !== undefined && {
-          actions: (
-            <TransferButton
-              small
-              secondary
-              actionDescription={cancelWarmupDescription}
-              actionName="Cancel"
-              submitButtonText="Cancel Warmup"
-              title="Cancel Warmup"
-              max={warmup}
-              transfer={cancelWarmup}
-            />
-          ),
-        }),
-      })}
-    >
-      {warmup}
-    </Position>
-    <Arrow />
-    <Position
-      name="Staked"
-      small={small}
-      nameClassName="bg-[rgba(105,_24,_238,_0.25)]"
-      {...(unstake !== undefined &&
-        staked > 0n && {
-          actions: (
-            <TransferButton
-              small
-              secondary
-              actionDescription={unstakeDescription}
-              actionName="Unstake"
-              max={staked}
-              transfer={unstake}
-            >
-              <StakingTimeline cooldownOnly />
-            </TransferButton>
-          ),
-        })}
-    >
-      {staked}
-    </Position>
-    <Arrow />
-    <Position
-      name="Cooldown"
-      small={small}
-      nameClassName="bg-[rgba(179,_157,_222,_0.25)]"
-      details={
-        <>
-          {cooldown > 0n && (
-            <div className="mt-2 text-xs text-neutral-500">
-              <Tokens>{cooldown}</Tokens> end{" "}
-              {getUpcomingEpoch().toLocaleString()}
-            </div>
-          )}
-          {cooldown2 > 0n && (
-            <div className="mt-2 text-xs text-neutral-500">
-              <Tokens>{cooldown2}</Tokens> end{" "}
-              {getNextFullEpoch().toLocaleString()}
-            </div>
-          )}
-        </>
-      }
-    >
-      {cooldown + cooldown2}
-    </Position>
-  </div>
-);
-
-type PositionProps = {
-  name: string;
-  small: boolean | undefined;
-  nameClassName?: string | undefined;
-  className?: string | undefined;
-  children: bigint;
-  actions?: ReactNode | ReactNode[];
-  details?: ReactNode;
-};
-
-const Position = ({
-  name,
-  small,
-  nameClassName,
-  details,
-  className,
-  children,
-  actions,
-}: PositionProps) => (
-  <div
-    className={clsx(
-      "flex w-full flex-col justify-between gap-6 overflow-hidden border border-neutral-600/50 bg-pythpurple-800",
-      small ? "p-4" : "p-6",
-      className,
-    )}
-  >
-    <div>
-      <div
-        className={clsx(
-          "mb-2 inline-block border border-neutral-600/50 text-xs text-neutral-400",
-          small ? "px-1 py-0.5" : "px-3 py-1",
-          nameClassName,
-        )}
-      >
-        {name}
-      </div>
-      <div>
-        <Tokens className={clsx("font-light", small ? "text-lg" : "text-3xl")}>
-          {children}
-        </Tokens>
-      </div>
-      {details}
-    </div>
-    {actions && <div>{actions}</div>}
-  </div>
-);
-
-const Arrow = () => (
-  <div className="grid place-content-center">
-    <ArrowLongRightIcon className="mx-4 size-4 flex-none scale-x-[200%] [mask-image:linear-gradient(to_right,_transparent,_black_125%)]" />
-  </div>
-);

+ 203 - 15
apps/staking/src/components/ProgramSection/index.tsx

@@ -1,38 +1,226 @@
+import { ArrowLongDownIcon } from "@heroicons/react/24/outline";
 import clsx from "clsx";
-import type { HTMLAttributes, ComponentProps } from "react";
+import type { HTMLAttributes, ReactNode, ComponentProps } from "react";
 
-import { PositionFlowchart } from "../PositionFlowchart";
+import { getUpcomingEpoch, getNextFullEpoch } from "../../api";
+import { StakingTimeline } from "../StakingTimeline";
+import { Tokens } from "../Tokens";
+import { TransferButton } from "../TransferButton";
 
-type Props = HTMLAttributes<HTMLElement> & {
+type Props = HTMLAttributes<HTMLDivElement> & {
   name: string;
   description: string;
-  positions: ComponentProps<typeof PositionFlowchart>;
-};
+  warmup: bigint;
+  staked: bigint;
+  cooldown: bigint;
+  cooldown2: bigint;
+  availableToStakeDetails?: ReactNode | ReactNode[] | undefined;
+} & (
+    | {
+        stake?: never;
+        stakeDescription?: never;
+        available?: bigint | undefined;
+      }
+    | {
+        available: bigint;
+        stake: ComponentProps<typeof TransferButton>["transfer"] | undefined;
+        stakeDescription: string;
+      }
+  ) &
+  (
+    | { cancelWarmup?: never; cancelWarmupDescription?: never }
+    | {
+        cancelWarmup:
+          | ComponentProps<typeof TransferButton>["transfer"]
+          | undefined;
+        cancelWarmupDescription: string;
+      }
+  ) &
+  (
+    | { unstake?: never; unstakeDescription?: never }
+    | {
+        unstake: ComponentProps<typeof TransferButton>["transfer"] | undefined;
+        unstakeDescription: string;
+      }
+  );
 
 export const ProgramSection = ({
   className,
   name,
   description,
   children,
-  positions,
+  warmup,
+  staked,
+  cooldown,
+  cooldown2,
+  availableToStakeDetails,
+  stake,
+  stakeDescription,
+  available,
+  cancelWarmup,
+  cancelWarmupDescription,
+  unstake,
+  unstakeDescription,
   ...props
 }: Props) => (
   <section
     className={clsx(
-      "border border-neutral-600/50 bg-pythpurple-800 p-10",
+      "border border-neutral-600/50 bg-pythpurple-800 px-4 py-6 sm:p-10",
       className,
     )}
     {...props}
   >
-    <h2 className="text-3xl font-light">{name}</h2>
-    <p>{description}</p>
-    <PositionFlowchart
-      {...positions}
-      className={clsx(
-        "mt-8 border border-neutral-600/50 bg-white/5 p-10",
-        positions.className,
+    <h2 className="text-xl font-light sm:text-3xl">{name}</h2>
+    <p className="text-sm sm:text-base">{description}</p>
+    <div className="mt-8 flex flex-col items-stretch justify-center border border-neutral-600/50 bg-white/5 px-2 py-6 sm:p-10 xl:flex-row">
+      {available !== undefined && (
+        <>
+          <Position
+            name="Available to Stake"
+            nameClassName="bg-[rgba(43,_129,_167,_0.25)]"
+            details={availableToStakeDetails}
+            {...(stake !== undefined &&
+              available > 0n && {
+                actions: (
+                  <TransferButton
+                    small
+                    actionDescription={stakeDescription}
+                    actionName="Stake"
+                    max={available}
+                    transfer={stake}
+                  >
+                    <StakingTimeline />
+                  </TransferButton>
+                ),
+              })}
+          >
+            {available}
+          </Position>
+          <Arrow />
+        </>
       )}
-    />
+      <Position
+        name="Warmup"
+        nameClassName="bg-[rgba(206,_153,_247,_0.25)]"
+        {...(warmup > 0n && {
+          details: (
+            <div className="mt-2 text-xs text-neutral-500">
+              Staking {getUpcomingEpoch().toLocaleString()}
+            </div>
+          ),
+          ...(cancelWarmup !== undefined && {
+            actions: (
+              <TransferButton
+                small
+                secondary
+                actionDescription={cancelWarmupDescription}
+                actionName="Cancel"
+                submitButtonText="Cancel Warmup"
+                title="Cancel Warmup"
+                max={warmup}
+                transfer={cancelWarmup}
+              />
+            ),
+          }),
+        })}
+      >
+        {warmup}
+      </Position>
+      <Arrow />
+      <Position
+        name="Staked"
+        nameClassName="bg-[rgba(105,_24,_238,_0.25)]"
+        {...(unstake !== undefined &&
+          staked > 0n && {
+            actions: (
+              <TransferButton
+                small
+                secondary
+                actionDescription={unstakeDescription}
+                actionName="Unstake"
+                max={staked}
+                transfer={unstake}
+              >
+                <StakingTimeline cooldownOnly />
+              </TransferButton>
+            ),
+          })}
+      >
+        {staked}
+      </Position>
+      <Arrow />
+      <Position
+        name="Cooldown"
+        nameClassName="bg-[rgba(179,_157,_222,_0.25)]"
+        details={
+          <>
+            {cooldown > 0n && (
+              <div className="mt-2 text-xs text-neutral-500">
+                <Tokens>{cooldown}</Tokens> end{" "}
+                {getUpcomingEpoch().toLocaleString()}
+              </div>
+            )}
+            {cooldown2 > 0n && (
+              <div className="mt-2 text-xs text-neutral-500">
+                <Tokens>{cooldown2}</Tokens> end{" "}
+                {getNextFullEpoch().toLocaleString()}
+              </div>
+            )}
+          </>
+        }
+      >
+        {cooldown + cooldown2}
+      </Position>
+    </div>
     {children}
   </section>
 );
+
+type PositionProps = {
+  name: string;
+  nameClassName?: string | undefined;
+  className?: string | undefined;
+  children: bigint;
+  actions?: ReactNode | ReactNode[];
+  details?: ReactNode;
+};
+
+const Position = ({
+  name,
+  nameClassName,
+  details,
+  className,
+  children,
+  actions,
+}: PositionProps) => (
+  <div
+    className={clsx(
+      "w-full overflow-hidden border border-neutral-600/50 bg-pythpurple-800 p-4 sm:p-6",
+      className,
+    )}
+  >
+    <div
+      className={clsx(
+        "mb-2 inline-block border border-neutral-600/50 px-1 py-0.5 text-xs text-neutral-400 sm:px-3 sm:py-1",
+        nameClassName,
+      )}
+    >
+      {name}
+    </div>
+    <div className="flex flex-row items-end justify-between gap-6 xl:flex-col xl:items-start">
+      <div>
+        <div>
+          <Tokens className="text-xl font-light sm:text-3xl">{children}</Tokens>
+        </div>
+        {details}
+      </div>
+      {actions && <div>{actions}</div>}
+    </div>
+  </div>
+);
+
+const Arrow = () => (
+  <div className="grid place-content-center">
+    <ArrowLongDownIcon className="m-4 size-4 flex-none scale-y-[200%] [mask-image:linear-gradient(to_bottom,_transparent,_black_125%)] xl:-rotate-90" />
+  </div>
+);

+ 42 - 16
apps/staking/src/components/Tokens/index.tsx

@@ -1,26 +1,52 @@
 import clsx from "clsx";
-import { useMemo, type HTMLAttributes } from "react";
+import * as dnum from "dnum";
+import { type ComponentProps, useMemo } from "react";
+import { Button, TooltipTrigger } from "react-aria-components";
 
 import Pyth from "./pyth.svg";
-import { tokensToString } from "../../tokens";
+import { DECIMALS } from "../../tokens";
+import { Tooltip } from "../Tooltip";
 
-type Props = Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
+type Props = Omit<ComponentProps<typeof Button>, "children"> & {
   children: bigint;
 };
 
-export const Tokens = ({ children, className, ...props }: Props) => {
-  const value = useMemo(() => tokensToString(children), [children]);
+export const Tokens = ({ children, ...props }: Props) => {
+  const compactValue = useMemo(
+    () => dnum.format([children, DECIMALS], { compact: true }),
+    [children],
+  );
+  const fullValue = useMemo(
+    () => dnum.format([children, DECIMALS]),
+    [children],
+  );
 
-  return (
-    <span
-      className={clsx(
-        "inline-flex items-center gap-[0.25em] align-top",
-        className,
-      )}
-      {...props}
-    >
-      <Pyth className="aspect-square size-[1em]" />
-      <span>{value}</span>
-    </span>
+  return compactValue === fullValue ? (
+    <TokenButton {...props}>{compactValue}</TokenButton>
+  ) : (
+    <TooltipTrigger delay={0}>
+      <TokenButton {...props}>{compactValue}</TokenButton>
+      <Tooltip className="flex flex-row items-center gap-[0.25em]">
+        <Pyth className="aspect-square size-[1em]" />
+        <span>{fullValue}</span>
+      </Tooltip>
+    </TooltipTrigger>
   );
 };
+
+type TokenButtonProps = Omit<ComponentProps<typeof Button>, "children"> & {
+  children: string;
+};
+
+const TokenButton = ({ children, className, ...props }: TokenButtonProps) => (
+  <Button
+    className={clsx(
+      "inline-flex cursor-default items-center gap-[0.25em] align-top active:outline-none focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
+      className,
+    )}
+    {...props}
+  >
+    <Pyth className="aspect-square size-[1em]" />
+    <span>{children}</span>
+  </Button>
+);

+ 25 - 0
apps/staking/src/components/Tooltip/index.tsx

@@ -0,0 +1,25 @@
+import clsx from "clsx";
+import type { ComponentProps } from "react";
+import { OverlayArrow, Tooltip as TooltipImpl } from "react-aria-components";
+
+type Props = Omit<ComponentProps<typeof TooltipImpl>, "children"> & {
+  children: React.ReactNode;
+};
+
+export const Tooltip = ({ children, className, offset, ...props }: Props) => (
+  <TooltipImpl
+    className={clsx(
+      "border border-neutral-900 bg-neutral-200 px-2 py-1 text-sm text-neutral-900 shadow shadow-white/50 transition data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:fade-in data-[exiting]:fade-out data-[entering]:slide-in-from-bottom",
+      className,
+    )}
+    offset={offset ?? 10}
+    {...props}
+  >
+    <OverlayArrow>
+      <svg width={8} height={8} viewBox="0 0 8 8" className="fill-neutral-200">
+        <path d="M0 0 L4 4 L8 0" />
+      </svg>
+    </OverlayArrow>
+    {children}
+  </TooltipImpl>
+);

+ 4 - 2
apps/staking/src/components/TransferButton/index.tsx

@@ -82,7 +82,7 @@ export const TransferButton = ({
       >
         {(close) => (
           <>
-            <Field className="mb-8 flex w-full min-w-96 flex-col gap-1">
+            <Field className="mb-8 flex w-full flex-col gap-1 sm:min-w-96">
               <div className="flex flex-row items-center justify-between">
                 <Label className="text-sm">Amount</Label>
                 <div className="flex flex-row items-center gap-2">
@@ -112,7 +112,9 @@ export const TransferButton = ({
                 </div>
               </div>
               {state.type === StateType.Error && (
-                <p>Uh oh, an error occurred!</p>
+                <p className="mt-1 text-red-600">
+                  Uh oh, an error occurred! Please try again
+                </p>
               )}
             </Field>
             {children && (

+ 9 - 6
apps/staking/src/components/WalletButton/index.tsx

@@ -81,7 +81,7 @@ const ConnectedButton = (props: Props) => {
       <Menu as="div" className="relative">
         <MenuButton as="div" className="group">
           <ButtonComponent
-            className="w-52 group-data-[open]:bg-pythpurple-600/60"
+            className="group-data-[open]:bg-pythpurple-600/60"
             {...props}
           >
             <span className="truncate">
@@ -93,7 +93,7 @@ const ConnectedButton = (props: Props) => {
         <MenuItems
           transition
           anchor="bottom end"
-          className="z-10 flex min-w-[var(--button-width)] origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus-visible:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
+          className="z-10 flex min-w-[var(--button-width)] origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] data-[closed]:scale-95 data-[closed]:opacity-0 focus-visible:outline-none"
         >
           <MenuSection className="flex w-full flex-col">
             {stakeAccountState.type === StateType.Loaded &&
@@ -107,7 +107,7 @@ const ConnectedButton = (props: Props) => {
                     <ChevronRightIcon className="size-4" />
                   </WalletMenuItem>
                   <ListboxOptions
-                    className="z-10 flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus-visible:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
+                    className="z-10 flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] data-[closed]:scale-95 data-[closed]:opacity-0 focus-visible:outline-none"
                     anchor="left start"
                     transition
                   >
@@ -201,7 +201,7 @@ const WalletMenuItemImpl = <T extends ElementType>(
   return (
     <Component
       className={clsx(
-        "flex items-center gap-2 whitespace-nowrap px-4 py-2 text-left hover:bg-pythpurple-800/20 data-[focus]:bg-pythpurple-800/20",
+        "flex items-center gap-2 whitespace-nowrap px-4 py-2 text-left data-[focus]:bg-pythpurple-800/20 hover:bg-pythpurple-800/20",
         className,
       )}
       ref={ref}
@@ -221,7 +221,7 @@ const DisconnectedButton = (props: Props) => {
   }, [modal]);
 
   return (
-    <ButtonComponent onClick={showModal} className="w-52" {...props}>
+    <ButtonComponent onClick={showModal} {...props}>
       <span>Connect wallet</span>
     </ButtonComponent>
   );
@@ -241,7 +241,10 @@ const ButtonComponent = ({
   ...props
 }: ButtonComponentProps) => (
   <Button
-    className={clsx("flex flex-row items-center gap-2", className)}
+    className={clsx(
+      "flex w-36 flex-row items-center justify-center gap-2 text-sm sm:w-52 sm:text-base",
+      className,
+    )}
     {...props}
   >
     <WalletIcon className="size-4 flex-none opacity-60" />

+ 10 - 2
apps/staking/src/components/WalletProvider/index.tsx

@@ -7,6 +7,10 @@ import {
 } from "@solana/wallet-adapter-react";
 import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
 import {
+  BraveWalletAdapter,
+  BackpackWalletAdapter,
+  CoinbaseWalletAdapter,
+  PhantomWalletAdapter,
   GlowWalletAdapter,
   LedgerWalletAdapter,
   SolflareWalletAdapter,
@@ -31,10 +35,14 @@ export const WalletProvider = ({
   walletConnectProjectId,
   rpc,
 }: Props) => {
-  const endpoint = useMemo(() => rpc ?? clusterApiUrl(network), [rpc]);
+  const endpoint = useMemo(() => rpc ?? clusterApiUrl(network), [rpc, network]);
 
   const wallets = useMemo(
     () => [
+      new BraveWalletAdapter(),
+      new BackpackWalletAdapter(),
+      new CoinbaseWalletAdapter(),
+      new PhantomWalletAdapter(),
       new GlowWalletAdapter(),
       new LedgerWalletAdapter(),
       new SolflareWalletAdapter(),
@@ -57,7 +65,7 @@ export const WalletProvider = ({
           ]
         : []),
     ],
-    [walletConnectProjectId],
+    [walletConnectProjectId, network],
   );
 
   return (

+ 56 - 0
apps/staking/src/hooks/use-account-history.ts

@@ -0,0 +1,56 @@
+import useSWR from "swr";
+
+import { useApiContext } from "./use-api-context";
+import { loadAccountHistory } from "../api";
+
+const ONE_SECOND_IN_MS = 1000;
+const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
+const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+
+export const getCacheKey = ({
+  stakeAccount,
+}: ReturnType<typeof useApiContext>) =>
+  `${stakeAccount.address.toBase58()}/history`;
+
+export const useAccountHistory = () => {
+  const apiContext = useApiContext();
+
+  const { data, isLoading, ...rest } = useSWR(
+    getCacheKey(apiContext),
+    () => loadAccountHistory(apiContext),
+    {
+      refreshInterval: REFRESH_INTERVAL,
+    },
+  );
+  const error = rest.error as unknown;
+
+  if (error) {
+    return State.ErrorState(error);
+  } else if (isLoading) {
+    return State.Loading();
+  } else if (data) {
+    return State.Loaded(data);
+  } else {
+    return State.NotLoaded();
+  }
+};
+
+export enum StateType {
+  NotLoaded,
+  Loading,
+  Loaded,
+  Error,
+}
+const State = {
+  NotLoaded: () => ({ type: StateType.NotLoaded as const }),
+  Loading: () => ({ type: StateType.Loading as const }),
+  Loaded: (data: Awaited<ReturnType<typeof loadAccountHistory>>) => ({
+    type: StateType.Loaded as const,
+    data,
+  }),
+  ErrorState: (error: unknown) => ({
+    type: StateType.Error as const,
+    error,
+  }),
+};
+type State = ReturnType<(typeof State)[keyof typeof State]>;

+ 17 - 0
apps/staking/src/hooks/use-amplitude.ts

@@ -0,0 +1,17 @@
+import * as amplitude from "@amplitude/analytics-browser";
+import { autocapturePlugin } from "@amplitude/plugin-autocapture-browser";
+import { useEffect, useRef } from "react";
+
+export const useAmplitude = (apiKey: string | undefined) => {
+  const amplitudeInitialized = useRef(false);
+
+  useEffect(() => {
+    if (!amplitudeInitialized.current && apiKey) {
+      amplitude.add(autocapturePlugin());
+      amplitude.init(apiKey, {
+        defaultTracking: true,
+      });
+      amplitudeInitialized.current = true;
+    }
+  }, [apiKey]);
+};

+ 73 - 0
apps/staking/src/hooks/use-dashboard-data.ts

@@ -0,0 +1,73 @@
+import { useCallback } from "react";
+import useSWR from "swr";
+
+import { useApiContext } from "./use-api-context";
+import { loadData } from "../api";
+
+const ONE_SECOND_IN_MS = 1000;
+const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
+const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+
+export const getCacheKey = ({
+  stakeAccount,
+}: ReturnType<typeof useApiContext>) => stakeAccount.address.toBase58();
+
+export const useDashboardData = () => {
+  const apiContext = useApiContext();
+
+  const { data, isLoading, mutate, ...rest } = useSWR(
+    getCacheKey(apiContext),
+    () => loadData(apiContext),
+    {
+      refreshInterval: REFRESH_INTERVAL,
+    },
+  );
+  const error = rest.error as unknown;
+
+  const reset = useCallback(() => {
+    mutate(undefined).catch(() => {
+      /* no-op */
+    });
+  }, [mutate]);
+
+  if (error) {
+    return State.ErrorState(new LoadDashboardDataError(error), reset);
+  } else if (isLoading) {
+    return State.Loading();
+  } else if (data) {
+    return State.Loaded(data);
+  } else {
+    return State.NotLoaded();
+  }
+};
+
+export enum StateType {
+  NotLoaded,
+  Loading,
+  Loaded,
+  Error,
+}
+
+const State = {
+  NotLoaded: () => ({ type: StateType.NotLoaded as const }),
+  Loading: () => ({ type: StateType.Loading as const }),
+  Loaded: (data: Awaited<ReturnType<typeof loadData>>) => ({
+    type: StateType.Loaded as const,
+    data,
+  }),
+  ErrorState: (error: LoadDashboardDataError, reset: () => void) => ({
+    type: StateType.Error as const,
+    error,
+    reset,
+  }),
+};
+
+type State = ReturnType<(typeof State)[keyof typeof State]>;
+
+class LoadDashboardDataError extends Error {
+  constructor(cause: unknown) {
+    super(cause instanceof Error ? cause.message : "");
+    this.name = "LoadDashboardDataError";
+    this.cause = cause;
+  }
+}

+ 0 - 11
apps/staking/src/hooks/use-is-mounted.ts

@@ -1,11 +0,0 @@
-import { useState, useEffect } from "react";
-
-export const useIsMounted = () => {
-  const [mounted, setMounted] = useState(false);
-
-  useEffect(() => {
-    setMounted(true);
-  }, []);
-
-  return mounted;
-};

+ 1 - 3
apps/staking/src/hooks/use-logger.tsx

@@ -3,8 +3,6 @@
 import pino, { type Logger } from "pino";
 import { type ComponentProps, createContext, useContext, useMemo } from "react";
 
-import { IS_PRODUCTION_BUILD } from "../config/isomorphic";
-
 const LoggerContext = createContext<undefined | Logger<string>>(undefined);
 
 type LoggerContextProps = Omit<
@@ -19,7 +17,7 @@ export const LoggerProvider = ({ config, ...props }: LoggerContextProps) => {
     () =>
       pino({
         ...config,
-        browser: { ...config?.browser, disabled: IS_PRODUCTION_BUILD },
+        browser: { ...config?.browser },
       }),
     [config],
   );

+ 19 - 3
apps/staking/src/hooks/use-stake-account.tsx

@@ -38,7 +38,11 @@ const State = {
     allAccounts,
     selectAccount,
   }),
-  ErrorState: (error: unknown) => ({ type: StateType.Error as const, error }),
+  ErrorState: (error: LoadStakeAccountError, reset: () => void) => ({
+    type: StateType.Error as const,
+    error,
+    reset,
+  }),
 };
 
 type State = ReturnType<(typeof State)[keyof typeof State]>;
@@ -70,7 +74,7 @@ const useStakeAccountState = () => {
     [setState],
   );
 
-  useEffect(() => {
+  const reset = useCallback(() => {
     if (wallet.connected && !wallet.disconnecting && !loading.current) {
       loading.current = true;
       setState(State.Loading());
@@ -101,7 +105,7 @@ const useStakeAccountState = () => {
           }
         })
         .catch((error: unknown) => {
-          setState(State.ErrorState(error));
+          setState(State.ErrorState(new LoadStakeAccountError(error), reset));
         })
         .finally(() => {
           loading.current = false;
@@ -109,6 +113,10 @@ const useStakeAccountState = () => {
     }
   }, [connection, setAccount, wallet]);
 
+  useEffect(() => {
+    reset();
+  }, [reset]);
+
   return wallet.connected && !wallet.disconnecting ? state : State.NoWallet();
 };
 
@@ -121,6 +129,14 @@ export const useStakeAccount = () => {
   }
 };
 
+class LoadStakeAccountError extends Error {
+  constructor(cause: unknown) {
+    super(cause instanceof Error ? cause.message : "");
+    this.name = "LoadStakeAccountError";
+    this.cause = cause;
+  }
+}
+
 class NotInitializedError extends Error {
   constructor() {
     super(

+ 4 - 2
apps/staking/src/hooks/use-transfer.ts

@@ -1,7 +1,9 @@
 import { useState, useCallback } from "react";
 import { useSWRConfig } from "swr";
 
+import { getCacheKey as getAccountHistoryCacheKey } from "./use-account-history";
 import { useApiContext } from "./use-api-context";
+import { getCacheKey as getDashboardDataCacheKey } from "./use-dashboard-data";
 
 export const useTransfer = (
   transfer: (context: ReturnType<typeof useApiContext>) => Promise<void>,
@@ -21,8 +23,8 @@ export const useTransfer = (
       // TODO enable mutate without awaiting?
       // Prob by changing `api.ts` to encode the change & history item along with each update?
       await Promise.all([
-        mutate(context.stakeAccount.address.toBase58()),
-        mutate(`${context.stakeAccount.address.toBase58()}/history`),
+        mutate(getDashboardDataCacheKey(context)),
+        mutate(getAccountHistoryCacheKey(context)),
       ]);
       setState(State.Complete());
     } catch (error: unknown) {

+ 1 - 1
apps/staking/src/tokens.ts

@@ -1,4 +1,4 @@
-const DECIMALS = 6;
+export const DECIMALS = 6;
 
 export const tokensToString = (value: bigint): string => {
   const asStr = value.toString();

+ 3 - 1
apps/staking/tailwind.config.ts

@@ -1,10 +1,12 @@
 import forms from "@tailwindcss/forms";
 import type { Config } from "tailwindcss";
+import animate from "tailwindcss-animate";
+import reactAria from "tailwindcss-react-aria-components";
 
 const tailwindConfig = {
   darkMode: "class",
   content: ["src/components/**/*.{ts,tsx}", "src/markdown-components.tsx"],
-  plugins: [forms],
+  plugins: [forms, animate, reactAria],
   theme: {
     extend: {
       backgroundImage: {

+ 291 - 9
pnpm-lock.yaml

@@ -68,7 +68,7 @@ importers:
         version: 2.1.1
       connectkit:
         specifier: ^1.8.2
-        version: 1.8.2(@babel/core@7.24.7)(@tanstack/react-query@5.45.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))
+        version: 1.8.2(m5fu6jwi7nvuqo5lp7m3jyfehy)
       cryptocurrency-icons:
         specifier: ^0.18.1
         version: 0.18.1
@@ -357,6 +357,9 @@ importers:
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
+      dnum:
+        specifier: ^2.13.1
+        version: 2.13.1
       next:
         specifier: ^14.2.5
         version: 14.2.6(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -369,6 +372,9 @@ importers:
       react-aria:
         specifier: ^3.34.3
         version: 3.34.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      react-aria-components:
+        specifier: ^1.3.3
+        version: 1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       react-dom:
         specifier: ^18.3.1
         version: 18.3.1(react@18.3.1)
@@ -433,6 +439,12 @@ importers:
       tailwindcss:
         specifier: ^3.4.7
         version: 3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))
+      tailwindcss-animate:
+        specifier: ^1.0.7
+        version: 1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))
+      tailwindcss-react-aria-components:
+        specifier: ^1.1.5
+        version: 1.1.5(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))
       typescript:
         specifier: ^5.5.4
         version: 5.5.4
@@ -1438,7 +1450,7 @@ importers:
         version: 0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.3)
       '@matterlabs/hardhat-zksync':
         specifier: ^1.1.0
-        version: 1.1.0(@matterlabs/hardhat-zksync-deploy@0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-ethers@1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-node@1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-solc@0.3.17(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-upgradable@1.5.2(bufferutil@4.0.7)(encoding@0.1.13)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(@matterlabs/hardhat-zksync-verify@1.6.0(@nomicfoundation/hardhat-verify@2.0.9(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)
+        version: 1.1.0(w3kf4mhrlhkqd2mcprd2ix74zu)
       '@matterlabs/hardhat-zksync-deploy':
         specifier: ^0.6.6
         version: 0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)))
@@ -6319,6 +6331,18 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/collections@3.0.0-alpha.4':
+    resolution: {integrity: sha512-chMNAlsubnpErBWN7sLhmAMOnE7o17hSfq3s0VDHlvRN9K/mPOPlYokmyWkkPqi7fYiR50EPVHDtwTWLJoqfnw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
+  '@react-aria/color@3.0.0-rc.2':
+    resolution: {integrity: sha512-h4P7LocDEHPOEWgHYb8VPJLRGkyMhcsXemmvGao6G23zGTpTX8Nr6pEuJhcXQlGWt8hXvj/ASnC750my+zb1yA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/combobox@3.10.3':
     resolution: {integrity: sha512-EdDwr2Rp1xy7yWjOYHt2qF1IpAtUrkaNKZJzlIw1XSwcqizQY6E8orNPdZr6ZwD6/tgujxF1N71JTKyffrR0Xw==}
     peerDependencies:
@@ -6515,11 +6539,22 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/toolbar@3.0.0-beta.8':
+    resolution: {integrity: sha512-nMlA1KK54/Kohb3HlHAzobg69PVIEr8Q1j5P3tLd9apY8FgGvnz7yLpcj6kO1GA872gseEzgiO0Rzk+yRHQRCA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/tooltip@3.7.7':
     resolution: {integrity: sha512-UOTTDbbUz7OaE48VjNSWl+XQbYCUs5Gss4I3Tv1pfRLXzVtGYXv3ur/vRayvZR0xd12ANY26fZPNkSmCFpmiXw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/tree@3.0.0-alpha.5':
+    resolution: {integrity: sha512-6JtkvQ/KQNFyqxc5M6JMVY63heHt2gZAwXxEt+Ojx/sbWDtDb5RrZVgkb44n7R/tMrFPJEiYZLMFPbGCsUQeJQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/utils@3.24.1':
     resolution: {integrity: sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==}
     peerDependencies:
@@ -6530,6 +6565,12 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/virtualizer@4.0.2':
+    resolution: {integrity: sha512-HNhpZl53UM2Z8g0DNvjAW7aZRwOReYgKRxdTF/IlYHNMLpdqWZinKwLbxZCsbgX3SCjdIGns90YhkMSKVpfrpw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/visually-hidden@3.8.15':
     resolution: {integrity: sha512-l+sJ7xTdD5Sd6+rDNDaeJCSPnHOsI+BaJyApvb/YcVgHa7rB47lp6TXCWUCDItcPY4JqRGyeByRJVrtzBFTWCw==}
     peerDependencies:
@@ -6653,11 +6694,21 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/color@3.7.2':
+    resolution: {integrity: sha512-tNJ7pQjBqXtfASdLRjIYzeI8q0b3JtxqkJbusyEEdLAumpcWkbOvl3Vp9un0Bu/XXWihDa4v2dEdpKxjM+pPxg==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-stately/combobox@3.9.2':
     resolution: {integrity: sha512-ZsbAcD58IvxZqwYxg9d2gOf8R/k5RUB2TPUiGKD6wgWfEKH6SDzY3bgRByHGOyMCyJB62cHjih/ZShizNTguqA==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/data@3.11.6':
+    resolution: {integrity: sha512-S8q1Ejuhijl8SnyVOdDNFrMrWWnLk/Oh1ZT3KHSbTdpfMRtvhi5HukoiP06jlzz75phnpSPQL40npDtUB/kk3Q==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-stately/datepicker@3.10.2':
     resolution: {integrity: sha512-pa5IZUw+49AyOnddwu4XwU2kI5eo/1thbiIVNHP8uDpbbBrBkquSk3zVFDAGX1cu/I1U2VUkt64U/dxgkwaMQw==}
     peerDependencies:
@@ -6681,6 +6732,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/layout@4.0.2':
+    resolution: {integrity: sha512-g3IOrYQcaWxWKW44fYCOLoLMYKEmoOAcT9vQIbgK8MLTQV9Zgt9sGREwn4WJPm85N58Ij6yP72aQ7og/PSymvg==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-stately/list@3.10.8':
     resolution: {integrity: sha512-rHCiPLXd+Ry3ztR9DkLA5FPQeH4Zd4/oJAEDWJ77W3oBBOdiMp3ZdHDLP7KBRh17XGNLO/QruYoHWAQTPiMF4g==}
     peerDependencies:
@@ -6761,6 +6817,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/virtualizer@4.0.2':
+    resolution: {integrity: sha512-LiSr6E6OoL/cKVFO088zEzkNGj41g02nlOAgLluYONncNEjoYiHmb8Yw0otPgViVLKiFjO6Kk4W+dbt8EZ51Ag==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-types/breadcrumbs@3.7.7':
     resolution: {integrity: sha512-ZmhXwD2LLzfEA2OvOCp/QvXu8A/Edsrn5q0qUDGsmOZj9SCVeT82bIv8P+mQnATM13mi2gyoik6102Jc1OscJA==}
     peerDependencies:
@@ -6781,6 +6842,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-types/color@3.0.0-rc.1':
+    resolution: {integrity: sha512-aw6FzrBlZTWKrFaFskM7e3AFICe6JqH10wO0E919goa3LZDDFbyYEwRpatwjIyiZH1elEUkFPgwqpv3ZcPPn8g==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-types/combobox@3.12.1':
     resolution: {integrity: sha512-bd5YwHZWtgnJx4jGbplWbYzXj7IbO5w3IY5suNR7r891rx6IktquZ8GQwyYH0pQ/x+X5LdK2xI59i6+QC2PmlA==}
     peerDependencies:
@@ -6796,6 +6862,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-types/form@3.7.6':
+    resolution: {integrity: sha512-lhS2y1bVtRnyYjkM+ylJUp2g663ZNbeZxu2o+mFfD5c2wYmVLA58IWR90c7DL8IVUitoANnZ1JPhhXvutiFpQQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-types/grid@3.2.8':
     resolution: {integrity: sha512-6PJrpukwMqlv3IhJSDkJuVbhHM8Oe6hd2supWqd9adMXrlSP7QHt9a8SgFcFblCCTx8JzUaA0PvY5sTudcEtOQ==}
     peerDependencies:
@@ -11537,6 +11608,9 @@ packages:
   dlv@1.1.3:
     resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
 
+  dnum@2.13.1:
+    resolution: {integrity: sha512-4oZ+BtlvNtKFJji1Fc5073LyJFvgioBQ0PNu/C+r1A8P09Yvka/aXYYD5bsUHMTUPEu01iv4bk+5nPQmt5AA8A==}
+
   docker-modem@1.0.9:
     resolution: {integrity: sha512-lVjqCSCIAUDZPAZIeyM125HXfNvOmYYInciphNrLrylUtKyW66meAjSPXWchKVzoIYZx69TPnAepVSSkeawoIw==}
     engines: {node: '>= 0.8'}
@@ -12846,6 +12920,9 @@ packages:
     resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
     engines: {node: '>= 0.6'}
 
+  from-exponential@1.1.1:
+    resolution: {integrity: sha512-VBE7f5OVnYwdgB3LHa+Qo29h8qVpxhVO9Trlc+AWm+/XNAgks1tAwMFHb33mjeiof77GglsJzeYF7OqXrROP/A==}
+
   fs-constants@1.0.0:
     resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
 
@@ -17032,6 +17109,12 @@ packages:
     resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
     hasBin: true
 
+  react-aria-components@1.3.3:
+    resolution: {integrity: sha512-wNjcoyIFTL14Z07OJ1I5m37CYB+1oH2DW8PIgZQjGt9lLcYKKEBLSgsenHVKu1F1L9tqlpXgYk5TeXCzU/xUKw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   react-aria@3.34.3:
     resolution: {integrity: sha512-wSprEI5EojDFCm357MxnKAxJZN68OYIt6UH6N0KCo6MEUAVZMbhMSmGYjw/kLK4rI7KrbJDqGqUMQkwc93W9Ng==}
     peerDependencies:
@@ -17147,6 +17230,11 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
 
+  react-stately@3.32.2:
+    resolution: {integrity: sha512-pDSrbCIJtir4HeSa//PTqLSR7Tl7pFC9usmkkBObNKktObQq3Vdgkf46cxeTD1ov7J7GDdR3meIyjXGnZoEzUg==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   react-transition-group@4.4.5:
     resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
     peerDependencies:
@@ -18353,6 +18441,16 @@ packages:
     resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
     engines: {node: '>=10.0.0'}
 
+  tailwindcss-animate@1.0.7:
+    resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
+    peerDependencies:
+      tailwindcss: '>=3.0.0 || insiders'
+
+  tailwindcss-react-aria-components@1.1.5:
+    resolution: {integrity: sha512-0qNr/RlKe5MtDMGMsCFbCoQzpURzEg1raQgnssvOyOWtIIpqleu0lwj8KVRbiZGh8DvX9fNxT+GnfYdiNLJ+Bw==}
+    peerDependencies:
+      tailwindcss: '*'
+
   tailwindcss@3.2.4:
     resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
     engines: {node: '>=12.13.0'}
@@ -28066,8 +28164,8 @@ snapshots:
       - encoding
       - supports-color
 
-  ? '@matterlabs/hardhat-zksync@1.1.0(@matterlabs/hardhat-zksync-deploy@0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-ethers@1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-node@1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-solc@0.3.17(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-upgradable@1.5.2(bufferutil@4.0.7)(encoding@0.1.13)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(@matterlabs/hardhat-zksync-verify@1.6.0(@nomicfoundation/hardhat-verify@2.0.9(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)'
-  : dependencies:
+  '@matterlabs/hardhat-zksync@1.1.0(w3kf4mhrlhkqd2mcprd2ix74zu)':
+    dependencies:
       '@matterlabs/hardhat-zksync-deploy': 0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)))
       '@matterlabs/hardhat-zksync-ethers': 1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)))
       '@matterlabs/hardhat-zksync-node': 1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))
@@ -29670,6 +29768,34 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/collections@3.0.0-alpha.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-aria/ssr': 3.9.5(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+      use-sync-external-store: 1.2.0(react@18.3.1)
+
+  '@react-aria/color@3.0.0-rc.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-aria/i18n': 3.12.2(react@18.3.1)
+      '@react-aria/interactions': 3.22.2(react@18.3.1)
+      '@react-aria/numberfield': 3.11.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/slider': 3.7.11(react@18.3.1)
+      '@react-aria/spinbutton': 3.6.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/textfield': 3.14.8(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-aria/visually-hidden': 3.8.15(react@18.3.1)
+      '@react-stately/color': 3.7.2(react@18.3.1)
+      '@react-stately/form': 3.0.5(react@18.3.1)
+      '@react-types/color': 3.0.0-rc.1(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   '@react-aria/combobox@3.10.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
       '@react-aria/i18n': 3.12.2(react@18.3.1)
@@ -30110,6 +30236,15 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/toolbar@3.0.0-beta.8(react@18.3.1)':
+    dependencies:
+      '@react-aria/focus': 3.18.2(react@18.3.1)
+      '@react-aria/i18n': 3.12.2(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-aria/tooltip@3.7.7(react@18.3.1)':
     dependencies:
       '@react-aria/focus': 3.18.2(react@18.3.1)
@@ -30121,6 +30256,19 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/tree@3.0.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-aria/gridlist': 3.9.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/i18n': 3.12.2(react@18.3.1)
+      '@react-aria/selection': 3.19.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-stately/tree': 3.8.4(react@18.3.1)
+      '@react-types/button': 3.9.6(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   '@react-aria/utils@3.24.1(react@18.3.1)':
     dependencies:
       '@react-aria/ssr': 3.9.4(react@18.3.1)
@@ -30139,6 +30287,17 @@ snapshots:
       clsx: 2.1.1
       react: 18.3.1
 
+  '@react-aria/virtualizer@4.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-aria/i18n': 3.12.2(react@18.3.1)
+      '@react-aria/interactions': 3.22.2(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-stately/virtualizer': 4.0.2(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   '@react-aria/visually-hidden@3.8.15(react@18.3.1)':
     dependencies:
       '@react-aria/interactions': 3.22.2(react@18.3.1)
@@ -30581,6 +30740,20 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/color@3.7.2(react@18.3.1)':
+    dependencies:
+      '@internationalized/number': 3.5.3
+      '@internationalized/string': 3.2.3
+      '@react-aria/i18n': 3.12.2(react@18.3.1)
+      '@react-stately/form': 3.0.5(react@18.3.1)
+      '@react-stately/numberfield': 3.9.6(react@18.3.1)
+      '@react-stately/slider': 3.5.7(react@18.3.1)
+      '@react-stately/utils': 3.10.3(react@18.3.1)
+      '@react-types/color': 3.0.0-rc.1(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-stately/combobox@3.9.2(react@18.3.1)':
     dependencies:
       '@react-stately/collections': 3.10.9(react@18.3.1)
@@ -30594,6 +30767,12 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/data@3.11.6(react@18.3.1)':
+    dependencies:
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-stately/datepicker@3.10.2(react@18.3.1)':
     dependencies:
       '@internationalized/date': 3.5.5
@@ -30632,6 +30811,17 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/layout@4.0.2(react@18.3.1)':
+    dependencies:
+      '@react-stately/collections': 3.10.9(react@18.3.1)
+      '@react-stately/table': 3.12.2(react@18.3.1)
+      '@react-stately/virtualizer': 4.0.2(react@18.3.1)
+      '@react-types/grid': 3.2.8(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@react-types/table': 3.10.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-stately/list@3.10.8(react@18.3.1)':
     dependencies:
       '@react-stately/collections': 3.10.9(react@18.3.1)
@@ -30761,6 +30951,13 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/virtualizer@4.0.2(react@18.3.1)':
+    dependencies:
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-types/breadcrumbs@3.7.7(react@18.3.1)':
     dependencies:
       '@react-types/link': 3.5.7(react@18.3.1)
@@ -30783,6 +30980,12 @@ snapshots:
       '@react-types/shared': 3.24.1(react@18.3.1)
       react: 18.3.1
 
+  '@react-types/color@3.0.0-rc.1(react@18.3.1)':
+    dependencies:
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@react-types/slider': 3.7.5(react@18.3.1)
+      react: 18.3.1
+
   '@react-types/combobox@3.12.1(react@18.3.1)':
     dependencies:
       '@react-types/shared': 3.24.1(react@18.3.1)
@@ -30802,6 +31005,11 @@ snapshots:
       '@react-types/shared': 3.24.1(react@18.3.1)
       react: 18.3.1
 
+  '@react-types/form@3.7.6(react@18.3.1)':
+    dependencies:
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      react: 18.3.1
+
   '@react-types/grid@3.2.8(react@18.3.1)':
     dependencies:
       '@react-types/shared': 3.24.1(react@18.3.1)
@@ -34908,8 +35116,8 @@ snapshots:
 
   '@vue/shared@3.4.34': {}
 
-  ? '@wagmi/connectors@5.0.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.3)(@wagmi/core@2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)'
-  : dependencies:
+  '@wagmi/connectors@5.0.16(mehtb7r3xxh3anmscqllj3vxmi)':
+    dependencies:
       '@coinbase/wallet-sdk': 4.0.3
       '@metamask/sdk': 0.26.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(utf-8-validate@5.0.10)
       '@safe-global/safe-apps-provider': 0.18.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8)
@@ -37774,8 +37982,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  ? connectkit@1.8.2(@babel/core@7.24.7)(@tanstack/react-query@5.45.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))
-  : dependencies:
+  connectkit@1.8.2(m5fu6jwi7nvuqo5lp7m3jyfehy):
+    dependencies:
       '@tanstack/react-query': 5.45.1(react@18.3.1)
       buffer: 6.0.3
       detect-browser: 5.3.0
@@ -38679,6 +38887,10 @@ snapshots:
 
   dlv@1.1.3: {}
 
+  dnum@2.13.1:
+    dependencies:
+      from-exponential: 1.1.1
+
   docker-modem@1.0.9:
     dependencies:
       JSONStream: 1.3.2
@@ -40969,6 +41181,8 @@ snapshots:
 
   fresh@0.5.2: {}
 
+  from-exponential@1.1.1: {}
+
   fs-constants@1.0.0: {}
 
   fs-extra@0.30.0:
@@ -47738,6 +47952,39 @@ snapshots:
       minimist: 1.2.7
       strip-json-comments: 2.0.1
 
+  react-aria-components@1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+    dependencies:
+      '@internationalized/date': 3.5.5
+      '@internationalized/string': 3.2.3
+      '@react-aria/collections': 3.0.0-alpha.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/color': 3.0.0-rc.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/dnd': 3.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/focus': 3.18.2(react@18.3.1)
+      '@react-aria/interactions': 3.22.2(react@18.3.1)
+      '@react-aria/menu': 3.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/toolbar': 3.0.0-beta.8(react@18.3.1)
+      '@react-aria/tree': 3.0.0-alpha.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-aria/utils': 3.25.2(react@18.3.1)
+      '@react-aria/virtualizer': 4.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-stately/color': 3.7.2(react@18.3.1)
+      '@react-stately/layout': 4.0.2(react@18.3.1)
+      '@react-stately/menu': 3.8.2(react@18.3.1)
+      '@react-stately/table': 3.12.2(react@18.3.1)
+      '@react-stately/utils': 3.10.3(react@18.3.1)
+      '@react-stately/virtualizer': 4.0.2(react@18.3.1)
+      '@react-types/color': 3.0.0-rc.1(react@18.3.1)
+      '@react-types/form': 3.7.6(react@18.3.1)
+      '@react-types/grid': 3.2.8(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      '@react-types/table': 3.10.1(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      client-only: 0.0.1
+      react: 18.3.1
+      react-aria: 3.34.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      react-dom: 18.3.1(react@18.3.1)
+      react-stately: 3.32.2(react@18.3.1)
+      use-sync-external-store: 1.2.0(react@18.3.1)
+
   react-aria@3.34.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
       '@internationalized/string': 3.2.3
@@ -48021,6 +48268,33 @@ snapshots:
       react-dom: 18.3.1(react@18.3.1)
       react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
 
+  react-stately@3.32.2(react@18.3.1):
+    dependencies:
+      '@react-stately/calendar': 3.5.4(react@18.3.1)
+      '@react-stately/checkbox': 3.6.8(react@18.3.1)
+      '@react-stately/collections': 3.10.9(react@18.3.1)
+      '@react-stately/combobox': 3.9.2(react@18.3.1)
+      '@react-stately/data': 3.11.6(react@18.3.1)
+      '@react-stately/datepicker': 3.10.2(react@18.3.1)
+      '@react-stately/dnd': 3.4.2(react@18.3.1)
+      '@react-stately/form': 3.0.5(react@18.3.1)
+      '@react-stately/list': 3.10.8(react@18.3.1)
+      '@react-stately/menu': 3.8.2(react@18.3.1)
+      '@react-stately/numberfield': 3.9.6(react@18.3.1)
+      '@react-stately/overlays': 3.6.10(react@18.3.1)
+      '@react-stately/radio': 3.10.7(react@18.3.1)
+      '@react-stately/searchfield': 3.5.6(react@18.3.1)
+      '@react-stately/select': 3.6.7(react@18.3.1)
+      '@react-stately/selection': 3.16.2(react@18.3.1)
+      '@react-stately/slider': 3.5.7(react@18.3.1)
+      '@react-stately/table': 3.12.2(react@18.3.1)
+      '@react-stately/tabs': 3.6.9(react@18.3.1)
+      '@react-stately/toggle': 3.7.7(react@18.3.1)
+      '@react-stately/tooltip': 3.4.12(react@18.3.1)
+      '@react-stately/tree': 3.8.4(react@18.3.1)
+      '@react-types/shared': 3.24.1(react@18.3.1)
+      react: 18.3.1
+
   react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
       '@babel/runtime': 7.25.0
@@ -49543,6 +49817,14 @@ snapshots:
       string-width: 4.2.3
       strip-ansi: 6.0.1
 
+  tailwindcss-animate@1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))):
+    dependencies:
+      tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))
+
+  tailwindcss-react-aria-components@1.1.5(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))):
+    dependencies:
+      tailwindcss: 3.4.10(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))
+
   tailwindcss@3.2.4(postcss@8.4.38)(ts-node@10.9.2(@types/node@18.11.18)(typescript@5.4.5)):
     dependencies:
       arg: 5.0.2
@@ -51248,7 +51530,7 @@ snapshots:
   wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8):
     dependencies:
       '@tanstack/react-query': 5.45.1(react@18.3.1)
-      '@wagmi/connectors': 5.0.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.3)(@wagmi/core@2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)
+      '@wagmi/connectors': 5.0.16(mehtb7r3xxh3anmscqllj3vxmi)
       '@wagmi/core': 2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)
       react: 18.3.1
       use-sync-external-store: 1.2.0(react@18.3.1)