github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/js/src/solts/build.ts (about) 1 import { promises as fs } from 'fs'; 2 import * as path from 'path'; 3 import * as solcv5 from 'solc_v5'; 4 import * as solcv8 from 'solc_v8'; 5 import { 6 decodeOutput, 7 encodeInput, 8 importLocalResolver, 9 inputDescriptionFromFiles, 10 Solidity, 11 } from '../contracts/compile'; 12 import { Compiled, newFile, printNodes, tokenizeLinks } from './api'; 13 14 const solcCompilers = { 15 v5: solcv5, 16 v8: solcv8, 17 } as const; 18 19 export const defaultBuildOptions = { 20 solcVersion: 'v5' as keyof typeof solcCompilers, 21 burrowImportPath: (sourceFile: string) => '@hyperledger/burrow' as string, 22 binPath: 'bin' as string | false, 23 abiExt: '.abi' as string, 24 // Used to resolve layout in bin folder - defaults to srcPath if is passed or process.cwd() otherwise 25 basePath: undefined as undefined | string, 26 failOnWarnings: false as boolean, 27 } as const; 28 29 export type BuildOptions = typeof defaultBuildOptions; 30 31 /** 32 * This is our Solidity -> Typescript code generation function, it: 33 * - Compiles Solidity source 34 * - Generates typescript code wrapping the Solidity contracts and functions that calls Burrow 35 * - Generates typescript code to deploy the contracts 36 * - Outputs the ABI files into bin to be later included in the distribution (for Vent and other ABI-consuming services) 37 */ 38 export async function build(srcPathOrFiles: string | string[], opts?: Partial<BuildOptions>): Promise<void> { 39 const { failOnWarnings, solcVersion, binPath, basePath, burrowImportPath, abiExt } = { 40 ...defaultBuildOptions, 41 ...opts, 42 }; 43 const resolvedBasePath = basePath ?? (typeof srcPathOrFiles === 'string' ? srcPathOrFiles : process.cwd()); 44 process.chdir(resolvedBasePath); 45 const basePathPrefix = new RegExp('^' + path.resolve(resolvedBasePath)); 46 const solidityFiles = await getSourceFilesList(srcPathOrFiles); 47 const inputDescription = inputDescriptionFromFiles( 48 // solidityFiles.map((f) => path.resolve(resolvedBasePath, f.replace(basePathPrefix, ''))), 49 solidityFiles, 50 ); 51 const input = encodeInput(inputDescription); 52 const solc = solcCompilers[solcVersion]; 53 54 const solcOutput = solc.compile(input, { import: importLocalResolver(resolvedBasePath) }); 55 const output = decodeOutput(solcOutput); 56 const errors = output.errors?.filter((e) => failOnWarnings || e.severity === 'error') || []; 57 if (errors.length > 0) { 58 throw new Error('Solidity compiler errors:\n' + formatErrors(errors)); 59 } 60 const warnings = output.errors?.filter((e) => e.severity === 'warning') || []; 61 62 if (warnings.length) { 63 console.error('Solidity compiler warnings (not treated as fatal):\n' + formatErrors(warnings)); 64 } 65 66 const plan = Object.keys(output.contracts).map((filename) => ({ 67 source: filename, 68 target: filename.replace(/\.[^/.]+$/, '.abi.ts'), 69 contracts: Object.entries(output.contracts[filename]).map(([name, contract]) => ({ 70 name, 71 contract, 72 })), 73 })); 74 75 let binPlan: { source: string; filename: string; abi: string }[] = []; 76 77 if (binPath !== false) { 78 await fs.mkdir(binPath, { recursive: true }); 79 binPlan = plan.flatMap((f) => { 80 return f.contracts.map(({ name, contract }) => ({ 81 source: f.source, 82 name, 83 filename: path.join(binPath, path.dirname(path.resolve(f.source)).replace(basePathPrefix, ''), name + abiExt), 84 abi: JSON.stringify(contract), 85 })); 86 }); 87 88 const dupes = findDupes(binPlan, (b) => b.filename); 89 90 if (dupes.length) { 91 const dupeDescs = dupes.map(({ key, dupes }) => ({ duplicate: key, sources: dupes.map((d) => d.source) })); 92 throw Error( 93 `Duplicate contract names found (these contracts will result ABI filenames that will collide since ABIs ` + 94 `are flattened in '${binPath}'):\n${dupeDescs.map((d) => JSON.stringify(d)).join('\n')}`, 95 ); 96 } 97 } 98 99 // Write the ABIs emitted for each file to the name of that file without extension. We flatten into a single 100 // directory because that's what burrow deploy has always done. 101 await Promise.all([ 102 ...binPlan.map(async ({ filename, abi }) => { 103 await fs.mkdir(path.dirname(filename), { recursive: true }); 104 await fs.writeFile(filename, abi); 105 }), 106 ...plan.map(({ source, target, contracts }) => 107 fs.writeFile( 108 target, 109 printNodes( 110 ...newFile( 111 contracts.map(({ name, contract }) => getCompiled(name, contract)), 112 burrowImportPath(source), 113 ), 114 ), 115 ), 116 ), 117 ]); 118 } 119 120 function getCompiled(name: string, contract: Solidity.Contract): Compiled { 121 return { 122 name, 123 abi: contract.abi, 124 bytecode: contract.evm.bytecode.object, 125 deployedBytecode: contract.evm.deployedBytecode.object, 126 links: tokenizeLinks(contract.evm.bytecode.linkReferences), 127 }; 128 } 129 130 function findDupes<T>(list: T[], by: (t: T) => string): { key: string; dupes: T[] }[] { 131 const grouped = list.reduce((acc, t) => { 132 const k = by(t); 133 if (!acc[k]) { 134 acc[k] = []; 135 } 136 acc[k].push(t); 137 return acc; 138 }, {} as Record<string, T[]>); 139 return Object.entries(grouped) 140 .filter(([_, group]) => group.length > 1) 141 .map(([key, dupes]) => ({ 142 key, 143 dupes, 144 })); 145 } 146 147 async function getSourceFilesList(srcPathOrFiles: string | string[]): Promise<string[]> { 148 if (typeof srcPathOrFiles === 'string') { 149 const files: string[] = []; 150 for await (const f of walkDir(srcPathOrFiles)) { 151 if (path.extname(f) === '.sol') { 152 files.push(f); 153 } 154 } 155 return files; 156 } 157 return srcPathOrFiles; 158 } 159 160 async function* walkDir(dir: string): AsyncGenerator<string, void, void> { 161 for await (const d of await fs.opendir(dir)) { 162 const entry = path.join(dir, d.name); 163 if (d.isDirectory()) { 164 yield* walkDir(entry); 165 } else if (d.isFile()) { 166 yield entry; 167 } 168 } 169 } 170 171 function formatErrors(errors: Solidity.Error[]): string { 172 return errors.map((err) => err.formattedMessage || err.message).join(''); 173 }