| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- "use client";
- import {
- Tab,
- TabGroup,
- TabList,
- TabPanel,
- TabPanels,
- Field,
- Label,
- } from "@headlessui/react";
- import { ArrowPathIcon } from "@heroicons/react/24/outline";
- import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
- import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
- import { ChainIcon } from "connectkit";
- import {
- type Dispatch,
- type SetStateAction,
- type ComponentProps,
- type ElementType,
- type SVGAttributes,
- useState,
- useCallback,
- useMemo,
- } from "react";
- import Markdown from "react-markdown";
- import { useSwitchChain, useChainId, useConfig } from "wagmi";
- import { readContract } from "wagmi/actions";
- import { getContractAddress } from "./networks";
- import type { Parameter } from "./parameter";
- import { ParameterInput } from "./parameter-input";
- import { type EvmApiType, RunButton } from "./run-button";
- import { getLogger } from "../../browser-logger";
- import { MARKDOWN_COMPONENTS } from "../../markdown-components";
- import { useIsMounted } from "../../use-is-mounted";
- import { type SupportedLanguage, Code } from "../Code";
- import { ErrorTooltip } from "../ErrorTooltip";
- import { InlineLink } from "../InlineLink";
- import { Select } from "../Select";
- export { ParameterType } from "./parameter";
- export { EvmApiType } from "./run-button";
- const abi = [...PythAbi, ...PythErrorsAbi] as const;
- type Props<ParameterName extends string> =
- | ReadApi<ParameterName>
- | WriteApi<ParameterName>;
- type Common<ParameterName extends string> = {
- name: (typeof PythAbi)[number]["name"];
- summary: string;
- description: string;
- parameters: Parameter<ParameterName>[];
- examples: Example<ParameterName>[];
- code: CodeSample<ParameterName>[];
- };
- export type ReadApi<ParameterName extends string> = Common<ParameterName> & {
- type: EvmApiType.Read;
- };
- export type WriteApi<ParameterName extends string> = Common<ParameterName> & {
- type: EvmApiType.Write;
- valueParam: ParameterName;
- };
- type Example<ParameterName extends string> = {
- name: string;
- icon?: ElementType<SVGAttributes<SVGSVGElement>>;
- parameters: ValueOrFunctionOrAsyncFunction<Record<ParameterName, string>>;
- };
- type ValueOrFunctionOrAsyncFunction<T> =
- | T
- | ((ctx: ContractContext) => T)
- | ((ctx: ContractContext) => Promise<T>);
- type ContractContext = {
- readContract: (functionName: string, args: unknown[]) => Promise<unknown>;
- };
- export enum Language {
- Solidity,
- EthersJSV6,
- }
- type CodeSample<ParameterName extends string> = {
- language: Language;
- dimRange: ComponentProps<typeof Code>["dimRange"];
- code: (
- network: NetworkInfo,
- params: Partial<Record<ParameterName, string>>,
- ) => string;
- };
- export type NetworkInfo = {
- name: string;
- rpcUrl: string;
- contractAddress: string;
- };
- export const EvmApi = <ParameterName extends string>({
- name,
- summary,
- description,
- parameters,
- code,
- examples,
- ...props
- }: Props<ParameterName>) => {
- const [paramValues, setParamValues] = useState<
- Partial<Record<ParameterName, string>>
- >({});
- const chainId = useChainId();
- const { chains, switchChain } = useSwitchChain();
- const isMounted = useIsMounted();
- const currentChain = useMemo(() => {
- const chain = isMounted
- ? chains.find((chain) => chain.id === chainId)
- : chains[0];
- if (chain === undefined) {
- throw new Error(`Invalid current chain id: ${chainId.toString()}`);
- }
- return chain;
- }, [chainId, chains, isMounted]);
- return (
- <div className="gap-x-20 lg:grid lg:grid-cols-[2fr_1fr]">
- <h1 className="col-span-2 mb-6 font-mono text-4xl font-medium">{name}</h1>
- <div className="col-span-2 mb-6 opacity-60">
- <Markdown components={MARKDOWN_COMPONENTS}>{summary}</Markdown>
- </div>
- <section>
- <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
- Description
- </h2>
- <Markdown components={MARKDOWN_COMPONENTS}>{description}</Markdown>
- </section>
- <section className="flex flex-col">
- <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
- Arguments
- </h2>
- <div className="mb-8">
- {parameters.length > 0 ? (
- <ul className="flex flex-col gap-4">
- {parameters.map((parameter) => (
- <li key={parameter.name} className="contents">
- <ParameterInput
- spec={parameter}
- value={paramValues[parameter.name]}
- setParamValues={setParamValues}
- />
- </li>
- ))}
- </ul>
- ) : (
- <div className="rounded-lg bg-neutral-200 p-8 text-center text-sm dark:bg-neutral-800">
- This API takes no arguments
- </div>
- )}
- </div>
- <div className="grow" />
- {examples.length > 0 && (
- <div className="mb-8">
- <h3 className="text-sm font-semibold">Examples</h3>
- <ul className="ml-2 text-sm">
- {examples.map((example) => (
- <li key={example.name}>
- <Example example={example} setParamValues={setParamValues} />
- </li>
- ))}
- </ul>
- </div>
- )}
- <Field className="mb-4 flex w-full flex-row items-center gap-2">
- <Label className="text-sm font-bold">Network</Label>
- <Select
- value={currentChain}
- onChange={({ id }) => {
- switchChain({ chainId: id });
- }}
- renderButtonContents={({ id, name }) => (
- <div className="flex h-8 flex-row items-center gap-2">
- {isMounted && (
- <>
- <ChainIcon id={id} />
- <span>{name}</span>
- </>
- )}
- </div>
- )}
- renderOption={({ id, name }) => (
- <div className="flex flex-row items-center gap-2">
- <ChainIcon id={id} />
- <span>{name}</span>
- </div>
- )}
- options={chains}
- buttonClassName="grow"
- />
- </Field>
- <RunButton
- functionName={name}
- parameters={parameters}
- paramValues={paramValues}
- {...props}
- />
- </section>
- <TabGroup className="col-span-2 mt-24">
- <TabList className="mb-4 flex flex-row gap-2 border-b border-neutral-200 pb-px dark:border-neutral-800">
- {code.map(({ language }) => (
- <Tab
- key={LANGUAGE_TO_DISPLAY_NAME[language]}
- className="mb-[-2px] border-b-2 border-transparent px-2 text-sm font-medium leading-loose hover:text-pythpurple-600 data-[selected]:cursor-default data-[selected]:border-pythpurple-600 data-[selected]:text-pythpurple-600 dark:hover:text-pythpurple-400 dark:data-[selected]:border-pythpurple-400 dark:data-[selected]:text-pythpurple-400"
- >
- {LANGUAGE_TO_DISPLAY_NAME[language]}
- </Tab>
- ))}
- </TabList>
- <TabPanels>
- {code.map(({ code: codeContents, language, dimRange }) => (
- <TabPanel key={LANGUAGE_TO_DISPLAY_NAME[language]}>
- <Code
- language={LANUGAGE_TO_SHIKI_NAME[language]}
- dimRange={dimRange}
- >
- {codeContents(
- isMounted
- ? {
- name: currentChain.name,
- rpcUrl: currentChain.rpcUrls.default.http[0] ?? "",
- contractAddress: getContractAddress(chainId) ?? "",
- }
- : { name: "", rpcUrl: "", contractAddress: "" },
- paramValues,
- )}
- </Code>
- </TabPanel>
- ))}
- </TabPanels>
- </TabGroup>
- </div>
- );
- };
- const LANGUAGE_TO_DISPLAY_NAME = {
- [Language.Solidity]: "Solidity",
- [Language.EthersJSV6]: "ethers.js v6",
- };
- const LANUGAGE_TO_SHIKI_NAME: Record<Language, SupportedLanguage> = {
- [Language.Solidity]: "solidity",
- [Language.EthersJSV6]: "javascript",
- };
- type ExampleProps<ParameterName extends string> = {
- example: Example<ParameterName>;
- setParamValues: Dispatch<
- SetStateAction<Partial<Record<ParameterName, string>>>
- >;
- };
- const Example = <ParameterName extends string>({
- example,
- setParamValues,
- }: ExampleProps<ParameterName>) => {
- const config = useConfig();
- const [error, setError] = useState<string | undefined>(undefined);
- const [loading, setLoading] = useState(false);
- const updateValues = useCallback(() => {
- if (typeof example.parameters === "function") {
- setError(undefined);
- const address = getContractAddress(config.state.chainId);
- if (!address) {
- throw new Error(
- `No contract for chain id: ${config.state.chainId.toString()}`,
- );
- }
- const params = example.parameters({
- readContract: (functionName, args) =>
- readContract(config, { abi, address, functionName, args }),
- });
- if (params instanceof Promise) {
- setLoading(true);
- params
- .then((paramsResolved) => {
- setParamValues(paramsResolved);
- })
- .catch((error_: unknown) => {
- getLogger().error(error_);
- setError(
- "An error occurred while fetching data for this example, please try again",
- );
- })
- .finally(() => {
- setLoading(false);
- });
- } else {
- setParamValues(params);
- }
- } else {
- setParamValues(example.parameters);
- }
- }, [example, setParamValues, config]);
- const Icon = example.icon;
- return (
- <div className="flex flex-row items-center gap-2">
- <InlineLink
- as="button"
- onClick={updateValues}
- className="flex flex-row items-center gap-2"
- >
- {Icon && <Icon className="h-4" />}
- <span>{example.name}</span>
- </InlineLink>
- {error && <ErrorTooltip className="size-4">{error}</ErrorTooltip>}
- {loading && <ArrowPathIcon className="size-4 animate-spin" />}
- </div>
- );
- };
|