import { AbiDefinition, AbiType, ContractAbi, DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; import { BigNumber } from './configured_bignumber'; type ParamName = null | string | NestedParamName; interface NestedParamName { name: string | null; names: ParamName[]; } // Note(albrow): This function is unexported in ethers.js. Copying it here for // now. // Source: https://github.com/ethers-io/ethers.js/blob/884593ab76004a808bf8097e9753fb5f8dcc3067/contracts/interface.js#L30 function parseEthersParams(params: DataItem[]): { names: ParamName[]; types: string[] } { const names: ParamName[] = []; const types: string[] = []; params.forEach((param: DataItem) => { if (param.components != null) { let suffix = ''; const arrayBracket = param.type.indexOf('['); if (arrayBracket >= 0) { suffix = param.type.substring(arrayBracket); } const result = parseEthersParams(param.components); names.push({ name: param.name || null, names: result.names }); types.push(`tuple(${result.types.join(',')})${suffix}`); } else { names.push(param.name || null); types.push(param.type); } }); return { names, types, }; } // returns true if x is equal to y and false otherwise. Performs some minimal // type conversion and data massaging for x and y, depending on type. name and // type should typically be derived from parseEthersParams. function isAbiDataEqual(name: ParamName, type: string, x: any, y: any): boolean { if (_.isUndefined(x) && _.isUndefined(y)) { return true; } else if (_.isUndefined(x) && !_.isUndefined(y)) { return false; } else if (!_.isUndefined(x) && _.isUndefined(y)) { return false; } if (_.endsWith(type, '[]')) { // For array types, we iterate through the elements and check each one // individually. Strangely, name does not need to be changed in this // case. if (x.length !== y.length) { return false; } const newType = _.trimEnd(type, '[]'); for (let i = 0; i < x.length; i++) { if (!isAbiDataEqual(name, newType, x[i], y[i])) { return false; } } return true; } if (_.startsWith(type, 'tuple(')) { if (_.isString(name)) { throw new Error('Internal error: type was tuple but names was a string'); } else if (_.isNull(name)) { throw new Error('Internal error: type was tuple but names was null'); } // For tuples, we iterate through the underlying values and check each // one individually. const types = splitTupleTypes(type); if (types.length !== name.names.length) { throw new Error( `Internal error: parameter types/names length mismatch (${types.length} != ${name.names.length})`, ); } for (let i = 0; i < types.length; i++) { // For tuples, name is an object with a names property that is an // array. As an example, for orders, name looks like: // // { // name: 'orders', // names: [ // 'makerAddress', // // ... // 'takerAssetData' // ] // } // const nestedName = _.isString(name.names[i]) ? (name.names[i] as string) : ((name.names[i] as NestedParamName).name as string); if (!isAbiDataEqual(name.names[i], types[i], x[nestedName], y[nestedName])) { return false; } } return true; } else if (type === 'address' || type === 'bytes') { // HACK(albrow): ethers.js returns the checksummed address even when // initially passed in a non-checksummed address. To account for that, // we convert to lowercase before comparing. return _.isEqual(_.toLower(x), _.toLower(y)); } else if (_.startsWith(type, 'uint') || _.startsWith(type, 'int')) { return new BigNumber(x).eq(new BigNumber(y)); } return _.isEqual(x, y); } // splitTupleTypes splits a tuple type string (of the form `tuple(X)` where X is // any other type or list of types) into its component types. It works with // nested tuples, so, e.g., `tuple(tuple(uint256,address),bytes32)` will yield: // `['tuple(uint256,address)', 'bytes32']`. It expects exactly one tuple type as // an argument (not an array). function splitTupleTypes(type: string): string[] { if (_.endsWith(type, '[]')) { throw new Error('Internal error: array types are not supported'); } else if (!_.startsWith(type, 'tuple(')) { throw new Error(`Internal error: expected tuple type but got non-tuple type: ${type}`); } // Trim the outtermost tuple(). const trimmedType = type.substring('tuple('.length, type.length - 1); const types: string[] = []; let currToken = ''; let parenCount = 0; // Tokenize the type string while keeping track of parentheses. for (const char of trimmedType) { switch (char) { case '(': parenCount += 1; currToken += char; break; case ')': parenCount -= 1; currToken += char; break; case ',': if (parenCount === 0) { types.push(currToken); currToken = ''; break; } else { currToken += char; break; } default: currToken += char; break; } } types.push(currToken); return types; } export const abiUtils = { parseEthersParams, isAbiDataEqual, splitTupleTypes, parseFunctionParam(param: DataItem): string { if (param.type === 'tuple') { // Parse out tuple types into {type_1, type_2, ..., type_N} const tupleComponents = param.components; const paramString = _.map(tupleComponents, component => abiUtils.parseFunctionParam(component)); const tupleParamString = `{${paramString}}`; return tupleParamString; } return param.type; }, getFunctionSignature(methodAbi: MethodAbi): string { const functionName = methodAbi.name; const parameterTypeList = _.map(methodAbi.inputs, (param: DataItem) => abiUtils.parseFunctionParam(param)); const functionSignature = `${functionName}(${parameterTypeList})`; return functionSignature; }, /** * Solidity supports function overloading whereas TypeScript does not. * See: https://solidity.readthedocs.io/en/v0.4.21/contracts.html?highlight=overload#function-overloading * In order to support overloaded functions, we suffix overloaded function names with an index. * This index should be deterministic, regardless of function ordering within the smart contract. To do so, * we assign indexes based on the alphabetical order of function signatures. * * E.g * ['f(uint)', 'f(uint,byte32)'] * Should always be renamed to: * ['f1(uint)', 'f2(uint,byte32)'] * Regardless of the order in which these these overloaded functions are declared within the contract ABI. */ renameOverloadedMethods(inputContractAbi: ContractAbi): ContractAbi { const contractAbi = _.cloneDeep(inputContractAbi); const methodAbis = contractAbi.filter((abi: AbiDefinition) => abi.type === AbiType.Function) as MethodAbi[]; // Sort method Abis into alphabetical order, by function signature const methodAbisOrdered = _.sortBy(methodAbis, [ (methodAbi: MethodAbi) => { const functionSignature = abiUtils.getFunctionSignature(methodAbi); return functionSignature; }, ]); // Group method Abis by name (overloaded methods will be grouped together, in alphabetical order) const methodAbisByName: { [key: string]: MethodAbi[] } = {}; _.each(methodAbisOrdered, methodAbi => { (methodAbisByName[methodAbi.name] || (methodAbisByName[methodAbi.name] = [])).push(methodAbi); }); // Rename overloaded methods to overloadedMethodName1, overloadedMethodName2, ... _.each(methodAbisByName, methodAbisWithSameName => { _.each(methodAbisWithSameName, (methodAbi, i: number) => { if (methodAbisWithSameName.length > 1) { const overloadedMethodId = i + 1; const sanitizedMethodName = `${methodAbi.name}${overloadedMethodId}`; const indexOfExistingAbiWithSanitizedMethodNameIfExists = _.findIndex( methodAbis, currentMethodAbi => currentMethodAbi.name === sanitizedMethodName, ); if (indexOfExistingAbiWithSanitizedMethodNameIfExists >= 0) { const methodName = methodAbi.name; throw new Error( `Failed to rename overloaded method '${methodName}' to '${sanitizedMethodName}'. A method with this name already exists.`, ); } methodAbi.name = sanitizedMethodName; } }); }); return contractAbi; }, };