@nomicfoundation/hardhat-ethers
Version:
Hardhat plugin for ethers
1,370 lines (1,154 loc) • 37.7 kB
text/typescript
import type { HardhatEthersProvider as HardhatEthersProviderI } from "../../types.js";
import type {
AddressLike,
BlockTag,
TransactionRequest,
Filter,
FilterByBlockHash,
Listener,
ProviderEvent,
PerformActionTransaction,
TransactionResponseParams,
BlockParams,
TransactionReceiptParams,
LogParams,
PerformActionFilter,
EventFilter,
ethers,
} from "ethers";
import type { NetworkConfig } from "hardhat/types/config";
import type { EthereumProvider } from "hardhat/types/providers";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
import { deepEqual } from "@nomicfoundation/hardhat-utils/lang";
import debug from "debug";
import {
Block,
FeeData,
Log,
Network as EthersNetwork,
Transaction,
TransactionReceipt,
TransactionResponse,
getBigInt,
isHexString,
resolveAddress,
toQuantity,
} from "ethers";
import { assertCanConvertToBigInt } from "../assertion.js";
import {
copyRequest,
formatBlock,
formatLog,
formatTransactionReceipt,
formatTransactionResponse,
getRpcTransaction,
} from "../ethers-utils/ethers-utils.js";
import { HardhatEthersSigner } from "../signers/signers.js";
const log = debug("hardhat:hardhat-ethers:provider");
interface ListenerItem {
listener: Listener;
once: boolean;
}
interface EventListenerItem {
event: EventFilter;
// map from the given listener to the block listener registered for that listener
listenersMap: Map<Listener, Listener>;
}
// this type has a more explicit and type-safe list
// of the events that we support
type HardhatEthersProviderEvent =
| {
kind: "block";
}
| {
kind: "transactionHash";
txHash: string;
}
| {
kind: "event";
eventFilter: EventFilter;
};
export class HardhatEthersProvider implements HardhatEthersProviderI {
#isHardhatNetworkCached: boolean | undefined;
readonly #hardhatProvider: EthereumProvider;
readonly #networkName: string;
readonly #networkConfig: Readonly<NetworkConfig>;
// event-emitter related fields
#latestBlockNumberPolled: number | undefined;
#blockListeners: ListenerItem[] = [];
#transactionHashListeners: Map<string, ListenerItem[]> = new Map();
#eventListeners: EventListenerItem[] = [];
#transactionHashPollingTimeout: NodeJS.Timeout | undefined;
#blockPollingTimeout: NodeJS.Timeout | undefined;
constructor(
hardhatProvider: EthereumProvider,
networkName: string,
networkConfig: NetworkConfig,
) {
this.#hardhatProvider = hardhatProvider;
this.#networkName = networkName;
this.#networkConfig = networkConfig;
}
public get provider(): this {
return this;
}
public destroy(): void {}
public async send(method: string, params?: any[]): Promise<any> {
return this.#hardhatProvider.request({
method,
params,
});
}
public async getSigner(
address?: number | string,
): Promise<HardhatEthersSigner> {
if (address === null || address === undefined) {
address = 0;
}
const accountsPromise = this.send("eth_accounts", []);
// Account index
if (typeof address === "number") {
const accounts: string[] = await accountsPromise;
if (address >= accounts.length) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.ACCOUNT_INDEX_OUT_OF_RANGE,
{
accountIndex: address,
accountsLength: accounts.length,
},
);
}
return HardhatEthersSigner.create(
this,
this.#networkName,
this.#networkConfig,
accounts[address],
);
}
if (typeof address === "string") {
return HardhatEthersSigner.create(
this,
this.#networkName,
this.#networkConfig,
address,
);
}
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.CANNOT_GET_ACCOUNT,
{
address,
},
);
}
public async getBlockNumber(): Promise<number> {
const blockNumber = await this.#hardhatProvider.request({
method: "eth_blockNumber",
});
return Number(blockNumber);
}
public async getNetwork(): Promise<EthersNetwork> {
const chainId = await this.#hardhatProvider.request({
method: "eth_chainId",
});
return new EthersNetwork(this.#networkName, Number(chainId));
}
public async getFeeData(): Promise<ethers.FeeData> {
let gasPrice: bigint | undefined;
let maxFeePerGas: bigint | undefined;
let maxPriorityFeePerGas: bigint | undefined;
try {
const value = await this.#hardhatProvider.request({
method: "eth_gasPrice",
});
assertCanConvertToBigInt(value, "value");
gasPrice = BigInt(value);
} catch {}
const latestBlock = await this.getBlock("latest");
const baseFeePerGas = latestBlock?.baseFeePerGas;
if (baseFeePerGas !== undefined && baseFeePerGas !== null) {
try {
const value = await this.#hardhatProvider.request({
method: "eth_maxPriorityFeePerGas",
});
assertCanConvertToBigInt(value, "value");
maxPriorityFeePerGas = BigInt(value);
} catch {
// the max priority fee RPC call is not supported by
// this chain
}
maxPriorityFeePerGas = maxPriorityFeePerGas ?? 1_000_000_000n;
maxFeePerGas = 2n * baseFeePerGas + maxPriorityFeePerGas;
}
return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
}
public async getBalance(
address: AddressLike,
blockTag?: BlockTag | undefined,
): Promise<bigint> {
const resolvedAddress = await this.#getAddress(address);
const resolvedBlockTag = await this.#getBlockTag(blockTag);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const balance = await this.#hardhatProvider.request({
method: "eth_getBalance",
params: [resolvedAddress, rpcBlockTag],
});
assertCanConvertToBigInt(balance, "balance");
return BigInt(balance);
}
public async getTransactionCount(
address: AddressLike,
blockTag?: BlockTag | undefined,
): Promise<number> {
const resolvedAddress = await this.#getAddress(address);
const resolvedBlockTag = await this.#getBlockTag(blockTag);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const transactionCount = await this.#hardhatProvider.request({
method: "eth_getTransactionCount",
params: [resolvedAddress, rpcBlockTag],
});
return Number(transactionCount);
}
public async getCode(
address: AddressLike,
blockTag?: BlockTag | undefined,
): Promise<string> {
const resolvedAddress = await this.#getAddress(address);
const resolvedBlockTag = await this.#getBlockTag(blockTag);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const code = await this.#hardhatProvider.request({
method: "eth_getCode",
params: [resolvedAddress, rpcBlockTag],
});
assertHardhatInvariant(typeof code === "string", "code should be a string");
return code;
}
public async getStorage(
address: AddressLike,
position: ethers.BigNumberish,
blockTag?: BlockTag | undefined,
): Promise<string> {
const resolvedAddress = await this.#getAddress(address);
const resolvedPosition = getBigInt(position, "position");
const resolvedBlockTag = await this.#getBlockTag(blockTag);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const storage = await this.#hardhatProvider.request({
method: "eth_getStorageAt",
params: [
resolvedAddress,
numberToHexString(resolvedPosition),
rpcBlockTag,
],
});
assertHardhatInvariant(
typeof storage === "string",
"storage should be a string",
);
return storage;
}
public async estimateGas(tx: TransactionRequest): Promise<bigint> {
const blockTag =
tx.blockTag === undefined ? "pending" : this.#getBlockTag(tx.blockTag);
const [resolvedTx, resolvedBlockTag] = await Promise.all([
this.#getTransactionRequest(tx),
blockTag,
]);
const rpcTransaction = getRpcTransaction(resolvedTx);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const gasEstimation = await this.#hardhatProvider.request({
method: "eth_estimateGas",
params: [rpcTransaction, rpcBlockTag],
});
assertCanConvertToBigInt(gasEstimation, "gasEstimation");
return BigInt(gasEstimation);
}
public async call(tx: TransactionRequest): Promise<string> {
const [resolvedTx, resolvedBlockTag] = await Promise.all([
this.#getTransactionRequest(tx),
this.#getBlockTag(tx.blockTag),
]);
const rpcTransaction = getRpcTransaction(resolvedTx);
const rpcBlockTag = this.#getRpcBlockTag(resolvedBlockTag);
const call = await this.#hardhatProvider.request({
method: "eth_call",
params: [rpcTransaction, rpcBlockTag],
});
assertHardhatInvariant(typeof call === "string", "call should be a string");
return call;
}
public async broadcastTransaction(
signedTx: string,
): Promise<ethers.TransactionResponse> {
const hashPromise = this.#hardhatProvider.request({
method: "eth_sendRawTransaction",
params: [signedTx],
});
const [hash, blockNumber] = await Promise.all([
hashPromise,
this.getBlockNumber(),
]);
assertHardhatInvariant(typeof hash === "string", "hash should be a string");
const tx = Transaction.from(signedTx);
assertHardhatInvariant(
tx.hash !== null,
"Hash of signed tx shouldn't be null",
);
if (tx.hash !== hash) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.BROADCASTED_TX_DIFFERENT_HASH,
{
txHash: tx.hash,
broadcastedTxHash: hash,
},
);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- 'tx' overlaps with the type exported by ethers
return this.#wrapTransactionResponse(tx as any).replaceableTransaction(
blockNumber,
);
}
public async getBlock(
blockHashOrBlockTag: BlockTag,
prefetchTxs?: boolean | undefined,
): Promise<ethers.Block | null> {
const block = await this.#getBlock(
blockHashOrBlockTag,
prefetchTxs ?? false,
);
if (block === null || block === undefined) {
return null;
}
return this.#wrapBlock(block);
}
public async getTransaction(
hash: string,
): Promise<ethers.TransactionResponse | null> {
const transaction = await this.#hardhatProvider.request({
method: "eth_getTransactionByHash",
params: [hash],
});
if (transaction === null || transaction === undefined) {
return null;
}
return this.#wrapTransactionResponse(
formatTransactionResponse(transaction),
);
}
public async getTransactionReceipt(
hash: string,
): Promise<ethers.TransactionReceipt | null> {
const receipt = await this.#hardhatProvider.request({
method: "eth_getTransactionReceipt",
params: [hash],
});
if (receipt === null || receipt === undefined) {
return null;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- 'receipt' overlaps with the type exported by ethers
return this.#wrapTransactionReceipt(receipt as TransactionReceiptParams);
}
public async getTransactionResult(_hash: string): Promise<string | null> {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "HardhatEthersProvider.getTransactionResult",
},
);
}
public async getLogs(
filter: Filter | FilterByBlockHash,
): Promise<ethers.Log[]> {
const resolvedFilter = await this.#getFilter(filter);
const logs = await this.#hardhatProvider.request({
method: "eth_getLogs",
params: [resolvedFilter],
});
assertHardhatInvariant(Array.isArray(logs), "logs should be an array");
return logs.map((l: any) => this.#wrapLog(formatLog(l)));
}
public async resolveName(_ensName: string): Promise<string | null> {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "HardhatEthersProvider.resolveName",
},
);
}
public async lookupAddress(_address: string): Promise<string | null> {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "HardhatEthersProvider.lookupAddress",
},
);
}
public async waitForTransaction(
_hash: string,
_confirms?: number | undefined,
_timeout?: number | undefined,
): Promise<ethers.TransactionReceipt | null> {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "HardhatEthersProvider.waitForTransaction",
},
);
}
public async waitForBlock(
_blockTag?: BlockTag | undefined,
): Promise<ethers.Block> {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "HardhatEthersProvider.waitForBlock",
},
);
}
// -------------------------------------- //
// event-emitter related public functions //
// -------------------------------------- //
public async on(
ethersEvent: ProviderEvent,
listener: Listener,
): Promise<this> {
const event = ethersToHardhatEvent(ethersEvent);
if (event.kind === "block") {
await this.#onBlock(listener, { once: false });
} else if (event.kind === "transactionHash") {
await this.#onTransactionHash(event.txHash, listener, { once: false });
} else if (event.kind === "event") {
const { eventFilter } = event;
const blockListener = this.#getBlockListenerForEvent(
eventFilter,
listener,
);
await this.#addEventListener(eventFilter, listener, blockListener);
await this.on("block", blockListener);
} else {
const _exhaustiveCheck: never = event;
}
return this;
}
public async once(
ethersEvent: ProviderEvent,
listener: Listener,
): Promise<this> {
const event = ethersToHardhatEvent(ethersEvent);
if (event.kind === "block") {
await this.#onBlock(listener, { once: true });
} else if (event.kind === "transactionHash") {
await this.#onTransactionHash(event.txHash, listener, { once: true });
} else if (event.kind === "event") {
const { eventFilter } = event;
const blockListener = this.#getBlockListenerForEvent(
eventFilter,
listener,
);
await this.#addEventListener(eventFilter, listener, blockListener);
await this.once("block", blockListener);
} else {
const _exhaustiveCheck: never = event;
}
return this;
}
public async emit(
ethersEvent: ProviderEvent,
...args: any[]
): Promise<boolean> {
const event = ethersToHardhatEvent(ethersEvent);
if (event.kind === "block") {
return this.#emitBlock(...args);
} else if (event.kind === "transactionHash") {
return this.#emitTransactionHash(event.txHash, ...args);
} else if (event.kind === "event") {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "emit(event)",
},
);
} else {
const _exhaustiveCheck: never = event;
return _exhaustiveCheck;
}
}
public async listenerCount(
event?: ProviderEvent | undefined,
): Promise<number> {
const listeners = await this.listeners(event);
return listeners.length;
}
public async listeners(
ethersEvent?: ProviderEvent | undefined,
): Promise<Listener[]> {
if (ethersEvent === undefined) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.METHOD_NOT_IMPLEMENTED,
{
method: "listeners()",
},
);
}
const event = ethersToHardhatEvent(ethersEvent);
if (event.kind === "block") {
return this.#blockListeners.map(({ listener }) => listener);
} else if (event.kind === "transactionHash") {
return (
this.#transactionHashListeners
.get(event.txHash)
?.map(({ listener }) => listener) ?? []
);
} else if (event.kind === "event") {
const eventListener = await this.#findEventListener(event.eventFilter);
if (eventListener === undefined) {
return [];
}
return [...eventListener.listenersMap.keys()];
} else {
const _exhaustiveCheck: never = event;
return _exhaustiveCheck;
}
}
public async off(
ethersEvent: ProviderEvent,
listener?: Listener | undefined,
): Promise<this> {
const event = ethersToHardhatEvent(ethersEvent);
if (event.kind === "block") {
this.#clearBlockListeners(listener);
} else if (event.kind === "transactionHash") {
this.#clearTransactionHashListeners(event.txHash, listener);
} else if (event.kind === "event") {
const { eventFilter } = event;
if (listener === undefined) {
await this.#clearEventListeners(eventFilter);
} else {
await this.#removeEventListener(eventFilter, listener);
}
} else {
const _exhaustiveCheck: never = event;
}
return this;
}
public async removeAllListeners(
ethersEvent?: ProviderEvent | undefined,
): Promise<this> {
const event =
ethersEvent !== undefined ? ethersToHardhatEvent(ethersEvent) : undefined;
if (event === undefined || event.kind === "block") {
this.#clearBlockListeners();
}
if (event === undefined || event.kind === "transactionHash") {
this.#clearTransactionHashListeners(event?.txHash);
}
if (event === undefined || event.kind === "event") {
await this.#clearEventListeners(event?.eventFilter);
}
if (
event !== undefined &&
event.kind !== "block" &&
event.kind !== "transactionHash" &&
event.kind !== "event"
) {
// this check is only to remember to add a proper if block
// in this method's implementation if we add support for a
// new kind of event
const _exhaustiveCheck: never = event;
}
return this;
}
public async addListener(
event: ProviderEvent,
listener: Listener,
): Promise<this> {
return this.on(event, listener);
}
public async removeListener(
event: ProviderEvent,
listener: Listener,
): Promise<this> {
return this.off(event, listener);
}
public toJSON() {
return "<EthersHardhatProvider>";
}
async #findEventListener(event: EventFilter) {
for (const item of this.#eventListeners) {
if ((await deepEqual(item.event, event)) === true) {
return item;
}
}
return undefined;
}
async #findEventListenerIndex(event: EventFilter): Promise<number> {
for (let i = 0; i < this.#eventListeners.length; i++) {
if ((await deepEqual(this.#eventListeners[i].event, event)) === true) {
return i;
}
}
return -1;
}
#getAddress(address: AddressLike): string | Promise<string> {
return resolveAddress(address, this);
}
#getBlockTag(blockTag?: BlockTag): string | Promise<string> {
if (blockTag === null || blockTag === undefined) {
return "latest";
}
switch (blockTag) {
case "earliest":
return "0x0";
case "latest":
case "pending":
case "safe":
case "finalized":
return blockTag;
default:
}
if (isHexString(blockTag)) {
if (isHexString(blockTag, 32)) {
return blockTag;
}
return toQuantity(blockTag);
}
if (typeof blockTag === "number") {
if (blockTag >= 0) {
return toQuantity(blockTag);
}
return this.getBlockNumber().then((b) => toQuantity(b + blockTag));
}
if (typeof blockTag === "bigint") {
if (blockTag >= 0n) {
return toQuantity(blockTag);
}
return this.getBlockNumber().then((b) =>
toQuantity(b + Number(blockTag)),
);
}
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.INVALID_BLOCK_TAG,
{
blockTag,
},
);
}
#getTransactionRequest(
_request: TransactionRequest,
): PerformActionTransaction | Promise<PerformActionTransaction> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- ethers.PreparedTransactionRequest overlaps with PerformActionTransaction
const request = copyRequest(_request) as PerformActionTransaction;
const requestKeys: Array<keyof PerformActionTransaction> = ["to", "from"];
const promises: Array<Promise<void>> = [];
requestKeys.forEach((key) => {
if (request[key] === null || request[key] === undefined) {
return;
}
const addr = resolveAddress(request[key]);
if (isPromise(addr)) {
promises.push(
(async function () {
request[key] = await addr;
})(),
);
} else {
request[key] = addr;
}
});
if (request.blockTag !== null && request.blockTag !== undefined) {
const blockTag = this.#getBlockTag(request.blockTag);
if (isPromise(blockTag)) {
promises.push(
(async function () {
request.blockTag = await blockTag;
})(),
);
} else {
request.blockTag = blockTag;
}
}
if (promises.length > 0) {
return (async function () {
await Promise.all(promises);
return request;
})();
}
return request;
}
#wrapTransactionResponse(tx: TransactionResponseParams): TransactionResponse {
return new TransactionResponse(tx, this);
}
async #getBlock(
block: BlockTag | string,
includeTransactions: boolean,
): Promise<any> {
if (isHexString(block, 32)) {
return this.#hardhatProvider.request({
method: "eth_getBlockByHash",
params: [block, includeTransactions],
});
}
let blockTag = this.#getBlockTag(block);
if (typeof blockTag !== "string") {
blockTag = await blockTag;
}
return this.#hardhatProvider.request({
method: "eth_getBlockByNumber",
params: [blockTag, includeTransactions],
});
}
#wrapBlock(value: BlockParams): Block {
return new Block(formatBlock(value), this);
}
#wrapTransactionReceipt(value: TransactionReceiptParams): TransactionReceipt {
return new TransactionReceipt(formatTransactionReceipt(value), this);
}
#getFilter(
filter: Filter | FilterByBlockHash,
): PerformActionFilter | Promise<PerformActionFilter> {
// Create a canonical representation of the topics
const topics = (filter.topics ?? []).map((topic) => {
if (topic === null || topic === undefined) {
return null;
}
if (Array.isArray(topic)) {
return concisify(topic.map((t) => t.toLowerCase()));
}
return topic.toLowerCase();
});
const blockHash = "blockHash" in filter ? filter.blockHash : undefined;
const resolve = (
_address: string[],
fromBlock?: string,
toBlock?: string,
) => {
let resolvedAddress: undefined | string | string[];
switch (_address.length) {
case 0:
break;
case 1:
resolvedAddress = _address[0];
break;
default:
_address.sort();
resolvedAddress = _address;
}
if (blockHash !== undefined) {
assertHardhatInvariant(
(fromBlock === null || fromBlock === undefined) &&
(toBlock === null || toBlock === undefined),
"invalid filter",
);
}
const resolvedFilter: any = {};
if (resolvedAddress !== undefined) {
resolvedFilter.address = resolvedAddress;
}
if (topics.length > 0) {
resolvedFilter.topics = topics;
}
if (fromBlock !== undefined) {
resolvedFilter.fromBlock = fromBlock;
}
if (toBlock !== undefined) {
resolvedFilter.toBlock = toBlock;
}
if (blockHash !== undefined) {
resolvedFilter.blockHash = blockHash;
}
return resolvedFilter;
};
// Addresses could be async (ENS names or Addressables)
const address: Array<string | Promise<string>> = [];
if (filter.address !== undefined) {
if (Array.isArray(filter.address)) {
for (const addr of filter.address) {
address.push(this.#getAddress(addr));
}
} else {
address.push(this.#getAddress(filter.address));
}
}
let resolvedFromBlock: undefined | string | Promise<string>;
if ("fromBlock" in filter) {
resolvedFromBlock = this.#getBlockTag(filter.fromBlock);
}
let resolvedToBlock: undefined | string | Promise<string>;
if ("toBlock" in filter) {
resolvedToBlock = this.#getBlockTag(filter.toBlock);
}
if (
address.filter((a) => typeof a !== "string").length > 0 ||
(resolvedFromBlock !== null &&
resolvedFromBlock !== undefined &&
typeof resolvedFromBlock !== "string") ||
(resolvedToBlock !== null &&
resolvedToBlock !== undefined &&
typeof resolvedToBlock !== "string")
) {
return Promise.all([
Promise.all(address),
resolvedFromBlock,
resolvedToBlock,
]).then((result) => {
return resolve(result[0], result[1], result[2]);
});
}
assertHardhatInvariant(
address.every((a) => typeof a === "string"),
"Every address should be a string",
);
return resolve(address, resolvedFromBlock, resolvedToBlock);
}
#wrapLog(value: LogParams): Log {
return new Log(formatLog(value), this);
}
#getRpcBlockTag(blockTag: string): string | { blockHash: string } {
if (isHexString(blockTag, 32)) {
return { blockHash: blockTag };
}
return blockTag;
}
async #isHardhatNetwork(): Promise<boolean> {
if (this.#isHardhatNetworkCached === undefined) {
this.#isHardhatNetworkCached = false;
try {
await this.#hardhatProvider.request({ method: "hardhat_metadata" });
this.#isHardhatNetworkCached = true;
} catch {}
}
return this.#isHardhatNetworkCached;
}
// ------------------------------------- //
// event-emitter related private helpers //
// ------------------------------------- //
async #onTransactionHash(
transactionHash: string,
listener: Listener,
{ once }: { once: boolean },
): Promise<void> {
const listeners = this.#transactionHashListeners.get(transactionHash) ?? [];
listeners.push({ listener, once });
this.#transactionHashListeners.set(transactionHash, listeners);
await this.#startTransactionHashPolling();
}
#clearTransactionHashListeners(
transactionHash?: string,
listener?: Listener,
): void {
if (transactionHash === undefined) {
this.#transactionHashListeners = new Map();
} else if (listener === undefined) {
this.#transactionHashListeners.delete(transactionHash);
} else {
const listeners = this.#transactionHashListeners.get(transactionHash);
if (listeners !== undefined) {
const listenerIndex = listeners.findIndex(
(item) => item.listener === listener,
);
if (listenerIndex >= 0) {
listeners.splice(listenerIndex, 1);
}
if (listeners.length === 0) {
this.#transactionHashListeners.delete(transactionHash);
}
}
}
if (this.#transactionHashListeners.size === 0) {
this.#stopTransactionHashPolling();
}
}
async #startTransactionHashPolling() {
await this.#pollTransactionHashes();
}
#stopTransactionHashPolling() {
clearTimeout(this.#transactionHashPollingTimeout);
this.#transactionHashPollingTimeout = undefined;
}
/**
* Traverse all the registered transaction hashes and check if they were mined.
*
* This function should NOT throw.
*/
async #pollTransactionHashes() {
try {
const listenersToRemove: Array<[string, Listener]> = [];
for (const [
transactionHash,
listeners,
] of this.#transactionHashListeners.entries()) {
const receipt = await this.getTransactionReceipt(transactionHash);
if (receipt !== null) {
for (const { listener, once } of listeners) {
listener(receipt);
if (once) {
listenersToRemove.push([transactionHash, listener]);
}
}
}
}
for (const [transactionHash, listener] of listenersToRemove) {
this.#clearTransactionHashListeners(transactionHash, listener);
}
} catch (e) {
ensureError(e);
log(`Error during transaction hash polling: ${e.message}`);
} finally {
// it's possible that the first poll cleans all the listeners,
// in that case we don't set the timeout
if (this.#transactionHashListeners.size > 0) {
const _isHardhatNetwork = await this.#isHardhatNetwork();
const timeout = _isHardhatNetwork ? 50 : 500;
clearTimeout(this.#transactionHashPollingTimeout);
this.#transactionHashPollingTimeout = setTimeout(async () => {
await this.#pollTransactionHashes();
}, timeout);
}
}
}
async #startBlockPolling() {
this.#latestBlockNumberPolled = await this.getBlockNumber();
await this.#pollBlocks();
}
#stopBlockPolling() {
clearInterval(this.#blockPollingTimeout);
this.#blockPollingTimeout = undefined;
}
async #pollBlocks() {
try {
const currentBlockNumber = await this.getBlockNumber();
const previousBlockNumber = this.#latestBlockNumberPolled ?? 0;
if (currentBlockNumber === previousBlockNumber) {
// Don't do anything, there are no new blocks
return;
} else if (currentBlockNumber < previousBlockNumber) {
// This can happen if there was a reset or a snapshot was reverted.
// We don't know which number the network was reset to, so we update
// the latest block number seen and do nothing else.
this.#latestBlockNumberPolled = currentBlockNumber;
return;
}
this.#latestBlockNumberPolled = currentBlockNumber;
for (
let blockNumber = previousBlockNumber + 1;
blockNumber <= this.#latestBlockNumberPolled;
blockNumber++
) {
const listenersToRemove: Listener[] = [];
for (const { listener, once } of this.#blockListeners) {
listener(blockNumber);
if (once) {
listenersToRemove.push(listener);
}
}
for (const listener of listenersToRemove) {
this.#clearBlockListeners(listener);
}
}
} catch (e) {
ensureError(e);
log(`Error during block polling: ${e.message}`);
} finally {
// it's possible that the first poll cleans all the listeners,
// in that case we don't set the timeout
if (this.#blockListeners.length > 0) {
const _isHardhatNetwork = await this.#isHardhatNetwork();
const timeout = _isHardhatNetwork ? 50 : 500;
clearTimeout(this.#blockPollingTimeout);
this.#blockPollingTimeout = setTimeout(async () => {
await this.#pollBlocks();
}, timeout);
}
}
}
#emitTransactionHash(transactionHash: string, ...args: any[]): boolean {
const listeners = this.#transactionHashListeners.get(transactionHash);
const listenersToRemove: Listener[] = [];
if (listeners === undefined) {
return false;
}
for (const { listener, once } of listeners) {
listener(...args);
if (once) {
listenersToRemove.push(listener);
}
}
for (const listener of listenersToRemove) {
this.#clearTransactionHashListeners(transactionHash, listener);
}
return true;
}
#emitBlock(...args: any[]): boolean {
const listeners = this.#blockListeners;
const listenersToRemove: Listener[] = [];
for (const { listener, once } of listeners) {
listener(...args);
if (once) {
listenersToRemove.push(listener);
}
}
for (const listener of listenersToRemove) {
this.#clearBlockListeners(listener);
}
return true;
}
async #onBlock(
listener: Listener,
{ once }: { once: boolean },
): Promise<void> {
const listeners = this.#blockListeners;
listeners.push({ listener, once });
this.#blockListeners = listeners;
await this.#startBlockPolling();
}
#clearBlockListeners(listener?: Listener): void {
if (listener === undefined) {
this.#blockListeners = [];
this.#stopBlockPolling();
} else {
const listenerIndex = this.#blockListeners.findIndex(
(item) => item.listener === listener,
);
if (listenerIndex >= 0) {
this.#blockListeners.splice(listenerIndex, 1);
}
if (this.#blockListeners.length === 0) {
this.#stopBlockPolling();
}
}
}
#getBlockListenerForEvent(event: EventFilter, listener: Listener) {
return async (blockNumber: number) => {
const eventLogs = await this.getLogs({
fromBlock: blockNumber,
toBlock: blockNumber,
});
const matchingLogs = eventLogs.filter((e) => {
if (event.address !== undefined && e.address !== event.address) {
return false;
}
if (event.topics !== undefined) {
const topicsToMatch = event.topics;
// the array of topics to match can be smaller than the actual
// array of topics; in that case only those first topics are
// checked
const topics = e.topics.slice(0, topicsToMatch.length);
const topicsMatch = topics.every((topic, i) => {
const topicToMatch = topicsToMatch[i];
if (topicToMatch === null) {
return true;
}
if (typeof topicToMatch === "string") {
return topic === topicsToMatch[i];
}
return topicToMatch.includes(topic);
});
return topicsMatch;
}
return true;
});
for (const matchingLog of matchingLogs) {
listener(matchingLog);
}
};
}
async #addEventListener(
event: EventFilter,
listener: Listener,
blockListener: Listener,
) {
const eventListener = await this.#findEventListener(event);
if (eventListener === undefined) {
const listenersMap = new Map();
listenersMap.set(listener, blockListener);
this.#eventListeners.push({ event, listenersMap });
} else {
eventListener.listenersMap.set(listener, blockListener);
}
}
async #clearEventListeners(event?: EventFilter) {
const blockListenersToRemove: Listener[] = [];
if (event === undefined) {
for (const eventListener of this.#eventListeners) {
for (const blockListener of eventListener.listenersMap.values()) {
blockListenersToRemove.push(blockListener);
}
}
this.#eventListeners = [];
} else {
const index = await this.#findEventListenerIndex(event);
if (index !== -1) {
const { listenersMap } = this.#eventListeners[index];
this.#eventListeners.splice(index, 1);
for (const blockListener of listenersMap.values()) {
blockListenersToRemove.push(blockListener);
}
}
}
for (const blockListener of blockListenersToRemove) {
await this.off("block", blockListener);
}
}
async #removeEventListener(event: EventFilter, listener: Listener) {
const index = await this.#findEventListenerIndex(event);
if (index === -1) {
// nothing to do
return;
}
const { listenersMap } = this.#eventListeners[index];
const blockListener = listenersMap.get(listener);
listenersMap.delete(listener);
if (blockListener === undefined) {
// nothing to do
return;
}
await this.off("block", blockListener);
}
}
function isPromise<T = any>(value: any): value is Promise<T> {
return Boolean(value) && typeof value.then === "function";
}
function concisify(items: string[]): string[] {
items = Array.from(new Set(items).values());
items.sort();
return items;
}
function isTransactionHash(x: string): boolean {
return x.startsWith("0x") && x.length === 66;
}
function isEventFilter(x: ProviderEvent): x is EventFilter {
if (typeof x !== "string" && !Array.isArray(x) && !("orphan" in x)) {
return true;
}
return false;
}
function ethersToHardhatEvent(
event: ProviderEvent,
): HardhatEthersProviderEvent {
if (typeof event === "string") {
if (event === "block") {
return { kind: "block" };
} else if (isTransactionHash(event)) {
return { kind: "transactionHash", txHash: event };
} else {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.EVENT_NOT_SUPPORTED,
{
event,
},
);
}
} else if (isEventFilter(event)) {
return { kind: "event", eventFilter: event };
} else {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_ETHERS.GENERAL.EVENT_NOT_SUPPORTED,
{
event,
},
);
}
}