Added initial support for recoverable coding erros (#800).
This commit is contained in:
parent
14e6811bf7
commit
bda6623091
@ -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 {
|
||||
@ -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;
|
||||
|
@ -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) {
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
@ -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>;
|
||||
|
||||
@ -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:
|
||||
@ -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") {
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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<ParamType>): Promise<any> {
|
||||
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<any> {
|
||||
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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user