Added support for Contract event parsing error recovery.

This commit is contained in:
Richard Moore 2020-04-16 21:33:24 -04:00
parent 4ef0e4f765
commit cc72f76695
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651

@ -51,6 +51,9 @@ export interface Event extends Log {
// The parsed arguments to the event // The parsed arguments to the event
args?: Result; args?: Result;
// If parsing the arguments failed, this is the error
decodeError?: Error;
// A function that can be used to decode event data and topics // A function that can be used to decode event data and topics
decode?: (data: string, topics?: Array<string>) => any; decode?: (data: string, topics?: Array<string>) => any;
@ -346,6 +349,11 @@ class RunningEvent {
prepareEvent(event: Event): void { prepareEvent(event: Event): void {
} }
// Returns the array that will be applied to an emit
getEmit(event: Event): Array<any> {
return [ event ];
}
} }
class ErrorRunningEvent extends RunningEvent { class ErrorRunningEvent extends RunningEvent {
@ -354,6 +362,12 @@ 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.
// A Fragment Event will populate all the properties that Wildcard
// will, and additioanlly dereference the arguments when emitting
class FragmentRunningEvent extends RunningEvent { class FragmentRunningEvent extends RunningEvent {
readonly address: string; readonly address: string;
readonly interface: Interface; readonly interface: Interface;
@ -389,15 +403,32 @@ class FragmentRunningEvent extends RunningEvent {
return this.interface.decodeEventLog(this.fragment, data, topics); return this.interface.decodeEventLog(this.fragment, data, topics);
}; };
try {
event.args = this.interface.decodeEventLog(this.fragment, event.data, event.topics); event.args = this.interface.decodeEventLog(this.fragment, event.data, event.topics);
} catch (error) {
event.args = null;
event.decodeError = error;
throw error;
}
}
getEmit(event: Event): Array<any> {
const args = (event.args || []).slice();
args.push(event);
return args;
} }
} }
// A Wildard Event will attempt to populate:
// - event The name of the event name
// - eventSignature The full signature of the event
// - decode A function to decode data and topics
// - args The decoded data and topics
class WildcardRunningEvent extends RunningEvent { class WildcardRunningEvent extends RunningEvent {
readonly address: string; readonly address: string;
readonly interface: Interface; readonly interface: Interface;
constructor(address :string, contractInterface: Interface) { constructor(address: string, contractInterface: Interface) {
super("*", { address: address }); super("*", { address: address });
defineReadOnly(this, "address", address); defineReadOnly(this, "address", address);
defineReadOnly(this, "interface", contractInterface); defineReadOnly(this, "interface", contractInterface);
@ -406,8 +437,8 @@ class WildcardRunningEvent extends RunningEvent {
prepareEvent(event: Event): void { prepareEvent(event: Event): void {
super.prepareEvent(event); super.prepareEvent(event);
try {
const parsed = this.interface.parseLog(event); const parsed = this.interface.parseLog(event);
if (parsed) {
event.event = parsed.name; event.event = parsed.name;
event.eventSignature = parsed.signature; event.eventSignature = parsed.signature;
@ -416,6 +447,8 @@ class WildcardRunningEvent extends RunningEvent {
}; };
event.args = parsed.args; event.args = parsed.args;
} catch (error) {
// No matching event
} }
} }
} }
@ -674,7 +707,6 @@ export class Contract {
private _getRunningEvent(eventName: EventFilter | string): RunningEvent { private _getRunningEvent(eventName: EventFilter | string): RunningEvent {
if (typeof(eventName) === "string") { if (typeof(eventName) === "string") {
// Listen for "error" events (if your contract has an error event, include // Listen for "error" events (if your contract has an error event, include
// the full signature to bypass this special event keyword) // the full signature to bypass this special event keyword)
if (eventName === "error") { if (eventName === "error") {
@ -686,34 +718,32 @@ export class Contract {
return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface)); return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface));
} }
// Get the event Fragment (throws if ambiguous/unknown event)
const fragment = this.interface.getEvent(eventName) const fragment = this.interface.getEvent(eventName)
if (!fragment) {
logger.throwArgumentError("unknown event - " + eventName, "eventName", eventName);
}
return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment)); return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment));
} }
const filter: EventFilter = { // We have topics to filter by...
address: this.address if (eventName.topics && eventName.topics.length > 0) {
}
// Find the matching event in the ABI; if none, we still allow filtering // Is it a known topichash? (throws if no matching topichash)
// since it may be a filter for an otherwise unknown event try {
if (eventName.topics) {
if (eventName.topics[0]) {
const 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)); return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment, eventName.topics));
} } catch (error) { }
}
filter.topics = eventName.topics; // Filter by the unknown topichash
const filter: EventFilter = {
address: this.address,
topics: eventName.topics
} }
return this._normalizeRunningEvent(new RunningEvent(getEventTag(filter), filter)); return this._normalizeRunningEvent(new RunningEvent(getEventTag(filter), filter));
} }
return this._normalizeRunningEvent(new WildcardRunningEvent(this.address, this.interface));
}
_checkRunningEvents(runningEvent: RunningEvent): void { _checkRunningEvents(runningEvent: RunningEvent): void {
if (runningEvent.listenerCount() === 0) { if (runningEvent.listenerCount() === 0) {
delete this._runningEvents[runningEvent.tag]; delete this._runningEvents[runningEvent.tag];
@ -727,16 +757,11 @@ export class Contract {
} }
} }
private _wrapEvent(runningEvent: RunningEvent, log: Log, listener: Listener): Event { // Subclasses can override this to gracefully recover
// from parse errors if they wish
_wrapEvent(runningEvent: RunningEvent, log: Log, listener: Listener): Event {
const event = <Event>deepCopy(log); const event = <Event>deepCopy(log);
try {
runningEvent.prepareEvent(event);
} catch (error) {
this.emit("error", error);
throw error;
}
event.removeListener = () => { event.removeListener = () => {
if (!listener) { return; } if (!listener) { return; }
runningEvent.removeListener(listener); runningEvent.removeListener(listener);
@ -747,6 +772,9 @@ export class Contract {
event.getTransaction = () => { return this.provider.getTransaction(log.transactionHash); } event.getTransaction = () => { return this.provider.getTransaction(log.transactionHash); }
event.getTransactionReceipt = () => { return this.provider.getTransactionReceipt(log.transactionHash); } event.getTransactionReceipt = () => { return this.provider.getTransactionReceipt(log.transactionHash); }
// This may throw if the topics and data mismatch the signature
runningEvent.prepareEvent(event);
return event; return event;
} }
@ -760,12 +788,18 @@ export class Contract {
// Track this running event and its listeners (may already be there; but no hard in updating) // Track this running event and its listeners (may already be there; but no hard in updating)
this._runningEvents[runningEvent.tag] = runningEvent; this._runningEvents[runningEvent.tag] = runningEvent;
// If we are not polling the provider, start // 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) => {
const event = this._wrapEvent(runningEvent, log, listener); let event = null;
const args = (event.args || []).slice(); try {
args.push(event); event = this._wrapEvent(runningEvent, log, listener);
} catch (error) {
// There was an error decoding the data and topics
this.emit("error", error, event);
return;
}
const args = runningEvent.getEmit(event);
this.emit(runningEvent.filter, ...args); this.emit(runningEvent.filter, ...args);
}; };
this._wrappedEmits[runningEvent.tag] = wrappedEmit; this._wrappedEmits[runningEvent.tag] = wrappedEmit;