import { ChevronUpIcon, XMarkIcon, MagnifyingGlassIcon, Bars3Icon, ChevronDownIcon, ChevronDoubleLeftIcon, ChevronDoubleRightIcon, } from "@heroicons/react/24/outline"; import { calculateApy } from "@pythnetwork/staking-sdk"; import { PublicKey } from "@solana/web3.js"; import clsx from "clsx"; import { useMemo, useCallback, useState, useRef, createElement, type ComponentProps, type Dispatch, type SetStateAction, type HTMLAttributes, type FormEvent, type ComponentType, type SVGProps, } from "react"; import { useFilter, useCollator } from "react-aria"; import { SearchField, Input, Button as BaseButton, Meter, Label, TextField, Form, MenuTrigger, } from "react-aria-components"; import { type States, StateType as ApiStateType } from "../../hooks/use-api"; import { StateType as UseAsyncStateType, useAsync, } from "../../hooks/use-async"; import { useToast } from "../../hooks/use-toast"; import { Button, LinkButton } from "../Button"; import { CopyButton } from "../CopyButton"; import { ErrorMessage } from "../ErrorMessage"; import { Menu, MenuItem, Section, Separator } from "../Menu"; import { ModalDialog } from "../ModalDialog"; import { OracleIntegrityStakingGuide } from "../OracleIntegrityStakingGuide"; import { ProgramSection } from "../ProgramSection"; import { PublisherFaq } from "../PublisherFaq"; import { Select } from "../Select"; import { SparkChart } from "../SparkChart"; import { StakingTimeline } from "../StakingTimeline"; import { Styled } from "../Styled"; import { Switch } from "../Switch"; import { Tokens } from "../Tokens"; import { AmountType, TransferButton } from "../TransferButton"; import { TruncatedKey } from "../TruncatedKey"; type Props = { api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; currentEpoch: bigint; availableToStake: bigint; locked: bigint; warmup: bigint; staked: bigint; cooldown: bigint; cooldown2: bigint; publishers: PublisherProps["publisher"][]; yieldRate: bigint; }; export const OracleIntegrityStaking = ({ api, currentEpoch, availableToStake, locked, warmup, staked, cooldown, cooldown2, publishers, yieldRate, }: Props) => { const self = useMemo( () => api.type === ApiStateType.Loaded ? publishers.find((publisher) => publisher.stakeAccount?.equals(api.account), ) : undefined, [publishers, api], ); const otherPublishers = useMemo( () => self === undefined ? publishers : publishers.filter((publisher) => publisher !== self), [publishers, self], ); return ( } className="pb-0 sm:pb-0" collapseTokenOverview tokenOverview={{ currentEpoch, available: availableToStake, warmup, staked, cooldown, cooldown2, ...(locked > 0n && { availableToStakeDetails: (
{locked} are locked and cannot be staked in OIS
), }), }} > {self && api.type == ApiStateType.Loaded && ( )}
); }; type SelfStakingProps = { api: States[ApiStateType.Loaded]; self: PublisherProps["publisher"]; currentEpoch: bigint; availableToStake: bigint; yieldRate: bigint; }; const SelfStaking = ({ self, api, currentEpoch, availableToStake, yieldRate, }: SelfStakingProps) => { const [publisherFaqOpen, setPublisherFaqOpen] = useState(false); const openPublisherFaq = useCallback(() => { setPublisherFaqOpen(true); }, [setPublisherFaqOpen]); const [reassignStakeAccountOpen, setReassignStakeAccountOpen] = useState(false); const openReassignStakeAccount = useCallback(() => { setReassignStakeAccountOpen(true); }, [setReassignStakeAccountOpen]); const [optOutOpen, setOptOutOpen] = useState(false); const openOptOut = useCallback(() => { setOptOutOpen(true); }, [setOptOutOpen]); return ( <>

Self Staking

{self}
Reassign Stake Account Opt Out of Rewards
Data Publisher FAQ Data Publisher Guide
Publisher Guide
PoolEstimated next APYHistorical APYNumber of feedsQuality ranking
); }; type ReassignStakeAccountButtonProps = Omit< ComponentProps, "title" | "children" | "closeButtonText" | "closeDisabled" | "description" > & { api: States[ApiStateType.Loaded]; self: PublisherProps["publisher"]; }; const ReassignStakeAccount = ({ api, self, ...props }: ReassignStakeAccountButtonProps) => { const [closeDisabled, setCloseDisabled] = useState(false); return hasAnyPositions(self) ? (

You cannot designate another account while self-staked.

Please close all self-staking positions, wait the cooldown period (if applicable), and try again once your self-stake is fully closed.

) : ( Designate a different stake account as the self-staking account for {self} } {...props} > {({ close }) => ( )} ); }; type ReassignStakeAccountFormProps = { api: States[ApiStateType.Loaded]; publisherPubkey: PublicKey; close: () => void; setCloseDisabled: (value: boolean) => void; }; const ReassignStakeAccountForm = ({ api, publisherPubkey, close, setCloseDisabled, }: ReassignStakeAccountFormProps) => { const [value, setValue] = useState(""); const key = useMemo(() => { try { return new PublicKey(value); } catch { return; } }, [value]); const doReassign = useCallback( () => key === undefined ? Promise.reject(new InvalidKeyError()) : api.reassignPublisherAccount(key, publisherPubkey), [api, key, publisherPubkey], ); const { state, execute } = useAsync(doReassign); const toast = useToast(); const handleSubmit = useCallback( (e: FormEvent) => { e.preventDefault(); setCloseDisabled(true); execute() .then(() => { close(); toast.success("You have reassigned your main account"); }) .catch(() => { /* no-op since this is already handled in the UI using `state` and is logged in useAsync */ }) .finally(() => { setCloseDisabled(false); }); }, [execute, close, setCloseDisabled, toast], ); return (
{state.type === UseAsyncStateType.Error && (
)}
); }; type ReassignStakeAccountButtonContentsProps = { value: string; publicKey: PublicKey | undefined; }; const ReassignStakeAccountButtonContents = ({ value, publicKey, }: ReassignStakeAccountButtonContentsProps) => { if (value === "") { return "Enter the new stake account key"; } else if (publicKey === undefined) { return "Please enter a valid public key"; } else { return "Submit"; } }; type OptOut = Omit< ComponentProps, "title" | "children" | "closeButtonText" | "closeDisabled" | "description" > & { api: States[ApiStateType.Loaded]; self: PublisherProps["publisher"]; }; const OptOut = ({ api, self, ...props }: OptOut) => { return hasAnyPositions(self) ? (

You cannot opt out of rewards while self-staked.

Please close all self-staking positions, wait the cooldown period (if applicable), and try again once your self-stake is fully closed.

) : ( {({ close }) => ( )} ); }; type OptOutModalContentsProps = { api: States[ApiStateType.Loaded]; self: PublisherProps["publisher"]; close: () => void; }; const OptOutModalContents = ({ api, self, close, }: OptOutModalContentsProps) => { const { state, execute } = useAsync(() => api.optPublisherOut(self.publicKey), ); const toast = useToast(); const doOptOut = useCallback(() => { execute() .then(() => { toast.success("You have opted out of rewards"); }) .catch(() => { /* no-op since this is already handled in the UI using `state` and is logged in useAsync */ }); }, [execute, toast]); return ( <>

Are you sure you want to opt out of rewards?

Opting out of rewards will prevent you from earning the publisher yield rate and delegation fees from your delegators. You will still be able to participate in OIS after opting out of rewards.

{state.type === UseAsyncStateType.Error && (
)}
); }; type PublisherListProps = { api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; currentEpoch: bigint; title: string; availableToStake: bigint; totalStaked: bigint; publishers: PublisherProps["publisher"][]; yieldRate: bigint; }; const PublisherList = ({ api, currentEpoch, title, availableToStake, publishers, totalStaked, yieldRate, }: PublisherListProps) => { const [pageSize, setPageSize] = useState<(typeof PageSize)[number]>( PageSize[2], ); const scrollTarget = useRef(null); const [search, setSearch] = useState(""); const [yoursFirst, setYoursFirst] = useState(true); const [sort, setSort] = useState(SortOption.SelfStakeDescending); const filter = useFilter({ sensitivity: "base", usage: "search" }); const [currentPage, setPage] = useState(1); const collator = useCollator(); const filteredSortedPublishers = useMemo( () => publishers .filter( (publisher) => filter.contains(publisher.publicKey.toBase58(), search) || (publisher.identity !== undefined && filter.contains(publisher.identity.name, search)), ) .sort((a, b) => { if (yoursFirst) { const aHasPositions = hasAnyPositions(a); const bHasPositions = hasAnyPositions(b); if (aHasPositions && !bHasPositions) { return -1; } else if (bHasPositions && !aHasPositions) { return 1; } } return compare(collator, a, b, yieldRate, sort); }), [publishers, search, sort, filter, yieldRate, yoursFirst, collator], ); const paginatedPublishers = useMemo( () => filteredSortedPublishers.slice( (currentPage - 1) * pageSize, currentPage * pageSize, ), [filteredSortedPublishers, currentPage, pageSize], ); const updatePage = useCallback( (newPage) => { if (scrollTarget.current) { scrollTarget.current.scrollIntoView({ behavior: "smooth" }); } setPage(newPage); }, [setPage], ); const updateSearch = useCallback( (newSearch) => { setSearch(newSearch); updatePage(1); }, [setSearch, updatePage], ); const updateSort = useCallback( (newSort) => { setSort(newSort); updatePage(1); }, [setSort, updatePage], ); const updateYoursFirst = useCallback( (newYoursFirst) => { setYoursFirst(newYoursFirst); updatePage(1); }, [setYoursFirst, updatePage], ); const updatePageSize = useCallback( (newPageSize) => { setPageSize(newPageSize); updatePage(1); }, [setPageSize, updatePage], ); const numPages = useMemo( () => Math.ceil(filteredSortedPublishers.length / pageSize), [filteredSortedPublishers, pageSize], ); return (

{title}

)}
); }; type PaginatorProps = { currentPage: number; numPages: number; onPageChange: (newPage: number) => void; }; const Paginator = ({ currentPage, numPages, onPageChange }: PaginatorProps) => { const { first, count } = getPageRange(currentPage, numPages); const pages = Array.from({ length: count }) .fill(undefined) .map((_, i) => i + first); return (
    {currentPage > 1 && (
  • )} {pages.map((page) => page === currentPage ? (
  • {page}
  • ) : (
  • ), )} {currentPage < numPages && (
  • )}
); }; const getPageRange = ( page: number, numPages: number, ): { first: number; count: number } => { const first = page <= 3 || numPages <= 5 ? 1 : page - 2 - Math.max(2 - (numPages - page), 0); return { first, count: Math.min(numPages - first + 1, 5) }; }; const compare = ( collator: Intl.Collator, a: PublisherProps["publisher"], b: PublisherProps["publisher"], yieldRate: bigint, sort: SortOption, ): number => { switch (sort) { case SortOption.PublisherNameAscending: case SortOption.PublisherNameDescending: { // No need for a fallback sort since each publisher has a unique value. return compareName( collator, a, b, sort === SortOption.PublisherNameAscending, ); } case SortOption.ApyAscending: case SortOption.ApyDescending: { const ascending = sort === SortOption.ApyAscending; return compareInOrder([ () => compareApy(a, b, yieldRate, ascending), () => compareSelfStake(a, b, ascending), () => comparePoolCapacity(a, b, ascending), () => compareName(collator, a, b, ascending), ]); } case SortOption.NumberOfFeedsAscending: case SortOption.NumberOfFeedsDescending: { const ascending = sort === SortOption.NumberOfFeedsAscending; return compareInOrder([ () => (ascending ? -1 : 1) * Number(b.numFeeds - a.numFeeds), () => compareSelfStake(a, b, ascending), () => comparePoolCapacity(a, b, ascending), () => compareApy(a, b, yieldRate, ascending), () => compareName(collator, a, b, ascending), ]); } case SortOption.RemainingPoolAscending: case SortOption.RemainingPoolDescending: { const ascending = sort === SortOption.RemainingPoolAscending; return compareInOrder([ () => comparePoolCapacity(a, b, ascending), () => compareSelfStake(a, b, ascending), () => compareApy(a, b, yieldRate, ascending), () => compareName(collator, a, b, ascending), ]); } case SortOption.QualityRankingDescending: case SortOption.QualityRankingAscending: { // No need for a fallback sort since each publisher has a unique value. return compareQualityRanking( a, b, sort === SortOption.QualityRankingAscending, ); } case SortOption.SelfStakeAscending: case SortOption.SelfStakeDescending: { const ascending = sort === SortOption.SelfStakeAscending; return compareInOrder([ () => compareSelfStake(a, b, ascending), () => comparePoolCapacity(a, b, ascending), () => compareApy(a, b, yieldRate, ascending), () => compareName(collator, a, b, ascending), ]); } } }; const compareInOrder = (comparisons: (() => number)[]): number => { for (const compare of comparisons) { const value = compare(); if (value !== 0) { return value; } } return 0; }; const compareName = ( collator: Intl.Collator, a: PublisherProps["publisher"], b: PublisherProps["publisher"], reverse?: boolean, ) => (reverse ? -1 : 1) * collator.compare( a.identity?.name ?? a.publicKey.toBase58(), b.identity?.name ?? b.publicKey.toBase58(), ); const compareApy = ( a: PublisherProps["publisher"], b: PublisherProps["publisher"], yieldRate: bigint, reverse?: boolean, ) => (reverse ? -1 : 1) * (calculateApy({ isSelf: false, selfStake: b.selfStake + b.selfStakeDelta, poolCapacity: b.poolCapacity, poolUtilization: b.poolUtilization + b.poolUtilizationDelta, yieldRate, delegationFee: b.delegationFee, }) - calculateApy({ isSelf: false, selfStake: a.selfStake + a.selfStakeDelta, poolCapacity: a.poolCapacity, poolUtilization: a.poolUtilization + a.poolUtilizationDelta, yieldRate, delegationFee: a.delegationFee, })); const comparePoolCapacity = ( a: PublisherProps["publisher"], b: PublisherProps["publisher"], reverse?: boolean, ) => { if (a.poolCapacity === 0n && b.poolCapacity === 0n) { return 0; } else if (a.poolCapacity === 0n) { return 1; } else if (b.poolCapacity === 0n) { return -1; } else { const remainingPoolA = a.poolCapacity - a.poolUtilization - a.poolUtilizationDelta; const remainingPoolB = b.poolCapacity - b.poolUtilization - b.poolUtilizationDelta; if (remainingPoolA <= 0n && remainingPoolB <= 0n) { return 0; } else if (remainingPoolA <= 0n && remainingPoolB > 0n) { return 1; } else if (remainingPoolB <= 0n && remainingPoolA > 0n) { return -1; } else { return (reverse ? -1 : 1) * Number(remainingPoolB - remainingPoolA); } } }; const compareQualityRanking = ( a: PublisherProps["publisher"], b: PublisherProps["publisher"], reverse?: boolean, ) => { if (a.qualityRanking === 0 && b.qualityRanking === 0) { return 0; } else if (a.qualityRanking === 0) { return 1; } else if (b.qualityRanking === 0) { return -1; } else { return (reverse ? -1 : 1) * Number(a.qualityRanking - b.qualityRanking); } }; const compareSelfStake = ( a: PublisherProps["publisher"], b: PublisherProps["publisher"], reverse?: boolean, ) => (reverse ? -1 : 1) * Number(b.selfStake + b.selfStakeDelta - (a.selfStake + a.selfStakeDelta)); type SortablePublisherTableHeaderProps = Omit< ComponentProps, "children" > & { children: string; asc: SortOption; desc: SortOption; sort: SortOption; setSort: Dispatch>; alignment?: "left" | "right"; }; const SortablePublisherTableHeader = ({ asc, desc, sort, setSort, children, className, alignment, ...props }: SortablePublisherTableHeaderProps) => { const updateSort = useCallback(() => { setSort((cur) => (cur === desc ? asc : desc)); }, [setSort, asc, desc]); return ( {children} ); }; const PublisherTableHeader = Styled( "th", "py-2 font-normal px-5 h-full whitespace-nowrap", ); type PublisherProps = { api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; currentEpoch: bigint; availableToStake: bigint; totalStaked: bigint; isSelf?: boolean | undefined; publisher: { identity: | { name: string; icon: ComponentType>; } | undefined; publicKey: PublicKey; stakeAccount: PublicKey | undefined; selfStake: bigint; selfStakeDelta: bigint; poolCapacity: bigint; poolUtilization: bigint; poolUtilizationDelta: bigint; numFeeds: number; qualityRanking: number; delegationFee: bigint; apyHistory: { date: Date; apy: number; selfApy: number }[]; positions?: | { warmup?: bigint | undefined; staked?: bigint | undefined; cooldown?: bigint | undefined; cooldown2?: bigint | undefined; } | undefined; }; yieldRate: bigint; compact?: boolean | undefined; }; const Publisher = ({ api, currentEpoch, publisher, availableToStake, totalStaked, isSelf, yieldRate, compact, }: 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( api.type === ApiStateType.Loaded ? api.cancelWarmupIntegrityStaking : undefined, publisher.publicKey, ); const unstake = useTransferActionForPublisher( api.type === ApiStateType.Loaded ? api.unstakeIntegrityStaking : undefined, publisher.publicKey, ); const estimatedNextApy = useMemo( () => calculateApy({ isSelf: isSelf ?? false, selfStake: publisher.selfStake + publisher.selfStakeDelta, poolCapacity: publisher.poolCapacity, poolUtilization: publisher.poolUtilization + publisher.poolUtilizationDelta, yieldRate, delegationFee: publisher.delegationFee, }).toFixed(2), [ isSelf, publisher.selfStake, publisher.selfStakeDelta, publisher.poolCapacity, publisher.poolUtilization, publisher.poolUtilizationDelta, publisher.delegationFee, yieldRate, ], ); return compact ? (
{!isSelf && (
{publisher}
)}
{!isSelf && (
)} {isSelf && ( )}
{!isSelf && (
{"Publisher's Stake:"}
{publisher.selfStake + publisher.selfStakeDelta}
)}
Estimated Next APY:
{estimatedNextApy}%
Number of feeds:
{publisher.numFeeds}
Quality ranking:
{publisher.qualityRanking === 0 ? "-" : publisher.qualityRanking}
{isSelf && ( )} {(warmup !== undefined || staked !== undefined) && ( )}
) : ( <> {!isSelf && ( <> {publisher} {publisher.selfStake + publisher.selfStakeDelta} )} {estimatedNextApy}%
({ date, value: isSelf ? selfApy : apy, }))} />
{publisher.numFeeds} {publisher.qualityRanking === 0 ? "-" : publisher.qualityRanking} {(warmup !== undefined || staked !== undefined) && ( )} ); }; type UtilizationMeterProps = Omit, "children"> & { publisher: PublisherProps["publisher"]; }; const UtilizationMeter = ({ publisher, ...props }: UtilizationMeterProps) => { const utilizationPercent = useMemo( () => publisher.poolCapacity > 0n ? Number( (100n * (publisher.poolUtilization + publisher.poolUtilizationDelta)) / publisher.poolCapacity, ) : Number.NaN, [ publisher.poolUtilization, publisher.poolUtilizationDelta, publisher.poolCapacity, ], ); return ( {({ percentage }) => ( <>
{Number.isNaN(utilizationPercent) ? "Empty Pool" : `${utilizationPercent.toString()}%`}
)} ); }; type YourPositionsTableProps = { publisher: PublisherProps["publisher"]; warmup: bigint | undefined; cancelWarmup: ((amount: bigint) => Promise) | undefined; staked: bigint | undefined; totalStaked: bigint; unstake: ((amount: bigint) => Promise) | undefined; currentEpoch: bigint; }; const YourPositionsTable = ({ warmup, cancelWarmup, staked, totalStaked, unstake, currentEpoch, publisher, }: YourPositionsTableProps) => (
{warmup !== undefined && ( )} {staked !== undefined && ( )}
Your Positions
Warmup {warmup} Cancel tokens that are in warmup for staking to {publisher} } actionName="Cancel" submitButtonText="Cancel Warmup" title="Cancel Warmup" successMessage="Your tokens are no longer in warmup for staking" max={warmup} transfer={cancelWarmup} />
Staked
{staked}
({Number((100n * staked) / totalStaked)}% of your staked tokens)
Unstake tokens from {publisher} } actionName="Unstake" successMessage="Your tokens are now cooling down and will be available to withdraw at the end of the next epoch" max={staked} transfer={unstake} >
); const PublisherTableCell = Styled("td", "py-4 px-5 whitespace-nowrap"); type StakeToPublisherButtonProps = { api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; publisher: PublisherProps["publisher"]; currentEpoch: bigint; availableToStake: bigint; yieldRate: bigint; isSelf: boolean; }; const StakeToPublisherButton = ({ api, currentEpoch, availableToStake, publisher, yieldRate, isSelf, }: StakeToPublisherButtonProps) => { const delegate = useTransferActionForPublisher( api.type === ApiStateType.Loaded ? api.delegateIntegrityStaking : undefined, publisher.publicKey, ); return ( Stake to {publisher} } actionName="Stake" max={availableToStake} transfer={delegate} successMessage="Your tokens are now in warm up and will be staked at the start of the next epoch" > {(amount) => ( <>
APY after staking
{amount.type === AmountType.Valid || amount.type === AmountType.AboveMax ? amount.amount : 0n}
)}
); }; type NewApyProps = Omit, "children"> & { isSelf: boolean; publisher: PublisherProps["publisher"]; yieldRate: bigint; children: bigint; }; const NewApy = ({ isSelf, publisher, yieldRate, children, ...props }: NewApyProps) => { const apy = useMemo( () => calculateApy({ poolCapacity: publisher.poolCapacity, yieldRate, delegationFee: publisher.delegationFee, ...(isSelf ? { isSelf: true, selfStake: publisher.selfStake + publisher.selfStakeDelta + children, } : { isSelf: false, selfStake: publisher.selfStake + publisher.selfStakeDelta, poolUtilization: publisher.poolUtilization + publisher.poolUtilizationDelta + children, }), }), [ publisher.poolCapacity, yieldRate, isSelf, publisher.selfStake, publisher.selfStakeDelta, publisher.poolUtilization, publisher.poolUtilizationDelta, publisher.delegationFee, children, ], ); return
{apy}%
; }; type PublisherIdentityProps = PublisherKeyProps & { withNameClassName?: string | undefined; }; const PublisherIdentity = ({ className, withNameClassName, ...props }: PublisherIdentityProps) => props.children.identity ? ( {createElement(props.children.identity.icon, { className: "mr-2 inline-block h-[20px] align-sub", })} {props.children.identity.name} ) : ( ); type PublisherKeyProps = { className?: string | undefined; children: PublisherProps["publisher"]; fullClassName?: string; truncatedClassName?: string; }; const PublisherKey = ({ children, fullClassName, truncatedClassName, className, }: PublisherKeyProps) => ( {fullClassName && ( {children.publicKey.toBase58()} )} {children.publicKey} ); const useTransferActionForPublisher = ( action: ((publisher: PublicKey, amount: bigint) => Promise) | undefined, publisher: PublicKey, ) => useMemo( () => action === undefined ? undefined : (amount: bigint) => action(publisher, amount), [action, publisher], ); const hasAnyPositions = ({ positions }: PublisherProps["publisher"]) => positions !== undefined && [ positions.warmup, positions.staked, positions.cooldown, positions.cooldown2, ].some((value) => value !== undefined && value > 0n); enum SortOption { PublisherNameDescending, PublisherNameAscending, RemainingPoolDescending, RemainingPoolAscending, ApyDescending, ApyAscending, SelfStakeDescending, SelfStakeAscending, NumberOfFeedsDescending, NumberOfFeedsAscending, QualityRankingDescending, QualityRankingAscending, } const getSortName = (sortOption: SortOption) => { switch (sortOption) { case SortOption.PublisherNameDescending: { return "Publisher Name (A-Z)"; } case SortOption.PublisherNameAscending: { return "Publisher Name (Z-A)"; } case SortOption.RemainingPoolDescending: { return "Most remaining pool"; } case SortOption.RemainingPoolAscending: { return "Least remaining pool"; } case SortOption.ApyDescending: { return "Highest estimated next APY"; } case SortOption.ApyAscending: { return "Lowest estimated next APY"; } case SortOption.SelfStakeDescending: { return "Highest publisher's stake"; } case SortOption.SelfStakeAscending: { return "Lowest publisher's stake"; } case SortOption.NumberOfFeedsDescending: { return "Most feeds"; } case SortOption.NumberOfFeedsAscending: { return "Least feeds"; } case SortOption.QualityRankingDescending: { return "Best quality ranking"; } case SortOption.QualityRankingAscending: { return "Worst quality ranking"; } } }; const PageSize = [10, 20, 30, 40, 50] as const; class InvalidKeyError extends Error { constructor() { super("Invalid public key"); } }