diff --git a/apps/dashboard/src/components/contract-functions/contract-function-comment.tsx b/apps/dashboard/src/components/contract-functions/contract-function-comment.tsx index 818f9b23fd6..696f52e70a1 100644 --- a/apps/dashboard/src/components/contract-functions/contract-function-comment.tsx +++ b/apps/dashboard/src/components/contract-functions/contract-function-comment.tsx @@ -1,7 +1,6 @@ import { Badge } from "@/components/ui/badge"; import { CodeClient } from "@/components/ui/code/code.client"; -import { useContractSources } from "contract-ui/hooks/useContractSources"; -import { useMemo } from "react"; +import { useContractFunctionComment } from "contract-ui/hooks/useContractFunctionComment"; import type { ThirdwebContract } from "thirdweb"; /** @@ -11,24 +10,12 @@ export default function ContractFunctionComment({ contract, functionName, }: { contract: ThirdwebContract; functionName: string }) { - const sourceQuery = useContractSources(contract); - const comment = useMemo(() => { - if (!sourceQuery.data?.length) { - return null; - } - const file = sourceQuery.data.find((item) => - item.source.includes(functionName), - ); - if (!file) { - return null; - } - return extractFunctionComment(file.source, functionName); - }, [sourceQuery.data, functionName]); + const query = useContractFunctionComment(contract, functionName); - if (sourceQuery.isLoading) { + if (query.isLoading) { return null; } - if (!comment) { + if (!query.data) { return null; } return ( @@ -36,41 +23,11 @@ export default function ContractFunctionComment({

About this function Beta

- + ); } - -function extractFunctionComment( - // Tthe whole code from the solidity file containing (possibly) the function - solidityCode: string, - functionName: string, -): string | null { - // Regular expression to match function declarations and their preceding comments - // This regex now captures both single-line (//) and multi-line (/** */) comments - const functionRegex = - /(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g; - - while (true) { - const match = functionRegex.exec(solidityCode); - if (match === null) { - return null; - } - const [fullMatch, name] = match; - if (!fullMatch || !fullMatch.length) { - return null; - } - if (name === functionName) { - // Extract the comment part - const comment = (fullMatch.split("function")[0] || "").trim(); - if (!comment) { - return null; - } - - if (/^[^a-zA-Z0-9]+$/.test(comment)) { - return null; - } - return comment; - } - } -} diff --git a/apps/dashboard/src/contract-ui/hooks/useContractFunctionComment.ts b/apps/dashboard/src/contract-ui/hooks/useContractFunctionComment.ts new file mode 100644 index 00000000000..f5bb0777fd1 --- /dev/null +++ b/apps/dashboard/src/contract-ui/hooks/useContractFunctionComment.ts @@ -0,0 +1,139 @@ +import { useThirdwebClient } from "@/constants/thirdweb.client"; +import { useQuery } from "@tanstack/react-query"; +import type { ThirdwebContract } from "thirdweb"; +import { getCompilerMetadata } from "thirdweb/contract"; +import { download } from "thirdweb/storage"; + +/** + * Try to extract the description (or comment) about a contract's method from our contract metadata endpoint + * + * An example of a contract that has both userdoc and devdoc: + * https://contract.thirdweb.com/metadata/1/0x303a465B659cBB0ab36eE643eA362c509EEb5213 + */ +export function useContractFunctionComment( + contract: ThirdwebContract, + functionName: string, +) { + const client = useThirdwebClient(); + return useQuery({ + queryKey: [ + "contract-function-comment", + contract?.chain.id || "", + contract?.address || "", + functionName, + ], + queryFn: async (): Promise => { + const data = await getCompilerMetadata(contract); + let comment = ""; + /** + * If the response data contains userdoc and/or devdoc + * we always prioritize using them. parsing the comment using regex should + * always be the last resort + */ + if (data.metadata.output.devdoc?.methods) { + const keys = Object.keys(data.metadata.output.devdoc.methods); + const matchingKey = keys.find( + (rawKey) => + rawKey.startsWith(functionName) && + rawKey.split("(")[0] === functionName, + ); + const devDocContent = matchingKey + ? data.metadata.output.devdoc.methods[matchingKey]?.details + : undefined; + if (devDocContent) { + comment += `@dev-doc: ${devDocContent}\n`; + } + } + if (data.metadata.output.userdoc?.methods) { + const keys = Object.keys(data.metadata.output.userdoc.methods); + const matchingKey = keys.find( + (rawKey) => + rawKey.startsWith(functionName) && + rawKey.split("(")[0] === functionName, + ); + const userDocContent = matchingKey + ? data.metadata.output.userdoc.methods[matchingKey]?.notice + : undefined; + if (userDocContent) { + comment += `@user-doc: ${userDocContent}\n`; + } + } + if (comment) { + return comment; + } + if (!data.metadata.sources) { + return ""; + } + const sources = await Promise.all( + Object.entries(data.metadata.sources).map(async ([path, info]) => { + if ("content" in info) { + return { + filename: path, + source: info.content || "Could not find source for this file", + }; + } + const urls = info.urls; + const ipfsLink = urls + ? urls.find((url) => url.includes("ipfs")) + : undefined; + if (ipfsLink) { + const ipfsHash = ipfsLink.split("ipfs/")[1]; + const source = await download({ + uri: `ipfs://${ipfsHash}`, + client, + }) + .then((r) => r.text()) + .catch(() => "Failed to fetch source from IPFS"); + return { + filename: path, + source, + }; + } + return { + filename: path, + source: "Could not find source for this file", + }; + }), + ); + const file = sources.find((item) => item.source.includes(functionName)); + if (!file) { + return ""; + } + return extractFunctionComment(file.source, functionName); + }, + }); +} + +function extractFunctionComment( + // The whole code from the solidity file containing (possibly) the function + solidityCode: string, + functionName: string, +): string { + // Regular expression to match function declarations and their preceding comments + // This regex now captures both single-line (//) and multi-line (/** */) comments + const functionRegex = + /(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g; + + while (true) { + const match = functionRegex.exec(solidityCode); + if (match === null) { + return ""; + } + const [fullMatch, name] = match; + if (!fullMatch || !fullMatch.length) { + return ""; + } + if (name === functionName) { + // Extract the comment part + const comment = (fullMatch.split("function")[0] || "").trim(); + if (!comment) { + return ""; + } + + if (/^[^a-zA-Z0-9]+$/.test(comment)) { + return ""; + } + return comment; + } + } +} diff --git a/packages/thirdweb/src/contract/actions/compiler-metadata.ts b/packages/thirdweb/src/contract/actions/compiler-metadata.ts index 941e9ecba64..ed15561a7e5 100644 --- a/packages/thirdweb/src/contract/actions/compiler-metadata.ts +++ b/packages/thirdweb/src/contract/actions/compiler-metadata.ts @@ -9,6 +9,11 @@ export type CompilerMetadata = { // biome-ignore lint/suspicious/noExplicitAny: TODO: fix later by updating this type to match the specs here: https://docs.soliditylang.org/en/latest/metadata.html metadata: Record & { sources: Record; + output: { + abi: Abi; + devdoc?: Record>; + userdoc?: Record>; + }; }; info: { title?: string;