diff options
14 files changed, 571 insertions, 535 deletions
diff --git a/packages/utils/src/abi_encoder/calldata.ts b/packages/utils/src/abi_encoder/calldata.ts deleted file mode 100644 index d108ef0a7..000000000 --- a/packages/utils/src/abi_encoder/calldata.ts +++ /dev/null @@ -1,531 +0,0 @@ -import * as ethUtil from 'ethereumjs-util'; -import * as _ from 'lodash'; - -import * as Constants from './constants'; - -export interface DecodingRules { - structsAsObjects: boolean; -} - -export interface EncodingRules { - optimize?: boolean; - annotate?: boolean; -} - -export abstract class CalldataBlock { - private readonly _signature: string; - private readonly _parentName: string; - private _name: string; - private _offsetInBytes: number; - private _headerSizeInBytes: number; - private _bodySizeInBytes: number; - - constructor( - name: string, - signature: string, - parentName: string, - headerSizeInBytes: number, - bodySizeInBytes: number, - ) { - this._name = name; - this._signature = signature; - this._parentName = parentName; - this._offsetInBytes = 0; - this._headerSizeInBytes = headerSizeInBytes; - this._bodySizeInBytes = bodySizeInBytes; - } - - protected _setHeaderSize(headerSizeInBytes: number): void { - this._headerSizeInBytes = headerSizeInBytes; - } - - protected _setBodySize(bodySizeInBytes: number): void { - this._bodySizeInBytes = bodySizeInBytes; - } - - protected _setName(name: string): void { - this._name = name; - } - - public getName(): string { - return this._name; - } - - public getParentName(): string { - return this._parentName; - } - - public getSignature(): string { - return this._signature; - } - public getHeaderSizeInBytes(): number { - return this._headerSizeInBytes; - } - - public getBodySizeInBytes(): number { - return this._bodySizeInBytes; - } - - public getSizeInBytes(): number { - return this.getHeaderSizeInBytes() + this.getBodySizeInBytes(); - } - - public getOffsetInBytes(): number { - return this._offsetInBytes; - } - - public setOffset(offsetInBytes: number): void { - this._offsetInBytes = offsetInBytes; - } - - public computeHash(): Buffer { - const rawData = this.getRawData(); - const hash = ethUtil.sha3(rawData); - return hash; - } - - public abstract toBuffer(): Buffer; - public abstract getRawData(): Buffer; -} - -export class PayloadCalldataBlock extends CalldataBlock { - private readonly _payload: Buffer; - - constructor(name: string, signature: string, parentName: string, payload: Buffer) { - const headerSizeInBytes = 0; - const bodySizeInBytes = payload.byteLength; - super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); - this._payload = payload; - } - - public toBuffer(): Buffer { - return this._payload; - } - - public getRawData(): Buffer { - return this._payload; - } -} - -export class DependentCalldataBlock extends CalldataBlock { - public static readonly RAW_DATA_START = new Buffer('<'); - public static readonly RAW_DATA_END = new Buffer('>'); - private static readonly _DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; - private static readonly _EMPTY_HEADER_SIZE = 0; - private readonly _parent: CalldataBlock; - private readonly _dependency: CalldataBlock; - private _aliasFor: CalldataBlock | undefined; - - constructor(name: string, signature: string, parentName: string, dependency: CalldataBlock, parent: CalldataBlock) { - const headerSizeInBytes = DependentCalldataBlock._EMPTY_HEADER_SIZE; - const bodySizeInBytes = DependentCalldataBlock._DEPENDENT_PAYLOAD_SIZE_IN_BYTES; - super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); - this._parent = parent; - this._dependency = dependency; - this._aliasFor = undefined; - } - - public toBuffer(): Buffer { - const destinationOffset = - this._aliasFor !== undefined ? this._aliasFor.getOffsetInBytes() : this._dependency.getOffsetInBytes(); - const parentOffset = this._parent.getOffsetInBytes(); - const parentHeaderSize = this._parent.getHeaderSizeInBytes(); - const pointer: number = destinationOffset - (parentOffset + parentHeaderSize); - const pointerBuf = ethUtil.toBuffer(`0x${pointer.toString(Constants.HEX_BASE)}`); - const evmWordWidthInBytes = 32; - const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, evmWordWidthInBytes); - return pointerBufPadded; - } - - public getDependency(): CalldataBlock { - return this._dependency; - } - - public setAlias(block: CalldataBlock): void { - this._aliasFor = block; - this._setName(`${this.getName()} (alias for ${block.getName()})`); - } - - public getAlias(): CalldataBlock | undefined { - return this._aliasFor; - } - - public getRawData(): Buffer { - const dependencyRawData = this._dependency.getRawData(); - const rawDataComponents: Buffer[] = []; - rawDataComponents.push(DependentCalldataBlock.RAW_DATA_START); - rawDataComponents.push(dependencyRawData); - rawDataComponents.push(DependentCalldataBlock.RAW_DATA_END); - const rawData = Buffer.concat(rawDataComponents); - return rawData; - } -} - -export class MemberCalldataBlock extends CalldataBlock { - private _header: Buffer | undefined; - private _members: CalldataBlock[]; - - constructor(name: string, signature: string, parentName: string) { - super(name, signature, parentName, 0, 0); - this._members = []; - this._header = undefined; - } - - public getRawData(): Buffer { - const rawDataComponents: Buffer[] = []; - if (this._header !== undefined) { - rawDataComponents.push(this._header); - } - _.each(this._members, (member: CalldataBlock) => { - const memberBuffer = member.getRawData(); - rawDataComponents.push(memberBuffer); - }); - - const rawData = Buffer.concat(rawDataComponents); - return rawData; - } - - public setMembers(members: CalldataBlock[]): void { - this._members = members; - } - - public setHeader(header: Buffer): void { - this._setHeaderSize(header.byteLength); - this._header = header; - } - - public toBuffer(): Buffer { - if (this._header !== undefined) { - return this._header; - } - return new Buffer(''); - } - - public getMembers(): CalldataBlock[] { - return this._members; - } -} - -class Queue<T> { - private _store: T[] = []; - public push(val: T): void { - this._store.push(val); - } - public pushFront(val: T): void { - this._store.unshift(val); - } - public pop(): T | undefined { - return this._store.shift(); - } - public popBack(): T | undefined { - if (this._store.length === 0) { - return undefined; - } - const backElement = this._store.splice(-1, 1)[0]; - return backElement; - } - public merge(q: Queue<T>): void { - this._store = this._store.concat(q._store); - } - public mergeFront(q: Queue<T>): void { - this._store = q._store.concat(this._store); - } - public getStore(): T[] { - return this._store; - } - public peek(): T | undefined { - return this._store.length >= 0 ? this._store[0] : undefined; - } -} - -export class Calldata { - private readonly _rules: EncodingRules; - private _selector: string; - private _sizeInBytes: number; - private _root: CalldataBlock | undefined; - - private static _createQueue(block: CalldataBlock): Queue<CalldataBlock> { - const blockQueue = new Queue<CalldataBlock>(); - - // Base Case - if (!(block instanceof MemberCalldataBlock)) { - blockQueue.push(block); - return blockQueue; - } - - // This is a Member Block - const memberBlock = block; - _.eachRight(memberBlock.getMembers(), (member: CalldataBlock) => { - if (member instanceof MemberCalldataBlock) { - blockQueue.mergeFront(Calldata._createQueue(member)); - } else { - blockQueue.pushFront(member); - } - }); - - // Children - _.each(memberBlock.getMembers(), (member: CalldataBlock) => { - if (member instanceof DependentCalldataBlock && member.getAlias() === undefined) { - const dependency = member.getDependency(); - if (dependency instanceof MemberCalldataBlock) { - blockQueue.merge(Calldata._createQueue(dependency)); - } else { - blockQueue.push(dependency); - } - } - }); - - blockQueue.pushFront(memberBlock); - return blockQueue; - } - - public constructor(rules: EncodingRules) { - this._rules = rules; - this._selector = ''; - this._sizeInBytes = 0; - this._root = undefined; - } - - public optimize(): void { - if (this._root === undefined) { - throw new Error('expected root'); - } - - const blocksByHash: { [key: string]: CalldataBlock } = {}; - - // 1. Create a queue of subtrees by hash - // Note that they are ordered the same as - const subtreeQueue = Calldata._createQueue(this._root); - let block: CalldataBlock | undefined; - for (block = subtreeQueue.popBack(); block !== undefined; block = subtreeQueue.popBack()) { - if (block instanceof DependentCalldataBlock) { - const dependencyBlockHashBuf = block.getDependency().computeHash(); - const dependencyBlockHash = ethUtil.bufferToHex(dependencyBlockHashBuf); - if (dependencyBlockHash in blocksByHash) { - const blockWithSameHash = blocksByHash[dependencyBlockHash]; - if (blockWithSameHash !== block.getDependency()) { - block.setAlias(blockWithSameHash); - } - } - continue; - } - - const blockHashBuf = block.computeHash(); - const blockHash = ethUtil.bufferToHex(blockHashBuf); - if (!(blockHash in blocksByHash)) { - blocksByHash[blockHash] = block; - } - } - } - - public toHexString(): string { - if (this._root === undefined) { - throw new Error('expected root'); - } - - if (this._rules.optimize) { - this.optimize(); - } - - const offsetQueue = Calldata._createQueue(this._root); - let block: CalldataBlock | undefined; - let offset = 0; - for (block = offsetQueue.pop(); block !== undefined; block = offsetQueue.pop()) { - block.setOffset(offset); - offset += block.getSizeInBytes(); - } - - const hexValue = this._rules.annotate ? this._generateAnnotatedHexString() : this._generateCondensedHexString(); - return hexValue; - } - - public getSelectorHex(): string { - return this._selector; - } - - public getSizeInBytes(): number { - return this._sizeInBytes; - } - - public setRoot(block: CalldataBlock): void { - this._root = block; - this._sizeInBytes += block.getSizeInBytes(); - } - - public setSelector(selector: string): void { - this._selector = selector.startsWith('0x') ? selector : `$0x${selector}`; - if (this._selector.length !== Constants.HEX_SELECTOR_LENGTH_IN_CHARS) { - throw new Error(`Invalid selector '${this._selector}'`); - } - this._sizeInBytes += Constants.HEX_SELECTOR_LENGTH_IN_BYTES; // @TODO: Used to be += 8. Bad? - } - - private _generateAnnotatedHexString(): string { - let hexValue = `${this._selector}`; - if (this._root === undefined) { - throw new Error('expected root'); - } - - const valueQueue = Calldata._createQueue(this._root); - - let block: CalldataBlock | undefined; - let offset = 0; - const functionBlock = valueQueue.peek(); - const functionName: string = functionBlock === undefined ? '' : functionBlock.getName(); - for (block = valueQueue.pop(); block !== undefined; block = valueQueue.pop()) { - // Process each block 1 word at a time - const size = block.getSizeInBytes(); - const name = block.getName(); - const parentName = block.getParentName(); - const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, ''); - - // Current offset - let offsetStr = ''; - - // If this block is empty then it's a newline - const offsetPadding = 10; - const valuePadding = 74; - const namePadding = 80; - const evmWordStartIndex = 0; - const emptySize = 0; - let value = ''; - let nameStr = ''; - let line = ''; - if (size === emptySize) { - offsetStr = ' '.repeat(offsetPadding); - value = ' '.repeat(valuePadding); - nameStr = `### ${prettyName.padEnd(namePadding)}`; - line = `\n${offsetStr}${value}${nameStr}`; - } else { - offsetStr = `0x${offset.toString(Constants.HEX_BASE)}`.padEnd(offsetPadding); - value = ethUtil - .stripHexPrefix( - ethUtil.bufferToHex( - block.toBuffer().slice(evmWordStartIndex, Constants.EVM_WORD_WIDTH_IN_BYTES), - ), - ) - .padEnd(valuePadding); - if (block instanceof MemberCalldataBlock) { - nameStr = `### ${prettyName.padEnd(namePadding)}`; - line = `\n${offsetStr}${value}${nameStr}`; - } else { - nameStr = ` ${prettyName.padEnd(namePadding)}`; - line = `${offsetStr}${value}${nameStr}`; - } - } - - for (let j = Constants.EVM_WORD_WIDTH_IN_BYTES; j < size; j += Constants.EVM_WORD_WIDTH_IN_BYTES) { - offsetStr = `0x${(offset + j).toString(Constants.HEX_BASE)}`.padEnd(offsetPadding); - value = ethUtil - .stripHexPrefix( - ethUtil.bufferToHex(block.toBuffer().slice(j, j + Constants.EVM_WORD_WIDTH_IN_BYTES)), - ) - .padEnd(valuePadding); - nameStr = ' '.repeat(namePadding); - line = `${line}\n${offsetStr}${value}${nameStr}`; - } - - // Append to hex value - hexValue = `${hexValue}\n${line}`; - offset += size; - } - - return hexValue; - } - - private _generateCondensedHexString(): string { - const selectorBuffer = ethUtil.toBuffer(this._selector); - if (this._root === undefined) { - throw new Error('expected root'); - } - - const valueQueue = Calldata._createQueue(this._root); - const valueBufs: Buffer[] = [selectorBuffer]; - let block: CalldataBlock | undefined; - for (block = valueQueue.pop(); block !== undefined; block = valueQueue.pop()) { - valueBufs.push(block.toBuffer()); - } - - const combinedBuffers = Buffer.concat(valueBufs); - const hexValue = ethUtil.bufferToHex(combinedBuffers); - return hexValue; - } -} - -export class RawCalldata { - private static readonly _INITIAL_OFFSET = 0; - private readonly _value: Buffer; - private readonly _selector: string; - private readonly _scopes: Queue<number>; - private _offset: number; // tracks current offset into raw calldata; used for parsing - - constructor(value: string | Buffer, hasSelectorPrefix: boolean = true) { - if (typeof value === 'string' && !value.startsWith('0x')) { - throw new Error(`Expected raw calldata to start with '0x'`); - } - const valueBuf = ethUtil.toBuffer(value); - if (hasSelectorPrefix) { - this._selector = ethUtil.bufferToHex( - valueBuf.slice(Constants.HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA, Constants.HEX_SELECTOR_LENGTH_IN_BYTES), - ); - this._value = valueBuf.slice(Constants.HEX_SELECTOR_LENGTH_IN_BYTES); // disregard selector - } else { - this._selector = '0x'; - this._value = valueBuf; - } - - this._scopes = new Queue<number>(); - this._scopes.push(RawCalldata._INITIAL_OFFSET); - this._offset = RawCalldata._INITIAL_OFFSET; - } - - public popBytes(lengthInBytes: number): Buffer { - const value = this._value.slice(this._offset, this._offset + lengthInBytes); - this.setOffset(this._offset + lengthInBytes); - return value; - } - - public popWord(): Buffer { - const wordInBytes = 32; - return this.popBytes(wordInBytes); - } - - public popWords(length: number): Buffer { - const wordInBytes = 32; - return this.popBytes(length * wordInBytes); - } - - public readBytes(from: number, to: number): Buffer { - const value = this._value.slice(from, to); - return value; - } - - public setOffset(offsetInBytes: number): void { - this._offset = offsetInBytes; - } - - public startScope(): void { - this._scopes.pushFront(this._offset); - } - - public endScope(): void { - this._scopes.pop(); - } - - public getOffset(): number { - return this._offset; - } - - public toAbsoluteOffset(relativeOffset: number): number { - const scopeOffset = this._scopes.peek(); - if (scopeOffset === undefined) { - throw new Error(`Tried to access undefined scope.`); - } - const absoluteOffset = relativeOffset + scopeOffset; - return absoluteOffset; - } - - public getSelector(): string { - return this._selector; - } -} diff --git a/packages/utils/src/abi_encoder/calldata/calldata.ts b/packages/utils/src/abi_encoder/calldata/calldata.ts new file mode 100644 index 000000000..5ac4c1fe7 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata.ts @@ -0,0 +1,224 @@ + +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import * as Constants from '../constants'; +import { Queue } from '../utils/queue'; +import { EncodingRules } from '../utils/rules'; + +import { CalldataBlock } from './calldata_block'; +import * as CalldataBlocks from './calldata_blocks'; + +export class Calldata { + private readonly _rules: EncodingRules; + private _selector: string; + private _sizeInBytes: number; + private _root: CalldataBlock | undefined; + + private static _createQueue(block: CalldataBlock): Queue<CalldataBlock> { + const blockQueue = new Queue<CalldataBlock>(); + + // Base Case + if (!(block instanceof CalldataBlocks.MemberCalldataBlock)) { + blockQueue.push(block); + return blockQueue; + } + + // This is a Member Block + const memberBlock = block; + _.eachRight(memberBlock.getMembers(), (member: CalldataBlock) => { + if (member instanceof CalldataBlocks.MemberCalldataBlock) { + blockQueue.mergeFront(Calldata._createQueue(member)); + } else { + blockQueue.pushFront(member); + } + }); + + // Children + _.each(memberBlock.getMembers(), (member: CalldataBlock) => { + if (member instanceof CalldataBlocks.DependentCalldataBlock && member.getAlias() === undefined) { + const dependency = member.getDependency(); + if (dependency instanceof CalldataBlocks.MemberCalldataBlock) { + blockQueue.merge(Calldata._createQueue(dependency)); + } else { + blockQueue.push(dependency); + } + } + }); + + blockQueue.pushFront(memberBlock); + return blockQueue; + } + + public constructor(rules: EncodingRules) { + this._rules = rules; + this._selector = ''; + this._sizeInBytes = 0; + this._root = undefined; + } + + public optimize(): void { + if (this._root === undefined) { + throw new Error('expected root'); + } + + const blocksByHash: { [key: string]: CalldataBlock } = {}; + + // 1. Create a queue of subtrees by hash + // Note that they are ordered the same as + const subtreeQueue = Calldata._createQueue(this._root); + let block: CalldataBlock | undefined; + for (block = subtreeQueue.popBack(); block !== undefined; block = subtreeQueue.popBack()) { + if (block instanceof CalldataBlocks.DependentCalldataBlock) { + const dependencyBlockHashBuf = block.getDependency().computeHash(); + const dependencyBlockHash = ethUtil.bufferToHex(dependencyBlockHashBuf); + if (dependencyBlockHash in blocksByHash) { + const blockWithSameHash = blocksByHash[dependencyBlockHash]; + if (blockWithSameHash !== block.getDependency()) { + block.setAlias(blockWithSameHash); + } + } + continue; + } + + const blockHashBuf = block.computeHash(); + const blockHash = ethUtil.bufferToHex(blockHashBuf); + if (!(blockHash in blocksByHash)) { + blocksByHash[blockHash] = block; + } + } + } + + public toHexString(): string { + if (this._root === undefined) { + throw new Error('expected root'); + } + + if (this._rules.optimize) { + this.optimize(); + } + + const offsetQueue = Calldata._createQueue(this._root); + let block: CalldataBlock | undefined; + let offset = 0; + for (block = offsetQueue.pop(); block !== undefined; block = offsetQueue.pop()) { + block.setOffset(offset); + offset += block.getSizeInBytes(); + } + + const hexValue = this._rules.annotate ? this._generateAnnotatedHexString() : this._generateCondensedHexString(); + return hexValue; + } + + public getSelectorHex(): string { + return this._selector; + } + + public getSizeInBytes(): number { + return this._sizeInBytes; + } + + public setRoot(block: CalldataBlock): void { + this._root = block; + this._sizeInBytes += block.getSizeInBytes(); + } + + public setSelector(selector: string): void { + this._selector = selector.startsWith('0x') ? selector : `$0x${selector}`; + if (this._selector.length !== Constants.HEX_SELECTOR_LENGTH_IN_CHARS) { + throw new Error(`Invalid selector '${this._selector}'`); + } + this._sizeInBytes += Constants.HEX_SELECTOR_LENGTH_IN_BYTES; // @TODO: Used to be += 8. Bad? + } + + private _generateAnnotatedHexString(): string { + let hexValue = `${this._selector}`; + if (this._root === undefined) { + throw new Error('expected root'); + } + + const valueQueue = Calldata._createQueue(this._root); + + let block: CalldataBlock | undefined; + let offset = 0; + const functionBlock = valueQueue.peek(); + const functionName: string = functionBlock === undefined ? '' : functionBlock.getName(); + for (block = valueQueue.pop(); block !== undefined; block = valueQueue.pop()) { + // Process each block 1 word at a time + const size = block.getSizeInBytes(); + const name = block.getName(); + const parentName = block.getParentName(); + const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, ''); + + // Current offset + let offsetStr = ''; + + // If this block is empty then it's a newline + const offsetPadding = 10; + const valuePadding = 74; + const namePadding = 80; + const evmWordStartIndex = 0; + const emptySize = 0; + let value = ''; + let nameStr = ''; + let line = ''; + if (size === emptySize) { + offsetStr = ' '.repeat(offsetPadding); + value = ' '.repeat(valuePadding); + nameStr = `### ${prettyName.padEnd(namePadding)}`; + line = `\n${offsetStr}${value}${nameStr}`; + } else { + offsetStr = `0x${offset.toString(Constants.HEX_BASE)}`.padEnd(offsetPadding); + value = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex( + block.toBuffer().slice(evmWordStartIndex, Constants.EVM_WORD_WIDTH_IN_BYTES), + ), + ) + .padEnd(valuePadding); + if (block instanceof CalldataBlocks.MemberCalldataBlock) { + nameStr = `### ${prettyName.padEnd(namePadding)}`; + line = `\n${offsetStr}${value}${nameStr}`; + } else { + nameStr = ` ${prettyName.padEnd(namePadding)}`; + line = `${offsetStr}${value}${nameStr}`; + } + } + + for (let j = Constants.EVM_WORD_WIDTH_IN_BYTES; j < size; j += Constants.EVM_WORD_WIDTH_IN_BYTES) { + offsetStr = `0x${(offset + j).toString(Constants.HEX_BASE)}`.padEnd(offsetPadding); + value = ethUtil + .stripHexPrefix( + ethUtil.bufferToHex(block.toBuffer().slice(j, j + Constants.EVM_WORD_WIDTH_IN_BYTES)), + ) + .padEnd(valuePadding); + nameStr = ' '.repeat(namePadding); + line = `${line}\n${offsetStr}${value}${nameStr}`; + } + + // Append to hex value + hexValue = `${hexValue}\n${line}`; + offset += size; + } + + return hexValue; + } + + private _generateCondensedHexString(): string { + const selectorBuffer = ethUtil.toBuffer(this._selector); + if (this._root === undefined) { + throw new Error('expected root'); + } + + const valueQueue = Calldata._createQueue(this._root); + const valueBufs: Buffer[] = [selectorBuffer]; + let block: CalldataBlock | undefined; + for (block = valueQueue.pop(); block !== undefined; block = valueQueue.pop()) { + valueBufs.push(block.toBuffer()); + } + + const combinedBuffers = Buffer.concat(valueBufs); + const hexValue = ethUtil.bufferToHex(combinedBuffers); + return hexValue; + } +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata_block.ts b/packages/utils/src/abi_encoder/calldata/calldata_block.ts new file mode 100644 index 000000000..35bd994e5 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata_block.ts @@ -0,0 +1,77 @@ +import * as ethUtil from 'ethereumjs-util'; + +export abstract class CalldataBlock { + private readonly _signature: string; + private readonly _parentName: string; + private _name: string; + private _offsetInBytes: number; + private _headerSizeInBytes: number; + private _bodySizeInBytes: number; + + constructor( + name: string, + signature: string, + parentName: string, + headerSizeInBytes: number, + bodySizeInBytes: number, + ) { + this._name = name; + this._signature = signature; + this._parentName = parentName; + this._offsetInBytes = 0; + this._headerSizeInBytes = headerSizeInBytes; + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setHeaderSize(headerSizeInBytes: number): void { + this._headerSizeInBytes = headerSizeInBytes; + } + + protected _setBodySize(bodySizeInBytes: number): void { + this._bodySizeInBytes = bodySizeInBytes; + } + + protected _setName(name: string): void { + this._name = name; + } + + public getName(): string { + return this._name; + } + + public getParentName(): string { + return this._parentName; + } + + public getSignature(): string { + return this._signature; + } + public getHeaderSizeInBytes(): number { + return this._headerSizeInBytes; + } + + public getBodySizeInBytes(): number { + return this._bodySizeInBytes; + } + + public getSizeInBytes(): number { + return this.getHeaderSizeInBytes() + this.getBodySizeInBytes(); + } + + public getOffsetInBytes(): number { + return this._offsetInBytes; + } + + public setOffset(offsetInBytes: number): void { + this._offsetInBytes = offsetInBytes; + } + + public computeHash(): Buffer { + const rawData = this.getRawData(); + const hash = ethUtil.sha3(rawData); + return hash; + } + + public abstract toBuffer(): Buffer; + public abstract getRawData(): Buffer; +} diff --git a/packages/utils/src/abi_encoder/calldata/calldata_blocks.ts b/packages/utils/src/abi_encoder/calldata/calldata_blocks.ts new file mode 100644 index 000000000..37660ef21 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/calldata_blocks.ts @@ -0,0 +1,3 @@ +export * from './dependent_calldata_block'; +export * from './member_calldata_block'; +export * from './payload_calldata_block';
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/calldata/dependent_calldata_block.ts b/packages/utils/src/abi_encoder/calldata/dependent_calldata_block.ts new file mode 100644 index 000000000..d6870ec0b --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/dependent_calldata_block.ts @@ -0,0 +1,59 @@ +import * as ethUtil from 'ethereumjs-util'; + +import * as Constants from '../constants'; + +import { CalldataBlock } from './calldata_block'; + +export class DependentCalldataBlock extends CalldataBlock { + public static readonly RAW_DATA_START = new Buffer('<'); + public static readonly RAW_DATA_END = new Buffer('>'); + private static readonly _DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private static readonly _EMPTY_HEADER_SIZE = 0; + private readonly _parent: CalldataBlock; + private readonly _dependency: CalldataBlock; + private _aliasFor: CalldataBlock | undefined; + + constructor(name: string, signature: string, parentName: string, dependency: CalldataBlock, parent: CalldataBlock) { + const headerSizeInBytes = DependentCalldataBlock._EMPTY_HEADER_SIZE; + const bodySizeInBytes = DependentCalldataBlock._DEPENDENT_PAYLOAD_SIZE_IN_BYTES; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._parent = parent; + this._dependency = dependency; + this._aliasFor = undefined; + } + + public toBuffer(): Buffer { + const destinationOffset = + this._aliasFor !== undefined ? this._aliasFor.getOffsetInBytes() : this._dependency.getOffsetInBytes(); + const parentOffset = this._parent.getOffsetInBytes(); + const parentHeaderSize = this._parent.getHeaderSizeInBytes(); + const pointer: number = destinationOffset - (parentOffset + parentHeaderSize); + const pointerBuf = ethUtil.toBuffer(`0x${pointer.toString(Constants.HEX_BASE)}`); + const evmWordWidthInBytes = 32; + const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, evmWordWidthInBytes); + return pointerBufPadded; + } + + public getDependency(): CalldataBlock { + return this._dependency; + } + + public setAlias(block: CalldataBlock): void { + this._aliasFor = block; + this._setName(`${this.getName()} (alias for ${block.getName()})`); + } + + public getAlias(): CalldataBlock | undefined { + return this._aliasFor; + } + + public getRawData(): Buffer { + const dependencyRawData = this._dependency.getRawData(); + const rawDataComponents: Buffer[] = []; + rawDataComponents.push(DependentCalldataBlock.RAW_DATA_START); + rawDataComponents.push(dependencyRawData); + rawDataComponents.push(DependentCalldataBlock.RAW_DATA_END); + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } +}
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/calldata/index.ts b/packages/utils/src/abi_encoder/calldata/index.ts new file mode 100644 index 000000000..0e1f57a2d --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/index.ts @@ -0,0 +1,6 @@ +export * from './calldata_block'; +export * from './dependent_calldata_block'; +export * from './payload_calldata_block'; +export * from './member_calldata_block'; +export * from './calldata'; +export * from './raw_calldata';
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/calldata/member_calldata_block.ts b/packages/utils/src/abi_encoder/calldata/member_calldata_block.ts new file mode 100644 index 000000000..e9bcb2f34 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/member_calldata_block.ts @@ -0,0 +1,48 @@ +import * as _ from 'lodash'; + +import { CalldataBlock } from './calldata_block'; + +export class MemberCalldataBlock extends CalldataBlock { + private _header: Buffer | undefined; + private _members: CalldataBlock[]; + + constructor(name: string, signature: string, parentName: string) { + super(name, signature, parentName, 0, 0); + this._members = []; + this._header = undefined; + } + + public getRawData(): Buffer { + const rawDataComponents: Buffer[] = []; + if (this._header !== undefined) { + rawDataComponents.push(this._header); + } + _.each(this._members, (member: CalldataBlock) => { + const memberBuffer = member.getRawData(); + rawDataComponents.push(memberBuffer); + }); + + const rawData = Buffer.concat(rawDataComponents); + return rawData; + } + + public setMembers(members: CalldataBlock[]): void { + this._members = members; + } + + public setHeader(header: Buffer): void { + this._setHeaderSize(header.byteLength); + this._header = header; + } + + public toBuffer(): Buffer { + if (this._header !== undefined) { + return this._header; + } + return new Buffer(''); + } + + public getMembers(): CalldataBlock[] { + return this._members; + } +}
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/calldata/payload_calldata_block.ts b/packages/utils/src/abi_encoder/calldata/payload_calldata_block.ts new file mode 100644 index 000000000..81a88bc19 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/payload_calldata_block.ts @@ -0,0 +1,20 @@ +import { CalldataBlock } from './calldata_block'; + +export class PayloadCalldataBlock extends CalldataBlock { + private readonly _payload: Buffer; + + constructor(name: string, signature: string, parentName: string, payload: Buffer) { + const headerSizeInBytes = 0; + const bodySizeInBytes = payload.byteLength; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes); + this._payload = payload; + } + + public toBuffer(): Buffer { + return this._payload; + } + + public getRawData(): Buffer { + return this._payload; + } +}
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/calldata/raw_calldata.ts b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts new file mode 100644 index 000000000..8b946ee4b --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata/raw_calldata.ts @@ -0,0 +1,82 @@ +import * as ethUtil from 'ethereumjs-util'; + +import * as Constants from '../constants'; +import { Queue } from '../utils/queue'; + +export class RawCalldata { + private static readonly _INITIAL_OFFSET = 0; + private readonly _value: Buffer; + private readonly _selector: string; + private readonly _scopes: Queue<number>; + private _offset: number; // tracks current offset into raw calldata; used for parsing + + constructor(value: string | Buffer, hasSelectorPrefix: boolean = true) { + if (typeof value === 'string' && !value.startsWith('0x')) { + throw new Error(`Expected raw calldata to start with '0x'`); + } + const valueBuf = ethUtil.toBuffer(value); + if (hasSelectorPrefix) { + this._selector = ethUtil.bufferToHex( + valueBuf.slice(Constants.HEX_SELECTOR_BYTE_OFFSET_IN_CALLDATA, Constants.HEX_SELECTOR_LENGTH_IN_BYTES), + ); + this._value = valueBuf.slice(Constants.HEX_SELECTOR_LENGTH_IN_BYTES); // disregard selector + } else { + this._selector = '0x'; + this._value = valueBuf; + } + + this._scopes = new Queue<number>(); + this._scopes.push(RawCalldata._INITIAL_OFFSET); + this._offset = RawCalldata._INITIAL_OFFSET; + } + + public popBytes(lengthInBytes: number): Buffer { + const value = this._value.slice(this._offset, this._offset + lengthInBytes); + this.setOffset(this._offset + lengthInBytes); + return value; + } + + public popWord(): Buffer { + const wordInBytes = 32; + return this.popBytes(wordInBytes); + } + + public popWords(length: number): Buffer { + const wordInBytes = 32; + return this.popBytes(length * wordInBytes); + } + + public readBytes(from: number, to: number): Buffer { + const value = this._value.slice(from, to); + return value; + } + + public setOffset(offsetInBytes: number): void { + this._offset = offsetInBytes; + } + + public startScope(): void { + this._scopes.pushFront(this._offset); + } + + public endScope(): void { + this._scopes.pop(); + } + + public getOffset(): number { + return this._offset; + } + + public toAbsoluteOffset(relativeOffset: number): number { + const scopeOffset = this._scopes.peek(); + if (scopeOffset === undefined) { + throw new Error(`Tried to access undefined scope.`); + } + const absoluteOffset = relativeOffset + scopeOffset; + return absoluteOffset; + } + + public getSelector(): string { + return this._selector; + } +}
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/data_type.ts b/packages/utils/src/abi_encoder/data_type.ts index 9890619e5..0fbb9165a 100644 --- a/packages/utils/src/abi_encoder/data_type.ts +++ b/packages/utils/src/abi_encoder/data_type.ts @@ -9,14 +9,14 @@ import * as Constants from './constants'; import { Calldata, CalldataBlock, - DecodingRules, DependentCalldataBlock, - EncodingRules, MemberCalldataBlock, PayloadCalldataBlock, RawCalldata, } from './calldata'; +import { DecodingRules, EncodingRules } from './utils/rules'; + export interface DataTypeFactory { create: (dataItem: DataItem, parentDataType?: DataType) => DataType; mapDataItemToDataType: (dataItem: DataItem) => DataType; 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 e8e717bf1..c4e6ee93a 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/method.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -2,9 +2,10 @@ import { DataItem, MethodAbi } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; -import { DecodingRules, EncodingRules, RawCalldata } from '../calldata'; +import { RawCalldata } from '../calldata'; import * as Constants from '../constants'; import { DataType, DataTypeFactory, MemberDataType } from '../data_type'; +import { DecodingRules, EncodingRules } from '../utils/rules'; import { StaticBytes } from './static_bytes'; import { Tuple } from './tuple'; diff --git a/packages/utils/src/abi_encoder/index.ts b/packages/utils/src/abi_encoder/index.ts index a62569fab..ea037b40a 100644 --- a/packages/utils/src/abi_encoder/index.ts +++ b/packages/utils/src/abi_encoder/index.ts @@ -1,2 +1,2 @@ -export { EncodingRules, DecodingRules } from './calldata'; +export { EncodingRules, DecodingRules } from './utils/rules'; export * from './evm_data_type_factory'; diff --git a/packages/utils/src/abi_encoder/utils/queue.ts b/packages/utils/src/abi_encoder/utils/queue.ts new file mode 100644 index 000000000..2e27afd45 --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/queue.ts @@ -0,0 +1,39 @@ +export class Queue<T> { + private _store: T[] = []; + + public push(val: T): void { + this._store.push(val); + } + + public pushFront(val: T): void { + this._store.unshift(val); + } + + public pop(): T | undefined { + return this._store.shift(); + } + + public popBack(): T | undefined { + if (this._store.length === 0) { + return undefined; + } + const backElement = this._store.splice(-1, 1)[0]; + return backElement; + } + + public merge(q: Queue<T>): void { + this._store = this._store.concat(q._store); + } + + public mergeFront(q: Queue<T>): void { + this._store = q._store.concat(this._store); + } + + public getStore(): T[] { + return this._store; + } + + public peek(): T | undefined { + return this._store.length >= 0 ? this._store[0] : undefined; + } +}
\ No newline at end of file diff --git a/packages/utils/src/abi_encoder/utils/rules.ts b/packages/utils/src/abi_encoder/utils/rules.ts new file mode 100644 index 000000000..cf97ef6da --- /dev/null +++ b/packages/utils/src/abi_encoder/utils/rules.ts @@ -0,0 +1,8 @@ +export interface DecodingRules { + structsAsObjects: boolean; +} + +export interface EncodingRules { + optimize?: boolean; + annotate?: boolean; +}
\ No newline at end of file |