UNPKG

locklift

Version:

Node JS framework for working with Ever contracts. Inspired by Truffle and Hardhat. Helps you to build, test, run and maintain your smart contracts.

336 lines (318 loc) 12.2 kB
import fs from "fs"; import { DirectoryTree } from "directory-tree"; import { ExternalContracts, LockliftConfig } from "../../config"; import { BuilderConfig } from "./index"; import { ComponentType } from "../../compilerComponentsStore/constants"; import { getComponent } from "../../compilerComponentsStore"; import * as Buffer from "buffer"; import { ExecErrorOutput } from "./types"; import { exec, execSync, ExecSyncOptions, ExecSyncOptionsWithBufferEncoding, ExecSyncOptionsWithStringEncoding, } from "child_process"; import path, { parse, resolve } from "path"; import { promisify } from "util"; import { logger } from "../../logger"; import { catchError, concat, defer, filter, from, lastValueFrom, map, mergeMap, tap, throwError, toArray } from "rxjs"; import semver from "semver/preload"; export function checkDirEmpty(dir: fs.PathLike): fs.PathLike | boolean { if (!fs.existsSync(dir)) { return dir; } return fs.readdirSync(dir).length === 0; } export function flatDirTree(tree: DirectoryTree): DirectoryTree[] | undefined { return tree.children?.reduce((acc: DirectoryTree[], current: DirectoryTree) => { if (current.children === undefined) { return [...acc, current]; } const flatChild = flatDirTree(current); if (!flatChild) return acc; return [...acc, ...flatChild]; }, []); } export const compilerConfigResolver = async ({ compiler, linker, }: Pick<LockliftConfig, "compiler" | "linker">): Promise<BuilderConfig> => { const builderConfig: BuilderConfig = { includesPath: compiler.includesPath, externalContracts: compiler.externalContracts, compilerParams: compiler.compilerParams, externalContractsArtifacts: compiler.externalContractsArtifacts, } as BuilderConfig; if ("path" in compiler) { builderConfig.compilerPath = compiler.path; } if ("version" in compiler) { if (semver.lte(compiler.version, "0.71.0")) { builderConfig.compilerPath = await getComponent({ component: ComponentType.COMPILER, version: compiler.version, }); builderConfig.mode = "solc"; } if (semver.gte(compiler.version, "0.72.0") && semver.lt(compiler.version, "0.77.0")) { builderConfig.compilerPath = await getComponent({ component: ComponentType.SOLD_COMPILER, version: compiler.version, }); builderConfig.mode = "sold"; } if (semver.gte(compiler.version, "0.77.0")) { builderConfig.compilerPath = await getComponent({ component: ComponentType.SOLD_COMPILER_TYCHO, version: compiler.version, }); builderConfig.mode = "sold"; } } if (linker && "path" in linker) { builderConfig.linkerPath = linker.path; builderConfig.linkerLibPath = linker.lib; } if (linker && "version" in linker) { if (!("version" in compiler)) { throw new Error("You can't provide linker version without compiler version!"); } if (semver.gte(compiler.version, "0.77.0")) { logger.printWarn("Linker for version 0.77.0 and grater is deprecated."); return builderConfig; } builderConfig.linkerPath = await getComponent({ version: linker.version, component: ComponentType.LINKER, }); builderConfig.linkerLibPath = await getComponent({ version: compiler.version, component: ComponentType.LIB, }); } return builderConfig; }; export const tvcToBase64 = (tvc: Buffer) => tvc.toString("base64"); export const extractContractName = (pathToFile: string): string => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion pathToFile.match(new RegExp("contracts(.*).sol"))![1].slice(1); export function execSyncWrapper(command: string): Buffer; export function execSyncWrapper(command: string, options: ExecSyncOptionsWithStringEncoding): string; export function execSyncWrapper(command: string, options: ExecSyncOptionsWithBufferEncoding): Buffer; export function execSyncWrapper(command: string, options?: ExecSyncOptions): string | Buffer { try { return execSync(command, options); } catch (err) { const ioError: ExecErrorOutput = err as ExecErrorOutput; throw new Error(`${ioError.toString()}stdout: ${ioError.stdout.toString()}`); } } export const tryToGetNodeModules = (): string | undefined => { const findNodeModules = require("find-node-modules"); try { return resolve(findNodeModules()[0]); } catch (e) { return undefined; } }; export const isValidCompilerOutputLog = (output: string): boolean => !!output.trim() && output.trim() !== "Compiler run successful, no output requested."; export const resolveExternalContracts = async (externalContracts?: ExternalContracts) => { return Promise.all( Object.entries(externalContracts || {}).map(async ([pathToFolder, contractsNames]) => { const folderFiles = await promisify(fs.readdir)(pathToFolder).catch(() => { throw new Error(`Cannot read folder ${pathToFolder} that was provided in config.externalContracts`); }); const contractsArtifacts = folderFiles .filter(file => contractsNames.some(contract => contract === file.split(".")[0])) .filter(file => file.endsWith(".abi.json") || file.endsWith(".tvc") || file.endsWith(".base64")) .map(file => path.join(pathToFolder, file)); if (contractsArtifacts.length > 0) { logger.printWarn( `config.compiler.externalContracts WARNING: Folder ${pathToFolder} contains contract artifacts, but this is deprecated, please use config.compiler.externalContractsArtifacts instead`, ); } const contractFiles = folderFiles .filter(file => file.endsWith(".tsol") || file.endsWith(".sol")) .filter(file => contractsNames.some(contract => contract === file.split(".")[0])) .map(file => path.join(pathToFolder, file)); return { contractsArtifacts, contractFiles, }; }), ).then(contractsAndArtifacts => contractsAndArtifacts.reduce( (acc, { contractsArtifacts, contractFiles }) => { return { contractsToBuild: [...acc.contractsToBuild, ...contractFiles], contractArtifacts: [...acc.contractArtifacts, ...contractsArtifacts], }; }, { contractArtifacts: [] as string[], contractsToBuild: [] as string[] }, ), ); }; export const compileBySolC = async ({ contracts, compilerVersion, buildFolder, disableIncludePath, compilerPath, compilerParams, linkerLibPath, linkerPath, }: { contracts: Array<{ path: string; contractFileName: string }>; compilerVersion: string; buildFolder: string; disableIncludePath: boolean; compilerPath: string; compilerParams?: string[]; linkerLibPath: string; linkerPath: string; }) => { await lastValueFrom( from(contracts).pipe( mergeMap(({ path, contractFileName }) => { const nodeModules = tryToGetNodeModules(); return defer(async () => { if (semver.lte(compilerVersion, "0.66.0")) { const additionalIncludesPath = `--include-path ${resolve(process.cwd(), "node_modules")} ${ nodeModules ? `--include-path ${nodeModules}` : "" }`; const includePath = `${additionalIncludesPath}`; const execCommand = `cd ${buildFolder} && \ ${compilerPath} ${!disableIncludePath ? includePath : ""} ${path} ${(compilerParams || []).join(" ")}`; return promisify(exec)(execCommand); } if (semver.gte(compilerVersion, "0.68.0")) { const additionalIncludesPath = `${nodeModules ? `--include-path ${nodeModules}` : ""}`; const includePath = `${additionalIncludesPath} ${"--base-path"} . `; const execCommand = ` ${compilerPath} ${ !disableIncludePath ? includePath : "" } -o ${buildFolder} ${path} ${(compilerParams || []).join(" ")}`; return promisify(exec)(execCommand); } throw new Error("Unsupported compiler version"); }).pipe( map(output => ({ output, contractFileName: parse(contractFileName).name, path, })), catchError(e => { logger.printError(`path: ${path}, contractFile: ${contractFileName} error: ${e?.stderr?.toString() || e}`); return throwError(undefined); }), ); }), //Warnings tap( output => isValidCompilerOutputLog(output.output.stderr.toString()) && logger.printBuilderLog(output.output.stderr.toString()), ), filter(({ output }) => { //Only contracts return !!output?.stdout.toString(); }), mergeMap(({ contractFileName }) => { const lib = linkerLibPath ? ` --lib ${linkerLibPath} ` : ""; const resolvedPathCode = resolve(buildFolder, `${contractFileName}.code`); const resolvedPathAbi = resolve(buildFolder, `${contractFileName}.abi.json`); const resolvedPathMap = resolve(buildFolder, `${contractFileName}.map.json`); return defer(async () => { const command = `${linkerPath} compile "${resolvedPathCode}" -a "${resolvedPathAbi}" -o ${resolve( buildFolder, `${contractFileName}.tvc`, )} ${lib} --debug-map ${resolvedPathMap}`; return promisify(exec)(command); }).pipe( map(tvmLinkerLog => { return tvmLinkerLog.stdout.toString().match(new RegExp("Saved to file (.*).")); }), catchError(e => { logger.printError(`contractFileName: ${contractFileName} error:${e?.stderr?.toString()}`); return throwError(undefined); }), map(matchResult => { if (!matchResult) { throw new Error("Linking error, noting linking"); } return matchResult[1]; }), mergeMap(tvcFile => { return concat( defer(() => promisify(fs.writeFile)( resolve(buildFolder, `${contractFileName}.base64`), tvcToBase64(fs.readFileSync(tvcFile)), ), ), ).pipe( catchError(e => { logger.printError(e?.stderr?.toString()); return throwError(undefined); }), ); }), ); }), toArray(), ), ); }; export const compileBySolD = async ({ contracts, compilerVersion, buildFolder, disableIncludePath, compilerPath, compilerParams, }: { contracts: Array<{ path: string; contractFileName: string }>; compilerVersion: string; buildFolder: string; disableIncludePath: boolean; compilerPath: string; compilerParams?: string[]; soldPath: string; }) => { await lastValueFrom( from(contracts).pipe( mergeMap(({ path, contractFileName }) => { const nodeModules = tryToGetNodeModules(); return defer(async () => { if (semver.gte(compilerVersion, "0.72.0")) { const additionalIncludesPath = `${nodeModules ? `--include-path ${nodeModules}` : ""}`; const includePath = `${additionalIncludesPath} ${"--base-path"} . `; const execCommand = ` ${compilerPath} ${ !disableIncludePath ? includePath : "" } -o ${buildFolder} ${path} ${(compilerParams || []).join(" ")}`; return promisify(exec)(execCommand); } throw new Error("Unsupported compiler version"); }).pipe( map(output => ({ output, contractFileName: parse(contractFileName).name, path, })), catchError(e => { logger.printError(`path: ${path}, contractFile: ${contractFileName} error: ${e?.stderr?.toString() || e}`); return throwError(undefined); }), ); }), //Warnings tap( output => isValidCompilerOutputLog(output.output.stderr.toString()) && logger.printBuilderLog(output.output.stderr.toString()), ), toArray(), ), ); };