Skip to content

Commit

Permalink
Merge pull request #2206 from pyth-network/cprussin/add-component-table
Browse files Browse the repository at this point in the history
feat(staking): add price components table
  • Loading branch information
cprussin authored Dec 19, 2024
2 parents 956f53e + 3a9c600 commit f0659ce
Show file tree
Hide file tree
Showing 64 changed files with 1,794 additions and 999 deletions.
1 change: 1 addition & 0 deletions apps/insights/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"react-aria-components": "catalog:",
"react-dom": "catalog:",
"recharts": "catalog:",
"superjson": "catalog:",
"swr": "catalog:",
"zod": "catalog:"
},
Expand Down
6 changes: 3 additions & 3 deletions apps/insights/src/app/price-feeds/[slug]/layout.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Metadata } from "next";

import { client } from "../../../services/pyth";
import { getData } from "../../../services/pyth";
export { PriceFeedLayout as default } from "../../../components/PriceFeed/layout";

export const metadata: Metadata = {
title: "Price Feeds",
};

export const generateStaticParams = async () => {
const data = await client.getData();
return data.symbols.map((symbol) => ({ slug: encodeURIComponent(symbol) }));
const data = await getData();
return data.map(({ symbol }) => ({ slug: encodeURIComponent(symbol) }));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type Props = {
params: Promise<{
componentId: string;
}>;
};

const PriceFeedComponent = async ({ params }: Props) => {
const { componentId } = await params;
return componentId;
};
export default PriceFeedComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PriceComponents as default } from "../../../../components/PriceFeed/price-components";
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { PriceComponents as default } from "../../../../components/PriceFeed/price-components";
// eslint-disable-next-line unicorn/no-null
const Page = () => null;
export default Page;
16 changes: 16 additions & 0 deletions apps/insights/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { unstable_cache } from "next/cache";
import { parse, stringify } from "superjson";

export const cache = <T, P extends unknown[]>(
fn: (...params: P) => Promise<T>,
keys?: Parameters<typeof unstable_cache>[1],
opts?: Parameters<typeof unstable_cache>[2],
) => {
const cachedFn = unstable_cache(
async (params: P): Promise<string> => stringify(await fn(...params)),
keys,
opts,
);

return async (...params: P): Promise<T> => parse(await cachedFn(params));
};
18 changes: 13 additions & 5 deletions apps/insights/src/components/CopyButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@
import { Check } from "@phosphor-icons/react/dist/ssr/Check";
import { Copy } from "@phosphor-icons/react/dist/ssr/Copy";
import { useLogger } from "@pythnetwork/app-logger";
import { Button } from "@pythnetwork/component-library/Button";
import {
type Props as ButtonProps,
Button,
} from "@pythnetwork/component-library/Button";
import clsx from "clsx";
import { type ComponentProps, useCallback, useEffect, useState } from "react";
import { type ElementType, useCallback, useEffect, useState } from "react";

import styles from "./index.module.scss";

type OwnProps = {
text: string;
};

type Props = Omit<
ComponentProps<typeof Button>,
type Props<T extends ElementType> = Omit<
ButtonProps<T>,
keyof OwnProps | "onPress" | "afterIcon"
> &
OwnProps;

export const CopyButton = ({ text, children, className, ...props }: Props) => {
export const CopyButton = <T extends ElementType>({
text,
children,
className,
...props
}: Props<T>) => {
const [isCopied, setIsCopied] = useState(false);
const logger = useLogger();
const copy = useCallback(() => {
Expand Down
17 changes: 2 additions & 15 deletions apps/insights/src/components/FeedKey/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base58 from "bs58";
import { useMemo, type ComponentProps } from "react";

import { toHex, truncateHex } from "../../hex";
import { CopyButton } from "../CopyButton";

type OwnProps = {
Expand All @@ -22,24 +22,11 @@ export const FeedKey = ({ feed, ...props }: Props) => {
() => toHex(feed.product.price_account),
[feed.product.price_account],
);
const truncatedKey = useMemo(
() => toTruncatedHex(feed.product.price_account),
[feed.product.price_account],
);
const truncatedKey = useMemo(() => truncateHex(key), [key]);

return (
<CopyButton text={key} {...props}>
{truncatedKey}
</CopyButton>
);
};

const toHex = (value: string) => toHexString(base58.decode(value));

const toTruncatedHex = (value: string) => {
const hex = toHex(value);
return `${hex.slice(0, 6)}...${hex.slice(-4)}`;
};

const toHexString = (byteArray: Uint8Array) =>
`0x${Array.from(byteArray, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
13 changes: 13 additions & 0 deletions apps/insights/src/components/FormattedNumber/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use client";

import { useMemo } from "react";
import { useNumberFormatter } from "react-aria";

type Props = Parameters<typeof useNumberFormatter>[0] & {
value: number;
};

export const FormattedNumber = ({ value, ...args }: Props) => {
const numberFormatter = useNumberFormatter(args);
return useMemo(() => numberFormatter.format(value), [numberFormatter, value]);
};
2 changes: 1 addition & 1 deletion apps/insights/src/components/LayoutTransition/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const LayoutTransition = ({ children, ...props }: Props) => {

return (
<AnimatePresence
mode="wait"
mode="popLayout"
initial={false}
onExitComplete={updatePrevSegment}
custom={{ segment, prevSegment: prevSegment.current }}
Expand Down
4 changes: 2 additions & 2 deletions apps/insights/src/components/NotFound/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ButtonLink } from "@pythnetwork/component-library/Button";
import { Button } from "@pythnetwork/component-library/Button";

export const NotFound = () => (
<div>
<h1>Not Found</h1>
<p>{"The page you're looking for isn't here"}</p>
<ButtonLink href="/">Go Home</ButtonLink>
<Button href="/">Go Home</Button>
</div>
);
91 changes: 36 additions & 55 deletions apps/insights/src/components/PriceFeed/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import { ListDashes } from "@phosphor-icons/react/dist/ssr/ListDashes";
import { Alert, AlertTrigger } from "@pythnetwork/component-library/Alert";
import { Badge } from "@pythnetwork/component-library/Badge";
import { Breadcrumbs } from "@pythnetwork/component-library/Breadcrumbs";
import { Button, ButtonLink } from "@pythnetwork/component-library/Button";
import { Button } from "@pythnetwork/component-library/Button";
import { Drawer, DrawerTrigger } from "@pythnetwork/component-library/Drawer";
import { StatCard } from "@pythnetwork/component-library/StatCard";
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
import { z } from "zod";

import styles from "./layout.module.scss";
import { PriceFeedSelect } from "./price-feed-select";
import { ReferenceData } from "./reference-data";
import { TabPanel, TabRoot, Tabs } from "./tabs";
import { client } from "../../services/pyth";
import { toHex } from "../../hex";
import { getData } from "../../services/pyth";
import { YesterdaysPricesProvider, ChangePercent } from "../ChangePercent";
import { FeedKey } from "../FeedKey";
import { LivePrice, LiveConfidence, LiveLastUpdated } from "../LivePrices";
import { NotFound } from "../NotFound";
import {
LivePrice,
LiveConfidence,
LiveLastUpdated,
LiveValue,
} from "../LivePrices";
import { PriceFeedTag } from "../PriceFeedTag";

type Props = {
Expand All @@ -28,8 +34,9 @@ type Props = {
};

export const PriceFeedLayout = async ({ children, params }: Props) => {
const { slug } = await params;
const feed = await getPriceFeed(decodeURIComponent(slug));
const [{ slug }, data] = await Promise.all([params, getData()]);
const symbol = decodeURIComponent(slug);
const feed = data.find((item) => item.symbol === symbol);

return feed ? (
<div className={styles.priceFeedLayout}>
Expand All @@ -50,7 +57,24 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
</div>
</div>
<div className={styles.headerRow}>
<PriceFeedTag feed={feed} />
<PriceFeedSelect
feeds={data
.filter((feed) => feed.symbol !== symbol)
.map((feed) => ({
id: encodeURIComponent(feed.symbol),
key: toHex(feed.product.price_account),
displaySymbol: feed.product.display_symbol,
name: <PriceFeedTag compact feed={feed} />,
assetClassText: feed.product.asset_type,
assetClass: (
<Badge variant="neutral" style="outline" size="xs">
{feed.product.asset_type.toUpperCase()}
</Badge>
),
}))}
>
<PriceFeedTag feed={feed} />
</PriceFeedSelect>
<div className={styles.rightGroup}>
<FeedKey
variant="ghost"
Expand Down Expand Up @@ -96,14 +120,14 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
of the confidence of individual quoters and how well
individual quoters agree with each other.
</p>
<ButtonLink
<Button
size="xs"
variant="solid"
href="https://docs.pyth.network/price-feeds/best-practices#confidence-intervals"
target="_blank"
>
Learn more
</ButtonLink>
</Button>
</Alert>
</AlertTrigger>
}
Expand Down Expand Up @@ -134,7 +158,7 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
<div className={styles.priceComponentsTabLabel}>
<span>Price Components</span>
<Badge size="xs" style="filled" variant="neutral">
{feed.price.numComponentPrices}
<LiveValue feed={feed} field="numComponentPrices" />
</Badge>
</div>
),
Expand All @@ -145,49 +169,6 @@ export const PriceFeedLayout = async ({ children, params }: Props) => {
</TabRoot>
</div>
) : (
<NotFound />
notFound()
);
};

const getPriceFeed = async (symbol: string) => {
const data = await client.getData();
const priceFeeds = priceFeedsSchema.parse(
data.symbols.map((symbol) => ({
symbol,
product: data.productFromSymbol.get(symbol),
price: data.productPrice.get(symbol),
})),
);
return priceFeeds.find((feed) => feed.symbol === symbol);
};

const priceFeedsSchema = z.array(
z.object({
symbol: z.string(),
product: z.object({
display_symbol: z.string(),
asset_type: z.string(),
description: z.string(),
price_account: z.string(),
base: z.string().optional(),
country: z.string().optional(),
quote_currency: z.string().optional(),
tenor: z.string().optional(),
cms_symbol: z.string().optional(),
cqs_symbol: z.string().optional(),
nasdaq_symbol: z.string().optional(),
generic_symbol: z.string().optional(),
weekly_schedule: z.string().optional(),
schedule: z.string().optional(),
contract_id: z.string().optional(),
}),
price: z.object({
exponent: z.number(),
numComponentPrices: z.number(),
numQuoters: z.number(),
minPublishers: z.number(),
lastSlot: z.bigint(),
validSlot: z.bigint(),
}),
}),
);
44 changes: 44 additions & 0 deletions apps/insights/src/components/PriceFeed/price-component-drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { Drawer } from "@pythnetwork/component-library/Drawer";
import {
useSelectedLayoutSegment,
usePathname,
useRouter,
} from "next/navigation";
import { type ReactNode, useMemo, useCallback } from "react";

type Props = {
children: ReactNode;
};

export const PriceComponentDrawer = ({ children }: Props) => {
const pathname = usePathname();
const segment = useSelectedLayoutSegment();
const prevUrl = useMemo(
() =>
segment ? pathname.replace(new RegExp(`/${segment}$`), "") : pathname,
[pathname, segment],
);
const router = useRouter();

const onOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
router.push(prevUrl);
}
},
[router, prevUrl],
);

return (
<Drawer
title="Hello!"
closeHref={prevUrl}
onOpenChange={onOpenChange}
isOpen={segment !== null}
>
{children}
</Drawer>
);
};
Loading

0 comments on commit f0659ce

Please sign in to comment.