| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- "use client";
- 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 { ConnectKitButton, Avatar } from "connectkit";
- import { useCallback, useMemo, useState } from "react";
- import { ContractFunctionExecutionError } from "viem";
- import { useAccount, useConfig } from "wagmi";
- import { readContract, simulateContract, writeContract } from "wagmi/actions";
- import { type Parameter, TRANSFORMS } from "./parameter";
- import { getContractAddress } from "../../evm-networks";
- import { useIsMounted } from "../../use-is-mounted";
- import { Button } from "../Button";
- import { Code } from "../Code";
- import { InlineLink } from "../InlineLink";
- const abi = [...PythAbi, ...PythErrorsAbi] as const;
- type RunButtonProps<ParameterName extends string> = (
- | Read
- | Write<ParameterName>
- ) & {
- functionName: (typeof PythAbi)[number]["name"];
- parameters: Parameter<ParameterName>[];
- paramValues: Partial<Record<ParameterName, string>>;
- };
- type Read = {
- type: EvmApiType.Read;
- valueParam?: undefined;
- };
- type Write<ParameterName extends string> = {
- type: EvmApiType.Write;
- valueParam: ParameterName;
- };
- export enum EvmApiType {
- Read,
- Write,
- }
- export const RunButton = <ParameterName extends string>(
- props: RunButtonProps<ParameterName>,
- ) => {
- const { isConnected } = useAccount();
- const isMounted = useIsMounted();
- const { status, run, disabled } = useRunButton(props);
- return (
- <>
- {props.type === EvmApiType.Write && (
- <ConnectKitButton.Custom>
- {({ show, isConnected, address, truncatedAddress, ensName }) => (
- <InlineLink
- as="button"
- onClick={show}
- className="mb-2 flex flex-row items-center justify-center gap-2"
- >
- {isConnected ? (
- <>
- <Avatar address={address} size={24} />
- <span>Wallet: {ensName ?? truncatedAddress}</span>
- </>
- ) : (
- "Connect Wallet to Run"
- )}
- </InlineLink>
- )}
- </ConnectKitButton.Custom>
- )}
- {(props.type === EvmApiType.Read || (isMounted && isConnected)) && (
- <Button
- disabled={disabled}
- loading={status.type === StatusType.Loading}
- className="mb-8 flex h-10 w-full flex-row items-center justify-center gap-2"
- onClick={run}
- >
- {status.type === StatusType.Loading ? (
- <ArrowPathIcon className="size-4 animate-spin" />
- ) : (
- "Run"
- )}
- </Button>
- )}
- {status.type === StatusType.Results && (
- <div>
- <h3 className="mb-2 text-lg font-bold">Results</h3>
- <Code language="json">{stringifyResponse(status.data)}</Code>
- </div>
- )}
- {status.type === StatusType.Error && (
- <div>
- <h3 className="mb-2 text-lg font-bold">Error</h3>
- <div className="relative overflow-hidden rounded-md bg-neutral-100/25 dark:bg-neutral-800">
- <div className="flex size-full overflow-auto px-6 py-4">
- <p className="font-mono text-sm font-medium text-red-600 dark:text-red-400">
- {showError(status.error)}
- </p>
- </div>
- </div>
- </div>
- )}
- </>
- );
- };
- const useRunButton = <ParameterName extends string>({
- functionName,
- parameters,
- paramValues,
- ...props
- }: RunButtonProps<ParameterName>) => {
- const config = useConfig();
- const [status, setStatus] = useState<Status>(None());
- const args = useMemo(() => {
- const allParams =
- props.type === EvmApiType.Write
- ? parameters.filter((parameter) => parameter.name !== props.valueParam)
- : parameters;
- const orderedParams = allParams.map(({ name, type }) => {
- const transform = TRANSFORMS[type];
- const value = paramValues[name];
- return transform && value ? transform(value) : value;
- });
- return orderedParams.every((value) => value !== undefined)
- ? orderedParams
- : undefined;
- }, [parameters, paramValues, props]);
- const value = useMemo(() => {
- if (props.type === EvmApiType.Write) {
- const value = paramValues[props.valueParam];
- return value ? BigInt(value) : undefined;
- } else {
- return;
- }
- }, [paramValues, props]);
- const run = useCallback(() => {
- setStatus(Loading());
- if (args === undefined) {
- setStatus(ErrorStatus(new Error("Invalid parameters!")));
- } else {
- const address = getContractAddress(config.state.chainId);
- if (!address) {
- throw new Error(
- `No contract for chain id: ${config.state.chainId.toString()}`,
- );
- }
- switch (props.type) {
- case EvmApiType.Read: {
- readContract(config, { abi, address, functionName, args })
- .then((result) => {
- setStatus(Results(result));
- })
- .catch((error: unknown) => {
- setStatus(ErrorStatus(error));
- });
- return;
- }
- case EvmApiType.Write: {
- if (value === undefined) {
- setStatus(ErrorStatus(new Error("Missing value!")));
- } else {
- simulateContract(config, {
- abi,
- address,
- functionName,
- args,
- value,
- })
- .then(({ request }) => writeContract(config, request))
- .then((result) => {
- setStatus(Results(result));
- })
- .catch((error: unknown) => {
- setStatus(ErrorStatus(error));
- });
- }
- return;
- }
- }
- }
- }, [config, functionName, setStatus, args, value, props.type]);
- const { isConnected } = useAccount();
- const disabled = useMemo(
- () =>
- args === undefined ||
- status.type === StatusType.Loading ||
- (props.type === EvmApiType.Write &&
- (!isConnected || value === undefined)),
- [args, status, props, isConnected, value],
- );
- return { status, run, disabled };
- };
- enum StatusType {
- None,
- Loading,
- Error,
- Results,
- }
- const None = () => ({ type: StatusType.None as const });
- const Loading = () => ({ type: StatusType.Loading as const });
- const ErrorStatus = (error: unknown) => ({
- type: StatusType.Error as const,
- error,
- });
- const Results = (data: unknown) => ({
- type: StatusType.Results as const,
- data,
- });
- type Status =
- | ReturnType<typeof None>
- | ReturnType<typeof Loading>
- | ReturnType<typeof ErrorStatus>
- | ReturnType<typeof Results>;
- const showError = (error: unknown): string => {
- if (typeof error === "string") {
- return error;
- } else if (error instanceof ContractFunctionExecutionError) {
- return error.cause.metaMessages?.[0] ?? error.message;
- } else if (error instanceof Error) {
- return error.toString();
- } else {
- return "An unknown error occurred";
- }
- };
- const stringifyResponse = (response: unknown): string => {
- switch (typeof response) {
- case "string": {
- return `"${response}"`;
- }
- case "number":
- case "boolean":
- case "function": {
- return response.toString();
- }
- case "bigint": {
- return `${response.toString()}n`;
- }
- case "symbol": {
- return `Symbol(${response.toString()})`;
- }
- case "undefined": {
- return "undefined";
- }
- case "object": {
- return response === null
- ? "null"
- : `{\n${Object.entries(response)
- .map(([key, value]) => ` ${key}: ${stringifyResponse(value)}`)
- .join(",\n")}\n}`;
- }
- }
- };
|