Skip to content

Commit

Permalink
Added initial support for recoverable coding erros (#800).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Apr 25, 2020
1 parent 14e6811 commit bda6623
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 28 deletions.
24 changes: 24 additions & 0 deletions packages/abi/src.ts/coders/abstract-coder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ export interface Result extends ReadonlyArray<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 abstract class Coder {
Expand All @@ -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;
Expand Down
43 changes: 39 additions & 4 deletions packages/abi/src.ts/coders/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,29 @@ export function unpack(reader: Reader, coders: Array<Coder>): Array<any> {
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) {
Expand All @@ -99,9 +118,26 @@ export function unpack(reader: Reader, coders: Array<Coder>): Array<any> {

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);
}

Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/abi/src.ts/coders/tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export class TupleCoder extends Coder {

constructor(coders: Array<Coder>, localName: string) {
let dynamic = false;
let types: Array<string> = [];
const types: Array<string> = [];
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;
Expand Down
3 changes: 2 additions & 1 deletion packages/abi/src.ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +26,7 @@ export {
JsonFragmentType,

Result,
checkResultErrors,

LogDescription,
TransactionDescription
Expand Down
105 changes: 100 additions & 5 deletions packages/abi/src.ts/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogDescription> {
readonly eventFragment: EventFragment;
Expand Down Expand Up @@ -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 {
readonly fragments: Array<Fragment>;

Expand Down Expand Up @@ -87,12 +105,16 @@ export class Interface {
logger.warn("duplicate definition - constructor");
return;
}
checkNames(fragment, "input", fragment.inputs);
defineReadOnly(this, "deploy", <ConstructorFragment>fragment);
return;
case "function":
checkNames(fragment, "input", fragment.inputs);
checkNames(fragment, "output", (<FunctionFragment>fragment).outputs);
bucket = this.functions;
break;
case "event":
checkNames(fragment, "input", fragment.inputs);
bucket = this.events;
break;
default:
Expand Down Expand Up @@ -367,6 +389,49 @@ export class Interface {
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
decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: Array<string>): Result {
if (typeof(eventFragment) === "string") {
Expand Down Expand Up @@ -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);
}

Expand Down
44 changes: 32 additions & 12 deletions packages/contracts/src.ts/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<ParamType>): Promise<any> {
if (Array.isArray(paramType)) {
Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -408,11 +410,13 @@ class FragmentRunningEvent extends RunningEvent {
} catch (error) {
event.args = null;
event.decodeError = error;
throw error;
}
}

getEmit(event: Event): Array<any> {
const errors = checkResultErrors(event.args);
if (errors.length) { throw errors[0].error; }

const args = (event.args || []).slice();
args.push(event);
return args;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit bda6623

Please sign in to comment.