index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. "use client";
  2. import {
  3. Tab,
  4. TabGroup,
  5. TabList,
  6. TabPanel,
  7. TabPanels,
  8. Field,
  9. Label,
  10. } from "@headlessui/react";
  11. import { ArrowPathIcon } from "@heroicons/react/24/outline";
  12. import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
  13. import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
  14. import { ChainIcon } from "connectkit";
  15. import {
  16. type Dispatch,
  17. type SetStateAction,
  18. type ComponentProps,
  19. type ElementType,
  20. type SVGAttributes,
  21. useState,
  22. useCallback,
  23. useMemo,
  24. } from "react";
  25. import Markdown from "react-markdown";
  26. import { useSwitchChain, useChainId, useConfig } from "wagmi";
  27. import { readContract } from "wagmi/actions";
  28. import { getContractAddress } from "./networks";
  29. import type { Parameter } from "./parameter";
  30. import { ParameterInput } from "./parameter-input";
  31. import { type EvmApiType, RunButton } from "./run-button";
  32. import { getLogger } from "../../browser-logger";
  33. import { MARKDOWN_COMPONENTS } from "../../markdown-components";
  34. import { useIsMounted } from "../../use-is-mounted";
  35. import { type SupportedLanguage, Code } from "../Code";
  36. import { ErrorTooltip } from "../ErrorTooltip";
  37. import { InlineLink } from "../InlineLink";
  38. import { Select } from "../Select";
  39. export { ParameterType } from "./parameter";
  40. export { EvmApiType } from "./run-button";
  41. const abi = [...PythAbi, ...PythErrorsAbi] as const;
  42. type Props<ParameterName extends string> =
  43. | ReadApi<ParameterName>
  44. | WriteApi<ParameterName>;
  45. type Common<ParameterName extends string> = {
  46. name: (typeof PythAbi)[number]["name"];
  47. summary: string;
  48. description: string;
  49. parameters: Parameter<ParameterName>[];
  50. examples: Example<ParameterName>[];
  51. code: CodeSample<ParameterName>[];
  52. };
  53. export type ReadApi<ParameterName extends string> = Common<ParameterName> & {
  54. type: EvmApiType.Read;
  55. };
  56. export type WriteApi<ParameterName extends string> = Common<ParameterName> & {
  57. type: EvmApiType.Write;
  58. valueParam: ParameterName;
  59. };
  60. type Example<ParameterName extends string> = {
  61. name: string;
  62. icon?: ElementType<SVGAttributes<SVGSVGElement>>;
  63. parameters: ValueOrFunctionOrAsyncFunction<Record<ParameterName, string>>;
  64. };
  65. type ValueOrFunctionOrAsyncFunction<T> =
  66. | T
  67. | ((ctx: ContractContext) => T)
  68. | ((ctx: ContractContext) => Promise<T>);
  69. type ContractContext = {
  70. readContract: (functionName: string, args: unknown[]) => Promise<unknown>;
  71. };
  72. export enum Language {
  73. Solidity,
  74. EthersJSV6,
  75. }
  76. type CodeSample<ParameterName extends string> = {
  77. language: Language;
  78. dimRange: ComponentProps<typeof Code>["dimRange"];
  79. code: (
  80. network: NetworkInfo,
  81. params: Partial<Record<ParameterName, string>>,
  82. ) => string;
  83. };
  84. export type NetworkInfo = {
  85. name: string;
  86. rpcUrl: string;
  87. contractAddress: string;
  88. };
  89. export const EvmApi = <ParameterName extends string>({
  90. name,
  91. summary,
  92. description,
  93. parameters,
  94. code,
  95. examples,
  96. ...props
  97. }: Props<ParameterName>) => {
  98. const [paramValues, setParamValues] = useState<
  99. Partial<Record<ParameterName, string>>
  100. >({});
  101. const chainId = useChainId();
  102. const { chains, switchChain } = useSwitchChain();
  103. const isMounted = useIsMounted();
  104. const currentChain = useMemo(() => {
  105. const chain = isMounted
  106. ? chains.find((chain) => chain.id === chainId)
  107. : chains[0];
  108. if (chain === undefined) {
  109. throw new Error(`Invalid current chain id: ${chainId.toString()}`);
  110. }
  111. return chain;
  112. }, [chainId, chains, isMounted]);
  113. return (
  114. <div className="gap-x-20 lg:grid lg:grid-cols-[2fr_1fr]">
  115. <h1 className="col-span-2 mb-6 font-mono text-4xl font-medium">{name}</h1>
  116. <div className="col-span-2 mb-6 opacity-60">
  117. <Markdown components={MARKDOWN_COMPONENTS}>{summary}</Markdown>
  118. </div>
  119. <section>
  120. <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
  121. Description
  122. </h2>
  123. <Markdown components={MARKDOWN_COMPONENTS}>{description}</Markdown>
  124. </section>
  125. <section className="flex flex-col">
  126. <h2 className="mb-4 border-b border-neutral-200 text-2xl/loose font-medium dark:border-neutral-800">
  127. Arguments
  128. </h2>
  129. <div className="mb-8">
  130. {parameters.length > 0 ? (
  131. <ul className="flex flex-col gap-4">
  132. {parameters.map((parameter) => (
  133. <li key={parameter.name} className="contents">
  134. <ParameterInput
  135. spec={parameter}
  136. value={paramValues[parameter.name]}
  137. setParamValues={setParamValues}
  138. />
  139. </li>
  140. ))}
  141. </ul>
  142. ) : (
  143. <div className="rounded-lg bg-neutral-200 p-8 text-center text-sm dark:bg-neutral-800">
  144. This API takes no arguments
  145. </div>
  146. )}
  147. </div>
  148. <div className="grow" />
  149. {examples.length > 0 && (
  150. <div className="mb-8">
  151. <h3 className="text-sm font-semibold">Examples</h3>
  152. <ul className="ml-2 text-sm">
  153. {examples.map((example) => (
  154. <li key={example.name}>
  155. <Example example={example} setParamValues={setParamValues} />
  156. </li>
  157. ))}
  158. </ul>
  159. </div>
  160. )}
  161. <Field className="mb-4 flex w-full flex-row items-center gap-2">
  162. <Label className="text-sm font-bold">Network</Label>
  163. <Select
  164. value={currentChain}
  165. onChange={({ id }) => {
  166. switchChain({ chainId: id });
  167. }}
  168. renderButtonContents={({ id, name }) => (
  169. <div className="flex h-8 flex-row items-center gap-2">
  170. {isMounted && (
  171. <>
  172. <ChainIcon id={id} />
  173. <span>{name}</span>
  174. </>
  175. )}
  176. </div>
  177. )}
  178. renderOption={({ id, name }) => (
  179. <div className="flex flex-row items-center gap-2">
  180. <ChainIcon id={id} />
  181. <span>{name}</span>
  182. </div>
  183. )}
  184. options={chains}
  185. buttonClassName="grow"
  186. />
  187. </Field>
  188. <RunButton
  189. functionName={name}
  190. parameters={parameters}
  191. paramValues={paramValues}
  192. {...props}
  193. />
  194. </section>
  195. <TabGroup className="col-span-2 mt-24">
  196. <TabList className="mb-4 flex flex-row gap-2 border-b border-neutral-200 pb-px dark:border-neutral-800">
  197. {code.map(({ language }) => (
  198. <Tab
  199. key={LANGUAGE_TO_DISPLAY_NAME[language]}
  200. 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"
  201. >
  202. {LANGUAGE_TO_DISPLAY_NAME[language]}
  203. </Tab>
  204. ))}
  205. </TabList>
  206. <TabPanels>
  207. {code.map(({ code: codeContents, language, dimRange }) => (
  208. <TabPanel key={LANGUAGE_TO_DISPLAY_NAME[language]}>
  209. <Code
  210. language={LANUGAGE_TO_SHIKI_NAME[language]}
  211. dimRange={dimRange}
  212. >
  213. {codeContents(
  214. isMounted
  215. ? {
  216. name: currentChain.name,
  217. rpcUrl: currentChain.rpcUrls.default.http[0] ?? "",
  218. contractAddress: getContractAddress(chainId) ?? "",
  219. }
  220. : { name: "", rpcUrl: "", contractAddress: "" },
  221. paramValues,
  222. )}
  223. </Code>
  224. </TabPanel>
  225. ))}
  226. </TabPanels>
  227. </TabGroup>
  228. </div>
  229. );
  230. };
  231. const LANGUAGE_TO_DISPLAY_NAME = {
  232. [Language.Solidity]: "Solidity",
  233. [Language.EthersJSV6]: "ethers.js v6",
  234. };
  235. const LANUGAGE_TO_SHIKI_NAME: Record<Language, SupportedLanguage> = {
  236. [Language.Solidity]: "solidity",
  237. [Language.EthersJSV6]: "javascript",
  238. };
  239. type ExampleProps<ParameterName extends string> = {
  240. example: Example<ParameterName>;
  241. setParamValues: Dispatch<
  242. SetStateAction<Partial<Record<ParameterName, string>>>
  243. >;
  244. };
  245. const Example = <ParameterName extends string>({
  246. example,
  247. setParamValues,
  248. }: ExampleProps<ParameterName>) => {
  249. const config = useConfig();
  250. const [error, setError] = useState<string | undefined>(undefined);
  251. const [loading, setLoading] = useState(false);
  252. const updateValues = useCallback(() => {
  253. if (typeof example.parameters === "function") {
  254. setError(undefined);
  255. const address = getContractAddress(config.state.chainId);
  256. if (!address) {
  257. throw new Error(
  258. `No contract for chain id: ${config.state.chainId.toString()}`,
  259. );
  260. }
  261. const params = example.parameters({
  262. readContract: (functionName, args) =>
  263. readContract(config, { abi, address, functionName, args }),
  264. });
  265. if (params instanceof Promise) {
  266. setLoading(true);
  267. params
  268. .then((paramsResolved) => {
  269. setParamValues(paramsResolved);
  270. })
  271. .catch((error_: unknown) => {
  272. getLogger().error(error_);
  273. setError(
  274. "An error occurred while fetching data for this example, please try again",
  275. );
  276. })
  277. .finally(() => {
  278. setLoading(false);
  279. });
  280. } else {
  281. setParamValues(params);
  282. }
  283. } else {
  284. setParamValues(example.parameters);
  285. }
  286. }, [example, setParamValues, config]);
  287. const Icon = example.icon;
  288. return (
  289. <div className="flex flex-row items-center gap-2">
  290. <InlineLink
  291. as="button"
  292. onClick={updateValues}
  293. className="flex flex-row items-center gap-2"
  294. >
  295. {Icon && <Icon className="h-4" />}
  296. <span>{example.name}</span>
  297. </InlineLink>
  298. {error && <ErrorTooltip className="size-4">{error}</ErrorTooltip>}
  299. {loading && <ArrowPathIcon className="size-4 animate-spin" />}
  300. </div>
  301. );
  302. };