More flexible Contract for events and allow listening to multiple events in a single filter.

This commit is contained in:
Richard Moore 2022-11-04 18:10:00 -04:00
parent 8fe938e69b
commit d2271617a6
3 changed files with 129 additions and 91 deletions

@ -3,11 +3,11 @@ import { resolveAddress } from "../address/index.js";
import { copyRequest, Log, TransactionResponse } from "../providers/index.js";
import {
defineProperties, isCallException, isHexString, resolveProperties,
makeError, assertArgument, throwError
makeError, assert, assertArgument
} from "../utils/index.js";
import {
ContractEventPayload,
ContractEventPayload, ContractUnknownEventPayload,
ContractTransactionResponse,
EventLog
} from "./wrappers.js";
@ -63,12 +63,6 @@ function canSend(value: any): value is ContractRunnerSender {
return (value && typeof(value.sendTransaction) === "function");
}
function concisify(items: Array<string>): Array<string> {
items = Array.from((new Set(items)).values())
items.sort();
return items;
}
class PreparedTopicFilter implements DeferredTopicFilter {
#filter: Promise<TopicFilter>;
readonly fragment!: EventFragment;
@ -223,11 +217,9 @@ class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | Co
async send(...args: ContractMethodArgs<A>): Promise<ContractTransactionResponse> {
const runner = this._contract.runner;
if (!canSend(runner)) {
return throwError("contract runner does not support sending transactions", "UNSUPPORTED_OPERATION", {
operation: "sendTransaction"
});
}
assert(canSend(runner), "contract runner does not support sending transactions",
"UNSUPPORTED_OPERATION", { operation: "sendTransaction" });
const tx = await runner.sendTransaction(await this.populateTransaction(...args));
const provider = getProvider(this._contract.runner);
// @TODO: the provider can be null; make a custom dummy provider that will throw a
@ -237,21 +229,16 @@ class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | Co
async estimateGas(...args: ContractMethodArgs<A>): Promise<bigint> {
const runner = getRunner(this._contract.runner, "estimateGas");
if (!canEstimate(runner)) {
return throwError("contract runner does not support gas estimation", "UNSUPPORTED_OPERATION", {
operation: "estimateGas"
});
}
assert(canEstimate(runner), "contract runner does not support gas estimation",
"UNSUPPORTED_OPERATION", { operation: "estimateGas" });
return await runner.estimateGas(await this.populateTransaction(...args));
}
async staticCallResult(...args: ContractMethodArgs<A>): Promise<Result> {
const runner = getRunner(this._contract.runner, "call");
if (!canCall(runner)) {
return throwError("contract runner does not support calling", "UNSUPPORTED_OPERATION", {
operation: "call"
});
}
assert(canCall(runner), "contract runner does not support calling",
"UNSUPPORTED_OPERATION", { operation: "call" });
const tx = await this.populateTransaction(...args);
@ -290,7 +277,7 @@ class WrappedEvent<A extends Array<any> = Array<any>> extends _WrappedEventBase(
return new Proxy(this, {
// Perform the default operation for this fragment type
apply: async (target, thisArg, args: ContractEventArgs<A>) => {
apply: (target, thisArg, args: ContractEventArgs<A>) => {
return new PreparedTopicFilter(contract, target.getFragment(...args), args);
},
});
@ -344,23 +331,41 @@ function isDeferred(value: any): value is DeferredTopicFilter {
(typeof(value.getTopicFilter) === "function") && value.fragment);
}
async function getSubTag(contract: BaseContract, event: ContractEventName): Promise<{ tag: string, fragment: EventFragment, topics: TopicFilter }> {
let fragment: EventFragment;
async function getSubInfo(contract: BaseContract, event: ContractEventName): Promise<{ fragment: null | EventFragment, tag: string, topics: TopicFilter }> {
let topics: Array<null | string | Array<string>>;
let fragment: null | EventFragment = null;
// Convert named events to topicHash and get the fragment for
// events which need deconstructing.
if (Array.isArray(event)) {
// Topics; e.g. `[ "0x1234...89ab" ]`
fragment = contract.interface.getEvent(event[0] as string);
topics = event;
const topicHashify = function(name: string): string {
if (isHexString(name, 32)) { return name; }
return contract.interface.getEvent(name).topicHash;
}
// Array of Topics and Names; e.g. `[ "0x1234...89ab", "Transfer(address)" ]`
topics = event.map((e) => {
if (e == null) { return null; }
if (Array.isArray(e)) { return e.map(topicHashify); }
return topicHashify(e);
});
} else if (event === "*") {
topics = [ null ];
} else if (typeof(event) === "string") {
// Event name (name or signature); `"Transfer"`
if (isHexString(event, 32)) {
// Topic Hash
topics = [ event ];
} else {
// Name or Signature; e.g. `"Transfer", `"Transfer(address)"`
fragment = contract.interface.getEvent(event);
topics = [ fragment.topicHash ];
}
} else if (isDeferred(event)) {
// Deferred Topic Filter; e.g. `contract.filter.Transfer(from)`
fragment = event.fragment;
topics = await event.getTopicFilter();
} else if ("fragment" in event) {
@ -369,15 +374,17 @@ async function getSubTag(contract: BaseContract, event: ContractEventName): Prom
topics = [ fragment.topicHash ];
} else {
console.log(event);
throw new Error("TODO");
assertArgument(false, "unknown event name", "event", event);
}
// Normalize topics and sort TopicSets
topics = topics.map((t) => {
if (t == null) { return null; }
if (Array.isArray(t)) {
return concisify(t.map((t) => t.toLowerCase()));
const items = Array.from(new Set(t.map((t) => t.toLowerCase())).values());
if (items.length === 1) { return items[0]; }
items.sort();
return items;
}
return t.toLowerCase();
});
@ -393,19 +400,16 @@ async function getSubTag(contract: BaseContract, event: ContractEventName): Prom
async function hasSub(contract: BaseContract, event: ContractEventName): Promise<null | Sub> {
const { subs } = getInternal(contract);
return subs.get((await getSubTag(contract, event)).tag) || null;
return subs.get((await getSubInfo(contract, event)).tag) || null;
}
async function getSub(contract: BaseContract, event: ContractEventName): Promise<Sub> {
async function getSub(contract: BaseContract, operation: string, event: ContractEventName): Promise<Sub> {
// Make sure our runner can actually subscribe to events
const provider = getProvider(contract.runner);
if (!provider) {
return throwError("contract runner does not support subscribing", "UNSUPPORTED_OPERATION", {
operation: "on"
});
}
assert(provider, "contract runner does not support subscribing",
"UNSUPPORTED_OPERATION", { operation });
const { fragment, tag, topics } = await getSubTag(contract, event);
const { fragment, tag, topics } = await getSubInfo(contract, event);
const { addr, subs } = getInternal(contract);
@ -414,8 +418,26 @@ async function getSub(contract: BaseContract, event: ContractEventName): Promise
const address: string | Addressable = (addr ? addr: contract);
const filter = { address, topics };
const listener = (log: Log) => {
const payload = new ContractEventPayload(contract, null, event, fragment, log);
emit(contract, event, payload.args, payload);
let foundFragment = fragment;
if (foundFragment == null) {
try {
foundFragment = contract.interface.getEvent(log.topics[0]);
} catch (error) { }
}
// If fragment is null, we do not deconstruct the args to emit
if (foundFragment) {
const _foundFragment = foundFragment;
const args = fragment ? contract.interface.decodeEventLog(fragment, log.data, log.topics): [ ];
emit(contract, event, args, (listener: null | Listener) => {
return new ContractEventPayload(contract, listener, event, _foundFragment, log);
});
} else {
emit(contract, event, [ ], (listener: null | Listener) => {
return new ContractUnknownEventPayload(contract, listener, event, log);
});
}
};
let started = false;
@ -440,7 +462,9 @@ async function getSub(contract: BaseContract, event: ContractEventName): Promise
// notice to the event queu using setTimeout).
let lastEmit: Promise<any> = Promise.resolve();
async function _emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payload: null | ContractEventPayload): Promise<boolean> {
type PayloadFunc = (listener: null | Listener) => ContractUnknownEventPayload;
async function _emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payloadFunc: null | PayloadFunc): Promise<boolean> {
await lastEmit;
const sub = await hasSub(contract, event);
@ -449,9 +473,8 @@ async function _emit(contract: BaseContract, event: ContractEventName, args: Arr
const count = sub.listeners.length;
sub.listeners = sub.listeners.filter(({ listener, once }) => {
const passArgs = args.slice();
if (payload) {
passArgs.push(new ContractEventPayload(contract, (once ? null: listener),
event, payload.fragment, payload.log));
if (payloadFunc) {
passArgs.push(payloadFunc(once ? null: listener));
}
try {
listener.call(contract, ...passArgs);
@ -461,12 +484,12 @@ async function _emit(contract: BaseContract, event: ContractEventName, args: Arr
return (count > 0);
}
async function emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payload: null | ContractEventPayload): Promise<boolean> {
async function emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payloadFunc: null | PayloadFunc): Promise<boolean> {
try {
await lastEmit;
} catch (error) { }
const resultPromise = _emit(contract, event, args, payload);
const resultPromise = _emit(contract, event, args, payloadFunc);
lastEmit = resultPromise;
return await resultPromise;
}
@ -564,17 +587,19 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
throw new Error(`unknown contract method: ${ prop }`);
}
});
}
connect(runner: null | ContractRunner): BaseContract {
return new BaseContract(this.target, this.interface, runner);
}
async getAddress(): Promise<string> { return await getInternal(this).addrPromise; }
async getDeployedCode(): Promise<null | string> {
const provider = getProvider(this.runner);
if (!provider) {
return throwError("runner does not support .provider", "UNSUPPORTED_OPERATION", {
operation: "getDeployedCode"
});
}
assert(provider, "runner does not support .provider",
"UNSUPPORTED_OPERATION", { operation: "getDeployedCode" });
const code = await provider.getCode(await this.getAddress());
if (code === "0x") { return null; }
@ -595,11 +620,8 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
// Make sure we can subscribe to a provider event
const provider = getProvider(this.runner);
if (provider == null) {
return throwError("contract runner does not support .provider", "UNSUPPORTED_OPERATION", {
operation: "waitForDeployment"
});
}
assert(provider != null, "contract runner does not support .provider",
"UNSUPPORTED_OPERATION", { operation: "waitForDeployment" });
return new Promise((resolve, reject) => {
const checkCode = async () => {
@ -634,33 +656,41 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
throw new Error("@TODO");
}
async queryFilter(event: ContractEventName, fromBlock: BlockTag = 0, toBlock: BlockTag = "latest"): Promise<Array<EventLog>> {
async queryFilter(event: ContractEventName, fromBlock: BlockTag = 0, toBlock: BlockTag = "latest"): Promise<Array<EventLog | Log>> {
const { addr, addrPromise } = getInternal(this);
const address = (addr ? addr: (await addrPromise));
const { fragment, topics } = await getSubTag(this, event);
const { fragment, topics } = await getSubInfo(this, event);
const filter = { address, topics, fromBlock, toBlock };
const provider = getProvider(this.runner);
if (!provider) {
return throwError("contract runner does not have a provider", "UNSUPPORTED_OPERATION", {
operation: "queryFilter"
});
}
assert(provider, "contract runner does not have a provider",
"UNSUPPORTED_OPERATION", { operation: "queryFilter" });
return (await provider.getLogs(filter)).map((log) => {
return new EventLog(log, this.interface, fragment);
let foundFragment = fragment;
if (foundFragment == null) {
try {
foundFragment = this.interface.getEvent(log.topics[0]);
} catch (error) { }
}
if (foundFragment) {
return new EventLog(log, this.interface, foundFragment);
} else {
return new Log(log, provider);
}
});
}
async on(event: ContractEventName, listener: Listener): Promise<this> {
const sub = await getSub(this, event);
const sub = await getSub(this, "on", event);
sub.listeners.push({ listener, once: false });
sub.start();
return this;
}
async once(event: ContractEventName, listener: Listener): Promise<this> {
const sub = await getSub(this, event);
const sub = await getSub(this, "once", event);
sub.listeners.push({ listener, once: true });
sub.start();
return this;

@ -9,7 +9,7 @@ import type { ContractTransactionResponse } from "./wrappers.js";
// The types of events a Contract can listen for
export type ContractEventName = string | ContractEvent | TopicFilter;
export type ContractEventName = string | ContractEvent | TopicFilter | DeferredTopicFilter;
export interface ContractInterface {
[ name: string ]: BaseContractMethod;

@ -64,25 +64,12 @@ export class ContractTransactionResponse extends TransactionResponse {
}
}
export class ContractEventPayload extends EventPayload<ContractEventName> {
export class ContractUnknownEventPayload extends EventPayload<ContractEventName> {
readonly log!: Log;
readonly fragment!: EventFragment;
readonly log!: EventLog;
readonly args!: Result;
constructor(contract: BaseContract, listener: null | Listener, filter: ContractEventName, fragment: EventFragment, _log: Log) {
constructor(contract: BaseContract, listener: null | Listener, filter: ContractEventName, log: Log) {
super(contract, listener, filter);
const log = new EventLog(_log, contract.interface, fragment);
const args = contract.interface.decodeEventLog(fragment, log.data, log.topics);
defineProperties<ContractEventPayload>(this, { args, fragment, log });
}
get eventName(): string {
return this.fragment.name;
}
get eventSignature(): string {
return this.fragment.format();
defineProperties<ContractUnknownEventPayload>(this, { log });
}
async getBlock(): Promise<Block<string>> {
@ -97,3 +84,24 @@ export class ContractEventPayload extends EventPayload<ContractEventName> {
return await this.log.getTransactionReceipt();
}
}
export class ContractEventPayload extends ContractUnknownEventPayload {
declare readonly fragment: EventFragment;
declare readonly log: EventLog;
declare readonly args: Result;
constructor(contract: BaseContract, listener: null | Listener, filter: ContractEventName, fragment: EventFragment, _log: Log) {
super(contract, listener, filter, new EventLog(_log, contract.interface, fragment));
const args = contract.interface.decodeEventLog(fragment, this.log.data, this.log.topics);
defineProperties<ContractEventPayload>(this, { args, fragment });
}
get eventName(): string {
return this.fragment.name;
}
get eventSignature(): string {
return this.fragment.format();
}
}