diff --git a/src.ts/contract/contract.ts b/src.ts/contract/contract.ts index f9fb6ab4e..5915a35cc 100644 --- a/src.ts/contract/contract.ts +++ b/src.ts/contract/contract.ts @@ -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): Array { - items = Array.from((new Set(items)).values()) - items.sort(); - return items; -} - -class PreparedTopicFilter implements DeferredTopicFilter { +class PreparedTopicFilter implements DeferredTopicFilter { #filter: Promise; readonly fragment!: EventFragment; @@ -223,11 +217,9 @@ class WrappedMethod = Array, R = any, D extends R | Co async send(...args: ContractMethodArgs): Promise { 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 = Array, R = any, D extends R | Co async estimateGas(...args: ContractMethodArgs): Promise { 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): Promise { 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 = Array> extends _WrappedEventBase( return new Proxy(this, { // Perform the default operation for this fragment type - apply: async (target, thisArg, args: ContractEventArgs) => { + apply: (target, thisArg, args: ContractEventArgs) => { 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>; + 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 { 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 { +async function getSub(contract: BaseContract, operation: string, event: ContractEventName): Promise { // 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 = Promise.resolve(); -async function _emit(contract: BaseContract, event: ContractEventName, args: Array, payload: null | ContractEventPayload): Promise { +type PayloadFunc = (listener: null | Listener) => ContractUnknownEventPayload; + +async function _emit(contract: BaseContract, event: ContractEventName, args: Array, payloadFunc: null | PayloadFunc): Promise { 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, payload: null | ContractEventPayload): Promise { +async function emit(contract: BaseContract, event: ContractEventName, args: Array, payloadFunc: null | PayloadFunc): Promise { 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 { return await getInternal(this).addrPromise; } async getDeployedCode(): Promise { 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 { const checkCode = async () => { @@ -634,33 +656,41 @@ export class BaseContract implements Addressable, EventEmitterable> { + async queryFilter(event: ContractEventName, fromBlock: BlockTag = 0, toBlock: BlockTag = "latest"): Promise> { 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 { - 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 { - const sub = await getSub(this, event); + const sub = await getSub(this, "once", event); sub.listeners.push({ listener, once: true }); sub.start(); return this; diff --git a/src.ts/contract/types.ts b/src.ts/contract/types.ts index 81a01e4a5..80054507b 100644 --- a/src.ts/contract/types.ts +++ b/src.ts/contract/types.ts @@ -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; diff --git a/src.ts/contract/wrappers.ts b/src.ts/contract/wrappers.ts index 4b59dc1a0..7419ee7bb 100644 --- a/src.ts/contract/wrappers.ts +++ b/src.ts/contract/wrappers.ts @@ -64,25 +64,12 @@ export class ContractTransactionResponse extends TransactionResponse { } } -export class ContractEventPayload extends EventPayload { +export class ContractUnknownEventPayload extends EventPayload { + 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(this, { args, fragment, log }); - } - - get eventName(): string { - return this.fragment.name; - } - - get eventSignature(): string { - return this.fragment.format(); + defineProperties(this, { log }); } async getBlock(): Promise> { @@ -97,3 +84,24 @@ export class ContractEventPayload extends EventPayload { 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(this, { args, fragment }); + } + + get eventName(): string { + return this.fragment.name; + } + + get eventSignature(): string { + return this.fragment.format(); + } +}