Split Calldata into multiple files - 1 class per file
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
224
packages/utils/src/abi_encoder/calldata/calldata.ts
Normal file
224
packages/utils/src/abi_encoder/calldata/calldata.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
77
packages/utils/src/abi_encoder/calldata/calldata_block.ts
Normal file
77
packages/utils/src/abi_encoder/calldata/calldata_block.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './dependent_calldata_block';
|
||||
export * from './member_calldata_block';
|
||||
export * from './payload_calldata_block';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
6
packages/utils/src/abi_encoder/calldata/index.ts
Normal file
6
packages/utils/src/abi_encoder/calldata/index.ts
Normal file
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
82
packages/utils/src/abi_encoder/calldata/raw_calldata.ts
Normal file
82
packages/utils/src/abi_encoder/calldata/raw_calldata.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { EncodingRules, DecodingRules } from './calldata';
|
||||
export { EncodingRules, DecodingRules } from './utils/rules';
|
||||
export * from './evm_data_type_factory';
|
||||
|
||||
39
packages/utils/src/abi_encoder/utils/queue.ts
Normal file
39
packages/utils/src/abi_encoder/utils/queue.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
8
packages/utils/src/abi_encoder/utils/rules.ts
Normal file
8
packages/utils/src/abi_encoder/utils/rules.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface DecodingRules {
|
||||
structsAsObjects: boolean;
|
||||
}
|
||||
|
||||
export interface EncodingRules {
|
||||
optimize?: boolean;
|
||||
annotate?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user