"use strict"; import { checkResultErrors, EventFragment, Fragment, FunctionFragment, 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 { getAddress, getContractAddress } from "@ethersproject/address"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { arrayify, BytesLike, concat, hexlify, isBytes, isHexString } from "@ethersproject/bytes"; import { Deferrable, defineReadOnly, deepCopy, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { AccessList, accessListify, AccessListish } from "@ethersproject/transactions"; import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; const logger = new Logger(version); export interface Overrides { gasLimit?: BigNumberish | Promise; gasPrice?: BigNumberish | Promise; maxFeePerGas?: BigNumberish | Promise; maxPriorityFeePerGas?: BigNumberish | Promise; nonce?: BigNumberish | Promise; type?: number; accessList?: AccessListish; customData?: Record; }; export interface PayableOverrides extends Overrides { value?: BigNumberish | Promise; } export interface CallOverrides extends PayableOverrides { blockTag?: BlockTag | Promise; from?: string | Promise; } // @TODO: Better hierarchy with: (in v6) // - abstract-provider:TransactionRequest // - transactions:Transaction // - transaction:UnsignedTransaction export interface PopulatedTransaction { to?: string; from?: string; nonce?: number; gasLimit?: BigNumber; gasPrice?: BigNumber; data?: string; value?: BigNumber; chainId?: number; type?: number; accessList?: AccessList; maxFeePerGas?: BigNumber; maxPriorityFeePerGas?: BigNumber; customData?: Record; }; export type EventFilter = { address?: string; topics?: Array>; }; export type ContractFunction = (...args: Array) => Promise; // The (n + 1)th parameter passed to contract event callbacks export interface Event extends Log { // The event name event?: string; // The event signature eventSignature?: string; // The parsed arguments to the event 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 decode?: (data: string, topics?: Array) => any; // A function that will remove the listener responsible for this event (if any) removeListener: () => void; // Get blockchain details about this event's block and transaction getBlock: () => Promise; getTransaction: () => Promise; getTransactionReceipt: () => Promise; } export interface ContractReceipt extends TransactionReceipt { events?: Array; } export interface ContractTransaction extends TransactionResponse { wait(confirmations?: number): Promise; } /////////////////////////////// const allowedTransactionKeys: { [ key: string ]: boolean } = { chainId: true, data: true, from: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true, type: true, accessList: true, maxFeePerGas: true, maxPriorityFeePerGas: true, customData: true } async function resolveName(resolver: Signer | Provider, nameOrPromise: string | Promise): Promise { const name = await nameOrPromise; // If it is already an address, just use it (after adding checksum) try { return getAddress(name); } catch (error) { } if (!resolver) { logger.throwError("a provider or signer is needed to resolve ENS names", Logger.errors.UNSUPPORTED_OPERATION, { operation: "resolveName" }); } const address = await resolver.resolveName(name); if (address == null) { logger.throwArgumentError("resolver or addr is not configured for ENS name", "name", name); } return address; } // Recursively replaces ENS names with promises to resolve the name and resolves all properties async function resolveAddresses(resolver: Signer | Provider, value: any, paramType: ParamType | Array): Promise { if (Array.isArray(paramType)) { return await Promise.all(paramType.map((paramType, index) => { return resolveAddresses( resolver, ((Array.isArray(value)) ? value[index]: value[paramType.name]), paramType ); })); } if (paramType.type === "address") { return await resolveName(resolver, value); } if (paramType.type === "tuple") { return await resolveAddresses(resolver, value, paramType.components); } if (paramType.baseType === "array") { if (!Array.isArray(value)) { return Promise.reject(logger.makeError("invalid value for array", Logger.errors.INVALID_ARGUMENT, { argument: "value", value })); } return await Promise.all(value.map((v) => resolveAddresses(resolver, v, paramType.arrayChildren))); } return value; } async function populateTransaction(contract: Contract, fragment: FunctionFragment, args: Array): Promise { // If an extra argument is given, it is overrides let overrides: CallOverrides = { }; if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { overrides = shallowCopy(args.pop()); } // Make sure the parameter count matches logger.checkArgumentCount(args.length, fragment.inputs.length, "passed to contract"); // Populate "from" override (allow promises) if (contract.signer) { if (overrides.from) { // Contracts with a Signer are from the Signer's frame-of-reference; // but we allow overriding "from" if it matches the signer overrides.from = resolveProperties({ override: resolveName(contract.signer, overrides.from), signer: contract.signer.getAddress() }).then(async (check) => { if (getAddress(check.signer) !== check.override) { logger.throwError("Contract with a Signer cannot override from", Logger.errors.UNSUPPORTED_OPERATION, { operation: "overrides.from" }); } return check.override; }); } else { overrides.from = contract.signer.getAddress(); } } else if (overrides.from) { overrides.from = resolveName(contract.provider, overrides.from); //} else { // Contracts without a signer can override "from", and if // unspecified the zero address is used //overrides.from = AddressZero; } // Wait for all dependencies to be resolved (prefer the signer over the provider) const resolved = await resolveProperties({ args: resolveAddresses(contract.signer || contract.provider, args, fragment.inputs), address: contract.resolvedAddress, overrides: (resolveProperties(overrides) || { }) }); // The ABI coded transaction const data = contract.interface.encodeFunctionData(fragment, resolved.args); const tx: PopulatedTransaction = { data: data, to: resolved.address }; // Resolved Overrides const ro = resolved.overrides; // Populate simple overrides if (ro.nonce != null) { tx.nonce = BigNumber.from(ro.nonce).toNumber(); } if (ro.gasLimit != null) { tx.gasLimit = BigNumber.from(ro.gasLimit); } if (ro.gasPrice != null) { tx.gasPrice = BigNumber.from(ro.gasPrice); } if (ro.maxFeePerGas != null) { tx.maxFeePerGas = BigNumber.from(ro.maxFeePerGas); } if (ro.maxPriorityFeePerGas != null) { tx.maxPriorityFeePerGas = BigNumber.from(ro.maxPriorityFeePerGas); } if (ro.from != null) { tx.from = ro.from; } if (ro.type != null) { tx.type = ro.type; } if (ro.accessList != null) { tx.accessList = accessListify(ro.accessList); } // If there was no "gasLimit" override, but the ABI specifies a default, use it if (tx.gasLimit == null && fragment.gas != null) { // Compute the intrinsic gas cost for this transaction // @TODO: This is based on the yellow paper as of Petersburg; this is something // we may wish to parameterize in v6 as part of the Network object. Since this // is always a non-nil to address, we can ignore G_create, but may wish to add // similar logic to the ContractFactory. let intrinsic = 21000; const bytes = arrayify(data); for (let i = 0; i < bytes.length; i++) { intrinsic += 4; if (bytes[i]) { intrinsic += 64; } } tx.gasLimit = BigNumber.from(fragment.gas).add(intrinsic); } // Populate "value" override if (ro.value) { const roValue = BigNumber.from(ro.value); if (!roValue.isZero() && !fragment.payable) { logger.throwError("non-payable method cannot override value", Logger.errors.UNSUPPORTED_OPERATION, { operation: "overrides.value", value: overrides.value }); } tx.value = roValue; } if (ro.customData) { tx.customData = shallowCopy(ro.customData); } // Remove the overrides delete overrides.nonce; delete overrides.gasLimit; delete overrides.gasPrice; delete overrides.from; delete overrides.value; delete overrides.type; delete overrides.accessList; delete overrides.maxFeePerGas; delete overrides.maxPriorityFeePerGas; delete overrides.customData; // Make sure there are no stray overrides, which may indicate a // typo or using an unsupported key. const leftovers = Object.keys(overrides).filter((key) => ((overrides)[key] != null)); if (leftovers.length) { logger.throwError(`cannot override ${ leftovers.map((l) => JSON.stringify(l)).join(",") }`, Logger.errors.UNSUPPORTED_OPERATION, { operation: "overrides", overrides: leftovers }); } return tx; } function buildPopulate(contract: Contract, fragment: FunctionFragment): ContractFunction { return function(...args: Array): Promise { return populateTransaction(contract, fragment, args); }; } function buildEstimate(contract: Contract, fragment: FunctionFragment): ContractFunction { const signerOrProvider = (contract.signer || contract.provider); return async function(...args: Array): Promise { if (!signerOrProvider) { logger.throwError("estimate require a provider or signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "estimateGas" }) } const tx = await populateTransaction(contract, fragment, args); return await signerOrProvider.estimateGas(tx); }; } function buildCall(contract: Contract, fragment: FunctionFragment, collapseSimple: boolean): ContractFunction { const signerOrProvider = (contract.signer || contract.provider); return async function(...args: Array): Promise { // Extract the "blockTag" override if present let blockTag = undefined; if (args.length === fragment.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { const overrides = shallowCopy(args.pop()); if (overrides.blockTag != null) { blockTag = await overrides.blockTag; } delete overrides.blockTag; args.push(overrides); } // If the contract was just deployed, wait until it is mined if (contract.deployTransaction != null) { await contract._deployed(blockTag); } // Call a node and get the result const tx = await populateTransaction(contract, fragment, args); const result = await signerOrProvider.call(tx, blockTag); try { let value = contract.interface.decodeFunctionResult(fragment, result); if (collapseSimple && fragment.outputs.length === 1) { value = value[0]; } return value; } catch (error) { if (error.code === Logger.errors.CALL_EXCEPTION) { error.address = contract.address; error.args = args; error.transaction = tx; } throw error; } }; } function buildSend(contract: Contract, fragment: FunctionFragment): ContractFunction { return async function(...args: Array): Promise { if (!contract.signer) { logger.throwError("sending a transaction requires a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction" }) } // If the contract was just deployed, wait until it is mined if (contract.deployTransaction != null) { await contract._deployed(); } const txRequest = await populateTransaction(contract, fragment, args); const tx = await contract.signer.sendTransaction(txRequest); // Tweak the tx.wait so the receipt has extra properties const wait = tx.wait.bind(tx); tx.wait = (confirmations?: number) => { return wait(confirmations).then((receipt: ContractReceipt) => { receipt.events = receipt.logs.map((log) => { let event: Event = (deepCopy(log)); let parsed: LogDescription = null; try { parsed = contract.interface.parseLog(log); } catch (e){ } // Successfully parsed the event log; include it if (parsed) { event.args = parsed.args; event.decode = (data: BytesLike, topics?: Array) => { return contract.interface.decodeEventLog(parsed.eventFragment, data, topics); }; event.event = parsed.name; event.eventSignature = parsed.signature; } // Useful operations event.removeListener = () => { return contract.provider; } event.getBlock = () => { return contract.provider.getBlock(receipt.blockHash); } event.getTransaction = () => { return contract.provider.getTransaction(receipt.transactionHash); } event.getTransactionReceipt = () => { return Promise.resolve(receipt); } return event; }); return receipt; }); }; return tx; }; } function buildDefault(contract: Contract, fragment: FunctionFragment, collapseSimple: boolean): ContractFunction { if (fragment.constant) { return buildCall(contract, fragment, collapseSimple); } return buildSend(contract, fragment); } function getEventTag(filter: EventFilter): string { if (filter.address && (filter.topics == null || filter.topics.length === 0)) { return "*"; } return (filter.address || "*") + "@" + (filter.topics ? filter.topics.map((topic) => { if (Array.isArray(topic)) { return topic.join("|"); } return topic; }).join(":"): ""); } class RunningEvent { readonly tag: string; readonly filter: EventFilter; private _listeners: Array<{ listener: Listener, once: boolean }>; constructor(tag: string, filter: EventFilter) { defineReadOnly(this, "tag", tag); defineReadOnly(this, "filter", filter); this._listeners = [ ]; } addListener(listener: Listener, once: boolean): void { this._listeners.push({ listener: listener, once: once }); } removeListener(listener: Listener): void { let done = false; this._listeners = this._listeners.filter((item) => { if (done || item.listener !== listener) { return true; } done = true; return false; }); } removeAllListeners(): void { this._listeners = []; } listeners(): Array { return this._listeners.map((i) => i.listener); } listenerCount(): number { return this._listeners.length; } run(args: Array): number { const listenerCount = this.listenerCount(); this._listeners = this._listeners.filter((item) => { const argsCopy = args.slice(); // Call the callback in the next event loop setTimeout(() => { item.listener.apply(this, argsCopy); }, 0); // Reschedule it if it not "once" return !(item.once); }); return listenerCount; } prepareEvent(event: Event): void { } // Returns the array that will be applied to an emit getEmit(event: Event): Array { return [ event ]; } } class ErrorRunningEvent extends RunningEvent { constructor() { super("error", null); } } // @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 additionally dereference the arguments when emitting class FragmentRunningEvent extends RunningEvent { readonly address: string; readonly interface: Interface; readonly fragment: EventFragment; constructor(address: string, contractInterface: Interface, fragment: EventFragment, topics?: Array>) { const filter: EventFilter = { address: address } let topic = contractInterface.getEventTopic(fragment); if (topics) { if (topic !== topics[0]) { logger.throwArgumentError("topic mismatch", "topics", topics); } filter.topics = topics.slice(); } else { filter.topics = [ topic ]; } super(getEventTag(filter), filter); defineReadOnly(this, "address", address); defineReadOnly(this, "interface", contractInterface); defineReadOnly(this, "fragment", fragment); } prepareEvent(event: Event): void { super.prepareEvent(event); event.event = this.fragment.name; event.eventSignature = this.fragment.format(); event.decode = (data: BytesLike, topics?: Array) => { return this.interface.decodeEventLog(this.fragment, data, topics); }; try { event.args = this.interface.decodeEventLog(this.fragment, event.data, event.topics); } catch (error) { event.args = null; event.decodeError = error; } } getEmit(event: Event): Array { const errors = checkResultErrors(event.args); if (errors.length) { throw errors[0].error; } const args = (event.args || []).slice(); args.push(event); return args; } } // A Wildcard 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 { readonly address: string; readonly interface: Interface; constructor(address: string, contractInterface: Interface) { super("*", { address: address }); defineReadOnly(this, "address", address); defineReadOnly(this, "interface", contractInterface); } prepareEvent(event: Event): void { super.prepareEvent(event); try { const parsed = this.interface.parseLog(event); event.event = parsed.name; event.eventSignature = parsed.signature; event.decode = (data: BytesLike, topics?: Array) => { return this.interface.decodeEventLog(parsed.eventFragment, data, topics); }; event.args = parsed.args; } catch (error) { // No matching event } } } export type ContractInterface = string | ReadonlyArray | Interface; type InterfaceFunc = (contractInterface: ContractInterface) => Interface; export class BaseContract { readonly address: string; readonly interface: Interface; readonly signer: Signer; readonly provider: Provider; readonly functions: { [ name: string ]: ContractFunction }; readonly callStatic: { [ name: string ]: ContractFunction }; readonly estimateGas: { [ name: string ]: ContractFunction }; readonly populateTransaction: { [ name: string ]: ContractFunction }; readonly filters: { [ name: string ]: (...args: Array) => EventFilter }; // This will always be an address. This will only differ from // address if an ENS name was used in the constructor readonly resolvedAddress: Promise; // This is only set if the contract was created with a call to deploy readonly deployTransaction: TransactionResponse; _deployedPromise: Promise; // A list of RunningEvents to track listeners for each event tag _runningEvents: { [ eventTag: string ]: RunningEvent }; // Wrapped functions to call emit and allow deregistration from the provider _wrappedEmits: { [ eventTag: string ]: (...args: Array) => void }; constructor(addressOrName: string, contractInterface: ContractInterface, signerOrProvider?: Signer | Provider) { logger.checkNew(new.target, Contract); // @TODO: Maybe still check the addressOrName looks like a valid address or name? //address = getAddress(address); defineReadOnly(this, "interface", getStatic(new.target, "getInterface")(contractInterface)); if (signerOrProvider == null) { defineReadOnly(this, "provider", null); defineReadOnly(this, "signer", null); } else if (Signer.isSigner(signerOrProvider)) { defineReadOnly(this, "provider", signerOrProvider.provider || null); defineReadOnly(this, "signer", signerOrProvider); } else if (Provider.isProvider(signerOrProvider)) { defineReadOnly(this, "provider", signerOrProvider); defineReadOnly(this, "signer", null); } else { logger.throwArgumentError("invalid signer or provider", "signerOrProvider", signerOrProvider); } defineReadOnly(this, "callStatic", { }); defineReadOnly(this, "estimateGas", { }); defineReadOnly(this, "functions", { }); defineReadOnly(this, "populateTransaction", { }); defineReadOnly(this, "filters", { }); { const uniqueFilters: { [ name: string ]: Array } = { }; Object.keys(this.interface.events).forEach((eventSignature) => { const event = this.interface.events[eventSignature]; defineReadOnly(this.filters, eventSignature, (...args: Array) => { return { address: this.address, topics: this.interface.encodeFilterTopics(event, args) } }); if (!uniqueFilters[event.name]) { uniqueFilters[event.name] = [ ]; } uniqueFilters[event.name].push(eventSignature); }); Object.keys(uniqueFilters).forEach((name) => { const filters = uniqueFilters[name]; if (filters.length === 1) { defineReadOnly(this.filters, name, this.filters[filters[0]]); } else { logger.warn(`Duplicate definition of ${ name } (${ filters.join(", ")})`); } }); } defineReadOnly(this, "_runningEvents", { }); defineReadOnly(this, "_wrappedEmits", { }); if (addressOrName == null) { logger.throwArgumentError("invalid contract address or ENS name", "addressOrName", addressOrName); } defineReadOnly(this, "address", addressOrName); if (this.provider) { defineReadOnly(this, "resolvedAddress", resolveName(this.provider, addressOrName)); } else { try { defineReadOnly(this, "resolvedAddress", Promise.resolve(getAddress(addressOrName))); } catch (error) { // Without a provider, we cannot use ENS names logger.throwError("provider is required to use ENS name as contract address", Logger.errors.UNSUPPORTED_OPERATION, { operation: "new Contract" }); } } const uniqueNames: { [ name: string ]: Array } = { }; const uniqueSignatures: { [ signature: string ]: boolean } = { }; Object.keys(this.interface.functions).forEach((signature) => { const fragment = this.interface.functions[signature]; // Check that the signature is unique; if not the ABI generation has // not been cleaned or may be incorrectly generated if (uniqueSignatures[signature]) { logger.warn(`Duplicate ABI entry for ${ JSON.stringify(signature) }`); return; } uniqueSignatures[signature] = true; // Track unique names; we only expose bare named functions if they // are ambiguous { const name = fragment.name; if (!uniqueNames[name]) { uniqueNames[name] = [ ]; } uniqueNames[name].push(signature); } if ((this)[signature] == null) { defineReadOnly(this, signature, buildDefault(this, fragment, true)); } // We do not collapse simple calls on this bucket, which allows // frameworks to safely use this without introspection as well as // allows decoding error recovery. if (this.functions[signature] == null) { defineReadOnly(this.functions, signature, buildDefault(this, fragment, false)); } if (this.callStatic[signature] == null) { defineReadOnly(this.callStatic, signature, buildCall(this, fragment, true)); } if (this.populateTransaction[signature] == null) { defineReadOnly(this.populateTransaction, signature, buildPopulate(this, fragment)); } if (this.estimateGas[signature] == null) { defineReadOnly(this.estimateGas, signature, buildEstimate(this, fragment)); } }); Object.keys(uniqueNames).forEach((name) => { // Ambiguous names to not get attached as bare names const signatures = uniqueNames[name]; if (signatures.length > 1) { return; } const signature = signatures[0]; // If overwriting a member property that is null, swallow the error try { if ((this)[name] == null) { defineReadOnly(this, name, (this)[signature]); } } catch (e) { } if (this.functions[name] == null) { defineReadOnly(this.functions, name, this.functions[signature]); } if (this.callStatic[name] == null) { defineReadOnly(this.callStatic, name, this.callStatic[signature]); } if (this.populateTransaction[name] == null) { defineReadOnly(this.populateTransaction, name, this.populateTransaction[signature]); } if (this.estimateGas[name] == null) { defineReadOnly(this.estimateGas, name, this.estimateGas[signature]); } }); } static getContractAddress(transaction: { from: string, nonce: BigNumberish }): string { return getContractAddress(transaction); } static getInterface(contractInterface: ContractInterface): Interface { if (Interface.isInterface(contractInterface)) { return contractInterface; } return new Interface(contractInterface); } // @TODO: Allow timeout? deployed(): Promise { return this._deployed(); } _deployed(blockTag?: BlockTag): Promise { if (!this._deployedPromise) { // If we were just deployed, we know the transaction we should occur in if (this.deployTransaction) { this._deployedPromise = this.deployTransaction.wait().then(() => { return this; }); } else { // @TODO: Once we allow a timeout to be passed in, we will wait // up to that many blocks for getCode // Otherwise, poll for our code to be deployed this._deployedPromise = this.provider.getCode(this.address, blockTag).then((code) => { if (code === "0x") { logger.throwError("contract not deployed", Logger.errors.UNSUPPORTED_OPERATION, { contractAddress: this.address, operation: "getDeployed" }); } return this; }); } } return this._deployedPromise; } // @TODO: // estimateFallback(overrides?: TransactionRequest): Promise // @TODO: // estimateDeploy(bytecode: string, ...args): Promise fallback(overrides?: TransactionRequest): Promise { if (!this.signer) { logger.throwError("sending a transactions require a signer", Logger.errors.UNSUPPORTED_OPERATION, { operation: "sendTransaction(fallback)" }) } const tx: Deferrable = shallowCopy(overrides || {}); ["from", "to"].forEach(function(key) { if ((tx)[key] == null) { return; } logger.throwError("cannot override " + key, Logger.errors.UNSUPPORTED_OPERATION, { operation: key }) }); tx.to = this.resolvedAddress; return this.deployed().then(() => { return this.signer.sendTransaction(tx); }); } // Reconnect to a different signer or provider connect(signerOrProvider: Signer | Provider | string): Contract { if (typeof(signerOrProvider) === "string") { signerOrProvider = new VoidSigner(signerOrProvider, this.provider); } const contract = new (<{ new(...args: any[]): Contract }>(this.constructor))(this.address, this.interface, signerOrProvider); if (this.deployTransaction) { defineReadOnly(contract, "deployTransaction", this.deployTransaction); } return contract; } // Re-attach to a different on-chain instance of this contract attach(addressOrName: string): Contract { return new (<{ new(...args: any[]): Contract }>(this.constructor))(addressOrName, this.interface, this.signer || this.provider); } static isIndexed(value: any): value is Indexed { return Indexed.isIndexed(value); } private _normalizeRunningEvent(runningEvent: RunningEvent): RunningEvent { // Already have an instance of this event running; we can re-use it if (this._runningEvents[runningEvent.tag]) { return this._runningEvents[runningEvent.tag]; } return runningEvent } private _getRunningEvent(eventName: EventFilter | string): RunningEvent { if (typeof(eventName) === "string") { // Listen for "error" events (if your contract has an error event, include // the full signature to bypass this special event keyword) if (eventName === "error") { 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)); } // Get the event Fragment (throws if ambiguous/unknown event) const fragment = this.interface.getEvent(eventName) return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment)); } // We have topics to filter by... if (eventName.topics && eventName.topics.length > 0) { // Is it a known topichash? (throws if no matching topichash) try { const topic = eventName.topics[0]; if (typeof(topic) !== "string") { throw new Error("invalid topic"); // @TODO: May happen for anonymous events } const fragment = this.interface.getEvent(topic); return this._normalizeRunningEvent(new FragmentRunningEvent(this.address, this.interface, fragment, eventName.topics)); } catch (error) { } // 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 WildcardRunningEvent(this.address, this.interface)); } _checkRunningEvents(runningEvent: RunningEvent): void { if (runningEvent.listenerCount() === 0) { delete this._runningEvents[runningEvent.tag]; // If we have a poller for this, remove it const emit = this._wrappedEmits[runningEvent.tag]; if (emit && runningEvent.filter) { this.provider.off(runningEvent.filter, emit); delete this._wrappedEmits[runningEvent.tag]; } } } // Subclasses can override this to gracefully recover // from parse errors if they wish _wrapEvent(runningEvent: RunningEvent, log: Log, listener: Listener): Event { const event = deepCopy(log); event.removeListener = () => { if (!listener) { return; } runningEvent.removeListener(listener); this._checkRunningEvents(runningEvent); }; event.getBlock = () => { return this.provider.getBlock(log.blockHash); } event.getTransaction = () => { return this.provider.getTransaction(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; } private _addEventListener(runningEvent: RunningEvent, listener: Listener, once: boolean): void { if (!this.provider) { logger.throwError("events require a provider or a signer with a provider", Logger.errors.UNSUPPORTED_OPERATION, { operation: "once" }) } runningEvent.addListener(listener, once); // Track this running event and its listeners (may already be there; but no hard in updating) this._runningEvents[runningEvent.tag] = runningEvent; // If we are not polling the provider, start polling if (!this._wrappedEmits[runningEvent.tag]) { const wrappedEmit = (log: Log) => { 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); } }; this._wrappedEmits[runningEvent.tag] = wrappedEmit; // Special events, like "error" do not have a filter if (runningEvent.filter != null) { this.provider.on(runningEvent.filter, wrappedEmit); } } } queryFilter(event: EventFilter, fromBlockOrBlockhash?: BlockTag | string, toBlock?: BlockTag): Promise> { const runningEvent = this._getRunningEvent(event); const filter = shallowCopy(runningEvent.filter); if (typeof(fromBlockOrBlockhash) === "string" && isHexString(fromBlockOrBlockhash, 32)) { if (toBlock != null) { logger.throwArgumentError("cannot specify toBlock with blockhash", "toBlock", toBlock); } (filter).blockHash = fromBlockOrBlockhash; } else { (filter).fromBlock = ((fromBlockOrBlockhash != null) ? fromBlockOrBlockhash: 0); (filter).toBlock = ((toBlock != null) ? toBlock: "latest"); } return this.provider.getLogs(filter).then((logs) => { return logs.map((log) => this._wrapEvent(runningEvent, log, null)); }); } on(event: EventFilter | string, listener: Listener): this { this._addEventListener(this._getRunningEvent(event), listener, false); return this; } once(event: EventFilter | string, listener: Listener): this { this._addEventListener(this._getRunningEvent(event), listener, true); return this; } emit(eventName: EventFilter | string, ...args: Array): boolean { if (!this.provider) { return false; } const runningEvent = this._getRunningEvent(eventName); const result = (runningEvent.run(args) > 0); // May have drained all the "once" events; check for living events this._checkRunningEvents(runningEvent); return result; } listenerCount(eventName?: EventFilter | string): number { if (!this.provider) { return 0; } if (eventName == null) { return Object.keys(this._runningEvents).reduce((accum, key) => { return accum + this._runningEvents[key].listenerCount(); }, 0); } return this._getRunningEvent(eventName).listenerCount(); } listeners(eventName?: EventFilter | string): Array { if (!this.provider) { return []; } if (eventName == null) { const result: Array = [ ]; for (let tag in this._runningEvents) { this._runningEvents[tag].listeners().forEach((listener) => { result.push(listener) }); } return result; } return this._getRunningEvent(eventName).listeners(); } removeAllListeners(eventName?: EventFilter | string): this { if (!this.provider) { return this; } if (eventName == null) { for (const tag in this._runningEvents) { const runningEvent = this._runningEvents[tag]; runningEvent.removeAllListeners(); this._checkRunningEvents(runningEvent); } return this; } // Delete any listeners const runningEvent = this._getRunningEvent(eventName); runningEvent.removeAllListeners(); this._checkRunningEvents(runningEvent); return this; } off(eventName: EventFilter | string, listener: Listener): this { if (!this.provider) { return this; } const runningEvent = this._getRunningEvent(eventName); runningEvent.removeListener(listener); this._checkRunningEvents(runningEvent); return this; } removeListener(eventName: EventFilter | string, listener: Listener): this { return this.off(eventName, listener); } } export class Contract extends BaseContract { // The meta-class properties readonly [ key: string ]: ContractFunction | any; } export class ContractFactory { readonly interface: Interface; readonly bytecode: string; readonly signer: Signer; constructor(contractInterface: ContractInterface, bytecode: BytesLike | { object: string }, signer?: Signer) { let bytecodeHex: string = null; if (typeof(bytecode) === "string") { bytecodeHex = bytecode; } else if (isBytes(bytecode)) { bytecodeHex = hexlify(bytecode); } else if (bytecode && typeof(bytecode.object) === "string") { // Allow the bytecode object from the Solidity compiler bytecodeHex = (bytecode).object; } else { // Crash in the next verification step bytecodeHex = "!"; } // Make sure it is 0x prefixed if (bytecodeHex.substring(0, 2) !== "0x") { bytecodeHex = "0x" + bytecodeHex; } // Make sure the final result is valid bytecode if (!isHexString(bytecodeHex) || (bytecodeHex.length % 2)) { logger.throwArgumentError("invalid bytecode", "bytecode", bytecode); } // If we have a signer, make sure it is valid if (signer && !Signer.isSigner(signer)) { logger.throwArgumentError("invalid signer", "signer", signer); } defineReadOnly(this, "bytecode", bytecodeHex); defineReadOnly(this, "interface", getStatic(new.target, "getInterface")(contractInterface)); defineReadOnly(this, "signer", signer || null); } // @TODO: Future; rename to populateTransaction? getDeployTransaction(...args: Array): TransactionRequest { let tx: TransactionRequest = { }; // If we have 1 additional argument, we allow transaction overrides if (args.length === this.interface.deploy.inputs.length + 1 && typeof(args[args.length - 1]) === "object") { tx = shallowCopy(args.pop()); for (const key in tx) { if (!allowedTransactionKeys[key]) { throw new Error("unknown transaction override " + key); } } } // Do not allow these to be overridden in a deployment transaction ["data", "from", "to"].forEach((key) => { if ((tx)[key] == null) { return; } logger.throwError("cannot override " + key, Logger.errors.UNSUPPORTED_OPERATION, { operation: key }) }); if (tx.value) { const value = BigNumber.from(tx.value); if (!value.isZero() && !this.interface.deploy.payable) { logger.throwError("non-payable constructor cannot override value", Logger.errors.UNSUPPORTED_OPERATION, { operation: "overrides.value", value: tx.value }); } } // Make sure the call matches the constructor signature logger.checkArgumentCount(args.length, this.interface.deploy.inputs.length, " in Contract constructor"); // Set the data to the bytecode + the encoded constructor arguments tx.data = hexlify(concat([ this.bytecode, this.interface.encodeDeploy(args) ])); return tx } async deploy(...args: Array): Promise { let overrides: any = { }; // If 1 extra parameter was passed in, it contains overrides if (args.length === this.interface.deploy.inputs.length + 1) { overrides = args.pop(); } // Make sure the call matches the constructor signature logger.checkArgumentCount(args.length, this.interface.deploy.inputs.length, " in Contract constructor"); // Resolve ENS names and promises in the arguments const params = await resolveAddresses(this.signer, args, this.interface.deploy.inputs); params.push(overrides); // Get the deployment transaction (with optional overrides) const unsignedTx = this.getDeployTransaction(...params); // Send the deployment transaction const tx = await this.signer.sendTransaction(unsignedTx); const address = getStatic<(tx: TransactionResponse) => string>(this.constructor, "getContractAddress")(tx); const contract = getStatic<(address: string, contractInterface: ContractInterface, signer?: Signer) => Contract>(this.constructor, "getContract")(address, this.interface, this.signer); defineReadOnly(contract, "deployTransaction", tx); return contract; } attach(address: string): Contract { return ((this.constructor)).getContract(address, this.interface, this.signer); } connect(signer: Signer) { return new (<{ new(...args: any[]): ContractFactory }>(this.constructor))(this.interface, this.bytecode, signer); } static fromSolidity(compilerOutput: any, signer?: Signer): ContractFactory { if (compilerOutput == null) { logger.throwError("missing compiler output", Logger.errors.MISSING_ARGUMENT, { argument: "compilerOutput" }); } if (typeof(compilerOutput) === "string") { compilerOutput = JSON.parse(compilerOutput); } const abi = compilerOutput.abi; let bytecode: any = null; if (compilerOutput.bytecode) { bytecode = compilerOutput.bytecode; } else if (compilerOutput.evm && compilerOutput.evm.bytecode) { bytecode = compilerOutput.evm.bytecode; } return new this(abi, bytecode, signer); } static getInterface(contractInterface: ContractInterface) { return Contract.getInterface(contractInterface); } static getContractAddress(tx: { from: string, nonce: BytesLike | BigNumber | number }): string { return getContractAddress(tx); } static getContract(address: string, contractInterface: ContractInterface, signer?: Signer): Contract { return new Contract(address, contractInterface, signer); } }