Skip to content

Commit 4778966

Browse files
committed
CORE-458 | Improve the function comment of the Explorer (#5861)
CORE-458
1 parent 810f319 commit 4778966

File tree

3 files changed

+153
-52
lines changed

3 files changed

+153
-52
lines changed
Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Badge } from "@/components/ui/badge";
22
import { CodeClient } from "@/components/ui/code/code.client";
3-
import { useContractSources } from "contract-ui/hooks/useContractSources";
4-
import { useMemo } from "react";
3+
import { useContractFunctionComment } from "contract-ui/hooks/useContractFunctionComment";
54
import type { ThirdwebContract } from "thirdweb";
65

76
/**
@@ -11,66 +10,24 @@ export default function ContractFunctionComment({
1110
contract,
1211
functionName,
1312
}: { contract: ThirdwebContract; functionName: string }) {
14-
const sourceQuery = useContractSources(contract);
15-
const comment = useMemo(() => {
16-
if (!sourceQuery.data?.length) {
17-
return null;
18-
}
19-
const file = sourceQuery.data.find((item) =>
20-
item.source.includes(functionName),
21-
);
22-
if (!file) {
23-
return null;
24-
}
25-
return extractFunctionComment(file.source, functionName);
26-
}, [sourceQuery.data, functionName]);
13+
const query = useContractFunctionComment(contract, functionName);
2714

28-
if (sourceQuery.isLoading) {
15+
if (query.isLoading) {
2916
return null;
3017
}
31-
if (!comment) {
18+
if (!query.data) {
3219
return null;
3320
}
3421
return (
3522
<>
3623
<p className="mt-6">
3724
About this function <Badge>Beta</Badge>
3825
</p>
39-
<CodeClient lang="solidity" code={comment} />
26+
<CodeClient
27+
lang="wikitext"
28+
code={query.data}
29+
copyButtonClassName="hidden"
30+
/>
4031
</>
4132
);
4233
}
43-
44-
function extractFunctionComment(
45-
// Tthe whole code from the solidity file containing (possibly) the function
46-
solidityCode: string,
47-
functionName: string,
48-
): string | null {
49-
// Regular expression to match function declarations and their preceding comments
50-
// This regex now captures both single-line (//) and multi-line (/** */) comments
51-
const functionRegex =
52-
/(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g;
53-
54-
while (true) {
55-
const match = functionRegex.exec(solidityCode);
56-
if (match === null) {
57-
return null;
58-
}
59-
const [fullMatch, name] = match;
60-
if (!fullMatch || !fullMatch.length) {
61-
return null;
62-
}
63-
if (name === functionName) {
64-
// Extract the comment part
65-
const comment = (fullMatch.split("function")[0] || "").trim();
66-
if (!comment) {
67-
return null;
68-
}
69-
70-
if (/^[^a-zA-Z0-9]+$/.test(comment)) {
71-
return null;
72-
}
73-
return comment;
74-
}
75-
}
76-
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { useThirdwebClient } from "@/constants/thirdweb.client";
2+
import { useQuery } from "@tanstack/react-query";
3+
import type { ThirdwebContract } from "thirdweb";
4+
import { getCompilerMetadata } from "thirdweb/contract";
5+
import { download } from "thirdweb/storage";
6+
7+
/**
8+
* Try to extract the description (or comment) about a contract's method from our contract metadata endpoint
9+
*
10+
* An example of a contract that has both userdoc and devdoc:
11+
* https://contract.thirdweb.com/metadata/1/0x303a465B659cBB0ab36eE643eA362c509EEb5213
12+
*/
13+
export function useContractFunctionComment(
14+
contract: ThirdwebContract,
15+
functionName: string,
16+
) {
17+
const client = useThirdwebClient();
18+
return useQuery({
19+
queryKey: [
20+
"contract-function-comment",
21+
contract?.chain.id || "",
22+
contract?.address || "",
23+
functionName,
24+
],
25+
queryFn: async (): Promise<string> => {
26+
const data = await getCompilerMetadata(contract);
27+
let comment = "";
28+
/**
29+
* If the response data contains userdoc and/or devdoc
30+
* we always prioritize using them. parsing the comment using regex should
31+
* always be the last resort
32+
*/
33+
if (data.metadata.output.devdoc?.methods) {
34+
const keys = Object.keys(data.metadata.output.devdoc.methods);
35+
const matchingKey = keys.find(
36+
(rawKey) =>
37+
rawKey.startsWith(functionName) &&
38+
rawKey.split("(")[0] === functionName,
39+
);
40+
const devDocContent = matchingKey
41+
? data.metadata.output.devdoc.methods[matchingKey]?.details
42+
: undefined;
43+
if (devDocContent) {
44+
comment += `@dev-doc: ${devDocContent}\n`;
45+
}
46+
}
47+
if (data.metadata.output.userdoc?.methods) {
48+
const keys = Object.keys(data.metadata.output.userdoc.methods);
49+
const matchingKey = keys.find(
50+
(rawKey) =>
51+
rawKey.startsWith(functionName) &&
52+
rawKey.split("(")[0] === functionName,
53+
);
54+
const userDocContent = matchingKey
55+
? data.metadata.output.userdoc.methods[matchingKey]?.notice
56+
: undefined;
57+
if (userDocContent) {
58+
comment += `@user-doc: ${userDocContent}\n`;
59+
}
60+
}
61+
if (comment) {
62+
return comment;
63+
}
64+
if (!data.metadata.sources) {
65+
return "";
66+
}
67+
const sources = await Promise.all(
68+
Object.entries(data.metadata.sources).map(async ([path, info]) => {
69+
if ("content" in info) {
70+
return {
71+
filename: path,
72+
source: info.content || "Could not find source for this file",
73+
};
74+
}
75+
const urls = info.urls;
76+
const ipfsLink = urls
77+
? urls.find((url) => url.includes("ipfs"))
78+
: undefined;
79+
if (ipfsLink) {
80+
const ipfsHash = ipfsLink.split("ipfs/")[1];
81+
const source = await download({
82+
uri: `ipfs://${ipfsHash}`,
83+
client,
84+
})
85+
.then((r) => r.text())
86+
.catch(() => "Failed to fetch source from IPFS");
87+
return {
88+
filename: path,
89+
source,
90+
};
91+
}
92+
return {
93+
filename: path,
94+
source: "Could not find source for this file",
95+
};
96+
}),
97+
);
98+
const file = sources.find((item) => item.source.includes(functionName));
99+
if (!file) {
100+
return "";
101+
}
102+
return extractFunctionComment(file.source, functionName);
103+
},
104+
});
105+
}
106+
107+
function extractFunctionComment(
108+
// The whole code from the solidity file containing (possibly) the function
109+
solidityCode: string,
110+
functionName: string,
111+
): string {
112+
// Regular expression to match function declarations and their preceding comments
113+
// This regex now captures both single-line (//) and multi-line (/** */) comments
114+
const functionRegex =
115+
/(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g;
116+
117+
while (true) {
118+
const match = functionRegex.exec(solidityCode);
119+
if (match === null) {
120+
return "";
121+
}
122+
const [fullMatch, name] = match;
123+
if (!fullMatch || !fullMatch.length) {
124+
return "";
125+
}
126+
if (name === functionName) {
127+
// Extract the comment part
128+
const comment = (fullMatch.split("function")[0] || "").trim();
129+
if (!comment) {
130+
return "";
131+
}
132+
133+
if (/^[^a-zA-Z0-9]+$/.test(comment)) {
134+
return "";
135+
}
136+
return comment;
137+
}
138+
}
139+
}

packages/thirdweb/src/contract/actions/compiler-metadata.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export type CompilerMetadata = {
99
// 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
1010
metadata: Record<string, any> & {
1111
sources: Record<string, { content: string } | { urls: string[] }>;
12+
output: {
13+
abi: Abi;
14+
devdoc?: Record<string, Record<string, { details: string }>>;
15+
userdoc?: Record<string, Record<string, { notice: string }>>;
16+
};
1217
};
1318
info: {
1419
title?: string;

0 commit comments

Comments
 (0)