More robust/simple signature parsing, using a parse tree
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -12,5 +12,6 @@ export {
|
||||
Tuple,
|
||||
UInt,
|
||||
create,
|
||||
createMethod,
|
||||
} from './evm_data_type_factory';
|
||||
export { DataType } from './abstract_data_types/data_type';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user