diff options
author | Greg Hysen <greg.hysen@gmail.com> | 2018-11-21 05:24:45 +0800 |
---|---|---|
committer | Greg Hysen <greg.hysen@gmail.com> | 2018-11-29 08:38:11 +0800 |
commit | aed8b083b587e7b420ac6129b04004dea95c3f3a (patch) | |
tree | f6eacabc4061d6f616b85886a5f02d3149e16537 /packages/utils/src/abi_encoder/calldata | |
parent | e6ab6f38bacdec90c960ff1db4781d161b1f4103 (diff) | |
download | dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar.gz dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar.bz2 dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar.lz dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar.xz dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.tar.zst dexon-sol-tools-aed8b083b587e7b420ac6129b04004dea95c3f3a.zip |
Split Calldata into multiple files - 1 class per file
Diffstat (limited to 'packages/utils/src/abi_encoder/calldata')
8 files changed, 519 insertions, 0 deletions
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 |