More robust/simple signature parsing, using a parse tree

This commit is contained in:
Greg Hysen
2019-02-06 11:56:49 -08:00
parent 3939d516e6
commit 4079563f5d
4 changed files with 150 additions and 102 deletions

View File

@@ -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 '<type>' is interpreted as a `DataItem`
* For example, 'string' is interpreted as {type: 'string'}
* A signature in the form '(<type1>, <type2>, ..., <typen>)' 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 */

View File

@@ -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}`;

View File

@@ -12,5 +12,6 @@ export {
Tuple,
UInt,
create,
createMethod,
} from './evm_data_type_factory';
export { DataType } from './abstract_data_types/data_type';

View File

@@ -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: '<DataItem.type>' or '(<DataItem1.type>, <DataItem2.type>, ...)
* 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 ')'`);
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 = signature.substr(1, signature.length - 2);
}
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;
break;
case '[':
if (parenCount === 0) {
isParsingArrayModifier = true;
isCurrTokenArray = true;
currTokenArrayModifier += '[';
} else {
currToken += char;
}
break;
case ']':
if (parenCount === 0) {
isParsingArrayModifier = false;
currTokenArrayModifier += ']';
} else {
currToken += char;
}
break;
case ' ':
if (parenCount === 0) {
currTokenName = currToken;
currToken = '';
} else {
currToken += char;
}
node = node.parent as Node;
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 = '';
const sibling = {
name: '',
value: '',
children: [],
parent: node.parent,
};
(node.parent as Node).children.push(sibling);
node = sibling;
break;
} else {
currToken += char;
case ' ':
node.name = node.value;
node.value = '';
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;
}