From 4079563f5dca96477bb13935c1cc30ad8224ae4c Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 6 Feb 2019 11:56:49 -0800 Subject: More robust/simple signature parsing, using a parse tree --- .../utils/src/abi_encoder/evm_data_type_factory.ts | 87 +++++++++--- .../utils/src/abi_encoder/evm_data_types/method.ts | 5 + packages/utils/src/abi_encoder/index.ts | 1 + .../src/abi_encoder/utils/signature_parser.ts | 151 ++++++++++----------- 4 files changed, 146 insertions(+), 98 deletions(-) diff --git a/packages/utils/src/abi_encoder/evm_data_type_factory.ts b/packages/utils/src/abi_encoder/evm_data_type_factory.ts index 268649148..613eb887f 100644 --- a/packages/utils/src/abi_encoder/evm_data_type_factory.ts +++ b/packages/utils/src/abi_encoder/evm_data_type_factory.ts @@ -2,7 +2,7 @@ import { DataItem, MethodAbi } from 'ethereum-types'; import * as _ from 'lodash'; -import { generateDataItemsFromSignature } from './utils/signature_parser'; +import { generateDataItemFromSignature } from './utils/signature_parser'; import { DataType } from './abstract_data_types/data_type'; import { DataTypeFactory } from './abstract_data_types/interfaces'; @@ -134,32 +134,87 @@ export class EvmDataTypeFactory implements DataTypeFactory { /** * Convenience function for creating a DataType from different inputs. - * @param input A single or set of DataItem or a DataType signature. - * A signature in the form of '' is interpreted as a `DataItem` - * For example, 'string' is interpreted as {type: 'string'} - * A signature in the form '(, , ..., )' is interpreted as `DataItem[]` - * For eaxmple, '(string, uint256)' is interpreted as [{type: 'string'}, {type: 'uint256'}] + * @param input A single or set of DataItem or a signature for an EVM data type. * @return DataType corresponding to input. */ export function create(input: DataItem | DataItem[] | string): DataType { - // Handle different types of input - const isSignature = typeof input === 'string'; - const isTupleSignature = isSignature && (input as string).startsWith('('); - const shouldParseAsTuple = isTupleSignature || _.isArray(input); - // Create input `dataItem` + const dataItem = consolidateDataItemsIntoSingle(input); + const dataType = EvmDataTypeFactory.getInstance().create(dataItem); + return dataType; +} + +/** + * Convenience function to aggregate a single input or a set of inputs into a single DataItem. + * An array of data items is grouped into a single tuple. + * @param input A single data item; a set of data items; a signature. + * @return A single data item corresponding to input. + */ +function consolidateDataItemsIntoSingle(input: DataItem | DataItem[] | string): DataItem { let dataItem: DataItem; - if (shouldParseAsTuple) { - const dataItems = isSignature ? generateDataItemsFromSignature(input as string) : (input as DataItem[]); + if (_.isArray(input)) { + const dataItems = input as DataItem[]; dataItem = { name: '', type: 'tuple', components: dataItems, }; } else { - dataItem = isSignature ? generateDataItemsFromSignature(input as string)[0] : (input as DataItem); + dataItem = typeof input === 'string' ? generateDataItemFromSignature(input) : (input as DataItem); } - // Create data type - const dataType = EvmDataTypeFactory.getInstance().create(dataItem); + return dataItem; +} + +/** + * Convenience function for creating a Method encoder from different inputs. + * @param methodName name of method. + * @param input A single data item; a set of data items; a signature; or an array of signatures (optional). + * @param output A single data item; a set of data items; a signature; or an array of signatures (optional). + * @return Method corresponding to input. + */ +export function createMethod( + methodName: string, + input?: DataItem | DataItem[] | string | string[], + output?: DataItem | DataItem[] | string | string[], +): Method { + const methodInput = _.isUndefined(input) ? [] : consolidateDataItemsIntoArray(input); + const methodOutput = _.isUndefined(output) ? [] : consolidateDataItemsIntoArray(output); + const methodAbi: MethodAbi = { + name: methodName, + inputs: methodInput, + outputs: methodOutput, + type: 'function', + // default fields not used by ABI + constant: false, + payable: false, + stateMutability: 'nonpayable', + }; + const dataType = new Method(methodAbi); return dataType; } + +/** + * Convenience function that aggregates a single input or a set of inputs into an array of DataItems. + * @param input A single data item; a set of data items; a signature; or an array of signatures. + * @return Array of data items corresponding to input. + */ +function consolidateDataItemsIntoArray(input: DataItem | DataItem[] | string | string[]): DataItem[] { + let dataItems: DataItem[]; + if (_.isArray(input) && _.isEmpty(input)) { + dataItems = []; + } else if (_.isArray(input) && typeof input[0] === 'string') { + dataItems = []; + _.each(input as string[], (signature: string) => { + const dataItem = generateDataItemFromSignature(signature); + dataItems.push(dataItem); + }); + } else if (_.isArray(input)) { + dataItems = input as DataItem[]; + } else if (typeof input === 'string') { + const dataItem = generateDataItemFromSignature(input); + dataItems = [dataItem]; + } else { + dataItems = [input as DataItem]; + } + return dataItems; +} /* tslint:enable no-construct */ diff --git a/packages/utils/src/abi_encoder/evm_data_types/method.ts b/packages/utils/src/abi_encoder/evm_data_types/method.ts index c852a0fdf..93746fa00 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/method.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -65,6 +65,11 @@ export class MethodDataType extends AbstractSetDataType { return this._methodSelector; } + public getReturnValueDataItem(): DataItem { + const returnValueDataItem = this._returnDataType.getDataItem(); + return returnValueDataItem; + } + private _computeSignature(): string { const memberSignature = this._computeSignatureOfMembers(); const methodSignature = `${this.getDataItem().name}${memberSignature}`; diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts index cfacfe075..976bac8e6 100644 --- a/packages/utils/src/abi_encoder/index.ts +++ b/packages/utils/src/abi_encoder/index.ts @@ -12,5 +12,6 @@ export { Tuple, UInt, create, + createMethod, } from './evm_data_type_factory'; export { DataType } from './abstract_data_types/data_type'; diff --git a/packages/utils/src/abi_encoder/utils/signature_parser.ts b/packages/utils/src/abi_encoder/utils/signature_parser.ts index 315784cea..d3996bf8e 100644 --- a/packages/utils/src/abi_encoder/utils/signature_parser.ts +++ b/packages/utils/src/abi_encoder/utils/signature_parser.ts @@ -1,101 +1,88 @@ import { DataItem } from 'ethereum-types'; import * as _ from 'lodash'; +interface Node { + name: string; + value: string; + children: Node[]; + parent?: Node; +} + +function parseNode(node: Node): DataItem { + const components: DataItem[] = []; + _.each(node.children, (child: Node) => { + const component = parseNode(child); + components.push(component); + }); + const dataItem: DataItem = { + name: node.name, + type: node.value, + }; + if (!_.isEmpty(components)) { + dataItem.components = components; + } + return dataItem; +} + /** - * Returns an array of DataItem's corresponding to the input signature. - * A signature can be in two forms: '' or '(, , ...) - * An example of the first form would be 'address' or 'uint256' - * An example of the second form would be '(address, uint256)' - * Signatures can also include a name field, for example: 'foo address' or '(foo address, bar uint256)' - * @param signature of input DataItems - * @return DataItems derived from input signature + * Returns a DataItem corresponding to the input signature. + * A signature can be in two forms: `type` or `(type_1,type_2,...,type_n)` + * An example of the first form would be 'address' or 'uint256[]' or 'bytes[5][]' + * An example of the second form would be '(address,uint256)' or '(address,uint256)[]' + * @param signature of input DataItem. + * @return DataItem derived from input signature. */ -export function generateDataItemsFromSignature(signature: string): DataItem[] { - let trimmedSignature = signature; - if (signature.startsWith('(')) { - if (!signature.endsWith(')')) { - throw new Error(`Failed to generate data item. Must end with ')'`); - } - trimmedSignature = signature.substr(1, signature.length - 2); +export function generateDataItemFromSignature(signature: string): DataItem { + // No data item corresponds to an empty signature + if (_.isEmpty(signature)) { + throw new Error(`Cannot parse data item from empty signature, ''`); } - trimmedSignature += ','; - let isCurrTokenArray = false; - let currTokenArrayModifier = ''; - let isParsingArrayModifier = false; - let currToken = ''; - let parenCount = 0; - let currTokenName = ''; - const dataItems: DataItem[] = []; - for (const char of trimmedSignature) { - // Tokenize the type string while keeping track of parentheses. + // Create a parse tree for data item + let node: Node = { + name: '', + value: '', + children: [], + }; + for (const char of signature) { switch (char) { case '(': - parenCount += 1; - currToken += char; + const child = { + name: '', + value: '', + children: [], + parent: node, + }; + node.value = 'tuple'; + node.children.push(child); + node = child; break; + case ')': - parenCount -= 1; - currToken += char; + node = node.parent as Node; break; - case '[': - if (parenCount === 0) { - isParsingArrayModifier = true; - isCurrTokenArray = true; - currTokenArrayModifier += '['; - } else { - currToken += char; - } - break; - case ']': - if (parenCount === 0) { - isParsingArrayModifier = false; - currTokenArrayModifier += ']'; - } else { - currToken += char; - } + + case ',': + const sibling = { + name: '', + value: '', + children: [], + parent: node.parent, + }; + (node.parent as Node).children.push(sibling); + node = sibling; break; + case ' ': - if (parenCount === 0) { - currTokenName = currToken; - currToken = ''; - } else { - currToken += char; - } + node.name = node.value; + node.value = ''; break; - case ',': - if (parenCount === 0) { - // Generate new DataItem from token - const components = currToken.startsWith('(') ? generateDataItemsFromSignature(currToken) : []; - const isTuple = !_.isEmpty(components); - const dataItem: DataItem = { name: currTokenName, type: '' }; - if (isTuple) { - dataItem.type = 'tuple'; - dataItem.components = components; - } else { - dataItem.type = currToken; - } - if (isCurrTokenArray) { - dataItem.type += currTokenArrayModifier; - } - dataItems.push(dataItem); - // reset token state - currTokenName = ''; - currToken = ''; - isCurrTokenArray = false; - currTokenArrayModifier = ''; - break; - } else { - currToken += char; - break; - } + default: - if (isParsingArrayModifier) { - currTokenArrayModifier += char; - } else { - currToken += char; - } + node.value += char; break; } } - return dataItems; + // Interpret data item from parse tree + const dataItem = parseNode(node); + return dataItem; } -- cgit v1.2.3