import * as fs from "fs"; import * as path from "path"; const forbiddenStrings: any = ["selfdestruct"]; const getConnectorsList= async (connectorsRootsDirs: string | any[]): Promise> => { try { const connectors = []; for (let index = 0; index < connectorsRootsDirs.length; index++) { const dirs = [connectorsRootsDirs[index]]; while (dirs.length) { const currentDir = dirs.pop(); const subs = fs.readdirSync(currentDir, { withFileTypes: true }); for (let index = 0; index < subs.length; index++) { const sub = subs[index]; if (sub.isFile() && sub.name === "main.sol") { connectors.push(currentDir); } else if (sub.isDirectory()) { dirs.push(`${currentDir}/${sub.name}`); } } } } return connectors.map((dir) => ({ path: dir })); } catch (error) { return Promise.reject(error); } }; const checkCodeForbidden = async (code: string, codePath: string) => { try { const forbidden = []; for (let i1 = 0; i1 < forbiddenStrings.length; i1++) { const forbiddenStr = forbiddenStrings[i1]; const strs = code.split("\n"); for (let i2 = 0; i2 < strs.length; i2++) { if (strs[i2].includes(forbiddenStr)) { forbidden.push(`found '${forbiddenStr}' in ${codePath}:${i2 + 1}`); } } } return forbidden; } catch (error) { return Promise.reject(error); } }; const checkForbidden = async (parentPath: string, codePath = "./main.sol") => { try { if (codePath.startsWith("@")) { codePath = path.resolve("node_modules", `./${codePath}`); } else { codePath = path.resolve(parentPath, codePath); } const code = fs.readFileSync(codePath, { encoding: "utf8" }); const forbidden: any = await checkCodeForbidden(code, codePath); if (code.includes("import")) { const importsPathes = code .split("\n") .filter( (str) => str.includes("import") && str.includes("from") && str.includes(".sol") ) .map((str) => str.split("from")[1].replace(/["; ]/gi, "")); for (let index = 0; index < importsPathes.length; index++) { const forbiddenErrors = await checkForbidden( path.parse(codePath).dir, importsPathes[index] ); forbidden.push(...forbiddenErrors); } } return codePath.endsWith("main.sol") ? { forbiddenErrors: forbidden, code } : forbidden; } catch (error) { return Promise.reject(error); } }; const checkEvents = async (connector: { path: any; events?: any; mainEvents?: any; mainEventsLines?: any; }) => { try { const errors = []; const warnings = []; const eventsPath = `${connector.path}/events.sol`; const mainPath = `${connector.path}/main.sol`; if (connector.events.length) { const eventNames:string[] = []; for (let i1 = 0; i1 < connector.mainEvents.length; i1++) { const mainEvent = connector.mainEvents[i1]; const name = mainEvent.split("(")[0]; eventNames.push(name); const event = connector.events.find( (e: string) => e.split("(")[0].split(" ")[1] === name ); if (event) { const mainEventArgs = mainEvent .split("(")[1] .split(")")[0] .split(",") .map((a: string) => a.trim()); const eventArgs = event .split("(")[1] .split(")")[0] .split(",") .map((a: string) => a.trim()); if (mainEventArgs.length !== eventArgs.length) { errors.push( `arguments amount don't match for ${name} at ${mainPath}:${connector.mainEventsLines[i1]}` ); continue; } for (let i2 = 0; i2 < mainEventArgs.length; i2++) { if (!mainEventArgs[i2].startsWith(eventArgs[i2].split(" ")[0])) { errors.push( `invalid argument #${i2 + 1} for ${name} at ${mainPath}:${ connector.mainEventsLines[i1] }` ); } } } else { errors.push(`event ${name} missing at ${eventsPath}`); } } if (connector.mainEvents.length < connector.events.length) { const deprecatedEvents = connector.events.filter((e: string) => { let used = false; for (let index = 0; index < eventNames.length; index++) { if (e.split("(")[0].split(" ")[1] === eventNames[index]) used = true; } return !used; }); warnings.push( `${deprecatedEvents .map((e: string) => e.split("(")[0].split(" ")[1]) .join(", ")} event(s) not used at ${connector.path}/main.sol` ); } } else { warnings.push(`missing events file for ${connector.path}/main.sol`); } return { eventsErrors: errors, eventsWarnings: warnings }; } catch (error) { return Promise.reject(error); } }; const getCommments = async (strs: string | any[]) => { try { const comments = []; let type: string = ''; for (let index = strs.length - 1; index >= 0; index--) { const str = strs[index]; if (!type) { if (str.trim().startsWith("//")) { type = "single"; } else if (str.trim().startsWith("*/")) { type = "multiple"; } } if (type === "single" && str.trim().startsWith("//")) { comments.push(str.replace(/[/]/gi, "").trim()); } else if ( type === "multiple" && !str.trim().startsWith("/**") && !str.trim().startsWith("*/") ) { comments.push(str.replace(/[*]/gi, "").trim()); } else if (type === "single" && !str.trim().startsWith("//")) { break; } else if (type === "multiple" && str.trim().startsWith("/**")) { break; } } return comments; } catch (error) { return Promise.reject(error); } }; const parseCode = async (connector: { path: any; code?: any }) => { try { const strs = connector.code.split("\n"); const events = []; const eventsFirstLines = []; let func = []; let funcs = []; let event: string[] = []; let mainEvents: string[] = []; let firstLine: number = -1; let mainEventsLines = []; for (let index = 0; index < strs.length; index++) { const str = strs[index]; if (str.includes("function") && !str.trim().startsWith("//")) { func = [str]; firstLine = index + 1; } else if (func.length && !str.trim().startsWith("//")) { func.push(str); } if (func.length && str.startsWith(`${func[0].split("function")[0]}}`)) { funcs.push({ raw: func.map((str) => str.trim()).join(" "), comments: await getCommments(strs.slice(0, firstLine)), firstLine, }); func = []; } } const allPublicFuncs = funcs .filter(({ raw }) => { return raw.includes("external") || raw.includes("public"); }) .map((f) => { const name = f.raw .split("(")[0] .split("function")[1] .trim(); return { ...f, name, }; }); funcs = allPublicFuncs .filter(({ raw }) => { if (raw.includes("returns")) { const returns = raw .split("returns")[1] .split("(")[1] .split(")")[0]; return returns.includes("string") && returns.includes("bytes"); } return false; }) .map((f) => { const args = f.raw .split("(")[1] .split(")")[0] .split(",") .map((arg) => arg.trim()) .filter((arg) => arg !== ""); return { ...f, args, }; }); const eventsPath = `${connector.path}/events.sol`; if (fs.existsSync(eventsPath)) { mainEvents = funcs .map( ({ raw }) => raw .split("_eventName")[2] .trim() .split('"')[1] ) .filter((raw) => !!raw); mainEventsLines = mainEvents.map( (me) => strs.findIndex((str: string | any[]) => str.includes(me)) + 1 ); const eventsCode = fs.readFileSync(eventsPath, { encoding: "utf8" }); const eventsStrs = eventsCode.split("\n"); for (let index = 0; index < eventsStrs.length; index++) { const str = eventsStrs[index]; if (str.includes("event")) { event = [str]; firstLine = index + 1; } else if (event.length && !str.trim().startsWith("//")) { event.push(str); } if (event.length && str.includes(")")) { events.push(event.map((str) => str.trim()).join(" ")); eventsFirstLines.push(firstLine); event = []; } } } return { ...connector, events, eventsFirstLines, mainEvents, mainEventsLines, funcs, allPublicFuncs, }; } catch (error) { return Promise.reject(error); } }; const checkComments = async (connector:any) => { try { const errors = []; for (let i1 = 0; i1 < connector.funcs.length; i1++) { const func = connector.funcs[i1]; for (let i2 = 0; i2 < func.args.length; i2++) { const argName = func.args[i2].split(" ").pop(); if ( !func.comments.some( (comment: string) => comment.startsWith("@param") && comment.split(" ")[1] === argName ) ) { errors.push( `argument ${argName} has no @param for function ${func.name} at ${connector.path}/main.sol:${func.firstLine}` ); } } const reqs = ["@dev", "@notice"]; for (let i3 = 0; i3 < reqs.length; i3++) { if (!func.comments.some((comment: string) => comment.startsWith(reqs[i3]))) { errors.push( `no ${reqs[i3]} for function ${func.name} at ${connector.path}/main.sol:${func.firstLine}` ); } } } return errors; } catch (error) { return Promise.reject(error); } }; const checkPublicFuncs = async (connector: { path: any; allPublicFuncs?: any; }) => { try { const errors = []; for (let i1 = 0; i1 < connector.allPublicFuncs.length; i1++) { const { raw, firstLine, name } = connector.allPublicFuncs[i1]; if (!raw.includes("payable")) { errors.push( `public function ${name} is not payable at ${connector.path}/main.sol:${firstLine}` ); } } return errors; } catch (error) { return Promise.reject(error); } }; const checkName = async (connector: { path: any; code?: any }) => { try { const strs = connector.code.split("\n"); let haveName = false; for (let index = strs.length - 1; index > 0; index--) { const str = strs[index]; if ( str.includes("string") && str.includes("public") && str.includes("name = ") ) { haveName = true; } } return haveName ? [] : [`name variable missing in ${connector.path}/main.sol`]; } catch (error) { return Promise.reject(error); } }; const checkHeadComments = async (connector: { path: any; code?: any }) => { try { const errors = []; const strs = connector.code.split("\n"); let haveTitle = false; let haveDev = false; for (let index = 0; index < strs.length; index++) { if (!strs[index].includes("{")) { if (strs[index].includes("@title")) haveTitle = true; if (strs[index].includes("@dev")) haveDev = true; } else { break; } } if (!haveTitle) errors.push(`@title missing in ${connector.path}/main.sol`); if (!haveDev) errors.push(`@dev missing in ${connector.path}/main.sol`); return errors; } catch (error) { return Promise.reject(error); } }; async function checkMain() { try { const connectorsRootsDirsDefault = ["mainnet", "polygon"].map( (v) => `contracts/${v}/connectors` ); const customPathArg = process.argv.find((a) => a.startsWith("connector=")); const connectorsRootsDirs = customPathArg ? [customPathArg.slice(10)] : connectorsRootsDirsDefault; const errors = []; const warnings = []; const connectors = await getConnectorsList(connectorsRootsDirs); for (let index = 0; index < connectors.length; index++) { const { forbiddenErrors, code } = await checkForbidden( connectors[index].path ); connectors[index].code = code; connectors[index] = await parseCode(connectors[index]); const { eventsErrors, eventsWarnings } = await checkEvents( connectors[index] ); const commentsErrors = await checkComments(connectors[index]); const nameErrors = await checkName(connectors[index]); const headCommentsErrors = await checkHeadComments(connectors[index]); const publicFuncsErrors = await checkPublicFuncs(connectors[index]); errors.push(...forbiddenErrors); errors.push(...eventsErrors); errors.push(...commentsErrors); errors.push(...nameErrors); errors.push(...headCommentsErrors); errors.push(...publicFuncsErrors); warnings.push(...eventsWarnings); } if (errors.length) { console.log("\x1b[31m%s\x1b[0m", `Total errors: ${errors.length}`); errors.forEach((error) => console.log("\x1b[31m%s\x1b[0m", error)); } else { console.log("\x1b[32m%s\x1b[0m", "No Errors Found"); } if (warnings.length) { console.log("\x1b[33m%s\x1b[0m", `Total warnings: ${warnings.length}`); warnings.forEach((warning) => console.log("\x1b[33m%s\x1b[0m", warning)); } else { console.log("\x1b[32m%s\x1b[0m", "No Warnings Found"); } if (errors.length) return Promise.reject(errors.join("\n")); } catch (error) { console.error("check execution error:", error); } } export default checkMain;