diff --git a/packages/truffle-decode-utils/.gitignore b/packages/truffle-codec-utils/.gitignore similarity index 100% rename from packages/truffle-decode-utils/.gitignore rename to packages/truffle-codec-utils/.gitignore diff --git a/packages/truffle-decode-utils/.npmignore b/packages/truffle-codec-utils/.npmignore similarity index 100% rename from packages/truffle-decode-utils/.npmignore rename to packages/truffle-codec-utils/.npmignore diff --git a/packages/truffle-decode-utils/LICENSE b/packages/truffle-codec-utils/LICENSE similarity index 100% rename from packages/truffle-decode-utils/LICENSE rename to packages/truffle-codec-utils/LICENSE diff --git a/packages/truffle-decode-utils/README.md b/packages/truffle-codec-utils/README.md similarity index 100% rename from packages/truffle-decode-utils/README.md rename to packages/truffle-codec-utils/README.md diff --git a/packages/truffle-decode-utils/package.json b/packages/truffle-codec-utils/package.json similarity index 63% rename from packages/truffle-decode-utils/package.json rename to packages/truffle-codec-utils/package.json index e5d9457f504..ae68bf9ab0b 100644 --- a/packages/truffle-decode-utils/package.json +++ b/packages/truffle-codec-utils/package.json @@ -1,12 +1,13 @@ { - "name": "truffle-decode-utils", - "description": "Utilities for decoding data from the EVM", - "license": "MIT", - "author": "Truffle Suite ", - "homepage": "https://github.com/trufflesuite/truffle-decode-utils#readme", - "repository": "https://github.com/trufflesuite/truffle/tree/master/packages/truffle-decode-utils", - "bugs": { - "url": "https://github.com/trufflesuite/truffle-decode-utils/issues" + "name": "truffle-codec-utils", + "version": "1.0.15", + "description": "Utilities for decoding data from the EVM or encoding it for the EVM", + "dependencies": { + "bn.js": "^4.11.8", + "lodash.clonedeep": "^4.5.0", + "lodash.escaperegexp": "^4.1.2", + "semver": "^6.1.1", + "web3": "^1.2.1" }, "version": "1.0.16", "main": "dist/index.js", @@ -16,24 +17,26 @@ "start": "node_modules/.bin/tsc --watch", "test": "echo \"No test specified\" && exit 0;" }, - "types": "src/index.ts", - "dependencies": { - "bn.js": "^4.11.8", - "lodash.clonedeep": "^4.5.0", - "lodash.escaperegexp": "^4.1.2", - "web3": "1.2.1", - "web3-eth-abi": "1.0.0-beta.52" + "repository": "https://github.com/trufflesuite/truffle/tree/master/packages/truffle-codec-utils", + "author": "Truffle Suite ", + "license": "MIT", + "bugs": { + "url": "https://github.com/trufflesuite/truffle-codec-utils/issues" }, + "homepage": "https://github.com/trufflesuite/truffle-codec-utils#readme", + "publishConfig": { + "access": "public" + }, + "types": "src/index.ts", "devDependencies": { "@types/bn.js": "^4.11.2", "@types/lodash.clonedeep": "^4.5.4", "@types/lodash.escaperegexp": "^4.1.6", "@types/semver": "^6.0.0", - "@types/web3": "^1.0.5", + "@types/web3": "^1.0.19", + "truffle-contract-schema": "^3.0.11", + "json-schema-to-typescript": "^6.1.3", "typescript": "^3.5.1" }, - "publishConfig": { - "access": "public" - }, "gitHead": "b207efb3c1409746537293b3e0fc27350029188e" } diff --git a/packages/truffle-codec-utils/src/abi.ts b/packages/truffle-codec-utils/src/abi.ts new file mode 100644 index 00000000000..9ce7589a8c5 --- /dev/null +++ b/packages/truffle-codec-utils/src/abi.ts @@ -0,0 +1,198 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:abi"); + +import { Abi as SchemaAbi } from "truffle-contract-schema/spec"; +import { EVM as EVMUtils } from "./evm"; +import { AstDefinition, AstReferences, Mutability } from "./ast"; +import { definitionToAbi } from "./definition2abi"; +import Web3 from "web3"; + +//NOTE: SchemaAbi is kind of loose and a pain to use. +//So we'll generally coerce things to Abi before use. +//(we combine this with adding a "function" tag for +//entries that lack it) + +export namespace AbiUtils { + + export type Abi = AbiEntry[]; + + export type AbiEntry = FunctionAbiEntry | ConstructorAbiEntry | FallbackAbiEntry | EventAbiEntry; + + export interface FunctionAbiEntry { + type: "function"; + name: string; + inputs: AbiParameter[]; + outputs: AbiParameter[]; + stateMutability?: Mutability; //only in newer ones + constant?: boolean; //only in older ones + payable?: boolean; //only in older ones + } + + export interface ConstructorAbiEntry { + type: "constructor"; + inputs: AbiParameter[]; + stateMutability?: "payable" | "nonpayable"; //only in newer ones + payable?: boolean; //only in older ones + } + + export interface FallbackAbiEntry { + type: "fallback"; + stateMutability?: "payable" | "nonpayable"; //only in newer ones + payable?: boolean; //only in older ones + } + + export interface EventAbiEntry { + type: "event"; + name: string; + inputs: AbiParameter[]; + anonymous: boolean; + } + + export interface AbiParameter { + name: string; + type: string; + indexed?: boolean; //only present for inputs + components?: AbiParameter[]; //only preset for tuples (structs) + } + + export interface FunctionAbiBySelectors { + [selector: string]: FunctionAbiEntry + } + + export const DEFAULT_CONSTRUCTOR_ABI: ConstructorAbiEntry = { + type: "constructor", + inputs: [], + stateMutability: "nonpayable", + payable: false + }; + + export const DEFAULT_FALLBACK_ABI: FallbackAbiEntry = { + type: "fallback", + stateMutability: "nonpayable", + payable: false + }; + + export function schemaAbiToAbi(abiLoose: SchemaAbi): Abi { + return abiLoose.map( + entry => entry.type + ? entry + : {type: "function", ...entry} + ); + } + + //note the return value only includes functions! + export function computeSelectors(abi: Abi | undefined): FunctionAbiBySelectors | undefined { + if(abi === undefined) { + return undefined; + } + return Object.assign({}, + ...abi.filter( + (abiEntry: AbiEntry) => abiEntry.type === "function" + ).map( + (abiEntry: FunctionAbiEntry) => ({ [abiSelector(abiEntry)]: abiEntry }) + ) + ) + } + + //does this ABI have a payable fallback function? + export function abiHasPayableFallback(abi: Abi | undefined): boolean | undefined { + if(abi === undefined) { + return undefined; + } + return abiMutability(getFallbackEntry(abi)) === "payable"; + } + + //gets the fallback entry; if there isn't one, returns a default one + export function getFallbackEntry(abi: Abi): FallbackAbiEntry { + //no idea why TS's type inference is failing on this one... + return abi.find(abiEntry => abiEntry.type === "fallback") || DEFAULT_FALLBACK_ABI; + } + + export function fallbackAbiForPayability(payable: boolean): FallbackAbiEntry { + return { + type: "fallback", + stateMutability: payable ? "payable" : "nonpayable", + payable + }; + } + + //shim for old abi versions + function abiMutability(abiEntry: FunctionAbiEntry | ConstructorAbiEntry | FallbackAbiEntry): Mutability { + if(abiEntry.stateMutability !== undefined) { + return abiEntry.stateMutability; + } + if(abiEntry.payable) { + return "payable"; + } + if(abiEntry.type === "function" && abiEntry.constant) { + return "view"; + } + return "nonpayable"; + } + + //NOTE: this function returns the written out SIGNATURE, not the SELECTOR + export function abiSignature(abiEntry: FunctionAbiEntry | EventAbiEntry): string { + return abiEntry.name + abiTupleSignature(abiEntry.inputs); + } + + export function abiTupleSignature(parameters: AbiParameter[]): string { + let components = parameters.map(abiTypeSignature); + return "(" + components.join(",") + ")"; + } + + function abiTypeSignature(parameter: AbiParameter): string { + let tupleMatch = parameter.type.match(/tuple(.*)/); + if(tupleMatch === null) { //does not start with "tuple" + return parameter.type; + } + else { + let tail = tupleMatch[1]; //everything after "tuple" + let tupleSignature = abiTupleSignature(parameter.components); + return tupleSignature + tail; + } + } + + export function abiSelector(abiEntry: FunctionAbiEntry | EventAbiEntry): string { + let signature = abiSignature(abiEntry); + //NOTE: web3's soliditySha3 has a problem if the empty + //string is passed in. Fortunately, that should never happen here. + let hash = Web3.utils.soliditySha3({type: "string", value: signature}); + switch(abiEntry.type) { + case "event": + return hash; + case "function": + return hash.slice(0, 2 + 2 * EVMUtils.SELECTOR_SIZE); //arithmetic to account for hex string + } + } + + //note: undefined does not match itself :P + export function abisMatch(entry1: AbiEntry | undefined, entry2: AbiEntry | undefined): boolean { + //we'll consider two abi entries to match if they have the same + //type, name (if applicable), and inputs (if applicable). + //since there's already a signature function, we can just use that. + if(!entry1 || !entry2) { + return false; + } + if(entry1.type !== entry2.type) { + return false; + } + switch(entry1.type) { + case "function": + case "event": + return abiSignature(entry1) === abiSignature(entry2); + case "constructor": + return abiTupleSignature(entry1.inputs) === abiTupleSignature((entry2).inputs); + case "fallback": + return true; + } + } + + export function definitionMatchesAbi(abiEntry: AbiEntry, definition: AstDefinition, referenceDeclarations: AstReferences): boolean { + return abisMatch(abiEntry, definitionToAbi(definition, referenceDeclarations)); + } + + export function topicsCount(abiEntry: EventAbiEntry): number { + let selectorCount = abiEntry.anonymous ? 0 : 1; //if the event is not anonymous, we must account for the selector + return abiEntry.inputs.filter(({ indexed }) => indexed).length + selectorCount; + } +} diff --git a/packages/truffle-decode-utils/src/ast.ts b/packages/truffle-codec-utils/src/ast.ts similarity index 90% rename from packages/truffle-decode-utils/src/ast.ts rename to packages/truffle-codec-utils/src/ast.ts index 666e4c79fd4..06fb492453a 100644 --- a/packages/truffle-decode-utils/src/ast.ts +++ b/packages/truffle-codec-utils/src/ast.ts @@ -31,6 +31,12 @@ export interface AstDefinition { //things stateMutability?: Mutability; referencedDeclaration?: any; + parameters?: { + parameters: AstDefinition[]; + }; + returnParameters?: { + parameters: AstDefinition[]; + }; parameterTypes?: { parameters: AstDefinition[]; }; @@ -39,6 +45,8 @@ export interface AstDefinition { }; keyType?: AstDefinition; valueType?: AstDefinition; + indexed?: boolean; + anonymous?: boolean; contractKind?: ContractKind; isConstructor?: boolean; [k: string]: any; diff --git a/packages/truffle-codec-utils/src/compiler.ts b/packages/truffle-codec-utils/src/compiler.ts new file mode 100644 index 00000000000..9df329800bd --- /dev/null +++ b/packages/truffle-codec-utils/src/compiler.ts @@ -0,0 +1,6 @@ +export interface CompilerVersion { + name?: string; + version?: string; + //NOTE: both these should really be present, + //but they need to be optional for compilation reasons +} diff --git a/packages/truffle-decode-utils/src/constants.ts b/packages/truffle-codec-utils/src/constants.ts similarity index 78% rename from packages/truffle-decode-utils/src/constants.ts rename to packages/truffle-codec-utils/src/constants.ts index 9b03ccfd527..c920272064a 100644 --- a/packages/truffle-decode-utils/src/constants.ts +++ b/packages/truffle-codec-utils/src/constants.ts @@ -3,7 +3,7 @@ import BN from "bn.js"; export namespace Constants { export const WORD_SIZE = 0x20; export const ADDRESS_SIZE = 20; - export const SELECTOR_SIZE = 4; + export const SELECTOR_SIZE = 4; //function selectors, not event selectors export const PC_SIZE = 4; export const MAX_WORD = new BN(-1).toTwos(WORD_SIZE * 8); export const ZERO_ADDRESS = "0x" + "00".repeat(ADDRESS_SIZE); diff --git a/packages/truffle-decode-utils/src/contexts.ts b/packages/truffle-codec-utils/src/contexts.ts similarity index 66% rename from packages/truffle-decode-utils/src/contexts.ts rename to packages/truffle-codec-utils/src/contexts.ts index 3e5d902fbb9..b82974a3897 100644 --- a/packages/truffle-decode-utils/src/contexts.ts +++ b/packages/truffle-codec-utils/src/contexts.ts @@ -1,23 +1,27 @@ import debugModule from "debug"; -const debug = debugModule("decode-utils:contexts"); +const debug = debugModule("codec-utils:contexts"); -import { Abi } from "truffle-contract-schema/spec"; -import { AbiCoder } from "web3-eth-abi"; -import { AbiItem } from "web3-utils"; -const abiCoder = new AbiCoder(); import escapeRegExp from "lodash.escaperegexp"; import { EVM } from "./evm"; -import { ContractKind } from "./ast"; +import { Abi as SchemaAbi } from "truffle-contract-schema/spec"; +import { AbiUtils } from "./abi"; +import { Types } from "./types/types"; +import { AstDefinition, AstReferences, ContractKind } from "./ast"; +import { CompilerVersion } from "./compiler"; export namespace Contexts { - export type Contexts = DecoderContexts | DebuggerContexts; + export type Contexts = DecoderContexts | DebuggerContexts | DecoderContextsById; export type Context = DecoderContext | DebuggerContext; export interface DecoderContexts { - [contractId: number]: DecoderContext; + [context: string]: DecoderContext; + } + + export interface DecoderContextsById { + [id: number]: DecoderContext; } export interface DebuggerContexts { @@ -33,7 +37,7 @@ export namespace Contexts { contractName?: string; contractId?: number; contractKind?: ContractKind; //note: should never be "interface" - abi?: FunctionAbiWithSignatures; + abi?: AbiUtils.FunctionAbiBySelectors; payable?: boolean; compiler?: CompilerVersion; } @@ -47,95 +51,15 @@ export namespace Contexts { contractName?: string; contractId?: number; contractKind?: ContractKind; //note: should never be "interface" - abi?: Abi; + abi?: SchemaAbi; sourceMap?: string; primarySource?: number; compiler?: CompilerVersion; payable?: boolean; } - export interface CompilerVersion { - name?: string; - version?: string; - //NOTE: both these should really be present, - //but they need to be optional for compilation reasons - } - - //really, we should import the ABI spec from truffle-contract-schema; - //unfotunately, that doesn't include signatures. so here's a sloppy - //recreation that includes those; sorry for the duplication, but this seems - //the easiest way offhand - //(note that this is explicitly restricted to type function, since that's all - //we care about here) - export interface FunctionAbiEntryWithSignature { - constant: boolean; - inputs: FunctionAbiParameter[]; - name: string; - outputs: FunctionAbiParameter[]; - payable: boolean; - stateMutability: "pure" | "view" | "nonpayable" | "payable"; - type: "function"; //again, event/fallback/constructor are excluded - signature: string; - } - - export interface FunctionAbiWithSignatures { - [signature: string]: FunctionAbiEntryWithSignature - } - - //again, this is only for function parameters, not event parameters - export interface FunctionAbiParameter { - components?: FunctionAbiParameter[]; - name: string; - type: string; //restricting this is a lot of the work of the real spec :P - [k: string]: any //this really should *not* be here, but it's in the spec - //for some reason, so we have to be able to handle it :-/ - } - - export function abiToFunctionAbiWithSignatures(abi: Abi | undefined): FunctionAbiWithSignatures | undefined { - if(abi === undefined) { - return undefined; - } - return Object.assign({}, - ...abi.filter( - abiEntry => abiEntry.type === "function" - ).map( - abiEntry => { - let signature: string; - //let's try forcing it and see if it already has a signature :P - signature = (abiEntry).signature; - debug("signature read: %s", signature); - //if not, compute it ourselves - if(signature === undefined) { - signature = abiCoder.encodeFunctionSignature(abiEntry); - //Notice the type coercion -- web3 and our schema describe things a little - //differently, and TypeScript complains. I think we just have to force it, - //sorry. - debug("signature computed: %s", signature); - } - return { - [signature]: { - ...abiEntry, - signature - } - }; - } - ) - ) - } - - //does this ABI have a payable fallback function? - export function abiHasPayableFallback(abi: Abi | undefined): boolean | undefined { - if(abi === undefined) { - return undefined; - } - return abi.some( - abiEntry => abiEntry.type === "fallback" && - (abiEntry.stateMutability === "payable" || abiEntry.payable) - ); - } - - //I split these next two apart because the type system was giving me rouble - export function findDecoderContext(contexts: DecoderContexts, binary: string): DecoderContext | null { + //I split these next two apart because the type system was giving me trouble + export function findDecoderContext(contexts: DecoderContexts | DecoderContextsById, binary: string): DecoderContext | null { debug("binary %s", binary); let context = Object.values(contexts).find(context => matchContext(context, binary) @@ -195,6 +119,7 @@ export namespace Contexts { let newContexts: Contexts = {...contexts}; debug("contexts cloned"); + debug("cloned contexts: %O", newContexts); //next, we get all the library names and sort them descending by length. //We're going to want to go in descending order of length so that we @@ -266,4 +191,16 @@ export namespace Contexts { //finally, return this mess! return newContexts; } + + export function contextToType(context: DecoderContext | DebuggerContext): Types.ContractType { + return { + typeClass: "contract", + kind: "native", + id: context.contractId.toString(), + typeName: context.contractName, + contractKind: context.contractKind, + payable: context.payable + }; + } + } diff --git a/packages/truffle-codec-utils/src/conversion.ts b/packages/truffle-codec-utils/src/conversion.ts new file mode 100644 index 00000000000..f7bca755132 --- /dev/null +++ b/packages/truffle-codec-utils/src/conversion.ts @@ -0,0 +1,278 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:conversion"); + +import BN from "bn.js"; +import Web3 from "web3"; +import { Constants } from "./constants"; +import { Types } from "./types/types"; +import { Values } from "./types/values"; +import { enumFullName } from "./types/inspect"; + +export namespace Conversion { + + /** + * @param bytes - undefined | string | number | BN | Uint8Array + * @return {BN} + */ + export function toBN(bytes: undefined | string | number | BN | Uint8Array): BN { + if (bytes === undefined) { + return undefined; + } else if (typeof bytes == "string") { + return new BN(bytes, 16); + } else if (typeof bytes == "number" || BN.isBN(bytes)) { + return new BN(bytes); + } else if (bytes.reduce) { + return bytes.reduce( + (num: BN, byte: number) => num.shln(8).addn(byte), + new BN(0) + ); + } + } + + /** + * @param bytes - Uint8Array + * @return {BN} + */ + export function toSignedBN(bytes: Uint8Array): BN { + if (bytes[0] < 0x80) { // if first bit is 0 + return toBN(bytes); + } else { + return toBN(bytes.map( (b) => 0xff - b )).addn(1).neg(); + } + } + + /** + * @param bytes - Uint8Array | BN + * @param padLength - number - minimum desired byte length (left-pad with zeroes) + * @return {string} + */ + export function toHexString(bytes: Uint8Array | BN, padLength: number = 0): string { + + if (BN.isBN(bytes)) { + bytes = toBytes(bytes); + } + + const pad = (s: string) => `${"00".slice(0, 2 - s.length)}${s}`; + + // 0 1 2 3 4 + // 0 1 2 3 4 5 6 7 + // bytes.length: 5 - 0x( e5 c2 aa 09 11 ) + // length (preferred): 8 - 0x( 00 00 00 e5 c2 aa 09 11 ) + // `--.---' + // offset 3 + if (bytes.length < padLength) { + let prior = bytes; + bytes = new Uint8Array(padLength); + + bytes.set(prior, padLength - prior.length); + } + + debug("bytes: %o", bytes); + + let string = bytes.reduce( + (str, byte) => `${str}${pad(byte.toString(16))}`, "" + ); + + return `0x${string}`; + } + + export function toAddress(bytes: Uint8Array | string): string { + + if(typeof bytes === "string") { + //in this case, we can do some simple string manipulation and + //then pass to web3 + let hex = bytes; //just renaming for clarity + if (hex.startsWith("0x")) { + hex = hex.slice(2); + } + if(hex.length < 2 * Constants.ADDRESS_SIZE) + { + hex = hex.padStart(2 * Constants.ADDRESS_SIZE, "0"); + } + if(hex.length > 2 * Constants.ADDRESS_SIZE) + { + hex = "0x" + hex.slice(hex.length - 2 * Constants.ADDRESS_SIZE); + } + return Web3.utils.toChecksumAddress(hex); + } + //otherwise, we're in the Uint8Array case, which we can't fully handle ourself + + //truncate *on left* to 20 bytes + if(bytes.length > Constants.ADDRESS_SIZE) { + bytes = bytes.slice(bytes.length - Constants.ADDRESS_SIZE, bytes.length); + } + + //now, convert to hex string and apply checksum case that second argument + //(which ensures it's padded to 20 bytes) shouldn't actually ever be + //needed, but I'll be safe and include it + return Web3.utils.toChecksumAddress(toHexString(bytes, Constants.ADDRESS_SIZE)); + } + + export function toBytes(data: BN | string | number, length: number = 0): Uint8Array { + //note that length is a minimum output length + //strings will be 0-padded on left + //numbers/BNs will be sign-padded on left + //NOTE: if a number/BN is passed in that is too big for the given length, + //you will get an error! + //(note that strings passed in should be hex strings; this is not for converting + //generic strings to hex) + + if (typeof data === "string") { + + let hex = data; //renaming for clarity + + if (hex.startsWith("0x")) { + hex = hex.slice(2); + } + + if(hex === "") { + //this special case is necessary because the match below will return null, + //not an empty array, when given an empty string + return new Uint8Array(0); + } + + if (hex.length % 2 == 1) { + hex = `0${hex}`; + } + + let bytes = new Uint8Array( + hex.match(/.{2}/g) + .map( (byte) => parseInt(byte, 16) ) + ); + + if (bytes.length < length) { + let prior = bytes; + bytes = new Uint8Array(length); + bytes.set(prior, length - prior.length); + } + + return bytes; + } + else { + // BN/number case + if(typeof data === "number") { + data = new BN(data); + } + + //note that the argument for toTwos is given in bits + return new Uint8Array(data.toTwos(length * 8).toArrayLike(Buffer, "be", length)); //big-endian + } + } + + //for convenience: invokes the nativize method on all the given variables + export function nativizeVariables(variables: {[name: string]: Values.Result}): {[name: string]: any} { + return Object.assign({}, ...Object.entries(variables).map( + ([name, value]) => ({[name]: nativize(value)}) + )); + } + + //converts out of range booleans to true; something of a HACK + //NOTE: does NOT do this recursively inside structs, arrays, etc! + //I mean, those aren't elementary and therefore aren't in the domain + //anyway, but still + export function cleanBool(result: Values.ElementaryResult): Values.ElementaryResult { + switch(result.kind) { + case "value": + return result; + case "error": + switch(result.error.kind) { + case "BoolOutOfRangeError": + //return true + return { + type: result.type, + kind: "value", + value: { + asBool: true + } + }; + default: + return result; + } + } + } + + //HACK! Avoid using! Only use this if: + //1. you absolutely have to, or + //2. it's just testing, not real code + export function nativize(result: Values.Result): any { + if(result.kind === "error") { + return undefined; + } + switch(result.type.typeClass) { + case "uint": + case "int": + return (result).value.asBN.toNumber(); //WARNING + case "bool": + return (result).value.asBool; + case "bytes": + return (result).value.asHex; + case "address": + return (result).value.asAddress; + case "string": { + let coercedResult = result; + switch(coercedResult.value.kind) { + case "valid": + return coercedResult.value.asString; + case "malformed": + // this will turn malformed utf-8 into replacement characters (U+FFFD) (WARNING) + // note we need to cut off the 0x prefix + return Buffer.from(coercedResult.value.asHex.slice(2), 'hex').toString(); + } + } + //fixed and ufixed are skipped for now + case "array": //WARNING: circular case not handled; will loop infinitely + return (result).value.map(nativize); + case "mapping": + return Object.assign({}, ...(result).value.map( + ({key, value}) => ({[nativize(key).toString()]: nativize(value)}) + )); + case "struct": //WARNING: circular case not handled; will loop infinitely + return Object.assign({}, ...(result).value.map( + ({name, value}) => ({[name]: nativize(value)}) + )); + case "magic": + return Object.assign({}, ...Object.entries((result).value).map( + ([key, value]) => ({[key]: nativize(value)}) + )); + case "enum": + return enumFullName(result); + case "contract": { + let coercedResult = result; + switch(coercedResult.value.kind) { + case "known": + return `${coercedResult.value.class.typeName}(${coercedResult.value.address})`; + case "unknown": + return coercedResult.value.address; + } + break; //to satisfy typescript + } + case "function": + switch(result.type.visibility) { + case "external": { + let coercedResult = result; + switch(coercedResult.value.kind) { + case "known": + return `${coercedResult.value.contract.class.typeName}(${coercedResult.value.contract.address}).${coercedResult.value.abi.name}` + case "invalid": + return `${coercedResult.value.contract.class.typeName}(${coercedResult.value.contract.address}).call(${coercedResult.value.selector}...)` + case "unknown": + return `${coercedResult.value.contract.address}.call(${coercedResult.value.selector}...)` + } + } + case "internal": { + let coercedResult = result; + switch(coercedResult.value.kind) { + case "function": + return `${coercedResult.value.definedIn.typeName}.${coercedResult.value.name}`; + case "exception": + return coercedResult.value.deployedProgramCounter === 0 + ? `` + : `assert(false)`; + case "unknown": + return ``; + } + } + } + } + } +} diff --git a/packages/truffle-decode-utils/src/definition.ts b/packages/truffle-codec-utils/src/definition.ts similarity index 92% rename from packages/truffle-decode-utils/src/definition.ts rename to packages/truffle-codec-utils/src/definition.ts index 6ffd564650a..dfdf50d6fea 100644 --- a/packages/truffle-decode-utils/src/definition.ts +++ b/packages/truffle-codec-utils/src/definition.ts @@ -1,9 +1,10 @@ import debugModule from "debug"; -const debug = debugModule("decode-utils:definition"); +const debug = debugModule("codec-utils:definition"); import { EVM as EVMUtils } from "./evm"; import { AstDefinition, Scopes, Visibility, Mutability, Location, ContractKind } from "./ast"; import { Contexts } from "./contexts"; +import { CompilerVersion } from "./compiler"; import BN from "bn.js"; import cloneDeep from "lodash.clonedeep"; import semver from "semver"; @@ -28,6 +29,15 @@ export namespace Definition { return typeIdentifier(definition).match(/t_([^$_0-9]+)/)[1]; } + /** + * similar to typeClass, but includes any numeric qualifiers + * e.g.: + * `t_uint256` becomes `uint256` + */ + export function typeClassLongForm(definition: AstDefinition): string { + return typeIdentifier(definition).match(/t_([^$_]+)/)[1]; + } + //for user-defined types -- structs, enums, contracts //often you can get these from referencedDeclaration, but not //always @@ -73,7 +83,7 @@ export namespace Definition { return num; default: - // debug("Unknown type for size specification: %s", typeIdentifier(definition)); + debug("Unknown type for size specification: %s", typeIdentifier(definition)); } } @@ -132,11 +142,14 @@ export namespace Definition { } //HACK: you can set compiler to null to force nonpayable - export function isAddressPayable(definition: AstDefinition, compiler: Contexts.CompilerVersion | null): boolean { + export function isAddressPayable(definition: AstDefinition, compiler: CompilerVersion | null): boolean { if(compiler === null) { return false; } - if(semver.satisfies(compiler.version, ">=0.5.0", {includePrerelease: true})) { + if(semver.satisfies(compiler.version, "~0.5 || >=0.5.0", {includePrerelease: true})) { + //note that we use ~0.5 || >=0.5.0 to make sure we include prerelease versions of + //0.5.0 (no, that is *not* what the includePrerelease flag doesn; that allows + //prerelease versions to be included *at all*) return typeIdentifier(definition) === "t_address_payable"; } else { @@ -195,7 +208,7 @@ export namespace Definition { } //adds "_ptr" on to the end of type identifiers that might need it; note that - //this operats on identifiers, not defintions + //this operats on identifiers, not definitions export function restorePtr(identifier: string): string { return identifier.replace(/(?<=_(storage|memory|calldata))$/, "_ptr"); } @@ -281,6 +294,8 @@ export namespace Definition { } //returns input parameters, then output parameters + //NOTE: ONLY FOR VARIABLE DECLARATIONS OF FUNCTION TYPE + //NOT FOR FUNCTION DEFINITIONS export function parameters(definition: AstDefinition): [AstDefinition[], AstDefinition[]] { let typeObject = definition.typeName || definition; return [typeObject.parameterTypes.parameters, typeObject.returnParameterTypes.parameters]; @@ -309,10 +324,7 @@ export namespace Definition { //returns undefined if you don't give it a FunctionDefinition or //VariableDeclaration export function mutability(node: AstDefinition): Mutability | undefined { - if(node.typeName) { - //for variable declarations, e.g. - node = node.typeName; - } + node = node.typeName || node; if(node.nodeType !== "FunctionDefinition" && node.nodeType !== "FunctionTypeName") { return undefined; } @@ -403,7 +415,7 @@ export namespace Definition { nodeType: "VariableDeclaration", typeDescriptions: { typeIdentifier: "t_contract$_" + formattedName + "_$" + contractId, - typeString: contractKind + " " + contractName + typeString: contractKind + " " + contractName } } } diff --git a/packages/truffle-codec-utils/src/definition2abi.ts b/packages/truffle-codec-utils/src/definition2abi.ts new file mode 100644 index 00000000000..1e73c95ebd7 --- /dev/null +++ b/packages/truffle-codec-utils/src/definition2abi.ts @@ -0,0 +1,251 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:definition2abi"); + +import { AstDefinition, AstReferences } from "./ast"; +import { AbiUtils } from "./abi"; +import { Definition } from "./definition"; +import { UnknownUserDefinedTypeError } from "./errors"; + +//the main function. just does some dispatch. +//returns undefined on bad input +export function definitionToAbi(node: AstDefinition, referenceDeclarations: AstReferences): AbiUtils.AbiEntry | undefined { + switch(node.nodeType) { + case "FunctionDefinition": + if(node.visibility === "public" || node.visibility === "external") { + return functionDefinitionToAbi(node, referenceDeclarations); + } + else { + return undefined; + } + case "EventDefinition": + return eventDefinitionToAbi(node, referenceDeclarations); + case "VariableDeclaration": + if(node.visibility === "public") { + return getterDefinitionToAbi(node, referenceDeclarations); + } + else { + return undefined; + } + default: + return undefined; + } +} + +//note: not for FunctionTypeNames or VariableDeclarations +function functionDefinitionToAbi(node: AstDefinition, referenceDeclarations: AstReferences): AbiUtils.FunctionAbiEntry | AbiUtils.ConstructorAbiEntry | AbiUtils.FallbackAbiEntry { + let kind = Definition.functionKind(node); + let stateMutability = Definition.mutability(node); + let payable = stateMutability === "payable"; + let constant = stateMutability === "view" || stateMutability == "pure"; + let inputs; + switch(kind) { + case "function": + let name = node.name; + let outputs = parametersToAbi(node.returnParameters.parameters, referenceDeclarations); + inputs = parametersToAbi(node.parameters.parameters, referenceDeclarations); + return { + type: "function", + name, + inputs, + outputs, + stateMutability, + constant, + payable + }; + case "constructor": + inputs = parametersToAbi(node.parameters.parameters, referenceDeclarations); + //note: need to coerce because of mutability restrictions + return { + type: "constructor", + inputs, + stateMutability, + payable + }; + case "fallback": + //note: need to coerce because of mutability restrictions + return { + type: "fallback", + stateMutability, + payable + }; + } +} + +function eventDefinitionToAbi(node: AstDefinition, referenceDeclarations: AstReferences): AbiUtils.EventAbiEntry { + let inputs = parametersToAbi(node.parameters.parameters, referenceDeclarations, true); + let name = node.name; + let anonymous = node.anonymous; + return { + type: "event", + inputs, + name, + anonymous + }; +} + +function parametersToAbi(nodes: AstDefinition[], referenceDeclarations: AstReferences, checkIndexed: boolean = false): AbiUtils.AbiParameter[] { + return nodes.map(node => parameterToAbi(node, referenceDeclarations, checkIndexed)); +} + +function parameterToAbi(node: AstDefinition, referenceDeclarations: AstReferences, checkIndexed: boolean = false): AbiUtils.AbiParameter { + let name = node.name; //may be the empty string... or even undefined for a base type + let components: AbiUtils.AbiParameter[]; + let indexed: boolean; + if(checkIndexed) { + indexed = node.indexed; //note: may be undefined for a base type + } + //is this an array? if so use separate logic + if(Definition.typeClass(node) === "array") { + let baseType = node.typeName ? node.typeName.baseType : node.baseType; + let baseAbi = parameterToAbi(baseType, referenceDeclarations, checkIndexed); + let arraySuffix = Definition.isDynamicArray(node) + ? `[]` + : `[${Definition.staticLength(node)}]`; + return { + name, + type: baseAbi.type + arraySuffix, + indexed, + components: baseAbi.components + }; + } + let abiTypeString = toAbiType(node, referenceDeclarations); + //otherwise... is it a struct? if so we need to populate components + if(Definition.typeClass(node) === "struct") { + let id = Definition.typeId(node); + let referenceDeclaration = referenceDeclarations[id]; + if(referenceDeclaration === undefined) { + let typeToDisplay = Definition.typeString(node); + throw new UnknownUserDefinedTypeError(id, typeToDisplay); + } + components = parametersToAbi(referenceDeclaration.members, referenceDeclarations, checkIndexed); + } + return { + name, //may be empty string but should only be undefined in recursive calls + type: abiTypeString, + indexed, //undefined if !checkedIndex + components //undefined if not a struct or (multidim) array of structs + }; +} + +//note: this is only meant for non-array types that can go in the ABI +//it returns how that type is notated in the ABI -- just the string, +//to be clear, not components of tuples +//again, NOT FOR ARRAYS +function toAbiType(node: AstDefinition, referenceDeclarations: AstReferences): string { + let basicType = Definition.typeClassLongForm(node); //get that whole first segment! + switch(basicType) { + case "contract": + return "address"; + case "struct": + return "tuple"; //the more detailed checking will be handled elsewhere + case "enum": + let referenceId = Definition.typeId(node); + let referenceDeclaration = referenceDeclarations[referenceId]; + if(referenceDeclaration === undefined) { + let typeToDisplay = Definition.typeString(node); + throw new UnknownUserDefinedTypeError(referenceId, typeToDisplay); + } + let numOptions = referenceDeclaration.members.length; + let bits = 8 * Math.ceil(Math.log2(numOptions) / 8); + return `uint${bits}`; + default: + return basicType; + //note that: int/uint/fixed/ufixed/bytes will have their size and such left on; + //address will have "payable" left off; + //external functions will be reduced to "function" (and internal functions shouldn't + //be passed in!) + //(mappings shouldn't be passed in either obviously) + //(nor arrays :P ) + } +} + +function getterDefinitionToAbi(node: AstDefinition, referenceDeclarations: AstReferences): AbiUtils.FunctionAbiEntry { + debug("getter node: %O", node); + let name = node.name; + let { inputs, outputs } = getterParameters(node, referenceDeclarations); + let inputsAbi = parametersToAbi(inputs, referenceDeclarations); + let outputsAbi = parametersToAbi(outputs, referenceDeclarations); + return { + type: "function", + name, + inputs: inputsAbi, + outputs: outputsAbi, + stateMutability: "view", + constant: true, + payable: false + }; +} + +//how getter parameters work: +//INPUT: +//types other than arrays and mappings take no input. +//array getters take uint256 input. mapping getters take input of their key type. +//if arrays, mappings, stacked, then takes multiple inputs, in order from outside +//to in. +//These parameters are unnamed. +//OUTPUT: +//if base type (beneath mappings & arrays) is not a struct, returns that. +//(This return parameter has no name -- it is *not* named for the variable!) +//if it is a struct, returns multiple outputs, one for each member of the struct, +//*except* arrays and mappings. (And they have names, the names of the members.) +//important note: inner structs within a struct are just returned, not +//partially destructured like the outermost struct! Yes, this is confusing. + +//here's a simplified function that just does the inputs. it's for use by the +//allocator. I'm keeping it separate because it doesn't require a +//referenceDeclarations argument. +export function getterInputs(node: AstDefinition): AstDefinition[] { + node = node.typeName || node; + let inputs: AstDefinition[] = []; + while(Definition.typeClass(node) === "array" || Definition.typeClass(node) === "mapping") { + let keyNode = Definition.keyDefinition(node); //note: if node is an array, this spoofs up a uint256 definition + inputs.push({...keyNode, name: ""}); //getter input params have no name + switch(Definition.typeClass(node)) { + case "array": + node = node.baseType; + break; + case "mapping": + node = node.valueType; + break; + } + } + return inputs; +} + +//again, despite the duplication, this function is kept separate from the +//more straightforward getterInputs function because, since it has to handle +//outputs too, it requires referenceDeclarations +function getterParameters(node: AstDefinition, referenceDeclarations: AstReferences): {inputs: AstDefinition[], outputs: AstDefinition[]} { + let baseNode: AstDefinition = node.typeName || node; + let inputs: AstDefinition[] = []; + while(Definition.typeClass(baseNode) === "array" || Definition.typeClass(baseNode) === "mapping") { + let keyNode = Definition.keyDefinition(baseNode); //note: if baseNode is an array, this spoofs up a uint256 definition + inputs.push({...keyNode, name: ""}); //again, getter input params have no name + switch(Definition.typeClass(baseNode)) { + case "array": + baseNode = baseNode.baseType; + break; + case "mapping": + baseNode = baseNode.valueType; + break; + } + } + //at this point, baseNode should hold the base type + //now we face the question: is it a struct? + if(Definition.typeClass(baseNode) === "struct") { + let id = Definition.typeId(baseNode); + let referenceDeclaration = referenceDeclarations[id]; + if(referenceDeclaration === undefined) { + let typeToDisplay = Definition.typeString(baseNode); + throw new UnknownUserDefinedTypeError(id, typeToDisplay); + } + let outputs = referenceDeclaration.members.filter( + member => Definition.typeClass(member) !== "array" && Definition.typeClass(member) !== "mapping" + ); + return { inputs, outputs }; //no need to wash name! + } + else { + //only one output; it's just the base node, with its name washed + return { inputs, outputs: [{...baseNode, name: ""}] }; + } +} diff --git a/packages/truffle-codec-utils/src/errors.ts b/packages/truffle-codec-utils/src/errors.ts new file mode 100644 index 00000000000..96015f3ec8b --- /dev/null +++ b/packages/truffle-codec-utils/src/errors.ts @@ -0,0 +1,11 @@ +export class UnknownUserDefinedTypeError extends Error { + public typeString: string; + public id: number; + constructor(id: number, typeString: string) { + const message = `Cannot locate definition for ${typeString}$ (ID ${id})`; + super(message); + this.name = "UnknownUserDefinedTypeError"; + this.id = id; + this.typeString = typeString; + } +} diff --git a/packages/truffle-decode-utils/src/evm.ts b/packages/truffle-codec-utils/src/evm.ts similarity index 71% rename from packages/truffle-decode-utils/src/evm.ts rename to packages/truffle-codec-utils/src/evm.ts index de8710a62da..182c095f794 100644 --- a/packages/truffle-decode-utils/src/evm.ts +++ b/packages/truffle-codec-utils/src/evm.ts @@ -1,5 +1,5 @@ import debugModule from "debug"; -const debug = debugModule("decode-utils:evm"); +const debug = debugModule("codec-utils:evm"); import BN from "bn.js"; import Web3 from "web3"; @@ -36,4 +36,21 @@ export namespace EVM { } return ConversionUtils.toBN(sha); } + + //checks if two bytearrays (which may be undefined) are equal. + //does not consider undefined to be equal to itself. + export function equalData(bytes1: Uint8Array | undefined, bytes2: Uint8Array | undefined): boolean { + if(!bytes1 || !bytes2) { + return false; + } + if(bytes1.length !== bytes2.length) { + return false; + } + for(let i = 0; i < bytes1.length; i++) { + if(bytes1[i] !== bytes2[i]) { + return false; + } + } + return true; + } } diff --git a/packages/truffle-decode-utils/src/index.ts b/packages/truffle-codec-utils/src/index.ts similarity index 57% rename from packages/truffle-decode-utils/src/index.ts rename to packages/truffle-codec-utils/src/index.ts index 7e1e2ea46c8..217a399df6c 100644 --- a/packages/truffle-decode-utils/src/index.ts +++ b/packages/truffle-codec-utils/src/index.ts @@ -3,6 +3,12 @@ export * from "./evm"; export * from "./definition"; export * from "./ast"; export * from "./contexts"; +export * from "./definition2abi"; +export * from "./abi"; +export * from "./compiler"; +export * from "./errors"; +export * from "./wrap"; export * from "./types/types"; export * from "./types/values"; export * from "./types/errors"; +export * from "./types/inspect"; diff --git a/packages/truffle-codec-utils/src/types/errors.ts b/packages/truffle-codec-utils/src/types/errors.ts new file mode 100644 index 00000000000..edfd909d290 --- /dev/null +++ b/packages/truffle-codec-utils/src/types/errors.ts @@ -0,0 +1,401 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:types:errors"); + +//error counterpart to values.ts + +//Note: Many of the errors defined here deliberately *don't* extend Error. +//This is because they're not for throwing. If you want to throw one, +//wrap it in a DecodingError. + +import BN from "bn.js"; +import { Types } from "./types"; +import { InspectOptions } from "./inspect"; +import util from "util"; +import { AstDefinition } from "../ast"; +import { Definition as DefinitionUtils } from "../definition"; + +export namespace Errors { + + /* + * SECTION 1: Generic types for values in general (including errors). + */ + + //For when we need to throw an error, here's a wrapper class that extends Error. + //Apologies about the confusing name, but I wanted something that would make + //sense should it not be caught and thus accidentally exposed to the outside. + export class DecodingError extends Error{ + error: ErrorForThrowing; + constructor(error: ErrorForThrowing) { + super(message(error)); + this.error = error; + this.name = "DecodingError"; + } + } + + export type ErrorResult = ElementaryErrorResult + | ArrayErrorResult | MappingErrorResult | StructErrorResult | MagicErrorResult + | EnumErrorResult + | ContractErrorResult | FunctionExternalErrorResult | FunctionInternalErrorResult; + + export type DecoderError = GenericError + | UintError | IntError | BoolError | BytesStaticError | BytesDynamicError | AddressError + | StringError | FixedError | UfixedError + | ArrayError | MappingError | StructError | MagicError + | EnumError | ContractError | FunctionExternalError | FunctionInternalError + | InternalUseError; + //note that at the moment, no reference type has its own error category, so + //no reference types are listed here; this also includes Magic + + /* + * SECTION 2: Elementary values + */ + + export type ElementaryErrorResult = UintErrorResult | IntErrorResult | BoolErrorResult + | BytesErrorResult | AddressErrorResult | StringErrorResult + | FixedErrorResult | UfixedErrorResult; + export type BytesErrorResult = BytesStaticErrorResult | BytesDynamicErrorResult; + + //Uints + export interface UintErrorResult { + type: Types.UintType; + kind: "error"; + error: GenericError | UintError; + } + + export type UintError = UintPaddingError; + + export interface UintPaddingError { + raw: string; //hex string + kind: "UintPaddingError"; + } + + //Ints + export interface IntErrorResult { + type: Types.IntType; + kind: "error"; + error: GenericError | IntError; + } + + export type IntError = IntPaddingError; + + export interface IntPaddingError { + raw: string; //hex string + kind: "IntPaddingError"; + } + + //Bools + export interface BoolErrorResult { + type: Types.BoolType; + kind: "error"; + error: GenericError | BoolError; + } + + export type BoolError = BoolOutOfRangeError; + + export interface BoolOutOfRangeError { + rawAsBN: BN; + kind: "BoolOutOfRangeError"; + } + + //bytes (static) + export interface BytesStaticErrorResult { + type: Types.BytesTypeStatic; + kind: "error"; + error: GenericError | BytesStaticError; + } + + export type BytesStaticError = BytesPaddingError; + + export interface BytesPaddingError { + raw: string; //should be hex string + kind: "BytesPaddingError"; + } + + //bytes (dynamic) + export interface BytesDynamicErrorResult { + type: Types.BytesTypeDynamic; + kind: "error"; + error: GenericError | BytesDynamicError; + } + + export type BytesDynamicError = never; //bytes dynamic has no specific errors atm + + //addresses + export interface AddressErrorResult { + type: Types.AddressType; + kind: "error"; + error: GenericError | AddressError; + } + + export type AddressError = AddressPaddingError; + + export interface AddressPaddingError { + raw: string; //should be hex string + kind: "AddressPaddingError"; + } + + //strings + export interface StringErrorResult { + type: Types.StringType; + kind: "error"; + error: GenericError | StringError; + } + + export type StringError = never; //again, string has no specific errors + + //Fixed & Ufixed + //These don't have a value format yet, so they just decode to errors for now! + export interface FixedErrorResult { + type: Types.FixedType; + kind: "error"; + error: GenericError | FixedError; + } + export interface UfixedErrorResult { + type: Types.UfixedType; + kind: "error"; + error: GenericError | UfixedError; + } + + export type FixedError = FixedPointNotYetSupportedError; + export type UfixedError = FixedPointNotYetSupportedError; + + export interface FixedPointNotYetSupportedError { + raw: string; //hex string + kind: "FixedPointNotYetSupportedError"; + } + //no separate padding error here, that would be pointless right now; will make later + + /* + * SECTION 3: CONTAINER TYPES (including magic) + * none of these have type-specific errors + */ + + //Arrays + export interface ArrayErrorResult { + type: Types.ArrayType; + kind: "error"; + error: GenericError | ArrayError; + } + + export type ArrayError = never; + + //Mappings + export interface MappingErrorResult { + type: Types.MappingType; + kind: "error"; + error: GenericError | MappingError; + } + + export type MappingError = never; + + //Structs + export interface StructErrorResult { + type: Types.StructType; + kind: "error"; + error: GenericError | StructError; + } + + export type StructError = never; + + //Tuples + export interface TupleErrorResult { + type: Types.TupleType; + kind: "error"; + error: GenericError | TupleError; + } + + export type TupleError = never; + + //Magic variables + export interface MagicErrorResult { + type: Types.MagicType; + kind: "error"; + error: GenericError | MagicError; + } + + export type MagicError = never; + + /* + * SECTION 4: ENUMS + * (they didn't fit anywhere else :P ) + */ + + //Enums + export interface EnumErrorResult { + type: Types.EnumType; + kind: "error"; + error: GenericError | EnumError; + } + + export type EnumError = EnumOutOfRangeError | EnumNotFoundDecodingError; + + export interface EnumOutOfRangeError { + kind: "EnumOutOfRangeError"; + type: Types.EnumType; + rawAsBN: BN; + } + + export interface EnumNotFoundDecodingError { + kind: "EnumNotFoundDecodingError"; + type: Types.EnumType; + rawAsBN: BN; + } + + /* + * SECTION 5: CONTRACTS + */ + + //Contracts + export interface ContractErrorResult { + type: Types.ContractType; + kind: "error"; + error: GenericError | ContractError; + } + + export type ContractError = ContractPaddingError; + + export interface ContractPaddingError { + raw: string; //should be hex string + kind: "ContractPaddingError"; + } + + /* + * SECTION 6: External functions + */ + + //external functions + export interface FunctionExternalErrorResult { + type: Types.FunctionExternalType; + kind: "error"; + error: GenericError | FunctionExternalError; + } + + export type FunctionExternalError = FunctionExternalNonStackPaddingError | FunctionExternalStackPaddingError; + + export interface FunctionExternalNonStackPaddingError { + raw: string; //should be hex string + kind: "FunctionExternalNonStackPaddingError"; + } + + export interface FunctionExternalStackPaddingError { + rawAddress: string; + rawSelector: string; + kind: "FunctionExternalStackPaddingError"; + } + + /* + * SECTION 7: INTERNAL FUNCTIONS + */ + + //Internal functions + export interface FunctionInternalErrorResult { + type: Types.FunctionInternalType; + kind: "error"; + error: GenericError | FunctionInternalError; + } + + export type FunctionInternalError = FunctionInternalPaddingError | NoSuchInternalFunctionError + | DeployedFunctionInConstructorError | MalformedInternalFunctionError; + + export interface FunctionInternalPaddingError { + raw: string; //should be hex string + kind: "FunctionInternalPaddingError"; + } + + export interface NoSuchInternalFunctionError { + kind: "NoSuchInternalFunctionError"; + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + } + + export interface DeployedFunctionInConstructorError { + kind: "DeployedFunctionInConstructorError"; + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + } + + export interface MalformedInternalFunctionError { + kind: "MalformedInternalFunctionError"; + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + } + + /* + * SECTION 8: GENERIC ERRORS + */ + + export type GenericError = UserDefinedTypeNotFoundError | IndexedReferenceTypeError + | UnsupportedConstantError | ReadErrorStack; + + export type ErrorForThrowing = UserDefinedTypeNotFoundError | + UnsupportedConstantError | ReadErrorStack; + + //attempted to decode an indexed parameter of reference type error + export interface IndexedReferenceTypeError { + kind: "IndexedReferenceTypeError"; + type: Types.ReferenceType; + raw: string; //should be hex string + } + + //type-location error + export interface UserDefinedTypeNotFoundError { + kind: "UserDefinedTypeNotFoundError"; + type: Types.UserDefinedType; + } + + //Read errors + export interface UnsupportedConstantError { + kind: "UnsupportedConstantError"; + definition: AstDefinition; + } + + export interface ReadErrorStack { + kind: "ReadErrorStack"; + from: number; + to: number; + } + + //this function gives an error message + //for those errors that are meant to possibly + //be wrapped in a DecodingError and thrown + export function message(error: ErrorForThrowing) { + switch(error.kind) { + case "UserDefinedTypeNotFoundError": + let typeName = Types.isContractDefinedType(error.type) + ? error.type.definingContractName + "." + error.type.typeName + : error.type.typeName; + return `Unknown ${error.type.typeClass} type ${typeName} of id ${error.type.id}`; + case "UnsupportedConstantError": + return `Unsupported constant type ${DefinitionUtils.typeClass(error.definition)}$`; + case "ReadErrorStack": + return `Can't read stack from position ${error.from} to ${error.to}`; + } + } + + /* SECTION 9: Internal use errors */ + /* you should never see these returned. + * they are only for internal use. */ + + export type InternalUseError = OverlongArrayOrStringError | PointerTooLargeError | InternalFunctionInABIError; + + export interface OverlongArrayOrStringError { + kind: "OverlongArrayOrStringError"; + lengthAsBN: BN; + dataLength: number; + } + + export interface PointerTooLargeError { + kind: "PointerTooLargeError"; + pointerAsBN: BN; + dataLength: number; + } + + //this one should never come up at all, but just to be sure... + export interface InternalFunctionInABIError { + kind: "InternalFunctionInABIError"; + } + +} diff --git a/packages/truffle-codec-utils/src/types/inspect.ts b/packages/truffle-codec-utils/src/types/inspect.ts new file mode 100644 index 00000000000..86fff4e83ed --- /dev/null +++ b/packages/truffle-codec-utils/src/types/inspect.ts @@ -0,0 +1,243 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:types:inspect"); + +import util from "util"; +import { Types } from "./types"; +import { Values } from "./values"; +import { Errors } from "./errors"; + +//we'll need to write a typing for the options type ourself, it seems; just +//going to include the relevant properties here +export interface InspectOptions { + stylize?: (toMaybeColor: string, style?: string) => string; + colors: boolean; + breakLength: number; +} + +//HACK -- inspect options are ridiculous, I swear >_> +export function cleanStylize(options: InspectOptions) { + return Object.assign({}, ...Object.entries(options).map( + ([key,value]) => + key === "stylize" + ? {} + : {[key]: value} + )); +} + +export class ResultInspector { + result: Values.Result; + constructor(result: Values.Result) { + this.result = result; + } + [util.inspect.custom](depth: number | null, options: InspectOptions): string { + switch(this.result.kind) { + case "value": + switch(this.result.type.typeClass) { + case "uint": + case "int": + return options.stylize((this.result).value.asBN.toString(), "number"); + case "bool": + return util.inspect((this.result).value.asBool, options); + case "bytes": + let hex = (this.result).value.asHex; + switch(this.result.type.kind) { + case "static": + return options.stylize(hex, "number"); + case "dynamic": + return options.stylize(`hex'${hex.slice(2)}'`, "string"); + } + case "address": + return options.stylize((this.result).value.asAddress, "number"); + case "string": { + let coercedResult = this.result; + switch(coercedResult.value.kind) { + case "valid": + return util.inspect(coercedResult.value.asString, options); + case "malformed": + //note: this will turn malformed utf-8 into replacement characters (U+FFFD) + //note we need to cut off the 0x prefix + return util.inspect(Buffer.from(coercedResult.value.asHex.slice(2), 'hex').toString()); + } + } + case "array": { + let coercedResult = this.result; + if(coercedResult.reference !== undefined) { + return formatCircular(coercedResult.reference, options); + } + return util.inspect( + coercedResult.value.map( + element => new ResultInspector(element) + ), + options + ); + } + case "mapping": + return util.inspect( + new Map( + (this.result).value.map( + ({key, value}) => [new ResultInspector(key), new ResultInspector(value)] + ) + ), + options + ); + case "struct": { + let coercedResult = this.result; + if(coercedResult.reference !== undefined) { + return formatCircular(coercedResult.reference, options); + } + return util.inspect( + Object.assign({}, ...coercedResult.value.map( + ({name, value}) => ({[name]: new ResultInspector(value)}) + )), + options + ); + } + case "magic": + return util.inspect( + Object.assign({}, ...Object.entries((this.result).value).map( + ([key, value]) => ({[key]: new ResultInspector(value)}) + )), + options + ) + case "enum": { + return enumFullName(this.result); //not stylized + } + case "contract": { + return util.inspect( + new ContractInfoInspector( + (this.result).value + ), + options + ); + } + case "function": + switch(this.result.type.visibility) { + case "external": { + let coercedResult = this.result; + let contractString = util.inspect( + new ContractInfoInspector( + coercedResult.value.contract + ), + { ...cleanStylize(options), colors: false } + ); + let firstLine: string; + switch(coercedResult.value.kind) { + case "known": + firstLine = `[Function: ${coercedResult.value.abi.name} of`; + break; + case "invalid": + case "unknown": + firstLine = `[Function: Unknown selector ${coercedResult.value.selector} of`; + break; + } + let secondLine = `${contractString}]`; + let breakingSpace = firstLine.length >= options.breakLength ? "\n" : " "; + //now, put it together + return options.stylize(firstLine + breakingSpace + secondLine, "special"); + } + case "internal": { + let coercedResult = this.result; + switch(coercedResult.value.kind) { + case "function": + return options.stylize( + `[Function: ${coercedResult.value.definedIn.typeName}.${coercedResult.value.name}]`, + "special" + ); + case "exception": + return coercedResult.value.deployedProgramCounter === 0 + ? options.stylize(`[Function: ]`, "special") + : options.stylize(`[Function: assert(false)]`, "special"); + case "unknown": + let firstLine = `[Function: decoding not supported (raw info:`; + let secondLine = `deployed PC=${coercedResult.value.deployedProgramCounter}, constructor PC=${coercedResult.value.constructorProgramCounter})]`; + let breakingSpace = firstLine.length >= options.breakLength ? "\n" : " "; + //now, put it together + return options.stylize(firstLine + breakingSpace + secondLine, "special"); + } + } + } + } + case "error": { + debug("this.result: %O", this.result); + let errorResult = this.result; //the hell?? why couldn't it make this inference?? + switch(errorResult.error.kind) { + case "UintPaddingError": + return `Uint has extra leading bytes (padding error) (raw value ${errorResult.error.raw})`; + case "IntPaddingError": + return `Int out of range (padding error) (numeric value ${errorResult.error.raw})`; + case "BoolOutOfRangeError": + return `Invalid boolean (numeric value ${errorResult.error.rawAsBN.toString()})`; + case "BytesPaddingError": + return `Bytestring has extra trailing bytes (padding error) (raw value ${errorResult.error.raw})`; + case "AddressPaddingError": + return `Address has extra leading bytes (padding error) (raw value ${errorResult.error.raw})`; + case "FixedPointNotYetSupportedError": + return `Fixed-point decoding not yet supported (raw value: ${errorResult.error.raw})`; + case "EnumOutOfRangeError": + return `Invalid ${enumTypeName(errorResult.error.type)} (numeric value ${errorResult.error.rawAsBN.toString()})`; + case "EnumNotFoundDecodingError": + return `Unknown enum type ${enumTypeName(errorResult.error.type)} of id ${errorResult.error.type.id} (numeric value ${errorResult.error.rawAsBN.toString()})`; + case "ContractPaddingError": + return `Contract address has extra leading bytes (padding error) (raw value ${errorResult.error.raw})`; + case "FunctionExternalNonStackPaddingError": + return `External function has extra trailing bytes (padding error) (raw value ${errorResult.error.raw})`; + case "FunctionExternalStackPaddingError": + return `External function address or selector has extra leading bytes (padding error) (raw address ${errorResult.error.rawAddress}, raw selector ${errorResult.error.rawSelector})`; + case "FunctionInternalPaddingError": + return `Internal function has extra leading bytes (padding error) (raw value ${errorResult.error.raw})`; + case "NoSuchInternalFunctionError": + return `Invalid function (Deployed PC=${errorResult.error.deployedProgramCounter}, constructor PC=${errorResult.error.constructorProgramCounter}) of contract ${errorResult.error.context.typeName}`; + case "DeployedFunctionInConstructorError": + return `Deployed-style function (PC=${errorResult.error.deployedProgramCounter}) in constructor`; + case "MalformedInternalFunctionError": + return `Malformed internal function w/constructor PC only (value: ${errorResult.error.constructorProgramCounter})`; + case "IndexedReferenceTypeError": + return `Cannot decode indexed parameter of reference type ${errorResult.error.type.typeClass} (raw value ${errorResult.error.raw})`; + case "UserDefinedTypeNotFoundError": + case "UnsupportedConstantError": + case "ReadErrorStack": + return Errors.message(errorResult.error); //yay, these three are already defined! + } + } + } + } +} + +//these get their own class to deal with a minor complication +class ContractInfoInspector { + value: Values.ContractValueInfo; + constructor(value: Values.ContractValueInfo) { + this.value = value; + } + [util.inspect.custom](depth: number | null, options: InspectOptions): string { + switch(this.value.kind) { + case "known": + return options.stylize(this.value.address, "number") + ` (${this.value.class.typeName})`; + case "unknown": + return options.stylize(this.value.address, "number") + " of unknown class"; + } + } +} + +function enumTypeName(enumType: Types.EnumType) { + return (enumType.kind === "local" ? (enumType.definingContractName + ".") : "") + enumType.typeName; +} + +function styleHexString(hex: string, options: InspectOptions): string { + return options.stylize(`hex'${hex.slice(2)}'`, "string"); +} + +//this function will be used in the future for displaying circular +//structures +function formatCircular(loopLength: number, options: InspectOptions): string { + return options.stylize(`[Circular (=up ${this.loopLength})]`, "special"); +} + +export function enumFullName(value: Values.EnumValue): string { + switch(value.type.kind) { + case "local": + return `${value.type.definingContractName}.${value.type.typeName}.${value.value.name}`; + case "global": + return `${value.type.typeName}.${value.value.name}`; + } +} diff --git a/packages/truffle-decode-utils/src/types/types.ts b/packages/truffle-codec-utils/src/types/types.ts similarity index 84% rename from packages/truffle-decode-utils/src/types/types.ts rename to packages/truffle-codec-utils/src/types/types.ts index 5c8a3f50221..f2545433fd0 100644 --- a/packages/truffle-decode-utils/src/types/types.ts +++ b/packages/truffle-codec-utils/src/types/types.ts @@ -1,5 +1,5 @@ import debugModule from "debug"; -const debug = debugModule("decode-utils:types:types"); +const debug = debugModule("codec-utils:types:types"); //type objects for Solidity types //these will just be defined as interfaces; there's not any particular need for @@ -18,33 +18,40 @@ const debug = debugModule("decode-utils:types:types"); //Similarly with global structs and enums. //Foreign contract types aren't really implemented yet either, although //those aren't produced from definitions. +//(General external functions aren't really implemented yet either for the +//same reason.) //NOTE: not all of these optional fields are actually implemented. Some are //just intended for the future. +//ALSO NOTE: IDs are strings even though they're currently numeric because +//that might change in the future. import BN from "bn.js"; import * as Ast from "../ast"; import { Definition as DefinitionUtils } from "../definition"; -import { Contexts } from "../contexts"; +import { CompilerVersion } from "../compiler"; export namespace Types { export type Type = UintType | IntType | BoolType | BytesType | AddressType | FixedType | UfixedType | StringType | ArrayType | MappingType | FunctionType - | StructType | EnumType | ContractType | MagicType; + | StructType | EnumType | ContractType | MagicType | TupleType; export interface UintType { typeClass: "uint"; bits: number; + typeHint?: string; } export interface IntType { typeClass: "int"; bits: number; + typeHint?: string; } export interface BoolType { typeClass: "bool"; + typeHint?: string; } export type BytesType = BytesTypeStatic | BytesTypeDynamic; @@ -53,34 +60,40 @@ export namespace Types { typeClass: "bytes"; kind: "static"; length: number; + typeHint?: string; } export interface BytesTypeDynamic { typeClass: "bytes"; kind: "dynamic"; location?: Ast.Location; + typeHint?: string; } export interface AddressType { typeClass: "address"; payable: boolean; + typeHint?: string; } export interface StringType { typeClass: "string"; location?: Ast.Location; + typeHint?: string; } export interface FixedType { typeClass: "fixed"; bits: number; places: number; + typeHint?: string; } export interface UfixedType { typeClass: "ufixed"; bits: number; places: number; + typeHint?: string; } export type ArrayType = ArrayTypeStatic | ArrayTypeDynamic; @@ -91,6 +104,7 @@ export namespace Types { baseType: Type; length: BN; location?: Ast.Location; + typeHint?: string; } export interface ArrayTypeDynamic { @@ -98,6 +112,7 @@ export namespace Types { kind: "dynamic"; baseType: Type; location?: Ast.Location; + typeHint?: string; } export type ElementaryType = UintType | IntType | BoolType | BytesType | FixedType @@ -110,9 +125,9 @@ export namespace Types { location?: "storage"; } - export type FunctionType = FunctionTypeInternal | FunctionTypeExternal; + export type FunctionType = FunctionInternalType | FunctionExternalType; - export interface FunctionTypeInternal { + export interface FunctionInternalType { typeClass: "function"; visibility: "internal"; mutability: Ast.Mutability; @@ -121,15 +136,26 @@ export namespace Types { //we do not presently support bound functions } - export interface FunctionTypeExternal { + export type FunctionExternalType = FunctionExternalTypeSpecific | FunctionExternalTypeGeneral; + + export interface FunctionExternalTypeSpecific { typeClass: "function"; visibility: "external"; + kind: "specific"; mutability: Ast.Mutability; inputParameterTypes: Type[]; outputParameterTypes: Type[]; //we do not presently support bound functions } + export interface FunctionExternalTypeGeneral { + typeClass: "function"; + visibility: "external"; + kind: "general"; + //we do not presently support bound functions + typeHint?: string; + } + export type ContractDefinedType = StructTypeLocal | EnumTypeLocal; export type UserDefinedType = ContractDefinedType | ContractTypeNative | StructTypeGlobal | EnumTypeGlobal; @@ -143,29 +169,40 @@ export namespace Types { export interface StructTypeLocal { typeClass: "struct"; kind: "local"; - id: number; + id: string; typeName: string; definingContractName: string; definingContract?: ContractTypeNative; - memberTypes?: {name: string, type: Type}[]; //these should be in order + memberTypes?: NameTypePair[]; //these should be in order location?: Ast.Location; } export interface StructTypeGlobal { typeClass: "struct"; kind: "global"; - id: number; + id: string; typeName: string; memberTypes?: NameTypePair[]; //these should be in order location?: Ast.Location; } + export interface OptionallyNamedType { + name?: string; + type: Type; + } + + export interface TupleType { + typeClass: "tuple"; + memberTypes: OptionallyNamedType[]; + typeHint?: string; + } + export type EnumType = EnumTypeLocal | EnumTypeGlobal; export interface EnumTypeLocal { typeClass: "enum"; kind: "local"; - id: number; + id: string; typeName: string; definingContractName: string; definingContract?: ContractTypeNative; @@ -175,7 +212,7 @@ export namespace Types { export interface EnumTypeGlobal { typeClass: "enum"; kind: "global"; - id: number; + id: string; typeName: string; options?: string[]; //these should be in order } @@ -185,7 +222,7 @@ export namespace Types { export interface ContractTypeNative { typeClass: "contract"; kind: "native"; - id: number; + id: string; typeName: string; contractKind?: Ast.ContractKind; payable?: boolean; //will be useful in the future @@ -203,20 +240,24 @@ export namespace Types { //now } + export type MagicVariableName = "message" | "block" | "transaction"; + export interface MagicType { typeClass: "magic"; - variable: string; //not putting this in the type annotation for technical - //reasons, but this should be one of "message", "block", or "transaction"; - //we do *not* presently support abi or meta_type + variable: MagicVariableName; + //really, I could do this as a tagged union, but I don't see a reason to + //introduce such complexity here, especially as this type is basically just + //for the debugger memberTypes?: { [field: string]: Type }; + //may have more optional fields defined in the future } export type ReferenceType = ArrayType | MappingType | StructType | StringType | BytesTypeDynamic; export interface TypesById { - [id: number]: UserDefinedType; + [id: string]: UserDefinedType; }; //NOTE: the following function will *not* work for arbitrary nodes! It will, @@ -225,7 +266,7 @@ export namespace Types { //NOTE: set forceLocation to *null* to force no location. leave it undefined //to not force a location. //NOTE: set compiler to null to force addresses to *not* be payable (HACK)? - export function definitionToType(definition: Ast.AstDefinition, compiler: Contexts.CompilerVersion | null, forceLocation?: Ast.Location | null): Type { + export function definitionToType(definition: Ast.AstDefinition, compiler: CompilerVersion | null, forceLocation?: Ast.Location | null): Type { debug("definition %O", definition); let typeClass = DefinitionUtils.typeClass(definition); switch(typeClass) { @@ -380,16 +421,29 @@ export namespace Types { //note: don't force a location on these! use the listed location! let inputParameterTypes = inputParameters.map(parameter => definitionToType(parameter, compiler)); let outputParameterTypes = outputParameters.map(parameter => definitionToType(parameter, compiler)); - return { - typeClass, - visibility, - mutability, - inputParameterTypes, - outputParameterTypes + switch(visibility) { + case "internal": + return { + typeClass, + visibility, + mutability, + inputParameterTypes, + outputParameterTypes + }; + case "external": + return { + typeClass, + visibility, + kind: "specific", + mutability, + inputParameterTypes, + outputParameterTypes + }; } + break; //to satisfy typescript } case "struct": { - let id = DefinitionUtils.typeId(definition); + let id = DefinitionUtils.typeId(definition).toString(); let qualifiedName = definition.typeName ? definition.typeName.name : definition.name; @@ -414,7 +468,7 @@ export namespace Types { }; } case "enum": { - let id = DefinitionUtils.typeId(definition); + let id = DefinitionUtils.typeId(definition).toString(); let qualifiedName = definition.typeName ? definition.typeName.name : definition.name; @@ -428,7 +482,7 @@ export namespace Types { }; } case "contract": { - let id = DefinitionUtils.typeId(definition); + let id = DefinitionUtils.typeId(definition).toString(); let typeName = definition.typeName ? definition.typeName.name : definition.name; @@ -443,7 +497,7 @@ export namespace Types { } case "magic": { let typeIdentifier = DefinitionUtils.typeIdentifier(definition); - let variable = typeIdentifier.match(/^t_magic_(.*)$/)[1]; + let variable = typeIdentifier.match(/^t_magic_(.*)$/)[1]; return { typeClass, variable @@ -454,10 +508,10 @@ export namespace Types { //whereas the above takes variable definitions, this takes the actual type //definition - export function definitionToStoredType(definition: Ast.AstDefinition, compiler: Contexts.CompilerVersion, referenceDeclarations?: Ast.AstReferences): UserDefinedType { + export function definitionToStoredType(definition: Ast.AstDefinition, compiler: CompilerVersion, referenceDeclarations?: Ast.AstReferences): UserDefinedType { switch(definition.nodeType) { case "StructDefinition": { - let id = definition.id; + let id = definition.id.toString(); let [definingContractName, typeName] = definition.canonicalName.split("."); let memberTypes: {name: string, type: Type}[] = definition.members.map( member => ({name: member.name, type: definitionToType(member, compiler, null)}) @@ -467,7 +521,7 @@ export namespace Types { let contractDefinition = Object.values(referenceDeclarations).find( node => node.nodeType === "ContractDefinition" && node.nodes.some( - (subNode: Ast.AstDefinition) => subNode.id === id + (subNode: Ast.AstDefinition) => subNode.id.toString() === id ) ); definingContract = definitionToStoredType(contractDefinition, compiler); //can skip reference declarations @@ -483,7 +537,7 @@ export namespace Types { }; } case "EnumDefinition": { - let id = definition.id; + let id = definition.id.toString(); let [definingContractName, typeName] = definition.canonicalName.split("."); let options = definition.members.map(member => member.name); let definingContract; @@ -491,7 +545,7 @@ export namespace Types { let contractDefinition = Object.values(referenceDeclarations).find( node => node.nodeType === "ContractDefinition" && node.nodes.some( - (subNode: Ast.AstDefinition) => subNode.id === id + (subNode: Ast.AstDefinition) => subNode.id.toString() === id ) ); definingContract = definitionToStoredType(contractDefinition, compiler); //can skip reference declarations @@ -507,7 +561,7 @@ export namespace Types { }; } case "ContractDefinition": { - let id = definition.id; + let id = definition.id.toString(); let typeName = definition.name; let contractKind = definition.contractKind; let payable = DefinitionUtils.isContractPayable(definition); diff --git a/packages/truffle-codec-utils/src/types/values.ts b/packages/truffle-codec-utils/src/types/values.ts new file mode 100644 index 00000000000..ca5d7ec3405 --- /dev/null +++ b/packages/truffle-codec-utils/src/types/values.ts @@ -0,0 +1,377 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:types:values"); + +//objects for Solidity values + +//Note: This is NOT intended to represent every possible value that exists +//in Solidity! Only possible values of variables. (Though there may be +//some expansion in the future; I'm definitely intending to add tuples.) +//We do however count the builtin variables msg, block, and tx as variables +//(not other builtins though for now) so there is some support for the magic +//type. + +//We don't include fixed and ufixed for now. Those will be added when +//implemented. + +//NOTE: not all of these optional fields are actually implemented. Some are +//just intended for the future. More optional fields may be added in the +//future. + +import BN from "bn.js"; +import { Types } from "./types"; +import { Errors } from "./errors"; +import util from "util"; +import { AstDefinition, Mutability } from "../ast"; +import { Definition as DefinitionUtils } from "../definition"; +import { AbiUtils } from "../abi"; + +export namespace Values { + + /* + * SECTION 1: Generic types for values in general (including errors). + */ + + //This is the overall Result type. It may encode an actual value or an error. + export type Result = ElementaryResult + | ArrayResult | MappingResult | StructResult | MagicResult + | EnumResult + | ContractResult | FunctionExternalResult | FunctionInternalResult; + //for when you want an actual value + export type Value = ElementaryValue + | ArrayValue | MappingValue | StructValue | MagicValue + | EnumValue + | ContractValue | FunctionExternalValue | FunctionInternalValue; + + /* + * SECTION 2: Elementary values + */ + + export type ElementaryResult = UintResult | IntResult | BoolResult + | BytesResult | AddressResult | StringResult + | FixedResult | UfixedResult; + export type BytesResult = BytesStaticResult | BytesDynamicResult; + + //note that we often want an elementary *value*, and not an error! + //so let's define those types too + export type ElementaryValue = UintValue | IntValue | BoolValue + | BytesValue | AddressValue | StringValue; + //we don't include FixedValue or UfixedValue because those + //aren't implemented yet + export type BytesValue = BytesStaticValue | BytesDynamicValue; + + + //Uints + export type UintResult = UintValue | Errors.UintErrorResult; + + export interface UintValue { + type: Types.UintType; + kind: "value"; + value: { + asBN: BN; + rawAsBN?: BN; + }; + } + + //Ints + export type IntResult = IntValue | Errors.IntErrorResult; + + export interface IntValue { + type: Types.IntType; + kind: "value"; + value: { + asBN: BN; + rawAsBN?: BN; + }; + } + + //Bools + export type BoolResult = BoolValue | Errors.BoolErrorResult; + + export interface BoolValue { + type: Types.BoolType; + kind: "value"; + value: { + asBool: boolean; + }; + } + + //bytes (static) + export type BytesStaticResult = BytesStaticValue | Errors.BytesStaticErrorResult; + + export interface BytesStaticValue { + type: Types.BytesTypeStatic; + kind: "value"; + value: { + asHex: string; //should be hex-formatted, with leading "0x" + rawAsHex?: string; + }; + } + + //bytes (dynamic) + export type BytesDynamicResult = BytesDynamicValue | Errors.BytesDynamicErrorResult; + + export interface BytesDynamicValue { + type: Types.BytesTypeDynamic; + kind: "value"; + value: { + asHex: string; //should be hex-formatted, with leading "0x" + }; + } + + //addresses + export type AddressResult = AddressValue | Errors.AddressErrorResult; + + export interface AddressValue { + type: Types.AddressType; + kind: "value"; + value: { + asAddress: string; //should have 0x and be checksum-cased + rawAsHex?: string; + } + } + + //strings + export type StringResult = StringValue | Errors.StringErrorResult; + + //strings have a special new type as their value: StringValueInfo + export interface StringValue { + type: Types.StringType; + kind: "value"; + value: StringValueInfo; + } + + //these come in two types: valid strings and malformed strings + export type StringValueInfo = StringValueInfoValid | StringValueInfoMalformed; + + //valid strings + export interface StringValueInfoValid { + kind: "valid"; + asString: string; + } + + //malformed strings + export interface StringValueInfoMalformed { + kind: "malformed"; + asHex: string; + } + + //Fixed & Ufixed + //These don't have a value format yet, so they just decode to errors for now! + + export type FixedResult = Errors.FixedErrorResult; + export type UfixedResult = Errors.UfixedErrorResult; + + /* + * SECTION 3: CONTAINER TYPES (including magic) + */ + + //Arrays + export type ArrayResult = ArrayValue | Errors.ArrayErrorResult; + + export interface ArrayValue { + type: Types.ArrayType; + kind: "value"; + reference?: number; //will be used in the future for circular values + value: Result[]; + } + + //Mappings + export type MappingResult = MappingValue | Errors.MappingErrorResult; + + export interface MappingValue { + type: Types.MappingType; + kind: "value"; + //note that since mappings live in storage, a circular + //mapping is impossible + value: KeyValuePair[]; //order is irrelevant + //note that key is not allowed to be an error! + } + + export interface KeyValuePair { + key: ElementaryValue; //note must be a value, not an error! + value: Result; + } + + //Structs + export type StructResult = StructValue | Errors.StructErrorResult; + + export interface StructValue { + type: Types.StructType; + kind: "value"; + reference?: number; //will be used in the future for circular values + value: NameValuePair[]; //these should be stored in order! + } + + export interface NameValuePair { + name: string; + value: Result; + } + + //Tuples + export type TupleResult = TupleValue | Errors.TupleErrorResult; + + export interface TupleValue { + type: Types.TupleType; + kind: "value"; + value: OptionallyNamedValue[]; + } + + export interface OptionallyNamedValue { + name?: string; + value: Result; + } + + //Magic variables + export type MagicResult = MagicValue | Errors.MagicErrorResult; + + export interface MagicValue { + type: Types.MagicType; + kind: "value"; + //a magic variable can't be circular, duh! + value: { + [field: string]: Result + }; + } + + /* + * SECTION 4: ENUMS + * (they didn't fit anywhere else :P ) + */ + + //Enums + export type EnumResult = EnumValue | Errors.EnumErrorResult; + + export interface EnumValue { + type: Types.EnumType; + kind: "value"; + value: { + name: string; + numericAsBN: BN; + }; + }; + + /* + * SECTION 5: CONTRACTS + */ + + //Contracts + export type ContractResult = ContractValue | Errors.ContractErrorResult; + + //Contract values have a special new type as their value: ContractValueInfo. + export interface ContractValue { + type: Types.ContractType; + kind: "value"; + value: ContractValueInfo; + } + + //There are two types -- one for contracts whose class we can identify, and one + //for when we can't identify the class. + export type ContractValueInfo = ContractValueInfoKnown | ContractValueInfoUnknown; + + //when we can identify the class + export interface ContractValueInfoKnown { + kind: "known"; + address: string; //should be formatted as address + //NOT an AddressResult, note + rawAddress?: string; + class: Types.ContractType; + //may have more optional members defined later, but I'll leave these out for now + } + + //when we can't + export interface ContractValueInfoUnknown { + kind: "unknown"; + address: string; //should be formatted as address + //NOT an AddressResult, note + rawAddress?: string; + } + + /* + * SECTION 6: External functions + */ + + //external functions + export type FunctionExternalResult = FunctionExternalValue | Errors.FunctionExternalErrorResult; + + export interface FunctionExternalValue { + type: Types.FunctionExternalType; + kind: "value"; + value: FunctionExternalValueInfo; + } + + //External function values come in 3 types: + export type FunctionExternalValueInfo = + FunctionExternalValueInfoKnown //known function of known class + | FunctionExternalValueInfoInvalid //known class, but can't locate function + | FunctionExternalValueInfoUnknown; //can't determine class + + //known function of known class + export interface FunctionExternalValueInfoKnown { + kind: "known"; + contract: ContractValueInfoKnown; + selector: string; //formatted as a bytes4 + abi: AbiUtils.FunctionAbiEntry; + //may have more optional fields added later, I'll leave these out for now + } + + //known class but can't locate function + export interface FunctionExternalValueInfoInvalid { + kind: "invalid"; + contract: ContractValueInfoKnown; + selector: string; //formatted as a bytes4 + } + + //can't even locate class + export interface FunctionExternalValueInfoUnknown { + kind: "unknown"; + contract: ContractValueInfoUnknown; + selector: string; //formatted as a bytes4 + } + + /* + * SECTION 7: INTERNAL FUNCTIONS + */ + + //Internal functions + export type FunctionInternalResult = FunctionInternalValue | Errors.FunctionInternalErrorResult; + + export interface FunctionInternalValue { + type: Types.FunctionInternalType; + kind: "value"; + value: FunctionInternalValueInfo; + } + + //these also come in 3 types + export type FunctionInternalValueInfo = + FunctionInternalValueInfoKnown //actual function + | FunctionInternalValueInfoException //default value + | FunctionInternalValueInfoUnknown; //decoding not supported in this context + + //actual function + export interface FunctionInternalValueInfoKnown { + kind: "function" + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + name: string; + definedIn: Types.ContractType; + mutability?: Mutability; + //may have more optional fields added later + } + + //default value + export interface FunctionInternalValueInfoException { + kind: "exception" + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + } + + //value returned to indicate that decoding is not supported outside the debugger + export interface FunctionInternalValueInfoUnknown { + kind: "unknown" + context: Types.ContractType; + deployedProgramCounter: number; + constructorProgramCounter: number; + } +} diff --git a/packages/truffle-codec-utils/src/wrap.ts b/packages/truffle-codec-utils/src/wrap.ts new file mode 100644 index 00000000000..167ed2144d5 --- /dev/null +++ b/packages/truffle-codec-utils/src/wrap.ts @@ -0,0 +1,86 @@ +import debugModule from "debug"; +const debug = debugModule("codec-utils:wrap"); + +import Web3 from "web3"; +import BN from "bn.js"; +import { AstDefinition } from "./ast"; +import { Types } from "./types/types"; +import { Values } from "./types/values"; + +//Function for wrapping a value as an ElementaryValue +//WARNING: this function does not check its inputs! Please check before using! +//How to use: +//numbers may be BN, number, or numeric string +//strings should be given as strings. duh. +//bytes should be given as hex strings beginning with "0x" +//addresses are like bytes; checksum case is not required +//booleans may be given either as booleans, or as string "true" or "false" +//[NOTE: in the future this function will: +//1. check its inputs, +//2. take a slightly different input format, +//3. also be named differently and... it'll be different :P ] +export function wrapElementaryViaDefinition(value: any, definition: AstDefinition): Values.ElementaryValue { + //force location to undefined, force address to nonpayable + //(we force address to nonpayable since address payable can't be declared + //as a mapping key type) + let dataType = Types.definitionToType(definition, null, null); + return wrapElementaryValue(value, dataType); +} + +export function wrapElementaryValue(value: any, dataType: Types.Type): Values.ElementaryValue { + switch(dataType.typeClass) { + case "string": + return { + type: dataType, + kind: "value", + value: { + kind: "valid", + asString: value + } + }; + case "bytes": + //NOTE: in the future should add padding for static case + return { //TS is so bad at unions + type: dataType, + kind: "value", + value: { + asHex: value + } + }; + case "address": + value = Web3.utils.toChecksumAddress(value); + return { + type: dataType, + kind: "value", + value: { + asAddress: value + } + }; + case "uint": + case "int": + if(value instanceof BN) { + value = value.clone(); + } + else { + value = new BN(value); + } + return { //TS remains bad at unions + type: dataType, + kind: "value", + value: { + asBN: value + } + }; + case "bool": + if(typeof value === "string") { + value = value !== "false"; + } + return { + type: dataType, + kind: "value", + value: { + asBool: value + } + }; + } +} diff --git a/packages/truffle-decode-utils/tsconfig.json b/packages/truffle-codec-utils/tsconfig.json similarity index 100% rename from packages/truffle-decode-utils/tsconfig.json rename to packages/truffle-codec-utils/tsconfig.json diff --git a/packages/truffle-decoder-core/.gitignore b/packages/truffle-codec/.gitignore similarity index 100% rename from packages/truffle-decoder-core/.gitignore rename to packages/truffle-codec/.gitignore diff --git a/packages/truffle-decoder-core/.npmignore b/packages/truffle-codec/.npmignore similarity index 100% rename from packages/truffle-decoder-core/.npmignore rename to packages/truffle-codec/.npmignore diff --git a/packages/truffle-decoder-core/README.md b/packages/truffle-codec/README.md similarity index 100% rename from packages/truffle-decoder-core/README.md rename to packages/truffle-codec/README.md diff --git a/packages/truffle-codec/lib/allocate/abi.ts b/packages/truffle-codec/lib/allocate/abi.ts new file mode 100644 index 00000000000..f4f4e808089 --- /dev/null +++ b/packages/truffle-codec/lib/allocate/abi.ts @@ -0,0 +1,535 @@ +import debugModule from "debug"; +const debug = debugModule("codec:allocate:abi"); + +import * as Pointer from "../types/pointer"; +import * as Allocations from "../types/allocation"; +import { AstDefinition, AstReferences, AbiUtils } from "truffle-codec-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { UnknownUserDefinedTypeError } from "truffle-codec-utils"; +import { UnknownBaseContractIdError, NoDefinitionFoundForABIEntryError } from "../types/errors"; +import partition from "lodash.partition"; + +interface AbiAllocationInfo { + size?: number; //left out for types that don't go in the abi + dynamic?: boolean; //similarly + allocations: Allocations.AbiAllocations; +} + +export function getAbiAllocations(referenceDeclarations: AstReferences): Allocations.AbiAllocations { + let allocations: Allocations.AbiAllocations = {}; + for(const node of Object.values(referenceDeclarations)) { + if(node.nodeType === "StructDefinition") { + allocations = allocateStruct(node, referenceDeclarations, allocations); + } + } + return allocations; +} + +function allocateStruct(structDefinition: AstDefinition, referenceDeclarations: AstReferences, existingAllocations: Allocations.AbiAllocations): Allocations.AbiAllocations { + return allocateMembers(structDefinition, structDefinition.members, referenceDeclarations, existingAllocations); +} + +//note: we will still allocate circular structs, even though they're not allowed in the abi, because it's +//not worth the effort to detect them. However on mappings or internal functions, we'll vomit (allocate null) +function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[], referenceDeclarations: AstReferences, existingAllocations: Allocations.AbiAllocations, start: number = 0): Allocations.AbiAllocations { + let dynamic: boolean = false; + //note that we will mutate the start argument also! + + //don't allocate things that have already been allocated + if(parentNode.id in existingAllocations) { + return existingAllocations; + } + + let allocations = {...existingAllocations}; //otherwise, we'll be adding to this, so we better clone + + let memberAllocations: Allocations.AbiMemberAllocation[] = []; + + for(const member of definitions) + { + let length: number; + let dynamicMember: boolean; + ({size: length, dynamic: dynamicMember, allocations} = abiSizeAndAllocate(member, referenceDeclarations, allocations)); + + //vomit on illegal types in calldata -- note the short-circuit! + if(length === undefined) { + allocations[parentNode.id] = null; + return allocations; + } + + let pointer: Pointer.AbiPointer = { + location: "abi", + start, + length, + }; + + memberAllocations.push({ + definition: member, + pointer + }); + + start += length; + dynamic = dynamic || dynamicMember; + } + + allocations[parentNode.id] = { + definition: parentNode, + members: memberAllocations, + length: dynamic ? CodecUtils.EVM.WORD_SIZE : start, + dynamic + }; + + return allocations; +} + +//first return value is the actual size. +//second return value is whether the type is dynamic +//both will be undefined if type is a mapping or internal function +//third return value is resulting allocations, INCLUDING the ones passed in +function abiSizeAndAllocate(definition: AstDefinition, referenceDeclarations?: AstReferences, existingAllocations?: Allocations.AbiAllocations): AbiAllocationInfo { + switch (CodecUtils.Definition.typeClass(definition)) { + case "bool": + case "address": + case "contract": + case "int": + case "uint": + case "fixed": + case "ufixed": + case "enum": + return { + size: CodecUtils.EVM.WORD_SIZE, + dynamic: false, + allocations: existingAllocations + }; + + case "string": + return { + size: CodecUtils.EVM.WORD_SIZE, + dynamic: true, + allocations: existingAllocations + }; + + case "bytes": + return { + size: CodecUtils.EVM.WORD_SIZE, + dynamic: CodecUtils.Definition.specifiedSize(definition) == null, + allocations: existingAllocations + }; + + case "mapping": + return { + allocations: existingAllocations + }; + + case "function": + switch (CodecUtils.Definition.visibility(definition)) { + case "external": + return { + size: CodecUtils.EVM.WORD_SIZE, + dynamic: false, + allocations: existingAllocations + }; + case "internal": + return { + allocations: existingAllocations + }; + } + + case "array": { + if(CodecUtils.Definition.isDynamicArray(definition)) { + return { + size: CodecUtils.EVM.WORD_SIZE, + dynamic: true, + allocations: existingAllocations + }; + } + else { + //static array case + const length: number = CodecUtils.Definition.staticLength(definition); + if(length === 0) { + //arrays of length 0 are static regardless of base type + return { + size: 0, + dynamic: false, + allocations: existingAllocations + }; + } + const baseDefinition: AstDefinition = definition.baseType || definition.typeName.baseType; + const {size: baseSize, dynamic, allocations} = abiSizeAndAllocate(baseDefinition, referenceDeclarations, existingAllocations); + return { + size: length * baseSize, + dynamic, + allocations + }; + } + } + + case "struct": { + const referenceId: number = CodecUtils.Definition.typeId(definition); + let allocations: Allocations.AbiAllocations = existingAllocations; + let allocation: Allocations.AbiAllocation | null | undefined = allocations[referenceId]; + if(allocation === undefined) { + //if we don't find an allocation, we'll have to do the allocation ourselves + const referenceDeclaration: AstDefinition = referenceDeclarations[referenceId]; + if(referenceDeclaration === undefined) { + let typeString = CodecUtils.Definition.typeString(definition); + throw new UnknownUserDefinedTypeError(referenceId, typeString); + } + allocations = allocateStruct(referenceDeclaration, referenceDeclarations, existingAllocations); + allocation = allocations[referenceId]; + } + //having found our allocation, if it's not null, we can just look up its size and dynamicity + if(allocation !== null) { + return { + size: allocation.length, + dynamic: allocation.dynamic, + allocations + }; + } + //if it is null, this type doesn't go in the abi + else { + return { + allocations + }; + } + } + } +} + +//like abiSize, but for a Type object; also assumes you've already done allocation +//(note: function for dynamic is separate, see below) +//also, does not attempt to handle types that don't occur in calldata +export function abiSizeForType(dataType: CodecUtils.Types.Type, allocations?: Allocations.AbiAllocations): number { + switch(dataType.typeClass) { + case "array": + switch(dataType.kind) { + case "dynamic": + return CodecUtils.EVM.WORD_SIZE; + case "static": + const length = dataType.length.toNumber(); //if this is too big, we have a problem! + const baseSize = abiSizeForType(dataType.baseType, allocations); + return length * baseSize; + } + case "struct": + const allocation = allocations[parseInt(dataType.id)]; + if(!allocation) { + throw new CodecUtils.Errors.DecodingError( + { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + ); + } + return allocation.length; + default: + return CodecUtils.EVM.WORD_SIZE; + } +} + +//again, this function does not attempt to handle types that don't occur in the abi +export function isTypeDynamic(dataType: CodecUtils.Types.Type, allocations?: Allocations.AbiAllocations): boolean { + switch(dataType.typeClass) { + case "string": + return true; + case "bytes": + return dataType.kind === "dynamic"; + case "array": + return dataType.kind === "dynamic" || (dataType.length.gtn(0) && isTypeDynamic(dataType.baseType, allocations)); + case "struct": + const allocation = allocations[parseInt(dataType.id)]; + if(!allocation) { + throw new CodecUtils.Errors.DecodingError( + { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + ); + } + return allocation.dynamic; + default: + return false; + } +} + +//allocates an external call +//NOTE: returns just a single allocation; assumes primary allocation is already complete! +function allocateCalldata( + abiEntry: AbiUtils.FunctionAbiEntry | AbiUtils.ConstructorAbiEntry, + contractId: number, + referenceDeclarations: AstReferences, + abiAllocations: Allocations.AbiAllocations, + constructorContext?: CodecUtils.Contexts.DecoderContext +): Allocations.CalldataAllocation { + const contractNode = referenceDeclarations[contractId]; + const linearizedBaseContracts = contractNode.linearizedBaseContracts; + //first: determine the corresponding function node + //(simultaneously: determine the offset) + let node: AstDefinition; + let offset: number; + switch(abiEntry.type) { + case "constructor": + let rawLength = constructorContext.binary.length; + offset = (rawLength - 2)/2; //number of bytes in 0x-prefixed bytestring + //for a constructor, we only want to search the particular contract + node = contractNode.nodes.find( + functionNode => AbiUtils.definitionMatchesAbi( + //note this needn't actually be a function node, but then it will + //return false (well, unless it's a getter node!) + abiEntry, functionNode, referenceDeclarations + ) + ); + if(node === undefined) { + //if we can't find it, just throw + throw new NoDefinitionFoundForABIEntryError(abiEntry, [contractId]); + } + break; + case "function": + offset = CodecUtils.EVM.SELECTOR_SIZE; + //search through base contracts, from most derived (right) to most base (left) + node = linearizedBaseContracts.reduceRight( + (foundNode: AstDefinition, baseContractId: number) => { + if(foundNode) { + return foundNode //once we've found something, we don't need to keep looking + }; + let baseContractNode = referenceDeclarations[baseContractId]; + if(baseContractNode === undefined) { + throw new UnknownBaseContractIdError(contractNode.id, contractNode.name, contractNode.contractKind, baseContractId); + } + return baseContractNode.nodes.find( //may be undefined! that's OK! + functionNode => AbiUtils.definitionMatchesAbi( + abiEntry, functionNode, referenceDeclarations + ) + ); + }, + undefined //start with no node found + ); + if(node === undefined) { + //if we can't find it, just throw + //reverse the list (cloning first with slice) so that they're actually in the order searched + throw new NoDefinitionFoundForABIEntryError(abiEntry, linearizedBaseContracts.slice().reverse()); + } + break; + } + //now: perform the allocation! however this will depend on whether + //we're looking at a normal function or a getter + let parameters: AstDefinition[]; + switch(node.nodeType) { + case "FunctionDefinition": + parameters = node.parameters.parameters; + break; + case "VariableDeclaration": + //getter case + parameters = CodecUtils.getterInputs(node); + break; + } + const abiAllocation = allocateMembers(node, parameters, referenceDeclarations, abiAllocations, offset)[node.id]; + //finally: transform it appropriately + let argumentsAllocation = []; + for(const member of abiAllocation.members) { + const position = parameters.findIndex( + (parameter: AstDefinition) => parameter.id === member.definition.id + ); + argumentsAllocation[position] = { + definition: member.definition, + pointer: { + location: "calldata" as "calldata", + start: member.pointer.start, + length: member.pointer.length + } + }; + } + return { + definition: abiAllocation.definition, + abi: abiEntry, + offset, + arguments: argumentsAllocation + }; +} + +//allocates an event +//NOTE: returns just a single allocation; assumes primary allocation is already complete! +function allocateEvent( + abiEntry: AbiUtils.EventAbiEntry, + contractId: number, + referenceDeclarations: AstReferences, + abiAllocations: Allocations.AbiAllocations +): Allocations.EventAllocation { + const contractNode = referenceDeclarations[contractId]; + const linearizedBaseContracts = contractNode.linearizedBaseContracts; + //first: determine the corresponding event node + //search through base contracts, from most derived (right) to most base (left) + let node: AstDefinition; + node = linearizedBaseContracts.reduceRight( + (foundNode: AstDefinition, baseContractId: number) => { + if(foundNode) { + return foundNode //once we've found something, we don't need to keep looking + }; + let baseContractNode = referenceDeclarations[baseContractId]; + if(baseContractNode === undefined) { + throw new UnknownBaseContractIdError(contractNode.id, contractNode.name, contractNode.contractKind, baseContractId); + } + return baseContractNode.nodes.find( //may be undefined! that's OK! + eventNode => AbiUtils.definitionMatchesAbi( + //note this needn't actually be a event node, but then it will return false + abiEntry, eventNode, referenceDeclarations + ) + ); + }, + undefined //start with no node found + ); + if(node === undefined) { + //if we can't find it, just throw + //reverse the list (cloning first with slice) so that they're actually in the order searched + throw new NoDefinitionFoundForABIEntryError(abiEntry, linearizedBaseContracts.slice().reverse()); + } + //now: split the list of parameters into indexed and non-indexed + //but first attach positions so we can reconstruct the list later + const rawParameters = node.parameters.parameters; + const [indexed, nonIndexed] = partition(rawParameters, (parameter: AstDefinition) => parameter.indexed); + //now: perform the allocation for the non-indexed parameters! + const abiAllocation = allocateMembers(node, nonIndexed, referenceDeclarations, abiAllocations)[node.id]; + //now: transform it appropriately + let argumentsAllocation = []; + for(const member of abiAllocation.members) { + const position = rawParameters.findIndex( + (parameter: AstDefinition) => parameter.id === member.definition.id + ); + argumentsAllocation[position] = { + definition: member.definition, + pointer: { + location: "eventdata" as "eventdata", + start: member.pointer.start, + length: member.pointer.length + } + }; + } + //finally: add in the indexed parameters... + let currentTopic = node.anonymous ? 0 : 1; //if not anonymous, selector takes up topic 0 + for(const parameterNode of indexed) { + const position = rawParameters.findIndex( + (parameter: AstDefinition) => parameter.id === parameterNode.id + ); + argumentsAllocation[position] = { + definition: parameterNode, + pointer: { + location: "eventtopic" as "eventtopic", + topic: currentTopic + } + }; + currentTopic++; + } + //...and return + return { + definition: abiAllocation.definition, + abi: abiEntry, + contractId, + arguments: argumentsAllocation + }; +} + +function getCalldataAllocationsForContract( + abi: AbiUtils.Abi, + contractId: number, + constructorContext: CodecUtils.Contexts.DecoderContext, + referenceDeclarations: AstReferences, + abiAllocations: Allocations.AbiAllocations +): Allocations.CalldataContractAllocation { + let allocations: Allocations.CalldataContractAllocation = { + constructorAllocation: defaultConstructorAllocation(constructorContext), //will be overridden if abi has a constructor + //(if it doesn't then it will remain as default) + functionAllocations: {} + } + for(let abiEntry of abi) { + if(abiEntry.type === "constructor") { + allocations.constructorAllocation = allocateCalldata( + abiEntry, + contractId, + referenceDeclarations, + abiAllocations, + constructorContext + ); + } + else if(abiEntry.type === "function") { + allocations.functionAllocations[AbiUtils.abiSelector(abiEntry)] = + allocateCalldata( + abiEntry, + contractId, + referenceDeclarations, + abiAllocations, + constructorContext + ); + } + //skip over fallback and event + } + return allocations; +} + +function defaultConstructorAllocation(constructorContext: CodecUtils.Contexts.DecoderContext) { + let rawLength = constructorContext.binary.length; + let offset = (rawLength - 2)/2; //number of bytes in 0x-prefixed bytestring + return { + offset, + abi: AbiUtils.DEFAULT_CONSTRUCTOR_ABI, + arguments: [] as Allocations.CalldataArgumentAllocation[] + }; +} + +//note: contract allocation info should include constructor context +export function getCalldataAllocations(contracts: Allocations.ContractAllocationInfo[], referenceDeclarations: AstReferences, abiAllocations: Allocations.AbiAllocations): Allocations.CalldataAllocations { + return Object.assign({}, ...contracts.map( + ({abi, id, constructorContext}) => ({ + [id]: getCalldataAllocationsForContract( + abi, id, constructorContext, referenceDeclarations, abiAllocations + ) + }) + )); +} + +function getEventAllocationsForContract( + abi: AbiUtils.Abi, + contractId: number, + referenceDeclarations: AstReferences, + abiAllocations: Allocations.AbiAllocations +): Allocations.EventAllocationTemporary[] { + return abi.filter( + (abiEntry: AbiUtils.AbiEntry) => abiEntry.type === "event" + ).map( + (abiEntry: AbiUtils.EventAbiEntry) => + abiEntry.anonymous + ? { + topics: AbiUtils.topicsCount(abiEntry), + allocation: allocateEvent(abiEntry, contractId, referenceDeclarations, abiAllocations) + } + : { + selector: AbiUtils.abiSelector(abiEntry), + topics: AbiUtils.topicsCount(abiEntry), + allocation: allocateEvent(abiEntry, contractId, referenceDeclarations, abiAllocations) + } + ); +} + +//note: constructor context is ignored by this function; no need to pass it in +export function getEventAllocations(contracts: Allocations.ContractAllocationInfo[], referenceDeclarations: AstReferences, abiAllocations: Allocations.AbiAllocations): Allocations.EventAllocations { + let allocations: Allocations.EventAllocations = {}; + for(let {abi, id: contractId} of contracts) { + let contractKind = referenceDeclarations[contractId].contractKind; + let contractAllocations = getEventAllocationsForContract(abi, contractId, referenceDeclarations, abiAllocations); + for(let {selector, topics, allocation} of contractAllocations) { + if(allocations[topics] === undefined) { + allocations[topics] = { bySelector: {}, anonymous: { contract: {}, library: {} } }; + } + if(selector !== undefined) { + if(allocations[topics].bySelector[selector] === undefined) { + allocations[topics].bySelector[selector] = { contract: {}, library: {} }; + } + allocations[topics].bySelector[selector][contractKind][contractId] = allocation; + } + else { + if(allocations[topics].anonymous[contractKind][contractId] === undefined) { + allocations[topics].anonymous[contractKind][contractId] = []; + } + allocations[topics].anonymous[contractKind][contractId].push(allocation); + } + } + } + return allocations; +} diff --git a/packages/truffle-decoder-core/lib/allocate/memory.ts b/packages/truffle-codec/lib/allocate/memory.ts similarity index 75% rename from packages/truffle-decoder-core/lib/allocate/memory.ts rename to packages/truffle-codec/lib/allocate/memory.ts index d8dca8e94ad..80d4b8e18ab 100644 --- a/packages/truffle-decoder-core/lib/allocate/memory.ts +++ b/packages/truffle-codec/lib/allocate/memory.ts @@ -1,10 +1,10 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:allocate:memory"); +const debug = debugModule("codec:allocate:memory"); import { MemoryPointer } from "../types/pointer"; import { MemoryAllocations, MemoryAllocation, MemoryMemberAllocation } from "../types/allocation"; -import { AstDefinition, AstReferences } from "truffle-decode-utils"; -import * as DecodeUtils from "truffle-decode-utils"; +import { AstDefinition, AstReferences } from "truffle-codec-utils"; +import * as CodecUtils from "truffle-codec-utils"; export function getMemoryAllocations(referenceDeclarations: AstReferences): MemoryAllocations { let allocations: MemoryAllocations = {}; @@ -22,16 +22,15 @@ function allocateStruct(definition: AstDefinition): MemoryAllocation { let memberAllocations: MemoryMemberAllocation[] = []; let position = 0; for(const member of definition.members) { - const length = DecodeUtils.Definition.isMapping(member) + const length = CodecUtils.Definition.isMapping(member) ? 0 - : DecodeUtils.EVM.WORD_SIZE; + : CodecUtils.EVM.WORD_SIZE; memberAllocations.push({ definition: member, pointer: { - memory: { - start: position, - length - } + location: "memory", + start: position, + length } }); position += length; diff --git a/packages/truffle-decoder-core/lib/allocate/storage.ts b/packages/truffle-codec/lib/allocate/storage.ts similarity index 80% rename from packages/truffle-decoder-core/lib/allocate/storage.ts rename to packages/truffle-codec/lib/allocate/storage.ts index 7084f6b3c86..b81a913faf4 100644 --- a/packages/truffle-decoder-core/lib/allocate/storage.ts +++ b/packages/truffle-codec/lib/allocate/storage.ts @@ -1,13 +1,14 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:allocate:storage"); +const debug = debugModule("codec:allocate:storage"); import { StoragePointer } from "../types/pointer"; import { StorageAllocations, StorageAllocation, StorageMemberAllocation } from "../types/allocation"; import { StorageLength, isWordsLength, Range } from "../types/storage"; -import { UnknownBaseContractIdError, UnknownUserDefinedTypeError } from "../types/errors"; -import { AstDefinition, AstReferences } from "truffle-decode-utils"; +import { UnknownBaseContractIdError } from "../types/errors"; +import { UnknownUserDefinedTypeError } from "truffle-codec-utils"; +import { AstDefinition, AstReferences } from "truffle-codec-utils"; import { readDefinition } from "../read/constant" -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import BN from "bn.js"; interface StorageAllocationInfo { @@ -36,7 +37,7 @@ function allocateStruct(structDefinition: AstDefinition, referenceDeclarations: function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[], referenceDeclarations: AstReferences, existingAllocations: StorageAllocations, suppressSize: boolean = false): StorageAllocations { let offset: number = 0; //will convert to BN when placing in slot - let index: number = DecodeUtils.EVM.WORD_SIZE - 1; + let index: number = CodecUtils.EVM.WORD_SIZE - 1; //don't allocate things that have already been allocated if(parentNode.id in existingAllocations) { @@ -53,9 +54,9 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] //first off: is this a constant? if so we use a different, simpler process if(node.constant) { - let pointer = { definition: node.value }; + let pointer = { location: "definition" as "definition", definition: node.value }; //HACK restrict ourselves to the types of constants we know how to handle - if(DecodeUtils.Definition.isSimpleConstant(node.value)) { + if(CodecUtils.Definition.isSimpleConstant(node.value)) { memberAllocations.push({definition: node, pointer}); } //if we don't know how to handle it, we just ignore it @@ -68,9 +69,9 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] //if it's sized in words (and we're not at the start of slot) we need to start on a new slot //if it's sized in bytes but there's not enough room, we also need a new slot if ( isWordsLength(size) - ? index < DecodeUtils.EVM.WORD_SIZE - 1 + ? index < CodecUtils.EVM.WORD_SIZE - 1 : size.bytes > index + 1) { - index = DecodeUtils.EVM.WORD_SIZE - 1; + index = CodecUtils.EVM.WORD_SIZE - 1; offset += 1; } //otherwise, we remain in place @@ -90,7 +91,7 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] slot: { offset: new BN(offset + size.words - 1) //end at the current slot plus # of words minus 1... }, - index: DecodeUtils.EVM.WORD_SIZE - 1 //...at the end of the word. + index: CodecUtils.EVM.WORD_SIZE - 1 //...at the end of the word. }, }; } @@ -115,7 +116,8 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] memberAllocations.push({ definition: node, pointer: { - storage: range + location: "storage", + range } }); @@ -123,14 +125,14 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] //if it was sized in words, move down that many slots and reset position w/in slot if(isWordsLength(size)) { offset += size.words; - index = DecodeUtils.EVM.WORD_SIZE - 1; + index = CodecUtils.EVM.WORD_SIZE - 1; } //if it was sized in bytes, move down an appropriate number of bytes. else { index -= size.bytes; //but if this puts us into the next word, move to the next word. if(index < 0) { - index = DecodeUtils.EVM.WORD_SIZE - 1; + index = CodecUtils.EVM.WORD_SIZE - 1; offset += 1; } } @@ -149,7 +151,7 @@ function allocateMembers(parentNode: AstDefinition, definitions: AstDefinition[] //SPECIAL CASE: if *nothing* has been used, allocate a single word (that's how //empty structs behave in versions where they're legal) if(!suppressSize) { - if(index === DecodeUtils.EVM.WORD_SIZE - 1 && offset !== 0) { + if(index === CodecUtils.EVM.WORD_SIZE - 1 && offset !== 0) { allocations[parentNode.id].size = {words: offset}; } else { @@ -197,7 +199,7 @@ export function storageSize(definition: AstDefinition, referenceDeclarations?: A } function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations?: AstReferences, existingAllocations?: StorageAllocations): StorageAllocationInfo { - switch (DecodeUtils.Definition.typeClass(definition)) { + switch (CodecUtils.Definition.typeClass(definition)) { case "bool": return { size: {bytes: 1}, @@ -207,14 +209,14 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations case "address": case "contract": return { - size: {bytes: DecodeUtils.EVM.ADDRESS_SIZE}, + size: {bytes: CodecUtils.EVM.ADDRESS_SIZE}, allocations: existingAllocations }; case "int": case "uint": return { - size: {bytes: DecodeUtils.Definition.specifiedSize(definition) || 32 }, // default of 256 bits + size: {bytes: CodecUtils.Definition.specifiedSize(definition) || 32 }, // default of 256 bits //(should 32 here be WORD_SIZE? I thought so, but comparing with case //of fixed/ufixed makes the appropriate generalization less clear) allocations: existingAllocations @@ -223,19 +225,19 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations case "fixed": case "ufixed": return { - size: {bytes: DecodeUtils.Definition.specifiedSize(definition) || 16 }, // default of 128 bits + size: {bytes: CodecUtils.Definition.specifiedSize(definition) || 16 }, // default of 128 bits allocations: existingAllocations }; case "enum": { debug("enum definition %O", definition); - const referenceId: number = DecodeUtils.Definition.typeId(definition); + const referenceId: number = CodecUtils.Definition.typeId(definition); //note: we use the preexisting function here for convenience, but we //should never need to worry about faked-up enum definitions, so just //checking the referencedDeclaration field would also work const referenceDeclaration: AstDefinition = referenceDeclarations[referenceId]; if(referenceDeclaration === undefined) { - let typeString = DecodeUtils.Definition.typeString(definition); + let typeString = CodecUtils.Definition.typeString(definition); throw new UnknownUserDefinedTypeError(referenceId, typeString); } const numValues: number = referenceDeclaration.members.length; @@ -247,7 +249,7 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations case "bytes": { //this case is really two different cases! - const staticSize: number = DecodeUtils.Definition.specifiedSize(definition); + const staticSize: number = CodecUtils.Definition.specifiedSize(definition); if(staticSize) { return { size: {bytes: staticSize}, @@ -272,22 +274,22 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations case "function": { //this case is also really two different cases - switch (DecodeUtils.Definition.visibility(definition)) { + switch (CodecUtils.Definition.visibility(definition)) { case "internal": return { - size: {bytes: DecodeUtils.EVM.PC_SIZE * 2}, + size: {bytes: CodecUtils.EVM.PC_SIZE * 2}, allocations: existingAllocations }; case "external": return { - size: {bytes: DecodeUtils.EVM.ADDRESS_SIZE + DecodeUtils.EVM.SELECTOR_SIZE}, + size: {bytes: CodecUtils.EVM.ADDRESS_SIZE + CodecUtils.EVM.SELECTOR_SIZE}, allocations: existingAllocations }; } } case "array": { - if(DecodeUtils.Definition.isDynamicArray(definition)) { + if(CodecUtils.Definition.isDynamicArray(definition)) { return { size: {words: 1}, allocations: existingAllocations @@ -295,7 +297,7 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations } else { //static array case - const length: number = DecodeUtils.Definition.staticLength(definition); + const length: number = CodecUtils.Definition.staticLength(definition); if(length === 0) { //in versions of Solidity where it's legal, arrays of length 0 still take up 1 word return { @@ -303,11 +305,11 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations allocations: existingAllocations }; } - const baseDefinition: AstDefinition = DecodeUtils.Definition.baseDefinition(definition); + const baseDefinition: AstDefinition = CodecUtils.Definition.baseDefinition(definition); const {size: baseSize, allocations} = storageSizeAndAllocate(baseDefinition, referenceDeclarations, existingAllocations); if(!isWordsLength(baseSize)) { //bytes case - const perWord: number = Math.floor(DecodeUtils.EVM.WORD_SIZE / baseSize.bytes); + const perWord: number = Math.floor(CodecUtils.EVM.WORD_SIZE / baseSize.bytes); debug("length %o", length); const numWords: number = Math.ceil(length / perWord); return { @@ -326,14 +328,14 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations } case "struct": { - const referenceId: number = DecodeUtils.Definition.typeId(definition); + const referenceId: number = CodecUtils.Definition.typeId(definition); let allocations: StorageAllocations = existingAllocations; let allocation: StorageAllocation | undefined = allocations[referenceId]; //may be undefined! if(allocation === undefined) { //if we don't find an allocation, we'll have to do the allocation ourselves const referenceDeclaration: AstDefinition = referenceDeclarations[referenceId]; if(referenceDeclaration === undefined) { - let typeString = DecodeUtils.Definition.typeString(definition); + let typeString = CodecUtils.Definition.typeString(definition); throw new UnknownUserDefinedTypeError(referenceId, typeString); } debug("definition %O", definition); @@ -350,23 +352,26 @@ function storageSizeAndAllocate(definition: AstDefinition, referenceDeclarations } //like storageSize, but for a Type object; also assumes you've already done allocation -export function storageSizeForType(dataType: DecodeUtils.Types.Type, userDefinedTypes: DecodeUtils.Types.TypesById, allocations: StorageAllocations): StorageLength { +export function storageSizeForType(dataType: CodecUtils.Types.Type, userDefinedTypes?: CodecUtils.Types.TypesById, allocations?: StorageAllocations): StorageLength { switch(dataType.typeClass) { case "bool": return {bytes: 1}; case "address": case "contract": - return {bytes: DecodeUtils.EVM.ADDRESS_SIZE}; + return {bytes: CodecUtils.EVM.ADDRESS_SIZE}; case "int": case "uint": case "fixed": case "ufixed": return {bytes: dataType.bits / 8 }; case "enum": { - let fullType = DecodeUtils.Types.fullType(dataType, userDefinedTypes); + let fullType = CodecUtils.Types.fullType(dataType, userDefinedTypes); if(!fullType.options) { - throw new DecodeUtils.Errors.DecodingError( - new DecodeUtils.Errors.UserDefinedTypeNotFoundError(dataType) + throw new CodecUtils.Errors.DecodingError( + { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } ); } return {bytes: Math.ceil(Math.log2(fullType.options.length) / 8)}; @@ -374,9 +379,9 @@ export function storageSizeForType(dataType: DecodeUtils.Types.Type, userDefined case "function": switch (dataType.visibility) { case "internal": - return {bytes: DecodeUtils.EVM.PC_SIZE * 2}; + return {bytes: CodecUtils.EVM.PC_SIZE * 2}; case "external": - return {bytes: DecodeUtils.EVM.ADDRESS_SIZE + DecodeUtils.EVM.SELECTOR_SIZE}; + return {bytes: CodecUtils.EVM.ADDRESS_SIZE + CodecUtils.EVM.SELECTOR_SIZE}; } break; //to satisfy typescript :P case "bytes": @@ -401,7 +406,7 @@ export function storageSizeForType(dataType: DecodeUtils.Types.Type, userDefined let baseSize = storageSizeForType(dataType.baseType, userDefinedTypes, allocations); if(!isWordsLength(baseSize)) { //bytes case - const perWord: number = Math.floor(DecodeUtils.EVM.WORD_SIZE / baseSize.bytes); + const perWord: number = Math.floor(CodecUtils.EVM.WORD_SIZE / baseSize.bytes); debug("length %o", length); const numWords: number = Math.ceil(length / perWord); return {words: numWords}; @@ -412,10 +417,13 @@ export function storageSizeForType(dataType: DecodeUtils.Types.Type, userDefined } } case "struct": - let allocation = allocations[dataType.id]; + let allocation = allocations[parseInt(dataType.id)]; if(!allocation) { - throw new DecodeUtils.Errors.DecodingError( - new DecodeUtils.Errors.UserDefinedTypeNotFoundError(dataType) + throw new CodecUtils.Errors.DecodingError( + { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } ); } return allocation.size; diff --git a/packages/truffle-codec/lib/decode/abi.ts b/packages/truffle-codec/lib/decode/abi.ts new file mode 100644 index 00000000000..afa9946182d --- /dev/null +++ b/packages/truffle-codec/lib/decode/abi.ts @@ -0,0 +1,374 @@ +import debugModule from "debug"; +const debug = debugModule("codec:decode:abi"); + +import read from "../read"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; +import decodeValue from "./value"; +import { AbiDataPointer, DataPointer } from "../types/pointer"; +import { AbiMemberAllocation } from "../types/allocation"; +import { abiSizeForType, isTypeDynamic } from "../allocate/abi"; +import { EvmInfo } from "../types/evm"; +import { DecoderOptions } from "../types/options"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; +import { StopDecodingError } from "../types/errors"; + +type AbiLocation = "calldata" | "eventdata"; //leaving out "abi" as it shouldn't occur here + +export default function* decodeAbi(dataType: Types.Type, pointer: AbiDataPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + if(Types.isReferenceType(dataType)) { + let dynamic: boolean; + try { + dynamic = isTypeDynamic(dataType, info.allocations.abi); + } + catch(error) { //error: Errors.DecodingError + if(options.strictAbiMode) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + if(dynamic) { + return yield* decodeAbiReferenceByAddress(dataType, pointer, info, options); + } + else { + return yield* decodeAbiReferenceStatic(dataType, pointer, info, options); + } + } + else { + debug("pointer %o", pointer); + return yield* decodeValue(dataType, pointer, info, options); + } +} + +export function* decodeAbiReferenceByAddress(dataType: Types.ReferenceType, pointer: DataPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + let { strictAbiMode: strict, abiPointerBase: base } = options; + base = base || 0; //in case base was undefined + const { allocations: { abi: allocations }, state } = info; + debug("pointer %o", pointer); + //this variable holds the location we should look to *next* + const location: AbiLocation = pointer.location === "eventdata" + ? "eventdata" + : "calldata"; //stack pointers (& stack literal pointers) point to calldata, not the stack + + let rawValue: Uint8Array; + try { + rawValue = yield* read(pointer, state); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + + let rawValueAsBN = CodecUtils.Conversion.toBN(rawValue); + if(strict && rawValueAsBN.gtn(state[location].length)) { + //why is this check here?? + //it's really just to protect us against the toNumber() + //conversion :) + throw new StopDecodingError( + { + kind: "PointerTooLargeError", + pointerAsBN: rawValueAsBN, + dataLength: state[location].length + } + ); + } + let startPosition = rawValueAsBN.toNumber() + base; + debug("startPosition %d", startPosition); + + let dynamic: boolean; + try { + dynamic = isTypeDynamic(dataType, allocations); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + if(!dynamic) { //this will only come up when called from stack.ts + let size: number; + try { + size = abiSizeForType(dataType, allocations); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + let staticPointer = { + location, + start: startPosition, + length: size + } + return yield* decodeAbiReferenceStatic(dataType, staticPointer, info, options); + } + let length: number; + let rawLength: Uint8Array; + switch (dataType.typeClass) { + + case "bytes": + case "string": + //initial word contains length + try { + rawLength = (yield* read({ + location, + start: startPosition, + length: CodecUtils.EVM.WORD_SIZE + }, state)); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + let lengthAsBN = CodecUtils.Conversion.toBN(rawLength); + if(strict && lengthAsBN.gtn(state[location].length)) { + //you may notice that the comparison is a bit crude; that's OK, this is + //just to prevent huge numbers from DOSing us, other errors will still + //be caught regardless + throw new StopDecodingError( + { + kind: "OverlongArrayOrStringError", + lengthAsBN, + dataLength: state[location].length + } + ); + } + length = lengthAsBN.toNumber(); + + let childPointer: AbiDataPointer = { + location, + start: startPosition + CodecUtils.EVM.WORD_SIZE, + length + } + + return yield* decodeValue(dataType, childPointer, info, options); + + case "array": + + switch(dataType.kind) { + case "dynamic": + //initial word contains array length + try { + rawLength = (yield* read({ + location, + start: startPosition, + length: CodecUtils.EVM.WORD_SIZE + }, state)); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + let lengthAsBN = CodecUtils.Conversion.toBN(rawLength); + if(strict && lengthAsBN.gtn(state[location].length)) { + //you may notice that the comparison is a bit crude; that's OK, this is + //just to prevent huge numbers from DOSing us, other errors will still + //be caught regardless + throw new StopDecodingError( + { + kind: "OverlongArrayOrStringError", + lengthAsBN, + dataLength: state[location].length + } + ); + } + length = lengthAsBN.toNumber(); + startPosition += CodecUtils.EVM.WORD_SIZE; //increment startPosition + //to next word, as first word was used for length + break; + case "static": + length = dataType.length.toNumber(); + break; + } + + //note: I've written this fairly generically, but it is worth noting that + //since this array is of dynamic type, we know that if it's static length + //then size must be EVM.WORD_SIZE + + let baseSize: number; + try { + baseSize = abiSizeForType(dataType.baseType, allocations); + } + catch(error) { //error: Errors.DecodingError + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + + let decodedChildren: Values.Result[] = []; + for(let index = 0; index < length; index++) { + decodedChildren.push( + (yield* decodeAbi( + dataType.baseType, + { + location, + start: startPosition + index * baseSize, + length: baseSize + }, + info, { ...options, abiPointerBase: startPosition } + )) + ); //pointer base is always start of list, never the length + } + return { + type: dataType, + kind: "value", + value: decodedChildren + }; + + case "struct": + return yield* decodeAbiStructByPosition(dataType, location, startPosition, info, options); + } +} + +export function* decodeAbiReferenceStatic(dataType: Types.ReferenceType, pointer: AbiDataPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + debug("static"); + debug("pointer %o", pointer); + const location = pointer.location; + + switch (dataType.typeClass) { + case "array": + + //we're in the static case, so we know the array must be statically sized + const length = (dataType).length.toNumber(); + let baseSize: number; + try { + baseSize = abiSizeForType(dataType.baseType, info.allocations.abi); + } + catch(error) { //error: Errors.DecodingError + if(options.strictAbiMode) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + + let decodedChildren: Values.Result[] = []; + for(let index = 0; index < length; index++) { + decodedChildren.push( + (yield* decodeAbi( + dataType.baseType, + { + location, + start: pointer.start + index * baseSize, + length: baseSize + }, + info, options + )) + ); + } + return { + type: dataType, + kind: "value", + value: decodedChildren + }; + + case "struct": + return yield* decodeAbiStructByPosition(dataType, location, pointer.start, info, options); + } +} + +//note that this function takes the start position as a *number*; it does not take a pointer +function* decodeAbiStructByPosition(dataType: Types.StructType, location: AbiLocation, startPosition: number, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + const { userDefinedTypes, allocations: { abi: allocations } } = info; + + const typeLocation = location === "eventdata" + ? null //eventdata is not a valid location for a type + : location; + + const typeId = dataType.id; + const structAllocation = allocations[parseInt(typeId)]; + if(!structAllocation) { + let error = { + kind: "UserDefinedTypeNotFoundError" as "UserDefinedTypeNotFoundError", + type: dataType + }; + if(options.strictAbiMode) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + + let decodedMembers: Values.NameValuePair[] = []; + for(let index = 0; index < structAllocation.members.length; index++) { + const memberAllocation = structAllocation.members[index]; + const memberPointer = memberAllocation.pointer; + const childPointer: AbiDataPointer = { + location, + start: startPosition + memberPointer.start, + length: memberPointer.length + }; + + let memberName = memberAllocation.definition.name; + let storedType = userDefinedTypes[typeId]; + if(!storedType) { + let error = { + kind: "UserDefinedTypeNotFoundError" as "UserDefinedTypeNotFoundError", + type: dataType + }; + if(options.strictAbiMode) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + let storedMemberType = storedType.memberTypes[index].type; + let memberType = Types.specifyLocation(storedMemberType, typeLocation); + + decodedMembers.push({ + name: memberName, + value: (yield* decodeAbi(memberType, childPointer, info, {...options, abiPointerBase: startPosition})) + //note that the base option is only needed in the dynamic case, but we're being indiscriminate + }); + } + return { + type: dataType, + kind: "value", + value: decodedMembers + }; +} diff --git a/packages/truffle-decoder-core/lib/decode/constant.ts b/packages/truffle-codec/lib/decode/constant.ts similarity index 69% rename from packages/truffle-decoder-core/lib/decode/constant.ts rename to packages/truffle-codec/lib/decode/constant.ts index aa7793e3bc7..1083108f878 100644 --- a/packages/truffle-decoder-core/lib/decode/constant.ts +++ b/packages/truffle-codec/lib/decode/constant.ts @@ -1,8 +1,8 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:constant"); +const debug = debugModule("codec:decode:constant"); -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; import read from "../read"; import decodeValue from "./value"; import { ConstantDefinitionPointer} from "../types/pointer"; @@ -27,16 +27,24 @@ export default function* decodeConstant(dataType: Types.Type, pointer: ConstantD word = yield* read(pointer, info.state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } //not bothering to check padding; shouldn't be necessary - let bytes = word.slice(DecodeUtils.EVM.WORD_SIZE - size); - return new Values.BytesStaticValue( - dataType, - DecodeUtils.Conversion.toHexString(bytes) - ); //we'll skip including a raw value, as that would be meaningless + let bytes = word.slice(CodecUtils.EVM.WORD_SIZE - size); + return { + type: dataType, + kind: "value", + value: { + asHex: CodecUtils.Conversion.toHexString(bytes) + } + }; //we'll skip including a raw value, as that would be meaningless } //otherwise, as mentioned, just dispatch to decodeValue + debug("not a static bytes"); return yield* decodeValue(dataType, pointer, info); } diff --git a/packages/truffle-codec/lib/decode/event.ts b/packages/truffle-codec/lib/decode/event.ts new file mode 100644 index 00000000000..35696246b77 --- /dev/null +++ b/packages/truffle-codec/lib/decode/event.ts @@ -0,0 +1,31 @@ +import debugModule from "debug"; +const debug = debugModule("codec:decode:event"); + +import decodeValue from "./value"; +import read from "../read"; +import { Types, Values, Conversion as ConversionUtils } from "truffle-codec-utils"; +import { EventTopicPointer } from "../types/pointer"; +import { EvmInfo } from "../types/evm"; +import { DecoderOptions } from "../types/options"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; +import { StopDecodingError } from "../types/errors"; + +export default function* decodeTopic(dataType: Types.Type, pointer: EventTopicPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + if(Types.isReferenceType(dataType)) { + //we cannot decode reference types "stored" in topics; we have to just return an error + let bytes: Uint8Array = yield* read(pointer, info.state); + let raw: string = ConversionUtils.toHexString(bytes); + //NOTE: even in strict mode we want to just return this, not throw an error here + return { + type: dataType, + kind: "error", + error: { + kind: "IndexedReferenceTypeError", + type: dataType, + raw + } + }; + } + //otherwise, dispatch to decodeValue + return yield* decodeValue(dataType, pointer, info, options); +} diff --git a/packages/truffle-codec/lib/decode/index.ts b/packages/truffle-codec/lib/decode/index.ts new file mode 100644 index 00000000000..284f9a9b6d3 --- /dev/null +++ b/packages/truffle-codec/lib/decode/index.ts @@ -0,0 +1,52 @@ +import debugModule from "debug"; +const debug = debugModule("codec:decode"); + +import decodeValue from "./value"; +import decodeMemory from "./memory"; +import decodeStorage from "./storage"; +import decodeStack from "./stack"; +import { decodeLiteral } from "./stack"; +import decodeAbi from "./abi"; +import decodeConstant from "./constant"; +import decodeSpecial from "./special"; +import decodeTopic from "./event"; +import { Types, Values } from "truffle-codec-utils"; +import * as Pointer from "../types/pointer"; +import { EvmInfo } from "../types/evm"; +import { DecoderOptions } from "../types/options"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; + +export default function* decode(dataType: Types.Type, pointer: Pointer.DataPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + debug("type %O", dataType); + debug("pointer %O", pointer); + + switch(pointer.location) { + + case "storage": + return yield* decodeStorage(dataType, pointer, info) + + case "stack": + return yield* decodeStack(dataType, pointer, info); + + case "stackliteral": + return yield* decodeLiteral(dataType, pointer, info); + + case "definition": + return yield* decodeConstant(dataType, pointer, info); + + case "special": + return yield* decodeSpecial(dataType, pointer, info); + + case "calldata": + case "eventdata": + return yield* decodeAbi(dataType, pointer, info, options); + + case "eventtopic": + return yield* decodeTopic(dataType, pointer, info, options); + + case "memory": + //NOTE: this case should never actually occur, but I'm including it + //anyway as a fallback + return yield* decodeMemory(dataType, pointer, info); + } +} diff --git a/packages/truffle-decoder-core/lib/decode/memory.ts b/packages/truffle-codec/lib/decode/memory.ts similarity index 59% rename from packages/truffle-decoder-core/lib/decode/memory.ts rename to packages/truffle-codec/lib/decode/memory.ts index f334b06059e..30c3c330e02 100644 --- a/packages/truffle-decoder-core/lib/decode/memory.ts +++ b/packages/truffle-codec/lib/decode/memory.ts @@ -1,9 +1,9 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:memory"); +const debug = debugModule("codec:decode:memory"); import read from "../read"; -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; import decodeValue from "./value"; import { MemoryPointer, DataPointer } from "../types/pointer"; import { MemoryMemberAllocation } from "../types/allocation"; @@ -27,10 +27,14 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p rawValue = yield* read(pointer, state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } - let startPosition = DecodeUtils.Conversion.toBN(rawValue).toNumber(); + let startPosition = CodecUtils.Conversion.toBN(rawValue).toNumber(); let rawLength: Uint8Array; let length: number; @@ -41,20 +45,25 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p //initial word contains length try { rawLength = yield* read({ - memory: { - start: startPosition, - length: DecodeUtils.EVM.WORD_SIZE - } + location: "memory", + start: startPosition, + length: CodecUtils.EVM.WORD_SIZE }, state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } - length = DecodeUtils.Conversion.toBN(rawLength).toNumber(); + length = CodecUtils.Conversion.toBN(rawLength).toNumber(); let childPointer: MemoryPointer = { - memory: { start: startPosition + DecodeUtils.EVM.WORD_SIZE, length } - } + location: "memory", + start: startPosition + CodecUtils.EVM.WORD_SIZE, + length + }; return yield* decodeValue(dataType, childPointer, info); @@ -64,17 +73,20 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p //initial word contains array length try { rawLength = yield* read({ - memory: { - start: startPosition, - length: DecodeUtils.EVM.WORD_SIZE - } + location: "memory", + start: startPosition, + length: CodecUtils.EVM.WORD_SIZE }, state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } - length = DecodeUtils.Conversion.toBN(rawLength).toNumber(); - startPosition += DecodeUtils.EVM.WORD_SIZE; //increment startPosition + length = CodecUtils.Conversion.toBN(rawLength).toNumber(); + startPosition += CodecUtils.EVM.WORD_SIZE; //increment startPosition //to next word, as first word was used for length } else { @@ -88,27 +100,36 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p decodedChildren.push( (yield* decodeMemory( baseType, - { memory: { - start: startPosition + index * DecodeUtils.EVM.WORD_SIZE, - length: DecodeUtils.EVM.WORD_SIZE - }}, + { + location: "memory", + start: startPosition + index * CodecUtils.EVM.WORD_SIZE, + length: CodecUtils.EVM.WORD_SIZE + }, info )) ); } - return new Values.ArrayValue(dataType, decodedChildren); + return { + type: dataType, + kind: "value", + value: decodedChildren + }; case "struct": - const { memoryAllocations, userDefinedTypes } = info; + const { allocations: { memory: allocations }, userDefinedTypes } = info; const typeId = dataType.id; - const structAllocation = memoryAllocations[typeId]; + const structAllocation = allocations[parseInt(typeId)]; if(!structAllocation) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); + return { + type: dataType, + kind: "error", + error: { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + }; } debug("structAllocation %O", structAllocation); @@ -118,19 +139,22 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p const memberAllocation = structAllocation.members[index]; const memberPointer = memberAllocation.pointer; const childPointer: MemoryPointer = { - memory: { - start: startPosition + memberPointer.memory.start, - length: memberPointer.memory.length //always equals WORD_SIZE or 0 - } + location: "memory", + start: startPosition + memberPointer.start, + length: memberPointer.length //always equals WORD_SIZE or 0 }; let memberName = memberAllocation.definition.name; let storedType = userDefinedTypes[typeId]; if(!storedType) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); + return { + type: dataType, + kind: "error", + error: { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + }; } let storedMemberType = storedType.memberTypes[index].type; let memberType = Types.specifyLocation(storedMemberType, "memory"); @@ -140,10 +164,18 @@ export function* decodeMemoryReferenceByAddress(dataType: Types.ReferenceType, p value: (yield* decodeMemory(memberType, childPointer, info)) }); } - return new Values.StructValue(dataType, decodedMembers); + return { + type: dataType, + kind: "value", + value: decodedMembers + }; case "mapping": //a mapping in memory is always empty - return new Values.MappingValue(dataType, []); + return { + type: dataType, + kind: "value", + value: [] + }; } } diff --git a/packages/truffle-codec/lib/decode/special.ts b/packages/truffle-codec/lib/decode/special.ts new file mode 100644 index 00000000000..ca59ccc2493 --- /dev/null +++ b/packages/truffle-codec/lib/decode/special.ts @@ -0,0 +1,128 @@ +import debugModule from "debug"; +const debug = debugModule("codec:decode:special"); + +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; +import decodeValue from "./value"; +import { EvmInfo } from "../types/evm"; +import { SpecialPointer } from "../types/pointer"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; + +export default function* decodeSpecial(dataType: Types.Type, pointer: SpecialPointer, info: EvmInfo): IterableIterator { + if(dataType.typeClass === "magic") { + return yield* decodeMagic(dataType, pointer, info); + } + else { + return yield* decodeValue(dataType, pointer, info); + } +} + +export function* decodeMagic(dataType: Types.MagicType, pointer: SpecialPointer, info: EvmInfo): IterableIterator { + //note: that's Values.Result and not Values.MagicResult due to some TypeScript generator jank + + let {state} = info; + + switch(pointer.special) { + case "msg": + return { + type: dataType, + kind: "value", + value: { + data: (yield* decodeValue( + { + typeClass: "bytes", + kind: "dynamic", + location: "calldata" + }, + { + location: "calldata", + start: 0, + length: state.calldata.length + }, + info + )), + sig: (yield* decodeValue( + { + typeClass: "bytes", + kind: "static", + length: CodecUtils.EVM.SELECTOR_SIZE + }, + { + location: "calldata", + start: 0, + length: CodecUtils.EVM.SELECTOR_SIZE, + }, + info + )), + sender: (yield* decodeValue( + { + typeClass: "address", + payable: true + }, + {location: "special", special: "sender"}, + info + )), + value: (yield* decodeValue( + { + typeClass: "uint", + bits: 256 + }, + {location: "special", special: "value"}, + info + )) + } + }; + case "tx": + return { + type: dataType, + kind: "value", + value: { + origin: (yield* decodeValue( + { + typeClass: "address", + payable: true + }, + {location: "special", special: "origin"}, + info + )), + gasprice: (yield* decodeValue( + { + typeClass: "uint", + bits: 256 + }, + {location: "special", special: "gasprice"}, + info + )) + } + }; + case "block": + let block: {[field: string]: Values.Result} = { + coinbase: (yield* decodeValue( + { + typeClass: "address", + payable: true + }, + {location: "special", special: "coinbase"}, + info + )) + }; + //the other ones are all uint's, so let's handle them all at once; due to + //the lack of generator arrow functions, we do it by mutating block + const variables = ["difficulty", "gaslimit", "number", "timestamp"]; + for (let variable of variables) { + block[variable] = (yield* decodeValue( + { + typeClass: "uint", + bits: 256 + }, + {location: "special", special: variable}, + info + )); + } + return { + type: dataType, + kind: "value", + value: block + }; + } +} diff --git a/packages/truffle-decoder-core/lib/decode/stack.ts b/packages/truffle-codec/lib/decode/stack.ts similarity index 62% rename from packages/truffle-decoder-core/lib/decode/stack.ts rename to packages/truffle-codec/lib/decode/stack.ts index e49f7519217..9235e6b718e 100644 --- a/packages/truffle-decoder-core/lib/decode/stack.ts +++ b/packages/truffle-codec/lib/decode/stack.ts @@ -1,14 +1,14 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:stack"); +const debug = debugModule("codec:decode:stack"); -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; import read from "../read"; import decodeValue from "./value"; import { decodeExternalFunction, checkPaddingLeft } from "./value"; import { decodeMemoryReferenceByAddress } from "./memory"; import { decodeStorageReferenceByAddress } from "./storage"; -import { decodeCalldataReferenceByAddress } from "./calldata"; +import { decodeAbiReferenceByAddress } from "./abi"; import { StackPointer, StackLiteralPointer } from "../types/pointer"; import { EvmInfo } from "../types/evm"; import { DecoderRequest, GeneratorJunk } from "../types/request"; @@ -19,9 +19,13 @@ export default function* decodeStack(dataType: Types.Type, pointer: StackPointer rawValue = yield* read(pointer, info.state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } - const literalPointer: StackLiteralPointer = { literal: rawValue }; + const literalPointer: StackLiteralPointer = { location: "stackliteral", literal: rawValue }; return yield* decodeLiteral(dataType, literalPointer, info); } @@ -49,9 +53,9 @@ export function* decodeLiteral(dataType: Types.Type, pointer: StackLiteralPointe //straight to decodeValue. this is to allow us to correctly handle the //case of msg.data used as a mapping key. if(dataType.typeClass === "bytes" || dataType.typeClass === "string") { - let start = DecodeUtils.Conversion.toBN(pointer.literal.slice(0, DecodeUtils.EVM.WORD_SIZE)).toNumber(); - let length = DecodeUtils.Conversion.toBN(pointer.literal.slice(DecodeUtils.EVM.WORD_SIZE)).toNumber(); - let newPointer = { calldata: { start, length }}; + let start = CodecUtils.Conversion.toBN(pointer.literal.slice(0, CodecUtils.EVM.WORD_SIZE)).toNumber(); + let length = CodecUtils.Conversion.toBN(pointer.literal.slice(CodecUtils.EVM.WORD_SIZE)).toNumber(); + let newPointer = { location: "calldata" as "calldata", start, length }; return yield* decodeValue(dataType, newPointer, info); } @@ -60,16 +64,22 @@ export function* decodeLiteral(dataType: Types.Type, pointer: StackLiteralPointe //in this case, we're actually going to *throw away* the length info, //because it makes the logic simpler -- we'll get the length info back //from calldata - let locationOnly = pointer.literal.slice(0, DecodeUtils.EVM.WORD_SIZE); + let locationOnly = pointer.literal.slice(0, CodecUtils.EVM.WORD_SIZE); //HACK -- in order to read the correct location, we need to add an offset //of -32 (since, again, we're throwing away the length info), so we pass //that in as the "base" value - return yield* decodeCalldataReferenceByAddress(dataType, {literal: locationOnly}, info, -DecodeUtils.EVM.WORD_SIZE); + return yield* decodeAbiReferenceByAddress( + dataType, + {location: "stackliteral", literal: locationOnly}, + info, + { abiPointerBase: -CodecUtils.EVM.WORD_SIZE} + ); } else { //multivalue case -- this case is straightforward //pass in 0 as the base since this is an absolute pointer - return yield* decodeCalldataReferenceByAddress(dataType, pointer, info, 0); + //(yeah we don't need to but let's be explicit) + return yield* decodeAbiReferenceByAddress(dataType, pointer, info, { abiPointerBase: 0 }); } } } @@ -77,23 +87,26 @@ export function* decodeLiteral(dataType: Types.Type, pointer: StackLiteralPointe //next: do we have an external function? these work differently on the stack //than elsewhere, so we can't just pass it on to decodeValue. if(dataType.typeClass === "function" && dataType.visibility === "external") { - let address = pointer.literal.slice(0, DecodeUtils.EVM.WORD_SIZE); - let selectorWord = pointer.literal.slice(-DecodeUtils.EVM.WORD_SIZE); - if(!checkPaddingLeft(address, DecodeUtils.EVM.ADDRESS_SIZE) - ||!checkPaddingLeft(selectorWord, DecodeUtils.EVM.SELECTOR_SIZE)) { - return new Errors.FunctionExternalErrorResult( - dataType, - new Errors.FunctionExternalStackPaddingError( - DecodeUtils.Conversion.toHexString(address), - DecodeUtils.Conversion.toHexString(selectorWord) - ) - ); + let address = pointer.literal.slice(0, CodecUtils.EVM.WORD_SIZE); + let selectorWord = pointer.literal.slice(-CodecUtils.EVM.WORD_SIZE); + if(!checkPaddingLeft(address, CodecUtils.EVM.ADDRESS_SIZE) + ||!checkPaddingLeft(selectorWord, CodecUtils.EVM.SELECTOR_SIZE)) { + return { + type: dataType, + kind: "error", + error: { + kind: "FunctionExternalStackPaddingError", + rawAddress: CodecUtils.Conversion.toHexString(address), + rawSelector: CodecUtils.Conversion.toHexString(selectorWord) + } + }; } - let selector = selectorWord.slice(-DecodeUtils.EVM.SELECTOR_SIZE); - return new Values.FunctionExternalValue( - dataType, - (yield* decodeExternalFunction(address, selector, info)) - ); + let selector = selectorWord.slice(-CodecUtils.EVM.SELECTOR_SIZE); + return { + type: dataType, + kind: "value", + value: (yield* decodeExternalFunction(address, selector, info)) + }; } //finally, if none of the above hold, we can just dispatch to decodeValue. diff --git a/packages/truffle-decoder-core/lib/decode/storage.ts b/packages/truffle-codec/lib/decode/storage.ts similarity index 71% rename from packages/truffle-decoder-core/lib/decode/storage.ts rename to packages/truffle-codec/lib/decode/storage.ts index 334187b06a7..8309c7a614f 100644 --- a/packages/truffle-decoder-core/lib/decode/storage.ts +++ b/packages/truffle-codec/lib/decode/storage.ts @@ -1,9 +1,9 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:storage"); +const debug = debugModule("codec:decode:storage"); import read from "../read"; -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; import decodeValue from "./value"; import { StoragePointer, DataPointer } from "../types/pointer"; import { EvmInfo } from "../types/evm"; @@ -27,27 +27,37 @@ export default function* decodeStorage(dataType: Types.Type, pointer: StoragePoi //Of course, pointers to value types don't exist in Solidity, so that warning is redundant, but... export function* decodeStorageReferenceByAddress(dataType: Types.ReferenceType, pointer: DataPointer, info: EvmInfo): IterableIterator { + const allocations = info.allocations.storage; + let rawValue: Uint8Array; try { rawValue = yield* read(pointer, info.state); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } - const startOffset = DecodeUtils.Conversion.toBN(rawValue); + const startOffset = CodecUtils.Conversion.toBN(rawValue); let rawSize: StorageTypes.StorageLength; try { - rawSize = storageSizeForType(dataType, info.userDefinedTypes, info.storageAllocations); + rawSize = storageSizeForType(dataType, info.userDefinedTypes, allocations); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } //we *know* the type being decoded must be sized in words, because it's a //reference type, but TypeScript doesn't, so we'll have to use a type //coercion const size = (<{words: number}>rawSize).words; //now, construct the storage pointer - const newPointer = { storage: { + const newPointer = { location: "storage" as "storage", range: { from: { slot: { offset: startOffset @@ -58,7 +68,7 @@ export function* decodeStorageReferenceByAddress(dataType: Types.ReferenceType, slot: { offset: startOffset.addn(size - 1) }, - index: DecodeUtils.EVM.WORD_SIZE - 1 + index: CodecUtils.EVM.WORD_SIZE - 1 } }}; //dispatch to decodeStorageReference @@ -70,6 +80,7 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: var length; const { state } = info; + const allocations = info.allocations.storage; switch (dataType.typeClass) { case "array": { @@ -78,13 +89,8 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: case "dynamic": debug("dynamic array"); debug("type %O", dataType); - try { - data = yield* read(pointer, state); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - length = DecodeUtils.Conversion.toBN(data).toNumber(); + data = yield* read(pointer, state); + length = CodecUtils.Conversion.toBN(data).toNumber(); break; case "static": debug("static array"); @@ -96,10 +102,14 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: debug("about to determine baseSize"); let baseSize: StorageTypes.StorageLength; try { - baseSize = storageSizeForType(dataType.baseType, info.userDefinedTypes, info.storageAllocations); + baseSize = storageSizeForType(dataType.baseType, info.userDefinedTypes, allocations); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } debug("baseSize %o", baseSize); @@ -111,7 +121,7 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: if(StorageTypes.isWordsLength(baseSize)) { //currentSlot will point to the start of the entry being decoded let currentSlot: StorageTypes.Slot = { - path: pointer.storage.from.slot, + path: pointer.range.from.slot, offset: new BN(0), hashPath: dataType.kind === "dynamic" }; @@ -132,7 +142,7 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: offset: currentSlot.offset.addn(baseSize.words - 1), hashPath: currentSlot.hashPath }, - index: DecodeUtils.EVM.WORD_SIZE - 1 + index: CodecUtils.EVM.WORD_SIZE - 1 }, }; @@ -143,18 +153,18 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: } else { - const perWord = Math.floor(DecodeUtils.EVM.WORD_SIZE / baseSize.bytes); + const perWord = Math.floor(CodecUtils.EVM.WORD_SIZE / baseSize.bytes); debug("perWord %d", perWord); //currentPosition will point to the start of the entry being decoded - //note we have baseSize.bytes <= DecodeUtils.EVM.WORD_SIZE + //note we have baseSize.bytes <= CodecUtils.EVM.WORD_SIZE let currentPosition: StorageTypes.StoragePosition = { slot: { - path: pointer.storage.from.slot, + path: pointer.range.from.slot, offset: new BN(0), hashPath: dataType.kind === "dynamic" }, - index: DecodeUtils.EVM.WORD_SIZE - baseSize.bytes //note the starting index! + index: CodecUtils.EVM.WORD_SIZE - baseSize.bytes //note the starting index! }; for (let i = 0; i < length; i++) { @@ -175,7 +185,7 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: currentPosition.index -= baseSize.bytes; if (currentPosition.index < 0) { currentPosition.slot.offset.iaddn(1); - currentPosition.index = DecodeUtils.EVM.WORD_SIZE - baseSize.bytes; + currentPosition.index = CodecUtils.EVM.WORD_SIZE - baseSize.bytes; } } } @@ -184,44 +194,43 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: for(let childRange of ranges) { decodedChildren.push( - (yield* decodeStorage(dataType.baseType, {storage: childRange}, info)) + (yield* decodeStorage(dataType.baseType, {location: "storage", range: childRange}, info)) ); } - return new Values.ArrayValue(dataType, decodedChildren); + return { + type: dataType, + kind: "value", + value: decodedChildren + }; } case "bytes": case "string": { - try { - data = yield* read(pointer, state); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } + data = yield* read(pointer, state); debug("data %O", data); - let lengthByte = data[DecodeUtils.EVM.WORD_SIZE - 1]; + let lengthByte = data[CodecUtils.EVM.WORD_SIZE - 1]; if (lengthByte % 2 == 0) { // string lives in word, length is last byte / 2 length = lengthByte / 2; debug("in-word; length %o", length); - return yield* decodeValue(dataType, { storage: { - from: { slot: pointer.storage.from.slot, index: 0 }, - to: { slot: pointer.storage.from.slot, index: length - 1} + return yield* decodeValue(dataType, { location: "storage", range: { + from: { slot: pointer.range.from.slot, index: 0 }, + to: { slot: pointer.range.from.slot, index: length - 1} }}, info); } else { - length = DecodeUtils.Conversion.toBN(data).subn(1).divn(2).toNumber(); + length = CodecUtils.Conversion.toBN(data).subn(1).divn(2).toNumber(); debug("new-word, length %o", length); - return yield* decodeValue(dataType, { - storage: { + return yield* decodeValue(dataType, { location: "storage", + range: { from: { slot: { - path: pointer.storage.from.slot, + path: pointer.range.from.slot, offset: new BN(0), hashPath: true }, @@ -236,12 +245,16 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: case "struct": { const typeId = dataType.id; - const structAllocation = info.storageAllocations[typeId]; + const structAllocation = allocations[parseInt(typeId)]; if(!structAllocation) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); + return { + type: dataType, + kind: "error", + error: { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + }; } let decodedMembers: Values.NameValuePair[] = []; @@ -257,40 +270,52 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: const childRange: StorageTypes.Range = { from: { slot: { - path: pointer.storage.from.slot, - offset: memberPointer.storage.from.slot.offset.clone() + path: pointer.range.from.slot, + offset: memberPointer.range.from.slot.offset.clone() //note that memberPointer should have no path }, - index: memberPointer.storage.from.index + index: memberPointer.range.from.index }, to: { slot: { - path: pointer.storage.from.slot, - offset: memberPointer.storage.to.slot.offset.clone() + path: pointer.range.from.slot, + offset: memberPointer.range.to.slot.offset.clone() //note that memberPointer should have no path }, - index: memberPointer.storage.to.index + index: memberPointer.range.to.index }, }; let memberName = memberAllocation.definition.name; let storedType = info.userDefinedTypes[typeId]; if(!storedType) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); + return { + type: dataType, + kind: "error", + error: { + kind: "UserDefinedTypeNotFoundError", + type: dataType + } + }; } let storedMemberType = storedType.memberTypes[index].type; let memberType = Types.specifyLocation(storedMemberType, "storage"); decodedMembers.push({ name: memberName, - value: (yield* decodeStorage(memberType, {storage: childRange}, info)) + value: (yield* decodeStorage( + memberType, + {location: "storage", range: childRange}, + info + )) }); } - return new Values.StructValue(dataType, decodedMembers); + return { + type: dataType, + kind: "value", + value: decodedMembers + }; } case "mapping": { @@ -300,15 +325,19 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: const valueType = dataType.valueType; let valueSize: StorageTypes.StorageLength; try { - valueSize = storageSizeForType(valueType, info.userDefinedTypes, info.storageAllocations); + valueSize = storageSizeForType(valueType, info.userDefinedTypes, allocations); } catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); + return { + type: dataType, + kind: "error", + error: error.error + }; } let decodedEntries: Values.KeyValuePair[] = []; - const baseSlot: StorageTypes.Slot = pointer.storage.from.slot; + const baseSlot: StorageTypes.Slot = pointer.range.from.slot; debug("baseSlot %o", baseSlot); debug("base slot address %o", slotAddress(baseSlot)); @@ -321,7 +350,8 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: if(StorageTypes.isWordsLength(valueSize)) { valuePointer = { - storage: { + location: "storage", + range: { from: { slot: { key, @@ -336,21 +366,22 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: path: baseSlot, offset: new BN(valueSize.words - 1) }, - index: DecodeUtils.EVM.WORD_SIZE - 1 + index: CodecUtils.EVM.WORD_SIZE - 1 } } }; } else { valuePointer = { - storage: { + location: "storage", + range: { from: { slot: { key, path: baseSlot, offset: new BN(0) }, - index: DecodeUtils.EVM.WORD_SIZE - valueSize.bytes + index: CodecUtils.EVM.WORD_SIZE - valueSize.bytes }, to: { slot: { @@ -358,7 +389,7 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: path: baseSlot, offset: new BN(0) }, - index: DecodeUtils.EVM.WORD_SIZE - 1 + index: CodecUtils.EVM.WORD_SIZE - 1 } } }; @@ -370,7 +401,11 @@ export function* decodeStorageReference(dataType: Types.ReferenceType, pointer: }); } - return new Values.MappingValue(dataType, decodedEntries); + return { + type: dataType, + kind: "value", + value: decodedEntries + }; } } } diff --git a/packages/truffle-codec/lib/decode/value.ts b/packages/truffle-codec/lib/decode/value.ts new file mode 100644 index 00000000000..26d4e635bf8 --- /dev/null +++ b/packages/truffle-codec/lib/decode/value.ts @@ -0,0 +1,554 @@ +import debugModule from "debug"; +const debug = debugModule("codec:decode:value"); + +import read from "../read"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values } from "truffle-codec-utils"; +import BN from "bn.js"; +import utf8 from "utf8"; +import { DataPointer } from "../types/pointer"; +import { EvmInfo } from "../types/evm"; +import { DecoderOptions } from "../types/options"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; +import { StopDecodingError } from "../types/errors"; + +export default function* decodeValue(dataType: Types.Type, pointer: DataPointer, info: EvmInfo, options: DecoderOptions = {}): IterableIterator { + const { state } = info; + const { permissivePadding, strictAbiMode: strict } = options; //if these are undefined they'll still be falsy so OK + + let bytes: Uint8Array; + let rawBytes: Uint8Array; + try { + bytes = yield* read(pointer, state); + } + catch(error) { //error: Errors.DecodingError + debug("segfault, pointer %o, state: %O", pointer, state); + if(strict) { + throw new StopDecodingError(error.error); + } + return { + type: dataType, + kind: "error", + error: error.error + }; + } + rawBytes = bytes; + + debug("type %O", dataType); + debug("pointer %o", pointer); + + switch(dataType.typeClass) { + + case "bool": { + const numeric = CodecUtils.Conversion.toBN(bytes); + if(numeric.eqn(0)) { + return { + type: dataType, + kind: "value", + value: { asBool: false } + }; + } + else if(numeric.eqn(1)) { + return { + type: dataType, + kind: "value", + value: { asBool: true } + }; + } + else { + let error = { + kind: "BoolOutOfRangeError" as "BoolOutOfRangeError", + rawAsBN: numeric + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + } + + case "uint": + //first, check padding (if needed) + if(!permissivePadding && !checkPaddingLeft(bytes, dataType.bits/8)) { + let error = { + kind: "UintPaddingError" as "UintPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + //now, truncate to appropriate length (keeping the bytes on the right) + bytes = bytes.slice(-dataType.bits/8); + return { + type: dataType, + kind: "value", + value: { + asBN: CodecUtils.Conversion.toBN(bytes), + rawAsBN: CodecUtils.Conversion.toBN(rawBytes) + } + }; + case "int": + //first, check padding (if needed) + if(!permissivePadding && !checkPaddingSigned(bytes, dataType.bits/8)) { + let error = { + kind: "IntPaddingError" as "IntPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + //now, truncate to appropriate length (keeping the bytes on the right) + bytes = bytes.slice(-dataType.bits/8); + return { + type: dataType, + kind: "value", + value: { + asBN: CodecUtils.Conversion.toSignedBN(bytes), + rawAsBN: CodecUtils.Conversion.toSignedBN(rawBytes) + } + }; + + case "address": + if(!permissivePadding && !checkPaddingLeft(bytes, CodecUtils.EVM.ADDRESS_SIZE)) { + let error = { + kind: "AddressPaddingError" as "AddressPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + return { + type: dataType, + kind: "value", + value: { + asAddress: CodecUtils.Conversion.toAddress(bytes), + rawAsHex: CodecUtils.Conversion.toHexString(rawBytes) + } + }; + + case "contract": + if(!permissivePadding && !checkPaddingLeft(bytes, CodecUtils.EVM.ADDRESS_SIZE)) { + let error = { + kind: "ContractPaddingError" as "ContractPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + const fullType = Types.fullType(dataType, info.userDefinedTypes); + const contractValueInfo = (yield* decodeContract(bytes, info)); + return { + type: fullType, + kind: "value", + value: contractValueInfo + }; + + case "bytes": + switch(dataType.kind) { + case "static": + //first, check padding (if needed) + if(!permissivePadding && !checkPaddingRight(bytes, dataType.length)) { + let error = { + kind: "BytesPaddingError" as "BytesPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + //now, truncate to appropriate length + bytes = bytes.slice(0, dataType.length); + return { + type: dataType, + kind: "value", + value: { + asHex: CodecUtils.Conversion.toHexString(bytes), + rawAsHex: CodecUtils.Conversion.toHexString(rawBytes) + } + }; + case "dynamic": + //no need to check padding here + return { + type: dataType, + kind: "value", + value: { + asHex: CodecUtils.Conversion.toHexString(bytes), + } + }; + } + + case "string": + //there is no padding check for strings + return { + type: dataType, + kind: "value", + value: decodeString(bytes) + }; + + case "function": + switch(dataType.visibility) { + case "external": + if(!checkPaddingRight(bytes, CodecUtils.EVM.ADDRESS_SIZE + CodecUtils.EVM.SELECTOR_SIZE)) { + let error = { + kind: "FunctionExternalNonStackPaddingError" as "FunctionExternalNonStackPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + const address = bytes.slice(0, CodecUtils.EVM.ADDRESS_SIZE); + const selector = bytes.slice(CodecUtils.EVM.ADDRESS_SIZE, CodecUtils.EVM.ADDRESS_SIZE + CodecUtils.EVM.SELECTOR_SIZE); + return { + type: dataType, + kind: "value", + value: (yield* decodeExternalFunction(address, selector, info)) + }; + case "internal": + if(strict) { + //internal functions don't go in the ABI! + //this should never happen, but just to be sure... + throw new StopDecodingError( + { kind: "InternalFunctionInABIError" } + ); + } + if(!checkPaddingLeft(bytes, 2 * CodecUtils.EVM.PC_SIZE)) { + return { + type: dataType, + kind: "error", + error: { + kind: "FunctionInternalPaddingError", + raw: CodecUtils.Conversion.toHexString(bytes) + } + }; + } + const deployedPc = bytes.slice(-CodecUtils.EVM.PC_SIZE); + const constructorPc = bytes.slice(-CodecUtils.EVM.PC_SIZE * 2, -CodecUtils.EVM.PC_SIZE); + return decodeInternalFunction(dataType, deployedPc, constructorPc, info); + } + break; //to satisfy TypeScript + + case "enum": { + const numeric = CodecUtils.Conversion.toBN(bytes); + const fullType = Types.fullType(dataType, info.userDefinedTypes); + if(!fullType.options) { + let error = { + kind: "EnumNotFoundDecodingError" as "EnumNotFoundDecodingError", + type: fullType, + rawAsBN: numeric + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: fullType, + kind: "error", + error + }; + } + const numOptions = fullType.options.length; + const numBytes = Math.ceil(Math.log2(numOptions) / 8); + if(numeric.ltn(numOptions)) { + const name = fullType.options[numeric.toNumber()]; + return { + type: fullType, + kind: "value", + value: { + name, + numericAsBN: numeric + } + }; + } + else { + let error = { + kind: "EnumOutOfRangeError" as "EnumOutOfRangeError", + type: fullType, + rawAsBN: numeric + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: fullType, + kind: "error", + error + }; + } + } + //will have to split these once we actually support fixed-point + case "fixed": + case "ufixed": { + //skipping padding check as we don't support this anyway + const hex = CodecUtils.Conversion.toHexString(bytes); + let error = { + kind: "FixedPointNotYetSupportedError" as "FixedPointNotYetSupportedError", + raw: hex + }; + if(strict) { + throw new StopDecodingError(error); + } + return { + type: dataType, + kind: "error", + error + }; + } + } +} + +export function decodeString(bytes: Uint8Array): Values.StringValueInfo { + //the following line takes our UTF-8 string... and interprets each byte + //as a UTF-16 bytepair. Yikes! Fortunately, we have a library to repair that. + let badlyEncodedString = String.fromCharCode.apply(undefined, bytes); + try { + //this will throw an error if we have malformed UTF-8 + let correctlyEncodedString = utf8.decode(badlyEncodedString); + //NOTE: we don't use node's builtin Buffer class to do the UTF-8 decoding + //here, because that handles malformed UTF-8 by means of replacement characters + //(U+FFFD). That loses information. So we use the utf8 package instead, + //and... well, see the catch block below. + return { + kind: "valid", + asString: correctlyEncodedString + }; + } + catch(_) { + //we're going to ignore the precise error and just assume it's because + //the string was malformed (what else could it be?) + let hexString = CodecUtils.Conversion.toHexString(bytes); + return { + kind: "malformed", + asHex: hexString + }; + } +} + +//NOTE that this function returns a ContractValueInfo, not a ContractResult +export function* decodeContract(addressBytes: Uint8Array, info: EvmInfo): IterableIterator { + let address = CodecUtils.Conversion.toAddress(addressBytes); + let rawAddress = CodecUtils.Conversion.toHexString(addressBytes); + let codeBytes: Uint8Array = yield { + type: "code", + address + }; + let code = CodecUtils.Conversion.toHexString(codeBytes); + let context = CodecUtils.Contexts.findDecoderContext(info.contexts, code); + if(context !== null && context.contractName !== undefined) { + return { + kind: "known", + address, + rawAddress, + class: CodecUtils.Contexts.contextToType(context) + }; + } + else { + return { + kind: "unknown", + address, + rawAddress + }; + } +} + +//note: address can have extra zeroes on the left like elsewhere, but selector should be exactly 4 bytes +//NOTE this again returns a FunctionExternalValueInfo, not a FunctionExternalResult +export function* decodeExternalFunction(addressBytes: Uint8Array, selectorBytes: Uint8Array, info: EvmInfo): IterableIterator { + let contract = (yield* decodeContract(addressBytes, info)); + let selector = CodecUtils.Conversion.toHexString(selectorBytes); + if(contract.kind === "unknown") { + return { + kind: "unknown", + contract, + selector + }; + } + let contractId = ( contract.class).id; //sorry! will be fixed soon! + let context = Object.values(info.contexts).find( + context => context.contractId.toString() === contractId //similarly! I hope! + ); + let abiEntry = context.abi !== undefined + ? context.abi[selector] + : undefined; + if(abiEntry === undefined) { + return { + kind: "invalid", + contract, + selector + }; + } + return { + kind: "known", + contract, + selector, + abi: abiEntry + }; +} + +//this one works a bit differently -- in order to handle errors, it *does* return a FunctionInternalResult +export function decodeInternalFunction(dataType: Types.FunctionInternalType, deployedPcBytes: Uint8Array, constructorPcBytes: Uint8Array, info: EvmInfo): Values.FunctionInternalResult { + let deployedPc: number = CodecUtils.Conversion.toBN(deployedPcBytes).toNumber(); + let constructorPc: number = CodecUtils.Conversion.toBN(constructorPcBytes).toNumber(); + let context: Types.ContractType = CodecUtils.Contexts.contextToType(info.currentContext); + //before anything else: do we even have an internal functions table? + //if not, we'll just return the info we have without really attemting to decode + if(!info.internalFunctionsTable) { + return { + type: dataType, + kind: "value", + value: { + kind: "unknown", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: constructorPc + } + }; + } + //also before we continue: is the PC zero? if so let's just return that + if(deployedPc === 0 && constructorPc === 0) { + return { + type: dataType, + kind: "value", + value: { + kind: "exception", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: constructorPc + } + }; + } + //another check: is only the deployed PC zero? + if(deployedPc === 0 && constructorPc !== 0) { + return { + type: dataType, + kind: "error", + error: { + kind: "MalformedInternalFunctionError", + context, + deployedProgramCounter: 0, + constructorProgramCounter: constructorPc + } + }; + } + //one last pre-check: is this a deployed-format pointer in a constructor? + if(info.currentContext.isConstructor && constructorPc === 0) { + return { + type: dataType, + kind: "error", + error: { + kind: "DeployedFunctionInConstructorError", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: 0 + } + }; + } + //otherwise, we get our function + let pc = info.currentContext.isConstructor + ? constructorPc + : deployedPc; + let functionEntry = info.internalFunctionsTable[pc]; + if(!functionEntry) { + //if it's not zero and there's no entry... error! + return { + type: dataType, + kind: "error", + error: { + kind: "NoSuchInternalFunctionError", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: constructorPc + } + }; + } + if(functionEntry.isDesignatedInvalid) { + return { + type: dataType, + kind: "value", + value: { + kind: "exception", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: constructorPc + } + }; + } + let name = functionEntry.name; + let mutability = functionEntry.mutability; + let definedIn: Types.ContractType = { + typeClass: "contract", + kind: "native", + id: functionEntry.contractId.toString(), + typeName: functionEntry.contractName, + contractKind: functionEntry.contractKind, + payable: functionEntry.contractPayable + }; + return { + type: dataType, + kind: "value", + value: { + kind: "function", + context, + deployedProgramCounter: deployedPc, + constructorProgramCounter: constructorPc, + name, + definedIn, + mutability + } + }; +} + +function checkPaddingRight(bytes: Uint8Array, length: number): boolean { + let padding = bytes.slice(length); //cut off the first length bytes + return padding.every(paddingByte => paddingByte === 0); +} + +//exporting this one for use in stack.ts +export function checkPaddingLeft(bytes: Uint8Array, length: number): boolean { + let padding = bytes.slice(0, -length); //cut off the last length bytes + return padding.every(paddingByte => paddingByte === 0); +} + +function checkPaddingSigned(bytes: Uint8Array, length: number): boolean { + let padding = bytes.slice(0, -length); //padding is all but the last length bytes + let value = bytes.slice(-length); //meanwhile the actual value is those last length bytes + let signByte = value[0] & 0x80 ? 0xff : 0x00; + return padding.every(paddingByte => paddingByte === signByte); +} diff --git a/packages/truffle-codec/lib/encode/abi.ts b/packages/truffle-codec/lib/encode/abi.ts new file mode 100644 index 00000000000..7685db1ed13 --- /dev/null +++ b/packages/truffle-codec/lib/encode/abi.ts @@ -0,0 +1,174 @@ +import { Values, Conversion as ConversionUtils, EVM as EVMUtils } from "truffle-codec-utils"; +import { AbiAllocations } from "../types/allocation"; +import { isTypeDynamic, abiSizeForType } from "../allocate/abi"; +import sum from "lodash.sum"; +import utf8 from "utf8"; + +//UGH -- it turns out TypeScript can't handle nested tagged unions +//see: https://github.com/microsoft/TypeScript/issues/18758 +//so, I'm just going to have to throw in a bunch of type coercions >_> + +//NOTE: Tuple (as opposed to struct) is not supported yet! +//Coming soon though! +export function encodeAbi(input: Values.Result, allocations?: AbiAllocations): Uint8Array | undefined { + //errors can't be encoded + if(input.kind === "error") { + return undefined; + } + let bytes: Uint8Array; + //TypeScript can at least infer in the rest of this that we're looking + //at a value, not an error! But that's hardly enough... + switch(input.type.typeClass) { + case "mapping": + case "magic": + //neither of these can go in the ABI + return undefined; + case "uint": + case "int": + return ConversionUtils.toBytes((input).value.asBN, EVMUtils.WORD_SIZE); + case "enum": + return ConversionUtils.toBytes((input).value.numericAsBN, EVMUtils.WORD_SIZE); + case "bool": { + bytes = new Uint8Array(EVMUtils.WORD_SIZE); //is initialized to zeroes + if((input).value.asBool) { + bytes[EVMUtils.WORD_SIZE - 1] = 1; + } + return bytes; + } + case "bytes": + bytes = ConversionUtils.toBytes((input).value.asHex); + switch(input.type.kind) { + case "static": + let padded = new Uint8Array(EVMUtils.WORD_SIZE); //initialized to zeroes + padded.set(bytes); + return padded; + case "dynamic": + return padAndPrependLength(bytes); + } + case "address": + return ConversionUtils.toBytes((input).value.asAddress, EVMUtils.WORD_SIZE); + case "contract": + return ConversionUtils.toBytes((input).value.address, EVMUtils.WORD_SIZE); + case "string": { + let coercedInput: Values.StringValue = input; + switch(coercedInput.value.kind) { + case "valid": + bytes = stringToBytes(coercedInput.value.asString); + break; + case "malformed": + bytes = ConversionUtils.toBytes(coercedInput.value.asHex); + break; + } + return padAndPrependLength(bytes); + } + case "function": { + switch(input.type.visibility) { + case "internal": + return undefined; //internal functions can't go in the ABI! + case "external": + let coercedInput: Values.FunctionExternalValue = input; + let encoded = new Uint8Array(EVMUtils.WORD_SIZE); //starts filled w/0s + let addressBytes = ConversionUtils.toBytes(coercedInput.value.contract.address); //should already be correct length + let selectorBytes = ConversionUtils.toBytes(coercedInput.value.selector); //should already be correct length + encoded.set(addressBytes); + encoded.set(selectorBytes, EVMUtils.ADDRESS_SIZE); //set it after the address + return encoded; + } + } + //the fixed & ufixed cases will get skipped for now + case "array": { + let coercedInput: Values.ArrayValue = input; + if(coercedInput.reference !== undefined) { + return undefined; //circular values can't be encoded + } + let staticEncoding = encodeTupleAbi(coercedInput.value, allocations); + switch(input.type.kind) { + case "static": + return staticEncoding; + case "dynamic": + let encoded = new Uint8Array(EVMUtils.WORD_SIZE + staticEncoding.length); //leave room for length + encoded.set(staticEncoding, EVMUtils.WORD_SIZE); //again, leave room for length beforehand + let lengthBytes = ConversionUtils.toBytes(coercedInput.value.length, EVMUtils.WORD_SIZE); + encoded.set(lengthBytes); //and now we set the length + return encoded; + } + } + case "struct": { + let coercedInput: Values.StructValue = input; + if(coercedInput.reference !== undefined) { + return undefined; //circular values can't be encoded + } + return encodeTupleAbi(coercedInput.value.map(({value}) => value), allocations); + } + } +} + +export function stringToBytes(input: string): Uint8Array { + input = utf8.encode(input); + let bytes = new Uint8Array(input.length); + for(let i = 0; i < input.length; i++) { + bytes[i] = input.charCodeAt(i); + } + return bytes; + //NOTE: this will throw an error if the string contained malformed UTF-16! + //but, well, it shouldn't contain that... +} + +function padAndPrependLength(bytes: Uint8Array): Uint8Array { + let length = bytes.length; + let paddedLength = EVMUtils.WORD_SIZE * Math.ceil(length / EVMUtils.WORD_SIZE); + let encoded = new Uint8Array(EVMUtils.WORD_SIZE + paddedLength); + encoded.set(bytes, EVMUtils.WORD_SIZE); //start 32 in to leave room for the length beforehand + let lengthBytes = ConversionUtils.toBytes(length, EVMUtils.WORD_SIZE); + encoded.set(lengthBytes); //and now we set the length + return encoded; +} + +export function encodeTupleAbi(tuple: Values.Result[], allocations?: AbiAllocations): Uint8Array | undefined { + let elementEncodings = tuple.map(element => encodeAbi(element, allocations)); + if(elementEncodings.some(element => element === undefined)) { + return undefined; + } + let isElementDynamic: boolean[] = tuple.map(element => isTypeDynamic(element.type, allocations)); + //heads and tails here are as discussed in the ABI docs; + //for a static type the head is the encoding and the tail is empty, + //for a dynamic type the head is the pointer and the tail is the encoding + let heads: Uint8Array[] = []; + let tails: Uint8Array[] = []; + //but first, we need to figure out where the first tail will start, + //by adding up the sizes of all the heads (we can easily do this in + //advance via abiSizeForType, without needing to know the particular + //values of the heads) + let startOfNextTail = sum(tuple.map(element => abiSizeForType(element.type, allocations))); + for(let i = 0; i < tuple.length; i++) { + let head: Uint8Array; + let tail: Uint8Array; + if(!isElementDynamic[i]) { + //static case + head = elementEncodings[i]; + tail = new Uint8Array(); //empty array + } + else { + //dynamic case + head = ConversionUtils.toBytes(startOfNextTail, EVMUtils.WORD_SIZE); + tail = elementEncodings[i]; + } + heads.push(head); + tails.push(tail); + startOfNextTail += tail.length; + } + //finally, we need to concatenate everything together! + //since we're dealing with Uint8Arrays, we have to do this manually + let totalSize = startOfNextTail; + let encoded = new Uint8Array(totalSize); + let position = 0; + for(let head of heads) { + encoded.set(head, position); + position += head.length; + } + for(let tail of tails) { + encoded.set(tail, position); + position += tail.length; + } + return encoded; +} diff --git a/packages/truffle-codec/lib/encode/key.ts b/packages/truffle-codec/lib/encode/key.ts new file mode 100644 index 00000000000..f63bafdfbba --- /dev/null +++ b/packages/truffle-codec/lib/encode/key.ts @@ -0,0 +1,107 @@ +import { Values, Conversion as ConversionUtils, EVM as EVMUtils } from "truffle-codec-utils"; +import { stringToBytes } from "./abi"; + +//UGH -- it turns out TypeScript can't handle nested tagged unions +//see: https://github.com/microsoft/TypeScript/issues/18758 +//so, I'm just going to have to throw in a bunch of type coercions >_> + +export function encodeMappingKey(input: Values.ElementaryValue): Uint8Array { + let bytes: Uint8Array; + //TypeScript can at least infer in the rest of this that we're looking + //at a value, not an error! But that's hardly enough... + switch(input.type.typeClass) { + case "uint": + case "int": + return ConversionUtils.toBytes((input).value.asBN, EVMUtils.WORD_SIZE); + case "bool": { + bytes = new Uint8Array(EVMUtils.WORD_SIZE); //is initialized to zeroes + if((input).value.asBool) { + bytes[EVMUtils.WORD_SIZE - 1] = 1; + } + return bytes; + } + case "bytes": + bytes = ConversionUtils.toBytes((input).value.asHex); + switch(input.type.kind) { + case "static": + let padded = new Uint8Array(EVMUtils.WORD_SIZE); //initialized to zeroes + padded.set(bytes); + return padded; + case "dynamic": + return bytes; //NO PADDING IS USED + } + case "address": + return ConversionUtils.toBytes((input).value.asAddress, EVMUtils.WORD_SIZE); + case "string": { + let coercedInput: Values.StringValue = input; + switch(coercedInput.value.kind) { //NO PADDING IS USED + case "valid": + return stringToBytes(coercedInput.value.asString); + case "malformed": + return ConversionUtils.toBytes(coercedInput.value.asHex); + } + } + //fixed and ufixed are skipped for now + } +} + +export function mappingKeyAsHex(input: Values.ElementaryValue): string { + return ConversionUtils.toHexString(encodeMappingKey(input)); +} + +//this is like the old toSoliditySha3Input, but for debugging purposes ONLY +//it will NOT produce correct input to soliditySha3 +//please use mappingKeyAsHex instead if you wish to encode a mapping key. +export function keyInfoForPrinting(input: Values.ElementaryValue): {type: string, value: string} { + switch(input.type.typeClass) { + case "uint": + return { + type: "uint", + value: (input).value.asBN.toString() + }; + case "int": + return { + type: "int", + value: (input).value.asBN.toString() + }; + case "bool": + //this is the case that won't work as valid input to soliditySha3 :) + return { + type: "uint", + value: (input).value.asBool.toString() + }; + case "bytes": + switch(input.type.kind) { + case "static": + return { + type: "bytes32", + value: (input).value.asHex + }; + case "dynamic": + return { + type: "bytes", + value: (input).value.asHex + }; + } + case "address": + return { + type: "address", + value: (input).value.asAddress + }; + case "string": + let coercedInput: Values.StringValue = input; + switch(coercedInput.value.kind) { + case "valid": + return { + type: "string", + value: coercedInput.value.asString + }; + case "malformed": + return { + type: "bytes", + value: coercedInput.value.asHex + }; + } + //fixed and ufixed are skipped for now + } +} diff --git a/packages/truffle-codec/lib/interface/decoding.ts b/packages/truffle-codec/lib/interface/decoding.ts new file mode 100644 index 00000000000..28aa8d491c1 --- /dev/null +++ b/packages/truffle-codec/lib/interface/decoding.ts @@ -0,0 +1,231 @@ +import debugModule from "debug"; +const debug = debugModule("codec:interface:decoding"); + +import { AstDefinition, Types, Values } from "truffle-codec-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import * as Pointer from "../types/pointer"; +import { EvmInfo } from "../types/evm"; +import { DecoderRequest, GeneratorJunk } from "../types/request"; +import { CalldataAllocation, EventAllocation, EventArgumentAllocation } from "../types/allocation"; +import { CalldataDecoding, LogDecoding, AbiArgument } from "../types/decoding"; +import { encodeTupleAbi } from "../encode/abi"; +import read from "../read"; +import decode from "../decode"; + +export function* decodeVariable(definition: AstDefinition, pointer: Pointer.DataPointer, info: EvmInfo): IterableIterator { + let compiler = info.currentContext.compiler; + let dataType = Types.definitionToType(definition, compiler); + debug("definition %O", definition); + return yield* decode(dataType, pointer, info); //no need to pass an offset +} + +export function* decodeCalldata(info: EvmInfo): IterableIterator { + const context = info.currentContext; + if(context === null) { + //if we don't know the contract ID, we can't decode + return { + kind: "unknown", + decodingMode: "full", + data: CodecUtils.Conversion.toHexString(info.state.calldata) + } + } + const compiler = info.currentContext.compiler; + const contractId = context.contractId; + const contractType = CodecUtils.Contexts.contextToType(context); + const allocations = info.allocations.calldata[contractId]; + let allocation: CalldataAllocation; + let isConstructor: boolean = info.currentContext.isConstructor; + let selector: string; + //first: is this a creation call? + if(isConstructor) { + allocation = allocations.constructorAllocation; + } + else { + //skipping any error-handling on this read, as a calldata read can't throw anyway + let rawSelector = (yield* read( + { location: "calldata", + start: 0, + length: CodecUtils.EVM.SELECTOR_SIZE + }, + info.state + )); + selector = CodecUtils.Conversion.toHexString(rawSelector); + allocation = allocations.functionAllocations[selector]; + } + if(allocation === undefined) { + return { + kind: "message", + class: contractType, + abi: CodecUtils.AbiUtils.fallbackAbiForPayability(context.payable), + data: CodecUtils.Conversion.toHexString(info.state.calldata), + decodingMode: "full", + }; + } + //you can't map with a generator, so we have to do this map manually + let decodedArguments: AbiArgument[] = []; + for(const argumentAllocation of allocation.arguments) { + const value = (yield* decode( + Types.definitionToType(argumentAllocation.definition, compiler), + argumentAllocation.pointer, + info, + { abiPointerBase: allocation.offset } //note the use of the offset for decoding pointers! + )); + const name = argumentAllocation.definition.name; + decodedArguments.push( + name //deliberate general falsiness test + ? { name, value } + : { value } + ); + } + if(isConstructor) { + return { + kind: "constructor", + class: contractType, + arguments: decodedArguments, + abi: allocation.abi, + bytecode: CodecUtils.Conversion.toHexString(info.state.calldata.slice(0, allocation.offset)), + decodingMode: "full", + }; + } + else { + return { + kind: "function", + class: contractType, + abi: allocation.abi, + arguments: decodedArguments, + selector, + decodingMode: "full" + }; + } +} + +//note: this will likely change in the future to take options rather than targetName, but I'm +//leaving it alone for now, as I'm not sure what form those options will take +//(and this is something we're a bit more OK with breaking since it's primarily +//for internal use :) ) +export function* decodeEvent(info: EvmInfo, address: string, targetName?: string): IterableIterator { + const allocations = info.allocations.event; + debug("event allocations: %O", allocations); + let rawSelector: Uint8Array; + let selector: string; + let contractAllocations: {[contractId: number]: EventAllocation}; //for non-anonymous events + let libraryAllocations: {[contractId: number]: EventAllocation}; //similar + const topicsCount = info.state.eventtopics.length; + //yeah, it's not great to read directly from the state like this (bypassing read), but what are you gonna do? + if(topicsCount > 0) { + rawSelector = (yield* read( + { location: "eventtopic", + topic: 0 + }, + info.state + )); + selector = CodecUtils.Conversion.toHexString(rawSelector); + ({ contract: contractAllocations, library: libraryAllocations } = allocations[topicsCount].bySelector[selector] || {contract: {}, library: {}}); + } + else { + //if we don't have a selector, it means we don't have any non-anonymous events + contractAllocations = {}; + libraryAllocations = {}; + } + //now: let's get our allocations for anonymous events + //note: these ones map contract IDs to *arrays* of event allocations, not individual allocations! + const { contract: contractAnonymousAllocations, library: libraryAnonymousAllocations } = allocations[topicsCount].anonymous; + //now: what contract are we (probably) dealing with? let's get its code to find out + const codeBytes: Uint8Array = yield { + type: "code", + address + }; + const codeAsHex = CodecUtils.Conversion.toHexString(codeBytes); + const contractContext = CodecUtils.Contexts.findDecoderContext(info.contexts, codeAsHex); + let possibleContractAllocations: EventAllocation[]; //excludes anonymous events + let possibleContractAnonymousAllocations: EventAllocation[]; + if(contractContext) { + //if we found the contract, maybe it's from that contract + const contractId = contractContext.contractId; + const contractAllocation = contractAllocations[contractId]; + const contractAnonymousAllocation = contractAnonymousAllocations[contractId]; + possibleContractAllocations = contractAllocation + ? [contractAllocation] + : []; + possibleContractAnonymousAllocations = contractAnonymousAllocation || []; + } + else { + //if we couldn't determine the contract, well, we have to assume it's from a library + possibleContractAllocations = []; + possibleContractAnonymousAllocations = []; + } + //now we get all the library allocations! + const possibleLibraryAllocations = Object.values(libraryAllocations); + const possibleLibraryAnonymousAllocations = [].concat(...Object.values(libraryAnonymousAllocations)); + //now we put it all together! + const possibleAllocations = possibleContractAllocations.concat(possibleLibraryAllocations); + const possibleAnonymousAllocations = possibleContractAnonymousAllocations.concat(possibleLibraryAnonymousAllocations); + const possibleAllocationsTotal = possibleAllocations.concat(possibleAnonymousAllocations); + let decodings: LogDecoding[] = []; + allocationAttempts: for(const allocation of possibleAllocationsTotal) { + //first: do a name check so we can skip decoding if name is wrong + if(targetName !== undefined && allocation.definition.name !== targetName) { + continue; + } + const id = allocation.contractId; + const attemptContext = info.contexts[id]; + const contractType = CodecUtils.Contexts.contextToType(attemptContext); + //you can't map with a generator, so we have to do this map manually + let decodedArguments: AbiArgument[] = []; + for(const argumentAllocation of allocation.arguments) { + let value: Values.Result; + try { + value = (yield* decode( + Types.definitionToType(argumentAllocation.definition, attemptContext.compiler), + argumentAllocation.pointer, + info, + { strictAbiMode: true } //turns on STRICT MODE to cause more errors to be thrown + )); + } + catch(_) { + continue allocationAttempts; //if an error occurred, this isn't a valid decoding! + } + const name = argumentAllocation.definition.name; + const indexed = argumentAllocation.pointer.location === "eventtopic"; + decodedArguments.push( + name //deliberate general falsiness test + ? { name, indexed, value } + : { indexed, value } + ); + } + debug("decodedArguments: %O", decodedArguments); + //OK, so, having decoded the result, the question is: does it reencode to the original? + //first, we have to filter out the indexed arguments, and also get rid of the name information + const nonIndexedValues = decodedArguments + .filter(argument => !argument.indexed) + .map(argument => argument.value); + //now, we can encode! + debug("nonIndexedValues: %O", nonIndexedValues); + const reEncodedData = encodeTupleAbi(nonIndexedValues, info.allocations.abi); + //are they equal? + const encodedData = info.state.eventdata; //again, not great to read this directly, but oh well + if(CodecUtils.EVM.equalData(encodedData, reEncodedData)) { + if(allocation.definition.anonymous) { + decodings.push({ + kind: "anonymous", + class: contractType, + abi: allocation.abi, + arguments: decodedArguments, + decodingMode: "full" + }); + } + else { + decodings.push({ + kind: "event", + class: contractType, + abi: allocation.abi, + arguments: decodedArguments, + selector, + decodingMode: "full" + }); + } + } + //otherwise, just move on + } + return decodings; +} diff --git a/packages/truffle-codec/lib/interface/index.ts b/packages/truffle-codec/lib/interface/index.ts new file mode 100644 index 00000000000..9bb77ba6e5e --- /dev/null +++ b/packages/truffle-codec/lib/interface/index.ts @@ -0,0 +1,15 @@ +export { getStorageAllocations, storageSize } from "../allocate/storage"; +export { getAbiAllocations, getCalldataAllocations, getEventAllocations } from "../allocate/abi"; +export { getMemoryAllocations } from "../allocate/memory"; +export { readStack } from "../read/stack"; +export { slotAddress } from "../read/storage"; +export { StoragePointer } from "../types/pointer"; +export { ContractAllocationInfo, StorageAllocations, StorageMemberAllocation, AbiAllocations, CalldataAllocations, EventAllocations } from "../types/allocation"; +export { Slot, isWordsLength, equalSlots } from "../types/storage"; +export { DecoderRequest, isStorageRequest, isCodeRequest } from "../types/request"; +export { EvmInfo, AllocationInfo } from "../types/evm"; +export { CalldataDecoding, LogDecoding } from "../types/decoding"; + +export { decodeVariable, decodeEvent, decodeCalldata } from "./decoding"; + +export { encodeAbi, encodeTupleAbi } from "../encode/abi"; //we have no need to export these at present, but someone might want them? diff --git a/packages/truffle-decoder-core/lib/read/memory.ts b/packages/truffle-codec/lib/read/bytes.ts similarity index 91% rename from packages/truffle-decoder-core/lib/read/memory.ts rename to packages/truffle-codec/lib/read/bytes.ts index 52a7c5c2eb6..76764b17439 100644 --- a/packages/truffle-decoder-core/lib/read/memory.ts +++ b/packages/truffle-codec/lib/read/bytes.ts @@ -1,6 +1,6 @@ import BN from "bn.js"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; /** * read word from memory @@ -12,7 +12,7 @@ import * as DecodeUtils from "truffle-decode-utils"; * @return {BN} */ export function read(memory: Uint8Array, offset: number) { - return readBytes(memory, offset, DecodeUtils.EVM.WORD_SIZE); + return readBytes(memory, offset, CodecUtils.EVM.WORD_SIZE); } /** diff --git a/packages/truffle-codec/lib/read/constant.ts b/packages/truffle-codec/lib/read/constant.ts new file mode 100644 index 00000000000..1a015fee396 --- /dev/null +++ b/packages/truffle-codec/lib/read/constant.ts @@ -0,0 +1,33 @@ +import debugModule from "debug"; +const debug = debugModule("codec:read:constant"); + +import * as CodecUtils from "truffle-codec-utils"; +import BN from "bn.js"; +import { Errors } from "truffle-codec-utils"; + +export function readDefinition(definition: CodecUtils.AstDefinition): Uint8Array { + + debug("definition %o", definition); + + switch(CodecUtils.Definition.typeClass(definition)) + { + case "rational": + let numericalValue: BN = CodecUtils.Definition.rationalValue(definition); + return CodecUtils.Conversion.toBytes(numericalValue, CodecUtils.EVM.WORD_SIZE); + //you may be wondering, why do we not just use definition.value here, + //like we do below? answer: because if this isn't a literal, that may not + //exist + case "stringliteral": + return CodecUtils.Conversion.toBytes(definition.hexValue); + default: + //unfortunately, other types of constants are just too complicated to + //handle right now. sorry. + debug("unsupported constant definition type"); + throw new Errors.DecodingError( + { + kind: "UnsupportedConstantError", + definition + } + ); + } +} diff --git a/packages/truffle-codec/lib/read/index.ts b/packages/truffle-codec/lib/read/index.ts new file mode 100644 index 00000000000..f3f247589f8 --- /dev/null +++ b/packages/truffle-codec/lib/read/index.ts @@ -0,0 +1,46 @@ +import * as storage from "./storage"; +import * as bytes from "./bytes"; +import * as stack from "./stack"; +import * as constant from "./constant"; +import * as Pointer from "../types/pointer"; +import { EvmState } from "../types/evm"; +import { DecoderRequest } from "../types/request"; +import { Errors } from "truffle-codec-utils"; + +export default function* read(pointer: Pointer.DataPointer, state: EvmState): IterableIterator { + switch(pointer.location) { + + case "stack": + return stack.readStack(state.stack, pointer.from, pointer.to); + + case "storage": + return yield* storage.readRange(state.storage, pointer.range); + + case "memory": + return bytes.readBytes(state.memory, pointer.start, pointer.length); + + case "calldata": + return bytes.readBytes(state.calldata, pointer.start, pointer.length); + + case "eventdata": + //similarly with eventdata + return bytes.readBytes(state.eventdata, pointer.start, pointer.length); + + case "stackliteral": + //nothing to do, just return it + return pointer.literal; + + case "definition": + return constant.readDefinition(pointer.definition); + + case "special": + //this one is simple enough to inline + //not bothering with error handling on this one as I don't expect errors + return state.specials[pointer.special]; + + case "eventtopic": + //this one is simple enough to inline as well; similarly not bothering + //with error handling + return state.eventtopics[pointer.topic]; + } +} diff --git a/packages/truffle-decoder-core/lib/read/stack.ts b/packages/truffle-codec/lib/read/stack.ts similarity index 67% rename from packages/truffle-decoder-core/lib/read/stack.ts rename to packages/truffle-codec/lib/read/stack.ts index f49ef19294d..164c130fe98 100644 --- a/packages/truffle-decoder-core/lib/read/stack.ts +++ b/packages/truffle-codec/lib/read/stack.ts @@ -1,12 +1,16 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:read:stack"); +const debug = debugModule("codec:read:stack"); -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; export function readStack(stack: Uint8Array[], from: number, to: number): Uint8Array { if(from < 0 || to >= stack.length) { - throw new DecodeUtils.Errors.DecodingError( - new DecodeUtils.Errors.ReadErrorStack(from, to) + throw new CodecUtils.Errors.DecodingError( + { + kind: "ReadErrorStack", + from, + to + } ); } //unforunately, Uint8Arrays don't support concat; if they did the rest of @@ -14,11 +18,11 @@ export function readStack(stack: Uint8Array[], from: number, to: number): Uint8A //but they don't support that either. But neither of those are the case, so //we'll have to concatenate a bit more manually. let words = stack.slice(from, to + 1); - let result = new Uint8Array(words.length * DecodeUtils.EVM.WORD_SIZE); + let result = new Uint8Array(words.length * CodecUtils.EVM.WORD_SIZE); //shouldn't we total up the lengths? yeah, but each one should have a //length of 32, so unless somehting's gone wrong we can just multiply for(let index = 0; index < words.length; index++) { - result.set(words[index], index * DecodeUtils.EVM.WORD_SIZE); + result.set(words[index], index * CodecUtils.EVM.WORD_SIZE); } return result; } diff --git a/packages/truffle-decoder-core/lib/read/storage.ts b/packages/truffle-codec/lib/read/storage.ts similarity index 73% rename from packages/truffle-decoder-core/lib/read/storage.ts rename to packages/truffle-codec/lib/read/storage.ts index 5172255cb58..9dc102f0545 100644 --- a/packages/truffle-decoder-core/lib/read/storage.ts +++ b/packages/truffle-codec/lib/read/storage.ts @@ -1,10 +1,11 @@ import debugModule from "debug"; -const debug = debugModule("decoder-core:read:storage"); +const debug = debugModule("codec:read:storage"); -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import { Slot, Range } from "../types/storage"; import { WordMapping } from "../types/evm"; import { DecoderRequest } from "../types/request"; +import { mappingKeyAsHex, keyInfoForPrinting } from "../encode/key"; import BN from "bn.js"; /** @@ -18,11 +19,11 @@ import BN from "bn.js"; export function slotAddress(slot: Slot): BN { if (slot.key !== undefined && slot.path !== undefined) { // mapping reference - return DecodeUtils.EVM.keccak256(slot.key.toSoliditySha3Input(), slotAddress(slot.path)).add(slot.offset); + return CodecUtils.EVM.keccak256(mappingKeyAsHex(slot.key), slotAddress(slot.path)).add(slot.offset); } else if (slot.path !== undefined) { const pathAddress = slotAddress(slot.path); - const path: BN = slot.hashPath ? DecodeUtils.EVM.keccak256(pathAddress) : pathAddress; + const path: BN = slot.hashPath ? CodecUtils.EVM.keccak256(pathAddress) : pathAddress; return path.add(slot.offset); } else { @@ -33,8 +34,8 @@ export function slotAddress(slot: Slot): BN { export function slotAddressPrintout(slot: Slot): string { if (slot.key !== undefined && slot.path !== undefined) { // mapping reference - let {type: keyEncoding} = slot.key.toSoliditySha3Input(); - return "keccak(" + slot.key.toString() + " as " + keyEncoding + ", " + slotAddressPrintout(slot.path) + ") + " + slot.offset.toString(); + let {type: keyEncoding, value: keyValue} = keyInfoForPrinting(slot.key); + return "keccak(" + keyValue + " as " + keyEncoding + ", " + slotAddressPrintout(slot.path) + ") + " + slot.offset.toString(); } else if (slot.path !== undefined) { const pathAddressPrintout = slotAddressPrintout(slot.path); @@ -57,9 +58,9 @@ export function* read(storage: WordMapping, slot: Slot): IterableIterator { // don't indent first line const padding = i > 0 ? Array(indent).join(" ") : ""; return padding + line; diff --git a/packages/truffle-debugger/lib/data/actions/index.js b/packages/truffle-debugger/lib/data/actions/index.js index 1cf21f07596..bcec130a0bb 100644 --- a/packages/truffle-debugger/lib/data/actions/index.js +++ b/packages/truffle-debugger/lib/data/actions/index.js @@ -57,11 +57,11 @@ export function defineType(node) { } export const ALLOCATE = "ALLOCATE"; -export function allocate(storage, memory, calldata) { +export function allocate(storage, memory, abi) { return { type: ALLOCATE, storage, memory, - calldata + abi }; } diff --git a/packages/truffle-debugger/lib/data/reducers.js b/packages/truffle-debugger/lib/data/reducers.js index 800dcd5ee3b..8f167d186b5 100644 --- a/packages/truffle-debugger/lib/data/reducers.js +++ b/packages/truffle-debugger/lib/data/reducers.js @@ -5,9 +5,9 @@ import { combineReducers } from "redux"; import * as actions from "./actions"; -import { slotAddress } from "truffle-decoder-core"; +import { slotAddress } from "truffle-codec"; import { makeAssignment } from "lib/helpers"; -import { Conversion, Definition, EVM } from "truffle-decode-utils"; +import { Conversion, Definition, EVM } from "truffle-codec-utils"; const DEFAULT_SCOPES = { byId: {} @@ -80,7 +80,7 @@ function userDefinedTypes(state = [], action) { const DEFAULT_ALLOCATIONS = { storage: {}, memory: {}, - calldata: {} + abi: {} }; function allocations(state = DEFAULT_ALLOCATIONS, action) { @@ -88,7 +88,7 @@ function allocations(state = DEFAULT_ALLOCATIONS, action) { return { storage: action.storage, memory: action.memory, - calldata: action.calldata + abi: action.abi }; } else { return state; @@ -102,11 +102,11 @@ const info = combineReducers({ }); const GLOBAL_ASSIGNMENTS = [ - [{ builtin: "msg" }, { special: "msg" }], - [{ builtin: "tx" }, { special: "tx" }], - [{ builtin: "block" }, { special: "block" }], - [{ builtin: "this" }, { special: "this" }], - [{ builtin: "now" }, { special: "timestamp" }] //we don't have an alias "now" + [{ builtin: "msg" }, { location: "special", special: "msg" }], + [{ builtin: "tx" }, { location: "special", special: "tx" }], + [{ builtin: "block" }, { location: "special", special: "block" }], + [{ builtin: "this" }, { location: "special", special: "this" }], + [{ builtin: "now" }, { location: "special", special: "timestamp" }] //we don't have an alias "now" ].map(([idObj, ref]) => makeAssignment(idObj, ref)); const DEFAULT_ASSIGNMENTS = { diff --git a/packages/truffle-debugger/lib/data/sagas/index.js b/packages/truffle-debugger/lib/data/sagas/index.js index cb65be970a0..cc01e2f0ed3 100644 --- a/packages/truffle-debugger/lib/data/sagas/index.js +++ b/packages/truffle-debugger/lib/data/sagas/index.js @@ -15,15 +15,15 @@ import data from "../selectors"; import sum from "lodash.sum"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import { getStorageAllocations, getMemoryAllocations, - getCalldataAllocations, + getAbiAllocations, readStack, storageSize, - forEvmState -} from "truffle-decoder-core"; + decodeVariable +} from "truffle-codec"; import BN from "bn.js"; export function* scope(nodeId, pointer, parentId, sourceId) { @@ -39,10 +39,7 @@ export function* defineType(node) { } function* tickSaga() { - debug("got TICK"); - yield* variablesAndMappingsSaga(); - debug("about to SUBTOCK"); yield* trace.signalTickSagaCompletion(); } @@ -63,8 +60,7 @@ export function* decode(definition, ref, options = DEFAULT_DECODE_OPTIONS) { ); let blockNumber = yield select(data.views.blockNumber); - let ZERO_WORD = new Uint8Array(DecodeUtils.EVM.WORD_SIZE); - ZERO_WORD.fill(0); + let ZERO_WORD = new Uint8Array(CodecUtils.EVM.WORD_SIZE); //automatically filled with zeroes let NO_CODE = new Uint8Array(); //empty array if (options.forceNonPayable) { @@ -76,20 +72,20 @@ export function* decode(definition, ref, options = DEFAULT_DECODE_OPTIONS) { currentContext = { ...currentContext, compiler: null }; } - let decoder = forEvmState(definition, ref, { + let decoder = decodeVariable(definition, ref, { userDefinedTypes, state, mappingKeys, - storageAllocations: allocations.storage, - memoryAllocations: allocations.memory, - calldataAllocations: allocations.calldata, + allocations, contexts, currentContext, internalFunctionsTable }); + debug("beginning decoding"); let result = decoder.next(); while (!result.done) { + debug("request received"); let request = result.value; let response; switch (request.type) { @@ -102,7 +98,7 @@ export function* decode(definition, ref, options = DEFAULT_DECODE_OPTIONS) { let address = request.address; if (address in instances) { response = instances[address]; - } else if (address === DecodeUtils.EVM.ZERO_ADDRESS) { + } else if (address === CodecUtils.EVM.ZERO_ADDRESS) { //HACK: to avoid displaying the zero address to the user as an //affected address just because they decoded a contract or external //function variable that hadn't been initialized yet, we give the @@ -115,15 +111,18 @@ export function* decode(definition, ref, options = DEFAULT_DECODE_OPTIONS) { let binary = (yield* web3.obtainBinaries([address], blockNumber))[0]; debug("adding instance"); yield* evm.addInstance(address, binary); - response = DecodeUtils.Conversion.toBytes(binary); + response = CodecUtils.Conversion.toBytes(binary); } break; default: debug("unrecognized request type!"); } + debug("sending response"); result = decoder.next(response); } //at this point, result.value holds the final value + debug("done decoding"); + debug("decoded value: %O", result.value); return result.value; } @@ -191,7 +190,7 @@ function* variablesAndMappingsSaga() { //now: look at the parameters *after* the current index. we'll need to //adjust for those. let parametersLeft = parameters.slice(currentIndex + 1); - let adjustment = sum(parametersLeft.map(DecodeUtils.Definition.stackSize)); + let adjustment = sum(parametersLeft.map(CodecUtils.Definition.stackSize)); debug("adjustment %d", adjustment); preambleAssignments = assignParameters( parameters, @@ -301,10 +300,9 @@ function* variablesAndMappingsSaga() { assignment = makeAssignment( { astId: varId, stackframe: currentDepth }, { - stack: { - from: top - DecodeUtils.Definition.stackSize(node) + 1, - to: top - } + location: "stack", + from: top - CodecUtils.Definition.stackSize(node) + 1, + to: top } ); assignments = { [assignment.id]: assignment }; @@ -339,24 +337,24 @@ function* variablesAndMappingsSaga() { //(note: we write it this way because mappings aren't caught by //isReference) if ( - DecodeUtils.Definition.typeClass(baseExpression) === "bytes" || - (DecodeUtils.Definition.typeClass(baseExpression) === "array" && - (DecodeUtils.Definition.isReference(node) - ? DecodeUtils.Definition.referenceType(baseExpression) !== "storage" - : !DecodeUtils.Definition.isMapping(node))) + CodecUtils.Definition.typeClass(baseExpression) === "bytes" || + (CodecUtils.Definition.typeClass(baseExpression) === "array" && + (CodecUtils.Definition.isReference(node) + ? CodecUtils.Definition.referenceType(baseExpression) !== "storage" + : !CodecUtils.Definition.isMapping(node))) ) { debug("Index case bailed out early"); - debug("typeClass %s", DecodeUtils.Definition.typeClass(baseExpression)); + debug("typeClass %s", CodecUtils.Definition.typeClass(baseExpression)); debug( "referenceType %s", - DecodeUtils.Definition.referenceType(baseExpression) + CodecUtils.Definition.referenceType(baseExpression) ); - debug("isReference(node) %o", DecodeUtils.Definition.isReference(node)); + debug("isReference(node) %o", CodecUtils.Definition.isReference(node)); yield put(actions.assign(assignments)); break; } - let keyDefinition = DecodeUtils.Definition.keyDefinition( + let keyDefinition = CodecUtils.Definition.keyDefinition( baseExpression, scopes ); @@ -378,7 +376,7 @@ function* variablesAndMappingsSaga() { //OK, not an actual path -- we're just going to use a simple offset for //the path. But that's OK, because the mappedPaths reducer will turn //it into an actual path. - if (indexValue !== null && indexValue.value) { + if (indexValue != null && indexValue.value) { path = fetchBasePath( baseExpression, mappedPaths, @@ -390,9 +388,9 @@ function* variablesAndMappingsSaga() { //we need to do things differently depending on whether we're dealing //with an array or mapping - switch (DecodeUtils.Definition.typeClass(baseExpression)) { + switch (CodecUtils.Definition.typeClass(baseExpression)) { case "array": - slot.hashPath = DecodeUtils.Definition.isDynamicArray( + slot.hashPath = CodecUtils.Definition.isDynamicArray( baseExpression ); slot.offset = indexValue.value.asBN.muln( @@ -416,8 +414,8 @@ function* variablesAndMappingsSaga() { address, slot, assignments, - DecodeUtils.Definition.typeIdentifier(node), - DecodeUtils.Definition.typeIdentifier(baseExpression) + CodecUtils.Definition.typeIdentifier(node), + CodecUtils.Definition.typeIdentifier(baseExpression) ) ); } else { @@ -448,10 +446,10 @@ function* variablesAndMappingsSaga() { //we'll just do the assignment and quit out (again, note that mappings //aren't caught by isReference) if ( - DecodeUtils.Definition.typeClass(baseExpression) !== "struct" || - (DecodeUtils.Definition.isReference(node) - ? DecodeUtils.Definition.referenceType(baseExpression) !== "storage" - : !DecodeUtils.Definition.isMapping(node)) + CodecUtils.Definition.typeClass(baseExpression) !== "struct" || + (CodecUtils.Definition.isReference(node) + ? CodecUtils.Definition.referenceType(baseExpression) !== "storage" + : !CodecUtils.Definition.isMapping(node)) ) { debug("Member case bailed out early"); yield put(actions.assign(assignments)); @@ -468,11 +466,11 @@ function* variablesAndMappingsSaga() { slot = { path }; - let structId = DecodeUtils.Definition.typeId(baseExpression); + let structId = CodecUtils.Definition.typeId(baseExpression); let memberAllocation = allocations[structId].members[node.referencedDeclaration]; - slot.offset = memberAllocation.pointer.storage.from.slot.offset.clone(); + slot.offset = memberAllocation.pointer.range.from.slot.offset.clone(); debug("slot %o", slot); yield put( @@ -480,8 +478,8 @@ function* variablesAndMappingsSaga() { address, slot, assignments, - DecodeUtils.Definition.typeIdentifier(node), - DecodeUtils.Definition.typeIdentifier(baseExpression) + CodecUtils.Definition.typeIdentifier(node), + CodecUtils.Definition.typeIdentifier(baseExpression) ) ); @@ -504,6 +502,13 @@ function* variablesAndMappingsSaga() { } function* decodeMappingKeySaga(indexDefinition, keyDefinition) { + //something of a HACK -- cleans any out-of-range booleans + //resulting from the main mapping key decoding loop + let indexValue = yield* decodeMappingKeyCore(indexDefinition, keyDefinition); + return indexValue ? CodecUtils.Conversion.cleanBool(indexValue) : indexValue; +} + +function* decodeMappingKeyCore(indexDefinition, keyDefinition) { let scopes = yield select(data.views.scopes.inlined); let currentAssignments = yield select(data.proc.assignments); let currentDepth = yield select(data.current.functionDepth); @@ -518,7 +523,7 @@ function* decodeMappingKeySaga(indexDefinition, keyDefinition) { const indexReference = (currentAssignments.byId[fullIndexId] || {}).ref; - if (DecodeUtils.Definition.isSimpleConstant(indexDefinition)) { + if (CodecUtils.Definition.isSimpleConstant(indexDefinition)) { //while the main case is the next one, where we look for a prior //assignment, we need this case (and need it first) for two reasons: //1. some constant expressions (specifically, string and hex literals) @@ -528,9 +533,11 @@ function* decodeMappingKeySaga(indexDefinition, keyDefinition) { //so looking for a prior assignment will read the wrong value. //so instead it's preferable to use the constant directly. debug("about to decode simple literal"); - return yield* decode(keyDefinition, { - definition: indexDefinition - }); + return yield* decode( + keyDefinition, + { location: "definition", definition: indexDefinition }, + { forceNonPayable: true } + ); } else if (indexReference) { //if a prior assignment is found let splicedDefinition; @@ -538,10 +545,10 @@ function* decodeMappingKeySaga(indexDefinition, keyDefinition) { //definition. however, the key definition may have the wrong location //on it. so, when applicable, we splice the index definition location //onto the key definition location. - if (DecodeUtils.Definition.isReference(indexDefinition)) { - splicedDefinition = DecodeUtils.Definition.spliceLocation( + if (CodecUtils.Definition.isReference(indexDefinition)) { + splicedDefinition = CodecUtils.Definition.spliceLocation( keyDefinition, - DecodeUtils.Definition.referenceType(indexDefinition) + CodecUtils.Definition.referenceType(indexDefinition) ); //we could put code here to add on the "_ptr" ending when absent, //but we presently ignore that ending, so we'll skip that @@ -549,7 +556,9 @@ function* decodeMappingKeySaga(indexDefinition, keyDefinition) { splicedDefinition = keyDefinition; } debug("about to decode"); - return yield* decode(splicedDefinition, indexReference); + return yield* decode(splicedDefinition, indexReference, { + forceNonPayable: true + }); } else if ( indexDefinition.referencedDeclaration && scopes[indexDefinition.referencedDeclaration] @@ -565,11 +574,16 @@ function* decodeMappingKeySaga(indexDefinition, keyDefinition) { if (indexConstantDeclaration.constant) { let indexConstantDefinition = indexConstantDeclaration.value; //next line filters out constants we don't know how to handle - if (DecodeUtils.Definition.isSimpleConstant(indexConstantDefinition)) { + if (CodecUtils.Definition.isSimpleConstant(indexConstantDefinition)) { debug("about to decode simple constant"); - return yield* decode(keyDefinition, { - definition: indexConstantDeclaration.value - }); + return yield* decode( + keyDefinition, + { + location: "definition", + definition: indexConstantDeclaration.value + }, + { forceNonPayable: true } + ); } else { return null; //can't decode; see below for more explanation } @@ -627,7 +641,7 @@ export function* recordAllocations() { ); debug("storageAllocations %O", storageAllocations); const memoryAllocations = getMemoryAllocations(referenceDeclarations); - const calldataAllocations = getCalldataAllocations(referenceDeclarations); + const calldataAllocations = getAbiAllocations(referenceDeclarations); yield put( actions.allocate(storageAllocations, memoryAllocations, calldataAllocations) ); @@ -640,7 +654,7 @@ function literalAssignments(node, stack, currentDepth) { try { literal = readStack( stack, - top - DecodeUtils.Definition.stackSize(node) + 1, + top - CodecUtils.Definition.stackSize(node) + 1, top ); } catch (error) { @@ -650,7 +664,7 @@ function literalAssignments(node, stack, currentDepth) { let assignment = makeAssignment( { astId: node.id, stackframe: currentDepth }, - { literal } + { location: "stackliteral", literal } ); return { [assignment.id]: assignment }; @@ -666,12 +680,11 @@ function assignParameters(parameters, top, functionDepth) { let assignments = {}; for (let parameter of reverseParameters) { - let words = DecodeUtils.Definition.stackSize(parameter); + let words = CodecUtils.Definition.stackSize(parameter); let pointer = { - stack: { - from: currentPosition - words + 1, - to: currentPosition - } + location: "stack", + from: currentPosition - words + 1, + to: currentPosition }; let assignment = makeAssignment( { astId: parameter.id, stackframe: functionDepth }, @@ -699,7 +712,7 @@ function fetchBasePath( debug("currentAssignments: %O", currentAssignments); //base expression is an expression, and so has a literal assigned to //it - let offset = DecodeUtils.Conversion.toBN( + let offset = CodecUtils.Conversion.toBN( currentAssignments.byId[fullId].ref.literal ); return { offset }; diff --git a/packages/truffle-debugger/lib/data/selectors/index.js b/packages/truffle-debugger/lib/data/selectors/index.js index 3f15689a3de..458d2b200f5 100644 --- a/packages/truffle-debugger/lib/data/selectors/index.js +++ b/packages/truffle-debugger/lib/data/selectors/index.js @@ -9,7 +9,7 @@ import { stableKeccak256 } from "lib/helpers"; import evm from "lib/evm/selectors"; import solidity from "lib/solidity/selectors"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; /** * @private @@ -50,7 +50,7 @@ function modifierForInvocation(invocation, scopes) { return rawNode.nodes.find( node => node.nodeType === "FunctionDefinition" && - DecodeUtils.Definition.functionKind(node) === "constructor" + CodecUtils.Definition.functionKind(node) === "constructor" ); default: //we should never hit this case @@ -76,7 +76,7 @@ function debuggerContextToDecoderContext(context) { contractId, contractKind, isConstructor, - abi: DecodeUtils.Contexts.abiToFunctionAbiWithSignatures(abi), + abi: CodecUtils.AbiUtils.computeSelectors(abi), payable, compiler }; @@ -150,16 +150,14 @@ const data = createSelectorTree({ userDefinedTypes: { //user-defined types for passing to the decoder _: createLeaf( - ["../referenceDeclarations", "../scopes/inlined", "../contexts"], - (referenceDeclarations, scopes, contexts) => { - const types = ["ContractDefinition", "SourceUnit"]; - //SourceUnit included as fallback + ["../referenceDeclarations", "/info/scopes", solidity.info.sources], + (referenceDeclarations, scopes, sources) => { return Object.assign( {}, ...Object.entries(referenceDeclarations).map(([id, node]) => ({ - [id]: DecodeUtils.Types.definitionToStoredType( + [id]: CodecUtils.Types.definitionToStoredType( node, - contexts[findAncestorOfType(node, types, scopes).id].compiler, + sources[scopes[node.id].sourceId].compiler, referenceDeclarations ) })) @@ -224,7 +222,7 @@ const data = createSelectorTree({ Object.assign( {}, ...Object.entries(instances).map(([address, { binary }]) => ({ - [address]: DecodeUtils.Conversion.toBytes(binary) + [address]: CodecUtils.Conversion.toBytes(binary) })) ) ), @@ -235,10 +233,9 @@ const data = createSelectorTree({ * 0. we only include non-constructor contexts * 1. we now index by contract ID rather than hash * 2. we strip out context, sourceMap, and primarySource - * 3. we alter abi in several ways: - * 3a. we strip abi down to just (ordinary) functions - * 3b. we augment these functions with signatures (here meaning selectors) - * 3c. abi is now an object, not an array, and indexed by these signatures + * 3. we alter abi in two ways: + * 3a. we strip out everything but functions + * 3b. abi is now an object, not an array, and indexed by these signatures */ contexts: createLeaf([evm.info.contexts], contexts => Object.assign( @@ -302,7 +299,7 @@ const data = createSelectorTree({ let definition = inlined[variable.id].definition; return ( !definition.constant || - DecodeUtils.Definition.isSimpleConstant(definition.value) + CodecUtils.Definition.isSimpleConstant(definition.value) ); }); @@ -361,9 +358,9 @@ const data = createSelectorTree({ memory: createLeaf(["/state"], state => state.info.allocations.memory), /* - * data.info.allocations.calldata + * data.info.allocations.abi */ - calldata: createLeaf(["/state"], state => state.info.allocations.calldata) + abi: createLeaf(["/state"], state => state.info.allocations.abi) }, /** @@ -419,7 +416,7 @@ const data = createSelectorTree({ stack: createLeaf( [evm.current.state.stack], - words => (words || []).map(word => DecodeUtils.Conversion.toBytes(word)) + words => (words || []).map(word => CodecUtils.Conversion.toBytes(word)) ), /** @@ -428,7 +425,7 @@ const data = createSelectorTree({ memory: createLeaf( [evm.current.state.memory], - words => DecodeUtils.Conversion.toBytes(words.join("")) + words => CodecUtils.Conversion.toBytes(words.join("")) ), /** @@ -437,7 +434,7 @@ const data = createSelectorTree({ calldata: createLeaf( [evm.current.call], - ({ data }) => DecodeUtils.Conversion.toBytes(data) + ({ data }) => CodecUtils.Conversion.toBytes(data) ), /** @@ -450,7 +447,7 @@ const data = createSelectorTree({ Object.assign( {}, ...Object.entries(mapping).map(([address, word]) => ({ - [`0x${address}`]: DecodeUtils.Conversion.toBytes(word) + [`0x${address}`]: CodecUtils.Conversion.toBytes(word) })) ) ), @@ -464,24 +461,24 @@ const data = createSelectorTree({ specials: createLeaf( ["/current/address", evm.current.call, evm.transaction.globals], (address, { sender, value }, { tx, block }) => ({ - this: DecodeUtils.Conversion.toBytes(address), + this: CodecUtils.Conversion.toBytes(address), - sender: DecodeUtils.Conversion.toBytes(sender), + sender: CodecUtils.Conversion.toBytes(sender), - value: DecodeUtils.Conversion.toBytes(value), + value: CodecUtils.Conversion.toBytes(value), //let's crack open that tx and block! ...Object.assign( {}, ...Object.entries(tx).map(([variable, value]) => ({ - [variable]: DecodeUtils.Conversion.toBytes(value) + [variable]: CodecUtils.Conversion.toBytes(value) })) ), ...Object.assign( {}, ...Object.entries(block).map(([variable, value]) => ({ - [variable]: DecodeUtils.Conversion.toBytes(value) + [variable]: CodecUtils.Conversion.toBytes(value) })) ) }) @@ -744,10 +741,10 @@ const data = createSelectorTree({ }) ); let builtins = { - msg: DecodeUtils.Definition.MSG_DEFINITION, - tx: DecodeUtils.Definition.TX_DEFINITION, - block: DecodeUtils.Definition.BLOCK_DEFINITION, - now: DecodeUtils.Definition.NOW_DEFINITION + msg: CodecUtils.Definition.MSG_DEFINITION, + tx: CodecUtils.Definition.TX_DEFINITION, + block: CodecUtils.Definition.BLOCK_DEFINITION, + now: CodecUtils.Definition.NOW_DEFINITION }; //only include this when it has a proper definition if (thisDefinition) { @@ -766,7 +763,7 @@ const data = createSelectorTree({ ["/current/contract"], contractNode => contractNode && contractNode.nodeType === "ContractDefinition" - ? DecodeUtils.Definition.spoofThisDefinition( + ? CodecUtils.Definition.spoofThisDefinition( contractNode.name, contractNode.id, contractNode.contractKind @@ -862,7 +859,7 @@ const data = createSelectorTree({ stack: createLeaf( [evm.next.state.stack], - words => (words || []).map(word => DecodeUtils.Conversion.toBytes(word)) + words => (words || []).map(word => CodecUtils.Conversion.toBytes(word)) ) }, @@ -939,7 +936,7 @@ const data = createSelectorTree({ step => ((step || {}).stack || []).map(word => - DecodeUtils.Conversion.toBytes(word) + CodecUtils.Conversion.toBytes(word) ) ) } diff --git a/packages/truffle-debugger/lib/evm/reducers.js b/packages/truffle-debugger/lib/evm/reducers.js index 0e39dbfd5ce..75ebb415a64 100644 --- a/packages/truffle-debugger/lib/evm/reducers.js +++ b/packages/truffle-debugger/lib/evm/reducers.js @@ -5,7 +5,7 @@ import { combineReducers } from "redux"; import * as actions from "./actions"; import { keccak256, extractPrimarySource } from "lib/helpers"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import BN from "bn.js"; @@ -54,14 +54,14 @@ function contexts(state = DEFAULT_CONTEXTS, action) { contractId, contractKind, isConstructor, - payable: DecodeUtils.Contexts.abiHasPayableFallback(abi) + payable: CodecUtils.AbiUtils.abiHasPayableFallback(abi) } } }; case actions.NORMALIZE_CONTEXTS: return { - byContext: DecodeUtils.Contexts.normalizeContexts(state.byContext) + byContext: CodecUtils.Contexts.normalizeContexts(state.byContext) }; /* @@ -78,7 +78,7 @@ const info = combineReducers({ const DEFAULT_TX = { gasprice: new BN(0), - origin: DecodeUtils.EVM.ZERO_ADDRESS + origin: CodecUtils.EVM.ZERO_ADDRESS }; function tx(state = DEFAULT_TX, action) { @@ -94,7 +94,7 @@ function tx(state = DEFAULT_TX, action) { } const DEFAULT_BLOCK = { - coinbase: DecodeUtils.EVM.ZERO_ADDRESS, + coinbase: CodecUtils.EVM.ZERO_ADDRESS, difficulty: new BN(0), gaslimit: new BN(0), number: new BN(0), @@ -252,7 +252,7 @@ function codex(state = DEFAULT_CODEX, action) { //now, do we need to add a new address to this stackframe? if ( topCodex.accounts[action.storageAddress] !== undefined || - action.storageAddress === DecodeUtils.EVM.ZERO_ADDRESS + action.storageAddress === CodecUtils.EVM.ZERO_ADDRESS ) { //if we don't return newState; @@ -276,7 +276,7 @@ function codex(state = DEFAULT_CODEX, action) { //on a store, the relevant page should already exist, so we can just //add or update the needed slot const { address, slot, value } = action; - if (address === DecodeUtils.EVM.ZERO_ADDRESS) { + if (address === CodecUtils.EVM.ZERO_ADDRESS) { //as always, we do not maintain a zero page return state; } @@ -296,7 +296,7 @@ function codex(state = DEFAULT_CODEX, action) { //it's an external load (there was nothing already there), then we want //to update *every* stackframe const { address, slot, value } = action; - if (address === DecodeUtils.EVM.ZERO_ADDRESS) { + if (address === CodecUtils.EVM.ZERO_ADDRESS) { //as always, we do not maintain a zero page return state; } diff --git a/packages/truffle-debugger/lib/evm/selectors/index.js b/packages/truffle-debugger/lib/evm/selectors/index.js index d92fec50228..2c60e0b958d 100644 --- a/packages/truffle-debugger/lib/evm/selectors/index.js +++ b/packages/truffle-debugger/lib/evm/selectors/index.js @@ -6,7 +6,7 @@ import BN from "bn.js"; import trace from "lib/trace/selectors"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import { isCallMnemonic, isCreateMnemonic, @@ -150,7 +150,7 @@ function createStepSelectors(step, state = null) { } let address = stack[stack.length - 2]; - return DecodeUtils.Conversion.toAddress(address); + return CodecUtils.Conversion.toAddress(address); } ), @@ -220,7 +220,7 @@ function createStepSelectors(step, state = null) { //otherwise, for CALL and CALLCODE, it's the 3rd argument let value = stack[stack.length - 3]; - return DecodeUtils.Conversion.toBN(value); + return CodecUtils.Conversion.toBN(value); } ), @@ -236,7 +236,7 @@ function createStepSelectors(step, state = null) { //creates have the value as the first argument let value = stack[stack.length - 1]; - return DecodeUtils.Conversion.toBN(value); + return CodecUtils.Conversion.toBN(value); }), /** @@ -316,7 +316,7 @@ const evm = createSelectorTree({ * (returns null on no match) */ search: createLeaf(["/info/contexts"], contexts => binary => - DecodeUtils.Contexts.findDebuggerContext(contexts, binary) + CodecUtils.Contexts.findDebuggerContext(contexts, binary) ) } }, @@ -446,7 +446,7 @@ const evm = createSelectorTree({ return null; } let address = stack[stack.length - 1]; - return DecodeUtils.Conversion.toAddress(address); + return CodecUtils.Conversion.toAddress(address); } ), @@ -508,7 +508,7 @@ const evm = createSelectorTree({ if (remaining <= 1) { return finalStatus; } else { - const ZERO_WORD = "00".repeat(DecodeUtils.EVM.WORD_SIZE); + const ZERO_WORD = "00".repeat(CodecUtils.EVM.WORD_SIZE); return stack[stack.length - 1] !== ZERO_WORD; } } @@ -535,7 +535,7 @@ const evm = createSelectorTree({ storage: createLeaf( ["./_", "../state/storage", "../call"], (codex, rawStorage, { storageAddress }) => - storageAddress === DecodeUtils.EVM.ZERO_ADDRESS + storageAddress === CodecUtils.EVM.ZERO_ADDRESS ? rawStorage //HACK -- if zero address ignore the codex : codex[codex.length - 1].accounts[storageAddress].storage ), diff --git a/packages/truffle-debugger/lib/helpers/index.js b/packages/truffle-debugger/lib/helpers/index.js index ceee76cbcdf..94f6217942a 100644 --- a/packages/truffle-debugger/lib/helpers/index.js +++ b/packages/truffle-debugger/lib/helpers/index.js @@ -1,4 +1,4 @@ -import * as utils from "truffle-decode-utils"; +import * as utils from "truffle-codec-utils"; const stringify = require("json-stable-stringify"); diff --git a/packages/truffle-debugger/lib/session/index.js b/packages/truffle-debugger/lib/session/index.js index e0ad2f35372..e7ac2432379 100644 --- a/packages/truffle-debugger/lib/session/index.js +++ b/packages/truffle-debugger/lib/session/index.js @@ -1,6 +1,8 @@ import debugModule from "debug"; const debug = debugModule("debugger:session"); +import { AbiUtils } from "truffle-codec-utils"; + import configureStore from "lib/store"; import * as controller from "lib/controller/actions"; @@ -129,6 +131,7 @@ export default class Session { let contractId = contractNode.id; let contractKind = contractNode.contractKind; + abi = AbiUtils.schemaAbiToAbi(abi); //let's handle this up front debug("contractName %s", contractName); debug("sourceMap %o", sourceMap); diff --git a/packages/truffle-debugger/lib/solidity/selectors/index.js b/packages/truffle-debugger/lib/solidity/selectors/index.js index bbd98b1840f..52374f07631 100644 --- a/packages/truffle-debugger/lib/solidity/selectors/index.js +++ b/packages/truffle-debugger/lib/solidity/selectors/index.js @@ -4,6 +4,7 @@ const debug = debugModule("debugger:solidity:selectors"); import { createSelectorTree, createLeaf } from "reselect-tree"; import SolidityUtils from "truffle-solidity-utils"; import CodeUtils from "truffle-code-utils"; +import { Definition as DefinitionUtils } from "truffle-codec-utils"; import { findRange } from "lib/ast/map"; import jsonpointer from "json-pointer"; @@ -323,6 +324,7 @@ let solidity = createSelectorTree({ node, name: node.name, id: node.id, + mutability: DefinitionUtils.mutability(node), contractPointer, contractNode, contractName: contractNode.name, diff --git a/packages/truffle-debugger/lib/trace/sagas/index.js b/packages/truffle-debugger/lib/trace/sagas/index.js index fe8bca6fe2d..160648befa2 100644 --- a/packages/truffle-debugger/lib/trace/sagas/index.js +++ b/packages/truffle-debugger/lib/trace/sagas/index.js @@ -4,7 +4,7 @@ const debug = debugModule("debugger:trace:sagas"); import { take, takeEvery, put, select } from "redux-saga/effects"; import { prefixName, isCallMnemonic } from "lib/helpers"; -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import * as actions from "../actions"; @@ -70,7 +70,7 @@ export function* processTrace(steps) { ({ op, stack }) => isCallMnemonic(op) ? //if it's a call, just fetch the address off the stack - DecodeUtils.Conversion.toAddress(stack[stack.length - 2]) + CodecUtils.Conversion.toAddress(stack[stack.length - 2]) : //if it's not a call, just return undefined (we've gone back to //skipping creates) undefined @@ -78,7 +78,7 @@ export function* processTrace(steps) { //filter out zero addresses from failed creates (as well as undefineds) .filter( address => - address !== undefined && address !== DecodeUtils.EVM.ZERO_ADDRESS + address !== undefined && address !== CodecUtils.EVM.ZERO_ADDRESS ) ) ]; diff --git a/packages/truffle-debugger/lib/web3/sagas/index.js b/packages/truffle-debugger/lib/web3/sagas/index.js index 05dbfd99faf..7abfd02e38d 100644 --- a/packages/truffle-debugger/lib/web3/sagas/index.js +++ b/packages/truffle-debugger/lib/web3/sagas/index.js @@ -17,7 +17,7 @@ import * as session from "lib/session/actions"; import BN from "bn.js"; import Web3 from "web3"; //just for utils! -import * as DecodeUtils from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; import Web3Adapter from "../adapter"; @@ -71,7 +71,7 @@ function* fetchTransactionInfo(adapter, { txHash }) { } else { let storageAddress = Web3.utils.isAddress(receipt.contractAddress) ? receipt.contractAddress - : DecodeUtils.EVM.ZERO_ADDRESS; + : CodecUtils.EVM.ZERO_ADDRESS; yield put( actions.receiveCall({ binary: tx.input, diff --git a/packages/truffle-debugger/package.json b/packages/truffle-debugger/package.json index 553dbdf270f..c702f959c67 100644 --- a/packages/truffle-debugger/package.json +++ b/packages/truffle-debugger/package.json @@ -30,8 +30,8 @@ "remote-redux-devtools": "^0.5.12", "reselect-tree": "^1.3.1", "truffle-code-utils": "^1.2.4", - "truffle-decode-utils": "^1.0.16", - "truffle-decoder-core": "^3.0.9", + "truffle-codec-utils": "^1.0.16", + "truffle-codec": "^3.0.9", "truffle-expect": "^0.0.9", "truffle-solidity-utils": "^1.2.3", "web3": "1.2.1", diff --git a/packages/truffle-debugger/test/data/calldata.js b/packages/truffle-debugger/test/data/calldata.js index 2750c5ca9aa..0c84e7c2856 100644 --- a/packages/truffle-debugger/test/data/calldata.js +++ b/packages/truffle-debugger/test/data/calldata.js @@ -8,7 +8,7 @@ import Ganache from "ganache-core"; import { prepareContracts, lineOf } from "../helpers"; import Debugger from "lib/debugger"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; import solidity from "lib/solidity/selectors"; @@ -143,7 +143,7 @@ describe("Calldata Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -179,7 +179,7 @@ describe("Calldata Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -213,7 +213,7 @@ describe("Calldata Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -249,7 +249,7 @@ describe("Calldata Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -283,7 +283,7 @@ describe("Calldata Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); diff --git a/packages/truffle-debugger/test/data/codex.js b/packages/truffle-debugger/test/data/codex.js index 33e0528318d..88baff3c6d5 100644 --- a/packages/truffle-debugger/test/data/codex.js +++ b/packages/truffle-debugger/test/data/codex.js @@ -2,6 +2,7 @@ import debugModule from "debug"; const debug = debugModule("test:data:codex"); import { assert } from "chai"; +import { Conversion as ConversionUtils } from "truffle-codec-utils"; import Ganache from "ganache-core"; @@ -122,9 +123,11 @@ describe("Codex", function() { let session = bugger.connect(); + debug("starting stepping"); await session.continueUntilBreakpoint(); //run till end + debug("made it to end of transaction"); - const surface = (await session.variable("surface")).nativize(); + const surface = ConversionUtils.nativize(await session.variable("surface")); assert.equal(surface["ping"], 1); }); @@ -145,7 +148,7 @@ describe("Codex", function() { await session.continueUntilBreakpoint(); //run till end - const x = (await session.variable("x")).nativize(); + const x = ConversionUtils.nativize(await session.variable("x")); assert.equal(x, 1); }); @@ -166,7 +169,7 @@ describe("Codex", function() { await session.continueUntilBreakpoint(); //run till end - const x = (await session.variable("x")).nativize(); + const x = ConversionUtils.nativize(await session.variable("x")); assert.equal(x, 1); }); diff --git a/packages/truffle-debugger/test/data/function-decoding.js b/packages/truffle-debugger/test/data/function-decoding.js index d52175e7a64..f248338aa0d 100644 --- a/packages/truffle-debugger/test/data/function-decoding.js +++ b/packages/truffle-debugger/test/data/function-decoding.js @@ -7,7 +7,7 @@ import Ganache from "ganache-core"; import { prepareContracts, lineOf } from "../helpers"; import Debugger from "lib/debugger"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; import solidity from "lib/solidity/selectors"; @@ -170,17 +170,17 @@ describe("Function Pointer Decoding", function() { variables.storageFn.value.contract.class.typeName, "ExternalsDerived" ); - assert.equal(variables.storageFn.value.name, "doThing"); + assert.equal(variables.storageFn.value.abi.name, "doThing"); assert.equal( variables.memoryFns.value[0].value.contract.class.typeName, "ExternalsDerived" ); - assert.equal(variables.memoryFns.value[0].value.name, "doThing"); + assert.equal(variables.memoryFns.value[0].value.abi.name, "doThing"); assert.equal( variables.stackFn.value.contract.class.typeName, "ExternalsDerived" ); - assert.equal(variables.stackFn.value.name, "doThing"); + assert.equal(variables.stackFn.value.abi.name, "doThing"); }); it("Decodes internal function pointers correctly (deployed)", async function() { @@ -207,7 +207,7 @@ describe("Function Pointer Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -246,7 +246,7 @@ describe("Function Pointer Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); diff --git a/packages/truffle-debugger/test/data/global.js b/packages/truffle-debugger/test/data/global.js index 437d3536dc4..54c9eda4ae3 100644 --- a/packages/truffle-debugger/test/data/global.js +++ b/packages/truffle-debugger/test/data/global.js @@ -8,7 +8,7 @@ import Ganache from "ganache-core"; import { prepareContracts, lineOf } from "../helpers"; import Debugger from "lib/debugger"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; import solidity from "lib/solidity/selectors"; @@ -189,7 +189,7 @@ describe("Globally-available variables", function() { await session.continueUntilBreakpoint(); //run till end - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -223,7 +223,7 @@ describe("Globally-available variables", function() { }); await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -257,7 +257,7 @@ describe("Globally-available variables", function() { }); await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -291,7 +291,7 @@ describe("Globally-available variables", function() { }); await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -318,7 +318,7 @@ describe("Globally-available variables", function() { await session.continueUntilBreakpoint(); //run till end - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -352,7 +352,7 @@ describe("Globally-available variables", function() { }); await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); diff --git a/packages/truffle-debugger/test/data/helpers.js b/packages/truffle-debugger/test/data/helpers.js index 35b0b990204..23e7292fc38 100644 --- a/packages/truffle-debugger/test/data/helpers.js +++ b/packages/truffle-debugger/test/data/helpers.js @@ -4,6 +4,7 @@ const debug = debugModule("test:data:decode"); import Ganache from "ganache-core"; import { assert } from "chai"; import changeCase from "change-case"; +import { Conversion as ConversionUtils } from "truffle-codec-utils"; import { prepareContracts } from "test/helpers"; @@ -84,9 +85,7 @@ async function prepareDebugger(testName, sources) { } async function decode(name) { - let result = await this.session.variable(name); - - return result.nativize(); + return ConversionUtils.nativize(await this.session.variable(name)); } export function describeDecoding(testName, fixtures, selector, generateSource) { diff --git a/packages/truffle-debugger/test/data/ids.js b/packages/truffle-debugger/test/data/ids.js index b32668453a0..70d61ffb4cc 100644 --- a/packages/truffle-debugger/test/data/ids.js +++ b/packages/truffle-debugger/test/data/ids.js @@ -10,6 +10,7 @@ import Debugger from "lib/debugger"; import trace from "lib/trace/selectors"; import solidity from "lib/solidity/selectors"; +import { Conversion as ConversionUtils } from "truffle-codec-utils"; const __FACTORIAL = ` pragma solidity ^0.5.0; @@ -214,7 +215,7 @@ describe("Variable IDs", function() { await session.continueUntilBreakpoint(); while (!session.view(trace.finished)) { - values.push((await session.variable("nbang")).nativize()); + values.push(ConversionUtils.nativize(await session.variable("nbang"))); await session.continueUntilBreakpoint(); } diff --git a/packages/truffle-debugger/test/data/more-decoding.js b/packages/truffle-debugger/test/data/more-decoding.js index d5642c377f5..ec5ff9390eb 100644 --- a/packages/truffle-debugger/test/data/more-decoding.js +++ b/packages/truffle-debugger/test/data/more-decoding.js @@ -2,6 +2,7 @@ import debugModule from "debug"; const debug = debugModule("test:data:more-decoding"); import { assert } from "chai"; +import Web3 from "web3"; //just using for utils import Ganache from "ganache-core"; @@ -11,7 +12,7 @@ import Debugger from "lib/debugger"; import solidity from "lib/solidity/selectors"; import data from "lib/data/selectors"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; const __CONTAINERS = ` pragma solidity ^0.5.0; @@ -227,12 +228,26 @@ contract OverflowTest { } `; +const __BADBOOL = ` +pragma solidity ^0.5.0; + +contract BadBoolTest { + + mapping(bool => uint) boolMap; + + function run(bool key) public { + boolMap[key] = 1; + } +} +`; + let sources = { "ContainersTest.sol": __CONTAINERS, "ElementaryTest.sol": __KEYSANDBYTES, "SpliceTest.sol": __SPLICING, "ComplexMappingsTest.sol": __INNERMAPS, - "OverflowTest.sol": __OVERFLOW + "OverflowTest.sol": __OVERFLOW, + "BadBoolTest.sol": __BADBOOL }; describe("Further Decoding", function() { @@ -279,7 +294,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -323,7 +338,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); debug("variables %O", variables); @@ -367,7 +382,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -402,7 +417,7 @@ describe("Further Decoding", function() { //we're only testing storage so run till end await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); @@ -425,10 +440,10 @@ describe("Further Decoding", function() { //get offsets of top-level variables for this contract //converting to numbers for convenience const startingOffsets = Object.values( - Object.values(session.view(data.info.allocations.storage)).filter( - ({ definition }) => definition.name === "ComplexMappingTest" - )[0].members - ).map(({ pointer }) => pointer.storage.from.slot.offset); + session.view(data.info.allocations.storage) + ) + .find(({ definition }) => definition.name === "ComplexMappingTest") + .members.map(({ pointer }) => pointer.range.from.slot.offset); const mappingKeys = session.view(data.views.mappingKeys); for (let slot of mappingKeys) { @@ -441,6 +456,42 @@ describe("Further Decoding", function() { } }); + it("Cleans badly-encoded booleans used as mapping keys", async function() { + this.timeout(12000); + + let instance = await abstractions.BadBoolTest.deployed(); + let signature = "run(bool)"; + //manually set up the selector; 10 is for initial 0x + 8 more hex digits + let selector = Web3.utils + .soliditySha3({ type: "string", value: signature }) + .slice(0, 10); + let argument = + "0000000000000000000000000000000000000000000000000000000000000002"; + let receipt = await instance.sendTransaction({ data: selector + argument }); + let txHash = receipt.tx; + + let bugger = await Debugger.forTx(txHash, { + provider, + files, + contracts: artifacts + }); + + let session = bugger.connect(); + + await session.continueUntilBreakpoint(); //run till end + + const variables = TruffleCodecUtils.Conversion.nativizeVariables( + await session.variables() + ); + debug("variables %O", variables); + + const expectedResult = { + boolMap: { true: 1 } + }; + + assert.deepInclude(variables, expectedResult); + }); + describe("Overflow", function() { it("Discards padding on unsigned integers", async function() { let instance = await abstractions.OverflowTest.deployed(); @@ -464,7 +515,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); debug("variables %O", variables); @@ -500,7 +551,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); debug("variables %O", variables); @@ -536,7 +587,7 @@ describe("Further Decoding", function() { await session.continueUntilBreakpoint(); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); debug("variables %O", variables); diff --git a/packages/truffle-debugger/test/data/utils.js b/packages/truffle-debugger/test/data/utils.js index ff72674dc5d..5d9239aa3de 100644 --- a/packages/truffle-debugger/test/data/utils.js +++ b/packages/truffle-debugger/test/data/utils.js @@ -2,7 +2,7 @@ import { assert } from "chai"; import BN from "bn.js"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; describe("Utils", function() { describe("typeClass()", function() { @@ -14,7 +14,7 @@ describe("Utils", function() { }; assert.equal( - TruffleDecodeUtils.Definition.typeClass(definition), + TruffleCodecUtils.Definition.typeClass(definition), "mapping" ); }); @@ -25,7 +25,7 @@ describe("Utils", function() { let bytes = [0xf5, 0xe2, 0xc5, 0x17]; let expectedValue = new BN("f5e2c517", 16); - let result = TruffleDecodeUtils.Conversion.toBN(bytes); + let result = TruffleCodecUtils.Conversion.toBN(bytes); assert.equal(result.toString(), expectedValue.toString()); }); @@ -46,7 +46,7 @@ describe("Utils", function() { let expectedValue = bitflipped.addn(1).neg(); - let result = TruffleDecodeUtils.Conversion.toSignedBN(bytes); + let result = TruffleCodecUtils.Conversion.toSignedBN(bytes); assert.equal(result.toString(), expectedValue.toString()); }); @@ -56,7 +56,7 @@ describe("Utils", function() { let raw = new BN("05e2c517", 16); let expectedValue = raw; - let result = TruffleDecodeUtils.Conversion.toSignedBN(bytes); + let result = TruffleCodecUtils.Conversion.toSignedBN(bytes); assert.equal(result.toString(), expectedValue.toString()); }); @@ -66,11 +66,11 @@ describe("Utils", function() { it("returns correct representation with full bytes", function() { // ie, 0x00 instead of 0x0 assert.equal( - TruffleDecodeUtils.Conversion.toHexString([0x05, 0x11]), + TruffleCodecUtils.Conversion.toHexString([0x05, 0x11]), "0x0511" ); assert.equal( - TruffleDecodeUtils.Conversion.toHexString([0xff, 0x00, 0xff]), + TruffleCodecUtils.Conversion.toHexString([0xff, 0x00, 0xff]), "0xff00ff" ); }); diff --git a/packages/truffle-debugger/test/endstate.js b/packages/truffle-debugger/test/endstate.js index ea90bddcc89..684968b58c3 100644 --- a/packages/truffle-debugger/test/endstate.js +++ b/packages/truffle-debugger/test/endstate.js @@ -11,7 +11,7 @@ import Debugger from "lib/debugger"; import evm from "lib/evm/selectors"; import data from "lib/data/selectors"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; const __FAILURE = ` pragma solidity ~0.5; @@ -102,7 +102,7 @@ describe("End State", function() { debug("proc.assignments %O", session.view(data.proc.assignments)); assert.ok(session.view(evm.transaction.status)); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); assert.include(variables, { x: 107 }); diff --git a/packages/truffle-debugger/test/load.js b/packages/truffle-debugger/test/load.js index 5542f5737ac..79fa966e5f0 100644 --- a/packages/truffle-debugger/test/load.js +++ b/packages/truffle-debugger/test/load.js @@ -6,7 +6,7 @@ import { assert } from "chai"; import Ganache from "ganache-core"; import { prepareContracts } from "./helpers"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; import Debugger from "lib/debugger"; import trace from "lib/trace/selectors"; @@ -71,7 +71,7 @@ describe("Loading and unloading transactions", function() { await session.load(txHash); assert.isTrue(session.view(trace.loaded)); await session.continueUntilBreakpoint(); //continue to end - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); const expected = { x: 1 }; @@ -97,7 +97,7 @@ describe("Loading and unloading transactions", function() { assert.isTrue(session.view(trace.loaded)); await session.continueUntilBreakpoint(); //continue to end - let variables = TruffleDecodeUtils.Conversion.nativizeVariables( + let variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); let expected = { x: 1 }; @@ -107,7 +107,7 @@ describe("Loading and unloading transactions", function() { await session.load(txHash2); assert.isTrue(session.view(trace.loaded)); await session.continueUntilBreakpoint(); //continue to end - variables = TruffleDecodeUtils.Conversion.nativizeVariables( + variables = TruffleCodecUtils.Conversion.nativizeVariables( await session.variables() ); expected = { y: 2 }; diff --git a/packages/truffle-debugger/test/reset.js b/packages/truffle-debugger/test/reset.js index 90c74daa3ec..5122b2d3485 100644 --- a/packages/truffle-debugger/test/reset.js +++ b/packages/truffle-debugger/test/reset.js @@ -6,7 +6,7 @@ import { assert } from "chai"; import Ganache from "ganache-core"; import { prepareContracts, lineOf } from "./helpers"; -import * as TruffleDecodeUtils from "truffle-decode-utils"; +import * as TruffleCodecUtils from "truffle-codec-utils"; import Debugger from "lib/debugger"; import solidity from "lib/solidity/selectors"; @@ -74,31 +74,31 @@ describe("Reset Button", function() { await session.addBreakpoint({ sourceId, line: lineOf("BREAK", source) }); variables[0].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); await session.continueUntilBreakpoint(); //advance to line 10 variables[0].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); await session.continueUntilBreakpoint(); //advance to the end variables[0].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); //now, reset and do it again await session.reset(); variables[1].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); await session.addBreakpoint({ sourceId, line: lineOf("BREAK", source) }); await session.continueUntilBreakpoint(); //advance to line 10 variables[1].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); await session.continueUntilBreakpoint(); //advance to the end variables[1].push( - TruffleDecodeUtils.Conversion.nativizeVariables(await session.variables()) + TruffleCodecUtils.Conversion.nativizeVariables(await session.variables()) ); assert.deepEqual(variables[1], variables[0]); diff --git a/packages/truffle-decode-utils/src/conversion.ts b/packages/truffle-decode-utils/src/conversion.ts deleted file mode 100644 index 6f595539f58..00000000000 --- a/packages/truffle-decode-utils/src/conversion.ts +++ /dev/null @@ -1,161 +0,0 @@ -import BN from "bn.js"; -import Web3 from "web3"; -import { Constants } from "./constants"; -import { Values } from "./types/values"; - -export namespace Conversion { - - /** - * @param bytes - undefined | string | number | BN | Uint8Array - * @return {BN} - */ - export function toBN(bytes: undefined | string | number | BN | Uint8Array): BN { - if (bytes === undefined) { - return undefined; - } else if (typeof bytes == "string") { - return new BN(bytes, 16); - } else if (typeof bytes == "number" || BN.isBN(bytes)) { - return new BN(bytes); - } else if (bytes.reduce) { - return bytes.reduce( - (num: BN, byte: number) => num.shln(8).addn(byte), - new BN(0) - ); - } - } - - /** - * @param bytes - Uint8Array - * @return {BN} - */ - export function toSignedBN(bytes: Uint8Array): BN { - if (bytes[0] < 0x80) { // if first bit is 0 - return toBN(bytes); - } else { - return toBN(bytes.map( (b) => 0xff - b )).addn(1).neg(); - } - } - - /** - * @param bytes - Uint8Array | BN - * @param padLength - number - minimum desired byte length (left-pad with zeroes) - * @return {string} - */ - export function toHexString(bytes: Uint8Array | BN, padLength: number = 0): string { - - if (BN.isBN(bytes)) { - bytes = toBytes(bytes); - } - - const pad = (s: string) => `${"00".slice(0, 2 - s.length)}${s}`; - - // 0 1 2 3 4 - // 0 1 2 3 4 5 6 7 - // bytes.length: 5 - 0x( e5 c2 aa 09 11 ) - // length (preferred): 8 - 0x( 00 00 00 e5 c2 aa 09 11 ) - // `--.---' - // offset 3 - if (bytes.length < padLength) { - let prior = bytes; - bytes = new Uint8Array(padLength); - - bytes.set(prior, padLength - prior.length); - } - - // debug("bytes: %o", bytes); - - let string = bytes.reduce( - (str, byte) => `${str}${pad(byte.toString(16))}`, "" - ); - - return `0x${string}`; - } - - export function toAddress(bytes: Uint8Array | string): string { - - if(typeof bytes === "string") { - //in this case, we can do some simple string manipulation and - //then pass to web3 - let hex = bytes; //just renaming for clarity - if (hex.startsWith("0x")) { - hex = hex.slice(2); - } - if(hex.length < 2 * Constants.ADDRESS_SIZE) - { - hex = hex.padStart(2 * Constants.ADDRESS_SIZE, "0"); - } - if(hex.length > 2 * Constants.ADDRESS_SIZE) - { - hex = "0x" + hex.slice(hex.length - 2 * Constants.ADDRESS_SIZE); - } - return Web3.utils.toChecksumAddress(hex); - } - //otherwise, we're in the Uint8Array case, which we can't fully handle ourself - - //truncate *on left* to 20 bytes - if(bytes.length > Constants.ADDRESS_SIZE) { - bytes = bytes.slice(bytes.length - Constants.ADDRESS_SIZE, bytes.length); - } - - //now, convert to hex string and apply checksum case that second argument - //(which ensures it's padded to 20 bytes) shouldn't actually ever be - //needed, but I'll be safe and include it - return Web3.utils.toChecksumAddress(toHexString(bytes, Constants.ADDRESS_SIZE)); - } - - export function toBytes(data: BN | string, length: number = 0): Uint8Array { - //note that length is a minimum output length - //strings will be 0-padded on left - //BN will be sign-padded on left - //NOTE: if a BN is passed in that is too big for the given length, - //you will get an error! - //(note that strings passed in should be hex strings; this is not for converting - //generic strings to hex) - - if (typeof data === "string") { - - let hex = data; //renaming for clarity - - if (hex.startsWith("0x")) { - hex = hex.slice(2); - } - - if(hex === "") { - //this special case is necessary because the match below will return null, - //not an empty array, when given an empty string - return new Uint8Array(0); - } - - if (hex.length % 2 == 1) { - hex = `0${hex}`; - } - - let bytes = new Uint8Array( - hex.match(/.{2}/g) - .map( (byte) => parseInt(byte, 16) ) - ); - - if (bytes.length < length) { - let prior = bytes; - bytes = new Uint8Array(length); - bytes.fill(0); - bytes.set(prior, length - prior.length); - } - - return bytes; - } - else { - // BN case - - //note that the argument for toTwos is given in bits - return new Uint8Array(data.toTwos(length * 8).toArrayLike(Buffer, "be", length)); //big-endian - } - } - - //for convenience: invokes the nativize method on all the given variables - export function nativizeVariables(variables: {[name: string]: Values.Result}): {[name: string]: any} { - return Object.assign({}, ...Object.entries(variables).map( - ([name, value]) => ({[name]: value.nativize()}) - )); - } -} diff --git a/packages/truffle-decode-utils/src/types/errors.ts b/packages/truffle-decode-utils/src/types/errors.ts deleted file mode 100644 index c581201ae68..00000000000 --- a/packages/truffle-decode-utils/src/types/errors.ts +++ /dev/null @@ -1,621 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decode-utils:types:errors"); - -//objects for Solidity values - -//Note: This is NOT intended to represent every possible value that exists -//in Solidity! Only possible values of variables. (Though there may be -//some expansion in the future.) We do however count the builtin variables -//msg, block, and tx as variables (not other builtins though for now) so -//there is some support for the magic type. - -//We don't include fixed and ufixed for now. Those will be added when -//implemented. - -//NOTE: not all of these optional fields are actually implemented. Some are -//just intended for the future. More optional fields may be added in the -//future. - -//Note: Many of the errors defined here deliberately *don't* extend Error. -//This is because their intended use is a little different. Only the ones -//that are for throwing extend Error. - -import BN from "bn.js"; -import { Types } from "./types"; -import { InspectOptions } from "./inspect"; -import util from "util"; -import { AstDefinition } from "../ast"; -import { Definition as DefinitionUtils } from "../definition"; - -export namespace Errors { - - /* - * SECTION 1: Generic types for values in general (including errors). - */ - - //For when we need to throw an error, here's a wrapper class that extends Error. - //Apologies about the confusing name, but I wanted something that would make - //sense should it not be caught and thus accidentally exposed to the outside. - export class DecodingError extends Error { - error: GenericError; - constructor(error: GenericError) { - super(error.message()); - this.error = error; - this.name = "DecodingError"; - } - } - - export type ErrorResult = ElementaryErrorResult - | ArrayErrorResult | MappingErrorResult | StructErrorResult | MagicErrorResult - | EnumErrorResult - | ContractErrorResult | FunctionExternalErrorResult | FunctionInternalErrorResult; - - //for internal use only! just here to facilitate code reuse! - //please use the union types for actual use! - //this class doesn't even have the error field! - abstract class ErrorResultBase { - type: Types.Type; - kind: "error"; - error: DecoderErrorBase; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.error, options); - } - nativize(): any { - return undefined; - } - toSoliditySha3Input(): {type: string; value: any} { - return undefined; //will cause an error! should not occur! - } - constructor() { - this.kind = "error"; - } - } - - //also just for internal use to make things easier! - abstract class DecoderErrorBase { - kind: string; - } - - /* - * SECTION 2: Elementary values - */ - - export type ElementaryErrorResult = UintErrorResult | IntErrorResult | BoolErrorResult - | BytesErrorResult | AddressErrorResult | StringErrorResult - | FixedErrorResult | UfixedErrorResult; - export type BytesErrorResult = BytesStaticErrorResult | BytesDynamicErrorResult; - - //Uints - export class UintErrorResult extends ErrorResultBase { - constructor( - public uintType: Types.UintType, - public error: GenericError | UintPaddingError - ) { - super(); - } - } - - export class UintPaddingError extends DecoderErrorBase { - raw: string; //hex string - kind: "UintPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Uint has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "UintPaddingError"; - } - } - - //Ints - export class IntErrorResult extends ErrorResultBase { - constructor( - public intType: Types.IntType, - public error: GenericError | IntPaddingError - ) { - super(); - } - } - - export class IntPaddingError extends DecoderErrorBase { - raw: string; //hex string - kind: "IntPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Int out of range (padding error) (numeric value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "IntPaddingError"; - } - } - - //Bools - export class BoolErrorResult extends ErrorResultBase { - constructor( - public boolType: Types.BoolType, - public error: GenericError | BoolPaddingError | BoolOutOfRangeError - ) { - super(); - } - } - - export class BoolPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "BoolPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Bool has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "BoolPaddingError"; - } - } - - export class BoolOutOfRangeError extends DecoderErrorBase { - raw: BN; - kind: "BoolOutOfRangeError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Invalid boolean (numeric value ${this.raw.toString()})`; - } - constructor(raw: BN) { - super(); - this.raw = raw; - this.kind = "BoolOutOfRangeError"; - } - } - - //bytes (static) - export class BytesStaticErrorResult extends ErrorResultBase { - constructor( - public bytesType: Types.BytesTypeStatic, - public error: GenericError | BytesPaddingError - ) { - super(); - } - } - - export class BytesPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "BytesPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Bytestring has extra trailing bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "BytesPaddingError"; - } - } - - //bytes (dynamic) - export class BytesDynamicErrorResult extends ErrorResultBase { - constructor( - public bytesType: Types.BytesTypeDynamic, - public error: GenericError - ) { - super(); - } - } - - //addresses - export class AddressErrorResult extends ErrorResultBase { - constructor( - public addressType: Types.AddressType, - public error: GenericError | AddressPaddingError - ) { - super(); - } - } - - export class AddressPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "AddressPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Address has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "AddressPaddingError"; - } - } - - //strings - export class StringErrorResult extends ErrorResultBase { - constructor( - public stringType: Types.StringType, - public error: GenericError - ) { - super(); - } - } - - //Fixed & Ufixed - //These don't have a value format yet, so they just decode to errors for now! - export class FixedErrorResult extends ErrorResultBase { - constructor( - public fixedType: Types.FixedType, - public error: GenericError | FixedPointNotYetSupportedError - ) { - super(); - } - } - export class UfixedErrorResult extends ErrorResultBase { - constructor( - public ufixedType: Types.UfixedType, - public error: GenericError | FixedPointNotYetSupportedError - ) { - super(); - } - } - - export class FixedPointNotYetSupportedError extends DecoderErrorBase { - raw: string; //hex string - kind: "FixedPointNotYetSupportedError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Fixed-point decoding not yet supported (raw value: ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "FixedPointNotYetSupportedError"; - } - } - //no separate padding error here, that would be pointless right now; will make later - - /* - * SECTION 3: CONTAINER TYPES (including magic) - */ - - //Arrays - export class ArrayErrorResult extends ErrorResultBase { - constructor( - public arrayType: Types.ArrayType, - public error: GenericError - ) { - super(); - } - } - - //Mappings - export class MappingErrorResult extends ErrorResultBase { - constructor( - public mappingType: Types.MappingType, - public error: GenericError - ) { - super(); - } - } - - //Structs - export class StructErrorResult extends ErrorResultBase { - constructor( - public structType: Types.StructType, - public error: GenericError - ) { - super(); - } - } - - //Magic variables - export class MagicErrorResult extends ErrorResultBase { - constructor( - public magicType: Types.MagicType, - public error: GenericError - ) { - super(); - } - } - - /* - * SECTION 4: ENUMS - * (they didn't fit anywhere else :P ) - */ - - //Enums - export class EnumErrorResult extends ErrorResultBase { - constructor( - public enumType: Types.EnumType, - public error: GenericError | EnumPaddingError | EnumOutOfRangeError | EnumNotFoundDecodingError - ) { - super(); - } - } - - export class EnumPaddingError extends DecoderErrorBase { - kind: "EnumPaddingError"; - type: Types.EnumType; - raw: string; //should be hex string - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - let typeName = (this.type.kind === "local" ? (this.type.definingContractName + ".") : "") + this.type.typeName; - return `${typeName} has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(enumType: Types.EnumType, raw: string) { - super(); - this.type = enumType; - this.raw = raw; - this.kind = "EnumPaddingError"; - } - } - - export class EnumOutOfRangeError extends DecoderErrorBase { - kind: "EnumOutOfRangeError"; - type: Types.EnumType; - raw: BN; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - let typeName = (this.type.kind === "local" ? (this.type.definingContractName + ".") : "") + this.type.typeName; - return `Invalid ${typeName} (numeric value ${this.raw.toString()})`; - } - constructor(enumType: Types.EnumType, raw: BN) { - super(); - this.type = enumType; - this.raw = raw; - this.kind = "EnumOutOfRangeError"; - } - } - - export class EnumNotFoundDecodingError extends DecoderErrorBase { - kind: "EnumNotFoundDecodingError"; - type: Types.EnumType; - raw: BN; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - let typeName = (this.type.kind === "local" ? (this.type.definingContractName + ".") : "") + this.type.typeName; - return `Unknown enum type ${typeName} of id ${this.type.id} (numeric value ${this.raw.toString()})`; - } - constructor(enumType: Types.EnumType, raw: BN) { - super(); - this.type = enumType; - this.raw = raw; - this.kind = "EnumNotFoundDecodingError"; - } - } - - /* - * SECTION 5: CONTRACTS - */ - - //Contracts - export class ContractErrorResult extends ErrorResultBase { - constructor( - public contractType: Types.ContractType, - public error: GenericError | ContractPaddingError - ) { - super(); - } - } - - export class ContractPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "ContractPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Contract address has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "ContractPaddingError"; - } - } - - /* - * SECTION 6: External functions - */ - - //external functions - export class FunctionExternalErrorResult extends ErrorResultBase { - constructor( - public functionType: Types.FunctionTypeExternal, - public error: GenericError | FunctionExternalNonStackPaddingError | FunctionExternalStackPaddingError - ) { - super(); - } - } - - export class FunctionExternalNonStackPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "FunctionExternalNonStackPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `External function has extra trailing bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "FunctionExternalNonStackPaddingError"; - } - } - - export class FunctionExternalStackPaddingError extends DecoderErrorBase { - rawAddress: string; - rawSelector: string; - kind: "FunctionExternalStackPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `External function address or selector has extra leading bytes (padding error) (raw address ${this.rawAddress}, raw selector ${this.rawSelector})`; - } - constructor(rawAddress: string, rawSelector: string) { - super(); - this.rawAddress = rawAddress; - this.rawSelector = rawSelector; - this.kind = "FunctionExternalStackPaddingError"; - } - } - - /* - * SECTION 7: INTERNAL FUNCTIONS - */ - - //Internal functions - export class FunctionInternalErrorResult extends ErrorResultBase { - constructor( - public functionType: Types.FunctionTypeInternal, - public error: GenericError | FunctionInternalPaddingError - | NoSuchInternalFunctionError | DeployedFunctionInConstructorError | MalformedInternalFunctionError - ) { - super(); - } - } - - export class FunctionInternalPaddingError extends DecoderErrorBase { - raw: string; //should be hex string - kind: "FunctionInternalPaddingError"; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Internal function has extra leading bytes (padding error) (raw value ${this.raw})`; - } - constructor(raw: string) { - super(); - this.raw = raw; - this.kind = "FunctionInternalPaddingError"; - } - } - - export class NoSuchInternalFunctionError extends DecoderErrorBase { - kind: "NoSuchInternalFunctionError"; - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Invalid function (Deployed PC=${this.deployedProgramCounter}, constructor PC=${this.constructorProgramCounter}) of contract ${this.context.typeName}`; - } - constructor(context: Types.ContractType, deployedProgramCounter: number, constructorProgramCounter: number) { - super(); - this.context = context; - this.deployedProgramCounter = deployedProgramCounter; - this.constructorProgramCounter = constructorProgramCounter; - this.kind = "NoSuchInternalFunctionError"; - } - } - - export class DeployedFunctionInConstructorError extends DecoderErrorBase { - kind: "DeployedFunctionInConstructorError"; - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Deployed-style function (PC=${this.deployedProgramCounter}) in constructor`; - } - constructor(context: Types.ContractType, deployedProgramCounter: number) { - super(); - this.context = context; - this.deployedProgramCounter = deployedProgramCounter; - this.constructorProgramCounter = 0; - this.kind = "DeployedFunctionInConstructorError"; - } - } - - export class MalformedInternalFunctionError extends DecoderErrorBase { - kind: "MalformedInternalFunctionError"; - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return `Malformed internal function w/constructor PC only (value: ${this.constructorProgramCounter})`; - } - constructor(context: Types.ContractType, constructorProgramCounter: number) { - super(); - this.context = context; - this.deployedProgramCounter = 0; - this.constructorProgramCounter = constructorProgramCounter; - this.kind = "MalformedInternalFunctionError"; - } - } - - /* - * SECTION 8: GENERIC ERRORS - */ - - export type GenericError = UserDefinedTypeNotFoundError | UnsupportedConstantError | ReadErrorStack; - - //type-location error - export class UserDefinedTypeNotFoundError extends DecoderErrorBase { - kind: "UserDefinedTypeNotFoundError"; - type: Types.UserDefinedType; - message() { - let typeName = Types.isContractDefinedType(this.type) - ? this.type.definingContractName + "." + this.type.typeName - : this.type.typeName; - return `Unknown ${this.type.typeClass} type ${typeName} of id ${this.type.id}`; - } - constructor(unknownType: Types.UserDefinedType) { - super(); - this.type = unknownType; - this.kind = "UserDefinedTypeNotFoundError"; - } - } - - //Read errors - export class UnsupportedConstantError extends DecoderErrorBase { - kind: "UnsupportedConstantError"; - definition: AstDefinition; - message() { - return `Unsupported constant type ${DefinitionUtils.typeClass(this.definition)}$`; - } - constructor(definition: AstDefinition) { - super(); - this.definition = definition; - this.kind = "UnsupportedConstantError"; - } - } - - export class ReadErrorStack extends DecoderErrorBase { - kind: "ReadErrorStack"; - from: number; - to: number; - message() { - return `Can't read stack from position ${this.from} to ${this.to}`; - } - constructor(from: number, to: number) { - super(); - this.from = from; - this.to = to; - this.kind = "ReadErrorStack"; - } - } - - //finally, a convenience function for constructing generic errors - export function makeGenericErrorResult(dataType: Types.Type, error: GenericError): ErrorResult { - switch(dataType.typeClass) { - case "uint": - return new UintErrorResult(dataType, error); - case "int": - return new IntErrorResult(dataType, error); - case "bool": - return new BoolErrorResult(dataType, error); - case "bytes": - switch(dataType.kind) { - case "static": - return new BytesStaticErrorResult(dataType, error); - case "dynamic": - return new BytesDynamicErrorResult(dataType, error); - } - case "address": - return new AddressErrorResult(dataType, error); - case "fixed": - return new FixedErrorResult(dataType, error); - case "ufixed": - return new UfixedErrorResult(dataType, error); - case "string": - return new StringErrorResult(dataType, error); - case "array": - return new ArrayErrorResult(dataType, error); - case "mapping": - return new MappingErrorResult(dataType, error); - case "struct": - return new StructErrorResult(dataType, error); - case "enum": - return new EnumErrorResult(dataType, error); - case "contract": - return new ContractErrorResult(dataType, error); - case "magic": - return new MagicErrorResult(dataType, error); - case "function": - switch(dataType.visibility) { - case "external": - return new FunctionExternalErrorResult(dataType, error); - case "internal": - return new FunctionInternalErrorResult(dataType, error); - } - } - } -} diff --git a/packages/truffle-decode-utils/src/types/inspect.ts b/packages/truffle-decode-utils/src/types/inspect.ts deleted file mode 100644 index ff6cd7e8153..00000000000 --- a/packages/truffle-decode-utils/src/types/inspect.ts +++ /dev/null @@ -1,22 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decode-utils:types:inspect"); - -import util from "util"; - -//we'll need to write a typing for the options type ourself, it seems; just -//going to include the relevant properties here -export interface InspectOptions { - stylize?: (toMaybeColor: string, style?: string) => string; - colors: boolean; - breakLength: number; -} - -//HACK -- inspect options are ridiculous, I swear >_> -export function cleanStylize(options: InspectOptions) { - return Object.assign({}, ...Object.entries(options).map( - ([key,value]) => - key === "stylize" - ? {} - : {[key]: value} - )); -} diff --git a/packages/truffle-decode-utils/src/types/values.ts b/packages/truffle-decode-utils/src/types/values.ts deleted file mode 100644 index 1bf419fb43c..00000000000 --- a/packages/truffle-decode-utils/src/types/values.ts +++ /dev/null @@ -1,850 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decode-utils:types:values"); - -//objects for Solidity values - -//Note: This is NOT intended to represent every possible value that exists -//in Solidity! Only possible values of variables. (Though there may be -//some expansion in the future; I'm definitely intending to add tuples.) -//We do however count the builtin variables msg, block, and tx as variables -//(not other builtins though for now) so there is some support for the magic -//type. - -//We don't include fixed and ufixed for now. Those will be added when -//implemented. - -//NOTE: not all of these optional fields are actually implemented. Some are -//just intended for the future. More optional fields may be added in the -//future. - -//Note: Many of the errors defined here deliberately *don't* extend Error. -//This is because their intended use is a little different. Only the ones -//that are for throwing extend Error. - -import BN from "bn.js"; -import { Types } from "./types"; -import { Errors } from "./errors"; -import { InspectOptions, cleanStylize } from "./inspect"; -import util from "util"; -import { AstDefinition } from "../ast"; -import { Definition as DefinitionUtils } from "../definition"; - -export namespace Values { - - /* - * SECTION 1: Generic types for values in general (including errors). - */ - - //This is the overall Result type. It may encode an actual value or an error. - export type Result = ElementaryResult - | ArrayResult | MappingResult | StructResult | MagicResult - | EnumResult - | ContractResult | FunctionExternalResult | FunctionInternalResult; - //for when you want an actual value - export type Value = ElementaryValue - | ArrayValue | MappingValue | StructValue | MagicValue - | EnumValue - | ContractValue | FunctionExternalValue | FunctionInternalValue; - - /* - * SECTION 2: Elementary values - */ - - export type ElementaryResult = UintResult | IntResult | BoolResult - | BytesResult | AddressResult | StringResult - | FixedResult | UfixedResult; - export type BytesResult = BytesStaticResult | BytesDynamicResult; - - //note that we often want an elementary *value*, and not an error! - //so let's define those types too - export type ElementaryValue = UintValue | IntValue | BoolValue - | BytesValue | AddressValue | StringValue; - //we don't include FixedValue or UfixedValue because those - //aren't implemented yet - export type BytesValue = BytesStaticValue | BytesDynamicValue; - - - //Uints - export type UintResult = UintValue | Errors.UintErrorResult; - - export class UintValue { - type: Types.UintType; - kind: "value"; - value: { - asBN: BN; - rawAsBN?: BN; - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(this.toString(), "number"); - } - toSoliditySha3Input() { - return { - type: "uint", - value: this.value.asBN - } - } - nativize(): any { - return this.value.asBN.toNumber(); //beware! - } - toString(): string { - return this.value.asBN.toString(); - } - constructor(uintType: Types.UintType, value: BN, rawValue?: BN) { - this.type = uintType; - this.kind = "value"; - this.value = { asBN: value, rawAsBN: rawValue }; - } - } - - //Ints - export type IntResult = IntValue | Errors.IntErrorResult; - - export class IntValue { - type: Types.IntType; - kind: "value"; - value: { - asBN: BN; - rawAsBN?: BN; - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(this.toString(), "number"); - } - toSoliditySha3Input() { - return { - type: "int", - value: this.value.asBN - } - } - nativize(): any { - return this.value.asBN.toNumber(); //beware! - } - toString(): string { - return this.value.asBN.toString(); - } - constructor(intType: Types.IntType, value: BN, rawValue?: BN) { - this.type = intType; - this.kind = "value"; - this.value = { asBN: value, rawAsBN: rawValue }; - } - } - - //Bools - export type BoolResult = BoolValue | Errors.BoolErrorResult; - - export class BoolValue { - type: Types.BoolType; - kind: "value"; - value: { - asBool: boolean; - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value.asBool, options); - } - toSoliditySha3Input() { - return { - type: "uint", //used to achieve left-padding - value: this.value.asBool ? new BN(1) : new BN(0) //true & false won't work here - } - } - nativize(): any { - return this.value.asBool; - } - toString(): string { - return this.value.asBool.toString(); - } - constructor(boolType: Types.BoolType, value: boolean) { - this.type = boolType; - this.kind = "value"; - this.value = { asBool: value }; - } - } - - //bytes (static) - export type BytesStaticResult = BytesStaticValue | Errors.BytesStaticErrorResult; - - export class BytesStaticValue { - type: Types.BytesTypeStatic; - kind: "value"; - value: { - asHex: string; //should be hex-formatted, with leading "0x" - rawAsHex: string; - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(this.value.asHex, "number"); - } - toSoliditySha3Input() { - return { - type: "bytes32", //used to achieve right-padding - value: this.value.asHex - }; - } - nativize(): any { - return this.value.asHex; - } - toString(): string { - return this.value.asHex; - } - constructor(bytesType: Types.BytesTypeStatic, value: string, rawValue?: string) { - this.type = bytesType; - this.kind = "value"; - this.value = { asHex: value, rawAsHex: rawValue }; - } - } - - //bytes (dynamic) - export type BytesDynamicResult = BytesDynamicValue | Errors.BytesDynamicErrorResult; - - export class BytesDynamicValue { - type: Types.BytesTypeDynamic; - kind: "value"; - value: { - asHex: string; //should be hex-formatted, with leading "0x" - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(`hex'${this.value.asHex.slice(2)}'`, "string") - } - toSoliditySha3Input() { - return { - type: "bytes", - value: this.value.asHex - }; - } - nativize(): any { - return this.value.asHex; - } - toString(): string { - return this.value.asHex; - } - constructor(bytesType: Types.BytesTypeDynamic, value: string) { - this.type = bytesType; - this.kind = "value"; - this.value = { asHex: value }; - } - } - - //addresses - export type AddressResult = AddressValue | Errors.AddressErrorResult; - - export class AddressValue { - type: Types.AddressType; - kind: "value"; - value: { - asAddress: string; //should have 0x and be checksum-cased - rawAsHex: string; - } - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(this.value.asAddress, "number"); - } - toSoliditySha3Input() { - return { - type: "uint", //used to achieve left-padding - value: this.value.asAddress - } - } - nativize(): any { - return this.value.asAddress; - } - toString(): string { - return this.value.asAddress; - } - constructor(addressType: Types.AddressType, value: string, rawValue?: string) { - this.type = addressType; - this.kind = "value"; - this.value = { asAddress: value, rawAsHex: rawValue }; - } - } - - //strings - export type StringResult = StringValue | Errors.StringErrorResult; - - //strings have a special new type as their value: StringValueInfo - export class StringValue { - type: Types.StringType; - kind: "value"; - value: StringValueInfo; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value, options); - } - toSoliditySha3Input() { - return this.value.toSoliditySha3Input(); - } - nativize(): any { - return this.value.nativize(); - } - toString(): string { - return this.value.toString(); - } - constructor(stringType: Types.StringType, value: StringValueInfo) { - this.type = stringType; - this.kind = "value"; - this.value = value; - } - } - - //these come in two types: valid strings and malformed strings - export type StringValueInfo = StringValueInfoValid | StringValueInfoMalformed; - - //valid strings - export class StringValueInfoValid { - kind: "valid"; - asString: string; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.asString, options); - } - toSoliditySha3Input() { - return { - type: "string", - value: this.asString - }; - } - nativize(): any { - return this.asString; - } - toString(): string { - return this.asString; - } - constructor(value: string) { - this.kind = "valid"; - this.asString = value; - } - } - - //malformed strings - export class StringValueInfoMalformed { - kind: "malformed"; - asHex: string; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(`hex'${this.asHex.slice(2)}'`, "string") + " (malformed)"; - } - toSoliditySha3Input() { - return { - type: "bytes", - value: this.asHex - }; - } - nativize(): any { - return this.asHex; //warning! - } - toString(): string { - return this.asHex; //warning! - } - constructor(value: string) { - this.kind = "malformed"; - this.asHex = value; - } - } - - //Fixed & Ufixed - //These don't have a value format yet, so they just decode to errors for now! - - export type FixedResult = Errors.FixedErrorResult; - export type UfixedResult = Errors.UfixedErrorResult; - - //Function for wrapping a value as an ElementaryValue - //WARNING: this function does not check its inputs! Please check before using! - //How to use: - //numbers may be BN, number, or numeric string - //strings should be given as strings. duh. - //bytes should be given as hex strings beginning with "0x" - //addresses are like bytes; checksum case is not required - //booleans may be given either as booleans, or as string "true" or "false" - //[NOTE: in the future this function will: - //1. be moved to truffle-codec/lib/encode, - //2. check its inputs, - //3. take a slightly different input format] - export function wrapElementaryValue(value: any, definition: AstDefinition): ElementaryValue { - //force location to undefined, force address to nonpayable - //(we force address to nonpayable since address payable can't be declared - //as a mapping key type) - let dataType = Types.definitionToType(definition, null, null); - switch(dataType.typeClass) { - case "string": - return new StringValue(dataType, new StringValueInfoValid(value)); - case "bytes": - switch(dataType.kind) { - case "static": - return new BytesStaticValue(dataType, value); - case "dynamic": - return new BytesDynamicValue(dataType, value); - } - case "address": - return new AddressValue(dataType, value); - case "int": - if(value instanceof BN) { - value = value.clone(); - } - else { - value = new BN(value); - } - return new IntValue(dataType, value); - case "uint": - if(value instanceof BN) { - value = value.clone(); - } - else { - value = new BN(value); - } - return new UintValue(dataType, value); - case "bool": - if(typeof value === "string") { - value = value !== "false"; - } - return new BoolValue(dataType, value); - } - } - - /* - * SECTION 3: CONTAINER TYPES (including magic) - */ - - //this function will be used in the future for displaying circular - //structures - function formatCircular(loopLength: number, options: InspectOptions) { - return options.stylize(`[Circular (=up ${this.loopLength})]`, "special"); - } - - //Arrays - export type ArrayResult = ArrayValue | Errors.ArrayErrorResult; - - export class ArrayValue { - type: Types.ArrayType; - kind: "value"; - reference?: number; //will be used in the future for circular values - value: Result[]; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - if(this.reference !== undefined) { - return formatCircular(this.reference, options); - } - return util.inspect(this.value, options) - } - nativize(): any { - return this.value.map(element => element.nativize()); - } - constructor(arrayType: Types.ArrayType, value: Result[], reference?: number) { - this.type = arrayType; - this.kind = "value"; - this.value = value; - this.reference = reference; - } - } - - //Mappings - export type MappingResult = MappingValue | Errors.MappingErrorResult; - - export class MappingValue { - type: Types.MappingType; - kind: "value"; - //note that since mappings live in storage, a circular - //mapping is impossible - value: KeyValuePair[]; //order is irrelevant - //note that key is not allowed to be an error! - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(new Map(this.value.map( - ({key, value}) => [key, value] - )), options); - } - nativize(): any { - return Object.assign({}, ...this.value.map(({key, value}) => - ({[key.toString()]: value.nativize()}) - )); - } - constructor(mappingType: Types.MappingType, value: KeyValuePair[]) { - this.type = mappingType; - this.kind = "value"; - this.value = value; - } - } - - export interface KeyValuePair { - key: ElementaryValue; //note must be a value, not an error! - value: Result; - } - - //Structs - export type StructResult = StructValue | Errors.StructErrorResult; - - export class StructValue { - type: Types.StructType; - kind: "value"; - reference?: number; //will be used in the future for circular values - value: NameValuePair[]; //these should be stored in order! - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - if(this.reference !== undefined) { - return formatCircular(this.reference, options); - } - return util.inspect( - Object.assign({}, ...this.value.map( - ({name, value}) => ({[name]: value}) - )), - options - ); - } - nativize(): any { - return Object.assign({}, ...this.value.map( - ({name, value}) => ({[name]: value.nativize()}) - )); - } - constructor(structType: Types.StructType, value: NameValuePair[], reference?: number) { - this.type = structType; - this.kind = "value"; - this.value = value; - this.reference = reference; - } - } - - export interface NameValuePair { - name: string; - value: Result; - } - - //Magic variables - export type MagicResult = MagicValue | Errors.MagicErrorResult; - - export class MagicValue { - type: Types.MagicType; - kind: "value"; - //a magic variable can't be circular, duh! - value: { - [field: string]: Result - }; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value, options); - } - nativize(): any { - return Object.assign({}, ...Object.entries(this.value).map( - ([key, value]) => ({[key]: value.nativize()}) - )); - } - constructor(magicType: Types.MagicType, value: {[field: string]: Result}) { - this.type = magicType; - this.kind = "value"; - this.value = value; - } - } - - /* - * SECTION 4: ENUMS - * (they didn't fit anywhere else :P ) - */ - - //Enums - export type EnumResult = EnumValue | Errors.EnumErrorResult; - - export class EnumValue { - type: Types.EnumType; - kind: "value"; - value: { - name: string; - numericAsBN: BN; - }; - fullName(): string { - switch(this.type.kind) { - case "local": - return `${this.type.definingContractName}.${this.type.typeName}.${this.value.name}`; - case "global": - return `${this.type.typeName}.${this.value.name}`; - } - } - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return this.fullName(); - } - nativize(): any { - return this.fullName(); - } - constructor(enumType: Types.EnumType, numeric: BN, name: string) { - this.type = enumType; - this.kind = "value"; - this.value = { name, numericAsBN: numeric }; - } - }; - - /* - * SECTION 5: CONTRACTS - */ - - //Contracts - export type ContractResult = ContractValue | Errors.ContractErrorResult; - - //Contract values have a special new type as their value: ContractValueInfo. - export class ContractValue { - type: Types.ContractType; - kind: "value"; - value: ContractValueInfo; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value, options); - } - nativize(): any { - return this.value.nativize(); - } - constructor(contractType: Types.ContractType, value: ContractValueInfo) { - this.type = contractType; - this.kind = "value"; - this.value = value; - } - } - - //There are two types -- one for contracts whose class we can identify, and one - //for when we can't identify the class. - export type ContractValueInfo = ContractValueInfoKnown | ContractValueInfoUnknown; - - //when we can identify the class - export class ContractValueInfoKnown { - kind: "known"; - address: string; //should be formatted as address - //NOT an AddressResult, note - rawAddress?: string; - class: Types.ContractType; - //may have more optional members defined later, but I'll leave these out for now - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(this.address, "number") + ` (${this.class.typeName})`; - } - nativize(): any { - return `${this.class.typeName}(${this.address})`; - } - constructor(address: string, contractClass: Types.ContractType, rawAddress?: string) { - this.kind = "known"; - this.address = address; - this.class = contractClass; - this.rawAddress = rawAddress; - } - } - - //when we can't - export class ContractValueInfoUnknown { - kind: "unknown"; - address: string; //should be formatted as address - //NOT an AddressResult, note - rawAddress?: string; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - debug("options: %O", options); - return options.stylize(this.address, "number") + " of unknown class"; - } - nativize(): any { - return this.address; - } - constructor(address: string, rawAddress?: string) { - this.kind = "unknown"; - this.address = address; - this.rawAddress = rawAddress; - } - } - - /* - * SECTION 6: External functions - */ - - //external functions - export type FunctionExternalResult = FunctionExternalValue | Errors.FunctionExternalErrorResult; - - export class FunctionExternalValue { - type: Types.FunctionTypeExternal; - kind: "value"; - value: FunctionExternalValueInfo; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value, options); - } - nativize(): any { - return this.value.nativize(); - } - constructor(functionType: Types.FunctionTypeExternal, value: FunctionExternalValueInfo) { - this.type = functionType; - this.kind = "value"; - this.value = value; - } - } - - //External function values come in 3 types: - export type FunctionExternalValueInfo = - FunctionExternalValueInfoKnown //known function of known class - | FunctionExternalValueInfoInvalid //known class, but can't locate function - | FunctionExternalValueInfoUnknown; //can't determine class - - //known function of known class - export class FunctionExternalValueInfoKnown { - kind: "known"; - contract: ContractValueInfoKnown; - selector: string; //formatted as a bytes4 - name: string; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - //first, let's inspect that contract, but w/o color - let contractString = util.inspect(this.contract, { ...cleanStylize(options), colors: false }); - let firstLine = `[Function: ${this.name} of`; - let secondLine = `${contractString}]`; - let breakingSpace = firstLine.length >= options.breakLength ? "\n" : " "; - //now, put it together - return options.stylize(firstLine + breakingSpace + secondLine, "special"); - } - nativize(): any { - return `${this.contract.nativize()}.${this.name}` - } - constructor(contract: ContractValueInfoKnown, selector: string, name: string) { - this.kind = "known"; - this.contract = contract; - this.selector = selector; - this.name = name; - } - } - - //known class but can't locate function - export class FunctionExternalValueInfoInvalid { - kind: "invalid"; - contract: ContractValueInfoKnown; - selector: string; //formatted as a bytes4 - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - //first, let's inspect that contract, but w/o color - let contractString = util.inspect(this.contract, { ...cleanStylize(options), colors: false }); - let selectorString = `Unknown selector 0x${this.selector}`; - let firstLine = `[Function: ${selectorString} of`; - let secondLine = `${contractString}]`; - let breakingSpace = firstLine.length >= options.breakLength ? "\n" : " "; - //now, put it together - return options.stylize(firstLine + breakingSpace + secondLine, "special"); - } - nativize(): any { - return `${this.contract.nativize()}.call(${this.selector}...)` - } - constructor(contract: ContractValueInfoKnown, selector: string) { - this.kind = "invalid"; - this.contract = contract; - this.selector = selector; - } - } - - //can't even locate class - export class FunctionExternalValueInfoUnknown { - kind: "unknown"; - contract: ContractValueInfoUnknown; - selector: string; //formatted as a bytes4 - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - //first, let's inspect that contract, but w/o color - let contractString = util.inspect(this.contract, { ...cleanStylize(options), colors: false }); - let selectorString = `Unknown selector 0x${this.selector}`; - let firstLine = `[Function: ${selectorString} of`; - let secondLine = `${contractString}]`; - let breakingSpace = firstLine.length >= options.breakLength ? "\n" : " "; - //now, put it together - return options.stylize(firstLine + breakingSpace + secondLine, "special"); - } - nativize(): any { - return `${this.contract.nativize()}.call(${this.selector}...)` - } - constructor(contract: ContractValueInfoUnknown, selector: string) { - this.kind = "unknown"; - this.contract = contract; - this.selector = selector; - } - } - - /* - * SECTION 7: INTERNAL FUNCTIONS - */ - - //Internal functions - export type FunctionInternalResult = FunctionInternalValue | Errors.FunctionInternalErrorResult; - - export class FunctionInternalValue { - type: Types.FunctionTypeInternal; - kind: "value"; - value: FunctionInternalValueInfo; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return util.inspect(this.value, options); - } - nativize(): any { - return this.value.nativize(); - } - constructor(functionType: Types.FunctionTypeInternal, value: FunctionInternalValueInfo) { - this.type = functionType; - this.kind = "value"; - this.value = value; - } - } - - //these also come in 3 types - export type FunctionInternalValueInfo = - FunctionInternalValueInfoKnown //actual function - | FunctionInternalValueInfoException //default value - | FunctionInternalValueInfoUnknown; //decoding not supported in this context - - //actual function - export class FunctionInternalValueInfoKnown { - kind: "function" - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - name: string; - definedIn: Types.ContractType; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(`[Function: ${this.definedIn.typeName}.${this.name}]`, "special"); - } - nativize(): any { - return `${this.definedIn.typeName}.${this.name}`; - } - constructor( - context: Types.ContractType, - deployedProgramCounter: number, - constructorProgramCounter: number, - name: string, - definedIn: Types.ContractType - ) { - this.kind = "function"; - this.context = context; - this.deployedProgramCounter = deployedProgramCounter; - this.constructorProgramCounter = constructorProgramCounter; - this.name = name; - this.definedIn = definedIn; - } - } - - //default value - export class FunctionInternalValueInfoException { - kind: "exception" - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return this.deployedProgramCounter === 0 - ? options.stylize(`[Function: ]`, "special") - : options.stylize(`[Function: assert(false)]`, "special"); - } - nativize(): any { - return this.deployedProgramCounter === 0 - ? `` - : `assert(false)`; - } - constructor( - context: Types.ContractType, - deployedProgramCounter: number, - constructorProgramCounter: number - ) { - this.kind = "exception"; - this.context = context; - this.deployedProgramCounter = deployedProgramCounter; - this.constructorProgramCounter = constructorProgramCounter; - } - } - - //value returned to indicate that decoding is not supported outside the debugger - export class FunctionInternalValueInfoUnknown { - kind: "unknown" - context: Types.ContractType; - deployedProgramCounter: number; - constructorProgramCounter: number; - [util.inspect.custom](depth: number | null, options: InspectOptions): string { - return options.stylize(`[Function: decoding not supported (raw info: deployed PC=${this.deployedProgramCounter}, constructor PC=${this.constructorProgramCounter})]`, "special"); - } - nativize(): any { - return ``; - } - constructor( - context: Types.ContractType, - deployedProgramCounter: number, - constructorProgramCounter: number - ) { - this.kind = "unknown"; - this.context = context; - this.deployedProgramCounter = deployedProgramCounter; - this.constructorProgramCounter = constructorProgramCounter; - } - } - -} diff --git a/packages/truffle-decoder-core/lib/allocate/calldata.ts b/packages/truffle-decoder-core/lib/allocate/calldata.ts deleted file mode 100644 index 43ae0c853ab..00000000000 --- a/packages/truffle-decoder-core/lib/allocate/calldata.ts +++ /dev/null @@ -1,231 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:allocate:calldata"); - -import { CalldataPointer } from "../types/pointer"; -import { CalldataAllocations, CalldataAllocation, CalldataMemberAllocation } from "../types/allocation"; -import { AstDefinition, AstReferences } from "truffle-decode-utils"; -import * as DecodeUtils from "truffle-decode-utils"; - -interface CalldataAllocationInfo { - size?: number; //left out for types that don't go in calldata - dynamic?: boolean; //similarly - allocations: CalldataAllocations; -} - -export function getCalldataAllocations(referenceDeclarations: AstReferences): CalldataAllocations { - let allocations: CalldataAllocations = {}; - for(const node of Object.values(referenceDeclarations)) { - if(node.nodeType === "StructDefinition") { - allocations = allocateStruct(node, referenceDeclarations, allocations); - } - } - return allocations; -} - -//note: we will still allocate circular structs, even though they're not allowed in calldata, because it's -//not worth the effort to detect them. However on mappings or internal functions, we'll vomit (allocate null) -function allocateStruct(definition: AstDefinition, referenceDeclarations: AstReferences, existingAllocations: CalldataAllocations): CalldataAllocations { - let start: number = 0; - let dynamic: boolean = false; - - //don't allocate things that have already been allocated - if(definition.id in existingAllocations) { - return existingAllocations; - } - - let allocations = {...existingAllocations}; //otherwise, we'll be adding to this, so we better clone - - let memberAllocations: CalldataMemberAllocation[] = []; - - for(const member of definition.members) - { - let length: number; - let dynamicMember: boolean; - ({size: length, dynamic: dynamicMember, allocations} = calldataSizeAndAllocate(member, referenceDeclarations, allocations)); - - //vomit on illegal types in calldata -- note the short-circuit! - if(length === undefined) { - allocations[definition.id] = null; - return allocations; - } - - let pointer: CalldataPointer = { - calldata: { - start, - length - } - }; - - memberAllocations.push({ - definition: member, - pointer - }); - - start += length; - dynamic = dynamic || dynamicMember; - } - - allocations[definition.id] = { - definition, - members: memberAllocations, - length: dynamic ? DecodeUtils.EVM.WORD_SIZE : start, - dynamic - }; - - return allocations; -} - -function calldataSizeAndAllocate(definition: AstDefinition, referenceDeclarations?: AstReferences, existingAllocations?: CalldataAllocations): CalldataAllocationInfo { - switch (DecodeUtils.Definition.typeClass(definition)) { - case "bool": - case "address": - case "contract": - case "int": - case "uint": - case "fixed": - case "ufixed": - case "enum": - return { - size: DecodeUtils.EVM.WORD_SIZE, - dynamic: false, - allocations: existingAllocations - }; - - case "string": - return { - size: DecodeUtils.EVM.WORD_SIZE, - dynamic: true, - allocations: existingAllocations - }; - - case "bytes": - return { - size: DecodeUtils.EVM.WORD_SIZE, - dynamic: DecodeUtils.Definition.specifiedSize(definition) == null, - allocations: existingAllocations - }; - - case "mapping": - return { - allocations: existingAllocations - }; - - case "function": - switch (DecodeUtils.Definition.visibility(definition)) { - case "external": - return { - size: DecodeUtils.EVM.WORD_SIZE, - dynamic: false, - allocations: existingAllocations - }; - case "internal": - return { - allocations: existingAllocations - }; - } - - case "array": { - if(DecodeUtils.Definition.isDynamicArray(definition)) { - return { - size: DecodeUtils.EVM.WORD_SIZE, - dynamic: true, - allocations: existingAllocations - }; - } - else { - //static array case - const length: number = DecodeUtils.Definition.staticLength(definition); - if(length === 0) { - //arrays of length 0 are static regardless of base type - return { - size: 0, - dynamic: false, - allocations: existingAllocations - }; - } - const baseDefinition: AstDefinition = definition.baseType || definition.typeName.baseType; - const {size: baseSize, dynamic, allocations} = calldataSizeAndAllocate(baseDefinition, referenceDeclarations, existingAllocations); - return { - size: length * baseSize, - dynamic, - allocations - }; - } - } - - case "struct": { - const referenceId: number = DecodeUtils.Definition.typeId(definition); - let allocations: CalldataAllocations = existingAllocations; - let allocation: CalldataAllocation | null | undefined = allocations[referenceId]; - if(allocation === undefined) { - //if we don't find an allocation, we'll have to do the allocation ourselves - const referenceDeclaration: AstDefinition = referenceDeclarations[referenceId]; - allocations = allocateStruct(referenceDeclaration, referenceDeclarations, existingAllocations); - allocation = allocations[referenceId]; - } - //having found our allocation, if it's not null, we can just look up its size and dynamicity - if(allocation !== null) { - return { - size: allocation.length, - dynamic: allocation.dynamic, - allocations - }; - } - //if it is null, this type doesn't go in calldata - else { - return { - allocations - }; - } - } - } -} - -//like calldataSize, but for a Type object; also assumes you've already done allocation -//(note: function for dynamic is separate, see below) -//also, does not attempt to handle types that don't occur in calldata -export function calldataSizeForType(dataType: DecodeUtils.Types.Type, allocations: CalldataAllocations): number { - switch(dataType.typeClass) { - case "array": - switch(dataType.kind) { - case "dynamic": - return DecodeUtils.EVM.WORD_SIZE; - case "static": - const length = dataType.length.toNumber(); //if this is too big, we have a problem! - const baseSize = calldataSizeForType(dataType.baseType, allocations); - return length * baseSize; - } - case "struct": - const allocation = allocations[dataType.id]; - if(!allocation) { - throw new DecodeUtils.Errors.DecodingError( - new DecodeUtils.Errors.UserDefinedTypeNotFoundError(dataType) - ); - } - return allocation.length; - default: - return DecodeUtils.EVM.WORD_SIZE; - } -} - -//again, this function does not attempt to handle types that don't occur in calldata -export function isTypeDynamic(dataType: DecodeUtils.Types.Type, allocations: CalldataAllocations): boolean { - switch(dataType.typeClass) { - case "string": - return true; - case "bytes": - return dataType.kind === "dynamic"; - case "array": - return dataType.kind === "dynamic" || (dataType.length.gtn(0) && isTypeDynamic(dataType.baseType, allocations)); - case "struct": - const allocation = allocations[dataType.id]; - if(!allocation) { - throw new DecodeUtils.Errors.DecodingError( - new DecodeUtils.Errors.UserDefinedTypeNotFoundError(dataType) - ); - } - return allocation.dynamic; - default: - return false; - } -} diff --git a/packages/truffle-decoder-core/lib/decode/calldata.ts b/packages/truffle-decoder-core/lib/decode/calldata.ts deleted file mode 100644 index 8c68fe60792..00000000000 --- a/packages/truffle-decoder-core/lib/decode/calldata.ts +++ /dev/null @@ -1,235 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:calldata"); - -import read from "../read"; -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; -import decodeValue from "./value"; -import { CalldataPointer, DataPointer } from "../types/pointer"; -import { CalldataMemberAllocation } from "../types/allocation"; -import { calldataSizeForType, isTypeDynamic } from "../allocate/calldata"; -import { EvmInfo } from "../types/evm"; -import { DecoderRequest, GeneratorJunk } from "../types/request"; - -export default function* decodeCalldata(dataType: Types.Type, pointer: CalldataPointer, info: EvmInfo, base: number = 0): IterableIterator { - if(Types.isReferenceType(dataType)) { - let dynamic: boolean; - try { - dynamic = isTypeDynamic(dataType, info.calldataAllocations); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - if(dynamic) { - return yield* decodeCalldataReferenceByAddress(dataType, pointer, info, base); - } - else { - return yield* decodeCalldataReferenceStatic(dataType, pointer, info); - } - } - else { - debug("pointer %o", pointer); - return yield* decodeValue(dataType, pointer, info); - } -} - -export function* decodeCalldataReferenceByAddress(dataType: Types.ReferenceType, pointer: DataPointer, info: EvmInfo, base: number = 0): IterableIterator { - const { state } = info; - debug("pointer %o", pointer); - let rawValue: Uint8Array; - try { - rawValue = yield* read(pointer, state); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - - let startPosition = DecodeUtils.Conversion.toBN(rawValue).toNumber() + base; - debug("startPosition %d", startPosition); - - let dynamic: boolean; - try { - dynamic = isTypeDynamic(dataType, info.calldataAllocations); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - if(!dynamic) { //this will only come up when called from stack.ts - let size: number; - try { - size = calldataSizeForType(dataType, info.calldataAllocations); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - let staticPointer = { - calldata: { - start: startPosition, - length: size - } - } - return yield* decodeCalldataReferenceStatic(dataType, staticPointer, info); - } - let length: number; - let rawLength: Uint8Array; - switch (dataType.typeClass) { - - case "bytes": - case "string": - //initial word contains length - try { - rawLength = (yield* read({ - calldata: { - start: startPosition, - length: DecodeUtils.EVM.WORD_SIZE - } - }, state)); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - length = DecodeUtils.Conversion.toBN(rawLength).toNumber(); - - let childPointer: CalldataPointer = { - calldata: { start: startPosition + DecodeUtils.EVM.WORD_SIZE, length } - } - - return yield* decodeValue(dataType, childPointer, info); - - case "array": - - switch(dataType.kind) { - case "dynamic": - //initial word contains array length - try { - rawLength = (yield* read({ - calldata: { - start: startPosition, - length: DecodeUtils.EVM.WORD_SIZE - } - }, state)); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - length = DecodeUtils.Conversion.toBN(rawLength).toNumber(); - startPosition += DecodeUtils.EVM.WORD_SIZE; //increment startPosition - //to next word, as first word was used for length - break; - case "static": - length = dataType.length.toNumber(); - break; - } - - //note: I've written this fairly generically, but it is worth noting that - //since this array is of dynamic type, we know that if it's static length - //then size must be EVM.WORD_SIZE - - let baseSize: number; - try { - baseSize = calldataSizeForType(dataType.baseType, info.calldataAllocations); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - - let decodedChildren: Values.Result[] = []; - for(let index = 0; index < length; index++) { - decodedChildren.push( - (yield* decodeCalldata( - dataType.baseType, - { calldata: { - start: startPosition + index * baseSize, - length: baseSize - }}, - info, startPosition - )) - ); //pointer base is always start of list, never the length - } - return new Values.ArrayValue(dataType, decodedChildren); - - case "struct": - return yield* decodeCalldataStructByPosition(dataType, startPosition, info); - } -} - -export function* decodeCalldataReferenceStatic(dataType: Types.ReferenceType, pointer: CalldataPointer, info: EvmInfo): IterableIterator { - debug("static"); - debug("pointer %o", pointer); - - switch (dataType.typeClass) { - case "array": - - //we're in the static case, so we know the array must be statically sized - const length = (dataType).length.toNumber(); - let baseSize: number; - try { - baseSize = calldataSizeForType(dataType.baseType, info.calldataAllocations); - } - catch(error) { //error: Errors.DecodingError - return Errors.makeGenericErrorResult(dataType, error.error); - } - - let decodedChildren: Values.Result[] = []; - for(let index = 0; index < length; index++) { - decodedChildren.push( - (yield* decodeCalldata( - dataType.baseType, - { calldata: { - start: pointer.calldata.start + index * baseSize, - length: baseSize - }}, - info - )) - ); //static case so don't need base - } - return new Values.ArrayValue(dataType, decodedChildren); - - case "struct": - return yield* decodeCalldataStructByPosition(dataType, pointer.calldata.start, info); - } -} - -//note that this function takes the start position as a *number*; it does not take a calldata pointer -function* decodeCalldataStructByPosition(dataType: Types.StructType, startPosition: number, info: EvmInfo): IterableIterator { - const { userDefinedTypes, calldataAllocations } = info; - - const typeId = dataType.id; - const structAllocation = calldataAllocations[typeId]; - if(!structAllocation) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); - } - - let decodedMembers: Values.NameValuePair[] = []; - for(let index = 0; index < structAllocation.members.length; index++) { - const memberAllocation = structAllocation.members[index]; - const memberPointer = memberAllocation.pointer; - const childPointer: CalldataPointer = { - calldata: { - start: startPosition + memberPointer.calldata.start, - length: memberPointer.calldata.length - } - }; - - let memberName = memberAllocation.definition.name; - let storedType = userDefinedTypes[typeId]; - if(!storedType) { - return new Errors.StructErrorResult( - dataType, - new Errors.UserDefinedTypeNotFoundError(dataType) - ); - } - let storedMemberType = storedType.memberTypes[index].type; - let memberType = Types.specifyLocation(storedMemberType, "calldata"); - - decodedMembers.push({ - name: memberName, - value: (yield* decodeCalldata(memberType, childPointer, info, startPosition)) - //note that startPosition is only needed in the dynamic case, but we don't know which case we're in - }); - } - return new Values.StructValue(dataType, decodedMembers); -} diff --git a/packages/truffle-decoder-core/lib/decode/index.ts b/packages/truffle-decoder-core/lib/decode/index.ts deleted file mode 100644 index 6dd842d700f..00000000000 --- a/packages/truffle-decoder-core/lib/decode/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:decode"); - -import decodeValue from "./value"; -import decodeMemory from "./memory"; -import decodeStorage from "./storage"; -import decodeStack from "./stack"; -import { decodeLiteral } from "./stack"; -import decodeCalldata from "./calldata"; -import decodeConstant from "./constant"; -import decodeSpecial from "./special"; -import { AstDefinition, Types, Values } from "truffle-decode-utils"; -import * as Pointer from "../types/pointer"; -import { EvmInfo } from "../types/evm"; -import { DecoderRequest, GeneratorJunk } from "../types/request"; - -export default function* decode(definition: AstDefinition, pointer: Pointer.DataPointer, info: EvmInfo): IterableIterator { - let compiler = info.currentContext.compiler; - let dataType = Types.definitionToType(definition, compiler); - debug("definition %O", definition); - return yield* decodePointer(dataType, pointer, info); -} - -export function* decodePointer(dataType: Types.Type, pointer: Pointer.DataPointer, info: EvmInfo): IterableIterator { - debug("type %O", dataType); - debug("pointer %O", pointer); - - if(Pointer.isStoragePointer(pointer)) { - return yield* decodeStorage(dataType, pointer, info) - } - - if(Pointer.isStackPointer(pointer)) { - return yield* decodeStack(dataType, pointer, info); - } - - if (Pointer.isStackLiteralPointer(pointer)) { - return yield* decodeLiteral(dataType, pointer, info); - } - - if(Pointer.isConstantDefinitionPointer(pointer)) { - return yield* decodeConstant(dataType, pointer, info); - //I'd like to just use decodeValue, but unfortunately there are some special - //cases to deal with - } - - if(Pointer.isSpecialPointer(pointer)) { - return yield* decodeSpecial(dataType, pointer, info); - } - - //NOTE: the following two cases shouldn't come up but they've been left in as - //fallback cases - - if(Pointer.isMemoryPointer(pointer)) { - return yield* decodeMemory(dataType, pointer, info); - } - - if(Pointer.isCalldataPointer(pointer)) { - return yield* decodeCalldata(dataType, pointer, info); - } -} diff --git a/packages/truffle-decoder-core/lib/decode/special.ts b/packages/truffle-decoder-core/lib/decode/special.ts deleted file mode 100644 index 6f359385c7b..00000000000 --- a/packages/truffle-decoder-core/lib/decode/special.ts +++ /dev/null @@ -1,114 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:special"); - -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values } from "truffle-decode-utils"; -import decodeValue from "./value"; -import { EvmInfo } from "../types/evm"; -import { SpecialPointer } from "../types/pointer"; -import { DecoderRequest, GeneratorJunk } from "../types/request"; - -export default function* decodeSpecial(dataType: Types.Type, pointer: SpecialPointer, info: EvmInfo): IterableIterator { - if(dataType.typeClass === "magic") { - return yield* decodeMagic(dataType, pointer, info); - } - else { - return yield* decodeValue(dataType, pointer, info); - } -} - -export function* decodeMagic(dataType: Types.MagicType, pointer: SpecialPointer, info: EvmInfo): IterableIterator { - //note: that's Values.Result and not Values.MagicResult due to some TypeScript generator jank - - let {state} = info; - - switch(pointer.special) { - case "msg": - return new Values.MagicValue(dataType, { - data: (yield* decodeValue( - { - typeClass: "bytes", - kind: "dynamic", - location: "calldata" - }, - {calldata: { - start: 0, - length: state.calldata.length - }}, - info - )), - sig: (yield* decodeValue( - { - typeClass: "bytes", - kind: "static", - length: DecodeUtils.EVM.SELECTOR_SIZE - }, - {calldata: { - start: 0, - length: DecodeUtils.EVM.SELECTOR_SIZE, - }}, - info - )), - sender: (yield* decodeValue( - { - typeClass: "address", - payable: true - }, - {special: "sender"}, - info - )), - value: (yield* decodeValue( - { - typeClass: "uint", - bits: 256 - }, - {special: "value"}, - info - )) - }); - case "tx": - return new Values.MagicValue(dataType, { - origin: (yield* decodeValue( - { - typeClass: "address", - payable: true - }, - {special: "origin"}, - info - )), - gasprice: (yield* decodeValue( - { - typeClass: "uint", - bits: 256 - }, - {special: "gasprice"}, - info - )) - }); - case "block": - let block: {[field: string]: Values.Result} = { - coinbase: (yield* decodeValue( - { - typeClass: "address", - payable: true - }, - {special: "coinbase"}, - info - )) - }; - //the other ones are all uint's, so let's handle them all at once; due to - //the lack of generator arrow functions, we do it by mutating block - const variables = ["difficulty", "gaslimit", "number", "timestamp"]; - for (let variable of variables) { - block[variable] = (yield* decodeValue( - { - typeClass: "uint", - bits: 256 - }, - {special: variable}, - info - )); - } - return new Values.MagicValue(dataType, block); - } -} diff --git a/packages/truffle-decoder-core/lib/decode/value.ts b/packages/truffle-decoder-core/lib/decode/value.ts deleted file mode 100644 index 06408ee2207..00000000000 --- a/packages/truffle-decoder-core/lib/decode/value.ts +++ /dev/null @@ -1,370 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:decode:value"); - -import read from "../read"; -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values, Errors } from "truffle-decode-utils"; -import BN from "bn.js"; -import utf8 from "utf8"; -import { DataPointer } from "../types/pointer"; -import { EvmInfo, DecoderOptions, DefaultDecoderOptions } from "../types/evm"; -import { DecoderRequest, GeneratorJunk } from "../types/request"; - -export default function* decodeValue(dataType: Types.Type, pointer: DataPointer, info: EvmInfo, options: DecoderOptions = DefaultDecoderOptions): IterableIterator { - //NOTE: this does not actually return a Uint8Aarray, but due to the use of yield* read, - //we have to include it in the type :-/ - const { state } = info; - const { permissivePadding } = options; - - let bytes: Uint8Array; - let rawBytes: Uint8Array; - try { - bytes = yield* read(pointer, state); - } - catch(error) { //error: Errors.DecodingError - debug("segfault, pointer %o, state: %O", pointer, state); - return Errors.makeGenericErrorResult(dataType, error.error); - } - rawBytes = bytes; - - debug("type %O", dataType); - debug("pointer %o", pointer); - - switch(dataType.typeClass) { - - case "bool": { - if(!checkPaddingLeft(bytes, 1)) { - return new Errors.BoolErrorResult( - dataType, - new Errors.BoolPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - const numeric = DecodeUtils.Conversion.toBN(bytes); - if(numeric.eqn(0)) { - return new Values.BoolValue(dataType, false); - } - else if(numeric.eqn(1)) { - return new Values.BoolValue(dataType, true); - } - else { - return new Errors.BoolErrorResult( - dataType, - new Errors.BoolOutOfRangeError(numeric) - ); - } - } - - case "uint": - //first, check padding (if needed) - if(!permissivePadding && !checkPaddingLeft(bytes, dataType.bits/8)) { - return new Errors.UintErrorResult( - dataType, - new Errors.UintPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - //now, truncate to appropriate length (keeping the bytes on the right) - bytes = bytes.slice(-dataType.bits/8); - return new Values.UintValue( - dataType, - DecodeUtils.Conversion.toBN(bytes), - DecodeUtils.Conversion.toBN(rawBytes) - ); - case "int": - //first, check padding (if needed) - if(!permissivePadding && !checkPaddingSigned(bytes, dataType.bits/8)) { - return new Errors.IntErrorResult( - dataType, - new Errors.IntPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - //now, truncate to appropriate length (keeping the bytes on the right) - bytes = bytes.slice(-dataType.bits/8); - return new Values.IntValue( - dataType, - DecodeUtils.Conversion.toSignedBN(bytes), - DecodeUtils.Conversion.toSignedBN(rawBytes) - ); - - case "address": - if(!permissivePadding && !checkPaddingLeft(bytes, DecodeUtils.EVM.ADDRESS_SIZE)) { - return new Errors.AddressErrorResult( - dataType, - new Errors.AddressPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - return new Values.AddressValue( - dataType, - DecodeUtils.Conversion.toAddress(bytes), - DecodeUtils.Conversion.toHexString(rawBytes) - ); - - case "contract": - if(!permissivePadding && !checkPaddingLeft(bytes, DecodeUtils.EVM.ADDRESS_SIZE)) { - return new Errors.ContractErrorResult( - dataType, - new Errors.ContractPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - const fullType = Types.fullType(dataType, info.userDefinedTypes); - const contractValueInfo = (yield* decodeContract(bytes, info)); - return new Values.ContractValue(fullType, contractValueInfo); - - case "bytes": - switch(dataType.kind) { - case "static": - //first, check padding (if needed) - if(!permissivePadding && !checkPaddingRight(bytes, dataType.length)) { - return new Errors.BytesStaticErrorResult( - dataType, - new Errors.BytesPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - //now, truncate to appropriate length - bytes = bytes.slice(0, dataType.length); - return new Values.BytesStaticValue( - dataType, - DecodeUtils.Conversion.toHexString(bytes), - DecodeUtils.Conversion.toHexString(rawBytes) - ); - case "dynamic": - //no need to check padding here - return new Values.BytesDynamicValue(dataType, DecodeUtils.Conversion.toHexString(bytes)); - } - - case "string": - //there is no padding check for strings - return new Values.StringValue(dataType, decodeString(bytes)); - - case "function": - switch(dataType.visibility) { - case "external": - if(!checkPaddingRight(bytes, DecodeUtils.EVM.ADDRESS_SIZE + DecodeUtils.EVM.SELECTOR_SIZE)) { - return new Errors.FunctionExternalErrorResult( - dataType, - new Errors.FunctionExternalNonStackPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - const address = bytes.slice(0, DecodeUtils.EVM.ADDRESS_SIZE); - const selector = bytes.slice(DecodeUtils.EVM.ADDRESS_SIZE, DecodeUtils.EVM.ADDRESS_SIZE + DecodeUtils.EVM.SELECTOR_SIZE); - return new Values.FunctionExternalValue(dataType, - (yield* decodeExternalFunction(address, selector, info)) - ); - case "internal": - if(!checkPaddingLeft(bytes, 2 * DecodeUtils.EVM.PC_SIZE)) { - return new Errors.FunctionInternalErrorResult( - dataType, - new Errors.FunctionInternalPaddingError(DecodeUtils.Conversion.toHexString(bytes)) - ); - } - const deployedPc = bytes.slice(-DecodeUtils.EVM.PC_SIZE); - const constructorPc = bytes.slice(-DecodeUtils.EVM.PC_SIZE * 2, -DecodeUtils.EVM.PC_SIZE); - return decodeInternalFunction(dataType, deployedPc, constructorPc, info); - } - break; //to satisfy TypeScript - - case "enum": { - const numeric = DecodeUtils.Conversion.toBN(bytes); - const fullType = Types.fullType(dataType, info.userDefinedTypes); - if(!fullType.options) { - return new Errors.EnumErrorResult( - fullType, - new Errors.EnumNotFoundDecodingError(fullType, numeric) - ); - } - const numOptions = fullType.options.length; - const numBytes = Math.ceil(Math.log2(numOptions) / 8); - if(!checkPaddingLeft(bytes, numBytes)) { - return new Errors.EnumErrorResult( - fullType, - new Errors.EnumPaddingError(fullType, DecodeUtils.Conversion.toHexString(bytes)) - ); - } - if(numeric.ltn(numOptions)) { - const name = fullType.options[numeric.toNumber()]; - return new Values.EnumValue(fullType, numeric, name); - } - else { - return new Errors.EnumErrorResult( - fullType, - new Errors.EnumOutOfRangeError(fullType, numeric) - ); - } - } - - case "fixed": { - //skipping padding check as we don't support this anyway - const hex = DecodeUtils.Conversion.toHexString(bytes); - return new Errors.FixedErrorResult( - dataType, - new Errors.FixedPointNotYetSupportedError(hex) - ); - } - case "ufixed": { - //skipping padding check as we don't support this anyway - const hex = DecodeUtils.Conversion.toHexString(bytes); - return new Errors.UfixedErrorResult( - dataType, - new Errors.FixedPointNotYetSupportedError(hex) - ); - } - } -} - -export function decodeString(bytes: Uint8Array): Values.StringValueInfo { - //the following line takes our UTF-8 string... and interprets each byte - //as a UTF-16 bytepair. Yikes! Fortunately, we have a library to repair that. - let badlyEncodedString = String.fromCharCode.apply(undefined, bytes); - try { - //this will throw an error if we have malformed UTF-8 - let correctlyEncodedString = utf8.decode(badlyEncodedString); - return new Values.StringValueInfoValid(correctlyEncodedString); - } - catch(error) { - //we're going to ignore the precise error and just assume it's because - //the string was malformed (what else could it be?) - let hexString = DecodeUtils.Conversion.toHexString(bytes); - return new Values.StringValueInfoMalformed(hexString); - } -} - -//NOTE that this function returns a ContractValueInfo, not a ContractResult -export function* decodeContract(addressBytes: Uint8Array, info: EvmInfo): IterableIterator { - let address = DecodeUtils.Conversion.toAddress(addressBytes); - let rawAddress = DecodeUtils.Conversion.toHexString(addressBytes); - let codeBytes: Uint8Array = yield { - type: "code", - address - }; - let code = DecodeUtils.Conversion.toHexString(codeBytes); - let context = DecodeUtils.Contexts.findDecoderContext(info.contexts, code); - if(context !== null && context.contractName !== undefined) { - return new Values.ContractValueInfoKnown( - address, - { - typeClass: "contract", - kind: "native", - id: context.contractId, - typeName: context.contractName, - contractKind: context.contractKind, - payable: context.payable - }, - rawAddress); - } - else { - return new Values.ContractValueInfoUnknown(address, rawAddress); - } -} - -//note: address can have extra zeroes on the left like elsewhere, but selector should be exactly 4 bytes -//NOTE this again returns a FunctionExternalValueInfo, not a FunctionExternalResult -export function* decodeExternalFunction(addressBytes: Uint8Array, selectorBytes: Uint8Array, info: EvmInfo): IterableIterator { - let contract = (yield* decodeContract(addressBytes, info)); - let selector = DecodeUtils.Conversion.toHexString(selectorBytes); - if(contract.kind === "unknown") { - return new Values.FunctionExternalValueInfoUnknown(contract, selector) - } - let contractId = ( contract.class).id; //sorry! will be fixed soon! - let context = Object.values(info.contexts).find( - context => context.contractId === contractId - ); - let abiEntry = context.abi !== undefined - ? context.abi[selector] - : undefined; - if(abiEntry === undefined) { - return new Values.FunctionExternalValueInfoInvalid(contract, selector) - } - let functionName = abiEntry.name; - return new Values.FunctionExternalValueInfoKnown(contract, selector, functionName) -} - -//this one works a bit differently -- in order to handle errors, it *does* return a FunctionInternalResult -export function decodeInternalFunction(dataType: Types.FunctionTypeInternal, deployedPcBytes: Uint8Array, constructorPcBytes: Uint8Array, info: EvmInfo): Values.FunctionInternalResult { - let deployedPc: number = DecodeUtils.Conversion.toBN(deployedPcBytes).toNumber(); - let constructorPc: number = DecodeUtils.Conversion.toBN(constructorPcBytes).toNumber(); - let context: Types.ContractType = { - typeClass: "contract", - kind: "native", - id: info.currentContext.contractId, - typeName: info.currentContext.contractName, - contractKind: info.currentContext.contractKind, - payable: info.currentContext.payable - }; - //before anything else: do we even have an internal functions table? - //if not, we'll just return the info we have without really attemting to decode - if(!info.internalFunctionsTable) { - return new Values.FunctionInternalValue( - dataType, - new Values.FunctionInternalValueInfoUnknown(context, deployedPc, constructorPc) - ); - } - //also before we continue: is the PC zero? if so let's just return that - if(deployedPc === 0 && constructorPc === 0) { - return new Values.FunctionInternalValue( - dataType, - new Values.FunctionInternalValueInfoException(context, deployedPc, constructorPc) - ); - } - //another check: is only the deployed PC zero? - if(deployedPc === 0 && constructorPc !== 0) { - return new Errors.FunctionInternalErrorResult( - dataType, - new Errors.MalformedInternalFunctionError(context, constructorPc) - ); - } - //one last pre-check: is this a deployed-format pointer in a constructor? - if(info.currentContext.isConstructor && constructorPc === 0) { - return new Errors.FunctionInternalErrorResult( - dataType, - new Errors.DeployedFunctionInConstructorError(context, deployedPc) - ); - } - //otherwise, we get our function - let pc = info.currentContext.isConstructor - ? constructorPc - : deployedPc; - let functionEntry = info.internalFunctionsTable[pc]; - if(!functionEntry) { - //if it's not zero and there's no entry... error! - return new Errors.FunctionInternalErrorResult( - dataType, - new Errors.NoSuchInternalFunctionError(context, deployedPc, constructorPc) - ); - } - if(functionEntry.isDesignatedInvalid) { - return new Values.FunctionInternalValue( - dataType, - new Values.FunctionInternalValueInfoException(context, deployedPc, constructorPc) - ); - } - let name = functionEntry.name; - let definedIn: Types.ContractType = { - typeClass: "contract", - kind: "native", - id: functionEntry.contractId, - typeName: functionEntry.contractName, - contractKind: functionEntry.contractKind, - payable: functionEntry.contractPayable - }; - return new Values.FunctionInternalValue( - dataType, - new Values.FunctionInternalValueInfoKnown(context, deployedPc, constructorPc, name, definedIn) - ); -} - -function checkPaddingRight(bytes: Uint8Array, length: number): boolean { - let padding = bytes.slice(length); //cut off the first length bytes - return padding.every(paddingByte => paddingByte === 0); -} - -//exporting this one for use in stack.ts -export function checkPaddingLeft(bytes: Uint8Array, length: number): boolean { - let padding = bytes.slice(0, -length); //cut off the last length bytes - return padding.every(paddingByte => paddingByte === 0); -} - -function checkPaddingSigned(bytes: Uint8Array, length: number): boolean { - let padding = bytes.slice(0, -length); //padding is all but the last length bytes - let value = bytes.slice(-length); //meanwhile the actual value is those last length bytes - let signByte = value[0] & 0x80 ? 0xff : 0x00; - return padding.every(paddingByte => paddingByte === signByte); -} diff --git a/packages/truffle-decoder-core/lib/interface/index.ts b/packages/truffle-decoder-core/lib/interface/index.ts deleted file mode 100644 index 8ef2b66fb27..00000000000 --- a/packages/truffle-decoder-core/lib/interface/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AstDefinition, Values } from "truffle-decode-utils"; -import { DataPointer } from "../types/pointer"; -import { EvmInfo } from "../types/evm"; -import decode from "../decode"; -import { DecoderRequest, GeneratorJunk } from "../types/request"; - -export { getStorageAllocations, storageSize } from "../allocate/storage"; -export { getCalldataAllocations } from "../allocate/calldata"; -export { getMemoryAllocations } from "../allocate/memory"; -export { readStack } from "../read/stack"; -export { slotAddress } from "../read/storage"; -export { StoragePointer, isStoragePointer } from "../types/pointer"; -export { StorageAllocations, StorageMemberAllocation } from "../types/allocation"; -export { Slot, isWordsLength, equalSlots } from "../types/storage"; -export { DecoderRequest, isStorageRequest, isCodeRequest } from "../types/request"; -export { EvmInfo } from "../types/evm"; - -export function* forEvmState(definition: AstDefinition, pointer: DataPointer, info: EvmInfo): IterableIterator { - return yield* decode(definition, pointer, info); -} diff --git a/packages/truffle-decoder-core/lib/read/constant.ts b/packages/truffle-decoder-core/lib/read/constant.ts deleted file mode 100644 index ae4a4a3fd91..00000000000 --- a/packages/truffle-decoder-core/lib/read/constant.ts +++ /dev/null @@ -1,30 +0,0 @@ -import debugModule from "debug"; -const debug = debugModule("decoder-core:read:constant"); - -import * as DecodeUtils from "truffle-decode-utils"; -import BN from "bn.js"; -import { Errors } from "truffle-decode-utils"; - -export function readDefinition(definition: DecodeUtils.AstDefinition): Uint8Array { - - debug("definition %o", definition); - - switch(DecodeUtils.Definition.typeClass(definition)) - { - case "rational": - let numericalValue: BN = DecodeUtils.Definition.rationalValue(definition); - return DecodeUtils.Conversion.toBytes(numericalValue, DecodeUtils.EVM.WORD_SIZE); - //you may be wondering, why do we not just use definition.value here, - //like we do below? answer: because if this isn't a literal, that may not - //exist - case "stringliteral": - return DecodeUtils.Conversion.toBytes(definition.hexValue); - default: - //unfortunately, other types of constants are just too complicated to - //handle right now. sorry. - debug("unsupported constant definition type"); - throw new Errors.DecodingError( - new Errors.UnsupportedConstantError(definition) - ); - } -} diff --git a/packages/truffle-decoder-core/lib/read/index.ts b/packages/truffle-decoder-core/lib/read/index.ts deleted file mode 100644 index 5123e374558..00000000000 --- a/packages/truffle-decoder-core/lib/read/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as storage from "./storage"; -import * as memory from "./memory"; -import * as stack from "./stack"; -import * as constant from "./constant"; -import * as Pointer from "../types/pointer"; -import { EvmState } from "../types/evm"; -import { DecoderRequest } from "../types/request"; - -export default function* read(pointer: Pointer.DataPointer, state: EvmState): IterableIterator { - if (Pointer.isStackPointer(pointer) && state.stack) { - return stack.readStack(state.stack, pointer.stack.from, pointer.stack.to); - } else if (Pointer.isStoragePointer(pointer) && state.storage) { - return yield* storage.readRange(state.storage, pointer.storage); - } else if (Pointer.isMemoryPointer(pointer) && state.memory) { - return memory.readBytes(state.memory, pointer.memory.start, pointer.memory.length); - } else if (Pointer.isCalldataPointer(pointer) && state.calldata) { - return memory.readBytes(state.calldata, pointer.calldata.start, pointer.calldata.length); - //there is no need for a separate calldata read function; the existing memory read function - //will do fine - } else if (Pointer.isStackLiteralPointer(pointer)) { - return pointer.literal; - } else if (Pointer.isConstantDefinitionPointer(pointer)) { - return constant.readDefinition(pointer.definition); - } else if (Pointer.isSpecialPointer(pointer)) { - return state.specials[pointer.special]; - } -} diff --git a/packages/truffle-decoder-core/lib/types/allocation.ts b/packages/truffle-decoder-core/lib/types/allocation.ts deleted file mode 100644 index 41d263c2f62..00000000000 --- a/packages/truffle-decoder-core/lib/types/allocation.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { StorageLength } from "./storage"; -import { StoragePointer, ConstantDefinitionPointer, CalldataPointer, MemoryPointer } from "./pointer"; -import { AstDefinition } from "truffle-decode-utils"; - -//holds a collection of storage allocations for structs and contracts, indexed -//by the ID of the struct or contract -export interface StorageAllocations { - [id: number]: StorageAllocation -} - -//an individual storage allocation for (the members of) a struct or (the state -//variables of) a contract -export interface StorageAllocation { - definition: AstDefinition; - size?: StorageLength; //only used for structs - members: StorageMemberAllocation[]; -} - -//an individual storage reference for a member of a struct or a state variable -//of a contract -export interface StorageMemberAllocation { - definition: AstDefinition; - pointer: StoragePointer | ConstantDefinitionPointer; -} - -//calldata types below work similar to storage types above; note these are only -//used for structs so there's no need to account for contracts or constants -//also, we now also keep track of which structs are dynamic -//also, we allow a calldata allocation to be null to indicate a type not allowed -//in calldata - -export interface CalldataAllocations { - [id: number]: CalldataAllocation | null -} - -export interface CalldataAllocation { - definition: AstDefinition; - length: number; //measured in bytes - dynamic: boolean; - members: CalldataMemberAllocation[]; -} - -export interface CalldataMemberAllocation { - definition: AstDefinition; - pointer: CalldataPointer; -} - -//and finally, memory; works the same as calldata, except we don't bother keeping -//track of size (it's always 1 word) or dynamicity (meaningless in memory) -//Note: for mappings we use a pointer of length 0 - -export interface MemoryAllocations { - [id: number]: MemoryAllocation -} - -export interface MemoryAllocation { - definition: AstDefinition; - members: MemoryMemberAllocation[]; -} - -export interface MemoryMemberAllocation { - definition: AstDefinition; - pointer: MemoryPointer; -} diff --git a/packages/truffle-decoder-core/lib/types/errors.ts b/packages/truffle-decoder-core/lib/types/errors.ts deleted file mode 100644 index 4652f1cdc3d..00000000000 --- a/packages/truffle-decoder-core/lib/types/errors.ts +++ /dev/null @@ -1,27 +0,0 @@ -export class UnknownBaseContractIdError extends Error { - public derivedId: number; - public derivedName: string; - public derivedKind: string; - public baseId: number; - constructor(derivedId: number, derivedName: string, derivedKind: string, baseId: number) { - const message = `Cannot locate base contract ID ${baseId}$ of ${derivedKind}$ ${derivedName} (ID ${derivedId})`; - super(message); - this.name = "UnknownBaseContractIdError"; - this.derivedId = derivedId; - this.derivedName = derivedName; - this.derivedKind = derivedKind; - this.baseId = baseId; - } -} - -export class UnknownUserDefinedTypeError extends Error { - public typeString: string; - public id: number; - constructor(id: number, typeString: string) { - const message = `Cannot locate definition for ${typeString}$ (ID ${id})`; - super(message); - this.name = "UnknownUserDefinedTypeError"; - this.id = id; - this.typeString = typeString; - } -} diff --git a/packages/truffle-decoder-core/lib/types/pointer.ts b/packages/truffle-decoder-core/lib/types/pointer.ts deleted file mode 100644 index efa88399752..00000000000 --- a/packages/truffle-decoder-core/lib/types/pointer.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { AstDefinition } from "truffle-decode-utils"; -import { Range } from "./storage"; - -export type DataPointer = StackPointer | MemoryPointer | StoragePointer - | CalldataPointer | StackLiteralPointer | ConstantDefinitionPointer - | SpecialPointer; - -export interface GenericPointer { - typeClass?: string; -} - -export interface StackPointer extends GenericPointer { - stack: { - from: number; - to: number; - } -} - -export interface MemoryPointer extends GenericPointer { - memory: { - start: number; - length: number; - } -} - -export interface CalldataPointer extends GenericPointer { - calldata: { - start: number; - length: number; - } -} - -export interface StoragePointer extends GenericPointer { - storage: Range; -} - -export interface StackLiteralPointer extends GenericPointer { - literal: Uint8Array; -} - -export interface ConstantDefinitionPointer extends GenericPointer { - definition: AstDefinition; -} - -export interface SpecialPointer extends GenericPointer { - special: string; //sorry -} - -export function isStackPointer(pointer: DataPointer): pointer is StackPointer { - return typeof pointer !== "undefined" && "stack" in pointer; -} - -export function isMemoryPointer(pointer: DataPointer): pointer is MemoryPointer { - return typeof pointer !== "undefined" && "memory" in pointer; -} - -export function isCalldataPointer(pointer: DataPointer): pointer is CalldataPointer { - return typeof pointer !== "undefined" && "calldata" in pointer; -} - -export function isStoragePointer(pointer: DataPointer): pointer is StoragePointer { - return typeof pointer !== "undefined" && "storage" in pointer; -} - -export function isStackLiteralPointer(pointer: DataPointer): pointer is StackLiteralPointer { - return typeof pointer !== "undefined" && "literal" in pointer; -} - -export function isConstantDefinitionPointer(pointer: DataPointer): pointer is ConstantDefinitionPointer { - return typeof pointer !== "undefined" && "definition" in pointer; -} - -export function isSpecialPointer(pointer: DataPointer): pointer is SpecialPointer { - return typeof pointer !== "undefined" && "special" in pointer; -} diff --git a/packages/truffle-decoder/lib/decoder.ts b/packages/truffle-decoder/lib/contract.ts similarity index 51% rename from packages/truffle-decoder/lib/decoder.ts rename to packages/truffle-decoder/lib/contract.ts index 901d21fc955..dbbf11e1b4d 100644 --- a/packages/truffle-decoder/lib/decoder.ts +++ b/packages/truffle-decoder/lib/contract.ts @@ -1,18 +1,18 @@ import debugModule from "debug"; -const debug = debugModule("decoder:decoder"); +const debug = debugModule("decoder:contract"); -import * as DecodeUtils from "truffle-decode-utils"; -import { Types, Values } from "truffle-decode-utils"; +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values, wrapElementaryViaDefinition, Contexts } from "truffle-codec-utils"; import AsyncEventEmitter from "async-eventemitter"; import Web3 from "web3"; import { ContractObject } from "truffle-contract-schema/spec"; import BN from "bn.js"; -import { Definition as DefinitionUtils, EVM, AstDefinition, AstReferences } from "truffle-decode-utils"; +import { Definition as DefinitionUtils, AbiUtils, EVM, AstDefinition, AstReferences } from "truffle-codec-utils"; +import TruffleWireDecoder from "./wire"; import { BlockType, Transaction } from "web3/eth/types"; -import { EventLog, Log } from "web3/types"; +import { Log } from "web3/types"; import { Provider } from "web3/providers"; -import abiDecoder from "abi-decoder"; -import * as Decoder from "truffle-decoder-core"; +import * as Codec from "truffle-codec"; import * as DecoderTypes from "./types"; import * as Utils from "./utils"; @@ -24,80 +24,77 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { private contractNetwork: string; private contractAddress: string; private contractCode: string; - private relevantContracts: ContractObject[]; + private context: Contexts.DecoderContext; + private constructorContext: Contexts.DecoderContext; + private contextHash: string; + private constructorContextHash: string; - private contracts: DecoderTypes.ContractMapping = {}; - private contractNodes: AstReferences = {}; - private contexts: DecodeUtils.Contexts.DecoderContexts = {}; - private context: DecodeUtils.Contexts.DecoderContext; + private contexts: Contexts.DecoderContexts = {}; + private contextsById: Contexts.DecoderContextsById = {}; //deployed contexts only + private constructorContextsById: Contexts.DecoderContextsById = {}; + private additionalContexts: Contexts.DecoderContextsById = {}; //for passing to wire decoder when contract has no deployedBytecode private referenceDeclarations: AstReferences; private userDefinedTypes: Types.TypesById; - private storageAllocations: Decoder.StorageAllocations; + private allocations: Codec.AllocationInfo; - private eventDefinitions: AstReferences; - private eventDefinitionIdsByName: { - [name: string]: number - }; + private stateVariableReferences: Codec.StorageMemberAllocation[]; - private stateVariableReferences: Decoder.StorageMemberAllocation[]; - - private mappingKeys: Decoder.Slot[] = []; + private mappingKeys: Codec.Slot[] = []; private storageCache: DecoderTypes.StorageCache = {}; - private codeCache: DecoderTypes.CodeCache = {}; - constructor(contract: ContractObject, relevantContracts: ContractObject[], provider: Provider, address: string) { - super(); + private wireDecoder: TruffleWireDecoder; - this.web3 = new Web3(provider); + constructor(contract: ContractObject, wireDecoder: TruffleWireDecoder, address?: string) { + super(); this.contract = contract; - this.relevantContracts = relevantContracts; - if(address !== undefined) { this.contractAddress = address; } + this.wireDecoder = wireDecoder; + this.web3 = wireDecoder.getWeb3(); + this.contractNode = Utils.getContractNode(this.contract); if(this.contractNode === undefined) { throw new DecoderTypes.ContractBeingDecodedHasNoNodeError(); } - this.contracts[this.contractNode.id] = this.contract; - this.contractNodes[this.contractNode.id] = this.contractNode; - if(this.contract.deployedBinary) { //just to be safe - this.context = this.makeContext(this.contract, this.contractNode); - this.contexts[this.contractNode.id] = this.context; + ({ byHash: this.contexts, byId: this.contextsById, constructorsById: this.constructorContextsById } = this.wireDecoder.getContexts()); + + if(this.contract.deployedBytecode && this.contract.deployedBytecode !== "0x") { + const hash = CodecUtils.Conversion.toHexString( + CodecUtils.EVM.keccak256({type: "string", + value: this.contract.deployedBytecode + }) + ); + this.contextHash = hash; + this.context = this.contexts[hash]; } - abiDecoder.addABI(this.contract.abi); - - for(let relevantContract of this.relevantContracts) { - abiDecoder.addABI(relevantContract.abi); - let node: AstDefinition = Utils.getContractNode(relevantContract); - if(node !== undefined) { - this.contracts[node.id] = relevantContract; - this.contractNodes[node.id] = node; - if(relevantContract.deployedBinary) { - this.contexts[node.id] = this.makeContext(relevantContract, node); - } - } + if(this.contract.bytecode && this.contract.bytecode !== "0x") { //now the constructor version + const hash = CodecUtils.Conversion.toHexString( + CodecUtils.EVM.keccak256({type: "string", + value: this.contract.bytecode + }) + ); + this.constructorContextHash = hash; + this.constructorContext = this.contexts[hash]; } - this.contexts = DecodeUtils.Contexts.normalizeContexts(this.contexts); - } + this.referenceDeclarations = this.wireDecoder.getReferenceDeclarations(); + this.userDefinedTypes = this.wireDecoder.getUserDefinedTypes(); - private makeContext(contract: ContractObject, node: AstDefinition) { - //we want the non-constructor context here - return { - contractName: contract.contractName, - binary: contract.deployedBytecode, - contractId: node.id, - contractKind: node.contractKind, - isConstructor: false, - abi: DecodeUtils.Contexts.abiToFunctionAbiWithSignatures(contract.abi), - payable: DecodeUtils.Contexts.abiHasPayableFallback(contract.abi), - compiler: contract.compiler - }; + this.allocations = {}; + this.allocations.abi = this.wireDecoder.getAbiAllocations(); + this.allocations.storage = Codec.getStorageAllocations( + this.referenceDeclarations, + {[this.contractNode.id]: this.contractNode} + ); + + debug("done with allocation"); + this.stateVariableReferences = this.allocations.storage[this.contractNode.id].members; + debug("stateVariableReferences %O", this.stateVariableReferences); } public async init(): Promise { @@ -106,73 +103,56 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { this.contractAddress = this.contract.networks[this.contractNetwork].address; } - debug("init called"); - [this.referenceDeclarations, this.userDefinedTypes] = this.getUserDefinedTypes(); - - this.eventDefinitions = Utils.getEventDefinitions(Object.values(this.contractNodes)); - this.eventDefinitionIdsByName = {}; - for (let id in this.eventDefinitions) { - this.eventDefinitionIdsByName[this.eventDefinitions[id].name] = parseInt(id); - //this parseInt shouldn't be necessary, but TypeScript refuses to believe - //that id must be a number even though the definition of AstReferences - //says so - } - debug("done with event definitions"); - - this.storageAllocations = Decoder.getStorageAllocations(this.referenceDeclarations, {[this.contractNode.id]: this.contractNode}); - debug("done with allocation"); - this.stateVariableReferences = this.storageAllocations[this.contractNode.id].members; - debug("stateVariableReferences %O", this.stateVariableReferences); - - this.contractCode = await this.web3.eth.getCode(this.contractAddress); - } + this.contractCode = CodecUtils.Conversion.toHexString( + await this.getCode(this.contractAddress, await this.web3.eth.getBlockNumber()) + ); - private getUserDefinedTypes(): [AstReferences, Types.TypesById] { - let references: AstReferences = {}; - let types: Types.TypesById = {}; - for(const id in this.contracts) { - const compiler = this.contracts[id].compiler; - //first, add the contract itself - const contractNode = this.contractNodes[id]; - references[id] = contractNode; - types[id] = Types.definitionToStoredType(contractNode, compiler); - //now, add its struct and enum definitions - for(const node of contractNode.nodes) { - if(node.nodeType === "StructDefinition" || node.nodeType === "EnumDefinition") { - references[node.id] = node; - //HACK even though we don't have all the references, we only need one: - //the reference to the contract itself, which we just added, so we're good - types[node.id] = Types.definitionToStoredType(node, compiler, references); - } - } + if(!this.contract.deployedBytecode || this.contract.deployedBytecode === "0x") { + //if this contract does *not* have the deployedBytecode field, then the decoder core + //has no way of knowing that contracts or function pointers with its address + //are of its class; this is an especial problem for function pointers, as it + //won't be able to determine what the selector points to. + //so, to get around this, we make an "additional context" for the contract, + //based on its *actual* deployed bytecode as pulled from the blockchain. + //This way the decoder core can recognize the address as the class, without us having + //to make serious modifications to contract decoding. And while sure this requires + //a little more work, I mean, it's all cached, so, no big deal. + let extraContext = Utils.makeContext(this.contract, this.contractNode); + //now override the binary + extraContext.binary = this.contractCode; + this.additionalContexts = {[extraContext.contractId]: extraContext}; + //the following line only has any effect if we're dealing with a library, + //since the code we pulled from the blockchain obviously does not have unresolved link references! + //(it's not strictly necessary even then, but, hey, why not?) + this.additionalContexts = Contexts.normalizeContexts(this.additionalContexts); + //again, since the code did not have unresolved link references, it is safe to just + //mash these together like I'm about to + this.contextsById = {...this.contextsById, ...this.additionalContexts}; } - return [references, types]; } - private async decodeVariable(variable: Decoder.StorageMemberAllocation, block: number): Promise { - const info: Decoder.EvmInfo = { + private async decodeVariable(variable: Codec.StorageMemberAllocation, block: number): Promise { + const info: Codec.EvmInfo = { state: { - stack: [], storage: {}, - memory: new Uint8Array(0) }, mappingKeys: this.mappingKeys, userDefinedTypes: this.userDefinedTypes, - storageAllocations: this.storageAllocations, - contexts: this.contexts, + allocations: this.allocations, + contexts: this.contextsById, currentContext: this.context }; - const decoder = Decoder.forEvmState(variable.definition, variable.pointer, info); + const decoder = Codec.decodeVariable(variable.definition, variable.pointer, info); let result = decoder.next(); while(!result.done) { - let request = (result.value); + let request = (result.value); let response: Uint8Array; - if(Decoder.isStorageRequest(request)) { + if(Codec.isStorageRequest(request)) { response = await this.getStorage(this.contractAddress, request.slot, block); } - else if(Decoder.isCodeRequest(request)) { + else if(Codec.isCodeRequest(request)) { response = await this.getCode(request.address, block); } //note: one of the above conditionals *must* be true by the type system. @@ -191,8 +171,8 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { let result: DecoderTypes.ContractState = { name: this.contract.contractName, code: this.contractCode, - balance: new BN(await this.web3.eth.getBalance(this.contractAddress, blockNumber)), - nonce: new BN(await this.web3.eth.getTransactionCount(this.contractAddress, blockNumber)), + balanceAsBN: new BN(await this.web3.eth.getBalance(this.contractAddress, blockNumber)), + nonceAsBN: new BN(await this.web3.eth.getTransactionCount(this.contractAddress, blockNumber)), variables: {} }; @@ -217,7 +197,7 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { ? block : (await this.web3.eth.getBlock(block)).number; - let variable: Decoder.StorageMemberAllocation; + let variable: Codec.StorageMemberAllocation; variable = this.stateVariableReferences.find( ({definition}) => definition.name === nameOrId || definition.id == nameOrId ); //there should be exactly one @@ -243,36 +223,20 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { return this.storageCache[block][address][slot.toString()]; } //otherwise, get it, cache it, and return it - let word = DecodeUtils.Conversion.toBytes( + let word = CodecUtils.Conversion.toBytes( await this.web3.eth.getStorageAt( address, slot, block ), - DecodeUtils.EVM.WORD_SIZE + CodecUtils.EVM.WORD_SIZE ); this.storageCache[block][address][slot.toString()] = word; return word; } private async getCode(address: string, block: number): Promise { - //first, set up any preliminary layers as needed - if(this.codeCache[block] === undefined) { - this.codeCache[block] = {}; - } - //now, if we have it cached, just return it - if(this.codeCache[block][address] !== undefined) { - return this.codeCache[block][address]; - } - //otherwise, get it, cache it, and return it - let code = DecodeUtils.Conversion.toBytes( - await this.web3.eth.getCode( - address, - block - ) - ); - this.codeCache[block][address] = code; - return code; + return await this.wireDecoder.getCode(address, block); } //EXAMPLE: to watch a.b.c[d][e], use watchMappingKey("a", "b", "c", d, e) @@ -280,12 +244,12 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { //feel free to mix arrays, mappings, and structs here! //see the comment on constructSlot for more detail on what forms are accepted public watchMappingKey(variable: number | string, ...indices: any[]): void { - let slot: Decoder.Slot | undefined = this.constructSlot(variable, ...indices)[0]; + let slot: Codec.Slot | undefined = this.constructSlot(variable, ...indices)[0]; //add mapping key and all ancestors debug("slot: %O", slot); while(slot !== undefined && this.mappingKeys.every(existingSlot => - !Decoder.equalSlots(existingSlot,slot) + !Codec.equalSlots(existingSlot,slot) //we put the newness requirement in the while condition rather than a //separate if because if we hit one ancestor that's not new, the futher //ones won't be either @@ -299,14 +263,14 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { //input is similar to watchMappingKey; will unwatch all descendants too public unwatchMappingKey(variable: number | string, ...indices: any[]): void { - let slot: Decoder.Slot | undefined = this.constructSlot(variable, ...indices)[0]; + let slot: Codec.Slot | undefined = this.constructSlot(variable, ...indices)[0]; if(slot === undefined) { return; //not strictly necessary, but may as well } //remove mapping key and all descendants this.mappingKeys = this.mappingKeys.filter( existingSlot => { while(existingSlot !== undefined) { - if(Decoder.equalSlots(existingSlot, slot)) { + if(Codec.equalSlots(existingSlot, slot)) { return false; //if it matches, remove it } existingSlot = existingSlot.path; @@ -318,70 +282,23 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { //all descendants, you'll need to alter watchMappingKey to use an if rather //than a while - public decodeTransaction(transaction: Transaction): any { - const decodedData = abiDecoder.decodeMethod(transaction.input); - - return decodedData; + public async decodeTransaction(transaction: Transaction): Promise { + return await this.wireDecoder.decodeTransaction(transaction, this.additionalContexts); } - public decodeLog(log: Log): any { - const decodedLogs = this.decodeLogs([log]); - - return decodedLogs[0]; + public async decodeLog(log: Log): Promise { + return await this.wireDecoder.decodeLog(log, {}, this.additionalContexts); } - public decodeLogs(logs: Log[]): any[] { - const decodedLogs = abiDecoder.decodeLogs(logs); - - return decodedLogs; + public async decodeLogs(logs: Log[]): Promise { + return await this.wireDecoder.decodeLogs(logs, {}, this.additionalContexts); } - private decodeEvent(event: EventLog): DecoderTypes.ContractEvent { - let contractEvent: DecoderTypes.ContractEvent = { - logIndex: event.logIndex, - name: event.event, - blockHash: event.blockHash, - blockNumber: event.blockNumber, - transactionHash: event.transactionHash, - transactionIndex: event.transactionIndex, - variables: {} - }; - - const eventDefinition = this.eventDefinitions[this.eventDefinitionIdsByName[contractEvent.name]]; - - if (typeof eventDefinition.parameters !== "undefined" && typeof eventDefinition.parameters.parameters !== "undefined") { - const argumentDefinitions = eventDefinition.parameters.parameters; - - for (let i = 0; i < argumentDefinitions.length; i++) { - const definition = argumentDefinitions[i]; - - if (definition.nodeType === "VariableDeclaration") { - contractEvent.variables[definition.name] = { - name: definition.name, - type: DefinitionUtils.typeClass(definition), - value: event.returnValues[definition.name] // TODO: this should be a decoded value, it currently is a string always - }; - } - } - } - - return contractEvent; - } - - public async events(name: string | null = null, block: BlockType = "latest"): Promise { - const web3Contract = new this.web3.eth.Contract(this.contract.abi, this.contractAddress); - const events = await web3Contract.getPastEvents(name, { - fromBlock: block, - toBlock: block - }); - - let contractEvents: DecoderTypes.ContractEvent[] = []; - - for (let i = 0; i < events.length; i++) { - contractEvents.push(this.decodeEvent(events[i])); - } - - return contractEvents; + //by default, will restrict to events from this address. + //but, you can explicitly pass another and it will work + //(or pass undefined to turn off address filtering) + public async events(options: DecoderTypes.EventOptions = {}): Promise { + return await this.wireDecoder.events({address: this.contractAddress, ...options}, this.additionalContexts); } public onEvent(name: string, callback: Function): void { @@ -401,10 +318,10 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { //bytes mapping keys should be given as hex strings beginning with "0x" //address mapping keys are like bytes; checksum case is not required //boolean mapping keys may be given either as booleans, or as string "true" or "false" - private constructSlot(variable: number | string, ...indices: any[]): [Decoder.Slot | undefined , AstDefinition | undefined] { + private constructSlot(variable: number | string, ...indices: any[]): [Codec.Slot | undefined , AstDefinition | undefined] { //base case: we need to locate the variable and its definition if(indices.length === 0) { - let allocation: Decoder.StorageMemberAllocation; + let allocation: Codec.StorageMemberAllocation; allocation = this.stateVariableReferences.find( ({definition}) => definition.name === variable || definition.id == variable ); //there should be exactly one @@ -412,10 +329,10 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { let definition = allocation.definition; let pointer = allocation.pointer; - if(!Decoder.isStoragePointer(pointer)) { //if it's a constant + if(pointer.location !== "storage") { //i.e., if it's a constant return [undefined, undefined]; } - return [pointer.storage.from.slot, definition]; + return [pointer.range.from.slot, definition]; } //main case @@ -427,7 +344,7 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { let rawIndex = indices[indices.length - 1]; let index: any; let key: Values.ElementaryValue; - let slot: Decoder.Slot; + let slot: Codec.Slot; let definition: AstDefinition; switch(DefinitionUtils.typeClass(parentDefinition)) { case "array": @@ -438,8 +355,8 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { index = new BN(rawIndex); } definition = parentDefinition.baseType || parentDefinition.typeName.baseType; - let size = Decoder.storageSize(definition, this.referenceDeclarations, this.storageAllocations); - if(!Decoder.isWordsLength(size)) { + let size = Codec.storageSize(definition, this.referenceDeclarations, this.allocations.storage); + if(!Codec.isWordsLength(size)) { return [undefined, undefined]; } slot = { @@ -450,7 +367,7 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { break; case "mapping": let keyDefinition = parentDefinition.keyType || parentDefinition.typeName.keyType; - key = Values.wrapElementaryValue(rawIndex, keyDefinition); + key = wrapElementaryViaDefinition(rawIndex, keyDefinition); definition = parentDefinition.valueType || parentDefinition.typeName.valueType; slot = { path: parentSlot, @@ -460,14 +377,14 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { break; case "struct": let parentId = DefinitionUtils.typeId(parentDefinition); - let allocation: Decoder.StorageMemberAllocation; + let allocation: Codec.StorageMemberAllocation; if(typeof rawIndex === "number") { index = rawIndex; - allocation = this.storageAllocations[parentId].members[index]; + allocation = this.allocations.storage[parentId].members[index]; definition = allocation.definition; } else { - allocation = Object.values(this.storageAllocations[parentId].members) + allocation = Object.values(this.allocations.storage[parentId].members) .find(({definition}) => definition.name === rawIndex); //there should be exactly one definition = allocation.definition; index = definition.id; //not really necessary, but may as well @@ -475,7 +392,7 @@ export default class TruffleContractDecoder extends AsyncEventEmitter { slot = { path: parentSlot, //need type coercion here -- we know structs don't contain constants but the compiler doesn't - offset: (allocation.pointer).storage.from.slot.offset.clone() + offset: (allocation.pointer).range.from.slot.offset.clone() } break; default: diff --git a/packages/truffle-decoder/lib/index.ts b/packages/truffle-decoder/lib/index.ts index 2693f72984f..765b6d99df1 100644 --- a/packages/truffle-decoder/lib/index.ts +++ b/packages/truffle-decoder/lib/index.ts @@ -1,7 +1,27 @@ -import TruffleContractDecoder from "./decoder"; +import debugModule from "debug"; +const debug = debugModule("decoder"); + +import TruffleContractDecoder from "./contract"; +import TruffleWireDecoder from "./wire"; import { Provider } from "web3/providers"; import { ContractObject } from "truffle-contract-schema/spec"; -export function forContract(contract: ContractObject, relevantContracts: ContractObject[], provider: Provider, address?: string): TruffleContractDecoder { - return new TruffleContractDecoder(contract, relevantContracts, provider, address); +export async function forContract(contract: ContractObject, relevantContracts: ContractObject[], provider: Provider, address?: string): Promise { + let contracts = relevantContracts.includes(contract) + ? relevantContracts + : [contract, ...relevantContracts]; + let wireDecoder = new TruffleWireDecoder(contracts, provider); + let contractDecoder = new TruffleContractDecoder(contract, wireDecoder, address); + await contractDecoder.init(); + return contractDecoder; +} + +export async function forProject(contracts: ContractObject[], provider: Provider): Promise { + return new TruffleWireDecoder(contracts, provider); +} + +export async function forContractWithDecoder(contract: ContractObject, decoder: TruffleWireDecoder, address?: string): Promise { + let contractDecoder = new TruffleContractDecoder(contract, decoder, address); + await contractDecoder.init(); + return contractDecoder; } diff --git a/packages/truffle-decoder/lib/types.ts b/packages/truffle-decoder/lib/types.ts index 2bf6f7d6384..dee66b816f2 100644 --- a/packages/truffle-decoder/lib/types.ts +++ b/packages/truffle-decoder/lib/types.ts @@ -1,35 +1,27 @@ import BN from "bn.js"; import { ContractObject } from "truffle-contract-schema/spec"; -import { Values } from "truffle-decode-utils"; - -interface EventVariable { - name: string; - type: string; - value: string; //NOTE: this should change to be a decoded variable object - //(although really that would replace EventVariable entirely) -}; +import { Values } from "truffle-codec-utils"; +import { CalldataDecoding, LogDecoding } from "truffle-codec"; +import { Transaction, BlockType } from "web3/eth/types"; +import { Log } from "web3/types"; export interface ContractState { name: string; - balance: BN; - nonce: BN; + balanceAsBN: BN; + nonceAsBN: BN; code: string; variables: { [name: string]: Values.Result }; }; -export interface ContractEvent { - logIndex: number; - name: string; - blockHash: string; - blockNumber: number; - transactionHash: string; - transactionIndex: number; - variables: { - [name: string]: EventVariable - } -}; +export interface DecodedTransaction extends Transaction { + decoding: CalldataDecoding; +} + +export interface DecodedLog extends Log { + decodings: LogDecoding[]; +} export interface ContractMapping { [nodeId: number]: ContractObject; @@ -49,6 +41,13 @@ export interface CodeCache { }; } +export interface EventOptions { + name?: string; + fromBlock?: BlockType; + toBlock?: BlockType; + address?: string; //ignored by contract decoder! +} + export class ContractBeingDecodedHasNoNodeError extends Error { constructor() { const message = "Contract does not appear to have been compiled with Solidity (cannot locate contract node)"; @@ -56,3 +55,15 @@ export class ContractBeingDecodedHasNoNodeError extends Error { this.name = "ContractBeingDecodedHasNoNodeError"; } } + +export class EventOrTransactionIsNotForThisContractError extends Error { + thisAddress: string; + decoderAddress: string; + constructor(thisAddress: string, decoderAddress: string) { + const message = "This event or transaction's address does not match that of the contract decoder"; + super(message); + this.name = "EventOrTransactionIsNotForThisContractError"; + this.thisAddress = thisAddress; + this.decoderAddress = decoderAddress; + } +} diff --git a/packages/truffle-decoder/lib/utils.ts b/packages/truffle-decoder/lib/utils.ts index 073a426d3f6..d1ccb38ff79 100644 --- a/packages/truffle-decoder/lib/utils.ts +++ b/packages/truffle-decoder/lib/utils.ts @@ -1,4 +1,4 @@ -import { AstDefinition, AstReferences } from "truffle-decode-utils"; +import { AstDefinition, AstReferences, AbiUtils, Contexts as ContextsUtils } from "truffle-codec-utils"; import { ContractObject } from "truffle-contract-schema/spec"; export function getContractNode(contract: ContractObject): AstDefinition { @@ -10,26 +10,16 @@ export function getContractNode(contract: ContractObject): AstDefinition { ); } -function getDeclarationsForTypes(contracts: AstDefinition[], types: string[]): AstReferences { - let result: AstReferences = {}; - - for (let contract of contracts) { - if (contract) { - for (const node of contract.nodes) { - if (types.includes(node.nodeType)) { - result[node.id] = node; - } - } - } - } - - return result; -} - -export function getEventDefinitions(contracts: AstDefinition[]): AstReferences { - const types = [ - "EventDefinition" - ]; - - return getDeclarationsForTypes(contracts, types); +export function makeContext(contract: ContractObject, node: AstDefinition, isConstructor = false): ContextsUtils.DecoderContext { + let abi = AbiUtils.schemaAbiToAbi(contract.abi); + return { + contractName: contract.contractName, + binary: isConstructor ? contract.bytecode : contract.deployedBytecode, + contractId: node.id, + contractKind: node.contractKind, + isConstructor, + abi: AbiUtils.computeSelectors(abi), + payable: AbiUtils.abiHasPayableFallback(abi), + compiler: contract.compiler + }; } diff --git a/packages/truffle-decoder/lib/wire.ts b/packages/truffle-decoder/lib/wire.ts new file mode 100644 index 00000000000..8c357d21cff --- /dev/null +++ b/packages/truffle-decoder/lib/wire.ts @@ -0,0 +1,291 @@ +import debugModule from "debug"; +const debug = debugModule("decoder:wire"); + +import * as CodecUtils from "truffle-codec-utils"; +import { Types, Values, Contexts } from "truffle-codec-utils"; +import AsyncEventEmitter from "async-eventemitter"; +import Web3 from "web3"; +import { ContractObject } from "truffle-contract-schema/spec"; +import BN from "bn.js"; +import { Definition as DefinitionUtils, AbiUtils, EVM, AstDefinition, AstReferences } from "truffle-codec-utils"; +import { BlockType, Transaction } from "web3/eth/types"; +import { Log } from "web3/types"; +import { Provider } from "web3/providers"; +import * as Codec from "truffle-codec"; +import * as DecoderTypes from "./types"; +import * as Utils from "./utils"; + +export default class TruffleWireDecoder extends AsyncEventEmitter { + private web3: Web3; + + private network: string; + + private contracts: DecoderTypes.ContractMapping = {}; + private contractNodes: AstReferences = {}; + private contexts: Contexts.DecoderContexts = {}; + private contextsById: Contexts.DecoderContextsById = {}; //deployed contexts only + private constructorContextsById: Contexts.DecoderContextsById = {}; + + private referenceDeclarations: AstReferences; + private userDefinedTypes: Types.TypesById; + private allocations: Codec.AllocationInfo; + + private codeCache: DecoderTypes.CodeCache = {}; + + constructor(contracts: ContractObject[], provider: Provider) { + super(); + + this.web3 = new Web3(provider); + + for(let contract of contracts) { + let node: AstDefinition = Utils.getContractNode(contract); + if(node !== undefined) { + this.contracts[node.id] = contract; + this.contractNodes[node.id] = node; + if(contract.deployedBytecode && contract.deployedBytecode !== "0x") { + const context = Utils.makeContext(contract, node); + const hash = CodecUtils.Conversion.toHexString( + CodecUtils.EVM.keccak256({type: "string", + value: context.binary + }) + ); + this.contexts[hash] = context; + } + if(contract.bytecode && contract.bytecode !== "0x") { + const constructorContext = Utils.makeContext(contract, node, true); + const hash = CodecUtils.Conversion.toHexString( + CodecUtils.EVM.keccak256({type: "string", + value: constructorContext.binary + }) + ); + this.contexts[hash] = constructorContext; + } + } + } + + this.contexts = Contexts.normalizeContexts(this.contexts); + this.contextsById = Object.assign({}, ...Object.values(this.contexts).filter( + ({isConstructor}) => !isConstructor + ).map(context => + ({[context.contractId]: context}) + )); + this.constructorContextsById = Object.assign({}, ...Object.values(this.contexts).filter( + ({isConstructor}) => isConstructor + ).map(context => + ({[context.contractId]: context}) + )); + ({definitions: this.referenceDeclarations, types: this.userDefinedTypes} = this.collectUserDefinedTypes()); + + let allocationInfo: Codec.ContractAllocationInfo[] = Object.entries(this.contracts).map( + ([id, { abi }]) => ({ + abi: AbiUtils.schemaAbiToAbi(abi), + id: parseInt(id), + constructorContext: this.constructorContextsById[parseInt(id)] + }) + ); + debug("allocationInfo: %O", allocationInfo); + + this.allocations = {}; + this.allocations.storage = Codec.getStorageAllocations(this.referenceDeclarations, this.contractNodes); + this.allocations.abi = Codec.getAbiAllocations(this.referenceDeclarations); + this.allocations.calldata = Codec.getCalldataAllocations(allocationInfo, this.referenceDeclarations, this.allocations.abi); + this.allocations.event = Codec.getEventAllocations(allocationInfo, this.referenceDeclarations, this.allocations.abi); + debug("done with allocation"); + } + + private collectUserDefinedTypes(): {definitions: AstReferences, types: Types.TypesById} { + let references: AstReferences = {}; + let types: Types.TypesById = {}; + for(const id in this.contracts) { + const compiler = this.contracts[id].compiler; + //first, add the contract itself + const contractNode = this.contractNodes[id]; + references[id] = contractNode; + types[id] = Types.definitionToStoredType(contractNode, compiler); + //now, add its struct and enum definitions + for(const node of contractNode.nodes) { + if(node.nodeType === "StructDefinition" || node.nodeType === "EnumDefinition") { + references[node.id] = node; + //HACK even though we don't have all the references, we only need one: + //the reference to the contract itself, which we just added, so we're good + types[node.id] = Types.definitionToStoredType(node, compiler, references); + } + } + } + return {definitions: references, types}; + } + + //for internal use + public async getCode(address: string, block: number): Promise { + //first, set up any preliminary layers as needed + if(this.codeCache[block] === undefined) { + this.codeCache[block] = {}; + } + //now, if we have it cached, just return it + if(this.codeCache[block][address] !== undefined) { + return this.codeCache[block][address]; + } + //otherwise, get it, cache it, and return it + let code = CodecUtils.Conversion.toBytes( + await this.web3.eth.getCode( + address, + block + ) + ); + this.codeCache[block][address] = code; + return code; + } + + //NOTE: additionalContexts parameter is for internal use only. + public async decodeTransaction(transaction: Transaction, additionalContexts: Contexts.DecoderContextsById = {}): Promise { + debug("transaction: %O", transaction); + const block = transaction.blockNumber; + const context = await this.getContextByAddress(transaction.to, block, transaction.input, additionalContexts); + + const data = CodecUtils.Conversion.toBytes(transaction.input); + const info: Codec.EvmInfo = { + state: { + storage: {}, + calldata: data, + }, + userDefinedTypes: this.userDefinedTypes, + allocations: this.allocations, + contexts: {...this.contextsById, ...additionalContexts}, + currentContext: context + }; + const decoder = Codec.decodeCalldata(info); + + let result = decoder.next(); + while(!result.done) { + let request = (result.value); + let response: Uint8Array; + //only code requests should occur here + if(Codec.isCodeRequest(request)) { + response = await this.getCode(request.address, block); + } + result = decoder.next(response); + } + //at this point, result.value holds the final value + const decoding = result.value; + + return { + ...transaction, + decoding + }; + } + + //NOTE: options is meant for internal use; do not rely on it + //NOTE: additionalContexts parameter is for internal use only. + public async decodeLog(log: Log, options: DecoderTypes.EventOptions = {}, additionalContexts: Contexts.DecoderContextsById = {}): Promise { + const block = log.blockNumber; + const data = CodecUtils.Conversion.toBytes(log.data); + const topics = log.topics.map(CodecUtils.Conversion.toBytes); + const info: Codec.EvmInfo = { + state: { + storage: {}, + eventdata: data, + eventtopics: topics + }, + userDefinedTypes: this.userDefinedTypes, + allocations: this.allocations, + contexts: {...this.contextsById, ...additionalContexts} + }; + const decoder = Codec.decodeEvent(info, log.address, options.name); + + let result = decoder.next(); + while(!result.done) { + let request = (result.value); + let response: Uint8Array; + //only code requests should occur here + if(Codec.isCodeRequest(request)) { + response = await this.getCode(request.address, block); + } + result = decoder.next(response); + } + //at this point, result.value holds the final value + const decodings = result.value; + + return { + ...log, + decodings + }; + } + + //NOTE: options is meant for internal use; do not rely on it + //NOTE: additionalContexts parameter is for internal use only. + public async decodeLogs(logs: Log[], options: DecoderTypes.EventOptions = {}, additionalContexts: Contexts.DecoderContextsById = {}): Promise { + return await Promise.all(logs.map(log => this.decodeLog(log, options, additionalContexts))); + } + + //NOTE: additionalContexts parameter is for internal use only. + public async events(options: DecoderTypes.EventOptions = {}, additionalContexts: Contexts.DecoderContextsById = {}): Promise { + let { address, name, fromBlock, toBlock } = options; + + const logs = await this.web3.eth.getPastLogs({ + address, + fromBlock, + toBlock, + }); + + let events = await this.decodeLogs(logs, options, additionalContexts); + debug("events: %o", events); + + //if a target name was specified, we'll restrict to events that decoded + //to something with that name. (note that only decodings with that name + //will have been returned from decodeLogs in the first place) + if(name !== undefined) { + events = events.filter( + event => event.decodings.length > 0 + ); + } + + return events; + } + + public onEvent(name: string, callback: Function): void { + //this.web3.eth.subscribe(name); + } + + public removeEventListener(name: string): void { + } + + //normally, this function gets the code of the given address at the given block, + //and checks this against the known contexts to determine the contract type + //however, if this fails and constructorBinary is passed in, it will then also + //attempt to determine it from that + private async getContextByAddress(address: string, block: number, constructorBinary?: string, additionalContexts: Contexts.DecoderContextsById = {}): Promise { + let code: string; + if(address !== null) { + code = CodecUtils.Conversion.toHexString( + await this.getCode(address, block) + ); + } + else if(constructorBinary) { + code = constructorBinary; + } + //if neither of these hold... we have a problem + let contexts = {...this.contexts, ...additionalContexts}; + return Contexts.findDecoderContext(contexts, code); + } + + //the following functions are intended for internal use only + public getReferenceDeclarations(): AstReferences { + return this.referenceDeclarations; + } + + public getUserDefinedTypes(): Types.TypesById { + return this.userDefinedTypes; + } + + public getAbiAllocations(): Codec.AbiAllocations { + return this.allocations.abi; + } + + public getWeb3(): Web3 { + return this.web3; + } + + public getContexts(): {byHash: Contexts.DecoderContexts, byId: Contexts.DecoderContextsById, constructorsById: Contexts.DecoderContextsById } { + return {byHash: this.contexts, byId: this.contextsById, constructorsById: this.constructorContextsById}; + } +} diff --git a/packages/truffle-decoder/package.json b/packages/truffle-decoder/package.json index 875f3f30ca4..dc5e618de4d 100644 --- a/packages/truffle-decoder/package.json +++ b/packages/truffle-decoder/package.json @@ -28,20 +28,23 @@ }, "homepage": "https://github.com/trufflesuite/truffle#readme", "dependencies": { - "abi-decoder": "^1.2.0", "async-eventemitter": "^0.2.4", "bn.js": "^4.11.8", "debug": "^4.1.1", "json-schema-to-typescript": "^6.1.3", + "truffle-codec": "^3.0.8", + "truffle-codec-utils": "^1.0.16", "truffle-contract-schema": "^3.0.9", - "truffle-decoder-core": "^3.0.3", - "truffle-decode-utils": "^1.0.16", "web3": "^1.2.1" }, "devDependencies": { "@types/bn.js": "^4.11.5", "@types/debug": "^4.1.4", "@types/web3": "1.0.18", - "typescript": "^3.5.1" + "chai": "^4.2.0", + "json-schema-to-typescript": "^6.1.3", + "truffle-contract-schema": "^3.0.9", + "typescript": "^3.5.1", + "web3-core": "1.0.0-beta.37" } } diff --git a/packages/truffle-decoder/test/contracts/DecodingSample.sol b/packages/truffle-decoder/test/contracts/DecodingSample.sol index 237147a6165..7627ac64b9d 100644 --- a/packages/truffle-decoder/test/contracts/DecodingSample.sol +++ b/packages/truffle-decoder/test/contracts/DecodingSample.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.10; contract DecodingSample { enum E { @@ -68,7 +68,7 @@ contract DecodingSample { varUint = 1; varString = "two"; varBool = true; - varAddress = 0x0012345567890abcdeffedcba09876543211337121; + varAddress = 0x12345567890abcDEffEDcBa09876543211337121; varBytes7 = hex"78554477331122"; varBytes = new bytes(4); varBytes[0] = 0x01; @@ -79,7 +79,7 @@ contract DecodingSample { varStructS.structInt = -2; varStructS.structString = "three"; varStructS.structBool = false; - varStructS.structAddress = 0x0054321567890abcdeffedcba09876543211337121; + varStructS.structAddress = 0x54321567890abcdeFfEDcBA09876543211337121; varStructS.structS2.structTwoFixedArrayUint[0] = 4; varStructS.structS2.structTwoFixedArrayUint[1] = 2; varStructS.structS2.structTwoDynamicArrayUint = new uint[](3); @@ -93,8 +93,8 @@ contract DecodingSample { fixedArrayString[1] = "world"; fixedArrayBool[0] = true; fixedArrayBool[1] = false; - fixedArrayAddress[0] = 0x0098761567890abcdeffedcba09876543211337121; - fixedArrayAddress[1] = 0x00fedc1567890abcdeffedcba09876543211337121; + fixedArrayAddress[0] = 0x98761567890ABCdeffEdCba09876543211337121; + fixedArrayAddress[1] = 0xfEDc1567890aBcDeFfEdcba09876543211337121; fixedArrayBytes7[0] = hex"75754477331122"; fixedArrayBytes7[1] = hex"e7d14477331122"; fixedArrayByte[0] = 0x37; @@ -112,8 +112,8 @@ contract DecodingSample { dynamicArrayBool[0] = true; dynamicArrayBool[1] = false; dynamicArrayAddress = new address[](2); - dynamicArrayAddress[0] = 0x0098761567890abcdeffedcba09876543211337121; - dynamicArrayAddress[1] = 0x00fedc1567890abcdeffedcba09876543211337121; + dynamicArrayAddress[0] = 0x98761567890ABCdeffEdCba09876543211337121; + dynamicArrayAddress[1] = 0xfEDc1567890aBcDeFfEdcba09876543211337121; dynamicArrayBytes7 = new bytes7[](2); dynamicArrayBytes7[0] = hex"75754477331122"; dynamicArrayBytes7[1] = hex"e7d14477331122"; diff --git a/packages/truffle-decoder/test/contracts/Migrations.sol b/packages/truffle-decoder/test/contracts/Migrations.sol index c4efb65e216..073de9cce7e 100644 --- a/packages/truffle-decoder/test/contracts/Migrations.sol +++ b/packages/truffle-decoder/test/contracts/Migrations.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.23; +pragma solidity ^0.5.10; contract Migrations { address public owner; diff --git a/packages/truffle-decoder/test/contracts/WireTest.sol b/packages/truffle-decoder/test/contracts/WireTest.sol new file mode 100644 index 00000000000..a0a92354ec2 --- /dev/null +++ b/packages/truffle-decoder/test/contracts/WireTest.sol @@ -0,0 +1,143 @@ +pragma solidity ^0.5.10; +pragma experimental ABIEncoderV2; + +contract WireTestParent { + + event Done(); + + function inherited(uint[2] memory) public { + emit Done(); + } + + //no constructor +} + +contract WireTest is WireTestParent { + constructor(bool status, bytes memory info, Ternary whoknows) public { + deepStruct["blornst"].length = 9; + deepString.length = 9; + emit ConstructorEvent(status, info, whoknows); + } + + event ConstructorEvent(bool bit, bytes, Ternary); + + struct Triple { + int x; + bytes32 y; + bytes z; + } + + enum Ternary { + Yes, No, MaybeSo + } + + event EmitStuff(Triple, address[2], string[]); + + function emitStuff(Triple memory p, address[2] memory precompiles, string[] memory strings) public { + emit EmitStuff(p, precompiles, strings); + } + + event MoreStuff(WireTest, uint[] data); + + function moreStuff(WireTest notThis, uint[] memory bunchOfInts) public { + emit MoreStuff(notThis, bunchOfInts); + } + + event Danger(function() external); + + function danger() public { + emit Danger(this.danger); + } + + event HasIndices(uint, uint indexed, string, string indexed, uint); + + function indexTest(uint a, uint b, string memory c, string memory d, uint e) public { + emit HasIndices(a, b, c, d, e); + } + + function libraryTest(string memory it) public { + WireTestLibrary.emitEvent(it); + } + + event AmbiguousEvent(uint8[] indexed, uint[5]); + + function ambiguityTest() public { + uint8[] memory short = new uint8[](3); + uint[5] memory long; + long[0] = 32; + long[1] = 3; + long[2] = short[0] = 17; + long[3] = short[1] = 18; + long[4] = short[2] = 19; + emit AmbiguousEvent(short, long); + } + + function unambiguityTest() public { + uint8[] memory empty; + uint[5] memory tooLong; + //array length too long + tooLong[0] = 32; + tooLong[1] = 1e12; //still small enough for JS :) + tooLong[2] = 17; + tooLong[3] = 18; + tooLong[4] = 19; + emit AmbiguousEvent(empty, tooLong); + + //bad padding + uint[5] memory badPadding; + badPadding[0] = 32; + badPadding[1] = 3; + badPadding[2] = 257; + badPadding[3] = 257; + badPadding[4] = 257; + emit AmbiguousEvent(empty, badPadding); + + //decodes, but fails re-encode + uint[5] memory nonStrict; + nonStrict[0] = 64; + nonStrict[1] = 0; + nonStrict[2] = 2; + nonStrict[3] = 1; + nonStrict[4] = 1; + emit AmbiguousEvent(empty, nonStrict); + + WireTestLibrary.emitUnambiguousEvent(); + } + + event AnonUints(uint indexed, uint indexed, uint indexed, uint indexed) anonymous; + event NonAnon(uint indexed, uint indexed, uint indexed); + event ObviouslyAnon(byte) anonymous; + + function anonymousTest() public { + //first test: unambiguous + emit AnonUints(257, 1, 1, 1); + //second test: uint8 (from library) or uint? + emit AnonUints(1, 2, 3, 4); + //third test: uint, or not anonymous? + emit NonAnon(1, 2, 3); + //fourth test: no selector + emit ObviouslyAnon(0xfe); + } + + mapping(string => Triple[]) public deepStruct; + mapping(string => string)[] public deepString; +} + +library WireTestLibrary { + event LibraryEvent(string); + + function emitEvent(string calldata it) external { + emit LibraryEvent(it); + } + + event AmbiguousEvent(uint8[], uint[5] indexed); + + function emitUnambiguousEvent() external { + uint8[] memory wrongLength = new uint8[](1); + wrongLength[0] = 107; + uint[5] memory allZeroes; + emit AmbiguousEvent(wrongLength, allZeroes); + } + + event AnonUint8s(uint8 indexed, uint8 indexed, uint8 indexed, uint8 indexed) anonymous; +} diff --git a/packages/truffle-decoder/test/migrations/2_decoding_sample.js b/packages/truffle-decoder/test/migrations/2_decoding_sample.js deleted file mode 100644 index c0bac8f0827..00000000000 --- a/packages/truffle-decoder/test/migrations/2_decoding_sample.js +++ /dev/null @@ -1,5 +0,0 @@ -const DecodingSample = artifacts.require("./DecodingSample.sol"); - -module.exports = function(deployer) { - deployer.deploy(DecodingSample); -}; diff --git a/packages/truffle-decoder/test/migrations/2_deploy_contracts.js b/packages/truffle-decoder/test/migrations/2_deploy_contracts.js new file mode 100644 index 00000000000..f980564cdb3 --- /dev/null +++ b/packages/truffle-decoder/test/migrations/2_deploy_contracts.js @@ -0,0 +1,10 @@ +const DecodingSample = artifacts.require("DecodingSample"); +const WireTest = artifacts.require("WireTest"); +const WireTestLibrary = artifacts.require("WireTestLibrary"); + +module.exports = function(deployer) { + deployer.deploy(DecodingSample); + deployer.deploy(WireTestLibrary); + deployer.link(WireTestLibrary, WireTest); + deployer.deploy(WireTest, false, "0x", 0); +}; diff --git a/packages/truffle-decoder/test/test/test.js b/packages/truffle-decoder/test/test/decoding-test.js similarity index 94% rename from packages/truffle-decoder/test/test/test.js rename to packages/truffle-decoder/test/test/decoding-test.js index 6031c8dfdc0..0901ab0ea52 100644 --- a/packages/truffle-decoder/test/test/test.js +++ b/packages/truffle-decoder/test/test/decoding-test.js @@ -1,8 +1,8 @@ const assert = require("assert"); const util = require("util"); // eslint-disable-line no-unused-vars -const TruffleContractDecoder = require("../../../truffle-decoder"); -const TruffleDecodeUtils = require("../../../truffle-decode-utils"); +const TruffleDecoder = require("../../../truffle-decoder"); +const TruffleCodecUtils = require("../../../truffle-codec-utils"); const DecodingSample = artifacts.require("DecodingSample"); @@ -31,12 +31,11 @@ contract("DecodingSample", _accounts => { it("should get the initial state properly", async () => { let deployedContract = await DecodingSample.deployed(); let address = deployedContract.address; - const decoder = TruffleContractDecoder.forContract( + const decoder = await TruffleDecoder.forContract( DecodingSample, [], web3.currentProvider ); - await decoder.init(); decoder.watchMappingKey("varMapping", 2); decoder.watchMappingKey("varMapping", 3); @@ -53,7 +52,7 @@ contract("DecodingSample", _accounts => { // ); assert.equal(initialState.name, "DecodingSample"); - const variables = TruffleDecodeUtils.Conversion.nativizeVariables( + const variables = TruffleCodecUtils.Conversion.nativizeVariables( initialState.variables ); diff --git a/packages/truffle-decoder/test/test/wire-test.js b/packages/truffle-decoder/test/test/wire-test.js new file mode 100644 index 00000000000..71caab5c733 --- /dev/null +++ b/packages/truffle-decoder/test/test/wire-test.js @@ -0,0 +1,656 @@ +const debug = require("debug")("decoder:test:wire-test"); +const assert = require("chai").assert; +const BN = require("bn.js"); + +const TruffleDecoder = require("../../../truffle-decoder"); +const ConversionUtils = require("../../../truffle-codec-utils").Conversion; + +const WireTest = artifacts.require("WireTest"); +const WireTestParent = artifacts.require("WireTestParent"); +const WireTestLibrary = artifacts.require("WireTestLibrary"); + +contract("WireTest", _accounts => { + it("should correctly decode transactions and events", async () => { + let deployedContract = await WireTest.new(true, "0xdeadbeef", 2); + let address = deployedContract.address; + let constructorHash = deployedContract.transactionHash; + + const decoder = await TruffleDecoder.forProject( + [WireTest, WireTestParent, WireTestLibrary], + web3.currentProvider + ); + + let deployedContractNoConstructor = await WireTestParent.new(); + let defaultConstructorHash = deployedContractNoConstructor.transactionHash; + + let emitStuffArgs = [ + { + x: -1, + y: "0xdeadbeef00000000deadbeef00000000deadbeef00000000deadbeef00000000", + z: "0xbababa" + }, + [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002" + ], + ["hello", "hi", "hooblypoob"] + ]; + + let emitStuff = await deployedContract.emitStuff(...emitStuffArgs); + let emitStuffHash = emitStuff.tx; + + let moreStuffArgs = [address, [8, 8, 8, 7, 8, 8, 8]]; + + let moreStuff = await deployedContract.moreStuff(...moreStuffArgs); + let moreStuffHash = moreStuff.tx; + + let inheritedArg = [2, 3]; + let inherited = await deployedContract.inherited(inheritedArg); + let inheritedHash = inherited.tx; + + let indexTestArgs = [7, 89, "hello", "indecipherable", 62]; + let indexTest = await deployedContract.indexTest(...indexTestArgs); + + let libraryTestArg = "zooglyzooglyzooglyzoogly"; + let libraryTest = await deployedContract.libraryTest(libraryTestArg); + + let getter1Args = ["blornst", 7]; + //this function is view so we have to use sendTransaction + let getterTest1 = await deployedContract.deepStruct.sendTransaction( + ...getter1Args + ); + let getterHash1 = getterTest1.tx; + + let getter2Args = [7, "blornst"]; + //this function is view so we have to use sendTransaction + let getterTest2 = await deployedContract.deepString.sendTransaction( + ...getter2Args + ); + let getterHash2 = getterTest2.tx; + + let constructorTx = await web3.eth.getTransaction(constructorHash); + let emitStuffTx = await web3.eth.getTransaction(emitStuffHash); + let moreStuffTx = await web3.eth.getTransaction(moreStuffHash); + let inheritedTx = await web3.eth.getTransaction(inheritedHash); + let getterTx1 = await web3.eth.getTransaction(getterHash1); + let getterTx2 = await web3.eth.getTransaction(getterHash2); + let defaultConstructorTx = await web3.eth.getTransaction( + defaultConstructorHash + ); + + let constructorDecoding = (await decoder.decodeTransaction(constructorTx)) + .decoding; + let emitStuffDecoding = (await decoder.decodeTransaction(emitStuffTx)) + .decoding; + let moreStuffDecoding = (await decoder.decodeTransaction(moreStuffTx)) + .decoding; + let inheritedDecoding = (await decoder.decodeTransaction(inheritedTx)) + .decoding; + let getterDecoding1 = (await decoder.decodeTransaction(getterTx1)).decoding; + let getterDecoding2 = (await decoder.decodeTransaction(getterTx2)).decoding; + let defaultConstructorDecoding = (await decoder.decodeTransaction( + defaultConstructorTx + )).decoding; + + assert.strictEqual(constructorDecoding.kind, "constructor"); + assert.strictEqual(constructorDecoding.class.typeName, "WireTest"); + assert.lengthOf(constructorDecoding.arguments, 3); + assert.strictEqual(constructorDecoding.arguments[0].name, "status"); + assert.strictEqual( + ConversionUtils.nativize(constructorDecoding.arguments[0].value), + true + ); + assert.strictEqual(constructorDecoding.arguments[1].name, "info"); + assert.strictEqual( + ConversionUtils.nativize(constructorDecoding.arguments[1].value), + "0xdeadbeef" + ); + assert.strictEqual(constructorDecoding.arguments[2].name, "whoknows"); + assert.strictEqual( + ConversionUtils.nativize(constructorDecoding.arguments[2].value), + "WireTest.Ternary.MaybeSo" + ); + + assert.strictEqual(emitStuffDecoding.kind, "function"); + assert.strictEqual(emitStuffDecoding.abi.name, "emitStuff"); + assert.strictEqual(emitStuffDecoding.class.typeName, "WireTest"); + assert.lengthOf(emitStuffDecoding.arguments, 3); + assert.strictEqual(emitStuffDecoding.arguments[0].name, "p"); + assert.deepEqual( + ConversionUtils.nativize(emitStuffDecoding.arguments[0].value), + emitStuffArgs[0] + ); + assert.strictEqual(emitStuffDecoding.arguments[1].name, "precompiles"); + assert.deepEqual( + ConversionUtils.nativize(emitStuffDecoding.arguments[1].value), + emitStuffArgs[1] + ); + assert.strictEqual(emitStuffDecoding.arguments[2].name, "strings"); + assert.deepEqual( + ConversionUtils.nativize(emitStuffDecoding.arguments[2].value), + emitStuffArgs[2] + ); + + assert.strictEqual(moreStuffDecoding.kind, "function"); + assert.strictEqual(moreStuffDecoding.abi.name, "moreStuff"); + assert.strictEqual(moreStuffDecoding.class.typeName, "WireTest"); + assert.lengthOf(moreStuffDecoding.arguments, 2); + assert.strictEqual(moreStuffDecoding.arguments[0].name, "notThis"); + assert.strictEqual( + ConversionUtils.nativize(moreStuffDecoding.arguments[0].value), + `WireTest(${moreStuffArgs[0]})` + ); + assert.strictEqual(moreStuffDecoding.arguments[1].name, "bunchOfInts"); + assert.deepEqual( + ConversionUtils.nativize(moreStuffDecoding.arguments[1].value), + moreStuffArgs[1] + ); + + assert.strictEqual(inheritedDecoding.kind, "function"); + assert.strictEqual(inheritedDecoding.abi.name, "inherited"); + assert.strictEqual(inheritedDecoding.class.typeName, "WireTest"); //NOT WireTestParent + assert.lengthOf(inheritedDecoding.arguments, 1); + assert.isUndefined(inheritedDecoding.arguments[0].name); + assert.deepEqual( + ConversionUtils.nativize(inheritedDecoding.arguments[0].value), + inheritedArg + ); + + assert.strictEqual(defaultConstructorDecoding.kind, "constructor"); + assert.strictEqual( + defaultConstructorDecoding.class.typeName, + "WireTestParent" + ); + assert.isEmpty(defaultConstructorDecoding.arguments); + + assert.strictEqual(getterDecoding1.kind, "function"); + assert.strictEqual(getterDecoding1.abi.name, "deepStruct"); + assert.strictEqual(getterDecoding1.class.typeName, "WireTest"); + assert.lengthOf(getterDecoding1.arguments, 2); + assert.isUndefined(getterDecoding1.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(getterDecoding1.arguments[0].value), + getter1Args[0] + ); + assert.isUndefined(getterDecoding1.arguments[1].name); + assert.strictEqual( + ConversionUtils.nativize(getterDecoding1.arguments[1].value), + getter1Args[1] + ); + + assert.strictEqual(getterDecoding2.kind, "function"); + assert.strictEqual(getterDecoding2.abi.name, "deepString"); + assert.strictEqual(getterDecoding2.class.typeName, "WireTest"); + assert.lengthOf(getterDecoding2.arguments, 2); + assert.isUndefined(getterDecoding2.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(getterDecoding2.arguments[0].value), + getter2Args[0] + ); + assert.isUndefined(getterDecoding2.arguments[1].name); + assert.strictEqual( + ConversionUtils.nativize(getterDecoding2.arguments[1].value), + getter2Args[1] + ); + + //now for events! + let constructorBlock = constructorTx.blockNumber; + let emitStuffBlock = emitStuff.receipt.blockNumber; + let moreStuffBlock = moreStuff.receipt.blockNumber; + let inheritedBlock = inherited.receipt.blockNumber; + let indexTestBlock = indexTest.receipt.blockNumber; + let libraryTestBlock = libraryTest.receipt.blockNumber; + + try { + //due to web3's having ethers's crappy decoder built in, + //we have to put this in a try block to catch the error + await deployedContract.danger(); + } catch (_) { + //discard the error! + } + + let constructorEvents = await decoder.events({ + fromBlock: constructorBlock, + toBlock: constructorBlock + }); + let emitStuffEvents = await decoder.events({ + fromBlock: emitStuffBlock, + toBlock: emitStuffBlock + }); + let moreStuffEvents = await decoder.events({ + fromBlock: moreStuffBlock, + toBlock: moreStuffBlock + }); + let inheritedEvents = await decoder.events({ + fromBlock: inheritedBlock, + toBlock: inheritedBlock + }); + let indexTestEvents = await decoder.events({ + fromBlock: indexTestBlock, + toBlock: indexTestBlock + }); + let libraryTestEvents = await decoder.events({ + fromBlock: libraryTestBlock, + toBlock: libraryTestBlock + }); + //HACK -- since danger was last, we can just ask for the + //events from the latest block + let dangerEvents = await decoder.events(); + + assert.lengthOf(constructorEvents, 1); + let constructorEventDecodings = constructorEvents[0].decodings; + assert.lengthOf(constructorEventDecodings, 1); + let constructorEventDecoding = constructorEventDecodings[0]; + + assert.lengthOf(emitStuffEvents, 1); + let emitStuffEventDecodings = emitStuffEvents[0].decodings; + assert.lengthOf(emitStuffEventDecodings, 1); + let emitStuffEventDecoding = emitStuffEventDecodings[0]; + + assert.lengthOf(moreStuffEvents, 1); + let moreStuffEventDecodings = moreStuffEvents[0].decodings; + assert.lengthOf(moreStuffEventDecodings, 1); + let moreStuffEventDecoding = moreStuffEventDecodings[0]; + + assert.lengthOf(inheritedEvents, 1); + let inheritedEventDecodings = inheritedEvents[0].decodings; + assert.lengthOf(inheritedEventDecodings, 1); + let inheritedEventDecoding = inheritedEventDecodings[0]; + + assert.lengthOf(indexTestEvents, 1); + let indexTestEventDecodings = indexTestEvents[0].decodings; + assert.lengthOf(indexTestEventDecodings, 1); + let indexTestEventDecoding = indexTestEventDecodings[0]; + + assert.lengthOf(libraryTestEvents, 1); + let libraryTestEventDecodings = libraryTestEvents[0].decodings; + assert.lengthOf(libraryTestEventDecodings, 1); + let libraryTestEventDecoding = libraryTestEventDecodings[0]; + + assert.lengthOf(dangerEvents, 1); + let dangerEventDecodings = dangerEvents[0].decodings; + assert.lengthOf(dangerEventDecodings, 1); + let dangerEventDecoding = dangerEventDecodings[0]; + + assert.strictEqual(constructorEventDecoding.kind, "event"); + assert.strictEqual(constructorEventDecoding.class.typeName, "WireTest"); + assert.strictEqual(constructorEventDecoding.abi.name, "ConstructorEvent"); + assert.lengthOf(constructorEventDecoding.arguments, 3); + assert.strictEqual(constructorEventDecoding.arguments[0].name, "bit"); + assert.strictEqual( + ConversionUtils.nativize(constructorEventDecoding.arguments[0].value), + true + ); + assert.isUndefined(constructorEventDecoding.arguments[1].name); + assert.strictEqual( + ConversionUtils.nativize(constructorEventDecoding.arguments[1].value), + "0xdeadbeef" + ); + assert.isUndefined(constructorEventDecoding.arguments[2].name); + assert.strictEqual( + ConversionUtils.nativize(constructorEventDecoding.arguments[2].value), + "WireTest.Ternary.MaybeSo" + ); + + assert.strictEqual(emitStuffEventDecoding.kind, "event"); + assert.strictEqual(emitStuffEventDecoding.abi.name, "EmitStuff"); + assert.strictEqual(emitStuffEventDecoding.class.typeName, "WireTest"); + assert.lengthOf(emitStuffEventDecoding.arguments, 3); + assert.isUndefined(emitStuffEventDecoding.arguments[0].name); + assert.deepEqual( + ConversionUtils.nativize(emitStuffEventDecoding.arguments[0].value), + emitStuffArgs[0] + ); + assert.isUndefined(emitStuffEventDecoding.arguments[1].name); + assert.deepEqual( + ConversionUtils.nativize(emitStuffEventDecoding.arguments[1].value), + emitStuffArgs[1] + ); + assert.isUndefined(emitStuffEventDecoding.arguments[2].name); + assert.deepEqual( + ConversionUtils.nativize(emitStuffEventDecoding.arguments[2].value), + emitStuffArgs[2] + ); + + assert.strictEqual(moreStuffEventDecoding.kind, "event"); + assert.strictEqual(moreStuffEventDecoding.abi.name, "MoreStuff"); + assert.strictEqual(moreStuffEventDecoding.class.typeName, "WireTest"); + assert.lengthOf(moreStuffEventDecoding.arguments, 2); + assert.isUndefined(moreStuffEventDecoding.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(moreStuffEventDecoding.arguments[0].value), + `WireTest(${moreStuffArgs[0]})` + ); + assert.strictEqual(moreStuffEventDecoding.arguments[1].name, "data"); + assert.deepEqual( + ConversionUtils.nativize(moreStuffEventDecoding.arguments[1].value), + moreStuffArgs[1] + ); + + assert.strictEqual(inheritedEventDecoding.kind, "event"); + assert.strictEqual(inheritedEventDecoding.abi.name, "Done"); + assert.strictEqual(inheritedEventDecoding.class.typeName, "WireTest"); //NOT WireTestParent + assert.isEmpty(inheritedEventDecoding.arguments); + + assert.strictEqual(indexTestEventDecoding.kind, "event"); + assert.strictEqual(indexTestEventDecoding.abi.name, "HasIndices"); + assert.strictEqual(indexTestEventDecoding.class.typeName, "WireTest"); + assert.lengthOf(indexTestEventDecoding.arguments, 5); + assert.isUndefined(indexTestEventDecoding.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(indexTestEventDecoding.arguments[0].value), + indexTestArgs[0] + ); + assert.isUndefined(indexTestEventDecoding.arguments[1].name); + assert.deepEqual( + ConversionUtils.nativize(indexTestEventDecoding.arguments[1].value), + indexTestArgs[1] + ); + assert.isUndefined(indexTestEventDecoding.arguments[2].name); + assert.deepEqual( + ConversionUtils.nativize(indexTestEventDecoding.arguments[2].value), + indexTestArgs[2] + ); + assert.isUndefined(indexTestEventDecoding.arguments[3].name); + assert.isUndefined( + ConversionUtils.nativize(indexTestEventDecoding.arguments[3].value) //can't decode indexed reference type! + ); + assert.isUndefined(indexTestEventDecoding.arguments[4].name); + assert.deepEqual( + ConversionUtils.nativize(indexTestEventDecoding.arguments[4].value), + indexTestArgs[4] + ); + + assert.strictEqual(libraryTestEventDecoding.kind, "event"); + assert.strictEqual(libraryTestEventDecoding.abi.name, "LibraryEvent"); + assert.strictEqual( + libraryTestEventDecoding.class.typeName, + "WireTestLibrary" + ); + assert.lengthOf(libraryTestEventDecoding.arguments, 1); + assert.isUndefined(libraryTestEventDecoding.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(libraryTestEventDecoding.arguments[0].value), + libraryTestArg + ); + + assert.strictEqual(dangerEventDecoding.kind, "event"); + assert.strictEqual(dangerEventDecoding.abi.name, "Danger"); + assert.lengthOf(dangerEventDecoding.arguments, 1); + assert.isUndefined(dangerEventDecoding.arguments[0].name); + assert.strictEqual( + ConversionUtils.nativize(dangerEventDecoding.arguments[0].value), + `WireTest(${address}).danger` + ); + }); + + it("disambiguates events when possible and not when impossible", async () => { + let deployedContract = await WireTest.deployed(); + + const decoder = await TruffleDecoder.forProject( + [WireTest, WireTestParent, WireTestLibrary], + web3.currentProvider + ); + + //HACK HACK -- we're going to repeatedly apply the hack from above + //because ethers also can't handle ambiguous events + try { + await deployedContract.ambiguityTest(); + } catch (_) { + //discard the error! + } + let ambiguityTestEvents = await decoder.events(); + try { + await deployedContract.unambiguityTest(); + } catch (_) { + //discard the error! + } + let unambiguityTestEvents = await decoder.events(); + + assert.lengthOf(ambiguityTestEvents, 1); + let ambiguityTestEventDecodings = ambiguityTestEvents[0].decodings; + assert.lengthOf(ambiguityTestEventDecodings, 2); //it's ambiguous! + //contract should always come before libraries + let ambiguityTestContractDecoding = ambiguityTestEventDecodings[0]; + let ambiguityTestLibraryDecoding = ambiguityTestEventDecodings[1]; + + assert.lengthOf(unambiguityTestEvents, 4); + for (let event of unambiguityTestEvents) { + assert.lengthOf(event.decodings, 1); //they're unambiguous! + } + let unambiguousDecodings = unambiguityTestEvents.map( + ({ decodings }) => decodings[0] + ); + assert.strictEqual(ambiguityTestContractDecoding.kind, "event"); + assert.strictEqual( + ambiguityTestContractDecoding.abi.name, + "AmbiguousEvent" + ); + assert.strictEqual( + ambiguityTestContractDecoding.class.typeName, + "WireTest" + ); + assert.lengthOf(ambiguityTestContractDecoding.arguments, 2); + assert.isUndefined( + ConversionUtils.nativize(ambiguityTestContractDecoding.arguments[0].value) + ); + assert.deepEqual( + ConversionUtils.nativize( + ambiguityTestContractDecoding.arguments[1].value + ), + [32, 3, 17, 18, 19] + ); + + assert.strictEqual(ambiguityTestLibraryDecoding.kind, "event"); + assert.strictEqual(ambiguityTestLibraryDecoding.abi.name, "AmbiguousEvent"); + assert.strictEqual( + ambiguityTestLibraryDecoding.class.typeName, + "WireTestLibrary" + ); + assert.lengthOf(ambiguityTestLibraryDecoding.arguments, 2); + assert.deepEqual( + ConversionUtils.nativize(ambiguityTestLibraryDecoding.arguments[0].value), + [17, 18, 19] + ); + assert.isUndefined( + ConversionUtils.nativize(ambiguityTestLibraryDecoding.arguments[1].value) + ); + + for (let decoding of unambiguousDecodings) { + assert.strictEqual(decoding.kind, "event"); + assert.strictEqual(decoding.abi.name, "AmbiguousEvent"); + } + + assert.strictEqual(unambiguousDecodings[0].class.typeName, "WireTest"); + assert.lengthOf(unambiguousDecodings[0].arguments, 2); + assert.isUndefined( + ConversionUtils.nativize(unambiguousDecodings[0].arguments[0].value) + ); + assert.deepEqual( + ConversionUtils.nativize(unambiguousDecodings[0].arguments[1].value), + [32, 1e12, 17, 18, 19] + ); + + assert.strictEqual(unambiguousDecodings[1].class.typeName, "WireTest"); + assert.lengthOf(unambiguousDecodings[1].arguments, 2); + assert.isUndefined( + ConversionUtils.nativize(unambiguousDecodings[1].arguments[0].value) + ); + assert.deepEqual( + ConversionUtils.nativize(unambiguousDecodings[1].arguments[1].value), + [32, 3, 257, 257, 257] + ); + + assert.strictEqual(unambiguousDecodings[2].class.typeName, "WireTest"); + assert.lengthOf(unambiguousDecodings[2].arguments, 2); + assert.isUndefined( + ConversionUtils.nativize(unambiguousDecodings[2].arguments[0].value) + ); + assert.deepEqual( + ConversionUtils.nativize(unambiguousDecodings[2].arguments[1].value), + [64, 0, 2, 1, 1] + ); + + assert.strictEqual( + unambiguousDecodings[3].class.typeName, + "WireTestLibrary" + ); + assert.lengthOf(unambiguousDecodings[3].arguments, 2); + assert.deepEqual( + ConversionUtils.nativize(unambiguousDecodings[3].arguments[0].value), + [107] + ); + assert.isUndefined( + ConversionUtils.nativize(unambiguousDecodings[3].arguments[1].value) + ); + }); + + it("Handles anonymous events", async () => { + let deployedContract = await WireTest.deployed(); + + const decoder = await TruffleDecoder.forProject( + [WireTest, WireTestParent, WireTestLibrary], + web3.currentProvider + ); + + //thankfully, ethers ignores anonymous events, + //so we don't need to use that hack here + let anonymousTest = await deployedContract.anonymousTest(); + let block = anonymousTest.blockNumber; + let anonymousTestEvents = await decoder.events({ + fromBlock: block, + toBlock: block + }); + //also, let's do a test with a specified name + let specifiedNameEvents = await decoder.events({ + name: "AnonUint8s", + fromBlock: block, + toBlock: block + }); + + assert.lengthOf(anonymousTestEvents, 4); + + assert.lengthOf(anonymousTestEvents[0].decodings, 1); + assert.strictEqual(anonymousTestEvents[0].decodings[0].kind, "anonymous"); + assert.strictEqual( + anonymousTestEvents[0].decodings[0].abi.name, + "AnonUints" + ); + assert.strictEqual( + anonymousTestEvents[0].decodings[0].class.typeName, + "WireTest" + ); + assert.lengthOf(anonymousTestEvents[0].decodings[0].arguments, 4); + assert.deepEqual( + anonymousTestEvents[0].decodings[0].arguments.map(({ value }) => + ConversionUtils.nativize(value) + ), + [257, 1, 1, 1] + ); + + assert.lengthOf(anonymousTestEvents[1].decodings, 2); + assert.strictEqual(anonymousTestEvents[1].decodings[0].kind, "anonymous"); + assert.strictEqual( + anonymousTestEvents[1].decodings[0].abi.name, + "AnonUints" + ); + assert.strictEqual( + anonymousTestEvents[1].decodings[0].class.typeName, + "WireTest" + ); + assert.lengthOf(anonymousTestEvents[1].decodings[0].arguments, 4); + assert.deepEqual( + anonymousTestEvents[1].decodings[0].arguments.map(({ value }) => + ConversionUtils.nativize(value) + ), + [1, 2, 3, 4] + ); + assert.strictEqual(anonymousTestEvents[1].decodings[1].kind, "anonymous"); + assert.strictEqual( + anonymousTestEvents[1].decodings[1].abi.name, + "AnonUint8s" + ); + assert.strictEqual( + anonymousTestEvents[1].decodings[1].class.typeName, + "WireTestLibrary" + ); + assert.lengthOf(anonymousTestEvents[1].decodings[1].arguments, 4); + assert.deepEqual( + anonymousTestEvents[1].decodings[1].arguments.map(({ value }) => + ConversionUtils.nativize(value) + ), + [1, 2, 3, 4] + ); + + assert.lengthOf(anonymousTestEvents[2].decodings, 2); + assert.strictEqual(anonymousTestEvents[2].decodings[0].kind, "event"); + assert.strictEqual(anonymousTestEvents[2].decodings[0].abi.name, "NonAnon"); + assert.strictEqual( + anonymousTestEvents[2].decodings[0].class.typeName, + "WireTest" + ); + assert.lengthOf(anonymousTestEvents[2].decodings[0].arguments, 3); + assert.deepEqual( + anonymousTestEvents[2].decodings[0].arguments.map(({ value }) => + ConversionUtils.nativize(value) + ), + [1, 2, 3] + ); + let selector = anonymousTestEvents[2].decodings[0].selector; + assert.strictEqual(anonymousTestEvents[2].decodings[1].kind, "anonymous"); + assert.strictEqual( + anonymousTestEvents[2].decodings[1].abi.name, + "AnonUints" + ); + assert.strictEqual( + anonymousTestEvents[2].decodings[1].class.typeName, + "WireTest" + ); + assert.lengthOf(anonymousTestEvents[2].decodings[1].arguments, 4); + assert.deepEqual( + anonymousTestEvents[2].decodings[1].arguments + .slice(1) + .map(({ value }) => ConversionUtils.nativize(value)), + [1, 2, 3] + ); + assert( + anonymousTestEvents[2].decodings[1].arguments[0].value.value.asBN.eq( + new BN(selector.slice(2), 16) + ) + ); + + assert.lengthOf(anonymousTestEvents[3].decodings, 1); + assert.strictEqual(anonymousTestEvents[3].decodings[0].kind, "anonymous"); + assert.strictEqual( + anonymousTestEvents[3].decodings[0].abi.name, + "ObviouslyAnon" + ); + assert.strictEqual( + anonymousTestEvents[3].decodings[0].class.typeName, + "WireTest" + ); + assert.lengthOf(anonymousTestEvents[3].decodings[0].arguments, 1); + assert.strictEqual( + ConversionUtils.nativize( + anonymousTestEvents[3].decodings[0].arguments[0].value + ), + "0xfe" + ); + + //now, let's test the specified name events + assert.lengthOf(specifiedNameEvents, 1); + let specifiedNameEvent = specifiedNameEvents[0]; + assert.lengthOf(specifiedNameEvent.decodings, 1); + let specifiedNameDecoding = specifiedNameEvent.decodings[0]; + assert.strictEqual(specifiedNameDecoding.kind, "anonymous"); + assert.strictEqual(specifiedNameDecoding.abi.name, "AnonUint8s"); + assert.strictEqual(specifiedNameDecoding.class.typeName, "WireTestLibrary"); + assert.lengthOf(specifiedNameDecoding.arguments, 4); + assert.deepEqual( + specifiedNameDecoding.arguments.map(({ value }) => + ConversionUtils.nativize(value) + ), + [1, 2, 3, 4] + ); + }); +}); diff --git a/packages/truffle-decoder/test/truffle-config.js b/packages/truffle-decoder/test/truffle-config.js index 3af7faaa4f7..54cd42d45d8 100644 --- a/packages/truffle-decoder/test/truffle-config.js +++ b/packages/truffle-decoder/test/truffle-config.js @@ -57,7 +57,7 @@ module.exports = { // Configure your compilers compilers: { solc: { - version: "0.4.23" // Fetch exact version from solc-bin (default: truffle's version) + version: "0.5.10" // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) // settings: { // See the solidity docs for advice about optimization and evmVersion // optimizer: { diff --git a/yarn.lock b/yarn.lock index 3d8c7689b4d..7bcac26e5e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1030,6 +1030,20 @@ dependencies: "@types/lodash" "*" +"@types/lodash.partition@^4.6.6": + version "4.6.6" + resolved "https://registry.yarnpkg.com/@types/lodash.partition/-/lodash.partition-4.6.6.tgz#fdc23c9809b64b1d2e2f07faef045c035f0cd2c7" + integrity sha512-s8ZNNFWhBgTKI4uNxVrTs3Aa7UQoi7Fesw55bfpBBMCLda+uSuwDyuax8ka9aBy8Ccsjp2SiS034DkSZa+CzVA== + dependencies: + "@types/lodash" "*" + +"@types/lodash.sum@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/lodash.sum/-/lodash.sum-4.0.6.tgz#acae9c8b24390a974667565acf17288e98dcdb8e" + integrity sha512-mqfBmDFruS3JFSAI/kPCEdFdOZvL4MJnakntlgPBUIYEwSTqxTZsENyWOgs1EMHq7+lU+jgs3yEtrljaUvvALQ== + dependencies: + "@types/lodash" "*" + "@types/lodash@*", "@types/lodash@^4.14.116": version "4.14.133" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.133.tgz#430721c96da22dd1694443e68e6cec7ba1c1003d" @@ -1097,7 +1111,7 @@ resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-2.1.6.tgz#430cabb71a42d0a3613cce5621324fe4f5a25753" integrity sha512-pRs2gYF5yoKYrgSaira0DJqVg2tFuF+Qjp838xS7K+mJyY2jJzjsrl6y17GbIa4uMRogMbxs+ghNCvKg6XyNrA== -"@types/web3@1.0.18", "@types/web3@^1.0.18", "@types/web3@^1.0.5": +"@types/web3@1.0.18", "@types/web3@^1.0.18": version "1.0.18" resolved "https://registry.yarnpkg.com/@types/web3/-/web3-1.0.18.tgz#87a8651041d21fc37602ff02327df2c7ecf105d1" integrity sha512-uXQL0LDszt2f476LEmYM6AvSv9F4vU4hWQvlUhwfLHNlIB6OyBXoYsCzWAIhhnc5U0HA7ZBcPybxRJ/yfA6THg== @@ -1105,6 +1119,14 @@ "@types/bn.js" "*" "@types/underscore" "*" +"@types/web3@^1.0.19": + version "1.0.19" + resolved "https://registry.yarnpkg.com/@types/web3/-/web3-1.0.19.tgz#46b85d91d398ded9ab7c85a5dd57cb33ac558924" + integrity sha512-fhZ9DyvDYDwHZUp5/STa9XW2re0E8GxoioYJ4pEUZ13YHpApSagixj7IAdoYH5uAK+UalGq6Ml8LYzmgRA/q+A== + dependencies: + "@types/bn.js" "*" + "@types/underscore" "*" + "@webassemblyjs/ast@1.8.5": version "1.8.5" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" @@ -1279,13 +1301,6 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abi-decoder@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/abi-decoder/-/abi-decoder-1.2.0.tgz#c42882dbb91b444805f0cd203a87a5cc3c22f4a8" - integrity sha512-y2OKSEW4gf2838Eavc56vQY9V46zaXkf3Jl1WpTfUBbzAVrXSr4JRZAAWv55Tv9s5WNz1rVgBgz5d2aJIL1QCg== - dependencies: - web3 "^0.18.4" - abstract-leveldown@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-3.0.0.tgz#5cb89f958a44f526779d740d1440e743e0c30a57" @@ -2682,10 +2697,6 @@ bignumber.js@^7.2.1: version "2.0.7" resolved "git+https://github.com/debris/bignumber.js#c7a38de919ed75e6fb6ba38051986e294b328df9" -"bignumber.js@git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2": - version "2.0.7" - resolved "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" - "bignumber.js@git+https://github.com/frozeman/bignumber.js-nolookahead.git": version "2.0.7" resolved "git+https://github.com/frozeman/bignumber.js-nolookahead.git#57692b3ecfc98bbdd6b3a516cb2353652ea49934" @@ -9352,6 +9363,11 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.partition@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.partition/-/lodash.partition-4.6.0.tgz#a38e46b73469e0420b0da1212e66d414be364ba4" + integrity sha1-o45GtzRp4EILDaEhLmbUFL42S6Q= + lodash.pick@^4.2.1: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -12917,6 +12933,11 @@ semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== +semver@^6.1.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -15128,6 +15149,15 @@ web3-core-helpers@1.0.0-beta.35: web3-eth-iban "1.0.0-beta.35" web3-utils "1.0.0-beta.35" +web3-core-helpers@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.0.0-beta.37.tgz#04ec354b7f5c57234c309eea2bda9bf1f2fe68ba" + integrity sha512-efaLOzN28RMnbugnyelgLwPWWaSwElQzcAJ/x3PZu+uPloM/lE5x0YuBKvIh7/PoSMlHqtRWj1B8CpuQOUQ5Ew== + dependencies: + underscore "1.8.3" + web3-eth-iban "1.0.0-beta.37" + web3-utils "1.0.0-beta.37" + web3-core-helpers@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-core-helpers/-/web3-core-helpers-1.0.0-beta.55.tgz#832b8499889f9f514b1d174f00172fd3683d63de" @@ -15159,6 +15189,17 @@ web3-core-method@1.0.0-beta.35: web3-core-subscriptions "1.0.0-beta.35" web3-utils "1.0.0-beta.35" +web3-core-method@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.0.0-beta.37.tgz#53d148e63f818b23461b26307afdfbdaa9457744" + integrity sha512-pKWFUeqnVmzx3VrZg+CseSdrl/Yrk2ioid/HzolNXZE6zdoITZL0uRjnsbqXGEzgRRd1Oe/pFndpTlRsnxXloA== + dependencies: + underscore "1.8.3" + web3-core-helpers "1.0.0-beta.37" + web3-core-promievent "1.0.0-beta.37" + web3-core-subscriptions "1.0.0-beta.37" + web3-utils "1.0.0-beta.37" + web3-core-method@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-core-method/-/web3-core-method-1.0.0-beta.55.tgz#0af994295ac2dd64ccd53305b7df8da76e11da49" @@ -15192,6 +15233,14 @@ web3-core-promievent@1.0.0-beta.35: any-promise "1.3.0" eventemitter3 "1.1.1" +web3-core-promievent@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.0.0-beta.37.tgz#4e51c469d0a7ac0a969885a4dbcde8504abe5b02" + integrity sha512-GTF2r1lP8nJBeA5Gxq5yZpJy9l8Fb9CXGZPfF8jHvaRdQHtm2Z+NDhqYmF833lcdkokRSyfPcXlz1mlWeClFpg== + dependencies: + any-promise "1.3.0" + eventemitter3 "1.1.1" + web3-core-promievent@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.2.1.tgz#003e8a3eb82fb27b6164a6d5b9cad04acf733838" @@ -15211,6 +15260,17 @@ web3-core-requestmanager@1.0.0-beta.35: web3-providers-ipc "1.0.0-beta.35" web3-providers-ws "1.0.0-beta.35" +web3-core-requestmanager@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.0.0-beta.37.tgz#721a75df5920621bff42d9d74f7a64413675d56b" + integrity sha512-66VUqye5BGp1Zz1r8psCxdNH+GtTjaFwroum2Osx+wbC5oRjAiXkkadiitf6wRb+edodjEMPn49u7B6WGNuewQ== + dependencies: + underscore "1.8.3" + web3-core-helpers "1.0.0-beta.37" + web3-providers-http "1.0.0-beta.37" + web3-providers-ipc "1.0.0-beta.37" + web3-providers-ws "1.0.0-beta.37" + web3-core-requestmanager@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.2.1.tgz#fa2e2206c3d738db38db7c8fe9c107006f5c6e3d" @@ -15231,6 +15291,15 @@ web3-core-subscriptions@1.0.0-beta.35: underscore "1.8.3" web3-core-helpers "1.0.0-beta.35" +web3-core-subscriptions@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.0.0-beta.37.tgz#40de5e2490cc05b15faa8f935c97fd48d670cd9a" + integrity sha512-FdXl8so9kwkRRWziuCSpFsAuAdg9KvpXa1fQlT16uoGcYYfxwFO/nkwyBGQzkZt7emShI2IRugcazyPCZDwkOA== + dependencies: + eventemitter3 "1.1.1" + underscore "1.8.3" + web3-core-helpers "1.0.0-beta.37" + web3-core-subscriptions@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-core-subscriptions/-/web3-core-subscriptions-1.0.0-beta.55.tgz#105902c13db53466fc17d07a981ad3d41c700f76" @@ -15259,6 +15328,16 @@ web3-core@1.0.0-beta.35: web3-core-requestmanager "1.0.0-beta.35" web3-utils "1.0.0-beta.35" +web3-core@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.0.0-beta.37.tgz#66c2c7000772c9db36d737ada31607ace09b7e90" + integrity sha512-cIwEqCj7OJyefQNauI0HOgW4sSaOQ98V99H2/HEIlnCZylsDzfw7gtQUdwnRFiIyIxjbWy3iWsjwDPoXNPZBYg== + dependencies: + web3-core-helpers "1.0.0-beta.37" + web3-core-method "1.0.0-beta.37" + web3-core-requestmanager "1.0.0-beta.37" + web3-utils "1.0.0-beta.37" + web3-core@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-1.0.0-beta.55.tgz#26b9abbf1bc1837c9cc90f06ecbc4ed714f89b53" @@ -15292,16 +15371,6 @@ web3-eth-abi@1.0.0-beta.35: web3-core-helpers "1.0.0-beta.35" web3-utils "1.0.0-beta.35" -web3-eth-abi@1.0.0-beta.52: - version "1.0.0-beta.52" - resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.0.0-beta.52.tgz#88dc2d36e2f99dfe255f8f64b6f613bad82779d8" - integrity sha512-c03sH6y7ncp9tBPt0EZEcyFyou4kyYdr72VJMY8ip0JAfZgl4WI9XcGpD207z0lR4Ki1PSCfkh+ZigoXxggouw== - dependencies: - "@babel/runtime" "^7.3.1" - ethers "^4.0.27" - lodash "^4.17.11" - web3-utils "1.0.0-beta.52" - web3-eth-abi@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-1.0.0-beta.55.tgz#69250420039346105a3d0f899c0a8a53be926f97" @@ -15458,6 +15527,14 @@ web3-eth-iban@1.0.0-beta.35: bn.js "4.11.6" web3-utils "1.0.0-beta.35" +web3-eth-iban@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.0.0-beta.37.tgz#313a3f18ae2ab00ba98678ea1156b09ef32a3655" + integrity sha512-WQRniGJFxH/XCbd7miO6+jnUG+6bvuzfeufPIiOtCbeIC1ypp1kSqER8YVBDrTyinU1xnf1U5v0KBZ2yiWBJxQ== + dependencies: + bn.js "4.11.6" + web3-utils "1.0.0-beta.37" + web3-eth-iban@1.0.0-beta.55: version "1.0.0-beta.55" resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-1.0.0-beta.55.tgz#15146a69de21addc99e7dbfb2920555b1e729637" @@ -15660,6 +15737,14 @@ web3-providers-http@1.0.0-beta.35: web3-core-helpers "1.0.0-beta.35" xhr2-cookies "1.1.0" +web3-providers-http@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.0.0-beta.37.tgz#c06efd60e16e329e25bd268d2eefc68d82d13651" + integrity sha512-FM/1YDB1jtZuTo78habFj7S9tNHoqt0UipdyoQV29b8LkGKZV9Vs3is8L24hzuj1j/tbwkcAH+ewIseHwu0DTg== + dependencies: + web3-core-helpers "1.0.0-beta.37" + xhr2-cookies "1.1.0" + web3-providers-http@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-1.2.1.tgz#c93ea003a42e7b894556f7e19dd3540f947f5013" @@ -15677,6 +15762,15 @@ web3-providers-ipc@1.0.0-beta.35: underscore "1.8.3" web3-core-helpers "1.0.0-beta.35" +web3-providers-ipc@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.0.0-beta.37.tgz#55d247e7197257ca0c3e4f4b0fe1561311b9d5b9" + integrity sha512-NdRPRxYMIU0C3u18NI8u4bwbhI9pCg5nRgDGYcmSAx5uOBxiYcQy+hb0WkJRRhBoyIXJmy+s26FoH8904+UnPg== + dependencies: + oboe "2.1.3" + underscore "1.8.3" + web3-core-helpers "1.0.0-beta.37" + web3-providers-ipc@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-1.2.1.tgz#017bfc687a8fc5398df2241eb98f135e3edd672c" @@ -15695,6 +15789,15 @@ web3-providers-ws@1.0.0-beta.35: web3-core-helpers "1.0.0-beta.35" websocket "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible" +web3-providers-ws@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.0.0-beta.37.tgz#77c15aebc00b75d760d22d063ac2e415bdbef72f" + integrity sha512-8p6ZLv+1JYa5Vs8oBn33Nn3VGFBbF+wVfO+b78RJS1Qf1uIOzjFVDk3XwYDD7rlz9G5BKpxhaQw+6EGQ7L02aw== + dependencies: + underscore "1.8.3" + web3-core-helpers "1.0.0-beta.37" + websocket "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible" + web3-providers-ws@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-1.2.1.tgz#2d941eaf3d5a8caa3214eff8dc16d96252b842cb" @@ -15768,20 +15871,17 @@ web3-utils@1.0.0-beta.35: underscore "1.8.3" utf8 "2.1.1" -web3-utils@1.0.0-beta.52: - version "1.0.0-beta.52" - resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.0.0-beta.52.tgz#27f9beeac3e1ea981eba9824d79e2971f156eebc" - integrity sha512-WdHyzPcZu/sOnNrkcOZT20QEX9FhwD9OJJXENojQNvMK2a1xo3n8JWBcC2gzAGwsa0Aah6z2B3Xwa1P//8FaoA== +web3-utils@1.0.0-beta.37: + version "1.0.0-beta.37" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.0.0-beta.37.tgz#ab868a90fe5e649337e38bdaf72133fcbf4d414d" + integrity sha512-kA1fyhO8nKgU21wi30oJQ/ssvu+9srMdjOTKbHYbQe4ATPcr5YNwwrxG3Bcpbu1bEwRUVKHCkqi+wTvcAWBdlQ== dependencies: - "@babel/runtime" "^7.3.1" - "@types/bn.js" "^4.11.4" - "@types/node" "^10.12.18" - bn.js "4.11.8" - eth-lib "0.2.8" - ethjs-unit "^0.1.6" - lodash "^4.17.11" + bn.js "4.11.6" + eth-lib "0.1.27" + ethjs-unit "0.1.6" number-to-bn "1.7.0" randomhex "0.1.5" + underscore "1.8.3" utf8 "2.1.1" web3-utils@1.0.0-beta.55: @@ -15849,17 +15949,6 @@ web3@^0.16.0: utf8 "^2.1.1" xmlhttprequest "*" -web3@^0.18.4: - version "0.18.4" - resolved "https://registry.yarnpkg.com/web3/-/web3-0.18.4.tgz#81ec1784145491f2eaa8955b31c06049e07c5e7d" - integrity sha1-gewXhBRUkfLqqJVbMcBgSeB8Xn0= - dependencies: - bignumber.js "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" - crypto-js "^3.1.4" - utf8 "^2.1.1" - xhr2 "*" - xmlhttprequest "*" - web3@^0.20.1: version "0.20.7" resolved "https://registry.yarnpkg.com/web3/-/web3-0.20.7.tgz#1605e6d81399ed6f85a471a4f3da0c8be57df2f7" @@ -16337,11 +16426,6 @@ xhr2-cookies@1.1.0, xhr2-cookies@^1.1.0: dependencies: cookiejar "^2.1.1" -xhr2@*: - version "0.2.0" - resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.2.0.tgz#eddeff782f3b7551061b8d75645085269396e521" - integrity sha512-BDtiD0i2iKPK/S8OAZfpk6tyzEDnKKSjxWHcMBVmh+LuqJ8A32qXTyOx+TVOg2dKvq6zGBq2sgKPkEeRs1qTRA== - xhr@^2.0.4, xhr@^2.2.0, xhr@^2.3.3: version "2.5.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.5.0.tgz#bed8d1676d5ca36108667692b74b316c496e49dd"