|
| 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 | +} |
0 commit comments