diff --git a/packages/abi/src.ts/coders/abstract-coder.ts b/packages/abi/src.ts/coders/abstract-coder.ts index a8468e58a..e6df42d08 100644 --- a/packages/abi/src.ts/coders/abstract-coder.ts +++ b/packages/abi/src.ts/coders/abstract-coder.ts @@ -12,6 +12,29 @@ export interface Result extends ReadonlyArray { readonly [key: string]: any; } +export function checkResultErrors(result: Result): Array<{ path: Array, error: Error }> { + // Find the first error (if any) + const errors: Array<{ path: Array, error: Error }> = [ ]; + + const checkErrors = function(path: Array, object: any): void { + if (!Array.isArray(object)) { return; } + for (let key in object) { + const childPath = path.slice(); + childPath.push(key); + + try { + checkErrors(childPath, object[key]); + } catch (error) { + errors.push({ path: childPath, error: error }); + } + } + } + checkErrors([ ], result); + + return errors; + +} + export type CoerceFunc = (type: string, value: any) => any; export abstract class Coder { @@ -34,6 +57,7 @@ export abstract class Coder { readonly dynamic: boolean; constructor(name: string, type: string, localName: string, dynamic: boolean) { + // @TODO: defineReadOnly these this.name = name; this.type = type; this.localName = localName; diff --git a/packages/abi/src.ts/coders/array.ts b/packages/abi/src.ts/coders/array.ts index 12f7d9f43..eaf19735d 100644 --- a/packages/abi/src.ts/coders/array.ts +++ b/packages/abi/src.ts/coders/array.ts @@ -75,10 +75,29 @@ export function unpack(reader: Reader, coders: Array): Array { if (coder.dynamic) { let offset = reader.readValue(); let offsetReader = baseReader.subReader(offset.toNumber()); - value = coder.decode(offsetReader); + try { + value = coder.decode(offsetReader); + } catch (error) { + // Cannot recover from this + if (error.code === Logger.errors.BUFFER_OVERRUN) { throw error; } + value = error; + value.baseType = coder.name; + value.name = coder.localName; + value.type = coder.type; + } dynamicLength += offsetReader.consumed; + } else { - value = coder.decode(reader); + try { + value = coder.decode(reader); + } catch (error) { + // Cannot recover from this + if (error.code === Logger.errors.BUFFER_OVERRUN) { throw error; } + value = error; + value.baseType = coder.name; + value.name = coder.localName; + value.type = coder.type; + } } if (value != undefined) { @@ -99,9 +118,26 @@ export function unpack(reader: Reader, coders: Array): Array { if (values[name] != null) { return; } - values[name] = values[index]; + const value = values[index]; + + if (value instanceof Error) { + Object.defineProperty(values, name, { + get: () => { throw value; } + }); + } else { + values[name] = value; + } }); + for (let i = 0; i < values.length; i++) { + const value = values[i]; + if (value instanceof Error) { + Object.defineProperty(values, i, { + get: () => { throw value; } + }); + } + } + return Object.freeze(values); } @@ -126,7 +162,6 @@ export class ArrayCoder extends Coder { let count = this.length; - //let result = new Uint8Array(0); if (count === -1) { count = value.length; writer.writeValue(value.length); diff --git a/packages/abi/src.ts/coders/tuple.ts b/packages/abi/src.ts/coders/tuple.ts index 257fec7f7..dea643ae4 100644 --- a/packages/abi/src.ts/coders/tuple.ts +++ b/packages/abi/src.ts/coders/tuple.ts @@ -8,12 +8,12 @@ export class TupleCoder extends Coder { constructor(coders: Array, localName: string) { let dynamic = false; - let types: Array = []; + const types: Array = []; coders.forEach((coder) => { if (coder.dynamic) { dynamic = true; } types.push(coder.type); }); - let type = ("tuple(" + types.join(",") + ")"); + const type = ("tuple(" + types.join(",") + ")"); super("tuple", type, localName, dynamic); this.coders = coders; diff --git a/packages/abi/src.ts/index.ts b/packages/abi/src.ts/index.ts index f0f4cfc0a..7c8845f2d 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, LogDescription, Result, TransactionDescription } from "./interface"; +import { checkResultErrors, Indexed, Interface, LogDescription, Result, TransactionDescription } from "./interface"; export { ConstructorFragment, @@ -26,6 +26,7 @@ export { JsonFragmentType, Result, + checkResultErrors, LogDescription, TransactionDescription diff --git a/packages/abi/src.ts/interface.ts b/packages/abi/src.ts/interface.ts index 0e3293c54..7d3efa8f6 100644 --- a/packages/abi/src.ts/interface.ts +++ b/packages/abi/src.ts/interface.ts @@ -8,14 +8,14 @@ import { keccak256 } from "@ethersproject/keccak256" import { defineReadOnly, Description, getStatic } from "@ethersproject/properties"; import { AbiCoder, defaultAbiCoder } from "./abi-coder"; -import { Result } from "./coders/abstract-coder"; +import { checkResultErrors, 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 { Result }; +export { checkResultErrors, Result }; export class LogDescription extends Description { readonly eventFragment: EventFragment; @@ -43,6 +43,24 @@ export class Indexed extends Description { } } +function wrapAccessError(property: string, error: Error): Error { + const wrap = new Error(`deferred error during ABI decoding triggered accessing ${ property }`); + (wrap).error = error; + return wrap; +} + +function checkNames(fragment: Fragment, type: "input" | "output", params: Array): void { + params.reduce((accum, param) => { + if (param.name) { + if (accum[param.name]) { + logger.throwArgumentError(`duplicate ${ type } parameter ${ JSON.stringify(param.name) } in ${ fragment.format("full") }`, "fragment", fragment); + } + accum[param.name] = true; + } + return accum; + }, <{ [ name: string ]: boolean }>{ }); +} + export class Interface { readonly fragments: Array; @@ -87,12 +105,16 @@ export class Interface { logger.warn("duplicate definition - constructor"); return; } + checkNames(fragment, "input", fragment.inputs); defineReadOnly(this, "deploy", fragment); return; case "function": + checkNames(fragment, "input", fragment.inputs); + checkNames(fragment, "output", (fragment).outputs); bucket = this.functions; break; case "event": + checkNames(fragment, "input", fragment.inputs); bucket = this.events; break; default: @@ -367,6 +389,49 @@ export class Interface { return topics; } + encodeEventLog(eventFragment: EventFragment, values: Array): { data: string, topics: Array } { + if (typeof(eventFragment) === "string") { + eventFragment = this.getEvent(eventFragment); + } + + const topics: Array = [ ]; + + const dataTypes: Array = [ ]; + const dataValues: Array = [ ]; + + if (!eventFragment.anonymous) { + topics.push(this.getEventTopic(eventFragment)); + } + + if (values.length !== eventFragment.inputs.length) { + logger.throwArgumentError("event arguments/values mismatch", "values", values); + } + + eventFragment.inputs.forEach((param, index) => { + const value = values[index]; + if (param.indexed) { + if (param.type === "string") { + topics.push(id(value)) + } else if (param.type === "bytes") { + topics.push(keccak256(value)) + } else if (param.baseType === "tuple" || param.baseType === "array") { + // @TOOD + throw new Error("not implemented"); + } else { + topics.push(this._abiCoder.encode([ param.type] , [ value ])); + } + } else { + dataTypes.push(param); + dataValues.push(value); + } + }); + + return { + data: this._abiCoder.encode(dataTypes , dataValues), + topics: topics + }; + } + // Decode a filter for the event and the search criteria decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array): Result { if (typeof(eventFragment) === "string") { @@ -414,15 +479,45 @@ export class Interface { result[index] = new Indexed({ _isIndexed: true, hash: resultIndexed[indexedIndex++] }); } else { - result[index] = resultIndexed[indexedIndex++]; + try { + result[index] = resultIndexed[indexedIndex++]; + } catch (error) { + result[index] = error; + } } } else { - result[index] = resultNonIndexed[nonIndexedIndex++]; + try { + result[index] = resultNonIndexed[nonIndexedIndex++]; + } catch (error) { + result[index] = error; + } } - if (param.name && result[param.name] == null) { result[param.name] = result[index]; } + // Add the keyword argument if named and safe + if (param.name && result[param.name] == null) { + const value = result[index]; + + // Make error named values throw on access + if (value instanceof Error) { + Object.defineProperty(result, param.name, { + get: () => { throw wrapAccessError(`property ${ JSON.stringify(param.name) }`, value); } + }); + } else { + result[param.name] = value; + } + } }); + // Make all error indexed values throw on access + for (let i = 0; i < result.length; i++) { + const value = result[i]; + if (value instanceof Error) { + Object.defineProperty(result, i, { + get: () => { throw wrapAccessError(`index ${ i }`, value); } + }); + } + } + return Object.freeze(result); } diff --git a/packages/contracts/src.ts/index.ts b/packages/contracts/src.ts/index.ts index 212d452f2..9357cb60c 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, LogDescription, ParamType, Result } from "@ethersproject/abi"; +import { checkResultErrors, EventFragment, Fragment, Indexed, Interface, JsonFragment, LogDescription, ParamType, Result } from "@ethersproject/abi"; import { Block, BlockTag, Filter, FilterByBlockHash, Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider"; import { Signer, VoidSigner } from "@ethersproject/abstract-signer"; import { getContractAddress } from "@ethersproject/address"; @@ -80,6 +80,7 @@ const allowedTransactionKeys: { [ key: string ]: boolean } = { chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true } + // Recursively replaces ENS names with promises to resolve the name and resolves all properties function resolveAddresses(signerOrProvider: Signer | Provider, value: any, paramType: ParamType | Array): Promise { if (Array.isArray(paramType)) { @@ -147,7 +148,7 @@ function runMethod(contract: Contract, functionName: string, options: RunOptions // Check for unexpected keys (e.g. using "gas" instead of "gasLimit") for (let key in tx) { if (!allowedTransactionKeys[key]) { - logger.throwError(("unknown transaction override - " + key), "overrides", tx); + logger.throwArgumentError(("unknown transaction override - " + key), "overrides", tx); } } } @@ -362,6 +363,7 @@ class ErrorRunningEvent extends RunningEvent { } } + // @TODO Fragment should inherit Wildcard? and just override getEmit? // or have a common abstract super class, with enough constructor // options to configure both. @@ -408,11 +410,13 @@ class FragmentRunningEvent extends RunningEvent { } catch (error) { event.args = null; event.decodeError = error; - throw error; } } getEmit(event: Event): Array { + const errors = checkResultErrors(event.args); + if (errors.length) { throw errors[0].error; } + const args = (event.args || []).slice(); args.push(event); return args; @@ -713,6 +717,11 @@ export class Contract { return this._normalizeRunningEvent(new ErrorRunningEvent()); } + // Listen for any event that is registered + if (eventName === "event") { + return this._normalizeRunningEvent(new RunningEvent("event", null)); + } + // Listen for any event if (eventName === "*") { return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface)); @@ -791,16 +800,27 @@ export class Contract { // If we are not polling the provider, start polling if (!this._wrappedEmits[runningEvent.tag]) { const wrappedEmit = (log: Log) => { - let event = null; - try { - event = this._wrapEvent(runningEvent, log, listener); - } catch (error) { - // There was an error decoding the data and topics - this.emit("error", error, event); - return; + let event = this._wrapEvent(runningEvent, log, listener); + + // Try to emit the result for the parameterized event... + if (event.decodeError == null) { + try { + const args = runningEvent.getEmit(event); + this.emit(runningEvent.filter, ...args); + } catch (error) { + event.decodeError = error.error; + } + } + + // Always emit "event" for fragment-base events + if (runningEvent.filter != null) { + this.emit("event", event); + } + + // Emit "error" if there was an error + if (event.decodeError != null) { + this.emit("error", event.decodeError, event); } - const args = runningEvent.getEmit(event); - this.emit(runningEvent.filter, ...args); }; this._wrappedEmits[runningEvent.tag] = wrappedEmit; diff --git a/packages/ethers/src.ts/ethers.ts b/packages/ethers/src.ts/ethers.ts index 5ef82636f..d455beabf 100644 --- a/packages/ethers/src.ts/ethers.ts +++ b/packages/ethers/src.ts/ethers.ts @@ -16,9 +16,7 @@ import { Wordlist, wordlists} from "@ethersproject/wordlists"; import * as utils from "./utils"; -import { Logger } from "@ethersproject/logger"; - -const errors: { [ name: string ]: string } = Logger.errors; +import { ErrorCode as errors, Logger } from "@ethersproject/logger"; //////////////////////// // Types diff --git a/packages/ethers/src.ts/utils.ts b/packages/ethers/src.ts/utils.ts index a21441d5a..d012b9587 100644 --- a/packages/ethers/src.ts/utils.ts +++ b/packages/ethers/src.ts/utils.ts @@ -1,6 +1,6 @@ "use strict"; -import { AbiCoder, defaultAbiCoder, EventFragment, FormatTypes, Fragment, FunctionFragment, Indexed, Interface, ParamType } from "@ethersproject/abi"; +import { AbiCoder, checkResultErrors, defaultAbiCoder, EventFragment, FormatTypes, Fragment, FunctionFragment, Indexed, Interface, ParamType, Result } from "@ethersproject/abi"; import { getAddress, getCreate2Address, getContractAddress, getIcapAddress, isAddress } from "@ethersproject/address"; import * as base64 from "@ethersproject/base64"; import { arrayify, concat, hexDataSlice, hexDataLength, hexlify, hexStripZeros, hexValue, hexZeroPad, isBytes, isBytesLike, isHexString, joinSignature, zeroPad, splitSignature, stripZeros } from "@ethersproject/bytes"; @@ -51,6 +51,9 @@ export { ParamType, FormatTypes, + checkResultErrors, + Result, + Logger, RLP,