Added initial support for recoverable coding erros (#800).

This commit is contained in:
Richard Moore 2020-04-25 03:25:42 -04:00
parent 14e6811bf7
commit bda6623091
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
8 changed files with 204 additions and 28 deletions

View File

@ -12,6 +12,29 @@ export interface Result extends ReadonlyArray<any> {
readonly [key: string]: any; readonly [key: string]: any;
} }
export function checkResultErrors(result: Result): Array<{ path: Array<string | number>, error: Error }> {
// Find the first error (if any)
const errors: Array<{ path: Array<string | number>, error: Error }> = [ ];
const checkErrors = function(path: Array<string | number>, 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 type CoerceFunc = (type: string, value: any) => any;
export abstract class Coder { export abstract class Coder {
@ -34,6 +57,7 @@ export abstract class Coder {
readonly dynamic: boolean; readonly dynamic: boolean;
constructor(name: string, type: string, localName: string, dynamic: boolean) { constructor(name: string, type: string, localName: string, dynamic: boolean) {
// @TODO: defineReadOnly these
this.name = name; this.name = name;
this.type = type; this.type = type;
this.localName = localName; this.localName = localName;

View File

@ -75,10 +75,29 @@ export function unpack(reader: Reader, coders: Array<Coder>): Array<any> {
if (coder.dynamic) { if (coder.dynamic) {
let offset = reader.readValue(); let offset = reader.readValue();
let offsetReader = baseReader.subReader(offset.toNumber()); 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; dynamicLength += offsetReader.consumed;
} else { } 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) { if (value != undefined) {
@ -99,9 +118,26 @@ export function unpack(reader: Reader, coders: Array<Coder>): Array<any> {
if (values[name] != null) { return; } 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); return Object.freeze(values);
} }
@ -126,7 +162,6 @@ export class ArrayCoder extends Coder {
let count = this.length; let count = this.length;
//let result = new Uint8Array(0);
if (count === -1) { if (count === -1) {
count = value.length; count = value.length;
writer.writeValue(value.length); writer.writeValue(value.length);

View File

@ -8,12 +8,12 @@ export class TupleCoder extends Coder {
constructor(coders: Array<Coder>, localName: string) { constructor(coders: Array<Coder>, localName: string) {
let dynamic = false; let dynamic = false;
let types: Array<string> = []; const types: Array<string> = [];
coders.forEach((coder) => { coders.forEach((coder) => {
if (coder.dynamic) { dynamic = true; } if (coder.dynamic) { dynamic = true; }
types.push(coder.type); types.push(coder.type);
}); });
let type = ("tuple(" + types.join(",") + ")"); const type = ("tuple(" + types.join(",") + ")");
super("tuple", type, localName, dynamic); super("tuple", type, localName, dynamic);
this.coders = coders; this.coders = coders;

View File

@ -2,7 +2,7 @@
import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments"; import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments";
import { AbiCoder, CoerceFunc, defaultAbiCoder } from "./abi-coder"; 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 { export {
ConstructorFragment, ConstructorFragment,
@ -26,6 +26,7 @@ export {
JsonFragmentType, JsonFragmentType,
Result, Result,
checkResultErrors,
LogDescription, LogDescription,
TransactionDescription TransactionDescription

View File

@ -8,14 +8,14 @@ import { keccak256 } from "@ethersproject/keccak256"
import { defineReadOnly, Description, getStatic } from "@ethersproject/properties"; import { defineReadOnly, Description, getStatic } from "@ethersproject/properties";
import { AbiCoder, defaultAbiCoder } from "./abi-coder"; 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 { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, ParamType } from "./fragments";
import { Logger } from "@ethersproject/logger"; import { Logger } from "@ethersproject/logger";
import { version } from "./_version"; import { version } from "./_version";
const logger = new Logger(version); const logger = new Logger(version);
export { Result }; export { checkResultErrors, Result };
export class LogDescription extends Description<LogDescription> { export class LogDescription extends Description<LogDescription> {
readonly eventFragment: EventFragment; readonly eventFragment: EventFragment;
@ -43,6 +43,24 @@ export class Indexed extends Description<Indexed> {
} }
} }
function wrapAccessError(property: string, error: Error): Error {
const wrap = new Error(`deferred error during ABI decoding triggered accessing ${ property }`);
(<any>wrap).error = error;
return wrap;
}
function checkNames(fragment: Fragment, type: "input" | "output", params: Array<ParamType>): 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 { export class Interface {
readonly fragments: Array<Fragment>; readonly fragments: Array<Fragment>;
@ -87,12 +105,16 @@ export class Interface {
logger.warn("duplicate definition - constructor"); logger.warn("duplicate definition - constructor");
return; return;
} }
checkNames(fragment, "input", fragment.inputs);
defineReadOnly(this, "deploy", <ConstructorFragment>fragment); defineReadOnly(this, "deploy", <ConstructorFragment>fragment);
return; return;
case "function": case "function":
checkNames(fragment, "input", fragment.inputs);
checkNames(fragment, "output", (<FunctionFragment>fragment).outputs);
bucket = this.functions; bucket = this.functions;
break; break;
case "event": case "event":
checkNames(fragment, "input", fragment.inputs);
bucket = this.events; bucket = this.events;
break; break;
default: default:
@ -367,6 +389,49 @@ export class Interface {
return topics; return topics;
} }
encodeEventLog(eventFragment: EventFragment, values: Array<any>): { data: string, topics: Array<string> } {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
}
const topics: Array<string> = [ ];
const dataTypes: Array<ParamType> = [ ];
const dataValues: Array<string> = [ ];
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 // Decode a filter for the event and the search criteria
decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array<string>): Result { decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array<string>): Result {
if (typeof(eventFragment) === "string") { if (typeof(eventFragment) === "string") {
@ -414,15 +479,45 @@ export class Interface {
result[index] = new Indexed({ _isIndexed: true, hash: resultIndexed[indexedIndex++] }); result[index] = new Indexed({ _isIndexed: true, hash: resultIndexed[indexedIndex++] });
} else { } else {
result[index] = resultIndexed[indexedIndex++]; try {
result[index] = resultIndexed[indexedIndex++];
} catch (error) {
result[index] = error;
}
} }
} else { } 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); return Object.freeze(result);
} }

View File

@ -1,6 +1,6 @@
"use strict"; "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 { Block, BlockTag, Filter, FilterByBlockHash, Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse } from "@ethersproject/abstract-provider";
import { Signer, VoidSigner } from "@ethersproject/abstract-signer"; import { Signer, VoidSigner } from "@ethersproject/abstract-signer";
import { getContractAddress } from "@ethersproject/address"; 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 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 // Recursively replaces ENS names with promises to resolve the name and resolves all properties
function resolveAddresses(signerOrProvider: Signer | Provider, value: any, paramType: ParamType | Array<ParamType>): Promise<any> { function resolveAddresses(signerOrProvider: Signer | Provider, value: any, paramType: ParamType | Array<ParamType>): Promise<any> {
if (Array.isArray(paramType)) { 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") // Check for unexpected keys (e.g. using "gas" instead of "gasLimit")
for (let key in tx) { for (let key in tx) {
if (!allowedTransactionKeys[key]) { 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? // @TODO Fragment should inherit Wildcard? and just override getEmit?
// or have a common abstract super class, with enough constructor // or have a common abstract super class, with enough constructor
// options to configure both. // options to configure both.
@ -408,11 +410,13 @@ class FragmentRunningEvent extends RunningEvent {
} catch (error) { } catch (error) {
event.args = null; event.args = null;
event.decodeError = error; event.decodeError = error;
throw error;
} }
} }
getEmit(event: Event): Array<any> { getEmit(event: Event): Array<any> {
const errors = checkResultErrors(event.args);
if (errors.length) { throw errors[0].error; }
const args = (event.args || []).slice(); const args = (event.args || []).slice();
args.push(event); args.push(event);
return args; return args;
@ -713,6 +717,11 @@ export class Contract {
return this._normalizeRunningEvent(new ErrorRunningEvent()); 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 // Listen for any event
if (eventName === "*") { if (eventName === "*") {
return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface)); 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 we are not polling the provider, start polling
if (!this._wrappedEmits[runningEvent.tag]) { if (!this._wrappedEmits[runningEvent.tag]) {
const wrappedEmit = (log: Log) => { const wrappedEmit = (log: Log) => {
let event = null; let event = this._wrapEvent(runningEvent, log, listener);
try {
event = this._wrapEvent(runningEvent, log, listener); // Try to emit the result for the parameterized event...
} catch (error) { if (event.decodeError == null) {
// There was an error decoding the data and topics try {
this.emit("error", error, event); const args = runningEvent.getEmit(event);
return; 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; this._wrappedEmits[runningEvent.tag] = wrappedEmit;

View File

@ -16,9 +16,7 @@ import { Wordlist, wordlists} from "@ethersproject/wordlists";
import * as utils from "./utils"; import * as utils from "./utils";
import { Logger } from "@ethersproject/logger"; import { ErrorCode as errors, Logger } from "@ethersproject/logger";
const errors: { [ name: string ]: string } = Logger.errors;
//////////////////////// ////////////////////////
// Types // Types

View File

@ -1,6 +1,6 @@
"use strict"; "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 { getAddress, getCreate2Address, getContractAddress, getIcapAddress, isAddress } from "@ethersproject/address";
import * as base64 from "@ethersproject/base64"; 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"; 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, ParamType,
FormatTypes, FormatTypes,
checkResultErrors,
Result,
Logger, Logger,
RLP, RLP,