| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- import clsx from "clsx";
- import { useMemo, useCallback } from "react";
- import {
- type Context,
- delegateIntegrityStaking,
- cancelWarmupIntegrityStaking,
- unstakeIntegrityStaking,
- calculateApy,
- } from "../../api";
- import { ProgramSection } from "../ProgramSection";
- import { SparkChart } from "../SparkChart";
- import { StakingTimeline } from "../StakingTimeline";
- import { Styled } from "../Styled";
- import { Tokens } from "../Tokens";
- import { AmountType, TransferButton } from "../TransferButton";
- type Props = {
- availableToStake: bigint;
- locked: bigint;
- warmup: bigint;
- staked: bigint;
- cooldown: bigint;
- cooldown2: bigint;
- publishers: PublisherProps["publisher"][];
- };
- export const OracleIntegrityStaking = ({
- availableToStake,
- locked,
- warmup,
- staked,
- cooldown,
- cooldown2,
- publishers,
- }: Props) => {
- const self = useMemo(
- () => publishers.find((publisher) => publisher.isSelf),
- [publishers],
- );
- const otherPublishers = useMemo(
- () => publishers.filter((publisher) => !publisher.isSelf),
- [publishers],
- );
- return (
- <ProgramSection
- name="Oracle Integrity Staking (OIS)"
- description="Protect DeFi"
- className="pb-0 sm:pb-0"
- available={availableToStake}
- warmup={warmup}
- staked={staked}
- cooldown={cooldown}
- cooldown2={cooldown2}
- {...(locked > 0n && {
- availableToStakeDetails: (
- <div className="mt-2 text-xs text-red-600">
- <Tokens>{locked}</Tokens> are locked and cannot be staked in OIS
- </div>
- ),
- })}
- >
- {self && (
- <div className="relative -mx-4 mt-6 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 sm:mt-10">
- <div className="relative w-full overflow-x-auto">
- <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
- You ({self.name})
- </h3>
- <table className="mx-auto border border-neutral-600/50 text-sm">
- <thead className="bg-pythpurple-400/30 font-light">
- <tr>
- <PublisherTableHeader>Pool</PublisherTableHeader>
- <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
- <PublisherTableHeader>Historical APY</PublisherTableHeader>
- <PublisherTableHeader>Number of feeds</PublisherTableHeader>
- <PublisherTableHeader>Quality ranking</PublisherTableHeader>
- {availableToStake > 0n && <PublisherTableHeader />}
- </tr>
- </thead>
- <tbody className="bg-pythpurple-400/10">
- <Publisher
- isSelf
- availableToStake={availableToStake}
- publisher={self}
- totalStaked={staked}
- />
- </tbody>
- </table>
- </div>
- </div>
- )}
- <div
- className={clsx(
- "relative -mx-4 overflow-hidden border-t border-neutral-600/50 pt-6 sm:-mx-10 lg:mt-10",
- { "mt-6": self === undefined },
- )}
- >
- <div className="relative w-full overflow-x-auto">
- <h3 className="sticky left-0 mb-4 pl-4 text-2xl font-light sm:pb-4 sm:pl-10 sm:pt-6">
- {self ? "Other Publishers" : "Publishers"}
- </h3>
- <table className="min-w-full text-sm">
- <thead className="bg-pythpurple-100/30 font-light">
- <tr>
- <PublisherTableHeader className="pl-4 text-left sm:pl-10">
- Publisher
- </PublisherTableHeader>
- <PublisherTableHeader>Self stake</PublisherTableHeader>
- <PublisherTableHeader>Pool</PublisherTableHeader>
- <PublisherTableHeader>Last epoch APY</PublisherTableHeader>
- <PublisherTableHeader>Historical APY</PublisherTableHeader>
- <PublisherTableHeader>Number of feeds</PublisherTableHeader>
- <PublisherTableHeader
- className={clsx({ "pr-4 sm:pr-10": availableToStake <= 0n })}
- >
- Quality ranking
- </PublisherTableHeader>
- {availableToStake > 0n && (
- <PublisherTableHeader className="pr-4 sm:pr-10" />
- )}
- </tr>
- </thead>
- <tbody className="bg-white/5">
- {otherPublishers.map((publisher) => (
- <Publisher
- key={publisher.publicKey}
- availableToStake={availableToStake}
- publisher={publisher}
- totalStaked={staked}
- />
- ))}
- </tbody>
- </table>
- </div>
- </div>
- </ProgramSection>
- );
- };
- const PublisherTableHeader = Styled(
- "th",
- "py-2 font-normal px-5 whitespace-nowrap",
- );
- type PublisherProps = {
- availableToStake: bigint;
- totalStaked: bigint;
- isSelf?: 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 = ({
- publisher,
- availableToStake,
- totalStaked,
- isSelf,
- }: PublisherProps) => {
- const warmup = useMemo(
- () =>
- publisher.positions?.warmup !== undefined &&
- publisher.positions.warmup > 0n
- ? publisher.positions.warmup
- : undefined,
- [publisher.positions?.warmup],
- );
- const staked = useMemo(
- () =>
- publisher.positions?.staked !== undefined &&
- publisher.positions.staked > 0n
- ? publisher.positions.staked
- : undefined,
- [publisher.positions?.staked],
- );
- const cancelWarmup = useTransferActionForPublisher(
- cancelWarmupIntegrityStaking,
- publisher.publicKey,
- );
- const unstake = useTransferActionForPublisher(
- unstakeIntegrityStaking,
- publisher.publicKey,
- );
- const utilizationPercent = useMemo(
- () => Number((100n * publisher.poolUtilization) / publisher.poolCapacity),
- [publisher.poolUtilization, publisher.poolCapacity],
- );
- return (
- <>
- <tr className="border-t border-neutral-600/50 first:border-0">
- {!isSelf && (
- <>
- <PublisherTableCell className="py-4 pl-4 font-medium sm:pl-10">
- {publisher.name}
- </PublisherTableCell>
- <PublisherTableCell className="text-center">
- <Tokens>{publisher.selfStake}</Tokens>
- </PublisherTableCell>
- </>
- )}
- <PublisherTableCell className="text-center">
- <div className="relative mx-auto grid h-5 w-52 place-content-center border border-black bg-pythpurple-600/50">
- <div
- style={{
- width: `${utilizationPercent.toString()}%`,
- }}
- className={clsx(
- "absolute inset-0 max-w-full",
- publisher.poolUtilization > publisher.poolCapacity
- ? "bg-fuchsia-900"
- : "bg-pythpurple-400",
- )}
- />
- <div
- className={clsx("isolate text-sm font-medium", {
- "mix-blend-difference":
- publisher.poolUtilization <= publisher.poolCapacity,
- })}
- >
- {utilizationPercent.toString()}%
- </div>
- </div>
- <div className="mt-2 flex flex-row items-center justify-center gap-1 text-sm">
- <span>
- <Tokens>{publisher.poolUtilization}</Tokens>
- </span>
- <span>/</span>
- <span>
- <Tokens>{publisher.poolCapacity}</Tokens>
- </span>
- </div>
- </PublisherTableCell>
- <PublisherTableCell className="text-center">
- <div>
- {calculateApy(
- publisher.poolCapacity,
- publisher.poolUtilization,
- publisher.isSelf,
- )}
- %
- </div>
- </PublisherTableCell>
- <PublisherTableCell>
- <div className="mx-auto h-14 w-28">
- <SparkChart
- data={publisher.apyHistory.map(({ date, apy }) => ({
- date,
- value: apy,
- }))}
- />
- </div>
- </PublisherTableCell>
- <PublisherTableCell className="text-center">
- {publisher.numFeeds}
- </PublisherTableCell>
- <PublisherTableCell
- className={clsx("text-center", {
- "pr-4 sm:pr-10": availableToStake <= 0n && !isSelf,
- })}
- >
- {publisher.qualityRanking}
- </PublisherTableCell>
- {availableToStake > 0 && (
- <PublisherTableCell
- className={clsx("text-right", { "pr-4 sm:pr-10": !isSelf })}
- >
- <StakeToPublisherButton
- availableToStake={availableToStake}
- poolCapacity={publisher.poolCapacity}
- poolUtilization={publisher.poolUtilization}
- publisherKey={publisher.publicKey}
- publisherName={publisher.name}
- isSelf={publisher.isSelf}
- />
- </PublisherTableCell>
- )}
- </tr>
- {(warmup !== undefined || staked !== undefined) && (
- <tr>
- <td colSpan={8} className="border-separate border-spacing-8">
- <div className="mx-auto mb-8 mt-4 w-[30rem] border border-neutral-600/50 bg-pythpurple-800 px-8 py-6">
- <table className="w-full">
- <caption className="mb-2 text-left text-lg font-light">
- Your Positions
- </caption>
- <tbody>
- {warmup !== undefined && (
- <tr>
- <td className="opacity-80">Warmup</td>
- <td className="px-4">
- <Tokens>{warmup}</Tokens>
- </td>
- <td
- className={clsx("text-right", {
- "pb-2": staked !== undefined,
- })}
- >
- <TransferButton
- small
- secondary
- className="w-28"
- actionDescription={`Cancel tokens that are in warmup for staking to ${publisher.name}`}
- actionName="Cancel"
- submitButtonText="Cancel Warmup"
- title="Cancel Warmup"
- max={warmup}
- transfer={cancelWarmup}
- />
- </td>
- </tr>
- )}
- {staked !== undefined && (
- <tr>
- <td className="opacity-80">Staked</td>
- <td className="px-4">
- <div className="flex items-center gap-2">
- <Tokens>{staked}</Tokens>
- <div className="text-xs opacity-60">
- ({Number((100n * staked) / totalStaked)}% of your
- staked tokens)
- </div>
- </div>
- </td>
- <td className="py-0.5 text-right">
- <TransferButton
- small
- secondary
- className="w-28"
- actionDescription={`Unstake tokens from ${publisher.name}`}
- actionName="Unstake"
- max={staked}
- transfer={unstake}
- >
- <StakingTimeline cooldownOnly />
- </TransferButton>
- </td>
- </tr>
- )}
- </tbody>
- </table>
- </div>
- </td>
- </tr>
- )}
- </>
- );
- };
- const PublisherTableCell = Styled("td", "py-4 px-5 whitespace-nowrap");
- 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
- small
- actionDescription={`Stake to ${publisherName}`}
- actionName="Stake"
- max={availableToStake}
- transfer={delegate}
- >
- {(amount) => (
- <>
- <div className="mb-8 flex flex-row items-center justify-between text-sm">
- <div>APY after staking</div>
- <div className="font-medium">
- {calculateApy(
- poolCapacity,
- poolUtilization +
- (amount.type === AmountType.Valid ? amount.amount : 0n),
- isSelf,
- )}
- %
- </div>
- </div>
- <StakingTimeline />
- </>
- )}
- </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],
- );
|