163 lines
5.6 KiB
TypeScript
163 lines
5.6 KiB
TypeScript
import { AbiDefinition, ContractAbi, DataItem, EventAbi, MethodAbi } from 'ethereum-types';
|
|
import * as ethUtil from 'ethereumjs-util';
|
|
import * as fs from 'fs';
|
|
import * as _ from 'lodash';
|
|
import * as path from 'path';
|
|
import * as process from 'process';
|
|
|
|
const keccak256 = ethUtil.sha3;
|
|
const ARGS = process.argv.slice(2);
|
|
const INDENT = ' ';
|
|
const LINEBREAK = '\n';
|
|
const VISIBILITY = 'internal';
|
|
|
|
interface ParsedContract {
|
|
methods: {
|
|
[functionName: string]: Array<{
|
|
selector: string;
|
|
signature: string;
|
|
}>;
|
|
};
|
|
events: {
|
|
[eventName: string]: {
|
|
selector: string;
|
|
signature: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
// tslint:disable: no-console
|
|
(() => {
|
|
const [exchangeArtifactsFile, outputFile] = ARGS;
|
|
const exchangeArtifacts = require(exchangeArtifactsFile);
|
|
const contractName = path.basename(outputFile, '.sol');
|
|
const parsedContract = parseContract(exchangeArtifacts.compilerOutput.abi);
|
|
const contractDefinition = defineContract(contractName, parsedContract);
|
|
const preamble = extractOutputFilePreamble(outputFile);
|
|
const outputFileContents = `${preamble}${contractDefinition}${LINEBREAK}`;
|
|
fs.writeFileSync(outputFile, outputFileContents);
|
|
console.log(`Wrote exchange selectors to "${path.resolve(outputFile)}."`);
|
|
})();
|
|
|
|
function parseContract(abi: ContractAbi): ParsedContract {
|
|
const parsedContract: ParsedContract = {
|
|
methods: {},
|
|
events: {},
|
|
};
|
|
for (const abiItem of abi) {
|
|
if (isMethodAbi(abiItem)) {
|
|
const name = abiItem.name;
|
|
const signature = `${name}(${encodeMethodInputs(abiItem.inputs)})`;
|
|
const selector = `0x${keccak256(signature)
|
|
.slice(0, 4)
|
|
.toString('hex')}`;
|
|
if (parsedContract.methods[name] === undefined) {
|
|
parsedContract.methods[name] = [];
|
|
}
|
|
parsedContract.methods[name].push({
|
|
selector,
|
|
signature,
|
|
});
|
|
} else if (isEventAbi(abiItem)) {
|
|
const name = abiItem.name;
|
|
const signature = `${name}(${encodeMethodInputs(abiItem.inputs)})`;
|
|
const selector = `0x${keccak256(signature).toString('hex')}`;
|
|
parsedContract.events[name] = {
|
|
selector,
|
|
signature,
|
|
};
|
|
}
|
|
}
|
|
return parsedContract;
|
|
}
|
|
|
|
function isMethodAbi(abiItem: AbiDefinition): abiItem is MethodAbi {
|
|
return abiItem.type === 'function';
|
|
}
|
|
|
|
function isEventAbi(abiItem: AbiDefinition): abiItem is EventAbi {
|
|
return abiItem.type === 'event';
|
|
}
|
|
|
|
function defineContract(contractName: string, parsedContract: ParsedContract): string {
|
|
const constantDefinitions = [];
|
|
// Define function selectors.
|
|
const sortedMethodNames = _.sortBy(_.keys(parsedContract.methods), name => name.toLowerCase());
|
|
for (const name of sortedMethodNames) {
|
|
const methods = parsedContract.methods[name];
|
|
for (let idx = 0; idx < methods.length; idx++) {
|
|
const constantLines = generateFunctionSelectorConstantDefinition(
|
|
name,
|
|
methods[idx].signature,
|
|
methods[idx].selector,
|
|
idx,
|
|
methods.length,
|
|
);
|
|
constantDefinitions.push(constantLines);
|
|
}
|
|
}
|
|
// Define event selectors.
|
|
const sortedEventNames = _.sortBy(_.keys(parsedContract.events), name => name.toLowerCase());
|
|
for (const name of sortedEventNames) {
|
|
const event = parsedContract.events[name];
|
|
const constantLines = generateEventSelectorConstantDefinition(name, event.signature, event.selector);
|
|
constantDefinitions.push(constantLines);
|
|
}
|
|
return [
|
|
`contract ${contractName} {`,
|
|
`${INDENT}// solhint-disable max-line-length`,
|
|
'',
|
|
constantDefinitions
|
|
.map(lines => lines.map(line => `${INDENT}${line}`))
|
|
.map(lines => lines.join(LINEBREAK))
|
|
.join(`${LINEBREAK}${LINEBREAK}`),
|
|
`}`,
|
|
].join(LINEBREAK);
|
|
}
|
|
|
|
function extractOutputFilePreamble(outputFile: string): string {
|
|
const preambleLines = [];
|
|
const outputFileLines = fs.readFileSync(outputFile, 'utf-8').split(/\r?\n/);
|
|
for (const line of outputFileLines) {
|
|
if (/^\s*contract\s+[a-zA-Z][a-zA-Z0-9_]+/.test(line)) {
|
|
preambleLines.push('');
|
|
break;
|
|
}
|
|
preambleLines.push(line);
|
|
}
|
|
return preambleLines.join(LINEBREAK);
|
|
}
|
|
|
|
function generateFunctionSelectorConstantDefinition(
|
|
name: string,
|
|
signature: string,
|
|
selector: string,
|
|
idx: number,
|
|
total: number,
|
|
): string[] {
|
|
const varName = _.snakeCase(total === 1 ? name : `${name}_${idx + 1}`).toUpperCase();
|
|
return [`// function ${signature}`, `bytes4 constant ${VISIBILITY} ${varName}_SELECTOR = ${selector};`];
|
|
}
|
|
|
|
function generateEventSelectorConstantDefinition(name: string, signature: string, selector: string): string[] {
|
|
const varName = _.snakeCase(name).toUpperCase();
|
|
return [`// event ${signature}`, `bytes32 constant ${VISIBILITY} EVENT_${varName}_SELECTOR = ${selector};`];
|
|
}
|
|
|
|
function encodeMethodInputs(inputs?: DataItem[]): string {
|
|
if (inputs === undefined) {
|
|
throw new Error('encodeMethodInputs: inputs are undefined');
|
|
}
|
|
const types = [];
|
|
for (const input of inputs) {
|
|
if (input.type === 'tuple') {
|
|
types.push(`(${encodeMethodInputs(input.components)})`);
|
|
} else if (input.type === 'tuple[]') {
|
|
types.push(`(${encodeMethodInputs(input.components)})[]`);
|
|
} else {
|
|
types.push(input.type);
|
|
}
|
|
}
|
|
return types.join(',');
|
|
}
|