run-button.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. "use client";
  2. import { ArrowPathIcon } from "@heroicons/react/24/outline";
  3. import PythAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
  4. import PythErrorsAbi from "@pythnetwork/pyth-sdk-solidity/abis/PythErrors.json";
  5. import { ConnectKitButton, Avatar } from "connectkit";
  6. import { useCallback, useMemo, useState } from "react";
  7. import { ContractFunctionExecutionError } from "viem";
  8. import { useAccount, useConfig } from "wagmi";
  9. import { readContract, simulateContract, writeContract } from "wagmi/actions";
  10. import { type Parameter, TRANSFORMS } from "./parameter";
  11. import { getContractAddress } from "../../evm-networks";
  12. import { useIsMounted } from "../../use-is-mounted";
  13. import { Button } from "../Button";
  14. import { Code } from "../Code";
  15. import { InlineLink } from "../InlineLink";
  16. const abi = [...PythAbi, ...PythErrorsAbi] as const;
  17. type RunButtonProps<ParameterName extends string> = (
  18. | Read
  19. | Write<ParameterName>
  20. ) & {
  21. functionName: (typeof PythAbi)[number]["name"];
  22. parameters: Parameter<ParameterName>[];
  23. paramValues: Partial<Record<ParameterName, string>>;
  24. };
  25. type Read = {
  26. type: EvmApiType.Read;
  27. valueParam?: undefined;
  28. };
  29. type Write<ParameterName extends string> = {
  30. type: EvmApiType.Write;
  31. valueParam: ParameterName;
  32. };
  33. export enum EvmApiType {
  34. Read,
  35. Write,
  36. }
  37. export const RunButton = <ParameterName extends string>(
  38. props: RunButtonProps<ParameterName>,
  39. ) => {
  40. const { isConnected } = useAccount();
  41. const isMounted = useIsMounted();
  42. const { status, run, disabled } = useRunButton(props);
  43. return (
  44. <>
  45. {props.type === EvmApiType.Write && (
  46. <ConnectKitButton.Custom>
  47. {({ show, isConnected, address, truncatedAddress, ensName }) => (
  48. <InlineLink
  49. as="button"
  50. onClick={show}
  51. className="mb-2 flex flex-row items-center justify-center gap-2"
  52. >
  53. {isConnected ? (
  54. <>
  55. <Avatar address={address} size={24} />
  56. <span>Wallet: {ensName ?? truncatedAddress}</span>
  57. </>
  58. ) : (
  59. "Connect Wallet to Run"
  60. )}
  61. </InlineLink>
  62. )}
  63. </ConnectKitButton.Custom>
  64. )}
  65. {(props.type === EvmApiType.Read || (isMounted && isConnected)) && (
  66. <Button
  67. disabled={disabled}
  68. loading={status.type === StatusType.Loading}
  69. className="mb-8 flex h-10 w-full flex-row items-center justify-center gap-2"
  70. onClick={run}
  71. >
  72. {status.type === StatusType.Loading ? (
  73. <ArrowPathIcon className="size-4 animate-spin" />
  74. ) : (
  75. "Run"
  76. )}
  77. </Button>
  78. )}
  79. {status.type === StatusType.Results && (
  80. <div>
  81. <h3 className="mb-2 text-lg font-bold">Results</h3>
  82. <Code language="json">{stringifyResponse(status.data)}</Code>
  83. </div>
  84. )}
  85. {status.type === StatusType.Error && (
  86. <div>
  87. <h3 className="mb-2 text-lg font-bold">Error</h3>
  88. <div className="relative overflow-hidden rounded-md bg-neutral-100/25 dark:bg-neutral-800">
  89. <div className="flex size-full overflow-auto px-6 py-4">
  90. <p className="font-mono text-sm font-medium text-red-600 dark:text-red-400">
  91. {showError(status.error)}
  92. </p>
  93. </div>
  94. </div>
  95. </div>
  96. )}
  97. </>
  98. );
  99. };
  100. const useRunButton = <ParameterName extends string>({
  101. functionName,
  102. parameters,
  103. paramValues,
  104. ...props
  105. }: RunButtonProps<ParameterName>) => {
  106. const config = useConfig();
  107. const [status, setStatus] = useState<Status>(None());
  108. const args = useMemo(() => {
  109. const allParams =
  110. props.type === EvmApiType.Write
  111. ? parameters.filter((parameter) => parameter.name !== props.valueParam)
  112. : parameters;
  113. const orderedParams = allParams.map(({ name, type }) => {
  114. const transform = TRANSFORMS[type];
  115. const value = paramValues[name];
  116. return transform && value ? transform(value) : value;
  117. });
  118. return orderedParams.every((value) => value !== undefined)
  119. ? orderedParams
  120. : undefined;
  121. }, [parameters, paramValues, props]);
  122. const value = useMemo(() => {
  123. if (props.type === EvmApiType.Write) {
  124. const value = paramValues[props.valueParam];
  125. return value ? BigInt(value) : undefined;
  126. } else {
  127. return;
  128. }
  129. }, [paramValues, props]);
  130. const run = useCallback(() => {
  131. setStatus(Loading());
  132. if (args === undefined) {
  133. setStatus(ErrorStatus(new Error("Invalid parameters!")));
  134. } else {
  135. const address = getContractAddress(config.state.chainId);
  136. if (!address) {
  137. throw new Error(
  138. `No contract for chain id: ${config.state.chainId.toString()}`,
  139. );
  140. }
  141. switch (props.type) {
  142. case EvmApiType.Read: {
  143. readContract(config, { abi, address, functionName, args })
  144. .then((result) => {
  145. setStatus(Results(result));
  146. })
  147. .catch((error: unknown) => {
  148. setStatus(ErrorStatus(error));
  149. });
  150. return;
  151. }
  152. case EvmApiType.Write: {
  153. if (value === undefined) {
  154. setStatus(ErrorStatus(new Error("Missing value!")));
  155. } else {
  156. simulateContract(config, {
  157. abi,
  158. address,
  159. functionName,
  160. args,
  161. value,
  162. })
  163. .then(({ request }) => writeContract(config, request))
  164. .then((result) => {
  165. setStatus(Results(result));
  166. })
  167. .catch((error: unknown) => {
  168. setStatus(ErrorStatus(error));
  169. });
  170. }
  171. return;
  172. }
  173. }
  174. }
  175. }, [config, functionName, setStatus, args, value, props.type]);
  176. const { isConnected } = useAccount();
  177. const disabled = useMemo(
  178. () =>
  179. args === undefined ||
  180. status.type === StatusType.Loading ||
  181. (props.type === EvmApiType.Write &&
  182. (!isConnected || value === undefined)),
  183. [args, status, props, isConnected, value],
  184. );
  185. return { status, run, disabled };
  186. };
  187. enum StatusType {
  188. None,
  189. Loading,
  190. Error,
  191. Results,
  192. }
  193. const None = () => ({ type: StatusType.None as const });
  194. const Loading = () => ({ type: StatusType.Loading as const });
  195. const ErrorStatus = (error: unknown) => ({
  196. type: StatusType.Error as const,
  197. error,
  198. });
  199. const Results = (data: unknown) => ({
  200. type: StatusType.Results as const,
  201. data,
  202. });
  203. type Status =
  204. | ReturnType<typeof None>
  205. | ReturnType<typeof Loading>
  206. | ReturnType<typeof ErrorStatus>
  207. | ReturnType<typeof Results>;
  208. const showError = (error: unknown): string => {
  209. if (typeof error === "string") {
  210. return error;
  211. } else if (error instanceof ContractFunctionExecutionError) {
  212. return error.cause.metaMessages?.[0] ?? error.message;
  213. } else if (error instanceof Error) {
  214. return error.toString();
  215. } else {
  216. return "An unknown error occurred";
  217. }
  218. };
  219. const stringifyResponse = (response: unknown): string => {
  220. switch (typeof response) {
  221. case "string": {
  222. return `"${response}"`;
  223. }
  224. case "number":
  225. case "boolean":
  226. case "function": {
  227. return response.toString();
  228. }
  229. case "bigint": {
  230. return `${response.toString()}n`;
  231. }
  232. case "symbol": {
  233. return `Symbol(${response.toString()})`;
  234. }
  235. case "undefined": {
  236. return "undefined";
  237. }
  238. case "object": {
  239. return response === null
  240. ? "null"
  241. : `{\n${Object.entries(response)
  242. .map(([key, value]) => ` ${key}: ${stringifyResponse(value)}`)
  243. .join(",\n")}\n}`;
  244. }
  245. }
  246. };