Browse Source

Merge pull request #2021 from pyth-network/staking-app-release-batch

Staking app release batch
Connor Prussin 1 năm trước cách đây
mục cha
commit
c39c85ea62
32 tập tin đã thay đổi với 1134 bổ sung318 xóa
  1. BIN
      apps/staking/public/publisher-icons/blocksize.png
  2. 59 19
      apps/staking/src/api.ts
  3. 198 0
      apps/staking/src/components/Changelog/index.tsx
  4. 4 4
      apps/staking/src/components/CopyButton/index.tsx
  5. 1 2
      apps/staking/src/components/Dashboard/index.tsx
  6. 2 2
      apps/staking/src/components/Header/current-stake-account.tsx
  7. 30 2
      apps/staking/src/components/Header/help-menu.tsx
  8. 28 18
      apps/staking/src/components/Header/index.tsx
  9. 101 0
      apps/staking/src/components/Header/program-parameters.tsx
  10. 81 0
      apps/staking/src/components/Header/stats.tsx
  11. 2 0
      apps/staking/src/components/ModalDialog/index.tsx
  12. 224 100
      apps/staking/src/components/OracleIntegrityStaking/index.tsx
  13. 4 1
      apps/staking/src/components/Root/index.tsx
  14. 2 0
      apps/staking/src/components/Tokens/index.tsx
  15. 7 2
      apps/staking/src/components/Tooltip/index.tsx
  16. 2 1
      apps/staking/src/components/WalletButton/index.tsx
  17. 1 0
      apps/staking/src/config/server.ts
  18. 39 12
      apps/staking/src/hooks/use-api.tsx
  19. 41 0
      apps/staking/src/hooks/use-changelog.ts
  20. 6 0
      apps/staking/src/known-publishers.ts
  21. 76 0
      governance/pyth_staking_sdk/idl/stake-caps-parameters.json
  22. 1 0
      governance/pyth_staking_sdk/package.json
  23. 4 0
      governance/pyth_staking_sdk/src/constants.ts
  24. 1 0
      governance/pyth_staking_sdk/src/index.ts
  25. 8 0
      governance/pyth_staking_sdk/src/pdas.ts
  26. 13 0
      governance/pyth_staking_sdk/src/pyth-staking-client.ts
  27. 55 0
      governance/pyth_staking_sdk/src/pythnet-client.ts
  28. 1 0
      governance/pyth_staking_sdk/src/types.ts
  29. 17 2
      governance/pyth_staking_sdk/src/utils/apy.ts
  30. 13 5
      governance/pyth_staking_sdk/src/utils/pool.ts
  31. 82 0
      governance/pyth_staking_sdk/types/stake-caps-parameters.ts
  32. 31 148
      pnpm-lock.yaml

BIN
apps/staking/public/publisher-icons/blocksize.png


+ 59 - 19
apps/staking/src/api.ts

@@ -5,12 +5,15 @@ import {
   getAmountByTargetAndState,
   getCurrentEpoch,
   PositionState,
+  PythnetClient,
   PythStakingClient,
   type StakeAccountPositions,
 } from "@pythnetwork/staking-sdk";
 import { PublicKey } from "@solana/web3.js";
 import { z } from "zod";
 
+import { KNOWN_PUBLISHERS } from "./known-publishers";
+
 const publishersRankingSchema = z
   .object({
     publisher: z.string(),
@@ -43,8 +46,15 @@ type Data = {
     cooldown2: bigint;
   };
   yieldRate: bigint;
+  m: bigint;
+  z: bigint;
   integrityStakingPublishers: {
-    name: string | undefined;
+    identity:
+      | {
+          name: string;
+          icon: string;
+        }
+      | undefined;
     publicKey: PublicKey;
     stakeAccount: PublicKey | undefined;
     selfStake: bigint;
@@ -54,7 +64,8 @@ type Data = {
     poolUtilizationDelta: bigint;
     numFeeds: number;
     qualityRanking: number;
-    apyHistory: { date: Date; apy: number }[];
+    delegationFee: bigint;
+    apyHistory: { date: Date; apy: number; selfApy: number }[];
     positions?:
       | {
           warmup?: bigint | undefined;
@@ -95,18 +106,29 @@ export const getStakeAccount = async (
 
 export const loadData = async (
   client: PythStakingClient,
+  pythnetClient: PythnetClient,
   hermesClient: HermesClient,
   stakeAccount?: PublicKey | undefined,
 ): Promise<Data> =>
   stakeAccount === undefined
-    ? loadDataNoStakeAccount(client, hermesClient)
-    : loadDataForStakeAccount(client, hermesClient, stakeAccount);
+    ? loadDataNoStakeAccount(client, pythnetClient, hermesClient)
+    : loadDataForStakeAccount(
+        client,
+        pythnetClient,
+        hermesClient,
+        stakeAccount,
+      );
 
 const loadDataNoStakeAccount = async (
   client: PythStakingClient,
+  pythnetClient: PythnetClient,
   hermesClient: HermesClient,
 ): Promise<Data> => {
-  const { publishers, ...baseInfo } = await loadBaseInfo(client, hermesClient);
+  const { publishers, ...baseInfo } = await loadBaseInfo(
+    client,
+    pythnetClient,
+    hermesClient,
+  );
 
   return {
     ...baseInfo,
@@ -127,6 +149,7 @@ const loadDataNoStakeAccount = async (
 
 const loadDataForStakeAccount = async (
   client: PythStakingClient,
+  pythnetClient: PythnetClient,
   hermesClient: HermesClient,
   stakeAccount: PublicKey,
 ): Promise<Data> => {
@@ -137,7 +160,7 @@ const loadDataForStakeAccount = async (
     claimableRewards,
     stakeAccountPositions,
   ] = await Promise.all([
-    loadBaseInfo(client, hermesClient),
+    loadBaseInfo(client, pythnetClient, hermesClient),
     client.getStakeAccountCustody(stakeAccount),
     client.getUnlockSchedule(stakeAccount),
     client.getClaimableRewards(stakeAccount),
@@ -196,45 +219,61 @@ const loadDataForStakeAccount = async (
 
 const loadBaseInfo = async (
   client: PythStakingClient,
+  pythnetClient: PythnetClient,
   hermesClient: HermesClient,
 ) => {
-  const [publishers, walletAmount, poolConfig, currentEpoch] =
+  const [publishers, walletAmount, poolConfig, currentEpoch, parameters] =
     await Promise.all([
-      loadPublisherData(client, hermesClient),
+      loadPublisherData(client, pythnetClient, hermesClient),
       client.getOwnerPythBalance(),
       client.getPoolConfigAccount(),
       getCurrentEpoch(client.connection),
+      pythnetClient.getStakeCapParameters(),
     ]);
 
-  return { yieldRate: poolConfig.y, walletAmount, publishers, currentEpoch };
+  return {
+    yieldRate: poolConfig.y,
+    walletAmount,
+    publishers,
+    currentEpoch,
+    m: parameters.m,
+    z: parameters.z,
+  };
 };
 
 const loadPublisherData = async (
   client: PythStakingClient,
+  pythnetClient: PythnetClient,
   hermesClient: HermesClient,
 ) => {
-  const [poolData, publisherRankings, publisherCaps] = await Promise.all([
-    client.getPoolDataAccount(),
-    getPublisherRankings(),
-    hermesClient.getLatestPublisherCaps({
-      parsed: true,
-    }),
-  ]);
+  const [poolData, publisherRankings, publisherCaps, publisherNumberOfSymbols] =
+    await Promise.all([
+      client.getPoolDataAccount(),
+      getPublisherRankings(),
+      hermesClient.getLatestPublisherCaps({
+        parsed: true,
+      }),
+      pythnetClient.getPublisherNumberOfSymbols(),
+    ]);
 
   return extractPublisherData(poolData).map((publisher) => {
     const publisherPubkeyString = publisher.pubkey.toBase58();
     const publisherRanking = publisherRankings.find(
       (ranking) => ranking.publisher === publisherPubkeyString,
     );
-    const apyHistory = publisher.apyHistory.map(({ epoch, apy }) => ({
+    const numberOfSymbols = publisherNumberOfSymbols[publisherPubkeyString];
+    const apyHistory = publisher.apyHistory.map(({ epoch, apy, selfApy }) => ({
       date: epochToDate(epoch + 1n),
       apy,
+      selfApy,
     }));
 
     return {
       apyHistory,
-      name: undefined, // TODO
-      numFeeds: publisherRanking?.numSymbols ?? 0,
+      identity: (
+        KNOWN_PUBLISHERS as Record<string, { name: string; icon: string }>
+      )[publisher.pubkey.toBase58()],
+      numFeeds: numberOfSymbols ?? 0,
       poolCapacity: getPublisherCap(publisherCaps, publisher.pubkey),
       poolUtilization: publisher.totalDelegation,
       poolUtilizationDelta: publisher.totalDelegationDelta,
@@ -243,6 +282,7 @@ const loadPublisherData = async (
       selfStake: publisher.selfDelegation,
       selfStakeDelta: publisher.selfDelegationDelta,
       stakeAccount: publisher.stakeAccount ?? undefined,
+      delegationFee: publisher.delegationFee,
     };
   });
 };

+ 198 - 0
apps/staking/src/components/Changelog/index.tsx

@@ -0,0 +1,198 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { useDateFormatter } from "react-aria";
+
+import { useChangelog } from "../../hooks/use-changelog";
+import { Link } from "../Link";
+import { ModalDialog } from "../ModalDialog";
+
+export const Changelog = () => {
+  const { isOpen, toggleOpen } = useChangelog();
+
+  return (
+    <ModalDialog title="Changelog" isOpen={isOpen} onOpenChange={toggleOpen}>
+      <ul className="flex max-w-prose flex-col divide-y divide-neutral-600/50">
+        {messages.map(({ id, message }) => (
+          <li key={id}>{message}</li>
+        ))}
+      </ul>
+    </ModalDialog>
+  );
+};
+
+type ChangelogMessageProps = {
+  date: Date;
+  children: ReactNode | ReactNode[];
+};
+
+export const ChangelogMessage = ({ date, children }: ChangelogMessageProps) => {
+  const dateFormatter = useDateFormatter({
+    year: "numeric",
+    month: "short",
+    day: "numeric",
+  });
+
+  return (
+    <section className="py-8">
+      <h2 className="text-sm uppercase text-pythpurple-400">
+        {dateFormatter.format(date)}
+      </h2>
+      {children}
+    </section>
+  );
+};
+
+type ChangelogSectionProps = {
+  title: ReactNode;
+  children: ReactNode | ReactNode[];
+};
+
+export const ChangelogSection = ({
+  title,
+  children,
+}: ChangelogSectionProps) => (
+  <section className="mt-4">
+    <h3 className="text-lg font-semibold text-white">{title}</h3>
+    <div className="flex flex-col gap-2 pl-2 text-sm opacity-70">
+      {children}
+    </div>
+  </section>
+);
+
+export const messages = [
+  {
+    id: 1,
+    message: (
+      <ChangelogMessage date={new Date("2024-10-10")}>
+        <ChangelogSection title="Milestones">
+          <div>
+            <p>
+              We are pleased to announce the following Oracle Integrity Staking
+              milestones:
+            </p>
+            <ul className="list-disc pl-8">
+              <li>145M PYTH staked and securing DeFi.</li>
+              <li>10K unique stakers participating.</li>
+              <li>492K in PYTH programmatically distributed.</li>
+            </ul>
+          </div>
+          <p>We’re thrilled to see so many community participants.</p>
+        </ChangelogSection>
+        <ChangelogSection title="New Features to the Staking Frontend">
+          <ul className="list-disc pl-4">
+            <li>
+              New sort filter for publishers list. Publishers with self-stake
+              are displayed first by default. You can sort by publisher details,
+              pool composition, and more.
+            </li>
+            <li>
+              Publishers interested in de-anonymizing themselves can have their
+              names displayed in the publisher list.
+            </li>
+            <li>New OIS live stats added to navigation bar.</li>
+            <li>
+              New dialogue added under “Help” where you can view current program
+              parameters.
+            </li>
+            <li>
+              Option to remove PYTH from the smart contract program for parties
+              with restricted access to the staking frontend.
+            </li>
+            <li>
+              Full access to Pyth Governance for certain restricted
+              jurisdictions.
+            </li>
+            <li>APYs are now shown as net of delegation fees.</li>
+            <li>
+              Updates to educational materials (all Guides and FAQs) for clarity
+              and readability.
+            </li>
+            <li>
+              New Oracle Integrity Staking{" "}
+              <Link
+                href="https://forum.pyth.network/c/oracle-integrity-staking-ois-discussion/8"
+                className="underline"
+                target="_blank"
+              >
+                discussion catalogue
+              </Link>{" "}
+              opened in Pyth DAO forum. Let the community know your thoughts and
+              feedback!
+            </li>
+          </ul>
+        </ChangelogSection>
+        <ChangelogSection title="Security">
+          <p>
+            The Pyth contributors take security extremely seriously. The
+            contract code is{" "}
+            <Link
+              href="https://github.com/pyth-network/governance/tree/main/staking/programs/staking"
+              className="underline"
+              target="_blank"
+            >
+              open source
+            </Link>{" "}
+            and the upgrade authority is governed by the Pyth DAO. The official{" "}
+            <Link
+              href="https://github.com/pyth-network/audit-reports/blob/main/2024_09_11/pyth_cip_final_report.pdf"
+              className="underline"
+              target="_blank"
+            >
+              audit report
+            </Link>{" "}
+            is publicly accessible. All on-chain contract codes are verified
+            using{" "}
+            <Link
+              href="https://github.com/Ellipsis-Labs/solana-verifiable-build/"
+              className="underline"
+              target="_blank"
+            >
+              Solana verifiable build
+            </Link>{" "}
+            and the Pyth DAO governs the upgrade authority.
+          </p>
+        </ChangelogSection>
+        <ChangelogSection title="Best Practices">
+          <p>
+            Please remember that publishers have priority for programmatic
+            rewards distributions. By protocol design, if a pool’s stake cap is
+            exceeded, the programmatic reward rate for other stakers
+            participating in that pool will be lower than the Pyth DAO-set
+            maximum reward rate.
+          </p>
+        </ChangelogSection>
+        <ChangelogSection title="Acknowledgements">
+          <p>
+            The Pyth contributors are glad to see so many network participants
+            getting involved with Oracle Integrity Staking to help secure the
+            oracle and protect the wider DeFi industry. OIS wouldn’t be possible
+            without you!
+          </p>
+        </ChangelogSection>
+        <ChangelogSection title="Feedback">
+          <p>
+            Please reach out in the official{" "}
+            <Link
+              href="https://discord.com/invite/PythNetwork"
+              className="underline"
+              target="_blank"
+            >
+              Pyth Discord
+            </Link>{" "}
+            or the{" "}
+            <Link
+              href="https://forum.pyth.network"
+              className="underline"
+              target="_blank"
+            >
+              Pyth DAO Forum
+            </Link>{" "}
+            to share your questions, ideas, or feedback. We want to hear what
+            you think.
+          </p>
+        </ChangelogSection>
+      </ChangelogMessage>
+    ),
+  },
+];

+ 4 - 4
apps/staking/src/components/CopyButton/index.tsx

@@ -52,7 +52,7 @@ export const CopyButton = ({
       onPress={copy}
       isDisabled={isCopied}
       className={clsx(
-        "group -mx-2 -mt-0.5 rounded-md px-2 py-0.5 align-middle transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
+        "group mx-[-0.25em] -mt-0.5 inline-block rounded-md px-[0.25em] py-0.5 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
         className,
       )}
       {...(isCopied && { "data-is-copied": true })}
@@ -60,13 +60,13 @@ export const CopyButton = ({
     >
       {(...args) => (
         <>
-          <span className="align-middle">
+          <span>
             {typeof children === "function" ? children(...args) : children}
           </span>
-          <span className="relative ml-[0.25em] inline-block align-middle">
+          <span className="relative top-[0.125em] ml-[0.25em] inline-block">
             <span className="opacity-50 transition-opacity duration-100 group-data-[is-copied]:opacity-0">
               <ClipboardDocumentIcon className="size-[1em]" />
-              <div className="sr-only">Copy code to clipboaord</div>
+              <div className="sr-only">Copy to clipboard</div>
             </span>
             <CheckIcon className="absolute inset-0 text-green-600 opacity-0 transition-opacity duration-100 group-data-[is-copied]:opacity-100" />
           </span>

+ 1 - 2
apps/staking/src/components/Dashboard/index.tsx

@@ -249,8 +249,7 @@ const useIntegrityStakingSum = (
       publishers
         .map((publisher) => publisher.positions?.[field] ?? 0n)
         .reduce((acc, cur) => acc + cur, 0n),
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    publishers.map((publisher) => publisher.positions?.[field]),
+    [publishers, field],
   );
 
 // eslint-disable-next-line unicorn/no-array-reduce

+ 2 - 2
apps/staking/src/components/Header/current-stake-account.tsx

@@ -20,7 +20,7 @@ export const CurrentStakeAccount = ({
   return api.type === ApiStateType.Loaded && !isBlocked ? (
     <div
       className={clsx(
-        "hidden flex-col items-end justify-center text-xs xs:flex md:flex-row md:items-center md:text-sm",
+        "hidden flex-col items-end justify-center text-xs xs:flex xl:flex-row xl:items-center xl:text-sm",
         className,
       )}
       {...props}
@@ -28,7 +28,7 @@ export const CurrentStakeAccount = ({
       <div className="font-semibold">Stake account:</div>
       <CopyButton
         text={api.account.toBase58()}
-        className="-mr-2 text-pythpurple-400 md:ml-2 md:mr-0"
+        className="text-pythpurple-400 xl:ml-2 xl:mr-0"
       >
         <TruncatedKey>{api.account}</TruncatedKey>
       </CopyButton>

+ 30 - 2
apps/staking/src/components/Header/help-menu.tsx

@@ -7,6 +7,9 @@ import {
 import { useState, useCallback } from "react";
 import { MenuTrigger, Button } from "react-aria-components";
 
+import { ProgramParameters } from "./program-parameters";
+import { StateType, useApi } from "../../hooks/use-api";
+import { useChangelog } from "../../hooks/use-changelog";
 import { GeneralFaq } from "../GeneralFaq";
 import { GovernanceGuide } from "../GovernanceGuide";
 import { Menu, MenuItem, Section, Separator } from "../Menu";
@@ -14,6 +17,7 @@ import { OracleIntegrityStakingGuide } from "../OracleIntegrityStakingGuide";
 import { PublisherFaq } from "../PublisherFaq";
 
 export const HelpMenu = () => {
+  const api = useApi();
   const [faqOpen, setFaqOpen] = useState(false);
   const openFaq = useCallback(() => {
     setFaqOpen(true);
@@ -34,12 +38,18 @@ export const HelpMenu = () => {
     setPublisherFaqOpen(true);
   }, [setPublisherFaqOpen]);
 
+  const [parametersOpen, setParametersOpen] = useState(false);
+  const openParameters = useCallback(() => {
+    setParametersOpen(true);
+  }, [setParametersOpen]);
+  const { open: openChangelog } = useChangelog();
+
   return (
     <>
       <MenuTrigger>
-        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm p-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 sm:-mx-4 sm:px-4">
+        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm p-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 md:-mx-4 md:px-4">
           <QuestionMarkCircleIcon className="size-6 flex-none" />
-          <span className="sr-only xs:not-sr-only">Help</span>
+          <span className="sr-only md:not-sr-only">Help</span>
           <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
         </Button>
         <Menu placement="bottom end">
@@ -65,6 +75,16 @@ export const HelpMenu = () => {
               Data Publisher Guide
             </MenuItem>
           </Section>
+          <Separator />
+          <Section>
+            {(api.type === StateType.Loaded ||
+              api.type === StateType.LoadedNoStakeAccount) && (
+              <MenuItem onAction={openParameters}>
+                Current Program Parameters
+              </MenuItem>
+            )}
+            <MenuItem onAction={openChangelog}>Changelog</MenuItem>
+          </Section>
         </Menu>
       </MenuTrigger>
       <GeneralFaq isOpen={faqOpen} onOpenChange={setFaqOpen} />
@@ -80,6 +100,14 @@ export const HelpMenu = () => {
         isOpen={publisherFaqOpen}
         onOpenChange={setPublisherFaqOpen}
       />
+      {(api.type === StateType.Loaded ||
+        api.type === StateType.LoadedNoStakeAccount) && (
+        <ProgramParameters
+          api={api}
+          isOpen={parametersOpen}
+          onOpenChange={setParametersOpen}
+        />
+      )}
     </>
   );
 };

+ 28 - 18
apps/staking/src/components/Header/index.tsx

@@ -5,6 +5,7 @@ import { CurrentStakeAccount } from "./current-stake-account";
 import { HelpMenu } from "./help-menu";
 import Logo from "./logo.svg";
 import Logomark from "./logomark.svg";
+import { Stats } from "./stats";
 import { Link } from "../Link";
 import { MaxWidth } from "../MaxWidth";
 import { WalletButton } from "../WalletButton";
@@ -13,22 +14,31 @@ export const Header = ({
   className,
   ...props
 }: Omit<HTMLAttributes<HTMLElement>, "children">) => (
-  <header className={clsx("sticky top-0 w-full lg:px-4", className)} {...props}>
-    <div className="border-b border-neutral-600/50 bg-pythpurple-800 lg:border-x">
-      <MaxWidth className="flex h-header items-center justify-between gap-2 lg:-mx-4">
-        <Link
-          href="/"
-          className="-mx-2 h-[calc(var(--header-height)_-_0.5rem)] rounded-sm p-2 text-pythpurple-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
-        >
-          <Logo className="hidden h-full sm:block" />
-          <Logomark className="h-full sm:hidden" />
-        </Link>
-        <div className="flex flex-none flex-row items-stretch gap-4 sm:gap-8">
-          <CurrentStakeAccount />
-          <WalletButton className="flex-none" />
-          <HelpMenu />
-        </div>
-      </MaxWidth>
-    </div>
-  </header>
+  <>
+    <header
+      className={clsx("sticky top-0 w-full lg:px-4", className)}
+      {...props}
+    >
+      <div className="border-b border-neutral-600/50 bg-pythpurple-800 lg:border-x">
+        <MaxWidth className="flex h-header items-center justify-between gap-2 lg:-mx-4">
+          <div className="flex flex-row items-center gap-8 lg:gap-12">
+            <Link
+              href="/"
+              className="-mx-2 h-[calc(var(--header-height)_-_0.5rem)] rounded-sm p-2 text-pythpurple-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+            >
+              <Logo className="hidden h-full lg:block" />
+              <Logomark className="h-full lg:hidden" />
+            </Link>
+            <Stats className="hidden gap-4 sm:flex lg:gap-6" />
+          </div>
+          <div className="flex flex-none flex-row items-stretch gap-4 md:gap-8">
+            <CurrentStakeAccount />
+            <WalletButton className="flex-none" />
+            <HelpMenu />
+          </div>
+        </MaxWidth>
+      </div>
+    </header>
+    <Stats className="border-b border-neutral-600/50 py-4 text-center sm:hidden" />
+  </>
 );

+ 101 - 0
apps/staking/src/components/Header/program-parameters.tsx

@@ -0,0 +1,101 @@
+import type { ComponentProps, ReactNode } from "react";
+
+import type { StateType, States } from "../../hooks/use-api";
+import { StateType as DataStateType, useData } from "../../hooks/use-data";
+import { tokensToString } from "../../tokens";
+import { Link } from "../Link";
+import { ModalDialog } from "../ModalDialog";
+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;
+
+type Props = Omit<ComponentProps<typeof ModalDialog>, "title" | "children"> & {
+  api: States[StateType.Loaded] | States[StateType.LoadedNoStakeAccount];
+};
+
+export const ProgramParameters = ({ api, ...props }: Props) => {
+  const data = useData(api.dashboardDataCacheKey, api.loadData, {
+    refreshInterval: REFRESH_INTERVAL,
+  });
+
+  return (
+    <ModalDialog
+      title="Program Parameters"
+      description={
+        <>
+          See the current program parameters. For more details, see{" "}
+          <Link
+            href="https://docs.pyth.network/home/oracle-integrity-staking/mathematical-representation"
+            className="underline"
+            target="_blank"
+          >
+            the docs
+          </Link>
+        </>
+      }
+      {...props}
+    >
+      <ul className="mb-4 mt-8 flex flex-col gap-4 sm:mb-8 sm:mt-16">
+        <Parameter
+          value={
+            data.type === DataStateType.Loaded ? (
+              <Tokens>{data.data.m}</Tokens>
+            ) : (
+              <Loading />
+            )
+          }
+          variable="M"
+        >
+          A constant parameter representing the target stake per symbol
+        </Parameter>
+        <Parameter
+          value={
+            data.type === DataStateType.Loaded ? (
+              data.data.z.toString()
+            ) : (
+              <Loading />
+            )
+          }
+          variable="Z"
+        >
+          A constant parameter to control cap contribution from symbols with a
+          low number of publishers
+        </Parameter>
+        <Parameter
+          value={
+            data.type === DataStateType.Loaded ? (
+              `${tokensToString(data.data.yieldRate * 100n)}% / epoch`
+            ) : (
+              <Loading />
+            )
+          }
+          variable="y"
+        >
+          The cap to the rate of rewards for any pool
+        </Parameter>
+      </ul>
+    </ModalDialog>
+  );
+};
+
+type ParameterProps = {
+  value: ReactNode;
+  variable: ReactNode;
+  children: ReactNode;
+};
+
+const Parameter = ({ variable, value, children }: ParameterProps) => (
+  <li className="relative rounded-md bg-white/5 p-5">
+    <div className="absolute right-2 top-2 grid size-6 place-content-center rounded-md bg-pythpurple-100 text-center text-sm font-semibold text-pythpurple-950">
+      {variable}
+    </div>
+    <div className="mb-2 text-2xl font-semibold leading-none">{value}</div>
+    <p className="max-w-sm text-sm opacity-60">{children}</p>
+  </li>
+);
+
+const Loading = () => (
+  <div className="h-6 w-10 animate-pulse rounded-md bg-white/30" />
+);

+ 81 - 0
apps/staking/src/components/Header/stats.tsx

@@ -0,0 +1,81 @@
+"use client";
+
+import { PythStakingClient } from "@pythnetwork/staking-sdk";
+import { useConnection } from "@solana/wallet-adapter-react";
+import { Connection } from "@solana/web3.js";
+import clsx from "clsx";
+import type { HTMLProps } from "react";
+
+import { StateType, useData } from "../../hooks/use-data";
+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;
+const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n;
+
+export const Stats = ({ className, ...props }: HTMLProps<HTMLDivElement>) => {
+  const { connection } = useConnection();
+  const state = useData("poolStats", () => fetchStats(connection), {
+    refreshInterval: REFRESH_INTERVAL,
+  });
+
+  return (
+    <div className={clsx("flex flex-row items-stretch", className)} {...props}>
+      <div className="flex-1 sm:flex-none">
+        {state.type === StateType.Loaded ? (
+          <Tokens className="mb-1 text-xl font-semibold leading-none">
+            {state.data.totalStaked}
+          </Tokens>
+        ) : (
+          <Loading />
+        )}
+        <div className="text-xs leading-none text-pythpurple-400">
+          OIS Total Staked
+        </div>
+      </div>
+      <div className="border-l border-neutral-600/50" />
+      <div className="flex-1 sm:flex-none">
+        {state.type === StateType.Loaded ? (
+          <Tokens className="mb-1 text-xl font-semibold leading-none">
+            {state.data.rewardsDistributed}
+          </Tokens>
+        ) : (
+          <Loading />
+        )}
+        <div className="text-xs leading-none text-pythpurple-400">
+          OIS Rewards Distributed
+        </div>
+      </div>
+    </div>
+  );
+};
+
+const Loading = () => (
+  <div className="inline-block h-[1em] w-10 animate-pulse rounded-md bg-white/30" />
+);
+
+const fetchStats = async (connection: Connection) => {
+  const client = new PythStakingClient({ connection });
+  const [poolData, rewardCustodyAccount] = await Promise.all([
+    client.getPoolDataAccount(),
+    client.getRewardCustodyAccount(),
+  ]);
+
+  return {
+    totalStaked:
+      sumDelegations(poolData.delState) + sumDelegations(poolData.selfDelState),
+    rewardsDistributed:
+      poolData.claimableRewards +
+      INITIAL_REWARD_POOL_SIZE -
+      rewardCustodyAccount.amount,
+  };
+};
+
+const sumDelegations = (
+  values: { totalDelegation: bigint; deltaDelegation: bigint }[],
+) =>
+  values.reduce(
+    (acc, value) => acc + value.totalDelegation + value.deltaDelegation,
+    0n,
+  );

+ 2 - 0
apps/staking/src/components/ModalDialog/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import { XMarkIcon } from "@heroicons/react/24/outline";
 import clsx from "clsx";
 import type { ComponentProps, ReactNode } from "react";

+ 224 - 100
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -10,6 +10,7 @@ import {
 import { calculateApy } from "@pythnetwork/staking-sdk";
 import { PublicKey } from "@solana/web3.js";
 import clsx from "clsx";
+import Image from "next/image";
 import {
   useMemo,
   useCallback,
@@ -190,13 +191,13 @@ const SelfStaking = ({
           <div className="sticky left-0 mb-4 flex flex-row items-start justify-between px-4 sm:px-10 sm:pb-4 sm:pt-6 lg:items-center">
             <div>
               <h3 className="text-2xl font-light">Self Staking</h3>
-              <PublisherName
+              <PublisherIdentity
                 truncatedClassName="2xl:hidden"
                 fullClassName="hidden 2xl:inline"
                 className="opacity-60"
               >
                 {self}
-              </PublisherName>
+              </PublisherIdentity>
             </div>
             <div className="flex flex-row items-center gap-4">
               <MenuTrigger>
@@ -353,10 +354,12 @@ const ReassignStakeAccount = ({
       closeDisabled={closeDisabled}
       description={
         <>
-          <span className="mr-3 align-middle">
+          <span className="mr-[0.5em]">
             Designate a different stake account as the self-staking account for
           </span>
-          <PublisherName className="font-semibold">{self}</PublisherName>
+          <PublisherIdentity className="font-semibold">
+            {self}
+          </PublisherIdentity>
         </>
       }
       {...props}
@@ -603,7 +606,7 @@ const PublisherList = ({
   const scrollTarget = useRef<HTMLDivElement | null>(null);
   const [search, setSearch] = useState("");
   const [yoursFirst, setYoursFirst] = useState(true);
-  const [sort, setSort] = useState(SortOption.RemainingPoolDescending);
+  const [sort, setSort] = useState(SortOption.SelfStakeDescending);
   const filter = useFilter({ sensitivity: "base", usage: "search" });
   const [currentPage, setPage] = useState(1);
   const collator = useCollator();
@@ -613,8 +616,8 @@ const PublisherList = ({
         .filter(
           (publisher) =>
             filter.contains(publisher.publicKey.toBase58(), search) ||
-            (publisher.name !== undefined &&
-              filter.contains(publisher.name, search)),
+            (publisher.identity !== undefined &&
+              filter.contains(publisher.identity.name, search)),
         )
         .sort((a, b) => {
           if (yoursFirst) {
@@ -626,7 +629,7 @@ const PublisherList = ({
               return 1;
             }
           }
-          return doSort(collator, a, b, yieldRate, sort);
+          return compare(collator, a, b, yieldRate, sort);
         }),
     [publishers, search, sort, filter, yieldRate, yoursFirst, collator],
   );
@@ -938,7 +941,7 @@ const getPageRange = (
   return { first, count: Math.min(numPages - first + 1, 5) };
 };
 
-const doSort = (
+const compare = (
   collator: Intl.Collator,
   a: PublisherProps["publisher"],
   b: PublisherProps["publisher"],
@@ -948,80 +951,165 @@ const doSort = (
   switch (sort) {
     case SortOption.PublisherNameAscending:
     case SortOption.PublisherNameDescending: {
-      const value = collator.compare(
-        a.name ?? a.publicKey.toBase58(),
-        b.name ?? b.publicKey.toBase58(),
+      // No need for a fallback sort since each publisher has a unique value.
+      return compareName(
+        collator,
+        a,
+        b,
+        sort === SortOption.PublisherNameAscending,
       );
-      return sort === SortOption.PublisherNameAscending ? -1 * value : value;
     }
     case SortOption.ApyAscending:
     case SortOption.ApyDescending: {
-      const value =
-        calculateApy({
-          isSelf: false,
-          selfStake: a.selfStake + a.selfStakeDelta,
-          poolCapacity: a.poolCapacity,
-          poolUtilization: a.poolUtilization + a.poolUtilizationDelta,
-          yieldRate,
-        }) -
-        calculateApy({
-          isSelf: false,
-          selfStake: b.selfStake + b.selfStakeDelta,
-          poolCapacity: b.poolCapacity,
-          poolUtilization: b.poolUtilization + b.poolUtilizationDelta,
-          yieldRate,
-        });
-      return sort === SortOption.ApyDescending ? -1 * value : value;
-    }
-    case SortOption.NumberOfFeedsAscending: {
-      return Number(a.numFeeds - b.numFeeds);
+      const ascending = sort === SortOption.ApyAscending;
+      return compareInOrder([
+        () => compareApy(a, b, yieldRate, ascending),
+        () => compareSelfStake(a, b, ascending),
+        () => comparePoolCapacity(a, b, ascending),
+        () => compareName(collator, a, b, ascending),
+      ]);
     }
+    case SortOption.NumberOfFeedsAscending:
     case SortOption.NumberOfFeedsDescending: {
-      return Number(b.numFeeds - a.numFeeds);
+      const ascending = sort === SortOption.NumberOfFeedsAscending;
+      return compareInOrder([
+        () => (ascending ? -1 : 1) * Number(b.numFeeds - a.numFeeds),
+        () => compareSelfStake(a, b, ascending),
+        () => comparePoolCapacity(a, b, ascending),
+        () => compareApy(a, b, yieldRate, ascending),
+        () => compareName(collator, a, b, ascending),
+      ]);
     }
     case SortOption.RemainingPoolAscending:
     case SortOption.RemainingPoolDescending: {
-      if (a.poolCapacity === 0n && b.poolCapacity === 0n) {
-        return 0;
-      } else if (a.poolCapacity === 0n) {
-        return 1;
-      } else if (b.poolCapacity === 0n) {
-        return -1;
-      } else {
-        const remainingPoolA =
-          a.poolCapacity - a.poolUtilization - a.poolUtilizationDelta;
-        const remainingPoolB =
-          b.poolCapacity - b.poolUtilization - b.poolUtilizationDelta;
-        const value = Number(remainingPoolA - remainingPoolB);
-        return sort === SortOption.RemainingPoolDescending ? -1 * value : value;
-      }
+      const ascending = sort === SortOption.RemainingPoolAscending;
+      return compareInOrder([
+        () => comparePoolCapacity(a, b, ascending),
+        () => compareSelfStake(a, b, ascending),
+        () => compareApy(a, b, yieldRate, ascending),
+        () => compareName(collator, a, b, ascending),
+      ]);
     }
     case SortOption.QualityRankingDescending:
     case SortOption.QualityRankingAscending: {
-      if (a.qualityRanking === 0 && b.qualityRanking === 0) {
-        return 0;
-      } else if (a.qualityRanking === 0) {
-        return 1;
-      } else if (b.qualityRanking === 0) {
-        return -1;
-      } else {
-        const value = Number(a.qualityRanking - b.qualityRanking);
-        return sort === SortOption.QualityRankingAscending ? -1 * value : value;
-      }
-    }
-    case SortOption.SelfStakeAscending: {
-      return Number(
-        a.selfStake + a.selfStakeDelta - b.selfStake - b.selfStakeDelta,
+      // No need for a fallback sort since each publisher has a unique value.
+      return compareQualityRanking(
+        a,
+        b,
+        sort === SortOption.QualityRankingAscending,
       );
     }
+    case SortOption.SelfStakeAscending:
     case SortOption.SelfStakeDescending: {
-      return Number(
-        b.selfStake + b.selfStakeDelta - a.selfStake - a.selfStakeDelta,
-      );
+      const ascending = sort === SortOption.SelfStakeAscending;
+      return compareInOrder([
+        () => compareSelfStake(a, b, ascending),
+        () => comparePoolCapacity(a, b, ascending),
+        () => compareApy(a, b, yieldRate, ascending),
+        () => compareName(collator, a, b, ascending),
+      ]);
     }
   }
 };
 
+const compareInOrder = (comparisons: (() => number)[]): number => {
+  for (const compare of comparisons) {
+    const value = compare();
+    if (value !== 0) {
+      return value;
+    }
+  }
+  return 0;
+};
+
+const compareName = (
+  collator: Intl.Collator,
+  a: PublisherProps["publisher"],
+  b: PublisherProps["publisher"],
+  reverse?: boolean,
+) =>
+  (reverse ? -1 : 1) *
+  collator.compare(
+    a.identity?.name ?? a.publicKey.toBase58(),
+    b.identity?.name ?? b.publicKey.toBase58(),
+  );
+
+const compareApy = (
+  a: PublisherProps["publisher"],
+  b: PublisherProps["publisher"],
+  yieldRate: bigint,
+  reverse?: boolean,
+) =>
+  (reverse ? -1 : 1) *
+  (calculateApy({
+    isSelf: false,
+    selfStake: b.selfStake + b.selfStakeDelta,
+    poolCapacity: b.poolCapacity,
+    poolUtilization: b.poolUtilization + b.poolUtilizationDelta,
+    yieldRate,
+    delegationFee: b.delegationFee,
+  }) -
+    calculateApy({
+      isSelf: false,
+      selfStake: a.selfStake + a.selfStakeDelta,
+      poolCapacity: a.poolCapacity,
+      poolUtilization: a.poolUtilization + a.poolUtilizationDelta,
+      yieldRate,
+      delegationFee: a.delegationFee,
+    }));
+
+const comparePoolCapacity = (
+  a: PublisherProps["publisher"],
+  b: PublisherProps["publisher"],
+  reverse?: boolean,
+) => {
+  if (a.poolCapacity === 0n && b.poolCapacity === 0n) {
+    return 0;
+  } else if (a.poolCapacity === 0n) {
+    return 1;
+  } else if (b.poolCapacity === 0n) {
+    return -1;
+  } else {
+    const remainingPoolA =
+      a.poolCapacity - a.poolUtilization - a.poolUtilizationDelta;
+    const remainingPoolB =
+      b.poolCapacity - b.poolUtilization - b.poolUtilizationDelta;
+    if (remainingPoolA <= 0n && remainingPoolB <= 0n) {
+      return 0;
+    } else if (remainingPoolA <= 0n && remainingPoolB > 0n) {
+      return 1;
+    } else if (remainingPoolB <= 0n && remainingPoolA > 0n) {
+      return -1;
+    } else {
+      return (reverse ? -1 : 1) * Number(remainingPoolB - remainingPoolA);
+    }
+  }
+};
+
+const compareQualityRanking = (
+  a: PublisherProps["publisher"],
+  b: PublisherProps["publisher"],
+  reverse?: boolean,
+) => {
+  if (a.qualityRanking === 0 && b.qualityRanking === 0) {
+    return 0;
+  } else if (a.qualityRanking === 0) {
+    return 1;
+  } else if (b.qualityRanking === 0) {
+    return -1;
+  } else {
+    return (reverse ? -1 : 1) * Number(a.qualityRanking - b.qualityRanking);
+  }
+};
+
+const compareSelfStake = (
+  a: PublisherProps["publisher"],
+  b: PublisherProps["publisher"],
+  reverse?: boolean,
+) =>
+  (reverse ? -1 : 1) *
+  Number(b.selfStake + b.selfStakeDelta - (a.selfStake + a.selfStakeDelta));
+
 type SortablePublisherTableHeaderProps = Omit<
   ComponentProps<typeof BaseButton>,
   "children"
@@ -1081,7 +1169,12 @@ type PublisherProps = {
   totalStaked: bigint;
   isSelf?: boolean | undefined;
   publisher: {
-    name: string | undefined;
+    identity:
+      | {
+          name: string;
+          icon: string;
+        }
+      | undefined;
     publicKey: PublicKey;
     stakeAccount: PublicKey | undefined;
     selfStake: bigint;
@@ -1091,7 +1184,8 @@ type PublisherProps = {
     poolUtilizationDelta: bigint;
     numFeeds: number;
     qualityRanking: number;
-    apyHistory: { date: Date; apy: number }[];
+    delegationFee: bigint;
+    apyHistory: { date: Date; apy: number; selfApy: number }[];
     positions?:
       | {
           warmup?: bigint | undefined;
@@ -1152,6 +1246,7 @@ const Publisher = ({
         poolUtilization:
           publisher.poolUtilization + publisher.poolUtilizationDelta,
         yieldRate,
+        delegationFee: publisher.delegationFee,
       }).toFixed(2),
     [
       isSelf,
@@ -1160,6 +1255,7 @@ const Publisher = ({
       publisher.poolCapacity,
       publisher.poolUtilization,
       publisher.poolUtilizationDelta,
+      publisher.delegationFee,
       yieldRate,
     ],
   );
@@ -1168,13 +1264,14 @@ const Publisher = ({
     <div className="border-t border-neutral-600/50 p-4 sm:px-10 md:pt-8">
       {!isSelf && (
         <div className="flex flex-row items-center justify-between">
-          <PublisherName
+          <PublisherIdentity
             className="font-semibold"
             truncatedClassName="md:hidden"
             fullClassName="hidden md:inline"
+            withNameClassName="flex flex-col items-start"
           >
             {publisher}
-          </PublisherName>
+          </PublisherIdentity>
           <StakeToPublisherButton
             api={api}
             currentEpoch={currentEpoch}
@@ -1269,12 +1366,13 @@ const Publisher = ({
         {!isSelf && (
           <>
             <PublisherTableCell className="truncate py-4 pl-4 font-medium sm:pl-10">
-              <PublisherName
+              <PublisherIdentity
                 truncatedClassName="3xl:hidden"
                 fullClassName="hidden 3xl:inline"
+                withNameClassName="flex flex-col items-start"
               >
                 {publisher}
-              </PublisherName>
+              </PublisherIdentity>
             </PublisherTableCell>
             <PublisherTableCell className="text-center">
               <Tokens>{publisher.selfStake + publisher.selfStakeDelta}</Tokens>
@@ -1290,9 +1388,9 @@ const Publisher = ({
         <PublisherTableCell>
           <div className="mx-auto h-14 w-28">
             <SparkChart
-              data={publisher.apyHistory.map(({ date, apy }) => ({
+              data={publisher.apyHistory.map(({ date, apy, selfApy }) => ({
                 date,
-                value: apy,
+                value: isSelf ? selfApy : apy,
               }))}
             />
           </div>
@@ -1439,12 +1537,12 @@ const YourPositionsTable = ({
                 className="w-28"
                 actionDescription={
                   <>
-                    <span className="mr-3 align-middle">
+                    <span className="mr-[0.5em]">
                       Cancel tokens that are in warmup for staking to
                     </span>
-                    <PublisherName className="font-semibold">
+                    <PublisherIdentity className="font-semibold">
                       {publisher}
-                    </PublisherName>
+                    </PublisherIdentity>
                   </>
                 }
                 actionName="Cancel"
@@ -1476,12 +1574,10 @@ const YourPositionsTable = ({
                 className="md:w-28"
                 actionDescription={
                   <>
-                    <span className="mr-3 align-middle">
-                      Unstake tokens from
-                    </span>
-                    <PublisherName className="font-semibold">
+                    <span className="mr-[0.5em]">Unstake tokens from</span>
+                    <PublisherIdentity className="font-semibold">
                       {publisher}
-                    </PublisherName>
+                    </PublisherIdentity>
                   </>
                 }
                 actionName="Unstake"
@@ -1528,8 +1624,10 @@ const StakeToPublisherButton = ({
       size="small"
       actionDescription={
         <>
-          <span className="mr-3 align-middle">Stake to</span>
-          <PublisherName className="font-semibold">{publisher}</PublisherName>
+          <span className="mr-[0.5em]">Stake to</span>
+          <PublisherIdentity className="font-semibold">
+            {publisher}
+          </PublisherIdentity>
         </>
       }
       actionName="Stake"
@@ -1579,6 +1677,7 @@ const NewApy = ({
       calculateApy({
         poolCapacity: publisher.poolCapacity,
         yieldRate,
+        delegationFee: publisher.delegationFee,
         ...(isSelf
           ? {
               isSelf: true,
@@ -1602,6 +1701,7 @@ const NewApy = ({
       publisher.selfStakeDelta,
       publisher.poolUtilization,
       publisher.poolUtilizationDelta,
+      publisher.delegationFee,
       children,
     ],
   );
@@ -1609,34 +1709,58 @@ const NewApy = ({
   return <div {...props}>{apy}%</div>;
 };
 
-type PublisherNameProps = {
+type PublisherIdentityProps = PublisherKeyProps & {
+  withNameClassName?: string | undefined;
+};
+
+const PublisherIdentity = ({
+  className,
+  withNameClassName,
+  ...props
+}: PublisherIdentityProps) =>
+  props.children.identity ? (
+    <span className={clsx(className, withNameClassName)}>
+      <span>
+        <Image
+          alt={`${props.children.identity.name} icon`}
+          src={props.children.identity.icon}
+          className="mr-2 inline-block size-[20px] align-sub"
+          width={20}
+          height={20}
+        />
+        <span className="mr-[0.5em]">{props.children.identity.name}</span>
+      </span>
+      <PublisherKey className="text-sm opacity-50" {...props} />
+    </span>
+  ) : (
+    <PublisherKey className={className} {...props} />
+  );
+
+type PublisherKeyProps = {
   className?: string | undefined;
   children: PublisherProps["publisher"];
   fullClassName?: string;
   truncatedClassName?: string;
 };
 
-const PublisherName = ({
+const PublisherKey = ({
   children,
   fullClassName,
   truncatedClassName,
   className,
-}: PublisherNameProps) =>
-  children.name ? (
-    <span className={className}>{children.name}</span>
-  ) : (
-    <CopyButton
-      text={children.publicKey.toBase58()}
-      {...(className && { className })}
-    >
-      {fullClassName && (
-        <code className={fullClassName}>{children.publicKey.toBase58()}</code>
-      )}
-      <TruncatedKey className={truncatedClassName}>
-        {children.publicKey}
-      </TruncatedKey>
-    </CopyButton>
-  );
+}: PublisherKeyProps) => (
+  <CopyButton
+    text={children.publicKey.toBase58()}
+    {...(className && { className })}
+  >
+    {fullClassName && (
+      <code className={fullClassName}>{children.publicKey.toBase58()}</code>
+    )}
+    <TruncatedKey className={truncatedClassName}>
+      {children.publicKey}
+    </TruncatedKey>
+  </CopyButton>
+);
 
 const useTransferActionForPublisher = (
   action: ((publisher: PublicKey, amount: bigint) => Promise<void>) | undefined,

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

@@ -13,12 +13,14 @@ import {
   WALLETCONNECT_PROJECT_ID,
   MAINNET_RPC,
   HERMES_URL,
+  PYTHNET_RPC,
 } from "../../config/server";
 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 { Changelog } from "../Changelog";
 import { Footer } from "../Footer";
 import { Header } from "../Header";
 import { MaxWidth } from "../MaxWidth";
@@ -63,6 +65,7 @@ export const Root = ({ children }: Props) => (
       </MaxWidth>
       <Footer className="z-10" />
       <ToastRegion />
+      <Changelog />
     </body>
     {GOOGLE_ANALYTICS_ID && <GoogleAnalytics gaId={GOOGLE_ANALYTICS_ID} />}
     {AMPLITUDE_API_KEY && <Amplitude apiKey={AMPLITUDE_API_KEY} />}
@@ -79,7 +82,7 @@ const HtmlWithProviders = ({ lang, ...props }: HTMLProps<HTMLHtmlElement>) => (
             walletConnectProjectId={WALLETCONNECT_PROJECT_ID}
             mainnetRpc={MAINNET_RPC}
           >
-            <ApiProvider hermesUrl={HERMES_URL}>
+            <ApiProvider hermesUrl={HERMES_URL} pythnetRpcUrl={PYTHNET_RPC}>
               <ToastProvider>
                 <html lang={lang} {...props} />
               </ToastProvider>

+ 2 - 0
apps/staking/src/components/Tokens/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import * as dnum from "dnum";
 import { type ComponentProps, useMemo } from "react";

+ 7 - 2
apps/staking/src/components/Tooltip/index.tsx

@@ -9,14 +9,19 @@ type Props = Omit<ComponentProps<typeof TooltipImpl>, "children"> & {
 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",
+      "group 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">
+      <svg
+        width={8}
+        height={8}
+        viewBox="0 0 8 8"
+        className="fill-neutral-200 group-data-[placement=bottom]:rotate-180"
+      >
         <path d="M0 0 L4 4 L8 0" />
       </svg>
     </OverlayArrow>

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

@@ -295,7 +295,8 @@ const ButtonComponent = ({
   ...props
 }: ButtonComponentProps) => (
   <Button
-    className={clsx("w-36 text-sm sm:w-52 sm:text-base", className)}
+    className={clsx("w-36 text-sm lg:w-52 lg:text-base", className)}
+    size="nopad"
     {...props}
   >
     <WalletIcon className="size-4 flex-none opacity-60" />

+ 1 - 0
apps/staking/src/config/server.ts

@@ -52,6 +52,7 @@ export const WALLETCONNECT_PROJECT_ID = demandInProduction(
   "WALLETCONNECT_PROJECT_ID",
 );
 export const MAINNET_RPC = process.env.MAINNET_RPC;
+export const PYTHNET_RPC = getOr("PYTHNET_RPC", "https://pythnet.rpcpool.com");
 export const HERMES_URL = getOr("HERMES_URL", "https://hermes.pyth.network");
 export const BLOCKED_REGIONS = transformOr("BLOCKED_REGIONS", fromCsv, []);
 export const IP_ALLOWLIST = transformOr("IP_ALLOWLIST", fromCsv, []);

+ 39 - 12
apps/staking/src/hooks/use-api.tsx

@@ -1,10 +1,10 @@
 "use client";
 
 import { HermesClient } from "@pythnetwork/hermes-client";
-import { PythStakingClient } from "@pythnetwork/staking-sdk";
+import { PythnetClient, PythStakingClient } from "@pythnetwork/staking-sdk";
 import { useLocalStorageValue } from "@react-hookz/web";
 import { useConnection, useWallet } from "@solana/wallet-adapter-react";
-import type { PublicKey } from "@solana/web3.js";
+import { Connection, type PublicKey } from "@solana/web3.js";
 import { type ComponentProps, createContext, useContext, useMemo } from "react";
 import { useSWRConfig } from "swr";
 
@@ -43,6 +43,7 @@ const State = {
   [StateType.LoadedNoStakeAccount]: (
     isMainnet: boolean,
     client: PythStakingClient,
+    pythnetClient: PythnetClient,
     hermesClient: HermesClient,
     onCreateAccount: (newAccount: PublicKey) => Promise<void>,
   ) => ({
@@ -51,7 +52,7 @@ const State = {
       isMainnet ? "mainnet" : "devnet",
       client.wallet.publicKey.toBase58(),
     ],
-    loadData: () => api.loadData(client, hermesClient),
+    loadData: () => api.loadData(client, pythnetClient, hermesClient),
     deposit: async (amount: bigint) => {
       const account = await api.createStakeAccountAndDeposit(client, amount);
       return onCreateAccount(account);
@@ -61,6 +62,7 @@ const State = {
   [StateType.Loaded]: (
     isMainnet: boolean,
     client: PythStakingClient,
+    pythnetClient: PythnetClient,
     hermesClient: HermesClient,
     account: PublicKey,
     allAccounts: [PublicKey, ...PublicKey[]],
@@ -92,7 +94,8 @@ const State = {
       selectAccount,
       dashboardDataCacheKey,
 
-      loadData: () => api.loadData(client, hermesClient, account),
+      loadData: () =>
+        api.loadData(client, pythnetClient, hermesClient, account),
 
       claim: bindApi(api.claim),
       deposit: bindApi(api.deposit),
@@ -126,21 +129,30 @@ type ApiProviderProps = Omit<
   ComponentProps<typeof ApiContext.Provider>,
   "value"
 > & {
+  pythnetRpcUrl: string;
   hermesUrl: string;
 };
 
-export const ApiProvider = ({ hermesUrl, ...props }: ApiProviderProps) => {
-  const state = useApiContext(hermesUrl);
+export const ApiProvider = ({
+  hermesUrl,
+  pythnetRpcUrl,
+  ...props
+}: ApiProviderProps) => {
+  const state = useApiContext(hermesUrl, pythnetRpcUrl);
 
   return <ApiContext.Provider value={state} {...props} />;
 };
 
-const useApiContext = (hermesUrl: string) => {
+const useApiContext = (hermesUrl: string, pythnetRpcUrl: string) => {
   const wallet = useWallet();
   const { connection } = useConnection();
   const { isMainnet } = useNetwork();
   const { mutate } = useSWRConfig();
   const hermesClient = useMemo(() => new HermesClient(hermesUrl), [hermesUrl]);
+  const pythnetClient = useMemo(
+    () => new PythnetClient(new Connection(pythnetRpcUrl)),
+    [pythnetRpcUrl],
+  );
   const pythStakingClient = useMemo(
     () =>
       wallet.publicKey && wallet.signAllTransactions && wallet.signTransaction
@@ -174,7 +186,12 @@ const useApiContext = (hermesUrl: string) => {
       revalidateOnReconnect: false,
     },
   );
-  const lastStakeAccount = useLocalStorageValue<string>("last-stake-account");
+  const lastStakeAccountMainnet = useLocalStorageValue<string>(
+    `last-stake-account.mainnet`,
+  );
+  const lastStakeAccountDevnet = useLocalStorageValue<string>(
+    `last-stake-account.devnet`,
+  );
 
   return useMemo(() => {
     if (wallet.connecting) {
@@ -201,19 +218,26 @@ const useApiContext = (hermesUrl: string) => {
           } else {
             const [firstAccount, ...otherAccounts] = stakeAccounts.data;
             if (firstAccount) {
-              const selectedAccount = lastStakeAccount.value
+              const localStorageValue = isMainnet
+                ? lastStakeAccountMainnet
+                : lastStakeAccountDevnet;
+              const selectedAccount = localStorageValue.value
                 ? stakeAccounts.data.find(
-                    (account) => account.toBase58() === lastStakeAccount.value,
+                    (account) => account.toBase58() === localStorageValue.value,
                   )
                 : undefined;
+              if (!selectedAccount) {
+                localStorageValue.set(firstAccount.toBase58());
+              }
               return State[StateType.Loaded](
                 isMainnet,
                 pythStakingClient,
+                pythnetClient,
                 hermesClient,
                 selectedAccount ?? firstAccount,
                 [firstAccount, ...otherAccounts],
                 (account: PublicKey) => {
-                  lastStakeAccount.set(account.toBase58());
+                  localStorageValue.set(account.toBase58());
                 },
                 mutate,
               );
@@ -221,6 +245,7 @@ const useApiContext = (hermesUrl: string) => {
               return State[StateType.LoadedNoStakeAccount](
                 isMainnet,
                 pythStakingClient,
+                pythnetClient,
                 hermesClient,
                 async (newAccount) => {
                   await stakeAccounts.mutate([newAccount]);
@@ -239,9 +264,11 @@ const useApiContext = (hermesUrl: string) => {
     wallet.disconnecting,
     wallet.connected,
     pythStakingClient,
+    pythnetClient,
     stakeAccounts,
     hermesClient,
-    lastStakeAccount,
+    lastStakeAccountMainnet,
+    lastStakeAccountDevnet,
     mutate,
   ]);
 };

+ 41 - 0
apps/staking/src/hooks/use-changelog.ts

@@ -0,0 +1,41 @@
+import { useLocalStorageValue } from "@react-hookz/web";
+import { useCallback, useMemo } from "react";
+
+import { messages } from "../components/Changelog";
+
+export const useChangelog = () => {
+  const lastMessageSeen = useLocalStorageValue<number>(
+    "last-changelog-message-seen",
+    {
+      parse: (value) =>
+        // eslint-disable-next-line unicorn/no-null
+        value === null || value === "" ? null : Number.parseInt(value, 10),
+      stringify: (value) => value.toString(),
+    },
+  );
+
+  const isOpen = useMemo(() => {
+    const lastClosed = lastMessageSeen.value;
+    return (
+      lastClosed === undefined ||
+      messages.some((message) => message.id > lastClosed)
+    );
+  }, [lastMessageSeen.value]);
+
+  const toggleOpen = useCallback(
+    (isOpen: boolean) => {
+      if (isOpen) {
+        lastMessageSeen.remove();
+      } else {
+        lastMessageSeen.set(Math.max(...messages.map(({ id }) => id)));
+      }
+    },
+    [lastMessageSeen],
+  );
+
+  const open = useCallback(() => {
+    toggleOpen(true);
+  }, [toggleOpen]);
+
+  return { isOpen, toggleOpen, open };
+};

+ 6 - 0
apps/staking/src/known-publishers.ts

@@ -0,0 +1,6 @@
+export const KNOWN_PUBLISHERS = {
+  CfVkYofcLC1iVBcYFzgdYPeiX25SVRmWvBQVHorP1A3y: {
+    name: "BLOCKSIZE",
+    icon: "/publisher-icons/blocksize.png",
+  },
+};

+ 76 - 0
governance/pyth_staking_sdk/idl/stake-caps-parameters.json

@@ -0,0 +1,76 @@
+{
+  "address": "ujSFv8q8woXW5PUnby52PQyxYGUudxkrvgN6A631Qmm",
+  "metadata": {
+    "name": "stake_caps_parameters",
+    "version": "0.1.0",
+    "spec": "0.1.0",
+    "description": "Created with Anchor"
+  },
+  "instructions": [
+    {
+      "name": "set_parameters",
+      "discriminator": [218, 114, 41, 75, 208, 237, 97, 28],
+      "accounts": [
+        {
+          "name": "signer",
+          "writable": true,
+          "signer": true
+        },
+        {
+          "name": "parameters",
+          "writable": true,
+          "pda": {
+            "seeds": [
+              {
+                "kind": "const",
+                "value": [112, 97, 114, 97, 109, 101, 116, 101, 114, 115]
+              }
+            ]
+          }
+        },
+        {
+          "name": "system_program",
+          "address": "11111111111111111111111111111111"
+        }
+      ],
+      "args": [
+        {
+          "name": "parameters",
+          "type": {
+            "defined": {
+              "name": "Parameters"
+            }
+          }
+        }
+      ]
+    }
+  ],
+  "accounts": [
+    {
+      "name": "Parameters",
+      "discriminator": [233, 2, 25, 109, 70, 228, 206, 228]
+    }
+  ],
+  "types": [
+    {
+      "name": "Parameters",
+      "type": {
+        "kind": "struct",
+        "fields": [
+          {
+            "name": "current_authority",
+            "type": "pubkey"
+          },
+          {
+            "name": "m",
+            "type": "u64"
+          },
+          {
+            "name": "z",
+            "type": "u64"
+          }
+        ]
+      }
+    }
+  ]
+}

+ 1 - 0
governance/pyth_staking_sdk/package.json

@@ -38,6 +38,7 @@
   },
   "dependencies": {
     "@coral-xyz/anchor": "^0.30.1",
+    "@pythnetwork/client": "^2.22.0",
     "@pythnetwork/solana-utils": "workspace:*",
     "@solana/spl-governance": "^0.3.28",
     "@solana/spl-token": "^0.3.7",

+ 4 - 0
governance/pyth_staking_sdk/src/constants.ts

@@ -33,3 +33,7 @@ export const PUBLISHER_CAPS_PROGRAM_ADDRESS = new PublicKey(
 export const GOVERNANCE_ADDRESS = new PublicKey(
   "pytGY6tWRgGinSCvRLnSv4fHfBTMoiDGiCsesmHWM6U",
 );
+
+export const STAKE_CAPS_PARAMETERS_PROGRAM_ADDRESS = new PublicKey(
+  "ujSFv8q8woXW5PUnby52PQyxYGUudxkrvgN6A631Qmm",
+);

+ 1 - 0
governance/pyth_staking_sdk/src/index.ts

@@ -1,5 +1,6 @@
 export * from "./pdas";
 export * from "./pyth-staking-client";
+export * from "./pythnet-client";
 export * from "./types";
 export * from "./utils/apy";
 export * from "./utils/clock";

+ 8 - 0
governance/pyth_staking_sdk/src/pdas.ts

@@ -2,6 +2,7 @@ import { PublicKey } from "@solana/web3.js";
 
 import {
   INTEGRITY_POOL_PROGRAM_ADDRESS,
+  STAKE_CAPS_PARAMETERS_PROGRAM_ADDRESS,
   STAKING_PROGRAM_ADDRESS,
 } from "./constants";
 
@@ -73,3 +74,10 @@ export const getMaxVoterWeightRecordAddress = () => {
     STAKING_PROGRAM_ADDRESS,
   );
 };
+
+export const getStakeCapsParametersAddress = () => {
+  return PublicKey.findProgramAddressSync(
+    [Buffer.from("parameters")],
+    STAKE_CAPS_PARAMETERS_PROGRAM_ADDRESS,
+  );
+};

+ 13 - 0
governance/pyth_staking_sdk/src/pyth-staking-client.ts

@@ -989,4 +989,17 @@ export class PythStakingClient {
     const globalConfig = await this.getGlobalConfig();
     return getMint(this.connection, globalConfig.pythTokenMint);
   }
+
+  public async getRewardCustodyAccount(): Promise<Account> {
+    const poolConfigAddress = getPoolConfigAddress();
+    const config = await this.getGlobalConfig();
+
+    const rewardCustodyAccountAddress = getAssociatedTokenAddressSync(
+      config.pythTokenMint,
+      poolConfigAddress,
+      true,
+    );
+
+    return getAccount(this.connection, rewardCustodyAccountAddress);
+  }
 }

+ 55 - 0
governance/pyth_staking_sdk/src/pythnet-client.ts

@@ -0,0 +1,55 @@
+import { AnchorProvider, Program } from "@coral-xyz/anchor";
+import {
+  AccountType,
+  getPythProgramKeyForCluster,
+  parseBaseData,
+  parsePriceData,
+} from "@pythnetwork/client";
+import { Connection } from "@solana/web3.js";
+
+import { getStakeCapsParametersAddress } from "./pdas";
+import { convertBNToBigInt } from "./utils/bn";
+import { DummyWallet } from "./utils/wallet";
+import * as StakeCapsParametersIdl from "../idl/stake-caps-parameters.json";
+import type { StakeCapsParameters } from "../types/stake-caps-parameters";
+export class PythnetClient {
+  connection: Connection;
+  provider: AnchorProvider;
+  stakeCapParametersProgram: Program<StakeCapsParameters>;
+
+  constructor(connection: Connection) {
+    this.connection = connection;
+    this.provider = new AnchorProvider(connection, DummyWallet);
+    this.stakeCapParametersProgram = new Program<StakeCapsParameters>(
+      StakeCapsParametersIdl as StakeCapsParameters,
+      this.provider,
+    );
+  }
+
+  async getPublisherNumberOfSymbols() {
+    const publisherNumberOfSymbols: Record<string, number> = {};
+    const pythAccounts = await this.connection.getProgramAccounts(
+      getPythProgramKeyForCluster("pythnet"),
+    );
+    for (const account of pythAccounts) {
+      const base = parseBaseData(account.account.data);
+      if (base?.type === AccountType.Price) {
+        const parsed = parsePriceData(account.account.data);
+        for (const priceComponent of parsed.priceComponents) {
+          publisherNumberOfSymbols[priceComponent.publisher.toBase58()] =
+            (publisherNumberOfSymbols[priceComponent.publisher.toBase58()] ??
+              0) + 1;
+        }
+      }
+    }
+    return publisherNumberOfSymbols;
+  }
+
+  async getStakeCapParameters() {
+    const parameters =
+      await this.stakeCapParametersProgram.account.parameters.fetch(
+        getStakeCapsParametersAddress()[0],
+      );
+    return convertBNToBigInt(parameters);
+  }
+}

+ 1 - 0
governance/pyth_staking_sdk/src/types.ts

@@ -63,6 +63,7 @@ export type PublisherData = {
   totalDelegationDelta: bigint;
   selfDelegation: bigint;
   selfDelegationDelta: bigint;
+  delegationFee: bigint;
   apyHistory: { epoch: bigint; apy: number; selfApy: number }[];
 }[];
 

+ 17 - 2
governance/pyth_staking_sdk/src/utils/apy.ts

@@ -4,16 +4,25 @@ export const convertEpochYieldToApy = (epochYield: bigint) => {
   return (Number(epochYield) * 52 * 100) / FRACTION_PRECISION;
 };
 
+export const computeDelegatorRewardPercentage = (delegationFee: bigint) => {
+  return 1 - Number(delegationFee) / FRACTION_PRECISION;
+};
+
 export const calculateApy = (
   options: {
     selfStake: bigint;
     poolCapacity: bigint;
     yieldRate: bigint;
+    delegationFee: bigint;
   } & ({ isSelf: true } | { isSelf: false; poolUtilization: bigint }),
 ) => {
   const { selfStake, poolCapacity, yieldRate, isSelf } = options;
   const eligibleSelfStake = selfStake > poolCapacity ? poolCapacity : selfStake;
 
+  if (poolCapacity === 0n) {
+    return 0;
+  }
+
   const apyPercentage = convertEpochYieldToApy(yieldRate);
 
   if (isSelf) {
@@ -24,6 +33,9 @@ export const calculateApy = (
   }
 
   const { poolUtilization } = options;
+  const delegatorPercentage = computeDelegatorRewardPercentage(
+    options.delegationFee,
+  );
 
   const delegatorPoolUtilization = poolUtilization - selfStake;
   const delegatorPoolCapacity = poolCapacity - eligibleSelfStake;
@@ -33,10 +45,13 @@ export const calculateApy = (
       : delegatorPoolUtilization;
 
   if (poolUtilization === selfStake) {
-    return apyPercentage;
+    return (
+      (selfStake >= poolCapacity ? 0 : apyPercentage) * delegatorPercentage
+    );
   }
 
   return (
-    (apyPercentage * Number(eligibleStake)) / Number(delegatorPoolUtilization)
+    (apyPercentage * delegatorPercentage * Number(eligibleStake)) /
+    Number(delegatorPoolUtilization)
   );
 };

+ 13 - 5
governance/pyth_staking_sdk/src/utils/pool.ts

@@ -1,6 +1,9 @@
 import { PublicKey } from "@solana/web3.js";
 
-import { convertEpochYieldToApy } from "./apy";
+import {
+  computeDelegatorRewardPercentage,
+  convertEpochYieldToApy,
+} from "./apy";
 import { FRACTION_PRECISION_N } from "../constants";
 import type { PoolDataAccount, PublisherData } from "../types";
 
@@ -24,14 +27,19 @@ export const extractPublisherData = (
         (poolData.selfDelState[index]?.deltaDelegation ?? 0n),
       selfDelegation: poolData.selfDelState[index]?.totalDelegation ?? 0n,
       selfDelegationDelta: poolData.selfDelState[index]?.deltaDelegation ?? 0n,
+      delegationFee: poolData.delegationFees[index] ?? 0n,
       apyHistory: poolData.events
         .filter((event) => event.epoch > 0n)
         .map((event) => ({
           epoch: event.epoch,
-          apy: convertEpochYieldToApy(
-            (event.y * (event.eventData[index]?.otherRewardRatio ?? 0n)) /
-              FRACTION_PRECISION_N,
-          ),
+          apy:
+            convertEpochYieldToApy(
+              (event.y * (event.eventData[index]?.otherRewardRatio ?? 0n)) /
+                FRACTION_PRECISION_N,
+            ) *
+            computeDelegatorRewardPercentage(
+              poolData.delegationFees[index] ?? 0n,
+            ),
           selfApy: convertEpochYieldToApy(
             (event.y * (event.eventData[index]?.selfRewardRatio ?? 0n)) /
               FRACTION_PRECISION_N,

+ 82 - 0
governance/pyth_staking_sdk/types/stake-caps-parameters.ts

@@ -0,0 +1,82 @@
+/**
+ * Program IDL in camelCase format in order to be used in JS/TS.
+ *
+ * Note that this is only a type helper and is not the actual IDL. The original
+ * IDL can be found at `target/idl/stake_caps_parameters.json`.
+ */
+export type StakeCapsParameters = {
+  address: "ujSFv8q8woXW5PUnby52PQyxYGUudxkrvgN6A631Qmm";
+  metadata: {
+    name: "stakeCapsParameters";
+    version: "0.1.0";
+    spec: "0.1.0";
+    description: "Created with Anchor";
+  };
+  instructions: [
+    {
+      name: "setParameters";
+      discriminator: [218, 114, 41, 75, 208, 237, 97, 28];
+      accounts: [
+        {
+          name: "signer";
+          writable: true;
+          signer: true;
+        },
+        {
+          name: "parameters";
+          writable: true;
+          pda: {
+            seeds: [
+              {
+                kind: "const";
+                value: [112, 97, 114, 97, 109, 101, 116, 101, 114, 115];
+              },
+            ];
+          };
+        },
+        {
+          name: "systemProgram";
+          address: "11111111111111111111111111111111";
+        },
+      ];
+      args: [
+        {
+          name: "parameters";
+          type: {
+            defined: {
+              name: "parameters";
+            };
+          };
+        },
+      ];
+    },
+  ];
+  accounts: [
+    {
+      name: "parameters";
+      discriminator: [233, 2, 25, 109, 70, 228, 206, 228];
+    },
+  ];
+  types: [
+    {
+      name: "parameters";
+      type: {
+        kind: "struct";
+        fields: [
+          {
+            name: "currentAuthority";
+            type: "pubkey";
+          },
+          {
+            name: "m";
+            type: "u64";
+          },
+          {
+            name: "z";
+            type: "u64";
+          },
+        ];
+      };
+    },
+  ];
+};

+ 31 - 148
pnpm-lock.yaml

@@ -359,13 +359,13 @@ importers:
         version: 0.9.23(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-react':
         specifier: ^0.15.28
-        version: 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
+        version: 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
       '@solana/wallet-adapter-react-ui':
         specifier: ^0.9.27
-        version: 0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
+        version: 0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
       '@solana/wallet-adapter-wallets':
         specifier: 0.19.10
-        version: 0.19.10(@babel/runtime@7.25.0)(@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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
+        version: 0.19.10(@babel/runtime@7.25.0)(@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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)
       '@solana/web3.js':
         specifier: 1.92.3
         version: 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -420,7 +420,7 @@ importers:
         version: 4.9.1
       '@cprussin/eslint-config':
         specifier: ^3.0.0
-        version: 3.0.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)
+        version: 3.0.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)
       '@cprussin/jest-config':
         specifier: ^1.4.1
         version: 1.4.1(@babel/core@7.24.7)(@jest/globals@29.7.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(@types/jest@29.5.12)(@types/node@22.2.0)(babel-jest@29.7.0(@babel/core@7.24.7))(bufferutil@4.0.8)(eslint@9.9.0(jiti@1.21.0))(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(utf-8-validate@5.0.10)
@@ -718,6 +718,9 @@ importers:
       '@coral-xyz/anchor':
         specifier: ^0.30.1
         version: 0.30.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
+      '@pythnetwork/client':
+        specifier: ^2.22.0
+        version: 2.22.0(@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)
       '@pythnetwork/solana-utils':
         specifier: workspace:*
         version: link:../../target_chains/solana/sdk/js/solana_utils
@@ -745,7 +748,7 @@ importers:
         version: 3.0.1
       '@solana/wallet-adapter-react':
         specifier: ^0.15.28
-        version: 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
+        version: 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
       '@types/jest':
         specifier: ^29.5.12
         version: 29.5.12
@@ -24635,45 +24638,6 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
-  '@cprussin/eslint-config@3.0.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)':
-    dependencies:
-      '@babel/core': 7.24.7
-      '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@9.5.0)
-      '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.7)
-      '@eslint/compat': 1.1.0
-      '@eslint/eslintrc': 3.1.0
-      '@eslint/js': 9.5.0
-      '@next/eslint-plugin-next': 14.2.3
-      eslint: 9.5.0
-      eslint-config-prettier: 9.1.0(eslint@9.5.0)
-      eslint-config-turbo: 1.13.4(eslint@9.5.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.5.0)
-      eslint-plugin-jest: 28.6.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4)
-      eslint-plugin-jest-dom: 5.4.0(eslint@9.5.0)
-      eslint-plugin-jsonc: 2.16.0(eslint@9.5.0)
-      eslint-plugin-jsx-a11y: 6.8.0(eslint@9.5.0)
-      eslint-plugin-n: 17.9.0(eslint@9.5.0)
-      eslint-plugin-react: 7.34.2(eslint@9.5.0)
-      eslint-plugin-react-hooks: 4.6.2(eslint@9.5.0)
-      eslint-plugin-storybook: 0.8.0(eslint@9.5.0)(typescript@5.5.4)
-      eslint-plugin-tailwindcss: 3.17.3(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))
-      eslint-plugin-testing-library: 6.2.2(eslint@9.5.0)(typescript@5.5.4)
-      eslint-plugin-tsdoc: 0.3.0
-      eslint-plugin-unicorn: 53.0.0(eslint@9.5.0)
-      globals: 15.6.0
-      tailwindcss: 3.4.4(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))
-      typescript-eslint: 7.13.1(eslint@9.5.0)(typescript@5.5.4)
-    transitivePeerDependencies:
-      - '@testing-library/dom'
-      - '@typescript-eslint/eslint-plugin'
-      - '@typescript-eslint/parser'
-      - eslint-import-resolver-typescript
-      - eslint-import-resolver-webpack
-      - jest
-      - supports-color
-      - ts-node
-      - typescript
-
   '@cprussin/eslint-config@3.0.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)':
     dependencies:
       '@babel/core': 7.24.7
@@ -24687,7 +24651,7 @@ snapshots:
       eslint-config-prettier: 9.1.0(eslint@9.5.0)
       eslint-config-turbo: 1.13.4(eslint@9.5.0)
       eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)
-      eslint-plugin-jest: 28.6.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4)
+      eslint-plugin-jest: 28.6.0(@typescript-eslint/eslint-plugin@7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4)
       eslint-plugin-jest-dom: 5.4.0(eslint@9.5.0)
       eslint-plugin-jsonc: 2.16.0(eslint@9.5.0)
       eslint-plugin-jsx-a11y: 6.8.0(eslint@9.5.0)
@@ -24725,7 +24689,7 @@ snapshots:
       eslint: 9.5.0
       eslint-config-prettier: 9.1.0(eslint@9.5.0)
       eslint-config-turbo: 1.13.4(eslint@9.5.0)
-      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.5.0)
+      eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)
       eslint-plugin-jest: 28.6.0(@typescript-eslint/eslint-plugin@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(jest@29.7.0(@types/node@20.14.7)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2)))(typescript@5.5.2)
       eslint-plugin-jest-dom: 5.4.0(eslint@9.5.0)
       eslint-plugin-jsonc: 2.16.0(eslint@9.5.0)
@@ -29664,12 +29628,6 @@ snapshots:
       crypto-js: 4.2.0
       uuidv4: 6.2.13
 
-  '@particle-network/solana-wallet@1.3.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)':
-    dependencies:
-      '@particle-network/auth': 1.3.1
-      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
-      bs58: 5.0.0
-
   '@particle-network/solana-wallet@1.3.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)':
     dependencies:
       '@particle-network/auth': 1.3.1
@@ -32031,18 +31989,18 @@ snapshots:
       '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(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)
 
-  '@solana/wallet-adapter-base-ui@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)':
+  '@solana/wallet-adapter-base-ui@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)':
     dependencies:
-      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
+      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       react: 18.3.1
     transitivePeerDependencies:
       - bs58
       - react-native
 
-  '@solana/wallet-adapter-base-ui@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)':
+  '@solana/wallet-adapter-base-ui@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)':
     dependencies:
-      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
+      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       react: 18.3.1
     transitivePeerDependencies:
@@ -32196,14 +32154,6 @@ snapshots:
       '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(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)
 
-  '@solana/wallet-adapter-particle@0.1.12(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)':
-    dependencies:
-      '@particle-network/solana-wallet': 1.3.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)
-      '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(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)
-    transitivePeerDependencies:
-      - bs58
-
   '@solana/wallet-adapter-particle@0.1.12(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)':
     dependencies:
       '@particle-network/solana-wallet': 1.3.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)
@@ -32217,11 +32167,11 @@ snapshots:
       '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(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)
 
-  '@solana/wallet-adapter-react-ui@0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)':
+  '@solana/wallet-adapter-react-ui@0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)':
     dependencies:
       '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
-      '@solana/wallet-adapter-base-ui': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
-      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(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)
+      '@solana/wallet-adapter-base-ui': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
+      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
@@ -32229,11 +32179,11 @@ snapshots:
       - bs58
       - react-native
 
-  '@solana/wallet-adapter-react-ui@0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)':
+  '@solana/wallet-adapter-react-ui@0.9.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)':
     dependencies:
       '@solana/wallet-adapter-base': 0.9.23(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
-      '@solana/wallet-adapter-base-ui': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
-      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(react-native@0.74.2(@babel/core@7.24.0)(@babel/preset-env@7.24.7(@babel/core@7.24.0))(@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)
+      '@solana/wallet-adapter-base-ui': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
+      '@solana/wallet-adapter-react': 0.15.35(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(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)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
@@ -32498,7 +32448,7 @@ snapshots:
       - uWebSockets.js
       - utf-8-validate
 
-  '@solana/wallet-adapter-wallets@0.19.10(@babel/runtime@7.25.0)(@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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
+  '@solana/wallet-adapter-wallets@0.19.10(@babel/runtime@7.25.0)(@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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
     dependencies:
       '@solana/wallet-adapter-alpha': 0.1.10(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-avana': 0.1.13(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
@@ -32526,7 +32476,7 @@ snapshots:
       '@solana/wallet-adapter-nightly': 0.1.16(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-nufi': 0.1.17(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-onto': 0.1.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
-      '@solana/wallet-adapter-particle': 0.1.12(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@5.0.0)
+      '@solana/wallet-adapter-particle': 0.1.12(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bs58@6.0.0)
       '@solana/wallet-adapter-phantom': 0.9.24(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-safepal': 0.5.18(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-saifu': 0.1.15(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
@@ -34457,7 +34407,7 @@ snapshots:
   '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.5.0)(typescript@5.5.4)':
     dependencies:
       '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
+      '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
       '@typescript-eslint/scope-manager': 7.13.1
       '@typescript-eslint/type-utils': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
       '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
@@ -34472,25 +34422,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)':
-    dependencies:
-      '@eslint-community/regexpp': 4.10.0
-      '@typescript-eslint/parser': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-      '@typescript-eslint/scope-manager': 7.13.1
-      '@typescript-eslint/type-utils': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-      '@typescript-eslint/utils': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-      '@typescript-eslint/visitor-keys': 7.13.1
-      eslint: 9.9.0(jiti@1.21.0)
-      graphemer: 1.4.0
-      ignore: 5.3.1
-      natural-compare: 1.4.0
-      ts-api-utils: 1.3.0(typescript@5.5.4)
-    optionalDependencies:
-      typescript: 5.5.4
-    transitivePeerDependencies:
-      - supports-color
-    optional: true
-
   '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)':
     dependencies:
       '@eslint-community/regexpp': 4.10.0
@@ -34639,14 +34570,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)':
+  '@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4)':
     dependencies:
       '@typescript-eslint/scope-manager': 7.13.1
       '@typescript-eslint/types': 7.13.1
       '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.4)
       '@typescript-eslint/visitor-keys': 7.13.1
       debug: 4.3.6(supports-color@8.1.1)
-      eslint: 9.9.0(jiti@1.21.0)
+      eslint: 9.5.0
     optionalDependencies:
       typescript: 5.5.4
     transitivePeerDependencies:
@@ -40055,16 +39986,6 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.8.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.5.0):
-    dependencies:
-      debug: 3.2.7
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-      eslint: 9.5.0
-      eslint-import-resolver-node: 0.3.9
-    transitivePeerDependencies:
-      - supports-color
-
   eslint-module-utils@2.8.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.5.0):
     dependencies:
       debug: 3.2.7
@@ -40082,33 +40003,6 @@ snapshots:
       eslint: 9.5.0
       eslint-compat-utils: 0.5.1(eslint@9.5.0)
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.5.0):
-    dependencies:
-      array-includes: 3.1.8
-      array.prototype.findlastindex: 1.2.5
-      array.prototype.flat: 1.3.2
-      array.prototype.flatmap: 1.3.2
-      debug: 3.2.7
-      doctrine: 2.1.0
-      eslint: 9.5.0
-      eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.5.0)
-      hasown: 2.0.2
-      is-core-module: 2.13.1
-      is-glob: 4.0.3
-      minimatch: 3.1.2
-      object.fromentries: 2.0.8
-      object.groupby: 1.0.3
-      object.values: 1.2.0
-      semver: 6.3.1
-      tsconfig-paths: 3.15.0
-    optionalDependencies:
-      '@typescript-eslint/parser': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-    transitivePeerDependencies:
-      - eslint-import-resolver-typescript
-      - eslint-import-resolver-webpack
-      - supports-color
-
   eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.3.0(eslint@8.56.0)(typescript@5.4.5))(eslint@8.56.0):
     dependencies:
       array-includes: 3.1.8
@@ -40169,18 +40063,18 @@ snapshots:
       eslint: 9.5.0
       requireindex: 1.2.0
 
-  eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4):
+  eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(jest@29.7.0(@types/node@20.14.7)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2)))(typescript@5.5.2):
     dependencies:
-      '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.4)
+      '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.2)
       eslint: 9.5.0
     optionalDependencies:
-      '@typescript-eslint/eslint-plugin': 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
-      jest: 29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))
+      '@typescript-eslint/eslint-plugin': 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2)
+      jest: 29.7.0(@types/node@20.14.7)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@8.3.0(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4):
+  eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4))(eslint@9.5.0)(jest@29.7.0(@types/node@22.2.0)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4)))(typescript@5.5.4):
     dependencies:
       '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.4)
       eslint: 9.5.0
@@ -40191,17 +40085,6 @@ snapshots:
       - supports-color
       - typescript
 
-  eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(jest@29.7.0(@types/node@20.14.7)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2)))(typescript@5.5.2):
-    dependencies:
-      '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.2)
-      eslint: 9.5.0
-    optionalDependencies:
-      '@typescript-eslint/eslint-plugin': 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint@9.5.0)(typescript@5.5.2)
-      jest: 29.7.0(@types/node@20.14.7)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))
-    transitivePeerDependencies:
-      - supports-color
-      - typescript
-
   eslint-plugin-jsonc@2.16.0(eslint@9.5.0):
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@9.5.0)
@@ -51547,7 +51430,7 @@ snapshots:
   typescript-eslint@7.13.1(eslint@9.5.0)(typescript@5.5.4):
     dependencies:
       '@typescript-eslint/eslint-plugin': 7.13.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.4))(eslint@9.5.0)(typescript@5.5.4)
-      '@typescript-eslint/parser': 7.13.1(eslint@9.9.0(jiti@1.21.0))(typescript@5.5.4)
+      '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
       '@typescript-eslint/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
       eslint: 9.5.0
     optionalDependencies: