From 13f50abd847f7ddcc7e54c102da54e2d23b86fae Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Tue, 7 Jan 2020 19:46:51 -0500 Subject: [PATCH] Better property access on ABI decoded results (#698). --- packages/abi/src.ts/abi-coder.ts | 14 +-- packages/abi/src.ts/coders/abstract-coder.ts | 4 + packages/abi/src.ts/index.ts | 6 +- packages/abi/src.ts/interface.ts | 120 +++++++++---------- packages/contracts/src.ts/index.ts | 78 ++++++------ 5 files changed, 110 insertions(+), 112 deletions(-) diff --git a/packages/abi/src.ts/abi-coder.ts b/packages/abi/src.ts/abi-coder.ts index c607b1302..376f739ba 100644 --- a/packages/abi/src.ts/abi-coder.ts +++ b/packages/abi/src.ts/abi-coder.ts @@ -9,7 +9,7 @@ import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; const logger = new Logger(version); -import { Coder, Reader, Writer } from "./coders/abstract-coder"; +import { Coder, Reader, Result, Writer } from "./coders/abstract-coder"; import { AddressCoder } from "./coders/address"; import { ArrayCoder } from "./coders/array"; import { BooleanCoder } from "./coders/boolean"; @@ -99,17 +99,17 @@ export class AbiCoder { }); } - let coders = types.map((type) => this._getCoder(ParamType.from(type))); - let coder = (new TupleCoder(coders, "_")); + const coders = types.map((type) => this._getCoder(ParamType.from(type))); + const coder = (new TupleCoder(coders, "_")); - let writer = this._getWriter(); + const writer = this._getWriter(); coder.encode(writer, values); return writer.data; } - decode(types: Array, data: BytesLike): any { - let coders: Array = types.map((type) => this._getCoder(ParamType.from(type))); - let coder = new TupleCoder(coders, "_"); + decode(types: Array, data: BytesLike): Result { + const coders: Array = types.map((type) => this._getCoder(ParamType.from(type))); + const coder = new TupleCoder(coders, "_"); return coder.decode(this._getReader(arrayify(data))); } } diff --git a/packages/abi/src.ts/coders/abstract-coder.ts b/packages/abi/src.ts/coders/abstract-coder.ts index 704eefeb1..c3d11e095 100644 --- a/packages/abi/src.ts/coders/abstract-coder.ts +++ b/packages/abi/src.ts/coders/abstract-coder.ts @@ -8,6 +8,10 @@ import { Logger } from "@ethersproject/logger"; import { version } from "../_version"; const logger = new Logger(version); +export interface Result extends Array { + [key: string]: any; +} + export type CoerceFunc = (type: string, value: any) => any; export abstract class Coder { diff --git a/packages/abi/src.ts/index.ts b/packages/abi/src.ts/index.ts index 5b4e2047c..f954a4009 100644 --- a/packages/abi/src.ts/index.ts +++ b/packages/abi/src.ts/index.ts @@ -2,7 +2,7 @@ import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments"; import { AbiCoder, CoerceFunc, defaultAbiCoder } from "./abi-coder"; -import { Indexed, Interface } from "./interface"; +import { Indexed, Interface, Result } from "./interface"; export { ConstructorFragment, @@ -23,5 +23,7 @@ export { CoerceFunc, JsonFragment, - JsonFragmentType + JsonFragmentType, + + Result }; diff --git a/packages/abi/src.ts/interface.ts b/packages/abi/src.ts/interface.ts index bf4d760a2..54b54bcc4 100644 --- a/packages/abi/src.ts/interface.ts +++ b/packages/abi/src.ts/interface.ts @@ -8,43 +8,41 @@ import { keccak256 } from "@ethersproject/keccak256" import { defineReadOnly, Description, getStatic } from "@ethersproject/properties"; import { AbiCoder, defaultAbiCoder } from "./abi-coder"; -import { ConstructorFragment, EventFragment, Fragment, FunctionFragment, JsonFragment, ParamType } from "./fragments"; +import { Result } from "./coders/abstract-coder"; +import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, ParamType } from "./fragments"; import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; const logger = new Logger(version); -export class LogDescription extends Description { +export { Result }; + +export class LogDescription extends Description { readonly eventFragment: EventFragment; readonly name: string; readonly signature: string; readonly topic: string; - readonly values: any + readonly args: Result } -export class TransactionDescription extends Description { +export class TransactionDescription extends Description { readonly functionFragment: FunctionFragment; readonly name: string; - readonly args: Array; + readonly args: Result; readonly signature: string; readonly sighash: string; readonly value: BigNumber; } -export class Indexed extends Description { +export class Indexed extends Description { readonly hash: string; + readonly _isIndexed: boolean; static isIndexed(value: any): value is Indexed { return !!(value && value._isIndexed); } } -export class Result { - [key: string]: any; - [key: number]: any; -} - - export class Interface { readonly fragments: Array; @@ -110,29 +108,31 @@ export class Interface { bucket[signature] = fragment; }); - // Add any fragments with a unique name by its name (sans signature parameters) - /* - [this.events, this.functions].forEach((bucket) => { - let count = getNameCount(bucket); - Object.keys(bucket).forEach((signature) => { - let fragment = bucket[signature]; - if (count[fragment.name] !== 1) { - logger.warn("duplicate definition - " + fragment.name); - return; - } - bucket[fragment.name] = fragment; - }); - }); - */ - // If we do not have a constructor use the default "constructor() payable" if (!this.deploy) { - defineReadOnly(this, "deploy", ConstructorFragment.from( { type: "constructor" } )); + defineReadOnly(this, "deploy", ConstructorFragment.from({ type: "constructor" })); } defineReadOnly(this, "_isInterface", true); } + format(format?: string): string | Array { + if (!format) { format = FormatTypes.full; } + if (format === FormatTypes.sighash) { + logger.throwArgumentError("interface does not support formating sighash", "format", format); + } + + const abi = this.fragments.map((fragment) => fragment.format(format)); + + // We need to re-bundle the JSON fragments a bit + if (format === FormatTypes.json) { + return JSON.stringify(abi.map((j) => JSON.parse(j))); + } + + return abi; + } + + // Sub-classes can override these to handle other blockchains static getAbiCoder(): AbiCoder { return defaultAbiCoder; } @@ -141,14 +141,15 @@ export class Interface { return getAddress(address); } - _sighashify(functionFragment: FunctionFragment): string { + static getSighash(functionFragment: FunctionFragment): string { return hexDataSlice(id(functionFragment.format()), 0, 4); } - _topicify(eventFragment: EventFragment): string { + static getTopic(eventFragment: EventFragment): string { return id(eventFragment.format()); } + // Find a function definition by any means necessary (unless it is ambiguous) getFunction(nameOrSignatureOrSighash: string): FunctionFragment { if (isHexString(nameOrSignatureOrSighash)) { for (const name in this.functions) { @@ -180,6 +181,7 @@ export class Interface { return result; } + // Find an event definition by any means necessary (unless it is ambiguous) getEvent(nameOrSignatureOrTopic: string): EventFragment { if (isHexString(nameOrSignatureOrTopic)) { const topichash = nameOrSignatureOrTopic.toLowerCase(); @@ -212,24 +214,26 @@ export class Interface { return result; } + // Get the sighash (the bytes4 selector) used by Solidity to identify a function getSighash(functionFragment: FunctionFragment | string): string { if (typeof(functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } - return this._sighashify(functionFragment); + return getStatic<(f: FunctionFragment) => string>(this.constructor, "getSighash")(functionFragment); } + // Get the topic (the bytes32 hash) used by Solidity to identify an event getEventTopic(eventFragment: EventFragment | string): string { if (typeof(eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); } - return this._topicify(eventFragment); + return getStatic<(e: EventFragment) => string>(this.constructor, "getTopic")(eventFragment); } - _decodeParams(params: Array, data: BytesLike): Array { + _decodeParams(params: Array, data: BytesLike): Result { return this._abiCoder.decode(params, data) } @@ -241,7 +245,8 @@ export class Interface { return this._encodeParams(this.deploy.inputs, values || [ ]); } - decodeFunctionData(functionFragment: FunctionFragment | string, data: BytesLike): Array { + // Decode the data for a function call (e.g. tx.data) + decodeFunctionData(functionFragment: FunctionFragment | string, data: BytesLike): Result { if (typeof(functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } @@ -255,6 +260,7 @@ export class Interface { return this._decodeParams(functionFragment.inputs, bytes.slice(4)); } + // Encode the data for a function call (e.g. tx.data) encodeFunctionData(functionFragment: FunctionFragment | string, values?: Array): string { if (typeof(functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); @@ -266,7 +272,8 @@ export class Interface { ])); } - decodeFunctionResult(functionFragment: FunctionFragment | string, data: BytesLike): Array { + // Decode the result from a function call (e.g. from eth_call) + decodeFunctionResult(functionFragment: FunctionFragment | string, data: BytesLike): Result { if (typeof(functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } @@ -285,7 +292,7 @@ export class Interface { case 4: if (hexlify(bytes.slice(0, 4)) === "0x08c379a0") { errorSignature = "Error(string)"; - reason = this._abiCoder.decode([ "string" ], bytes.slice(4)); + reason = this._abiCoder.decode([ "string" ], bytes.slice(4))[0]; } break; } @@ -298,6 +305,7 @@ export class Interface { }); } + // Encode the result for a function call (e.g. for eth_call) encodeFunctionResult(functionFragment: FunctionFragment | string, values?: Array): string { if (typeof(functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); @@ -306,6 +314,7 @@ export class Interface { return hexlify(this._abiCoder.encode(functionFragment.outputs, values || [ ])); } + // Create the filter for the event with search criteria (e.g. for eth_filterLog) encodeFilterTopics(eventFragment: EventFragment, values: Array): Array> { if (typeof(eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); @@ -355,7 +364,8 @@ export class Interface { return topics; } - decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array): Array { + // Decode a filter for the event and the search criteria + decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array): Result { if (typeof(eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); } @@ -390,7 +400,7 @@ export class Interface { let resultIndexed = (topics != null) ? this._abiCoder.decode(indexed, concat(topics)): null; let resultNonIndexed = this._abiCoder.decode(nonIndexed, data); - let result: Array = [ ]; + let result: Result = [ ]; let nonIndexedIndex = 0, indexedIndex = 0; eventFragment.inputs.forEach((param, index) => { if (param.indexed) { @@ -406,13 +416,14 @@ export class Interface { } else { result[index] = resultNonIndexed[nonIndexedIndex++]; } - //if (param.name && result[param.name] == null) { result[param.name] = result[index]; } + if (param.name && result[param.name] == null) { result[param.name] = result[index]; } }); return result; } - + // Given a transaction, find the matching function fragment (if any) and + // determine all its properties and call parameters parseTransaction(tx: { data: string, value?: BigNumberish }): TransactionDescription { let fragment = this.getFunction(tx.data.substring(0, 10).toLowerCase()) @@ -428,12 +439,16 @@ export class Interface { }); } + // Given an event log, find the matching event fragment (if any) and + // determine all its properties and values parseLog(log: { topics: Array, data: string}): LogDescription { let fragment = this.getEvent(log.topics[0]); if (!fragment || fragment.anonymous) { return null; } // @TODO: If anonymous, and the only method, and the input count matches, should we parse? + // Probably not, because just because it is the only event in the ABI does + // not mean we have the full ABI; maybe jsut a fragment? return new LogDescription({ @@ -441,7 +456,7 @@ export class Interface { name: fragment.name, signature: fragment.format(), topic: this.getEventTopic(fragment), - values: this.decodeEventLog(fragment, log.data, log.topics) + args: this.decodeEventLog(fragment, log.data, log.topics) }); } @@ -462,27 +477,4 @@ export class Interface { return !!(value && value._isInterface); } } -/* -function getFragment(hash: string, calcFunc: (f: Fragment) => string, items: { [ sig: string ]: Fragment } ) { - for (let signature in items) { - if (signature.indexOf("(") === -1) { continue; } - let fragment = items[signature]; - if (calcFunc(fragment) === hash) { return fragment; } - } - return null; -} -*/ -/* -function getNameCount(fragments: { [ signature: string ]: Fragment }): { [ name: string ]: number } { - let unique: { [ name: string ]: number } = { }; - // Count each name - for (let signature in fragments) { - let name = fragments[signature].name; - if (!unique[name]) { unique[name] = 0; } - unique[name]++; - } - - return unique; -} -*/ diff --git a/packages/contracts/src.ts/index.ts b/packages/contracts/src.ts/index.ts index 5103b7fda..b365ab9d0 100644 --- a/packages/contracts/src.ts/index.ts +++ b/packages/contracts/src.ts/index.ts @@ -1,6 +1,6 @@ "use strict"; -import { EventFragment, Fragment, Indexed, Interface, JsonFragment, ParamType } from "@ethersproject/abi"; +import { EventFragment, Fragment, Indexed, Interface, JsonFragment, ParamType, Result } from "@ethersproject/abi"; import { Block, BlockTag, Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider"; import { Signer, VoidSigner } from "@ethersproject/abstract-signer"; import { getContractAddress } from "@ethersproject/address"; @@ -48,7 +48,7 @@ export interface Event extends Log { eventSignature?: string; // The parsed arguments to the event - values?: Array; + args?: Result; // A function that can be used to decode event data and topics decode?: (data: string, topics?: Array) => any; @@ -124,7 +124,7 @@ export function _sendTransaction(func: FunctionFragment, args: Array, overr */ function runMethod(contract: Contract, functionName: string, options: RunOptions): RunFunction { - let method = contract.interface.functions[functionName]; + const method = contract.interface.functions[functionName]; return function(...params): Promise { let tx: any = {} @@ -233,7 +233,7 @@ function runMethod(contract: Contract, functionName: string, options: RunOptions } return contract.signer.sendTransaction(tx).then((tx) => { - let wait = tx.wait.bind(tx); + const wait = tx.wait.bind(tx); tx.wait = (confirmations?: number) => { return wait(confirmations).then((receipt: ContractReceipt) => { @@ -242,7 +242,7 @@ function runMethod(contract: Contract, functionName: string, options: RunOptions let parsed = contract.interface.parseLog(log); if (parsed) { - event.values = parsed.values; + event.args = parsed.args; event.decode = (data: BytesLike, topics?: Array) => { return this.interface.decodeEventLog(parsed.eventFragment, data, topics); }; @@ -322,10 +322,10 @@ class RunningEvent { } run(args: Array): number { - let listenerCount = this.listenerCount(); + const listenerCount = this.listenerCount(); this._listeners = this._listeners.filter((item) => { - let argsCopy = args.slice(); + const argsCopy = args.slice(); // Call the callback in the next event loop setTimeout(() => { @@ -355,7 +355,7 @@ class FragmentRunningEvent extends RunningEvent { readonly fragment: EventFragment; constructor(address: string, contractInterface: Interface, fragment: EventFragment, topics?: Array) { - let filter: EventFilter = { + const filter: EventFilter = { address: address } @@ -384,7 +384,7 @@ class FragmentRunningEvent extends RunningEvent { return this.interface.decodeEventLog(this.fragment, data, topics); }; - event.values = this.interface.decodeEventLog(this.fragment, event.data, event.topics); + event.args = this.interface.decodeEventLog(this.fragment, event.data, event.topics); } } @@ -401,7 +401,7 @@ class WildcardRunningEvent extends RunningEvent { prepareEvent(event: Event): void { super.prepareEvent(event); - let parsed = this.interface.parseLog(event); + const parsed = this.interface.parseLog(event); if (parsed) { event.event = parsed.name; event.eventSignature = parsed.signature; @@ -410,7 +410,7 @@ class WildcardRunningEvent extends RunningEvent { return this.interface.decodeEventLog(parsed.eventFragment, data, topics); }; - event.values = parsed.values; + event.args = parsed.args; } } } @@ -476,7 +476,7 @@ export class Contract { { const uniqueFilters: { [ name: string ]: Array } = { }; Object.keys(this.interface.events).forEach((eventSignature) => { - let event = this.interface.events[eventSignature]; + const event = this.interface.events[eventSignature]; defineReadOnly(this.filters, eventSignature, (...args: Array) => { return { address: this.address, @@ -523,7 +523,7 @@ export class Contract { const fragment = this.interface.functions[name]; // @TODO: This should take in fragment - let run = runMethod(this, name, { }); + const run = runMethod(this, name, { }); if (this[name] == null) { defineReadOnly(this, name, run); @@ -623,7 +623,7 @@ export class Contract { logger.throwError("sending a transactions require a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction(fallback)" }) } - let tx: TransactionRequest = shallowCopy(overrides || {}); + const tx: TransactionRequest = shallowCopy(overrides || {}); ["from", "to"].forEach(function(key) { if ((tx)[key] == null) { return; } @@ -642,7 +642,7 @@ export class Contract { signerOrProvider = new VoidSigner(signerOrProvider, this.provider); } - let contract = new (<{ new(...args: any[]): Contract }>(this.constructor))(this.address, this.interface, signerOrProvider); + const contract = new (<{ new(...args: any[]): Contract }>(this.constructor))(this.address, this.interface, signerOrProvider); if (this.deployTransaction) { defineReadOnly(contract, "deployTransaction", this.deployTransaction); } @@ -681,7 +681,7 @@ export class Contract { return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface)); } - let fragment = this.interface.getEvent(eventName) + const fragment = this.interface.getEvent(eventName) if (!fragment) { logger.throwArgumentError("unknown event - " + eventName, "eventName", eventName); } @@ -689,7 +689,7 @@ export class Contract { return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment)); } - let filter: EventFilter = { + const filter: EventFilter = { address: this.address } @@ -697,7 +697,7 @@ export class Contract { // since it may be a filter for an otherwise unknown event if (eventName.topics) { if (eventName.topics[0]) { - let fragment = this.interface.getEvent(eventName.topics[0]); + const fragment = this.interface.getEvent(eventName.topics[0]); if (fragment) { return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment, eventName.topics)); } @@ -715,7 +715,7 @@ export class Contract { } // If we have a poller for this, remove it - let emit = this._wrappedEmits[runningEvent.tag]; + const emit = this._wrappedEmits[runningEvent.tag]; if (emit) { this.provider.off(runningEvent.filter, emit); delete this._wrappedEmits[runningEvent.tag]; @@ -723,7 +723,7 @@ export class Contract { } private _wrapEvent(runningEvent: RunningEvent, log: Log, listener: Listener): Event { - let event = deepCopy(log); + const event = deepCopy(log); try { runningEvent.prepareEvent(event); @@ -757,11 +757,11 @@ export class Contract { // If we are not polling the provider, start if (!this._wrappedEmits[runningEvent.tag]) { - let wrappedEmit = (log: Log) => { - let event = this._wrapEvent(runningEvent, log, listener); - let values = (event.values || []); - values.push(event); - this.emit(runningEvent.filter, ...values); + const wrappedEmit = (log: Log) => { + const event = this._wrapEvent(runningEvent, log, listener); + const args = (event.args || []); + args.push(event); + this.emit(runningEvent.filter, ...args); }; this._wrappedEmits[runningEvent.tag] = wrappedEmit; @@ -773,8 +773,8 @@ export class Contract { } queryFilter(event: EventFilter, fromBlockOrBlockhash?: BlockTag | string, toBlock?: BlockTag): Promise> { - let runningEvent = this._getRunningEvent(event); - let filter = shallowCopy(runningEvent.filter); + const runningEvent = this._getRunningEvent(event); + const filter = shallowCopy(runningEvent.filter); if (typeof(fromBlockOrBlockhash) === "string" && isHexString(fromBlockOrBlockhash, 32)) { if (toBlock != null) { @@ -804,8 +804,8 @@ export class Contract { emit(eventName: EventFilter | string, ...args: Array): boolean { if (!this.provider) { return false; } - let runningEvent = this._getRunningEvent(eventName); - let result = (runningEvent.run(args) > 0); + const runningEvent = this._getRunningEvent(eventName); + const result = (runningEvent.run(args) > 0); // May have drained all the "once" events; check for living events this._checkRunningEvents(runningEvent); @@ -822,7 +822,7 @@ export class Contract { if (!this.provider) { return []; } if (eventName == null) { - let result: Array = [ ]; + const result: Array = [ ]; for (let tag in this._runningEvents) { this._runningEvents[tag].listeners().forEach((listener) => { result.push(listener) @@ -838,8 +838,8 @@ export class Contract { if (!this.provider) { return this; } if (eventName == null) { - for (let tag in this._runningEvents) { - let runningEvent = this._runningEvents[tag]; + for (const tag in this._runningEvents) { + const runningEvent = this._runningEvents[tag]; runningEvent.removeAllListeners(); this._checkRunningEvents(runningEvent); } @@ -847,7 +847,7 @@ export class Contract { } // Delete any listeners - let runningEvent = this._getRunningEvent(eventName); + const runningEvent = this._getRunningEvent(eventName); runningEvent.removeAllListeners(); this._checkRunningEvents(runningEvent); @@ -856,7 +856,7 @@ export class Contract { off(eventName: EventFilter | string, listener: Listener): this { if (!this.provider) { return this; } - let runningEvent = this._getRunningEvent(eventName); + const runningEvent = this._getRunningEvent(eventName); runningEvent.removeListener(listener); this._checkRunningEvents(runningEvent); return this; @@ -915,7 +915,7 @@ export class ContractFactory { // If we have 1 additional argument, we allow transaction overrides if (args.length === this.interface.deploy.inputs.length + 1) { tx = shallowCopy(args.pop()); - for (let key in tx) { + for (const key in tx) { if (!allowedTransactionKeys[key]) { throw new Error("unknown transaction override " + key); } @@ -944,12 +944,12 @@ export class ContractFactory { return resolveAddresses(this.signer, args, this.interface.deploy.inputs).then((args) => { // Get the deployment transaction (with optional overrides) - let tx = this.getDeployTransaction(...args); + const tx = this.getDeployTransaction(...args); // Send the deployment transaction return this.signer.sendTransaction(tx).then((tx) => { - let address = ((this.constructor)).getContractAddress(tx); - let contract = ((this.constructor)).getContract(address, this.interface, this.signer); + const address = ((this.constructor)).getContractAddress(tx); + const contract = ((this.constructor)).getContract(address, this.interface, this.signer); defineReadOnly(contract, "deployTransaction", tx); return contract; }); @@ -973,7 +973,7 @@ export class ContractFactory { compilerOutput = JSON.parse(compilerOutput); } - let abi = compilerOutput.abi; + const abi = compilerOutput.abi; let bytecode: any = null; if (compilerOutput.bytecode) {