From 5b0d554f7baec54837d795b6568ae5ba8d8a0908 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Wed, 14 Nov 2018 15:27:07 -0800 Subject: Moved Abi Encoder into utils package --- packages/utils/src/abi_encoder/calldata.ts | 520 +++++++++++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 packages/utils/src/abi_encoder/calldata.ts (limited to 'packages/utils/src/abi_encoder/calldata.ts') diff --git a/packages/utils/src/abi_encoder/calldata.ts b/packages/utils/src/abi_encoder/calldata.ts new file mode 100644 index 000000000..32278e5c5 --- /dev/null +++ b/packages/utils/src/abi_encoder/calldata.ts @@ -0,0 +1,520 @@ +import ethUtil = require('ethereumjs-util'); +import CommunicationChatBubbleOutline from 'material-ui/SvgIcon'; +var _ = require('lodash'); + +export interface DecodingRules { + structsAsObjects: boolean; +} + +export interface EncodingRules { + optimize?: boolean; + annotate?: boolean; +} + +export abstract class CalldataBlock { + private name: string; + private signature: string; + private offsetInBytes: number; + private headerSizeInBytes: number; + private bodySizeInBytes: number; + private relocatable: boolean; + private parentName: string; + + constructor(name: string, signature: string, parentName: string, /*offsetInBytes: number,*/ headerSizeInBytes: number, bodySizeInBytes: number, relocatable: boolean) { + this.name = name; + this.signature = signature; + this.parentName = parentName; + this.offsetInBytes = 0; + this.headerSizeInBytes = headerSizeInBytes; + this.bodySizeInBytes = bodySizeInBytes; + this.relocatable = relocatable; + } + + protected setHeaderSize(headerSizeInBytes: number) { + this.headerSizeInBytes = headerSizeInBytes; + } + + protected setBodySize(bodySizeInBytes: number) { + this.bodySizeInBytes = bodySizeInBytes; + } + + protected setName(name: string) { + this.name = name; + } + + public getName(): string { + return this.name; + } + + public getParentName(): string { + return this.parentName; + } + + public getSignature(): string { + return this.signature; + } + + public isRelocatable(): boolean { + return this.relocatable; + } + + public getHeaderSizeInBytes(): number { + return this.headerSizeInBytes; + } + + public getBodySizeInBytes(): number { + return this.bodySizeInBytes; + } + + public getSizeInBytes(): number { + return this.headerSizeInBytes + this.bodySizeInBytes; + } + + public getOffsetInBytes(): number { + return this.offsetInBytes; + } + + public setOffset(offsetInBytes: number) { + 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 payload: Buffer; + + constructor(name: string, signature: string, parentName: string, /*offsetInBytes: number,*/ relocatable: boolean, payload: Buffer) { + const headerSizeInBytes = 0; + const bodySizeInBytes = payload.byteLength; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes, relocatable); + this.payload = payload; + } + + public toBuffer(): Buffer { + return this.payload; + } + + public getRawData(): Buffer { + return this.payload; + } +} + +export class DependentCalldataBlock extends CalldataBlock { + public static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + public static RAW_DATA_START = new Buffer('<'); + public static RAW_DATA_END = new Buffer('>'); + private parent: CalldataBlock; + private dependency: CalldataBlock; + private aliasFor: CalldataBlock | undefined; + + constructor(name: string, signature: string, parentName: string, relocatable: boolean, dependency: CalldataBlock, parent: CalldataBlock) { + const headerSizeInBytes = 0; + const bodySizeInBytes = DependentCalldataBlock.DEPENDENT_PAYLOAD_SIZE_IN_BYTES; + super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes, relocatable); + 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(16)}`); + const evmWordWidthInBytes = 32; + const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, evmWordWidthInBytes); + return pointerBufPadded; + } + + public getDependency(): CalldataBlock { + return this.dependency; + } + + public setAlias(block: CalldataBlock) { + 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 static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32; + private header: Buffer | undefined; + private members: CalldataBlock[]; + private contiguous: boolean; + + constructor(name: string, signature: string, parentName: string, relocatable: boolean, contiguous: boolean) { + super(name, signature, parentName, 0, 0, relocatable); + this.members = []; + this.header = undefined; + this.contiguous = contiguous; + } + + 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[]) { + let bodySizeInBytes = 0; + _.each(members, (member: CalldataBlock) => { + bodySizeInBytes += member.getSizeInBytes(); + }); + this.members = members; + this.setBodySize(0); + } + + public isContiguous(): boolean { + return true; + } + + public setHeader(header: Buffer) { + 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 { + private store: T[] = []; + push(val: T) { + this.store.push(val); + } + pushFront(val: T) { + this.store.unshift(val); + } + pop(): T | undefined { + return this.store.shift(); + } + popBack(): T | undefined { + if (this.store.length === 0) return undefined; + const backElement = this.store.splice(-1, 1)[0]; + return backElement; + } + merge(q: Queue) { + this.store = this.store.concat(q.store); + } + mergeFront(q: Queue) { + this.store = q.store.concat(this.store); + } + getStore(): T[] { + return this.store; + } + peek(): T | undefined { + return this.store.length >= 0 ? this.store[0] : undefined; + } +} + +export class Calldata { + private selector: string; + private rules: EncodingRules; + private sizeInBytes: number; + private root: MemberCalldataBlock | undefined; + + constructor(rules: EncodingRules) { + this.selector = ''; + this.rules = rules; + this.sizeInBytes = 0; + this.root = undefined; + } + + private createQueue(memberBlock: MemberCalldataBlock): Queue { + const blockQueue = new Queue(); + _.eachRight(memberBlock.getMembers(), (member: CalldataBlock) => { + if (member instanceof MemberCalldataBlock) { + blockQueue.mergeFront(this.createQueue(member)); + } else { + blockQueue.pushFront(member); + } + }); + + // Children + _.each(memberBlock.getMembers(), (member: CalldataBlock) => { + if (member instanceof DependentCalldataBlock && member.getAlias() === undefined) { + let dependency = member.getDependency(); + if (dependency instanceof MemberCalldataBlock) { + blockQueue.merge(this.createQueue(dependency)); + } else { + blockQueue.push(dependency); + } + } + }); + + blockQueue.pushFront(memberBlock); + return blockQueue; + } + + private generateAnnotatedHexString(): string { + let hexValue = `${this.selector}`; + if (this.root === undefined) { + throw new Error('expected root'); + } + + const valueQueue = this.createQueue(this.root); + + let block: CalldataBlock | undefined; + let offset = 0; + const functionBlock = valueQueue.peek(); + let functionName: string = functionBlock === undefined ? '' : functionBlock.getName(); + while ((block = valueQueue.pop()) !== undefined) { + // Set f + + // Process each block 1 word at a time + const size = block.getSizeInBytes(); + const name = block.getName(); + const parentName = block.getParentName(); + + //const ancestrialNamesOffset = name.startsWith('ptr<') ? 4 : 0; + //const parentOffset = name.lastIndexOf(parentName); + const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, '');//.replace(`${parentName}[`, '['); + const signature = block.getSignature(); + + // Current offset + let offsetStr = ''; + + // If this block is empty then it's a newline + let value = ''; + let nameStr = ''; + let line = ''; + if (size === 0) { + offsetStr = ' '.repeat(10); + value = ' '.repeat(74); + nameStr = `### ${prettyName.padEnd(80)}`; + line = `\n${offsetStr}${value}${nameStr}`; + } else { + offsetStr = `0x${offset.toString(16)}`.padEnd(10, ' '); + value = ethUtil.stripHexPrefix(ethUtil.bufferToHex(block.toBuffer().slice(0, 32))).padEnd(74); + if (block instanceof MemberCalldataBlock) { + nameStr = `### ${prettyName.padEnd(80)}`; + line = `\n${offsetStr}${value}${nameStr}`; + } else { + nameStr = ` ${prettyName.padEnd(80)}`; + line = `${offsetStr}${value}${nameStr}`; + } + } + + for (let j = 32; j < size; j += 32) { + offsetStr = `0x${(offset + j).toString(16)}`.padEnd(10, ' '); + value = ethUtil.stripHexPrefix(ethUtil.bufferToHex(block.toBuffer().slice(j, j + 32))).padEnd(74); + nameStr = ' '.repeat(40); + + line = `${line}\n${offsetStr}${value}${nameStr}`; + } + + // Append to hex value + hexValue = `${hexValue}\n${line}`; + offset += size; + } + + return hexValue; + } + + private generateCondensedHexString(): string { + let selectorBuffer = ethUtil.toBuffer(this.selector); + if (this.root === undefined) { + throw new Error('expected root'); + } + + const valueQueue = this.createQueue(this.root); + const valueBufs: Buffer[] = [selectorBuffer]; + let block: CalldataBlock | undefined; + while ((block = valueQueue.pop()) !== undefined) { + valueBufs.push(block.toBuffer()); + } + + const combinedBuffers = Buffer.concat(valueBufs); + const hexValue = ethUtil.bufferToHex(combinedBuffers); + return hexValue; + } + + public optimize() { + 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 = this.createQueue(this.root); + let block: CalldataBlock | undefined; + while ((block = subtreeQueue.popBack()) !== undefined) { + if (block instanceof DependentCalldataBlock) { + const blockHashBuf = block.getDependency().computeHash(); + const blockHash = ethUtil.bufferToHex(blockHashBuf); + if (blockHash in blocksByHash) { + const blockWithSameHash = blocksByHash[blockHash]; + if (blockWithSameHash !== block.getDependency()) { + block.setAlias(blockWithSameHash); + } + } + continue; + } + + const blockHashBuf = block.computeHash(); + const blockHash = ethUtil.bufferToHex(blockHashBuf); + if (blockHash in blocksByHash === false) { + blocksByHash[blockHash] = block; + } + } + } + + public toHexString(): string { + if (this.root === undefined) { + throw new Error('expected root'); + } + + if (this.rules.optimize) this.optimize(); + + const offsetQueue = this.createQueue(this.root); + let block: CalldataBlock | undefined; + let offset = 0; + while ((block = offsetQueue.pop()) !== undefined) { + 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 toAnnotatedString(): string { + return ""; + } + + public setRoot(block: MemberCalldataBlock) { + this.root = block; + this.sizeInBytes += block.getSizeInBytes(); + } + + public setSelector(selector: string) { + // Ensure we have a 0x prefix + if (selector.startsWith('0x')) { + this.selector = selector; + } else { + this.selector = `$0x${selector}`; + } + + // The selector must be 10 characters: '0x' followed by 4 bytes (two hex chars per byte) + if (this.selector.length !== 10) { + throw new Error(`Invalid selector '${this.selector}'`); + } + this.sizeInBytes += 8; + } +} + +export class RawCalldata { + private value: Buffer; + private offset: number; // tracks current offset into raw calldata; used for parsing + private selector: string; + private scopes: Queue; + + constructor(value: string | Buffer) { + if (typeof value === 'string' && !value.startsWith('0x')) { + throw new Error(`Expected raw calldata to start with '0x'`); + } + const valueBuf = ethUtil.toBuffer(value); + this.selector = ethUtil.bufferToHex(valueBuf.slice(0, 4)); + this.value = valueBuf.slice(4); // disregard selector + this.offset = 0; + this.scopes = new Queue(); + this.scopes.push(0); + } + + 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) { + this.offset = offsetInBytes; + } + + public startScope() { + this.scopes.pushFront(this.offset); + } + + public endScope() { + this.scopes.pop(); + } + + public getOffset(): number { + return this.offset; + } + + public toAbsoluteOffset(relativeOffset: 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 -- cgit v1.2.3