From c06ecda43ccc45ff04cd5643c4d7ae209c22a67d Mon Sep 17 00:00:00 2001 From: "Nicholas St. Germain" Date: Mon, 6 Jan 2025 20:12:47 -0600 Subject: [PATCH] [TOOL-2917]: fix useReadContract type hinting (#5888) --- packages/thirdweb/src/contract/types.ts | 8 + packages/thirdweb/src/extensions/types.ts | 6 + .../core/hooks/contract/useReadContract.ts | 157 +++++++++--------- .../thirdweb/src/react/core/hooks/types.ts | 13 ++ 4 files changed, 110 insertions(+), 74 deletions(-) create mode 100644 packages/thirdweb/src/contract/types.ts create mode 100644 packages/thirdweb/src/extensions/types.ts create mode 100644 packages/thirdweb/src/react/core/hooks/types.ts diff --git a/packages/thirdweb/src/contract/types.ts b/packages/thirdweb/src/contract/types.ts new file mode 100644 index 00000000000..e9fad509bae --- /dev/null +++ b/packages/thirdweb/src/contract/types.ts @@ -0,0 +1,8 @@ +import type { Abi, AbiFunction } from "abitype"; +import type { ThirdwebContract } from "./contract.js"; + +export type AbiOfLength = { length: TLength }; + +export type AsyncGetAbiFunctionFromContract = ( + contract: ThirdwebContract, +) => Promise; diff --git a/packages/thirdweb/src/extensions/types.ts b/packages/thirdweb/src/extensions/types.ts new file mode 100644 index 00000000000..6c51a7b5ae4 --- /dev/null +++ b/packages/thirdweb/src/extensions/types.ts @@ -0,0 +1,6 @@ +import type { Abi } from "abitype"; +import type { BaseTransactionOptions } from "../transaction/types.js"; + +export type Extension = ( + options: BaseTransactionOptions, +) => Promise; diff --git a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts index 91fab7227db..917b58c1582 100644 --- a/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts +++ b/packages/thirdweb/src/react/core/hooks/contract/useReadContract.ts @@ -4,7 +4,11 @@ import { useQuery, } from "@tanstack/react-query"; import type { Abi, AbiFunction, ExtractAbiFunctionNames } from "abitype"; -import type { ThirdwebContract } from "../../../../contract/contract.js"; +import type { + AbiOfLength, + AsyncGetAbiFunctionFromContract, +} from "../../../../contract/types.js"; +import type { Extension } from "../../../../extensions/types.js"; import { type ReadContractOptions, type ReadContractResult, @@ -17,17 +21,16 @@ import type { import type { PreparedMethod } from "../../../../utils/abi/prepare-method.js"; import { getFunctionId } from "../../../../utils/function-id.js"; import { stringify } from "../../../../utils/json.js"; - -type PickedQueryOptions = { - enabled?: boolean; - refetchInterval?: number; - retry?: number; -}; +import type { + PickedOnceQueryOptions, + WithPickedOnceQueryOptions, +} from "../types.js"; /** * A hook to read state from a contract that automatically updates when the contract changes. * - * You can use raw read calls or read [extensions](https://portal.thirdweb.com/react/v5/extensions) to read from a contract. + * You can use raw read calls or read [extensions](https://portal.thirdweb.com/react/v5/extensions) to read from a + * contract. * * @param options - The options for reading from a contract * @returns a UseQueryResult object. @@ -52,20 +55,19 @@ type PickedQueryOptions = { * @contract */ export function useReadContract< - const abi extends Abi, - const method extends abi extends { length: 0 } + const TAbi extends Abi, + const TMethod extends TAbi extends AbiOfLength<0> ? AbiFunction | string - : ExtractAbiFunctionNames, + : ExtractAbiFunctionNames, >( - options: ReadContractOptions & { - queryOptions?: PickedQueryOptions; - }, + options: WithPickedOnceQueryOptions>, ): UseQueryResult< - ReadContractResult>[2]> + ReadContractResult>[2]> >; /** * A hook to read state from a contract that automatically updates when the contract changes. - * You can use raw read calls or read [extensions](https://portal.thirdweb.com/react/v5/extensions) to read from a contract. + * You can use raw read calls or read [extensions](https://portal.thirdweb.com/react/v5/extensions) to read from a + * contract. * * @param extension - An extension to call. * @param options - The read extension params. @@ -82,38 +84,42 @@ export function useReadContract< * ``` */ export function useReadContract< - const abi extends Abi, - const params extends object, - result, + const TAbi extends Abi, + const TParams extends object, + TResult, >( - extension: (options: BaseTransactionOptions) => Promise, - options: BaseTransactionOptions & { - queryOptions?: PickedQueryOptions; - }, -): UseQueryResult; + extension: Extension, + options: WithPickedOnceQueryOptions>, +): UseQueryResult; export function useReadContract< - const abi extends Abi, - const method extends abi extends { - length: 0; - } - ? - | AbiFunction - | `function ${string}` - | ((contract: ThirdwebContract) => Promise) - : ExtractAbiFunctionNames, - const params extends object, - result, + const TAbi extends Abi, + const TMethod extends TAbi extends AbiOfLength<0> + ? AbiFunction | `function ${string}` | AsyncGetAbiFunctionFromContract + : ExtractAbiFunctionNames, + const TParams extends object, + TResult, >( extensionOrOptions: - | ((options: BaseTransactionOptions) => Promise) - | (ReadContractOptions & { - queryOptions?: PickedQueryOptions; - }), - options?: BaseTransactionOptions & { - queryOptions?: PickedQueryOptions; - }, + | Extension + | WithPickedOnceQueryOptions>, + options?: WithPickedOnceQueryOptions>, ) { + type QueryKey = readonly [ + "readContract", + number | string, + string, + string | PreparedMethod>, + string, + ]; + type QueryFn = () => Promise< + TResult | ReadContractResult>[2]> + >; + + let queryKey: QueryKey | undefined; + let queryFn: QueryFn | undefined; + let queryOpts: PickedOnceQueryOptions | undefined; + // extension case if (typeof extensionOrOptions === "function") { if (!options) { @@ -122,46 +128,49 @@ export function useReadContract< ) as never; } const { queryOptions, contract, ...params } = options; + queryOpts = queryOptions; - const query = defineQuery({ - queryKey: [ - "readContract", - contract.chain.id, - contract.address, - getFunctionId(extensionOrOptions), - stringify(params), - ] as const, - // @ts-expect-error - TODO: clean up the type issues here - queryFn: () => extensionOrOptions({ ...params, contract }), - ...queryOptions, - }); + queryKey = [ + "readContract", + contract.chain.id, + contract.address, + getFunctionId(extensionOrOptions), + stringify(params), + ] as const; - // TODO - FIX LATER - // biome-ignore lint/correctness/useHookAtTopLevel: - return useQuery(query); + queryFn = () => + extensionOrOptions({ + ...(params as TParams), + contract, + }); } // raw tx case if ("method" in extensionOrOptions) { const { queryOptions, ...tx } = extensionOrOptions; + queryOpts = queryOptions; - const query = defineQuery({ - queryKey: [ - "readContract", - tx.contract.chain.id, - tx.contract.address, - tx.method, - stringify(tx.params), - ] as const, - queryFn: () => readContract(extensionOrOptions), - ...queryOptions, - }); + queryKey = [ + "readContract", + tx.contract.chain.id, + tx.contract.address, + tx.method, + stringify(tx.params), + ] as const; + + queryFn = () => readContract(extensionOrOptions); + } - // TODO - FIX LATER - // biome-ignore lint/correctness/useHookAtTopLevel: - return useQuery(query); + if (!queryKey || !queryFn) { + throw new Error( + `Invalid "useReadContract" options. Expected either a read extension or a transaction object.`, + ) as never; } - throw new Error( - `Invalid "useReadContract" options. Expected either a read extension or a transaction object.`, - ) as never; + return useQuery( + defineQuery({ + queryKey: queryKey as QueryKey, + queryFn: queryFn as QueryFn, + ...(queryOpts ?? {}), + }), + ); } diff --git a/packages/thirdweb/src/react/core/hooks/types.ts b/packages/thirdweb/src/react/core/hooks/types.ts new file mode 100644 index 00000000000..a5971b1d0da --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/types.ts @@ -0,0 +1,13 @@ +import type { Prettify } from "../../../utils/type-utils.js"; + +type BasePickedQueryOptions = T & { + enabled?: boolean; +}; + +export type PickedOnceQueryOptions = Prettify< + BasePickedQueryOptions & { refetchInterval?: number; retry?: number } +>; + +export type WithPickedOnceQueryOptions = T & { + queryOptions?: PickedOnceQueryOptions; +};