Browse Source

feat(staking): improve feedback

This PR adds better feedback to the staking app.  Included are:

- Better error messages
- Toasts on success / failure (failure toasts only show where there isn't a
  modal to display the error message)
- Buttons go into a spinner state when isLoading is true
Connor Prussin 1 year ago
parent
commit
2a2773a0a6

+ 2 - 0
apps/staking/package.json

@@ -28,7 +28,9 @@
     "@next/third-parties": "^14.2.5",
     "@pythnetwork/hermes-client": "workspace:*",
     "@pythnetwork/staking-sdk": "workspace:*",
+    "@react-aria/toast": "3.0.0-beta.16",
     "@react-hookz/web": "^24.0.4",
+    "@react-stately/toast": "3.0.0-beta.6",
     "@solana/wallet-adapter-base": "^0.9.20",
     "@solana/wallet-adapter-react": "^0.15.28",
     "@solana/wallet-adapter-react-ui": "^0.9.27",

+ 110 - 54
apps/staking/src/components/AccountSummary/index.tsx

@@ -6,6 +6,7 @@ import {
   type ComponentProps,
   type ReactNode,
   useCallback,
+  useState,
   useMemo,
 } from "react";
 import {
@@ -16,8 +17,10 @@ import {
 import background from "./background.png";
 import { type States, StateType as ApiStateType } from "../../hooks/use-api";
 import { StateType, useAsync } from "../../hooks/use-async";
+import { useToast } from "../../hooks/use-toast";
 import { Button } from "../Button";
 import { Date } from "../Date";
+import { ErrorMessage } from "../ErrorMessage";
 import { ModalDialog } from "../ModalDialog";
 import { Tokens } from "../Tokens";
 import { TransferButton } from "../TransferButton";
@@ -135,6 +138,7 @@ export const AccountSummary = ({
               max={walletAmount}
               transfer={api.deposit}
               submitButtonText="Add tokens"
+              successMessage="Your tokens have been added to your stake account"
             />
           )}
           {availableToWithdraw === 0n ? (
@@ -278,13 +282,20 @@ const OisUnstake = ({
     () => staked + warmup + cooldown + cooldown2,
     [staked, warmup, cooldown, cooldown2],
   );
+  const toast = useToast();
   const { state, execute } = useAsync(api.unstakeAllIntegrityStaking);
 
   const doUnstakeAll = useCallback(() => {
-    execute().catch(() => {
-      /* TODO figure out a better UI treatment for when claim fails */
-    });
-  }, [execute]);
+    execute()
+      .then(() => {
+        toast.success(
+          "Your tokens are now cooling down and will be available to withdraw at the end of the next epoch",
+        );
+      })
+      .catch((error: unknown) => {
+        toast.error(error);
+      });
+  }, [execute, toast]);
 
   // eslint-disable-next-line unicorn/no-null
   return total === 0n ? null : (
@@ -344,7 +355,7 @@ const OisUnstake = ({
 
 type WithdrawButtonProps = Omit<
   ComponentProps<typeof TransferButton>,
-  "variant" | "actionDescription" | "actionName" | "transfer"
+  "variant" | "actionDescription" | "actionName" | "transfer" | "successMessage"
 > & {
   api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
 };
@@ -354,6 +365,7 @@ const WithdrawButton = ({ api, ...props }: WithdrawButtonProps) => (
     variant="secondary"
     actionDescription="Move funds from your account back to your wallet"
     actionName="Withdraw"
+    successMessage="You have withdrawn tokens from your stake account to your wallet"
     {...(api.type === ApiStateType.Loaded && {
       transfer: api.withdraw,
     })}
@@ -419,58 +431,96 @@ const ClaimDialog = ({
   expiringRewards,
   availableRewards,
 }: ClaimDialogProps) => {
+  const [closeDisabled, setCloseDisabled] = useState(false);
+
+  return (
+    <ModalDialog title="Claim" closeDisabled={closeDisabled}>
+      {({ close }) => (
+        <ClaimDialogContents
+          expiringRewards={expiringRewards}
+          availableRewards={availableRewards}
+          api={api}
+          close={close}
+          setCloseDisabled={setCloseDisabled}
+        />
+      )}
+    </ModalDialog>
+  );
+};
+
+type ClaimDialogContentsProps = {
+  availableRewards: bigint;
+  expiringRewards: Date | undefined;
+  api: States[ApiStateType.Loaded];
+  close: () => void;
+  setCloseDisabled: (value: boolean) => void;
+};
+
+const ClaimDialogContents = ({
+  api,
+  expiringRewards,
+  availableRewards,
+  close,
+  setCloseDisabled,
+}: ClaimDialogContentsProps) => {
   const { state, execute } = useAsync(api.claim);
 
+  const toast = useToast();
+
   const doClaim = useCallback(() => {
-    execute().catch(() => {
-      /* TODO figure out a better UI treatment for when claim fails */
-    });
-  }, [execute]);
+    setCloseDisabled(true);
+    execute()
+      .then(() => {
+        close();
+        toast.success("You have claimed your rewards");
+      })
+      .catch(() => {
+        /* no-op since this is already handled in the UI using `state` and is logged in useAsync */
+      })
+      .finally(() => {
+        setCloseDisabled(false);
+      });
+  }, [execute, toast]);
 
   return (
-    <ModalDialog title="Claim">
-      {({ close }) => (
-        <>
-          <p className="mb-4">
-            Claim your <Tokens>{availableRewards}</Tokens> rewards
-          </p>
-          {expiringRewards && (
-            <div className="mb-4 flex max-w-96 flex-row gap-2 border border-neutral-600/50 bg-pythpurple-400/20 p-4">
-              <InformationCircleIcon className="size-8 flex-none" />
-              <div className="text-sm">
-                Rewards expire one year from the epoch in which they were
-                earned. You have rewards expiring on{" "}
-                <Date>{expiringRewards}</Date>.
-              </div>
-            </div>
-          )}
-          {state.type === StateType.Error && (
-            <p className="mt-8 text-red-600">
-              Uh oh, an error occurred! Please try again
-            </p>
-          )}
-          <div className="mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between">
-            <Button
-              variant="secondary"
-              className="w-full sm:w-auto"
-              size="noshrink"
-              onPress={close}
-            >
-              Cancel
-            </Button>
-            <Button
-              className="w-full sm:w-auto"
-              size="noshrink"
-              isDisabled={state.type === StateType.Complete}
-              isLoading={state.type === StateType.Running}
-              onPress={doClaim}
-            >
-              Claim
-            </Button>
+    <>
+      <p className="mb-4">
+        Claim your <Tokens>{availableRewards}</Tokens> rewards
+      </p>
+      {expiringRewards && (
+        <div className="mb-4 flex max-w-96 flex-row gap-2 border border-neutral-600/50 bg-pythpurple-400/20 p-4">
+          <InformationCircleIcon className="size-8 flex-none" />
+          <div className="text-sm">
+            Rewards expire one year from the epoch in which they were earned.
+            You have rewards expiring on <Date>{expiringRewards}</Date>.
           </div>
-        </>
+        </div>
       )}
-    </ModalDialog>
+      {state.type === StateType.Error && (
+        <div className="mt-4 max-w-sm">
+          <ErrorMessage error={state.error} />
+        </div>
+      )}
+      <div className="mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between">
+        <Button
+          variant="secondary"
+          className="w-full sm:w-auto"
+          size="noshrink"
+          onPress={close}
+        >
+          Cancel
+        </Button>
+        <Button
+          className="w-full sm:w-auto"
+          size="noshrink"
+          isDisabled={state.type === StateType.Complete}
+          isLoading={state.type === StateType.Running}
+          onPress={doClaim}
+        >
+          Claim
+        </Button>
+      </div>
+    </>
   );
 };
 
@@ -484,11 +534,17 @@ type ClaimButtonProps = Omit<
 const ClaimButton = ({ api, ...props }: ClaimButtonProps) => {
   const { state, execute } = useAsync(api.claim);
 
+  const toast = useToast();
+
   const doClaim = useCallback(() => {
-    execute().catch(() => {
-      /* TODO figure out a better UI treatment for when claim fails */
-    });
-  }, [execute]);
+    execute()
+      .then(() => {
+        toast.success("You have claimed your rewards");
+      })
+      .catch((error: unknown) => {
+        toast.error(error);
+      });
+  }, [execute, toast]);
 
   return (
     <Button

+ 25 - 2
apps/staking/src/components/Button/index.tsx

@@ -1,5 +1,6 @@
 "use client";
 
+import { ArrowPathIcon } from "@heroicons/react/24/outline";
 import clsx from "clsx";
 import type { ComponentProps } from "react";
 import { Button as ReactAriaButton } from "react-aria-components";
@@ -23,18 +24,40 @@ export const Button = ({
   size,
   isDisabled,
   className,
+  children,
   ...props
 }: ButtonProps) => (
   <ReactAriaButton
     isDisabled={isLoading === true || isDisabled === true}
     className={clsx(
-      "disabled:border-neutral-50/10 disabled:bg-neutral-50/10 disabled:text-white/60",
+      "relative text-center disabled:border-neutral-50/10 disabled:bg-neutral-50/10 disabled:text-white/60",
       isLoading ? "cursor-wait" : "disabled:cursor-not-allowed",
       baseClassName({ variant, size }),
       className,
     )}
     {...props}
-  />
+  >
+    {(values) => (
+      <>
+        <div
+          className={clsx(
+            "flex flex-row items-center justify-center gap-[0.5em] transition",
+            { "opacity-0": isLoading },
+          )}
+        >
+          {typeof children === "function" ? children(values) : children}
+        </div>
+        <div
+          className={clsx(
+            "absolute inset-0 grid place-content-center transition",
+            { "opacity-0": !isLoading },
+          )}
+        >
+          <ArrowPathIcon className="inline-block size-[1em] animate-spin" />
+        </div>
+      </>
+    )}
+  </ReactAriaButton>
 );
 
 type LinkButtonProps = ComponentProps<typeof Link> & VariantProps;

+ 60 - 0
apps/staking/src/components/ErrorMessage/index.tsx

@@ -0,0 +1,60 @@
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import { WalletError } from "@solana/wallet-adapter-base";
+import clsx from "clsx";
+import { LazyMotion, m, domAnimation } from "framer-motion";
+import { useCallback, useMemo, useState } from "react";
+import { Button } from "react-aria-components";
+
+export const ErrorMessage = ({ error }: { error: unknown }) => {
+  return error instanceof WalletError ? (
+    <p className="text-red-600">
+      The transaction was rejected by your wallet. Please check your wallet and
+      try again.
+    </p>
+  ) : (
+    <UnknownError error={error} />
+  );
+};
+
+const UnknownError = ({ error }: { error: unknown }) => {
+  const [detailsOpen, setDetailsOpen] = useState(false);
+
+  const toggleDetailsOpen = useCallback(() => {
+    setDetailsOpen((cur) => !cur);
+  }, [setDetailsOpen]);
+
+  const message = useMemo(() => {
+    if (error instanceof Error) {
+      return error.toString();
+    } else if (typeof error === "string") {
+      return error;
+    } else {
+      return "An unknown error occurred";
+    }
+  }, [error]);
+
+  return (
+    <LazyMotion features={domAnimation}>
+      <Button onPress={toggleDetailsOpen} className="text-left">
+        <div className="text-red-600">
+          Uh oh, an error occurred! Please try again
+        </div>
+        <div className="flex flex-row items-center gap-[0.25em] text-xs opacity-60">
+          <div>Details</div>
+          <ChevronRightIcon
+            className={clsx("inline-block size-[1em] transition-transform", {
+              "rotate-90": detailsOpen,
+            })}
+          />
+        </div>
+      </Button>
+      <m.div
+        className="overflow-hidden pt-1 opacity-60"
+        initial={{ height: 0 }}
+        animate={{ height: detailsOpen ? "auto" : 0 }}
+      >
+        {message}
+      </m.div>
+    </LazyMotion>
+  );
+};

+ 10 - 8
apps/staking/src/components/ModalDialog/index.tsx

@@ -46,14 +46,16 @@ export const ModalDialog = ({
         {(options) => (
           <>
             {!noClose && (
-              <Button
-                onPress={options.close}
-                className="absolute right-3 top-3 grid size-10 place-content-center"
-                size="nopad"
-                isDisabled={closeDisabled ?? false}
-              >
-                <XMarkIcon className="size-6" />
-              </Button>
+              <div className="absolute right-3 top-3">
+                <Button
+                  onPress={options.close}
+                  className="size-10"
+                  size="nopad"
+                  isDisabled={closeDisabled ?? false}
+                >
+                  <XMarkIcon className="size-6" />
+                </Button>
+              </div>
             )}
             <Heading
               className={clsx("mr-10 text-3xl font-light", {

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

@@ -57,11 +57,7 @@ export const NoWalletHome = () => {
           Choose Your Journey
         </h1>
         <p className="text-lg">You can participate in both programs.</p>
-        <Button
-          onPress={showModal}
-          className="flex flex-row items-center justify-center gap-2 px-10 py-4"
-          size="nopad"
-        >
+        <Button onPress={showModal} className="px-10 py-4" size="nopad">
           <WalletIcon className="size-4" />
           <div>Connect wallet</div>
         </Button>
@@ -183,11 +179,7 @@ export const NoWalletHome = () => {
             Staking or Pyth Governance.
           </p>
         </div>
-        <Button
-          onPress={showModal}
-          className="inline-flex flex-row items-center gap-2 px-10 py-4"
-          size="nopad"
-        >
+        <Button onPress={showModal} className="px-10 py-4" size="nopad">
           <WalletIcon className="size-4" />
           <span>Connect wallet</span>
         </Button>

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

@@ -38,8 +38,10 @@ import {
   StateType as UseAsyncStateType,
   useAsync,
 } from "../../hooks/use-async";
+import { useToast } from "../../hooks/use-toast";
 import { Button, LinkButton } from "../Button";
 import { CopyButton } from "../CopyButton";
+import { ErrorMessage } from "../ErrorMessage";
 import { Menu, MenuItem, Section, Separator } from "../Menu";
 import { ModalDialog } from "../ModalDialog";
 import { OracleIntegrityStakingGuide } from "../OracleIntegrityStakingGuide";
@@ -198,10 +200,7 @@ const SelfStaking = ({
             </div>
             <div className="flex flex-row items-center gap-4">
               <MenuTrigger>
-                <Button
-                  variant="secondary"
-                  className="group flex flex-row items-center gap-2 lg:hidden"
-                >
+                <Button variant="secondary" className="group lg:hidden">
                   <Bars3Icon className="size-6 flex-none" />
                   <span className="sr-only">Publisher Menu</span>
                   <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
@@ -407,6 +406,8 @@ const ReassignStakeAccountForm = ({
 
   const { state, execute } = useAsync(doReassign);
 
+  const toast = useToast();
+
   const handleSubmit = useCallback(
     (e: FormEvent<HTMLFormElement>) => {
       e.preventDefault();
@@ -414,15 +415,16 @@ const ReassignStakeAccountForm = ({
       execute()
         .then(() => {
           close();
+          toast.success("You have reassigned your main account");
         })
         .catch(() => {
-          /* no-op since this is already handled in the UI using `state` and is logged in useTransfer */
+          /* no-op since this is already handled in the UI using `state` and is logged in useAsync */
         })
         .finally(() => {
           setCloseDisabled(false);
         });
     },
-    [execute, close, setCloseDisabled],
+    [execute, close, setCloseDisabled, toast],
   );
 
   return (
@@ -446,9 +448,9 @@ const ReassignStakeAccountForm = ({
           placeholder={PublicKey.default.toBase58()}
         />
         {state.type === UseAsyncStateType.Error && (
-          <p className="mt-1 text-red-600">
-            Uh oh, an error occurred! Please try again
-          </p>
+          <div className="mt-4 max-w-sm">
+            <ErrorMessage error={state.error} />
+          </div>
         )}
       </TextField>
       <Button
@@ -492,16 +494,6 @@ type OptOut = Omit<
 };
 
 const OptOut = ({ api, self, ...props }: OptOut) => {
-  const { state, execute } = useAsync(() =>
-    api.optPublisherOut(self.publicKey),
-  );
-
-  const doOptOut = useCallback(() => {
-    execute().catch(() => {
-      /* no-op since this is already handled in the UI using `state` and is logged in useTransfer */
-    });
-  }, [execute]);
-
   return hasAnyPositions(self) ? (
     <ModalDialog title="You must unstake first" closeButtonText="Ok" {...props}>
       <div className="flex max-w-prose flex-col gap-4">
@@ -517,47 +509,75 @@ const OptOut = ({ api, self, ...props }: OptOut) => {
   ) : (
     <ModalDialog title="Are you sure?" {...props}>
       {({ close }) => (
-        <>
-          <div className="flex max-w-prose flex-col gap-4">
-            <p className="font-semibold">
-              Are you sure you want to opt out of rewards?
-            </p>
-            <p className="opacity-90">
-              Opting out of rewards will prevent you from earning the publisher
-              yield rate and delegation fees from your delegators. You will
-              still be able to participate in OIS after opting out of rewards.
-            </p>
-          </div>
-          {state.type === UseAsyncStateType.Error && (
-            <p className="mt-8 text-red-600">
-              Uh oh, an error occurred! Please try again
-            </p>
-          )}
-          <div className="mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between">
-            <Button
-              className="w-full sm:w-auto"
-              size="noshrink"
-              onPress={close}
-            >
-              No, I want rewards!
-            </Button>
-            <Button
-              className="w-full sm:w-auto"
-              variant="secondary"
-              size="noshrink"
-              isLoading={state.type === UseAsyncStateType.Running}
-              isDisabled={state.type === UseAsyncStateType.Complete}
-              onPress={doOptOut}
-            >
-              Yes, opt me out
-            </Button>
-          </div>
-        </>
+        <OptOutModalContents api={api} self={self} close={close} />
       )}
     </ModalDialog>
   );
 };
 
+type OptOutModalContentsProps = {
+  api: States[ApiStateType.Loaded];
+  self: PublisherProps["publisher"];
+  close: () => void;
+};
+
+const OptOutModalContents = ({
+  api,
+  self,
+  close,
+}: OptOutModalContentsProps) => {
+  const { state, execute } = useAsync(() =>
+    api.optPublisherOut(self.publicKey),
+  );
+
+  const toast = useToast();
+
+  const doOptOut = useCallback(() => {
+    execute()
+      .then(() => {
+        toast.success("You have opted out of rewards");
+      })
+      .catch(() => {
+        /* no-op since this is already handled in the UI using `state` and is logged in useAsync */
+      });
+  }, [execute, toast]);
+
+  return (
+    <>
+      <div className="flex max-w-prose flex-col gap-4">
+        <p className="font-semibold">
+          Are you sure you want to opt out of rewards?
+        </p>
+        <p className="opacity-90">
+          Opting out of rewards will prevent you from earning the publisher
+          yield rate and delegation fees from your delegators. You will still be
+          able to participate in OIS after opting out of rewards.
+        </p>
+      </div>
+      {state.type === UseAsyncStateType.Error && (
+        <div className="mt-4 max-w-prose">
+          <ErrorMessage error={state.error} />
+        </div>
+      )}
+      <div className="mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between">
+        <Button className="w-full sm:w-auto" size="noshrink" onPress={close}>
+          No, I want rewards!
+        </Button>
+        <Button
+          className="w-full sm:w-auto"
+          variant="secondary"
+          size="noshrink"
+          isLoading={state.type === UseAsyncStateType.Running}
+          isDisabled={state.type === UseAsyncStateType.Complete}
+          onPress={doOptOut}
+        >
+          Yes, opt me out
+        </Button>
+      </div>
+    </>
+  );
+};
+
 type PublisherListProps = {
   api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount];
   currentEpoch: bigint;
@@ -851,7 +871,7 @@ const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => {
             }}
             size="nopad"
             variant="secondary"
-            className="grid size-8 place-content-center"
+            className="size-8"
           >
             <ChevronDoubleLeftIcon className="size-4" />
           </Button>
@@ -874,7 +894,7 @@ const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => {
               }}
               size="nopad"
               variant="secondary"
-              className="grid size-8 place-content-center"
+              className="size-8"
             >
               {page}
             </Button>
@@ -889,7 +909,7 @@ const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => {
             }}
             size="nopad"
             variant="secondary"
-            className="grid size-8 place-content-center"
+            className="size-8"
           >
             <ChevronDoubleRightIcon className="size-4" />
           </Button>
@@ -1422,6 +1442,7 @@ const YourPositionsTable = ({
                 actionName="Cancel"
                 submitButtonText="Cancel Warmup"
                 title="Cancel Warmup"
+                successMessage="Your tokens are no longer in warmup for staking"
                 max={warmup}
                 transfer={cancelWarmup}
               />
@@ -1456,6 +1477,7 @@ const YourPositionsTable = ({
                   </>
                 }
                 actionName="Unstake"
+                successMessage="Your tokens are now cooling down and will be available to withdraw at the end of the next epoch"
                 max={staked}
                 transfer={unstake}
               >
@@ -1505,6 +1527,7 @@ const StakeToPublisherButton = ({
       actionName="Stake"
       max={availableToStake}
       transfer={delegate}
+      successMessage="Your tokens are now in warm up and will be staked at the start of the next epoch"
     >
       {(amount) => (
         <>

+ 3 - 0
apps/staking/src/components/ProgramSection/index.tsx

@@ -145,6 +145,7 @@ const TokenOverview = ({
             actionName="Stake"
             max={available}
             transfer={stake}
+            successMessage="Your tokens are now in warm up and will be staked at the start of the next epoch"
           >
             <StakingTimeline currentEpoch={currentEpoch} />
           </TransferButton>
@@ -175,6 +176,7 @@ const TokenOverview = ({
             title="Cancel Warmup"
             max={warmup}
             transfer={cancelWarmup}
+            successMessage="Your tokens are no longer in warmup for staking"
           />
         ),
       })}
@@ -194,6 +196,7 @@ const TokenOverview = ({
             actionName="Unstake"
             max={staked}
             transfer={unstake}
+            successMessage="Your tokens are now cooling down and will be available to withdraw at the end of the next epoch"
           >
             <StakingTimeline cooldownOnly currentEpoch={currentEpoch} />
           </TransferButton>

+ 6 - 1
apps/staking/src/components/Root/index.tsx

@@ -5,6 +5,7 @@ import type { ReactNode, CSSProperties, HTMLProps } from "react";
 
 import { I18nProvider } from "./i18n-provider";
 import { RestrictedRegionBanner } from "./restricted-region-banner";
+import { ToastRegion } from "./toast-region";
 import {
   IS_PRODUCTION_SERVER,
   GOOGLE_ANALYTICS_ID,
@@ -16,6 +17,7 @@ import {
 import { ApiProvider } from "../../hooks/use-api";
 import { LoggerProvider } from "../../hooks/use-logger";
 import { NetworkProvider } from "../../hooks/use-network";
+import { ToastProvider } from "../../hooks/use-toast";
 import { Amplitude } from "../Amplitude";
 import { Footer } from "../Footer";
 import { Header } from "../Header";
@@ -60,6 +62,7 @@ export const Root = ({ children }: Props) => (
         {children}
       </MaxWidth>
       <Footer className="z-10" />
+      <ToastRegion />
     </body>
     {GOOGLE_ANALYTICS_ID && <GoogleAnalytics gaId={GOOGLE_ANALYTICS_ID} />}
     {AMPLITUDE_API_KEY && <Amplitude apiKey={AMPLITUDE_API_KEY} />}
@@ -77,7 +80,9 @@ const HtmlWithProviders = ({ lang, ...props }: HTMLProps<HTMLHtmlElement>) => (
             mainnetRpc={MAINNET_RPC}
           >
             <ApiProvider hermesUrl={HERMES_URL}>
-              <html lang={lang} {...props} />
+              <ToastProvider>
+                <html lang={lang} {...props} />
+              </ToastProvider>
             </ApiProvider>
           </WalletProvider>
         </NetworkProvider>

+ 108 - 0
apps/staking/src/components/Root/toast-region.tsx

@@ -0,0 +1,108 @@
+"use client";
+
+import { XMarkIcon } from "@heroicons/react/24/solid";
+import {
+  type AriaToastRegionProps,
+  type AriaToastProps,
+  useToastRegion,
+  useToast as reactAriaUseToast,
+} from "@react-aria/toast";
+import clsx from "clsx";
+import { useRef, useState } from "react";
+import { Button } from "react-aria-components";
+
+import {
+  type Toast as ToastContentType,
+  ToastType,
+  useToast,
+} from "../../hooks/use-toast";
+import { ErrorMessage } from "../ErrorMessage";
+
+export const ToastRegion = (props: AriaToastRegionProps) => {
+  const state = useToast();
+  const ref = useRef(null);
+  const { regionProps } = useToastRegion(props, state, ref);
+
+  return (
+    <div
+      {...regionProps}
+      ref={ref}
+      className="pointer-events-none fixed top-0 z-50 flex w-full flex-col items-center"
+    >
+      {state.visibleToasts.map((toast) => (
+        <Toast key={toast.key} toast={toast} />
+      ))}
+    </div>
+  );
+};
+
+const Toast = (props: AriaToastProps<ToastContentType>) => {
+  const [isTimerStarted, setIsTimerStarted] = useState(false);
+  const state = useToast();
+  const ref = useRef(null);
+  const { toastProps, contentProps, titleProps, closeButtonProps } =
+    reactAriaUseToast(props, state, ref);
+
+  return (
+    <div
+      {...toastProps}
+      ref={ref}
+      className="pt-4 data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:slide-in-from-top data-[exiting]:slide-out-to-top"
+      {...((props.toast.animation === "entering" ||
+        props.toast.animation === "queued") && { "data-entering": "" })}
+      {...(props.toast.animation === "exiting" && { "data-exiting": "" })}
+      onAnimationEnd={() => {
+        if (
+          props.toast.animation === "entering" ||
+          props.toast.animation === "queued"
+        ) {
+          setIsTimerStarted(true);
+        }
+        if (props.toast.animation === "exiting") {
+          state.remove(props.toast.key);
+        }
+      }}
+    >
+      <div className="pointer-events-auto w-96 bg-pythpurple-100 text-pythpurple-950">
+        <div
+          className={clsx(
+            "h-1 w-full origin-left bg-green-500 transition-transform [transition-duration:5000ms] [transition-timing-function:linear]",
+            {
+              "scale-x-0": isTimerStarted,
+              "bg-green-500": props.toast.content.type === ToastType.Success,
+              "bg-red-500": props.toast.content.type === ToastType.Error,
+            },
+          )}
+          onTransitionEnd={() => {
+            state.close(props.toast.key);
+          }}
+        />
+        <div className="flex flex-row items-start justify-between gap-8 px-4 py-2">
+          <div {...contentProps}>
+            <div {...titleProps}>
+              <ToastContent>{props.toast.content}</ToastContent>
+            </div>
+          </div>
+          <Button {...closeButtonProps}>
+            <XMarkIcon className="mt-1 size-4" />
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+type ToastContentProps = {
+  children: ToastContentType;
+};
+
+const ToastContent = ({ children }: ToastContentProps) => {
+  switch (children.type) {
+    case ToastType.Error: {
+      return <ErrorMessage error={children.error} />;
+    }
+    case ToastType.Success: {
+      return children.message;
+    }
+  }
+};

+ 1 - 4
apps/staking/src/components/Select/index.tsx

@@ -40,10 +40,7 @@ export const Select = <T extends string | number>({
     {...props}
   >
     <Label className="whitespace-nowrap opacity-80">{label}</Label>
-    <Button
-      className="group flex flex-row items-center gap-2 px-2 py-3 text-xs transition sm:px-4"
-      size="nopad"
-    >
+    <Button className="group px-2 py-3 text-xs transition sm:px-4" size="nopad">
       <SelectValue />
       <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
     </Button>

+ 18 - 6
apps/staking/src/components/TransferButton/index.tsx

@@ -16,8 +16,10 @@ import {
 } from "react-aria-components";
 
 import { StateType, useAsync } from "../../hooks/use-async";
+import { useToast } from "../../hooks/use-toast";
 import { stringToTokens, tokensToString } from "../../tokens";
 import { Button } from "../Button";
+import { ErrorMessage } from "../ErrorMessage";
 import { ModalDialog } from "../ModalDialog";
 import { Tokens } from "../Tokens";
 import PythTokensIcon from "../Tokens/pyth.svg";
@@ -35,6 +37,7 @@ type Props = Omit<ComponentProps<typeof Button>, "children"> & {
     | ReactNode[]
     | undefined;
   transfer?: ((amount: bigint) => Promise<void>) | undefined;
+  successMessage: ReactNode;
 };
 
 export const TransferButton = ({
@@ -47,6 +50,7 @@ export const TransferButton = ({
   transfer,
   children,
   isDisabled,
+  successMessage,
   ...props
 }: Props) => {
   return transfer === undefined ||
@@ -64,6 +68,7 @@ export const TransferButton = ({
         max={max}
         transfer={transfer}
         submitButtonText={submitButtonText ?? actionName}
+        successMessage={successMessage}
       >
         {children}
       </TransferDialog>
@@ -83,13 +88,15 @@ type TransferDialogProps = Omit<
     | ReactNode
     | ReactNode[]
     | undefined;
+  successMessage: ReactNode;
 };
 
-export const TransferDialog = ({
+const TransferDialog = ({
   max,
   transfer,
   submitButtonText,
   children,
+  successMessage,
   ...props
 }: TransferDialogProps) => {
   const [closeDisabled, setCloseDisabled] = useState(false);
@@ -103,6 +110,7 @@ export const TransferDialog = ({
           setCloseDisabled={setCloseDisabled}
           submitButtonText={submitButtonText}
           close={close}
+          successMessage={successMessage}
         >
           {children}
         </DialogContents>
@@ -118,6 +126,7 @@ type DialogContentsProps = {
   setCloseDisabled: (value: boolean) => void;
   submitButtonText: ReactNode;
   close: () => void;
+  successMessage: ReactNode;
 };
 
 const DialogContents = ({
@@ -127,8 +136,10 @@ const DialogContents = ({
   submitButtonText,
   setCloseDisabled,
   close,
+  successMessage,
 }: DialogContentsProps) => {
   const { amount, setAmount, setMax, stringValue } = useAmountInput(max);
+  const toast = useToast();
 
   const validationError = useMemo(() => {
     switch (amount.type) {
@@ -167,15 +178,16 @@ const DialogContents = ({
       execute()
         .then(() => {
           close();
+          toast.success(successMessage);
         })
         .catch(() => {
-          /* no-op since this is already handled in the UI using `state` and is logged in useTransfer */
+          /* no-op since this is already handled in the UI using `state` and is logged in useAsync */
         })
         .finally(() => {
           setCloseDisabled(false);
         });
     },
-    [execute, close, setCloseDisabled],
+    [execute, close, setCloseDisabled, toast],
   );
 
   return (
@@ -217,9 +229,9 @@ const DialogContents = ({
           </div>
         </Group>
         {state.type === StateType.Error && (
-          <p className="mt-1 text-red-600">
-            Uh oh, an error occurred! Please try again
-          </p>
+          <div className="mt-4 max-w-sm">
+            <ErrorMessage error={state.error} />
+          </div>
         )}
       </TextField>
       {children && (

+ 1 - 4
apps/staking/src/components/WalletButton/index.tsx

@@ -295,10 +295,7 @@ const ButtonComponent = ({
   ...props
 }: ButtonComponentProps) => (
   <Button
-    className={clsx(
-      "flex w-36 flex-row items-center justify-center gap-2 text-sm sm:w-52 sm:text-base",
-      className,
-    )}
+    className={clsx("w-36 text-sm sm:w-52 sm:text-base", className)}
     {...props}
   >
     <WalletIcon className="size-4 flex-none opacity-60" />

+ 73 - 0
apps/staking/src/hooks/use-toast.tsx

@@ -0,0 +1,73 @@
+"use client";
+
+import {
+  type ToastState as BaseToastState,
+  useToastState,
+} from "@react-stately/toast";
+import {
+  type ComponentProps,
+  type ReactNode,
+  createContext,
+  useContext,
+  useCallback,
+} from "react";
+
+export enum ToastType {
+  Success,
+  Error,
+}
+const Toast = {
+  Success: (message: ReactNode) => ({
+    type: ToastType.Success as const,
+    message,
+  }),
+  ErrorToast: (error: unknown) => ({ type: ToastType.Error as const, error }),
+};
+export type Toast = ReturnType<(typeof Toast)[keyof typeof Toast]>;
+
+type ToastState = BaseToastState<Toast> & {
+  success: (message: ReactNode) => void;
+  error: (error: unknown) => void;
+};
+
+const ToastContext = createContext<undefined | ToastState>(undefined);
+
+type ToastContextProps = Omit<
+  ComponentProps<typeof ToastContext.Provider>,
+  "value"
+>;
+
+export const ToastProvider = (props: ToastContextProps) => {
+  const toast = useToastState<Toast>({
+    maxVisibleToasts: 3,
+    hasExitAnimation: true,
+  });
+
+  const success = useCallback(
+    (message: ReactNode) => toast.add(Toast.Success(message)),
+    [toast],
+  );
+  const error = useCallback(
+    (error: unknown) => toast.add(Toast.ErrorToast(error)),
+    [toast],
+  );
+
+  return (
+    <ToastContext.Provider value={{ ...toast, success, error }} {...props} />
+  );
+};
+
+export const useToast = () => {
+  const toast = useContext(ToastContext);
+  if (toast) {
+    return toast;
+  } else {
+    throw new NotInitializedError();
+  }
+};
+
+class NotInitializedError extends Error {
+  constructor() {
+    super("This component must be contained within a `ToastProvider`!");
+  }
+}

+ 507 - 22
pnpm-lock.yaml

@@ -345,9 +345,15 @@ importers:
       '@pythnetwork/staking-sdk':
         specifier: workspace:*
         version: link:../../governance/pyth_staking_sdk
+      '@react-aria/toast':
+        specifier: 3.0.0-beta.16
+        version: 3.0.0-beta.16(react@18.3.1)
       '@react-hookz/web':
         specifier: ^24.0.4
         version: 24.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@react-stately/toast':
+        specifier: 3.0.0-beta.6
+        version: 3.0.0-beta.6(react@18.3.1)
       '@solana/wallet-adapter-base':
         specifier: ^0.9.20
         version: 0.9.23(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
@@ -474,7 +480,7 @@ importers:
     dependencies:
       '@certusone/wormhole-sdk':
         specifier: ^0.9.8
-        version: 0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
+        version: 0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@5.0.10)
       '@coral-xyz/anchor':
         specifier: ^0.29.0
         version: 0.29.0(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -1474,7 +1480,7 @@ importers:
     dependencies:
       '@certusone/wormhole-sdk':
         specifier: ^0.9.22
-        version: 0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
+        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(w3kf4mhrlhkqd2mcprd2ix74zu)
@@ -1839,7 +1845,7 @@ importers:
     dependencies:
       '@certusone/wormhole-sdk':
         specifier: ^0.9.12
-        version: 0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
+        version: 0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.4)
       '@mysten/sui':
         specifier: ^1.3.0
         version: 1.3.0(svelte@4.2.18)(typescript@5.4.5)
@@ -1848,7 +1854,7 @@ importers:
         version: link:../../../contract_manager
       '@pythnetwork/price-service-client':
         specifier: ^1.4.0
-        version: 1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+        version: 1.9.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       '@pythnetwork/price-service-sdk':
         specifier: ^1.2.0
         version: 1.7.1
@@ -4650,15 +4656,27 @@ packages:
   '@internationalized/date@3.5.5':
     resolution: {integrity: sha512-H+CfYvOZ0LTJeeLOqm19E3uj/4YjrmOFtBufDHPfvtI80hFAMqtrp7oCACpe4Cil5l8S0Qu/9dYfZc/5lY8WQQ==}
 
+  '@internationalized/date@3.5.6':
+    resolution: {integrity: sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==}
+
   '@internationalized/message@3.1.4':
     resolution: {integrity: sha512-Dygi9hH1s7V9nha07pggCkvmRfDd3q2lWnMGvrJyrOwYMe1yj4D2T9BoH9I6MGR7xz0biQrtLPsqUkqXzIrBOw==}
 
+  '@internationalized/message@3.1.5':
+    resolution: {integrity: sha512-hjEpLKFlYA3m5apldLqzHqw531qqfOEq0HlTWdfyZmcloWiUbWsYXD6YTiUmQmOtarthzhdjCAwMVrB8a4E7uA==}
+
   '@internationalized/number@3.5.3':
     resolution: {integrity: sha512-rd1wA3ebzlp0Mehj5YTuTI50AQEx80gWFyHcQu+u91/5NgdwBecO8BH6ipPfE+lmQ9d63vpB3H9SHoIUiupllw==}
 
+  '@internationalized/number@3.5.4':
+    resolution: {integrity: sha512-h9huwWjNqYyE2FXZZewWqmCdkw1HeFds5q4Siuoms3hUQC5iPJK3aBmkFZoDSLN4UD0Bl8G22L/NdHpeOr+/7A==}
+
   '@internationalized/string@3.2.3':
     resolution: {integrity: sha512-9kpfLoA8HegiWTeCbR2livhdVeKobCnVv8tlJ6M2jF+4tcMqDo94ezwlnrUANBWPgd8U7OXIHCk2Ov2qhk4KXw==}
 
+  '@internationalized/string@3.2.4':
+    resolution: {integrity: sha512-BcyadXPn89Ae190QGZGDUZPqxLj/xsP4U1Br1oSy8yfIjmpJ8cJtGYleaodqW/EmzFjwELtwDojLkf3FhV6SjA==}
+
   '@ipld/dag-pb@2.1.18':
     resolution: {integrity: sha512-ZBnf2fuX9y3KccADURG5vb9FaOeMjFkCrNysB0PtftME/4iCTjxfaLoNq/IAh5fTqUOMXvryN6Jyka4ZGuMLIg==}
 
@@ -6427,6 +6445,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/i18n@3.12.3':
+    resolution: {integrity: sha512-0Tp/4JwnCVNKDfuknPF+/xf3/woOc8gUjTU2nCjO3mCVb4FU7KFtjxQ2rrx+6hpIVG6g+N9qfMjRa/ggVH0CJg==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/interactions@3.21.3':
     resolution: {integrity: sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==}
     peerDependencies:
@@ -6437,11 +6460,21 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/interactions@3.22.3':
+    resolution: {integrity: sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/label@3.7.11':
     resolution: {integrity: sha512-REgejE5Qr8cXG/b8H2GhzQmjQlII/0xQW/4eDzydskaTLvA7lF5HoJUE6biYTquH5va38d8XlH465RPk+bvHzA==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/landmark@3.0.0-beta.16':
+    resolution: {integrity: sha512-qr6jAu5KyI0R5IdAvRd2DBaXO1+7A148gO9pZutdhm2uvC8nV+fXrQu73C7dXcpvMyp5IFJOTwcRCHnsG1Fk9w==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/link@3.7.4':
     resolution: {integrity: sha512-E8SLDuS9ssm/d42+3sDFNthfMcNXMUrT2Tq1DIZt22EsMcuEzmJ9B0P7bDP5RgvIw05xVGqZ20nOpU4mKTxQtA==}
     peerDependencies:
@@ -6534,6 +6567,12 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/ssr@3.9.6':
+    resolution: {integrity: sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==}
+    engines: {node: '>= 12'}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/switch@3.6.7':
     resolution: {integrity: sha512-yBNvKylhc3ZRQ0+7mD0mIenRRe+1yb8YaqMMZr8r3Bf87LaiFtQyhRFziq6ZitcwTJz5LEWjBihxbSVvUrf49w==}
     peerDependencies:
@@ -6562,6 +6601,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/toast@3.0.0-beta.16':
+    resolution: {integrity: sha512-hIkpdfFf7/CvuEh1rp2zensEGJVq0mi8NuwVi0Spj5zN7WTgix0yuA5y6KuYr+SGurP32TXjpVg5rTwykYEwGQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-aria/toggle@3.10.7':
     resolution: {integrity: sha512-/RJQU8QlPZXRElZ3Tt10F5K5STgUBUGPpfuFUGuwF3Kw3GpPxYsA1YAVjxXz2MMGwS0+y6+U/J1xIs1AF0Jwzg==}
     peerDependencies:
@@ -6593,6 +6637,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-aria/utils@3.25.3':
+    resolution: {integrity: sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==}
+    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:
@@ -6834,6 +6883,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/toast@3.0.0-beta.6':
+    resolution: {integrity: sha512-ffvWaigbyNd7QfubTs2cKNRsFywBcbYA/WaSerKM2iw0ek9F+C7zb+9F7Ms3mdM4BGTh0JqmuMQTRXTI0sAxBw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-stately/toggle@3.7.7':
     resolution: {integrity: sha512-AS+xB4+hHWa3wzYkbS6pwBkovPfIE02B9SnuYTe0stKcuejpWKo5L3QMptW0ftFYsW3ZPCXuneImfObEw2T01A==}
     peerDependencies:
@@ -6859,6 +6913,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-stately/utils@3.10.4':
+    resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
+    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:
@@ -6869,6 +6928,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-types/button@3.10.0':
+    resolution: {integrity: sha512-rAyU+N9VaHLBdZop4zasn8IDwf9I5Q1EzHUKMtzIFf5aUlMUW+K460zI/l8UESWRSWAXK9/WPSXGxfcoCEjvAA==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-types/button@3.9.6':
     resolution: {integrity: sha512-8lA+D5JLbNyQikf8M/cPP2cji91aVTcqjrGpDqI7sQnaLFikM8eFR6l1ZWGtZS5MCcbfooko77ha35SYplSQvw==}
     peerDependencies:
@@ -6974,6 +7038,11 @@ packages:
     peerDependencies:
       react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
 
+  '@react-types/shared@3.25.0':
+    resolution: {integrity: sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
+
   '@react-types/slider@3.7.5':
     resolution: {integrity: sha512-bRitwQRQjQoOcKEdPMljnvm474dwrmsc6pdsVQDh/qynzr+KO9IHuYc3qPW53WVE2hMQJDohlqtCAWQXWQ5Vcg==}
     peerDependencies:
@@ -23770,7 +23839,7 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
-  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
+  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@5.0.10)':
     dependencies:
       '@certusone/wormhole-sdk-proto-web': 0.0.6(google-protobuf@3.21.4)
       '@certusone/wormhole-sdk-wasm': 0.0.1
@@ -23792,7 +23861,7 @@ snapshots:
       near-api-js: 1.1.0(encoding@0.1.13)
     optionalDependencies:
       '@injectivelabs/networks': 1.10.12(google-protobuf@3.21.4)
-      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
+      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.7)(utf-8-validate@5.0.10)
       '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.4)
     transitivePeerDependencies:
       - bufferutil
@@ -23805,7 +23874,7 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
-  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)':
+  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.3)':
     dependencies:
       '@certusone/wormhole-sdk-proto-web': 0.0.6(google-protobuf@3.21.4)
       '@certusone/wormhole-sdk-wasm': 0.0.1
@@ -23827,7 +23896,7 @@ snapshots:
       near-api-js: 1.1.0(encoding@0.1.13)
     optionalDependencies:
       '@injectivelabs/networks': 1.10.12(google-protobuf@3.21.4)
-      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)
+      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.4)
     transitivePeerDependencies:
       - bufferutil
@@ -23840,15 +23909,15 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
-  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
+  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.4)':
     dependencies:
       '@certusone/wormhole-sdk-proto-web': 0.0.6(google-protobuf@3.21.4)
       '@certusone/wormhole-sdk-wasm': 0.0.1
-      '@coral-xyz/borsh': 0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
-      '@mysten/sui.js': 0.32.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)
-      '@project-serum/anchor': 0.25.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
-      '@solana/spl-token': 0.3.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
-      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
+      '@coral-xyz/borsh': 0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))
+      '@mysten/sui.js': 0.32.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@project-serum/anchor': 0.25.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/spl-token': 0.3.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
       '@terra-money/terra.js': 3.1.9
       '@xpla/xpla.js': 0.2.3
       algosdk: 2.7.0
@@ -23862,7 +23931,7 @@ snapshots:
       near-api-js: 1.1.0(encoding@0.1.13)
     optionalDependencies:
       '@injectivelabs/networks': 1.10.12(google-protobuf@3.21.4)
-      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
+      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.4)
     transitivePeerDependencies:
       - bufferutil
@@ -24063,6 +24132,12 @@ snapshots:
       bn.js: 5.2.1
       buffer-layout: 1.2.2
 
+  '@coral-xyz/borsh@0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bn.js: 5.2.1
+      buffer-layout: 1.2.2
+
   '@coral-xyz/borsh@0.27.0(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -24253,6 +24328,17 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@cosmjs/socket@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@cosmjs/stream': 0.30.1
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/socket@0.32.3(bufferutil@4.0.7)(utf-8-validate@5.0.10)':
     dependencies:
       '@cosmjs/stream': 0.32.3
@@ -24332,6 +24418,26 @@ snapshots:
       - debug
       - utf-8-validate
 
+  '@cosmjs/stargate@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@confio/ics23': 0.6.8
+      '@cosmjs/amino': 0.30.1
+      '@cosmjs/encoding': 0.30.1
+      '@cosmjs/math': 0.30.1
+      '@cosmjs/proto-signing': 0.30.1
+      '@cosmjs/stream': 0.30.1
+      '@cosmjs/tendermint-rpc': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@cosmjs/utils': 0.30.1
+      cosmjs-types: 0.7.2
+      long: 4.0.0
+      protobufjs: 6.11.4
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/stargate@0.32.2(bufferutil@4.0.7)(utf-8-validate@5.0.10)':
     dependencies:
       '@confio/ics23': 0.6.8
@@ -24444,6 +24550,24 @@ snapshots:
       - debug
       - utf-8-validate
 
+  '@cosmjs/tendermint-rpc@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@cosmjs/crypto': 0.30.1
+      '@cosmjs/encoding': 0.30.1
+      '@cosmjs/json-rpc': 0.30.1
+      '@cosmjs/math': 0.30.1
+      '@cosmjs/socket': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@cosmjs/stream': 0.30.1
+      '@cosmjs/utils': 0.30.1
+      axios: 0.21.4(debug@4.3.6)
+      readonly-date: 1.0.0
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/tendermint-rpc@0.32.2(bufferutil@4.0.7)(utf-8-validate@5.0.10)':
     dependencies:
       '@cosmjs/crypto': 0.32.3
@@ -25233,6 +25357,33 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/basex': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/networks': 5.7.1
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/web': 5.7.1
+      bech32: 1.1.4
+      ws: 7.4.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   '@ethersproject/random@5.7.0':
     dependencies:
       '@ethersproject/bytes': 5.7.0
@@ -26176,7 +26327,7 @@ snapshots:
       - debug
       - google-protobuf
 
-  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
+  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.7)(utf-8-validate@5.0.10)':
     dependencies:
       '@apollo/client': 3.7.13(graphql@16.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@cosmjs/amino': 0.30.1
@@ -26224,7 +26375,7 @@ snapshots:
       - utf-8-validate
     optional: true
 
-  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)':
+  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.7)(utf-8-validate@6.0.3)':
     dependencies:
       '@apollo/client': 3.7.13(graphql@16.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@cosmjs/amino': 0.30.1
@@ -26319,6 +26470,54 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
+  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@apollo/client': 3.7.13(graphql@16.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@cosmjs/amino': 0.30.1
+      '@cosmjs/proto-signing': 0.30.1
+      '@cosmjs/stargate': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@ethersproject/bytes': 5.7.0
+      '@injectivelabs/core-proto-ts': 0.0.14
+      '@injectivelabs/exceptions': 1.14.6(google-protobuf@3.21.2)
+      '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.2)
+      '@injectivelabs/grpc-web-node-http-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1(google-protobuf@3.21.2))
+      '@injectivelabs/grpc-web-react-native-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1(google-protobuf@3.21.2))
+      '@injectivelabs/indexer-proto-ts': 1.10.8-rc.4
+      '@injectivelabs/mito-proto-ts': 1.0.9
+      '@injectivelabs/networks': 1.14.6(google-protobuf@3.21.2)
+      '@injectivelabs/test-utils': 1.14.4
+      '@injectivelabs/token-metadata': 1.10.42(google-protobuf@3.21.2)
+      '@injectivelabs/ts-types': 1.14.6
+      '@injectivelabs/utils': 1.14.6(google-protobuf@3.21.2)
+      '@metamask/eth-sig-util': 4.0.1
+      axios: 0.27.2
+      bech32: 2.0.0
+      bip39: 3.0.4
+      cosmjs-types: 0.7.2
+      eth-crypto: 2.6.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      ethereumjs-util: 7.1.5
+      ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      google-protobuf: 3.21.2
+      graphql: 16.6.0
+      http-status-codes: 2.2.0
+      js-sha3: 0.8.0
+      jscrypto: 1.0.3
+      keccak256: 1.0.6
+      link-module-alias: 1.2.0
+      rxjs: 7.8.0
+      secp256k1: 4.0.3
+      shx: 0.3.4
+      snakecase-keys: 5.4.5
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - graphql-ws
+      - react
+      - react-dom
+      - subscriptions-transport-ws
+      - utf-8-validate
+    optional: true
+
   '@injectivelabs/sdk-ts@1.14.7(bufferutil@4.0.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
     dependencies:
       '@apollo/client': 3.7.13(graphql@16.8.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -26548,19 +26747,36 @@ snapshots:
     dependencies:
       '@swc/helpers': 0.5.11
 
+  '@internationalized/date@3.5.6':
+    dependencies:
+      '@swc/helpers': 0.5.11
+
   '@internationalized/message@3.1.4':
     dependencies:
       '@swc/helpers': 0.5.11
       intl-messageformat: 10.5.14
 
+  '@internationalized/message@3.1.5':
+    dependencies:
+      '@swc/helpers': 0.5.11
+      intl-messageformat: 10.5.14
+
   '@internationalized/number@3.5.3':
     dependencies:
       '@swc/helpers': 0.5.11
 
+  '@internationalized/number@3.5.4':
+    dependencies:
+      '@swc/helpers': 0.5.11
+
   '@internationalized/string@3.2.3':
     dependencies:
       '@swc/helpers': 0.5.11
 
+  '@internationalized/string@3.2.4':
+    dependencies:
+      '@swc/helpers': 0.5.11
+
   '@ipld/dag-pb@2.1.18':
     dependencies:
       multiformats: 9.9.0
@@ -28582,6 +28798,22 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@mysten/sui.js@0.32.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@mysten/bcs': 0.7.1
+      '@noble/curves': 1.4.2
+      '@noble/hashes': 1.4.0
+      '@scure/bip32': 1.4.0
+      '@scure/bip39': 1.3.0
+      '@suchipi/femver': 1.0.0
+      jayson: 4.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      rpc-websockets: 7.5.1
+      superstruct: 1.0.4
+      tweetnacl: 1.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   '@mysten/sui@1.3.0(svelte@4.2.18)(typescript@5.4.5)':
     dependencies:
       '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0)
@@ -29514,6 +29746,28 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@project-serum/anchor@0.25.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@project-serum/borsh': 0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      base64-js: 1.5.1
+      bn.js: 5.2.1
+      bs58: 4.0.1
+      buffer-layout: 1.2.2
+      camelcase: 5.3.1
+      cross-fetch: 3.1.5(encoding@0.1.13)
+      crypto-hash: 1.3.0
+      eventemitter3: 4.0.7
+      js-sha256: 0.9.0
+      pako: 2.1.0
+      snake-case: 3.0.4
+      superstruct: 0.15.5
+      toml: 3.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@project-serum/borsh@0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -29532,6 +29786,12 @@ snapshots:
       bn.js: 5.2.1
       buffer-layout: 1.2.2
 
+  '@project-serum/borsh@0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bn.js: 5.2.1
+      buffer-layout: 1.2.2
+
   '@project-serum/sol-wallet-adapter@0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -29594,15 +29854,15 @@ snapshots:
       - encoding
       - utf-8-validate
 
-  '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+  '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
       '@pythnetwork/price-service-sdk': 1.7.1
       '@types/ws': 8.5.4
       axios: 1.7.2
       axios-retry: 3.9.1
-      isomorphic-ws: 4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      isomorphic-ws: 4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       ts-log: 2.2.5
-      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - bufferutil
       - debug
@@ -29991,6 +30251,18 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/i18n@3.12.3(react@18.3.1)':
+    dependencies:
+      '@internationalized/date': 3.5.6
+      '@internationalized/message': 3.1.5
+      '@internationalized/number': 3.5.4
+      '@internationalized/string': 3.2.4
+      '@react-aria/ssr': 3.9.6(react@18.3.1)
+      '@react-aria/utils': 3.25.3(react@18.3.1)
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-aria/interactions@3.21.3(react@18.3.1)':
     dependencies:
       '@react-aria/ssr': 3.9.4(react@18.3.1)
@@ -30007,6 +30279,14 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/interactions@3.22.3(react@18.3.1)':
+    dependencies:
+      '@react-aria/ssr': 3.9.6(react@18.3.1)
+      '@react-aria/utils': 3.25.3(react@18.3.1)
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-aria/label@3.7.11(react@18.3.1)':
     dependencies:
       '@react-aria/utils': 3.25.2(react@18.3.1)
@@ -30014,6 +30294,14 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/landmark@3.0.0-beta.16(react@18.3.1)':
+    dependencies:
+      '@react-aria/utils': 3.25.3(react@18.3.1)
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      use-sync-external-store: 1.2.0(react@18.3.1)
+
   '@react-aria/link@3.7.4(react@18.3.1)':
     dependencies:
       '@react-aria/focus': 3.18.2(react@18.3.1)
@@ -30208,6 +30496,11 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/ssr@3.9.6(react@18.3.1)':
+    dependencies:
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-aria/switch@3.6.7(react@18.3.1)':
     dependencies:
       '@react-aria/toggle': 3.10.7(react@18.3.1)
@@ -30278,6 +30571,18 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-aria/toast@3.0.0-beta.16(react@18.3.1)':
+    dependencies:
+      '@react-aria/i18n': 3.12.3(react@18.3.1)
+      '@react-aria/interactions': 3.22.3(react@18.3.1)
+      '@react-aria/landmark': 3.0.0-beta.16(react@18.3.1)
+      '@react-aria/utils': 3.25.3(react@18.3.1)
+      '@react-stately/toast': 3.0.0-beta.6(react@18.3.1)
+      '@react-types/button': 3.10.0(react@18.3.1)
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+
   '@react-aria/toggle@3.10.7(react@18.3.1)':
     dependencies:
       '@react-aria/focus': 3.18.2(react@18.3.1)
@@ -30340,6 +30645,15 @@ snapshots:
       clsx: 2.1.1
       react: 18.3.1
 
+  '@react-aria/utils@3.25.3(react@18.3.1)':
+    dependencies:
+      '@react-aria/ssr': 3.9.6(react@18.3.1)
+      '@react-stately/utils': 3.10.4(react@18.3.1)
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      '@swc/helpers': 0.5.11
+      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)
@@ -30979,6 +31293,12 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/toast@3.0.0-beta.6(react@18.3.1)':
+    dependencies:
+      '@swc/helpers': 0.5.11
+      react: 18.3.1
+      use-sync-external-store: 1.2.0(react@18.3.1)
+
   '@react-stately/toggle@3.7.7(react@18.3.1)':
     dependencies:
       '@react-stately/utils': 3.10.3(react@18.3.1)
@@ -31012,6 +31332,11 @@ snapshots:
       '@swc/helpers': 0.5.11
       react: 18.3.1
 
+  '@react-stately/utils@3.10.4(react@18.3.1)':
+    dependencies:
+      '@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)
@@ -31025,6 +31350,11 @@ snapshots:
       '@react-types/shared': 3.24.1(react@18.3.1)
       react: 18.3.1
 
+  '@react-types/button@3.10.0(react@18.3.1)':
+    dependencies:
+      '@react-types/shared': 3.25.0(react@18.3.1)
+      react: 18.3.1
+
   '@react-types/button@3.9.6(react@18.3.1)':
     dependencies:
       '@react-types/shared': 3.24.1(react@18.3.1)
@@ -31136,6 +31466,10 @@ snapshots:
     dependencies:
       react: 18.3.1
 
+  '@react-types/shared@3.25.0(react@18.3.1)':
+    dependencies:
+      react: 18.3.1
+
   '@react-types/slider@3.7.5(react@18.3.1)':
     dependencies:
       '@react-types/shared': 3.24.1(react@18.3.1)
@@ -31513,6 +31847,17 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bigint-buffer: 1.1.5
+      bignumber.js: 9.1.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solana/buffer-layout@4.0.1':
     dependencies:
       buffer: 6.0.3
@@ -31636,6 +31981,17 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/spl-token@0.3.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solana/spl-token@0.4.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)':
     dependencies:
       '@solana/buffer-layout': 4.0.1
@@ -32392,6 +32748,28 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@babel/runtime': 7.25.0
+      '@noble/curves': 1.4.2
+      '@noble/hashes': 1.4.0
+      '@solana/buffer-layout': 4.0.1
+      agentkeepalive: 4.5.0
+      bigint-buffer: 1.1.5
+      bn.js: 5.2.1
+      borsh: 0.7.0
+      bs58: 4.0.1
+      buffer: 6.0.3
+      fast-stable-stringify: 1.0.0
+      jayson: 4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      node-fetch: 2.7.0(encoding@0.1.13)
+      rpc-websockets: 8.0.1
+      superstruct: 1.0.4
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solflare-wallet/metamask-sdk@1.0.3(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/wallet-standard-features': 1.2.0
@@ -40365,6 +40743,20 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  eth-crypto@2.6.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@babel/runtime': 7.20.13
+      '@ethereumjs/tx': 3.5.2
+      '@types/bn.js': 5.1.1
+      eccrypto: 1.1.6(patch_hash=rjcfmtfgn3z72mudpdif5oxmye)
+      ethereumjs-util: 7.1.5
+      ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      secp256k1: 5.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   eth-ens-namehash@2.0.8:
     dependencies:
       idna-uts46-hx: 2.3.1
@@ -40743,6 +41135,43 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@ethersproject/abi': 5.7.0
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/basex': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/contracts': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/hdnode': 5.7.0
+      '@ethersproject/json-wallets': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/networks': 5.7.1
+      '@ethersproject/pbkdf2': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/providers': 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/signing-key': 5.7.0
+      '@ethersproject/solidity': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/units': 5.7.0
+      '@ethersproject/wallet': 5.7.0
+      '@ethersproject/web': 5.7.1
+      '@ethersproject/wordlists': 5.7.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   ethers@6.13.2(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     dependencies:
       '@adraffy/ens-normalize': 1.10.1
@@ -42593,13 +43022,17 @@ snapshots:
     dependencies:
       ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
 
+  isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
+    dependencies:
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+
   isomorphic-ws@4.0.1(ws@8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
     dependencies:
       ws: 8.16.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
-  isomorphic-ws@4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
+  isomorphic-ws@4.0.1(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
     dependencies:
-      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
   isomorphic-ws@5.0.0(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
     dependencies:
@@ -42791,6 +43224,24 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  jayson@4.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      JSONStream: 1.3.5
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      json-stringify-safe: 5.0.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   jayson@4.1.1(bufferutil@4.0.7)(utf-8-validate@5.0.10):
     dependencies:
       '@types/connect': 3.4.38
@@ -42845,6 +43296,24 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  jayson@4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      JSONStream: 1.3.5
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      json-stringify-safe: 5.0.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   jest-changed-files@27.5.1:
     dependencies:
       '@jest/types': 27.5.1
@@ -53230,6 +53699,12 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
+  ws@7.4.6(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+    optional: true
+
   ws@7.5.10(bufferutil@4.0.7)(utf-8-validate@5.0.10):
     optionalDependencies:
       bufferutil: 4.0.7
@@ -53245,6 +53720,11 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
+  ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+
   ws@8.11.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
     optionalDependencies:
       bufferutil: 4.0.8
@@ -53280,6 +53760,11 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
+  ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+
   xdg-app-paths@5.1.0:
     dependencies:
       xdg-portable: 7.3.0