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;