From 487f15170926964c7111a69807a79f2d34853071 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Fri, 5 Dec 2025 11:41:25 +0200 Subject: [PATCH 1/2] add useContractReadOnChain --- src/hooks/useContractReadOnChain.ts | 205 ++++++++++++++++++++++++++++ src/index.ts | 6 + 2 files changed, 211 insertions(+) create mode 100644 src/hooks/useContractReadOnChain.ts diff --git a/src/hooks/useContractReadOnChain.ts b/src/hooks/useContractReadOnChain.ts new file mode 100644 index 0000000..2aaae55 --- /dev/null +++ b/src/hooks/useContractReadOnChain.ts @@ -0,0 +1,205 @@ +import { useMemo } from "react"; +import { usePublicClient } from "wagmi"; +import { createPublicClient, http } from "viem"; +import type { Address, Abi, PublicClient, Chain } from "viem"; +import { getPoolzContractInfo } from "../utils/getPoolzContractInfo"; +import type { + ContractName, + ContractReadFunctionName, +} from "../contracts/contractTypes"; +import type { + ContractReadSchemas, + ContractReturnTypes, +} from "../contracts/contractTypes"; +import { config } from "../wagmi"; + +export interface ReadOnChainOptions< + T extends ContractName, + F extends ContractReadFunctionName, +> { + chainId: number; + contractName: T; + functionName: F; + args?: ContractReadSchemas[T][F]; +} + +export interface MulticallOnChainOptions { + chainId: number; + contractName: T; + calls: Array<{ + functionName: ContractReadFunctionName; + args?: any[]; + }>; +} + +export type MulticallOnChainResult = + | { result: T; status: "success" } + | { error: Error; status: "failure" }; + +const clientCache = new Map(); + +const getPublicClient = (chainId: number): PublicClient => { + if (clientCache.has(chainId)) { + return clientCache.get(chainId)!; + } + + const chain = config.chains.find((c: Chain) => c.id === chainId); + if (!chain) { + throw new Error(`Unsupported chain ID: ${chainId}`); + } + + const client = createPublicClient({ + chain, + transport: http(), + }) as PublicClient; + + clientCache.set(chainId, client); + return client; +}; + +export const useContractReadOnChain = () => { + const currentPublicClient = usePublicClient(); + + const read = useMemo( + () => + async >( + options: ReadOnChainOptions, + ): Promise< + F extends keyof ContractReturnTypes[T] ? ContractReturnTypes[T][F] : any + > => { + const { chainId, contractName, functionName, args } = options; + + const { smcAddress, abi } = getPoolzContractInfo({ + chainId, + contractName, + }); + + if (!smcAddress || !abi) { + throw new Error( + `Contract ${contractName} not found on chain ${chainId}`, + ); + } + + const publicClient = getPublicClient(chainId); + + try { + const result = await publicClient.readContract({ + address: smcAddress as Address, + abi: abi as Abi, + functionName: functionName as string, + args: args as any[], + }); + + return result as any; + } catch (error) { + throw new Error( + `Failed to read ${functionName as string} from ${contractName} on chain ${chainId}: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ); + } + }, + [], + ); + + const multicall = useMemo( + () => + async ( + options: MulticallOnChainOptions, + ): Promise => { + const { chainId, contractName, calls } = options; + + const { smcAddress, abi } = getPoolzContractInfo({ + chainId, + contractName, + }); + + if (!smcAddress || !abi) { + throw new Error( + `Contract ${contractName} not found on chain ${chainId}`, + ); + } + + const publicClient = getPublicClient(chainId); + + try { + const contracts = calls.map((call) => ({ + address: smcAddress as Address, + abi: abi as Abi, + functionName: call.functionName as string, + args: call.args, + })); + + const results = await publicClient.multicall({ + contracts, + allowFailure: true, + }); + + return results.map((result) => { + if (result.status === "success") { + return { + result: result.result, + status: "success" as const, + }; + } + return { + error: new Error(result.error?.message || "Multicall failed"), + status: "failure" as const, + }; + }); + } catch (error) { + throw new Error( + `Multicall failed on chain ${chainId}: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + ); + } + }, + [], + ); + + const readWithFallback = useMemo( + () => + async >( + options: ReadOnChainOptions, + ): Promise< + F extends keyof ContractReturnTypes[T] ? ContractReturnTypes[T][F] : any + > => { + try { + return await read(options); + } catch (error) { + if (currentPublicClient) { + console.warn( + `Failed to read from chain ${options.chainId}, falling back to connected chain`, + error, + ); + + const { smcAddress, abi } = getPoolzContractInfo({ + contractName: options.contractName, + }); + + if (smcAddress && abi) { + const result = await currentPublicClient.readContract({ + address: smcAddress as Address, + abi: abi as Abi, + functionName: options.functionName as string, + args: options.args as any[], + }); + return result as any; + } + } + throw error; + } + }, + [currentPublicClient, read], + ); + + return useMemo( + () => ({ + read, + multicall, + readWithFallback, + }), + [read, multicall, readWithFallback], + ); +}; diff --git a/src/index.ts b/src/index.ts index da0f2de..2d44d66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,12 @@ export type { TokenBalance, NativeBalance } from "./contexts/BalanceContext"; export { useSidNameForAddress } from "./hooks/useSidNameForAddress"; export { useWalletConnection } from "./hooks/useWalletConnection"; export { useTheSiwe } from "./hooks/useTheSiwe"; +export { useContractReadOnChain } from "./hooks/useContractReadOnChain"; +export type { + ReadOnChainOptions, + MulticallOnChainOptions, + MulticallOnChainResult, +} from "./hooks/useContractReadOnChain"; export { contractsByChain } from "./contracts"; export type { From e110807710be3a98d6c90a435684dc0b3af84407 Mon Sep 17 00:00:00 2001 From: Andrew Dmytrenko Date: Fri, 5 Dec 2025 12:01:12 +0200 Subject: [PATCH 2/2] simplify --- src/hooks/useContractReadOnChain.ts | 117 +--------------------------- src/index.ts | 6 +- 2 files changed, 2 insertions(+), 121 deletions(-) diff --git a/src/hooks/useContractReadOnChain.ts b/src/hooks/useContractReadOnChain.ts index 2aaae55..3226944 100644 --- a/src/hooks/useContractReadOnChain.ts +++ b/src/hooks/useContractReadOnChain.ts @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import { usePublicClient } from "wagmi"; import { createPublicClient, http } from "viem"; import type { Address, Abi, PublicClient, Chain } from "viem"; import { getPoolzContractInfo } from "../utils/getPoolzContractInfo"; @@ -23,19 +22,6 @@ export interface ReadOnChainOptions< args?: ContractReadSchemas[T][F]; } -export interface MulticallOnChainOptions { - chainId: number; - contractName: T; - calls: Array<{ - functionName: ContractReadFunctionName; - args?: any[]; - }>; -} - -export type MulticallOnChainResult = - | { result: T; status: "success" } - | { error: Error; status: "failure" }; - const clientCache = new Map(); const getPublicClient = (chainId: number): PublicClient => { @@ -58,8 +44,6 @@ const getPublicClient = (chainId: number): PublicClient => { }; export const useContractReadOnChain = () => { - const currentPublicClient = usePublicClient(); - const read = useMemo( () => async >( @@ -102,104 +86,5 @@ export const useContractReadOnChain = () => { [], ); - const multicall = useMemo( - () => - async ( - options: MulticallOnChainOptions, - ): Promise => { - const { chainId, contractName, calls } = options; - - const { smcAddress, abi } = getPoolzContractInfo({ - chainId, - contractName, - }); - - if (!smcAddress || !abi) { - throw new Error( - `Contract ${contractName} not found on chain ${chainId}`, - ); - } - - const publicClient = getPublicClient(chainId); - - try { - const contracts = calls.map((call) => ({ - address: smcAddress as Address, - abi: abi as Abi, - functionName: call.functionName as string, - args: call.args, - })); - - const results = await publicClient.multicall({ - contracts, - allowFailure: true, - }); - - return results.map((result) => { - if (result.status === "success") { - return { - result: result.result, - status: "success" as const, - }; - } - return { - error: new Error(result.error?.message || "Multicall failed"), - status: "failure" as const, - }; - }); - } catch (error) { - throw new Error( - `Multicall failed on chain ${chainId}: ${ - error instanceof Error ? error.message : "Unknown error" - }`, - ); - } - }, - [], - ); - - const readWithFallback = useMemo( - () => - async >( - options: ReadOnChainOptions, - ): Promise< - F extends keyof ContractReturnTypes[T] ? ContractReturnTypes[T][F] : any - > => { - try { - return await read(options); - } catch (error) { - if (currentPublicClient) { - console.warn( - `Failed to read from chain ${options.chainId}, falling back to connected chain`, - error, - ); - - const { smcAddress, abi } = getPoolzContractInfo({ - contractName: options.contractName, - }); - - if (smcAddress && abi) { - const result = await currentPublicClient.readContract({ - address: smcAddress as Address, - abi: abi as Abi, - functionName: options.functionName as string, - args: options.args as any[], - }); - return result as any; - } - } - throw error; - } - }, - [currentPublicClient, read], - ); - - return useMemo( - () => ({ - read, - multicall, - readWithFallback, - }), - [read, multicall, readWithFallback], - ); + return { read }; }; diff --git a/src/index.ts b/src/index.ts index 2d44d66..67ccbbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -102,11 +102,7 @@ export { useSidNameForAddress } from "./hooks/useSidNameForAddress"; export { useWalletConnection } from "./hooks/useWalletConnection"; export { useTheSiwe } from "./hooks/useTheSiwe"; export { useContractReadOnChain } from "./hooks/useContractReadOnChain"; -export type { - ReadOnChainOptions, - MulticallOnChainOptions, - MulticallOnChainResult, -} from "./hooks/useContractReadOnChain"; +export type { ReadOnChainOptions } from "./hooks/useContractReadOnChain"; export { contractsByChain } from "./contracts"; export type {