2021-12-05 05:23:21 +00:00
|
|
|
import * as fs from "fs";
|
|
|
|
import * as path from "path";
|
|
|
|
|
|
|
|
const forbiddenStrings: any = ["selfdestruct"];
|
|
|
|
|
2021-12-05 22:18:23 +00:00
|
|
|
const getConnectorsList= async (connectorsRootsDirs: string | any[]): Promise<Record<string, any>> => {
|
2021-12-05 05:23:21 +00:00
|
|
|
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 = [];
|
|
|
|
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) => {
|
|
|
|
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 = [];
|
|
|
|
let mainEvents = [];
|
|
|
|
let firstLine: number;
|
|
|
|
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) => {
|
|
|
|
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) => 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;
|