This repository has been archived by the owner on May 28, 2019. It is now read-only.
forked from veilco/augur-lite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ContractCompiler.ts
245 lines (221 loc) · 7.83 KB
/
ContractCompiler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import * as fs from "async-file";
import * as path from "path";
import * as recursiveReadDir from "recursive-readdir";
import asyncMkdirp = require("async-mkdirp");
import { CompilerInput, CompilerOutput } from "solc";
import { Abi } from "ethereum";
import { ChildProcess, exec, spawn } from "child_process";
import { format } from "util";
import { CompilerConfiguration } from "./CompilerConfiguration";
interface AbiOutput {
[contract: string]: Abi;
}
export class ContractCompiler {
private readonly configuration: CompilerConfiguration;
private readonly flattenerBin = "solidity_flattener";
private readonly flattenerCommand: string;
public constructor(configuration: CompilerConfiguration) {
this.configuration = configuration;
this.flattenerCommand = `${this.flattenerBin} --allow-path . %s`;
}
private async getCommandOutputFromInput(
childProcess: ChildProcess,
stdin: string
): Promise<string> {
return new Promise<string>((resolve, reject) => {
const buffers: Array<Buffer> = [];
childProcess.stdout.on("data", function(data: Buffer) {
buffers.push(data);
});
const errorBuffers: Array<Buffer> = [];
childProcess.stderr.on("data", function(data: Buffer) {
errorBuffers.push(data);
});
childProcess.on("close", function(code) {
const errorMessage = Buffer.concat(errorBuffers).toString();
if (code > 0)
return reject(
new Error(`Process Exit Code ${code}\n${errorMessage}`)
);
return resolve(Buffer.concat(buffers).toString());
});
childProcess.stdin.write(stdin);
childProcess.stdin.end();
});
}
private async compileCustomWrapper(
compilerInputJson: CompilerInput
): Promise<CompilerOutput> {
const childProcess = spawn("solc", ["--standard-json"]);
const compilerOutputJson = await this.getCommandOutputFromInput(
childProcess,
JSON.stringify(compilerInputJson)
);
return JSON.parse(compilerOutputJson);
}
public async compileContracts(): Promise<CompilerOutput> {
// Check if all contracts are cached (and thus do not need to be compiled)
try {
const stats = await fs.stat(this.configuration.contractOutputPath);
const lastCompiledTimestamp = stats.mtime;
const ignoreCachedFile = function(
file: string,
stats: fs.Stats
): boolean {
return (
(stats.isFile() && path.extname(file) !== ".sol") ||
(stats.isFile() &&
path.extname(file) === ".sol" &&
stats.mtime < lastCompiledTimestamp)
);
};
const uncachedFiles = await recursiveReadDir(
this.configuration.contractSourceRoot,
[ignoreCachedFile]
);
if (uncachedFiles.length === 0) {
return JSON.parse(
await fs.readFile(this.configuration.contractOutputPath, "utf8")
);
}
} catch {
// Unable to read compiled contracts output file (likely because it has not been generated)
}
console.log("Compiling contracts, this may take a minute...");
// Compile all contracts in the specified input directory
const compilerInputJson = await this.generateCompilerInput();
const compilerOutput = await this.compileCustomWrapper(compilerInputJson);
if (compilerOutput.errors) {
let errors = "";
for (let error of compilerOutput.errors) {
// FIXME: https://github.com/ethereum/solidity/issues/3273
if (
error.message.includes(
"instruction is only available after the Metropolis hard fork"
)
)
continue;
errors += error.formattedMessage + "\n";
}
if (errors.length > 0) {
throw new Error(
"The following errors/warnings were returned by solc:\n\n" + errors
);
}
}
// Create output directory (if it doesn't exist)
await asyncMkdirp(path.dirname(this.configuration.contractOutputPath));
// Output contract data to single file
const filteredCompilerOutput = this.filterCompilerOutput(compilerOutput);
await fs.writeFile(
this.configuration.contractOutputPath,
JSON.stringify(filteredCompilerOutput, null, "\t")
);
// Output abi data to a single file
const abiOutput = this.generateAbiOutput(filteredCompilerOutput);
await fs.writeFile(
this.configuration.abiOutputPath,
JSON.stringify(abiOutput, null, "\t")
);
return filteredCompilerOutput;
}
public async generateFlattenedSolidity(filePath: string): Promise<string> {
const relativeFilePath = filePath
.replace(this.configuration.contractSourceRoot, "")
.replace(/\\/g, "/");
const childProcess = exec(format(this.flattenerCommand, relativeFilePath), {
encoding: "buffer",
cwd: this.configuration.contractSourceRoot
});
return await this.getCommandOutputFromInput(childProcess, "");
}
public async generateCompilerInput(): Promise<CompilerInput> {
const ignoreFile = function(file: string, stats: fs.Stats): boolean {
return stats.isFile() && path.extname(file) !== ".sol";
};
const filePaths = await recursiveReadDir(
this.configuration.contractSourceRoot,
[ignoreFile]
);
const filesPromises = filePaths.map(
async filePath => await this.generateFlattenedSolidity(filePath)
);
const files = await Promise.all(filesPromises);
let inputJson: CompilerInput = {
language: "Solidity",
settings: {
optimizer: {
enabled: true,
runs: 500
},
outputSelection: {
"*": {
"*": ["abi", "evm.bytecode.object"]
}
}
},
sources: {}
};
for (var file in files) {
const filePath = filePaths[file]
.replace(this.configuration.contractSourceRoot, "")
.replace(/\\/g, "/");
inputJson.sources[filePath] = { content: files[file] };
}
return inputJson;
}
private filterCompilerOutput(compilerOutput: CompilerOutput): CompilerOutput {
const result: CompilerOutput = { contracts: {} };
for (let relativeFilePath in compilerOutput.contracts) {
for (let contractName in compilerOutput.contracts[relativeFilePath]) {
// don't include libraries
if (
relativeFilePath.startsWith("libraries/") &&
contractName !== "Delegator" &&
contractName !== "Map"
)
continue;
// don't include embedded libraries
if (
!(
relativeFilePath === `${contractName}.sol` ||
relativeFilePath.endsWith(`/${contractName}.sol`)
)
)
continue;
const abi =
compilerOutput.contracts[relativeFilePath][contractName].abi;
if (abi === undefined) continue;
const bytecode =
compilerOutput.contracts[relativeFilePath][contractName].evm.bytecode
.object;
if (bytecode === undefined) continue;
// don't include interfaces or Abstract contracts
if (/^(?:I|Base)[A-Z].*/.test(contractName)) continue;
if (bytecode.length === 0)
throw new Error(
"Contract: " +
contractName +
" has no bytecode, but this is not expected. It probably doesn't implement all its abstract methods"
);
result.contracts[relativeFilePath] = {
[contractName]: {
abi: abi,
evm: { bytecode: { object: bytecode } }
}
};
}
}
return result;
}
private generateAbiOutput(compilerOutput: CompilerOutput): AbiOutput {
const result: AbiOutput = {};
for (let relativeFilePath in compilerOutput.contracts) {
for (let contractName in compilerOutput.contracts[relativeFilePath]) {
result[contractName] =
compilerOutput.contracts[relativeFilePath][contractName].abi;
}
}
return result;
}
}