Преглед на файлове

feat(staking): add low-res implementation for new requirements

This PR adds low-res / unstyled implementation for the following new feature
requirements that we've discovered:

- Must claim rewards before withdrawing
- Must claim rewards before unstaking
- Add swr to loadData
- Calculate APY in the frontend
- Show how APY will change due to staking tokens
- Show historical APY in a spark chart
- Add functionality to select a stake account if multiple exist for the given wallet
- If wallet is a publisher, show self highlighted (& adjust apy calculation)
- Claim within 1 year
- Show next locked token unlock period & amount
- Show account history
Connor Prussin преди 1 година
родител
ревизия
0467660375

+ 3 - 1
apps/staking/package.json

@@ -11,7 +11,7 @@
     "fix": "pnpm fix:lint && pnpm fix:format",
     "fix:format": "prettier --write .",
     "fix:lint": "eslint --fix .",
-    "pull:env": "VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID= vercel env pull",
+    "pull:env": "VERCEL_ORG_ID=team_BKQrg3JJFLxZyTqpuYtIY0rj VERCEL_PROJECT_ID=prj_4aG4ZJ4S8b59nQID6LZDDb7c9cVY vercel env pull",
     "start:dev": "next dev",
     "start:prod": "next start",
     "test": "tsc && jest",
@@ -37,6 +37,8 @@
     "pino": "^9.3.2",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
+    "recharts": "^2.12.7",
+    "swr": "^2.2.5",
     "zod": "^3.23.8"
   },
   "devDependencies": {

+ 335 - 83
apps/staking/src/api.ts

@@ -1,58 +1,37 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
+// TODO remove these disables when moving off the mock APIs
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-non-null-assertion */
 
 import type { WalletContextState } from "@solana/wallet-adapter-react";
 import type { Connection } from "@solana/web3.js";
 
-const MOCK_DELAY = 500;
+export type StakeAccount = {
+  publicKey: `0x${string}`;
+};
 
-const MOCK_DATA: Data = {
-  total: 15_000_000n,
-  availableRewards: 156_000n,
-  locked: 3_000_000n,
-  walletAmount: 200_000_000n,
-  governance: {
-    warmup: 2_670_000n,
-    staked: 4_150_000n,
-    cooldown: 1_850_000n,
-    cooldown2: 4_765_000n,
-  },
-  integrityStakingPublishers: [
-    {
-      name: "Foo Bar",
-      publicKey: "0xF00",
-      selfStake: 5_000_000_000n,
-      poolCapacity: 500_000_000n,
-      poolUtilization: 200_000_000n,
-      apy: 20,
-      numFeeds: 42,
-      qualityRanking: 1,
-      positions: {
-        warmup: 5_000_000n,
-        staked: 4_000_000n,
-        cooldown: 1_000_000n,
-        cooldown2: 460_000n,
-      },
-    },
-    {
-      name: "Jump Trading",
-      publicKey: "0xBA4",
-      selfStake: 400_000_000n,
-      poolCapacity: 500_000_000n,
-      poolUtilization: 600_000_000n,
-      apy: 10,
-      numFeeds: 84,
-      qualityRanking: 2,
-      positions: {
-        staked: 1_000_000n,
-      },
-    },
-  ],
+export type Context = {
+  connection: Connection;
+  wallet: WalletContextState;
+  stakeAccount: StakeAccount;
 };
 
 type Data = {
   total: bigint;
   availableRewards: bigint;
+  lastSlash:
+    | {
+        amount: bigint;
+        date: Date;
+      }
+    | undefined;
+  expiringRewards: {
+    amount: bigint;
+    expiry: Date;
+  };
   locked: bigint;
+  unlockSchedule: {
+    date: Date;
+    amount: bigint;
+  }[];
   walletAmount: bigint;
   governance: {
     warmup: bigint;
@@ -62,13 +41,14 @@ type Data = {
   };
   integrityStakingPublishers: {
     name: string;
-    publicKey: string;
+    publicKey: `0x${string}`;
+    isSelf: boolean;
     selfStake: bigint;
     poolCapacity: bigint;
     poolUtilization: bigint;
-    apy: number;
     numFeeds: number;
     qualityRanking: number;
+    apyHistory: { date: Date; apy: number }[];
     positions?:
       | {
           warmup?: bigint | undefined;
@@ -80,80 +60,168 @@ type Data = {
   }[];
 };
 
-export const loadData = async (
+export enum StakeType {
+  Governance,
+  IntegrityStaking,
+}
+
+const StakeDetails = {
+  Governance: () => ({ type: StakeType.Governance as const }),
+  IntegrityStaking: (publisherName: string) => ({
+    type: StakeType.IntegrityStaking as const,
+    publisherName,
+  }),
+};
+
+export type StakeDetails = ReturnType<
+  (typeof StakeDetails)[keyof typeof StakeDetails]
+>;
+
+export enum AccountHistoryItemType {
+  Deposit,
+  LockedDeposit,
+  Withdrawal,
+  RewardsCredited,
+  Claim,
+  Slash,
+  Unlock,
+  StakeCreated,
+  StakeFinishedWarmup,
+  UnstakeCreated,
+  UnstakeExitedCooldown,
+}
+
+const AccountHistoryAction = {
+  Deposit: () => ({ type: AccountHistoryItemType.Deposit as const }),
+  LockedDeposit: (unlockDate: Date) => ({
+    type: AccountHistoryItemType.LockedDeposit as const,
+    unlockDate,
+  }),
+  Withdrawal: () => ({ type: AccountHistoryItemType.Withdrawal as const }),
+  RewardsCredited: () => ({
+    type: AccountHistoryItemType.RewardsCredited as const,
+  }),
+  Claim: () => ({ type: AccountHistoryItemType.Claim as const }),
+  Slash: (publisherName: string) => ({
+    type: AccountHistoryItemType.Slash as const,
+    publisherName,
+  }),
+  Unlock: () => ({ type: AccountHistoryItemType.Unlock as const }),
+  StakeCreated: (details: StakeDetails) => ({
+    type: AccountHistoryItemType.StakeCreated as const,
+    details,
+  }),
+  StakeFinishedWarmup: (details: StakeDetails) => ({
+    type: AccountHistoryItemType.StakeFinishedWarmup as const,
+    details,
+  }),
+  UnstakeCreated: (details: StakeDetails) => ({
+    type: AccountHistoryItemType.UnstakeCreated as const,
+    details,
+  }),
+  UnstakeExitedCooldown: (details: StakeDetails) => ({
+    type: AccountHistoryItemType.UnstakeExitedCooldown as const,
+    details,
+  }),
+};
+
+export type AccountHistoryAction = ReturnType<
+  (typeof AccountHistoryAction)[keyof typeof AccountHistoryAction]
+>;
+
+type AccountHistory = {
+  timestamp: Date;
+  action: AccountHistoryAction;
+  amount: bigint;
+  accountTotal: bigint;
+  availableToWithdraw: bigint;
+  availableRewards: bigint;
+  locked: bigint;
+}[];
+
+export const getStakeAccounts = async (
   _connection: Connection,
   _wallet: WalletContextState,
-  _signal?: AbortSignal | undefined,
-): Promise<Data> => {
+): Promise<StakeAccount[]> => {
+  await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+  return MOCK_STAKE_ACCOUNTS;
+};
+
+export const loadData = async (context: Context): Promise<Data> => {
+  await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+  // While using mocks we need to clone the MOCK_DATA object every time
+  // `loadData` is called so that swr treats the response as changed and
+  // triggers a rerender.
+  return { ...MOCK_DATA[context.stakeAccount.publicKey]! };
+};
+
+export const loadAccountHistory = async (
+  context: Context,
+): Promise<AccountHistory> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  return MOCK_DATA;
+  return [...MOCK_HISTORY[context.stakeAccount.publicKey]!];
 };
 
 export const deposit = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.total += amount;
-  MOCK_DATA.walletAmount -= amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.total += amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount -= amount;
 };
 
 export const withdraw = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.total -= amount;
-  MOCK_DATA.walletAmount += amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.total -= amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount += amount;
 };
 
-export const claim = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
-): Promise<void> => {
+export const claim = async (context: Context): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.total += MOCK_DATA.availableRewards;
-  MOCK_DATA.availableRewards = 0n;
+  MOCK_DATA[context.stakeAccount.publicKey]!.total +=
+    MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards;
+  MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards = 0n;
+  MOCK_DATA[context.stakeAccount.publicKey]!.expiringRewards.amount = 0n;
 };
 
 export const stakeGovernance = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.governance.warmup += amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup += amount;
 };
 
 export const cancelWarmupGovernance = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.governance.warmup -= amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup -= amount;
 };
 
 export const unstakeGovernance = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  MOCK_DATA.governance.staked -= amount;
-  MOCK_DATA.governance.cooldown += amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.governance.staked -= amount;
+  MOCK_DATA[context.stakeAccount.publicKey]!.governance.cooldown += amount;
 };
 
 export const delegateIntegrityStaking = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   publisherKey: string,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  const publisher = MOCK_DATA.integrityStakingPublishers.find(
+  const publisher = MOCK_DATA[
+    context.stakeAccount.publicKey
+  ]!.integrityStakingPublishers.find(
     (publisher) => publisher.publicKey === publisherKey,
   );
   if (publisher) {
@@ -165,13 +233,14 @@ export const delegateIntegrityStaking = async (
 };
 
 export const cancelWarmupIntegrityStaking = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   publisherKey: string,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  const publisher = MOCK_DATA.integrityStakingPublishers.find(
+  const publisher = MOCK_DATA[
+    context.stakeAccount.publicKey
+  ]!.integrityStakingPublishers.find(
     (publisher) => publisher.publicKey === publisherKey,
   );
   if (publisher) {
@@ -184,13 +253,14 @@ export const cancelWarmupIntegrityStaking = async (
 };
 
 export const unstakeIntegrityStaking = async (
-  _connection: Connection,
-  _wallet: WalletContextState,
+  context: Context,
   publisherKey: string,
   amount: bigint,
 ): Promise<void> => {
   await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
-  const publisher = MOCK_DATA.integrityStakingPublishers.find(
+  const publisher = MOCK_DATA[
+    context.stakeAccount.publicKey
+  ]!.integrityStakingPublishers.find(
     (publisher) => publisher.publicKey === publisherKey,
   );
   if (publisher) {
@@ -203,3 +273,185 @@ export const unstakeIntegrityStaking = async (
     throw new Error(`Invalid publisher key: "${publisherKey}"`);
   }
 };
+
+export const calculateApy = (
+  poolCapacity: bigint,
+  poolUtilization: bigint,
+  isSelf: boolean,
+) => {
+  const maxApy = isSelf ? 25 : 20;
+  const minApy = isSelf ? 10 : 5;
+  return Math.min(
+    Math.max(
+      maxApy - Number((poolUtilization - poolCapacity) / 100_000_000n),
+      minApy,
+    ),
+    maxApy,
+  );
+};
+
+const MOCK_DELAY = 500;
+
+const MOCK_STAKE_ACCOUNTS: StakeAccount[] = [
+  { publicKey: "0x000000" },
+  { publicKey: "0x111111" },
+];
+
+const mkMockData = (isDouro: boolean): Data => ({
+  total: 15_000_000n,
+  availableRewards: 156_000n,
+  lastSlash: isDouro
+    ? undefined
+    : {
+        amount: 2147n,
+        date: new Date("2024-05-04T00:00:00Z"),
+      },
+  expiringRewards: {
+    amount: 56_000n,
+    expiry: new Date("2025-08-01T00:00:00Z"),
+  },
+  locked: isDouro ? 3_000_000n : 0n,
+  unlockSchedule: isDouro
+    ? [
+        {
+          amount: 1_000_000n,
+          date: new Date("2025-08-01T00:00:00Z"),
+        },
+        {
+          amount: 2_000_000n,
+          date: new Date("2025-09-01T00:00:00Z"),
+        },
+      ]
+    : [],
+  walletAmount: 5_000_000_000_000n,
+  governance: {
+    warmup: 2_670_000n,
+    staked: 4_150_000n,
+    cooldown: 1_850_000n,
+    cooldown2: 4_765_000n,
+  },
+  integrityStakingPublishers: [
+    {
+      name: "Douro Labs",
+      publicKey: "0xF00",
+      isSelf: isDouro,
+      selfStake: 5_000_000_000n,
+      poolCapacity: 500_000_000n,
+      poolUtilization: 200_000_000n,
+      numFeeds: 42,
+      qualityRanking: 1,
+      apyHistory: [
+        { date: new Date("2024-07-22"), apy: 5 },
+        { date: new Date("2024-07-23"), apy: 10 },
+        { date: new Date("2024-07-24"), apy: 25 },
+        { date: new Date("2024-07-25"), apy: 20 },
+      ],
+      positions: {
+        warmup: 5_000_000n,
+        staked: 4_000_000n,
+        cooldown: 1_000_000n,
+        cooldown2: 460_000n,
+      },
+    },
+    {
+      name: "Jump Trading",
+      publicKey: "0xBA4",
+      isSelf: false,
+      selfStake: 400_000_000n,
+      poolCapacity: 500_000_000n,
+      poolUtilization: 750_000_000n,
+      numFeeds: 84,
+      qualityRanking: 2,
+      apyHistory: [
+        { date: new Date("2024-07-24"), apy: 5 },
+        { date: new Date("2024-07-25"), apy: 10 },
+      ],
+      positions: {
+        staked: 1_000_000n,
+      },
+    },
+    {
+      name: "Cboe",
+      publicKey: "0xAA",
+      isSelf: false,
+      selfStake: 200_000_000n,
+      poolCapacity: 600_000_000n,
+      poolUtilization: 450_000_000n,
+      numFeeds: 17,
+      qualityRanking: 5,
+      apyHistory: [
+        { date: new Date("2024-07-24"), apy: 5 },
+        { date: new Date("2024-07-25"), apy: 10 },
+      ],
+    },
+    {
+      name: "Raydium",
+      publicKey: "0x111",
+      isSelf: false,
+      selfStake: 400_000_000n,
+      poolCapacity: 500_000_000n,
+      poolUtilization: 750_000_000n,
+      numFeeds: 84,
+      qualityRanking: 3,
+      apyHistory: [
+        { date: new Date("2024-07-24"), apy: 5 },
+        { date: new Date("2024-07-25"), apy: 10 },
+      ],
+    },
+  ],
+});
+
+const MOCK_DATA: Record<
+  (typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
+  Data
+> = {
+  "0x000000": mkMockData(true),
+  "0x111111": mkMockData(false),
+};
+
+const mkMockHistory = (): AccountHistory => [
+  {
+    timestamp: new Date("2024-06-10T00:00:00Z"),
+    action: AccountHistoryAction.Deposit(),
+    amount: 2_000_000n,
+    accountTotal: 2_000_000n,
+    availableRewards: 0n,
+    availableToWithdraw: 2_000_000n,
+    locked: 0n,
+  },
+  {
+    timestamp: new Date("2024-06-14T02:00:00Z"),
+    action: AccountHistoryAction.RewardsCredited(),
+    amount: 200n,
+    accountTotal: 2_000_000n,
+    availableRewards: 200n,
+    availableToWithdraw: 2_000_000n,
+    locked: 0n,
+  },
+  {
+    timestamp: new Date("2024-06-16T08:00:00Z"),
+    action: AccountHistoryAction.Claim(),
+    amount: 200n,
+    accountTotal: 2_000_200n,
+    availableRewards: 0n,
+    availableToWithdraw: 2_000_200n,
+    locked: 0n,
+  },
+  {
+    timestamp: new Date("2024-06-16T08:00:00Z"),
+    action: AccountHistoryAction.Slash("Cboe"),
+    amount: 1000n,
+    accountTotal: 1_999_200n,
+    availableRewards: 0n,
+    availableToWithdraw: 1_999_200n,
+    locked: 0n,
+  },
+];
+
+const MOCK_HISTORY: Record<
+  (typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
+  AccountHistory
+> = {
+  "0x000000": mkMockHistory(),
+  "0x111111": mkMockHistory(),
+};

+ 185 - 0
apps/staking/src/components/AccountHistoryButton/index.tsx

@@ -0,0 +1,185 @@
+import useSWR from "swr";
+
+import {
+  type AccountHistoryAction,
+  type StakeDetails,
+  AccountHistoryItemType,
+  StakeType,
+  loadAccountHistory,
+} from "../../api";
+import { useApiContext } from "../../use-api-context";
+import { LoadingSpinner } from "../LoadingSpinner";
+import { ModalButton } from "../ModalButton";
+import { Tokens } from "../Tokens";
+
+const ONE_SECOND_IN_MS = 1000;
+const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
+const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+
+export const AccountHistoryButton = () => (
+  <ModalButton
+    title="Account history"
+    description="A history of events that have affected your account balances"
+  >
+    <ModalBody />
+  </ModalButton>
+);
+
+const ModalBody = () => {
+  const history = useAccountHistoryData();
+
+  switch (history.type) {
+    case DataStateType.NotLoaded:
+    case DataStateType.Loading: {
+      return <LoadingSpinner />;
+    }
+    case DataStateType.Error: {
+      return <p>Uh oh, an error occured!</p>;
+    }
+    case DataStateType.Loaded: {
+      return (
+        <table className="text-sm">
+          <thead className="font-medium">
+            <tr>
+              <td className="pr-4">Timestamp</td>
+              <td className="pr-4">Description</td>
+              <td className="pr-4">Amount</td>
+              <td className="pr-4">Account Total</td>
+              <td className="pr-4">Available Rewards</td>
+              <td className="pr-4">Available to Withdraw</td>
+              <td>Locked</td>
+            </tr>
+          </thead>
+          <tbody>
+            {history.data.map(
+              (
+                {
+                  accountTotal,
+                  action,
+                  amount,
+                  availableRewards,
+                  availableToWithdraw,
+                  locked,
+                  timestamp,
+                },
+                i,
+              ) => (
+                <tr key={i}>
+                  <td className="pr-4">{timestamp.toLocaleString()}</td>
+                  <td className="pr-4">{mkDescription(action)}</td>
+                  <td className="pr-4">
+                    <Tokens>{amount}</Tokens>
+                  </td>
+                  <td className="pr-4">
+                    <Tokens>{accountTotal}</Tokens>
+                  </td>
+                  <td className="pr-4">
+                    <Tokens>{availableRewards}</Tokens>
+                  </td>
+                  <td className="pr-4">
+                    <Tokens>{availableToWithdraw}</Tokens>
+                  </td>
+                  <td>
+                    <Tokens>{locked}</Tokens>
+                  </td>
+                </tr>
+              ),
+            )}
+          </tbody>
+        </table>
+      );
+    }
+  }
+};
+
+const mkDescription = (action: AccountHistoryAction): string => {
+  switch (action.type) {
+    case AccountHistoryItemType.Claim: {
+      return "Rewards claimed";
+    }
+    case AccountHistoryItemType.Deposit: {
+      return "Tokens deposited";
+    }
+    case AccountHistoryItemType.LockedDeposit: {
+      return `Locked tokens deposited, unlocking ${action.unlockDate.toLocaleString()}`;
+    }
+    case AccountHistoryItemType.RewardsCredited: {
+      return "Rewards credited";
+    }
+    case AccountHistoryItemType.Slash: {
+      return `Staked tokens slashed from ${action.publisherName}`;
+    }
+    case AccountHistoryItemType.StakeCreated: {
+      return `Created stake position for ${getStakeDetails(action.details)}`;
+    }
+    case AccountHistoryItemType.StakeFinishedWarmup: {
+      return `Warmup complete for position for ${getStakeDetails(action.details)}`;
+    }
+    case AccountHistoryItemType.Unlock: {
+      return "Locked tokens unlocked";
+    }
+    case AccountHistoryItemType.UnstakeCreated: {
+      return `Requested unstake for position for ${getStakeDetails(action.details)}`;
+    }
+    case AccountHistoryItemType.UnstakeExitedCooldown: {
+      return `Cooldown completed for ${getStakeDetails(action.details)}`;
+    }
+    case AccountHistoryItemType.Withdrawal: {
+      return "Tokens withdrawn to wallet";
+    }
+  }
+};
+
+const getStakeDetails = (details: StakeDetails): string => {
+  switch (details.type) {
+    case StakeType.Governance: {
+      return "Governance Staking";
+    }
+    case StakeType.IntegrityStaking: {
+      return `Integrity Staking, publisher: ${details.publisherName}`;
+    }
+  }
+};
+
+const useAccountHistoryData = () => {
+  const apiContext = useApiContext();
+
+  const { data, isLoading, ...rest } = useSWR(
+    `${apiContext.stakeAccount.publicKey}/history`,
+    () => loadAccountHistory(apiContext),
+    {
+      refreshInterval: REFRESH_INTERVAL,
+    },
+  );
+  const error = rest.error as unknown;
+
+  if (error) {
+    return DataState.ErrorState(error);
+  } else if (isLoading) {
+    return DataState.Loading();
+  } else if (data) {
+    return DataState.Loaded(data);
+  } else {
+    return DataState.NotLoaded();
+  }
+};
+
+enum DataStateType {
+  NotLoaded,
+  Loading,
+  Loaded,
+  Error,
+}
+const DataState = {
+  NotLoaded: () => ({ type: DataStateType.NotLoaded as const }),
+  Loading: () => ({ type: DataStateType.Loading as const }),
+  Loaded: (data: Awaited<ReturnType<typeof loadAccountHistory>>) => ({
+    type: DataStateType.Loaded as const,
+    data,
+  }),
+  ErrorState: (error: unknown) => ({
+    type: DataStateType.Error as const,
+    error,
+  }),
+};
+type DataState = ReturnType<(typeof DataState)[keyof typeof DataState]>;

+ 137 - 42
apps/staking/src/components/Dashboard/index.tsx

@@ -1,67 +1,162 @@
 "use client";
 
-import { ArrowPathIcon } from "@heroicons/react/24/outline";
-import { useWallet, useConnection } from "@solana/wallet-adapter-react";
-import { type ComponentProps, useCallback, useEffect, useState } from "react";
+import {
+  Listbox,
+  ListboxButton,
+  ListboxOptions,
+  ListboxOption,
+  Field,
+  Label,
+} from "@headlessui/react";
+import { ChevronDownIcon } from "@heroicons/react/24/outline";
+import useSWR from "swr";
 
 import { DashboardLoaded } from "./loaded";
-import { loadData } from "../../api";
+import { WalletButton } from "./wallet-button";
+import { type StakeAccount, loadData } from "../../api";
+import { useApiContext } from "../../use-api-context";
+import {
+  StateType,
+  StakeAccountProvider,
+  useStakeAccount,
+} from "../../use-stake-account";
+import { AccountHistoryButton } from "../AccountHistoryButton";
+import { LoadingSpinner } from "../LoadingSpinner";
 
-export const Dashboard = () => {
-  const { data, replaceData } = useDashboardData();
+const ONE_SECOND_IN_MS = 1000;
+const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
+const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+
+export const Dashboard = () => (
+  <StakeAccountProvider>
+    <DashboardHeader />
+    <DashboardBody />
+  </StakeAccountProvider>
+);
+
+const DashboardHeader = () => {
+  const stakeAccountState = useStakeAccount();
+  return (
+    <header className="mb-4 flex flex-row items-center justify-end gap-4">
+      {stakeAccountState.type === StateType.Loaded &&
+        stakeAccountState.allAccounts.length > 1 && (
+          <AccountSelector
+            accounts={stakeAccountState.allAccounts}
+            selectedAccount={stakeAccountState.account}
+            setAccount={stakeAccountState.selectAccount}
+          />
+        )}
+      {stakeAccountState.type === StateType.Loaded && <AccountHistoryButton />}
+      <WalletButton />
+    </header>
+  );
+};
+
+type AccountSelectorProps = {
+  selectedAccount: StakeAccount;
+  accounts: [StakeAccount, ...StakeAccount[]];
+  setAccount: (account: StakeAccount) => void;
+};
+
+const AccountSelector = ({
+  accounts,
+  selectedAccount,
+  setAccount,
+}: AccountSelectorProps) => (
+  <Field className="flex flex-row items-center gap-2">
+    <Label className="text-sm font-medium">Stake Account:</Label>
+    <Listbox
+      value={selectedAccount}
+      onChange={setAccount}
+      as="div"
+      className="relative"
+    >
+      <ListboxButton className="flex flex-row items-center gap-4 rounded border border-black px-4 py-2">
+        <pre>{selectedAccount.publicKey}</pre>
+        <ChevronDownIcon className="size-4" />
+      </ListboxButton>
+      <ListboxOptions
+        className="min-w-[var(--button-width)] rounded-xl border border-white/5 bg-white p-1 shadow transition duration-100 ease-in [--anchor-gap:var(--spacing-1)] focus:outline-none data-[leave]:data-[closed]:opacity-0"
+        anchor="bottom start"
+        transition
+      >
+        {accounts.map((account) => (
+          <ListboxOption
+            key={account.publicKey}
+            value={account}
+            className="cursor-pointer hover:bg-black/5"
+          >
+            <pre>{account.publicKey}</pre>
+          </ListboxOption>
+        ))}
+      </ListboxOptions>
+    </Listbox>
+  </Field>
+);
+
+const DashboardBody = () => {
+  const stakeAccountState = useStakeAccount();
+
+  switch (stakeAccountState.type) {
+    case StateType.Initialized:
+    case StateType.Loading: {
+      return <LoadingSpinner />;
+    }
+    case StateType.NoAccounts: {
+      return <p>No stake account found for your wallet!</p>;
+    }
+    case StateType.Error: {
+      return (
+        <p>
+          Uh oh, an error occurred while loading stake accounts. Please refresh
+          and try again
+        </p>
+      );
+    }
+    case StateType.Loaded: {
+      return <DashboardContents />;
+    }
+  }
+};
+
+const DashboardContents = () => {
+  const data = useDashboardData();
 
   switch (data.type) {
     case DataStateType.NotLoaded:
     case DataStateType.Loading: {
-      return <ArrowPathIcon className="size-6 animate-spin" />;
+      return <LoadingSpinner />;
     }
     case DataStateType.Error: {
       return <p>Uh oh, an error occured!</p>;
     }
     case DataStateType.Loaded: {
-      return <DashboardLoaded {...data.data} replaceData={replaceData} />;
+      return <DashboardLoaded {...data.data} />;
     }
   }
 };
 
-type DashboardData = Omit<
-  ComponentProps<typeof DashboardLoaded>,
-  "replaceData"
->;
-
 const useDashboardData = () => {
-  const [data, setData] = useState<DataState>(DataState.NotLoaded());
-  const wallet = useWallet();
-  const { connection } = useConnection();
+  const apiContext = useApiContext();
 
-  const replaceData = useCallback(
-    (newData: DashboardData) => {
-      setData(DataState.Loaded(newData));
+  const { data, isLoading, ...rest } = useSWR(
+    apiContext.stakeAccount.publicKey,
+    () => loadData(apiContext),
+    {
+      refreshInterval: REFRESH_INTERVAL,
     },
-    [setData],
   );
+  const error = rest.error as unknown;
 
-  useEffect(() => {
-    if (data.type === DataStateType.NotLoaded) {
-      setData(DataState.Loading());
-      const abortController = new AbortController();
-      loadData(connection, wallet, abortController.signal)
-        .then((data) => {
-          setData(DataState.Loaded(data));
-        })
-        .catch((error: unknown) => {
-          setData(DataState.ErrorState(error));
-        });
-      return () => {
-        abortController.abort();
-      };
-    } else {
-      return;
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, []);
-
-  return { data, replaceData };
+  if (error) {
+    return DataState.ErrorState(error);
+  } else if (isLoading) {
+    return DataState.Loading();
+  } else if (data) {
+    return DataState.Loaded(data);
+  } else {
+    return DataState.NotLoaded();
+  }
 };
 
 enum DataStateType {
@@ -73,7 +168,7 @@ enum DataStateType {
 const DataState = {
   NotLoaded: () => ({ type: DataStateType.NotLoaded as const }),
   Loading: () => ({ type: DataStateType.Loading as const }),
-  Loaded: (data: DashboardData) => ({
+  Loaded: (data: Awaited<ReturnType<typeof loadData>>) => ({
     type: DataStateType.Loaded as const,
     data,
   }),

+ 347 - 137
apps/staking/src/components/Dashboard/loaded.tsx

@@ -1,8 +1,7 @@
-import type { WalletContextState } from "@solana/wallet-adapter-react";
-import type { Connection } from "@solana/web3.js";
 import clsx from "clsx";
 import { type ReactNode, useMemo, useCallback } from "react";
 
+import { SparkChart } from "./spark-chart";
 import {
   deposit,
   withdraw,
@@ -13,38 +12,53 @@ import {
   cancelWarmupIntegrityStaking,
   unstakeIntegrityStaking,
   claim,
+  calculateApy,
 } from "../../api";
+import type { Context } from "../../use-api-context";
 import { StateType, useTransfer } from "../../use-transfer";
 import { Button } from "../Button";
+import { ModalButton } from "../ModalButton";
 import { Tokens } from "../Tokens";
 import { TransferButton } from "../TransferButton";
 
 type Props = {
-  replaceData: (newData: Omit<Props, "replaceData">) => void;
   total: bigint;
+  lastSlash:
+    | {
+        amount: bigint;
+        date: Date;
+      }
+    | undefined;
   walletAmount: bigint;
   availableRewards: bigint;
+  expiringRewards: {
+    amount: bigint;
+    expiry: Date;
+  };
   locked: bigint;
+  unlockSchedule: {
+    amount: bigint;
+    date: Date;
+  }[];
   governance: {
     warmup: bigint;
     staked: bigint;
     cooldown: bigint;
     cooldown2: bigint;
   };
-  integrityStakingPublishers: Omit<
-    PublisherProps,
-    "availableToStake" | "replaceData"
-  >[];
+  integrityStakingPublishers: PublisherProps["publisher"][];
 };
 
 export const DashboardLoaded = ({
   total,
+  lastSlash,
   walletAmount,
   availableRewards,
+  expiringRewards,
   governance,
   integrityStakingPublishers,
   locked,
-  replaceData,
+  unlockSchedule,
 }: Props) => {
   const availableToStakeGovernance = useMemo(
     () =>
@@ -102,6 +116,16 @@ export const DashboardLoaded = ({
     [availableToStakeGovernance, availableToStakeIntegrity],
   );
 
+  const self = useMemo(
+    () => integrityStakingPublishers.find((publisher) => publisher.isSelf),
+    [integrityStakingPublishers],
+  );
+
+  const otherPublishers = useMemo(
+    () => integrityStakingPublishers.filter((publisher) => !publisher.isSelf),
+    [integrityStakingPublishers],
+  );
+
   return (
     <>
       <div className="flex w-full flex-col gap-8 bg-pythpurple-100 p-8">
@@ -113,12 +137,19 @@ export const DashboardLoaded = ({
                 actionDescription="Add funds to your balance"
                 actionName="Deposit"
                 max={walletAmount}
-                replaceData={replaceData}
                 transfer={deposit}
               >
                 <strong>In wallet:</strong> <Tokens>{walletAmount}</Tokens>
               </TransferButton>
             }
+            {...(lastSlash && {
+              disclaimer: (
+                <>
+                  <Tokens>{lastSlash.amount}</Tokens> were slashed on{" "}
+                  {lastSlash.date.toLocaleString()}
+                </>
+              ),
+            })}
           >
             {total}
           </BalanceCategory>
@@ -126,31 +157,82 @@ export const DashboardLoaded = ({
             name="Available to withdraw"
             description="The lesser of the amount you have available to stake in governance & integrity staking"
             {...(availableToWithdraw > 0 && {
-              actions: (
-                <TransferButton
-                  actionDescription="Move funds from your account back to your wallet"
-                  actionName="Withdraw"
-                  max={availableToWithdraw}
-                  replaceData={replaceData}
-                  transfer={withdraw}
-                >
-                  <strong>Available to withdraw:</strong>{" "}
-                  <Tokens>{availableToWithdraw}</Tokens>
-                </TransferButton>
-              ),
+              actions:
+                availableRewards > 0 ? (
+                  <ClaimRequiredButton
+                    buttonText="Withdraw"
+                    description="Before you can withdraw tokens, you must claim your unclaimed rewards"
+                    availableRewards={availableRewards}
+                  />
+                ) : (
+                  <TransferButton
+                    actionDescription="Move funds from your account back to your wallet"
+                    actionName="Withdraw"
+                    max={availableToWithdraw}
+                    transfer={withdraw}
+                  >
+                    <strong>Available to withdraw:</strong>{" "}
+                    <Tokens>{availableToWithdraw}</Tokens>
+                  </TransferButton>
+                ),
             })}
           >
             {availableToWithdraw}
           </BalanceCategory>
           <BalanceCategory
-            name="Claimable rewards"
-            description="Rewards you have earned but not yet claimed from the Integrity Staking program"
+            name="Available rewards"
+            description="Rewards you have earned but not yet claimed from the Integrity Staking program."
+            {...(expiringRewards.amount > 0n && {
+              disclaimer: (
+                <>
+                  <Tokens>{expiringRewards.amount}</Tokens> will expire on{" "}
+                  {expiringRewards.expiry.toLocaleString()} if you have not
+                  claimed before then
+                </>
+              ),
+            })}
             {...(availableRewards > 0 && {
-              actions: <ClaimButton replaceData={replaceData} />,
+              actions: <ClaimButton />,
             })}
           >
             {availableRewards}
           </BalanceCategory>
+          {locked && (
+            <BalanceCategory
+              name="Locked tokens"
+              description="Locked tokens cannot be withdrawn to your wallet and cannot participate in Integrity Staking."
+              actions={
+                <ModalButton
+                  title="Unlock Schedule"
+                  buttonContent="Show Unlock Schedule"
+                  description="Your tokens will become available for withdrawal and for participation in Integrity Staking according to this schedule"
+                >
+                  <table>
+                    <thead className="font-medium">
+                      <tr>
+                        <td>Date</td>
+                        <td>Amount</td>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      {unlockSchedule.map((unlock, i) => (
+                        <tr key={i}>
+                          <td className="pr-4">
+                            {unlock.date.toLocaleString()}
+                          </td>
+                          <td>
+                            <Tokens>{unlock.amount}</Tokens>
+                          </td>
+                        </tr>
+                      ))}
+                    </tbody>
+                  </table>
+                </ModalButton>
+              }
+            >
+              {locked}
+            </BalanceCategory>
+          )}
         </div>
         <div className="flex flex-col items-stretch justify-between gap-8">
           <section className="bg-black/10 p-4">
@@ -165,7 +247,6 @@ export const DashboardLoaded = ({
                     actionDescription="Stake funds to participate in governance votes"
                     actionName="Stake"
                     max={availableToStakeGovernance}
-                    replaceData={replaceData}
                     transfer={stakeGovernance}
                   >
                     <strong>Available to stake:</strong>{" "}
@@ -185,7 +266,6 @@ export const DashboardLoaded = ({
                     submitButtonText="Cancel Warmup"
                     title="Cancel Governance Staking"
                     max={governance.warmup}
-                    replaceData={replaceData}
                     transfer={cancelWarmupGovernance}
                   >
                     <strong>Max:</strong> <Tokens>{governance.warmup}</Tokens>
@@ -206,7 +286,6 @@ export const DashboardLoaded = ({
                     actionName="Unstake"
                     title="Unstake From Governance"
                     max={governance.staked}
-                    replaceData={replaceData}
                     transfer={unstakeGovernance}
                   >
                     <strong>Max:</strong> <Tokens>{governance.staked}</Tokens>
@@ -271,24 +350,53 @@ export const DashboardLoaded = ({
                 {integrityStakingCooldown2}
               </Position>
             </div>
+            {self && (
+              <div className="mt-8 bg-black/5 p-4">
+                <table className="w-full text-left">
+                  <caption className="mb-4 text-left text-xl">
+                    You ({self.name})
+                  </caption>
+                  <thead>
+                    <tr>
+                      <th className="text-center">Pool</th>
+                      <th className="text-center">Historical APY</th>
+                      <th>Number of feeds</th>
+                      <th>Quality ranking</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <Publisher
+                      availableToStake={availableToStakeIntegrity}
+                      availableRewards={availableRewards}
+                      publisher={self}
+                      omitName
+                      omitSelfStake
+                    />
+                  </tbody>
+                </table>
+              </div>
+            )}
             <table className="mt-8 w-full text-left">
-              <caption className="mb-4 text-left text-xl">Publishers</caption>
+              <caption className="mb-4 text-left text-xl">
+                {self ? "Other Publishers" : "Publishers"}
+              </caption>
               <thead>
                 <tr>
                   <th>Publisher</th>
                   <th>Self stake</th>
                   <th className="text-center">Pool</th>
+                  <th className="text-center">Historical APY</th>
                   <th>Number of feeds</th>
                   <th>Quality ranking</th>
                 </tr>
               </thead>
               <tbody>
-                {integrityStakingPublishers.map((publisher) => (
+                {otherPublishers.map((publisher) => (
                   <Publisher
                     key={publisher.publicKey}
                     availableToStake={availableToStakeIntegrity}
-                    replaceData={replaceData}
-                    {...publisher}
+                    availableRewards={availableRewards}
+                    publisher={publisher}
                   />
                 ))}
               </tbody>
@@ -315,8 +423,9 @@ const useIntegrityStakingSum = (
 
 type BalanceCategoryProps = {
   children: bigint;
-  name: string;
-  description?: string | undefined;
+  name: ReactNode | ReactNode[];
+  description?: ReactNode | ReactNode[] | undefined;
+  disclaimer?: ReactNode | ReactNode[] | undefined;
   actions?: ReactNode | ReactNode[];
 };
 
@@ -324,6 +433,7 @@ const BalanceCategory = ({
   children,
   name,
   description,
+  disclaimer,
   actions,
 }: BalanceCategoryProps) => (
   <div className="flex w-1/3 flex-col items-start justify-between gap-2">
@@ -331,10 +441,15 @@ const BalanceCategory = ({
       <div className="text-4xl font-semibold">
         <Tokens>{children}</Tokens>
       </div>
-      <div className="text-lg">{name}</div>
+      <div className="flex items-center text-lg">{name}</div>
       {description && (
         <p className="max-w-xs text-xs font-light">{description}</p>
       )}
+      {disclaimer && (
+        <p className="mt-2 max-w-xs text-sm font-medium text-red-600">
+          {disclaimer}
+        </p>
+      )}
     </div>
     {actions && <div>{actions}</div>}
   </div>
@@ -375,63 +490,60 @@ const Position = ({
   );
 
 type PublisherProps = {
+  availableRewards: bigint;
   availableToStake: bigint;
-  replaceData: Props["replaceData"];
-  name: string;
-  publicKey: string;
-  selfStake: bigint;
-  poolCapacity: bigint;
-  poolUtilization: bigint;
-  apy: number;
-  numFeeds: number;
-  qualityRanking: number;
-  positions?:
-    | {
-        warmup?: bigint | undefined;
-        staked?: bigint | undefined;
-        cooldown?: bigint | undefined;
-        cooldown2?: bigint | undefined;
-      }
-    | undefined;
+  omitName?: boolean;
+  omitSelfStake?: boolean;
+  publisher: {
+    name: string;
+    publicKey: string;
+    isSelf: boolean;
+    selfStake: bigint;
+    poolCapacity: bigint;
+    poolUtilization: bigint;
+    numFeeds: number;
+    qualityRanking: number;
+    apyHistory: { date: Date; apy: number }[];
+    positions?:
+      | {
+          warmup?: bigint | undefined;
+          staked?: bigint | undefined;
+          cooldown?: bigint | undefined;
+          cooldown2?: bigint | undefined;
+        }
+      | undefined;
+  };
 };
 
 const Publisher = ({
-  name,
-  publicKey,
-  selfStake,
-  poolUtilization,
-  poolCapacity,
-  apy,
-  numFeeds,
-  qualityRanking,
-  positions,
+  availableRewards,
+  publisher,
   availableToStake,
-  replaceData,
+  omitName,
+  omitSelfStake,
 }: PublisherProps) => {
-  const delegate = useTransferActionForPublisher(
-    delegateIntegrityStaking,
-    publicKey,
-  );
   const cancelWarmup = useTransferActionForPublisher(
     cancelWarmupIntegrityStaking,
-    publicKey,
+    publisher.publicKey,
   );
   const unstake = useTransferActionForPublisher(
     unstakeIntegrityStaking,
-    publicKey,
+    publisher.publicKey,
   );
   const utilizationPercent = useMemo(
-    () => Number((100n * poolUtilization) / poolCapacity),
-    [poolUtilization, poolCapacity],
+    () => Number((100n * publisher.poolUtilization) / publisher.poolCapacity),
+    [publisher.poolUtilization, publisher.poolCapacity],
   );
 
   return (
     <>
       <tr>
-        <td className="py-4">{name}</td>
-        <td>
-          <Tokens>{selfStake}</Tokens>
-        </td>
+        {!omitName && <td className="py-4">{publisher.name}</td>}
+        {!omitSelfStake && (
+          <td>
+            <Tokens>{publisher.selfStake}</Tokens>
+          </td>
+        )}
         <td className="flex flex-row items-center justify-center gap-2 py-4">
           <div className="relative grid h-8 w-60 place-content-center border border-black bg-pythpurple-600/10">
             <div
@@ -440,7 +552,7 @@ const Publisher = ({
               }}
               className={clsx(
                 "absolute inset-0 max-w-full",
-                poolUtilization > poolCapacity
+                publisher.poolUtilization > publisher.poolCapacity
                   ? "bg-red-500"
                   : "bg-pythpurple-400",
               )}
@@ -448,45 +560,63 @@ const Publisher = ({
             <div
               className={clsx(
                 "isolate flex flex-row items-center justify-center gap-1 text-sm",
-                { "text-white": poolUtilization > poolCapacity },
+                {
+                  "text-white":
+                    publisher.poolUtilization > publisher.poolCapacity,
+                },
               )}
             >
               <span>
-                <Tokens>{poolUtilization}</Tokens>
+                <Tokens>{publisher.poolUtilization}</Tokens>
               </span>
               <span>/</span>
               <span>
-                <Tokens>{poolCapacity}</Tokens>
+                <Tokens>{publisher.poolCapacity}</Tokens>
               </span>
               <span>({utilizationPercent.toFixed(2)}%)</span>
             </div>
           </div>
           <div className="flex flex-row items-center gap-1">
             <div className="font-medium">APY:</div>
-            <div>{apy}%</div>
+            <div>
+              {calculateApy(
+                publisher.poolCapacity,
+                publisher.poolUtilization,
+                publisher.isSelf,
+              )}
+              %
+            </div>
+          </div>
+        </td>
+        <td className="px-4">
+          <div className="mx-auto h-14 w-28 border border-black bg-white/40">
+            <SparkChart
+              data={publisher.apyHistory.map(({ date, apy }) => ({
+                date,
+                value: apy,
+              }))}
+            />
           </div>
         </td>
-        <td>{numFeeds}</td>
-        <td>{qualityRanking}</td>
+        <td>{publisher.numFeeds}</td>
+        <td>{publisher.qualityRanking}</td>
         {availableToStake > 0 && (
           <td>
-            <TransferButton
-              actionDescription={`Stake to ${name}`}
-              actionName="Stake"
-              max={availableToStake}
-              replaceData={replaceData}
-              transfer={delegate}
-            >
-              <strong>Available to stake:</strong>{" "}
-              <Tokens>{availableToStake}</Tokens>
-            </TransferButton>
+            <StakeToPublisherButton
+              availableToStake={availableToStake}
+              poolCapacity={publisher.poolCapacity}
+              poolUtilization={publisher.poolUtilization}
+              publisherKey={publisher.publicKey}
+              publisherName={publisher.name}
+              isSelf={publisher.isSelf}
+            />
           </td>
         )}
       </tr>
-      {positions && (
-        <tr>
+      {publisher.positions && (
+        <tr className="group">
           <td colSpan={6} className="border-separate border-spacing-8">
-            <div className="mx-auto mb-8 w-fit bg-black/5 p-4">
+            <div className="mx-auto mb-8 w-fit bg-black/5 p-4 group-last:mb-0">
               <table className="w-full">
                 <caption className="mb-2 text-left text-xl">
                   Your Positions
@@ -502,44 +632,50 @@ const Publisher = ({
                     name="Warmup"
                     actions={
                       <TransferButton
-                        actionDescription={`Cancel tokens that are in warmup for staking to ${name}`}
+                        actionDescription={`Cancel tokens that are in warmup for staking to ${publisher.name}`}
                         actionName="Cancel"
                         submitButtonText="Cancel Warmup"
                         title="Cancel Staking"
-                        max={positions.warmup ?? 0n}
-                        replaceData={replaceData}
+                        max={publisher.positions.warmup ?? 0n}
                         transfer={cancelWarmup}
                       >
                         <strong>Max:</strong>{" "}
-                        <Tokens>{positions.warmup ?? 0n}</Tokens>
+                        <Tokens>{publisher.positions.warmup ?? 0n}</Tokens>
                       </TransferButton>
                     }
                   >
-                    {positions.warmup}
+                    {publisher.positions.warmup}
                   </PublisherPosition>
                   <PublisherPosition
                     name="Staked"
                     actions={
-                      <TransferButton
-                        actionDescription={`Unstake tokens from ${name}`}
-                        actionName="Unstake"
-                        title="Unstake"
-                        max={positions.staked ?? 0n}
-                        replaceData={replaceData}
-                        transfer={unstake}
-                      >
-                        <strong>Max:</strong>{" "}
-                        <Tokens>{positions.staked ?? 0n}</Tokens>
-                      </TransferButton>
+                      availableRewards > 0 ? (
+                        <ClaimRequiredButton
+                          buttonText="Unstake"
+                          description={`Before you can unstake tokens from ${publisher.name}, you must claim your unclaimed rewards`}
+                          availableRewards={availableRewards}
+                        />
+                      ) : (
+                        <TransferButton
+                          actionDescription={`Unstake tokens from ${publisher.name}`}
+                          actionName="Unstake"
+                          title="Unstake"
+                          max={publisher.positions.staked ?? 0n}
+                          transfer={unstake}
+                        >
+                          <strong>Max:</strong>{" "}
+                          <Tokens>{publisher.positions.staked ?? 0n}</Tokens>
+                        </TransferButton>
+                      )
                     }
                   >
-                    {positions.staked}
+                    {publisher.positions.staked}
                   </PublisherPosition>
                   <PublisherPosition name="Cooldown (this epoch)">
-                    {positions.cooldown}
+                    {publisher.positions.cooldown}
                   </PublisherPosition>
                   <PublisherPosition name="Cooldown (next epoch)">
-                    {positions.cooldown2}
+                    {publisher.positions.cooldown2}
                   </PublisherPosition>
                 </tbody>
               </table>
@@ -551,21 +687,6 @@ const Publisher = ({
   );
 };
 
-const useTransferActionForPublisher = (
-  action: (
-    connection: Connection,
-    wallet: WalletContextState,
-    publicKey: string,
-    amount: bigint,
-  ) => Promise<void>,
-  publicKey: string,
-) =>
-  useCallback(
-    (connection: Connection, wallet: WalletContextState, amount: bigint) =>
-      action(connection, wallet, publicKey, amount),
-    [action, publicKey],
-  );
-
 type PublisherPositionProps = {
   name: string;
   children: bigint | undefined;
@@ -591,23 +712,112 @@ const PublisherPosition = ({
 // eslint-disable-next-line unicorn/no-array-reduce
 const bigIntMin = (...args: bigint[]) => args.reduce((m, e) => (e < m ? e : m));
 
-type ClaimButtonProps = {
-  replaceData: Props["replaceData"];
-};
-
-const ClaimButton = ({ replaceData }: ClaimButtonProps) => {
-  const { state, execute } = useTransfer(claim, replaceData);
+const ClaimButton = () => {
+  const { state, execute } = useTransfer(claim);
 
   return (
     <Button
       onClick={execute}
       disabled={state.type !== StateType.Base}
-      loading={
-        state.type === StateType.LoadingData ||
-        state.type === StateType.Submitting
-      }
+      loading={state.type === StateType.Submitting}
     >
       Claim
     </Button>
   );
 };
+
+type ClaimRequiredButtonProps = {
+  buttonText: string;
+  description: string;
+  availableRewards: bigint;
+};
+
+const ClaimRequiredButton = ({
+  buttonText,
+  description,
+  availableRewards,
+}: ClaimRequiredButtonProps) => {
+  const { state, execute } = useTransfer(claim);
+
+  const isSubmitting = state.type === StateType.Submitting;
+
+  return (
+    <ModalButton
+      buttonContent={buttonText}
+      title="Claim Required"
+      closeDisabled={isSubmitting}
+      additionalButtons={(close) => (
+        <Button
+          onClick={() => execute().then(close)}
+          disabled={state.type !== StateType.Base}
+          loading={isSubmitting}
+        >
+          Claim
+        </Button>
+      )}
+      description={description}
+    >
+      <div>
+        <strong>Available Rewards:</strong> <Tokens>{availableRewards}</Tokens>
+      </div>
+    </ModalButton>
+  );
+};
+
+type StakeToPublisherButtonProps = {
+  publisherName: string;
+  publisherKey: string;
+  availableToStake: bigint;
+  poolCapacity: bigint;
+  poolUtilization: bigint;
+  isSelf: boolean;
+};
+
+const StakeToPublisherButton = ({
+  publisherName,
+  publisherKey,
+  poolCapacity,
+  poolUtilization,
+  availableToStake,
+  isSelf,
+}: StakeToPublisherButtonProps) => {
+  const delegate = useTransferActionForPublisher(
+    delegateIntegrityStaking,
+    publisherKey,
+  );
+
+  return (
+    <TransferButton
+      actionDescription={`Stake to ${publisherName}`}
+      actionName="Stake"
+      max={availableToStake}
+      transfer={delegate}
+    >
+      {(amount) => (
+        <>
+          <strong>Available to stake:</strong>{" "}
+          <Tokens>{availableToStake}</Tokens>
+          {amount !== undefined && (
+            <div>
+              Staking these tokens will change the APY to:{" "}
+              {calculateApy(poolCapacity, poolUtilization + amount, isSelf)}%
+            </div>
+          )}
+        </>
+      )}
+    </TransferButton>
+  );
+};
+
+const useTransferActionForPublisher = (
+  action: (
+    context: Context,
+    publicKey: string,
+    amount: bigint,
+  ) => Promise<void>,
+  publicKey: string,
+) =>
+  useCallback(
+    (context: Context, amount: bigint) => action(context, publicKey, amount),
+    [action, publicKey],
+  );

+ 40 - 0
apps/staking/src/components/Dashboard/spark-chart.tsx

@@ -0,0 +1,40 @@
+"use client";
+
+import { useCallback } from "react";
+import { ResponsiveContainer, LineChart, Tooltip, Line, XAxis } from "recharts";
+
+type Props = {
+  data: { date: Date; value: number }[];
+};
+
+export const SparkChart = ({ data }: Props) => {
+  const formatDate = useCallback((date: Date) => date.toLocaleDateString(), []);
+
+  return (
+    <ResponsiveContainer width="100%" height="100%">
+      <LineChart data={data}>
+        <Tooltip
+          content={<TooltipContent formatDate={formatDate} />}
+          allowEscapeViewBox={{ x: true, y: true }}
+        />
+        <Line type="monotone" dataKey="value" />
+        <XAxis dataKey="date" hide />
+      </LineChart>
+    </ResponsiveContainer>
+  );
+};
+
+type TooltipProps = {
+  formatDate: (date: Date) => string;
+  label?: Date;
+  payload?: {
+    value?: number;
+  }[];
+};
+
+const TooltipContent = ({ payload, label, formatDate }: TooltipProps) => (
+  <div className="flex flex-row gap-2 rounded bg-white p-2 text-xs shadow">
+    <span className="font-medium">{label ? formatDate(label) : ""}</span>
+    <span>{payload?.[0]?.value ?? 0}</span>
+  </div>
+);

+ 1 - 1
apps/staking/src/components/Home/wallet-button.tsx → apps/staking/src/components/Dashboard/wallet-button.tsx

@@ -24,7 +24,7 @@ export const WalletButton = (
           /* no-op, no worries if we can't show a SNS domain */
         });
     }
-  }, [wallet, connection]);
+  }, [wallet.publicKey, connection]);
 
   return <WalletMultiButton {...props}>{primaryDomain}</WalletMultiButton>;
 };

+ 4 - 7
apps/staking/src/components/Home/index.tsx

@@ -1,14 +1,13 @@
 "use client";
 
-import { ArrowPathIcon } from "@heroicons/react/24/outline";
 import { useWallet } from "@solana/wallet-adapter-react";
 import { useWalletModal } from "@solana/wallet-adapter-react-ui";
 import { useCallback } from "react";
 
-import { WalletButton } from "./wallet-button";
 import { useIsMounted } from "../../use-is-mounted";
 import { Button } from "../Button";
 import { Dashboard } from "../Dashboard";
+import { LoadingSpinner } from "../LoadingSpinner";
 
 export const Home = () => (
   <main className="px-8 py-16">
@@ -26,12 +25,10 @@ const HomeContents = () => {
   const showModal = useCallback(() => {
     modal.setVisible(true);
   }, [modal]);
+
   if (isMounted) {
     return wallet.connected ? (
-      <>
-        <WalletButton />
-        <Dashboard />
-      </>
+      <Dashboard />
     ) : (
       <>
         <p className="mx-auto mb-8 max-w-prose text-center">
@@ -47,6 +44,6 @@ const HomeContents = () => {
       </>
     );
   } else {
-    return <ArrowPathIcon className="size-6 animate-spin" />;
+    return <LoadingSpinner />;
   }
 };

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

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

+ 54 - 41
apps/staking/src/components/Modal/index.tsx

@@ -5,9 +5,10 @@ import {
   Description,
   DialogPanel,
   CloseButton,
+  Transition,
 } from "@headlessui/react";
 import { XMarkIcon } from "@heroicons/react/24/outline";
-import type { ReactNode } from "react";
+import { type ReactNode, useCallback } from "react";
 
 import { Button } from "../Button";
 
@@ -17,7 +18,7 @@ type Props = {
   closeDisabled?: boolean | undefined;
   afterLeave?: (() => void) | undefined;
   children?: ReactNode | ReactNode[] | undefined;
-  title: string;
+  title: ReactNode | ReactNode[];
   description?: string;
   additionalButtons?: ReactNode | ReactNode[] | undefined;
 };
@@ -26,49 +27,61 @@ export const Modal = ({
   open,
   onClose,
   closeDisabled,
+  afterLeave,
   children,
   title,
   description,
   additionalButtons,
-}: Props) => (
-  <Dialog open={open} onClose={onClose} className="relative z-50">
-    <DialogBackdrop
-      transition
-      className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0"
-    />
-    <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
-      <DialogPanel
+}: Props) => {
+  const handleClose = useCallback(() => {
+    if (!closeDisabled) {
+      onClose();
+    }
+  }, [closeDisabled, onClose]);
+
+  return (
+    <Dialog open={open} onClose={handleClose} className="relative z-50">
+      <DialogBackdrop
         transition
-        className="relative max-w-lg rounded-md bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
-      >
-        <DialogTitle
-          as="h1"
-          className="text-lg font-medium leading-6 text-neutral-800 dark:text-neutral-200 md:text-xl lg:text-2xl"
+        className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0"
+      />
+      <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
+        <Transition
+          as={DialogPanel}
+          show={open}
+          static
+          className="relative rounded-md bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
+          {...(afterLeave && { afterLeave })}
         >
-          {title}
-        </DialogTitle>
-        {closeDisabled !== true && (
-          <CloseButton className="absolute right-3 top-3 rounded-md p-2 text-neutral-500 transition hover:bg-black/10 dark:hover:bg-white/5">
-            <XMarkIcon className="size-5" />
-          </CloseButton>
-        )}
-        {description && (
-          <Description className="mb-10 mt-2 text-sm text-neutral-500 dark:text-neutral-400">
-            {description}
-          </Description>
-        )}
-        {children}
-        <div className="mt-8 flex flex-row justify-end gap-4 text-right">
-          <CloseButton
-            as={Button}
-            className="px-4 py-2"
-            disabled={closeDisabled ?? false}
+          <DialogTitle
+            as="h1"
+            className="text-lg font-medium leading-6 text-neutral-800 dark:text-neutral-200 md:text-xl lg:text-2xl"
           >
-            Close
-          </CloseButton>
-          {additionalButtons}
-        </div>
-      </DialogPanel>
-    </div>
-  </Dialog>
-);
+            {title}
+          </DialogTitle>
+          {closeDisabled !== true && (
+            <CloseButton className="absolute right-3 top-3 rounded-md p-2 text-neutral-500 transition hover:bg-black/10 dark:hover:bg-white/5">
+              <XMarkIcon className="size-5" />
+            </CloseButton>
+          )}
+          {description && (
+            <Description className="mb-10 mt-2 text-sm text-neutral-500 dark:text-neutral-400">
+              {description}
+            </Description>
+          )}
+          {children}
+          <div className="mt-8 flex flex-row justify-end gap-4 text-right">
+            <CloseButton
+              as={Button}
+              className="px-4 py-2"
+              disabled={closeDisabled ?? false}
+            >
+              Close
+            </CloseButton>
+            {additionalButtons}
+          </div>
+        </Transition>
+      </div>
+    </Dialog>
+  );
+};

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

@@ -0,0 +1,60 @@
+"use client";
+
+import {
+  type ComponentProps,
+  type ReactNode,
+  useCallback,
+  useState,
+} from "react";
+
+import { Button } from "../Button";
+import { Modal } from "../Modal";
+
+type Props = Omit<
+  ComponentProps<typeof Modal>,
+  "open" | "onClose" | "additionalButtons"
+> & {
+  buttonContent?: ReactNode | ReactNode[] | undefined;
+  onClose?: () => void;
+  additionalButtons?:
+    | ((onClose: () => void) => ReactNode | ReactNode[])
+    | ReactNode
+    | ReactNode[]
+    | undefined;
+};
+
+export const ModalButton = ({
+  buttonContent,
+  title,
+  onClose,
+  additionalButtons,
+  ...props
+}: Props) => {
+  const [isOpen, setIsOpen] = useState(false);
+  const close = useCallback(() => {
+    if (onClose) {
+      onClose();
+    }
+    setIsOpen(false);
+  }, [setIsOpen, onClose]);
+  const open = useCallback(() => {
+    setIsOpen(true);
+  }, [setIsOpen]);
+
+  return (
+    <>
+      <Button onClick={open}>{buttonContent ?? title}</Button>
+      <Modal
+        open={isOpen}
+        onClose={close}
+        title={title}
+        additionalButtons={
+          typeof additionalButtons === "function"
+            ? additionalButtons(close)
+            : additionalButtons
+        }
+        {...props}
+      />
+    </>
+  );
+};

+ 0 - 2
apps/staking/src/components/Root/wallet-provider.tsx

@@ -9,7 +9,6 @@ import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
 import {
   GlowWalletAdapter,
   LedgerWalletAdapter,
-  PhantomWalletAdapter,
   SolflareWalletAdapter,
   SolletExtensionWalletAdapter,
   SolletWalletAdapter,
@@ -41,7 +40,6 @@ export const WalletProvider = ({
     () => [
       new GlowWalletAdapter(),
       new LedgerWalletAdapter(),
-      new PhantomWalletAdapter(),
       new SolflareWalletAdapter(),
       new SolletExtensionWalletAdapter(),
       new SolletWalletAdapter(),

+ 8 - 4
apps/staking/src/components/Tokens/index.tsx

@@ -1,17 +1,21 @@
-import { useMemo } from "react";
+import clsx from "clsx";
+import { useMemo, type HTMLAttributes } from "react";
 
 import Pyth from "./pyth.svg";
 import { tokensToString } from "../../tokens";
 
-type Props = {
+type Props = Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
   children: bigint;
 };
 
-export const Tokens = ({ children }: Props) => {
+export const Tokens = ({ children, className, ...props }: Props) => {
   const value = useMemo(() => tokensToString(children), [children]);
 
   return (
-    <span className="inline-flex items-center gap-0.5 align-top">
+    <span
+      className={clsx("inline-flex items-center gap-0.5 align-top", className)}
+      {...props}
+    >
       <Pyth className="aspect-square h-[1em]" />
       <span>{value}</span>
     </span>

+ 87 - 78
apps/staking/src/components/TransferButton/index.tsx

@@ -1,5 +1,3 @@
-import { type WalletContextState } from "@solana/wallet-adapter-react";
-import type { Connection } from "@solana/web3.js";
 import {
   type ChangeEvent,
   type ComponentProps,
@@ -9,11 +7,12 @@ import {
   useState,
 } from "react";
 
+import { useLogger } from "../../logger";
 import { stringToTokens } from "../../tokens";
+import { type Context } from "../../use-api-context";
 import { StateType, useTransfer } from "../../use-transfer";
 import { Button } from "../Button";
-import type { DashboardLoaded } from "../Dashboard/loaded";
-import { Modal } from "../Modal";
+import { ModalButton } from "../ModalButton";
 
 type Props = {
   actionName: string;
@@ -21,13 +20,12 @@ type Props = {
   title?: string | undefined;
   submitButtonText?: string | undefined;
   max: bigint;
-  replaceData: ComponentProps<typeof DashboardLoaded>["replaceData"];
-  children?: ReactNode | ReactNode[] | undefined;
-  transfer: (
-    connection: Connection,
-    wallet: WalletContextState,
-    amount: bigint,
-  ) => Promise<void>;
+  children?:
+    | ((amount: bigint | undefined) => ReactNode | ReactNode[])
+    | ReactNode
+    | ReactNode[]
+    | undefined;
+  transfer: (context: Context, amount: bigint) => Promise<void>;
 };
 
 export const TransferButton = ({
@@ -36,88 +34,99 @@ export const TransferButton = ({
   actionDescription,
   title,
   max,
-  replaceData,
   transfer,
   children,
 }: Props) => {
-  const [isOpen, setIsOpen] = useState(false);
-  const [amountInput, setAmountInput] = useState<string>("");
-
-  const updateAmount = useCallback(
-    (event: ChangeEvent<HTMLInputElement>) => {
-      setAmountInput(event.target.value);
-    },
-    [setAmountInput],
-  );
-
-  const amount = useMemo(() => {
-    const amount = stringToTokens(amountInput);
-    return amount !== undefined && amount <= max && amount > 0n
-      ? amount
-      : undefined;
-  }, [amountInput, max]);
-
+  const { amountInput, updateAmount, resetAmount, amount } =
+    useAmountInput(max);
   const doTransfer = useCallback(
-    (connection: Connection, wallet: WalletContextState) =>
+    (context: Context) =>
       amount === undefined
         ? Promise.reject(new InvalidAmountError())
-        : transfer(connection, wallet, amount),
+        : transfer(context, amount),
     [amount, transfer],
   );
 
-  const close = useCallback(() => {
-    setAmountInput("");
-    setIsOpen(false);
-  }, [setAmountInput, setIsOpen]);
-
-  const { state, execute } = useTransfer(doTransfer, replaceData, (reset) => {
-    close();
-    reset();
-  });
+  const { state, execute } = useTransfer(doTransfer);
+  const isSubmitting = state.type === StateType.Submitting;
 
-  const isLoading = useMemo(
-    () =>
-      state.type === StateType.Submitting ||
-      state.type === StateType.LoadingData,
-    [state],
+  return (
+    <ModalButton
+      title={title ?? actionName}
+      buttonContent={actionName}
+      closeDisabled={isSubmitting}
+      description={actionDescription}
+      afterLeave={resetAmount}
+      additionalButtons={(close) => (
+        <ExecuteButton
+          disabled={amount === undefined}
+          execute={execute}
+          loading={isSubmitting}
+          close={close}
+        >
+          {submitButtonText ?? actionName}
+        </ExecuteButton>
+      )}
+    >
+      <input name="amount" value={amountInput} onChange={updateAmount} />
+      {children && (
+        <div>
+          {typeof children === "function" ? children(amount) : children}
+        </div>
+      )}
+      {state.type === StateType.Error && <p>Uh oh, an error occurred!</p>}
+    </ModalButton>
   );
+};
 
-  const open = useCallback(() => {
-    setIsOpen(true);
-  }, [setIsOpen]);
+const useAmountInput = (max: bigint) => {
+  const [amountInput, setAmountInput] = useState<string>("");
 
-  const closeUnlessLoading = useCallback(() => {
-    if (!isLoading) {
-      close();
-    }
-  }, [isLoading, close]);
+  return {
+    amountInput,
 
-  return (
-    <>
-      <Button onClick={open}>{actionName}</Button>
-      <Modal
-        open={isOpen}
-        onClose={closeUnlessLoading}
-        closeDisabled={isLoading}
-        title={title ?? actionName}
-        description={actionDescription}
-        additionalButtons={
-          <Button
-            disabled={amount === undefined}
-            onClick={execute}
-            loading={isLoading}
-          >
-            {submitButtonText ?? actionName}
-          </Button>
-        }
-      >
-        <input name="amount" value={amountInput} onChange={updateAmount} />
-        {children && <div>{children}</div>}
-      </Modal>
-    </>
-  );
+    updateAmount: useCallback(
+      (event: ChangeEvent<HTMLInputElement>) => {
+        setAmountInput(event.target.value);
+      },
+      [setAmountInput],
+    ),
+    resetAmount: useCallback(() => {
+      setAmountInput("");
+    }, [setAmountInput]),
+
+    amount: useMemo(() => {
+      const amountAsTokens = stringToTokens(amountInput);
+      return amountAsTokens !== undefined &&
+        amountAsTokens <= max &&
+        amountAsTokens > 0n
+        ? amountAsTokens
+        : undefined;
+    }, [amountInput, max]),
+  };
 };
 
 class InvalidAmountError extends Error {
-  override message = "Invalid amount";
+  constructor() {
+    super("Invalid amount");
+  }
 }
+
+type ExecuteButtonProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
+  execute: () => Promise<void>;
+  close: () => void;
+};
+
+const ExecuteButton = ({ execute, close, ...props }: ExecuteButtonProps) => {
+  const logger = useLogger();
+  const handleClick = useCallback(async () => {
+    try {
+      await execute();
+      close();
+    } catch (error: unknown) {
+      logger.error(error);
+    }
+  }, [execute, close, logger]);
+
+  return <Button onClick={handleClick} {...props} />;
+};

+ 30 - 0
apps/staking/src/use-api-context.ts

@@ -0,0 +1,30 @@
+import { useWallet, useConnection } from "@solana/wallet-adapter-react";
+import { useMemo } from "react";
+
+import type { Context } from "./api";
+import { StateType, useStakeAccount } from "./use-stake-account";
+
+export type { Context } from "./api";
+
+export const useApiContext = (): Context => {
+  const wallet = useWallet();
+  const { connection } = useConnection();
+  const stakeAccount = useStakeAccount();
+
+  if (stakeAccount.type !== StateType.Loaded) {
+    throw new NoStakeAccountSelectedError();
+  }
+
+  return useMemo(
+    () => ({ wallet, connection, stakeAccount: stakeAccount.account }),
+    [wallet, connection, stakeAccount],
+  );
+};
+
+class NoStakeAccountSelectedError extends Error {
+  constructor() {
+    super(
+      "The `useApiContext` hook cannot be called before stake accounts have loaded!  Ensure all components that use this hook are only rendered if `useStakeAccount` returns a loaded state!",
+    );
+  }
+}

+ 108 - 0
apps/staking/src/use-stake-account.tsx

@@ -0,0 +1,108 @@
+"use client";
+
+import { useConnection, useWallet } from "@solana/wallet-adapter-react";
+import {
+  createContext,
+  useContext,
+  useCallback,
+  useState,
+  useEffect,
+  type ComponentProps,
+} from "react";
+
+import { type StakeAccount, getStakeAccounts } from "./api";
+
+export enum StateType {
+  Initialized,
+  Loading,
+  NoAccounts,
+  Loaded,
+  Error,
+}
+
+const State = {
+  Initialized: () => ({ type: StateType.Initialized as const }),
+  Loading: () => ({ type: StateType.Loading as const }),
+  NoAccounts: () => ({ type: StateType.NoAccounts as const }),
+  Loaded: (
+    account: StakeAccount,
+    allAccounts: [StakeAccount, ...StakeAccount[]],
+    selectAccount: (account: StakeAccount) => void,
+  ) => ({
+    type: StateType.Loaded as const,
+    account,
+    allAccounts,
+    selectAccount,
+  }),
+  ErrorState: (error: unknown) => ({ type: StateType.Error as const, error }),
+};
+
+type State = ReturnType<(typeof State)[keyof typeof State]>;
+
+const StakeAccountContext = createContext<State | undefined>(undefined);
+
+export const StakeAccountProvider = (
+  props: Omit<ComponentProps<typeof StakeAccountContext.Provider>, "value">,
+) => {
+  const state = useStakeAccountState();
+
+  return <StakeAccountContext.Provider value={state} {...props} />;
+};
+
+const useStakeAccountState = () => {
+  const wallet = useWallet();
+  const { connection } = useConnection();
+  const [state, setState] = useState<State>(State.Initialized());
+
+  const setAccount = useCallback(
+    (account: StakeAccount) => {
+      setState((cur) =>
+        cur.type === StateType.Loaded
+          ? State.Loaded(account, cur.allAccounts, setAccount)
+          : cur,
+      );
+    },
+    [setState],
+  );
+
+  useEffect(() => {
+    setState(State.Loading());
+    getStakeAccounts(connection, wallet)
+      .then((accounts) => {
+        const [firstAccount, ...otherAccounts] = accounts;
+        if (firstAccount) {
+          setState(
+            State.Loaded(
+              firstAccount,
+              [firstAccount, ...otherAccounts],
+              setAccount,
+            ),
+          );
+        } else {
+          setState(State.NoAccounts());
+        }
+      })
+      .catch((error: unknown) => {
+        setState(State.ErrorState(error));
+      });
+  }, [connection, setAccount, wallet]);
+
+  return state;
+};
+
+export const useStakeAccount = () => {
+  const state = useContext(StakeAccountContext);
+  if (state === undefined) {
+    throw new NotInitializedError();
+  } else {
+    return state;
+  }
+};
+
+class NotInitializedError extends Error {
+  constructor() {
+    super(
+      "This component must be a child of <StakeAccountProvider> to use the `useStakeAccount` hook",
+    );
+  }
+}

+ 35 - 56
apps/staking/src/use-transfer.ts

@@ -1,53 +1,33 @@
-import {
-  type WalletContextState,
-  useConnection,
-  useWallet,
-} from "@solana/wallet-adapter-react";
-import type { Connection } from "@solana/web3.js";
-import { type ComponentProps, useState, useCallback } from "react";
+import { useState, useCallback } from "react";
+import { useSWRConfig } from "swr";
 
-import { loadData } from "./api";
-import type { DashboardLoaded } from "./components/Dashboard/loaded";
+import { type Context, useApiContext } from "./use-api-context";
 
-export const useTransfer = (
-  transfer: (
-    connection: Connection,
-    wallet: WalletContextState,
-  ) => Promise<void>,
-  replaceData: ComponentProps<typeof DashboardLoaded>["replaceData"],
-  onFinish?: (reset: () => void) => void,
-) => {
-  const wallet = useWallet();
-  const { connection } = useConnection();
+export const useTransfer = (transfer: (context: Context) => Promise<void>) => {
+  const context = useApiContext();
   const [state, setState] = useState<State>(State.Base());
+  const { mutate } = useSWRConfig();
 
-  const reset = useCallback(() => {
-    setState(State.Base());
-  }, [setState]);
+  const execute = useCallback(async () => {
+    if (state.type === StateType.Submitting) {
+      throw new DuplicateSubmitError();
+    }
 
-  const execute = useCallback(() => {
     setState(State.Submitting());
-    transfer(connection, wallet)
-      .then(() => {
-        setState(State.LoadingData());
-        loadData(connection, wallet)
-          .then((data) => {
-            replaceData(data);
-            if (onFinish) {
-              setState(State.Finished());
-              onFinish(reset);
-            } else {
-              setState(State.Base());
-            }
-          })
-          .catch((error: unknown) => {
-            setState(State.ErrorLoadingData(error));
-          });
-      })
-      .catch((error: unknown) => {
-        setState(State.ErrorSubmitting(error));
-      });
-  }, [connection, wallet, transfer, replaceData, onFinish, setState, reset]);
+    try {
+      await transfer(context);
+      // TODO enable mutate without awaiting?
+      // Prob by changing `api.ts` to encode the change & history item along with each update?
+      await Promise.all([
+        mutate(context.stakeAccount.publicKey),
+        mutate(`${context.stakeAccount.publicKey}/history`),
+      ]);
+      setState(State.Complete());
+    } catch (error: unknown) {
+      setState(State.ErrorState(error));
+      throw error;
+    }
+  }, [state, context, transfer, setState, mutate]);
 
   return { state, execute };
 };
@@ -55,25 +35,24 @@ export const useTransfer = (
 export enum StateType {
   Base,
   Submitting,
-  LoadingData,
-  ErrorSubmitting,
-  ErrorLoadingData,
-  Finished,
+  Error,
+  Complete,
 }
 
 const State = {
   Base: () => ({ type: StateType.Base as const }),
   Submitting: () => ({ type: StateType.Submitting as const }),
-  LoadingData: () => ({ type: StateType.LoadingData as const }),
-  ErrorSubmitting: (error: unknown) => ({
-    type: StateType.ErrorSubmitting as const,
+  Complete: () => ({ type: StateType.Complete as const }),
+  ErrorState: (error: unknown) => ({
+    type: StateType.Error as const,
     error,
   }),
-  ErrorLoadingData: (error: unknown) => ({
-    type: StateType.ErrorLoadingData as const,
-    error,
-  }),
-  Finished: () => ({ type: StateType.Finished as const }),
 };
 
 type State = ReturnType<(typeof State)[keyof typeof State]>;
+
+class DuplicateSubmitError extends Error {
+  constructor() {
+    super("Attempted to submit a transaction when one is already in process");
+  }
+}

+ 317 - 60
pnpm-lock.yaml

@@ -111,7 +111,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.2))(eslint@9.5.0)(typescript@5.5.2))(@typescript-eslint/parser@7.13.1(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)))(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))(typescript@5.5.2)
+        version: 3.0.0(@typescript-eslint/eslint-plugin@7.13.1(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)))(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))(typescript@5.5.2)
       '@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@20.14.7)(babel-jest@29.7.0(@babel/core@7.24.7))(bufferutil@4.0.8)(eslint@9.5.0)(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))(utf-8-validate@5.0.10)
@@ -341,13 +341,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@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)
       '@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@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)
+        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)
       '@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@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)
+        version: 0.19.10(@babel/runtime@7.24.8)(@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/web3.js':
         specifier: 1.92.3
         version: 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -366,6 +366,12 @@ importers:
       react-dom:
         specifier: ^18.3.1
         version: 18.3.1(react@18.3.1)
+      recharts:
+        specifier: ^2.12.7
+        version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      swr:
+        specifier: ^2.2.5
+        version: 2.2.5(react@18.3.1)
       zod:
         specifier: ^3.23.8
         version: 3.23.8
@@ -375,7 +381,7 @@ importers:
         version: 4.9.1
       '@cprussin/eslint-config':
         specifier: ^3.0.0
-        version: 3.0.0(@typescript-eslint/eslint-plugin@7.13.1(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)
+        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)
       '@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)
@@ -898,13 +904,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.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)
+        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.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':
         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.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)
+        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.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-wallets':
         specifier: 0.19.10
-        version: 0.19.10(@babel/runtime@7.24.8)(@react-native-async-storage/async-storage@1.23.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)))(@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.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)))(@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)
@@ -7567,6 +7573,33 @@ packages:
   '@types/cors@2.8.17':
     resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
 
+  '@types/d3-array@3.2.1':
+    resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
+
+  '@types/d3-color@3.1.3':
+    resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+  '@types/d3-ease@3.0.2':
+    resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+  '@types/d3-interpolate@3.0.4':
+    resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+  '@types/d3-path@3.1.0':
+    resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
+
+  '@types/d3-scale@4.0.8':
+    resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+
+  '@types/d3-shape@3.1.6':
+    resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
+
+  '@types/d3-time@3.0.3':
+    resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
+
+  '@types/d3-timer@3.0.2':
+    resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
   '@types/debug@4.1.12':
     resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
 
@@ -10409,6 +10442,50 @@ packages:
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  d3-array@3.2.4:
+    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+    engines: {node: '>=12'}
+
+  d3-color@3.1.0:
+    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+    engines: {node: '>=12'}
+
+  d3-ease@3.0.1:
+    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+    engines: {node: '>=12'}
+
+  d3-format@3.1.0:
+    resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+    engines: {node: '>=12'}
+
+  d3-interpolate@3.0.1:
+    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+    engines: {node: '>=12'}
+
+  d3-path@3.1.0:
+    resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+    engines: {node: '>=12'}
+
+  d3-scale@4.0.2:
+    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+    engines: {node: '>=12'}
+
+  d3-shape@3.2.0:
+    resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+    engines: {node: '>=12'}
+
+  d3-time-format@4.1.0:
+    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+    engines: {node: '>=12'}
+
+  d3-time@3.1.0:
+    resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+    engines: {node: '>=12'}
+
+  d3-timer@3.0.1:
+    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+    engines: {node: '>=12'}
+
   d@1.0.1:
     resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==}
 
@@ -10567,6 +10644,9 @@ packages:
     resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==}
     engines: {node: '>=10'}
 
+  decimal.js-light@2.5.1:
+    resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
   decimal.js@10.4.3:
     resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
 
@@ -10822,6 +10902,9 @@ packages:
   dom-accessibility-api@0.6.3:
     resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
 
+  dom-helpers@5.2.1:
+    resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
   dom-serializer@1.4.1:
     resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
 
@@ -11840,6 +11923,10 @@ packages:
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
+  fast-equals@5.0.1:
+    resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
+    engines: {node: '>=6.0.0'}
+
   fast-glob@3.2.7:
     resolution: {integrity: sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==}
     engines: {node: '>=8'}
@@ -12835,6 +12922,10 @@ packages:
     resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
     engines: {node: '>= 0.4'}
 
+  internmap@2.0.3:
+    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+    engines: {node: '>=12'}
+
   interpret@1.4.0:
     resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
     engines: {node: '>= 0.10'}
@@ -16366,6 +16457,18 @@ packages:
     peerDependencies:
       react: ^16.0.0 || ^17.0.0 || ^18.0.0
 
+  react-smooth@4.0.1:
+    resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+  react-transition-group@4.4.5:
+    resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+    peerDependencies:
+      react: '>=16.6.0'
+      react-dom: '>=16.6.0'
+
   react-transition-state@1.1.5:
     resolution: {integrity: sha512-ITY2mZqc2dWG2eitJkYNdcSFW8aKeOlkL2A/vowRrLL8GH3J6Re/SpD/BLvQzrVOTqjsP0b5S9N10vgNNzwMUQ==}
     peerDependencies:
@@ -16488,6 +16591,16 @@ packages:
     resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==}
     engines: {node: '>= 4'}
 
+  recharts-scale@0.4.5:
+    resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+  recharts@2.12.7:
+    resolution: {integrity: sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      react: ^16.0.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+
   rechoir@0.6.2:
     resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
     engines: {node: '>= 0.10'}
@@ -17513,6 +17626,11 @@ packages:
   swarm-js@0.1.42:
     resolution: {integrity: sha512-BV7c/dVlA3R6ya1lMlSSNPLYrntt0LUq4YMgy3iwpCIc6rZnS5W2wUoctarZ5pXlpKtxDDf9hNziEkcfrxdhqQ==}
 
+  swr@2.2.5:
+    resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
+    peerDependencies:
+      react: ^16.11.0 || ^17.0.0 || ^18.0.0
+
   symbol-observable@1.2.0:
     resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
     engines: {node: '>=0.10.0'}
@@ -17711,6 +17829,9 @@ packages:
   tiny-inflate@1.0.3:
     resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
 
+  tiny-invariant@1.3.3:
+    resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
   tiny-secp256k1@1.1.6:
     resolution: {integrity: sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==}
     engines: {node: '>=6.0.0'}
@@ -18566,6 +18687,9 @@ packages:
   vfile@6.0.1:
     resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
 
+  victory-vendor@36.9.2:
+    resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
   viem@1.21.4:
     resolution: {integrity: sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ==}
     peerDependencies:
@@ -23469,7 +23593,7 @@ 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.2))(eslint@9.5.0)(typescript@5.5.2))(@typescript-eslint/parser@7.13.1(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)))(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))(typescript@5.5.2)':
+  '@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)
@@ -23481,22 +23605,22 @@ 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.2))(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.2))(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-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.2)
-      eslint-plugin-tailwindcss: 3.17.3(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2)))
-      eslint-plugin-testing-library: 6.2.2(eslint@9.5.0)(typescript@5.5.2)
+      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@20.14.7)(typescript@5.5.2))
-      typescript-eslint: 7.13.1(eslint@9.5.0)(typescript@5.5.2)
+      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'
@@ -23508,7 +23632,7 @@ snapshots:
       - ts-node
       - typescript
 
-  '@cprussin/eslint-config@3.0.0(@typescript-eslint/eslint-plugin@7.13.1(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/eslint-config@3.0.0(@typescript-eslint/eslint-plugin@7.13.1(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)))(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))(typescript@5.5.2)':
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@9.5.0)
@@ -23520,22 +23644,22 @@ 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.2))(eslint@9.5.0)
-      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-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(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)
       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-storybook: 0.8.0(eslint@9.5.0)(typescript@5.5.2)
+      eslint-plugin-tailwindcss: 3.17.3(tailwindcss@3.4.4(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2)))
+      eslint-plugin-testing-library: 6.2.2(eslint@9.5.0)(typescript@5.5.2)
       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)
+      tailwindcss: 3.4.4(ts-node@10.9.2(@types/node@20.14.7)(typescript@5.5.2))
+      typescript-eslint: 7.13.1(eslint@9.5.0)(typescript@5.5.2)
     transitivePeerDependencies:
       - '@testing-library/dom'
       - '@typescript-eslint/eslint-plugin'
@@ -29499,18 +29623,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.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@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)':
     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.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@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/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.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@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.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:
@@ -29685,11 +29809,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.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@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)':
     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.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@5.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@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/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)
@@ -29697,11 +29821,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.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@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/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)
@@ -29709,9 +29833,9 @@ snapshots:
       - bs58
       - react-native
 
-  '@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.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@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)':
     dependencies:
-      '@solana-mobile/wallet-adapter-mobile': 2.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(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-mobile/wallet-adapter-mobile': 2.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(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': 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-standard-wallet-adapter-react': 1.1.2(@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))(bs58@5.0.0)(react@18.3.1)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -29720,9 +29844,9 @@ snapshots:
       - bs58
       - react-native
 
-  '@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/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)':
     dependencies:
-      '@solana-mobile/wallet-adapter-mobile': 2.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(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-mobile/wallet-adapter-mobile': 2.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(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': 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-standard-wallet-adapter-react': 1.1.2(@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))(bs58@6.0.0)(react@18.3.1)
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -29898,7 +30022,7 @@ snapshots:
       - uWebSockets.js
       - utf-8-validate
 
-  '@solana/wallet-adapter-wallets@0.19.10(@babel/runtime@7.24.8)(@react-native-async-storage/async-storage@1.23.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)))(@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.24.8)(@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)':
     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))
@@ -29943,7 +30067,7 @@ snapshots:
       '@solana/wallet-adapter-torus': 0.11.28(@babel/runtime@7.24.8)(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       '@solana/wallet-adapter-trust': 0.1.13(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-unsafe-burner': 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-walletconnect': 0.1.16(@react-native-async-storage/async-storage@1.23.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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
+      '@solana/wallet-adapter-walletconnect': 0.1.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       '@solana/wallet-adapter-xdefi': 0.1.7(@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:
@@ -29972,7 +30096,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@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/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.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)))(@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))
@@ -30017,7 +30141,7 @@ snapshots:
       '@solana/wallet-adapter-torus': 0.11.28(@babel/runtime@7.25.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)
       '@solana/wallet-adapter-trust': 0.1.13(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))
       '@solana/wallet-adapter-unsafe-burner': 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-walletconnect': 0.1.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
+      '@solana/wallet-adapter-walletconnect': 0.1.16(@react-native-async-storage/async-storage@1.23.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)))(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       '@solana/wallet-adapter-xdefi': 0.1.7(@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:
@@ -31591,6 +31715,30 @@ snapshots:
     dependencies:
       '@types/node': 20.14.15
 
+  '@types/d3-array@3.2.1': {}
+
+  '@types/d3-color@3.1.3': {}
+
+  '@types/d3-ease@3.0.2': {}
+
+  '@types/d3-interpolate@3.0.4':
+    dependencies:
+      '@types/d3-color': 3.1.3
+
+  '@types/d3-path@3.1.0': {}
+
+  '@types/d3-scale@4.0.8':
+    dependencies:
+      '@types/d3-time': 3.0.3
+
+  '@types/d3-shape@3.1.6':
+    dependencies:
+      '@types/d3-path': 3.1.0
+
+  '@types/d3-time@3.0.3': {}
+
+  '@types/d3-timer@3.0.2': {}
+
   '@types/debug@4.1.12':
     dependencies:
       '@types/ms': 0.7.34
@@ -32035,7 +32183,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.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/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)
@@ -32050,9 +32198,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/eslint-plugin@7.13.1(eslint@9.9.0(jiti@1.21.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.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)
@@ -32197,14 +32346,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@7.13.1(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)':
     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.5.0
+      eslint: 9.9.0(jiti@1.21.0)
     optionalDependencies:
       typescript: 5.5.4
     transitivePeerDependencies:
@@ -36443,6 +36592,44 @@ snapshots:
 
   csstype@3.1.3: {}
 
+  d3-array@3.2.4:
+    dependencies:
+      internmap: 2.0.3
+
+  d3-color@3.1.0: {}
+
+  d3-ease@3.0.1: {}
+
+  d3-format@3.1.0: {}
+
+  d3-interpolate@3.0.1:
+    dependencies:
+      d3-color: 3.1.0
+
+  d3-path@3.1.0: {}
+
+  d3-scale@4.0.2:
+    dependencies:
+      d3-array: 3.2.4
+      d3-format: 3.1.0
+      d3-interpolate: 3.0.1
+      d3-time: 3.1.0
+      d3-time-format: 4.1.0
+
+  d3-shape@3.2.0:
+    dependencies:
+      d3-path: 3.1.0
+
+  d3-time-format@4.1.0:
+    dependencies:
+      d3-time: 3.1.0
+
+  d3-time@3.1.0:
+    dependencies:
+      d3-array: 3.2.4
+
+  d3-timer@3.0.1: {}
+
   d@1.0.1:
     dependencies:
       es5-ext: 0.10.62
@@ -36573,6 +36760,8 @@ snapshots:
 
   decamelize@4.0.0: {}
 
+  decimal.js-light@2.5.1: {}
+
   decimal.js@10.4.3: {}
 
   decode-named-character-reference@1.0.2:
@@ -36803,6 +36992,11 @@ snapshots:
 
   dom-accessibility-api@0.6.3: {}
 
+  dom-helpers@5.2.1:
+    dependencies:
+      '@babel/runtime': 7.25.0
+      csstype: 3.1.3
+
   dom-serializer@1.4.1:
     dependencies:
       domelementtype: 2.3.0
@@ -37443,11 +37637,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.8.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9)(eslint@9.5.0):
+  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.5.0)(typescript@5.5.2)
+      '@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:
@@ -37487,7 +37681,7 @@ snapshots:
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.13.1(eslint@9.5.0)(typescript@5.5.2))(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
@@ -37497,7 +37691,7 @@ snapshots:
       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.2))(eslint-import-resolver-node@0.3.9)(eslint@9.5.0)
+      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
@@ -37508,7 +37702,7 @@ snapshots:
       semver: 6.3.1
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 7.13.1(eslint@9.5.0)(typescript@5.5.2)
+      '@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
@@ -37520,24 +37714,24 @@ 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.2))(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@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):
     dependencies:
-      '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.2)
+      '@typescript-eslint/utils': 7.7.1(eslint@9.5.0)(typescript@5.5.4)
       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))
+      '@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))
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  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@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(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
@@ -38722,6 +38916,8 @@ snapshots:
 
   fast-deep-equal@3.1.3: {}
 
+  fast-equals@5.0.1: {}
+
   fast-glob@3.2.7:
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -39983,6 +40179,8 @@ snapshots:
       hasown: 2.0.2
       side-channel: 1.0.6
 
+  internmap@2.0.3: {}
+
   interpret@1.4.0: {}
 
   invariant@2.2.4:
@@ -45946,6 +46144,23 @@ snapshots:
       react: 18.3.1
       react-is: 18.3.1
 
+  react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+    dependencies:
+      fast-equals: 5.0.1
+      prop-types: 15.8.1
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+      react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
+  react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+    dependencies:
+      '@babel/runtime': 7.25.0
+      dom-helpers: 5.2.1
+      loose-envify: 1.4.0
+      prop-types: 15.8.1
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
     dependencies:
       react: 18.3.1
@@ -46107,6 +46322,23 @@ snapshots:
       source-map: 0.6.1
       tslib: 2.6.3
 
+  recharts-scale@0.4.5:
+    dependencies:
+      decimal.js-light: 2.5.1
+
+  recharts@2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+    dependencies:
+      clsx: 2.1.1
+      eventemitter3: 4.0.7
+      lodash: 4.17.21
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+      react-is: 16.13.1
+      react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      recharts-scale: 0.4.5
+      tiny-invariant: 1.3.3
+      victory-vendor: 36.9.2
+
   rechoir@0.6.2:
     dependencies:
       resolve: 1.22.8
@@ -47400,6 +47632,12 @@ snapshots:
       - supports-color
       - utf-8-validate
 
+  swr@2.2.5(react@18.3.1):
+    dependencies:
+      client-only: 0.0.1
+      react: 18.3.1
+      use-sync-external-store: 1.2.0(react@18.3.1)
+
   symbol-observable@1.2.0: {}
 
   symbol-observable@2.0.3: {}
@@ -47709,6 +47947,8 @@ snapshots:
 
   tiny-inflate@1.0.3: {}
 
+  tiny-invariant@1.3.3: {}
+
   tiny-secp256k1@1.1.6:
     dependencies:
       bindings: 1.5.0
@@ -48590,7 +48830,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.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/utils': 7.13.1(eslint@9.5.0)(typescript@5.5.4)
       eslint: 9.5.0
     optionalDependencies:
@@ -49002,6 +49242,23 @@ snapshots:
       unist-util-stringify-position: 4.0.0
       vfile-message: 4.0.2
 
+  victory-vendor@36.9.2:
+    dependencies:
+      '@types/d3-array': 3.2.1
+      '@types/d3-ease': 3.0.2
+      '@types/d3-interpolate': 3.0.4
+      '@types/d3-scale': 4.0.8
+      '@types/d3-shape': 3.1.6
+      '@types/d3-time': 3.0.3
+      '@types/d3-timer': 3.0.2
+      d3-array: 3.2.4
+      d3-ease: 3.0.1
+      d3-interpolate: 3.0.1
+      d3-scale: 4.0.2
+      d3-shape: 3.2.0
+      d3-time: 3.1.0
+      d3-timer: 3.0.1
+
   viem@1.21.4(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8):
     dependencies:
       '@adraffy/ens-normalize': 1.10.0