More flexible Contract for events and allow listening to multiple events in a single filter.
This commit is contained in:
parent
8fe938e69b
commit
d2271617a6
@ -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,13 +63,7 @@ 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 {
|
||||
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"`
|
||||
fragment = contract.interface.getEvent(event);
|
||||
topics = [ fragment.topicHash ];
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user