/** * The available providers should suffice for most developers purposes, * but the [[AbstractProvider]] class has many features which enable * sub-classing it for specific purposes. * * @_section: api/providers/abstract-provider: Subclassing Provider [abstract-provider] */ // @TODO // Event coalescence // When we register an event with an async value (e.g. address is a Signer // or ENS name), we need to add it immeidately for the Event API, but also // need time to resolve the address. Upon resolving the address, we need to // migrate the listener to the static event. We also need to maintain a map // of Signer/ENS name to address so we can sync respond to listenerCount. import { getAddress, resolveAddress } from "../address/index.js"; import { ZeroAddress } from "../constants/index.js"; import { Contract } from "../contract/index.js"; import { namehash } from "../hash/index.js"; import { Transaction } from "../transaction/index.js"; import { concat, dataLength, dataSlice, hexlify, isHexString, getBigInt, getBytes, getNumber, isCallException, isError, makeError, assert, assertArgument, FetchRequest, toBeArray, toQuantity, defineProperties, EventPayload, resolveProperties, toUtf8String } from "../utils/index.js"; import { EnsResolver } from "./ens-resolver.js"; import { formatBlock, formatLog, formatTransactionReceipt, formatTransactionResponse } from "./format.js"; import { Network } from "./network.js"; import { copyRequest, Block, FeeData, Log, TransactionReceipt, TransactionResponse } from "./provider.js"; import { PollingBlockSubscriber, PollingBlockTagSubscriber, PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber } from "./subscriber-polling.js"; import type { Addressable, AddressLike } from "../address/index.js"; import type { BigNumberish, BytesLike } from "../utils/index.js"; import type { Listener } from "../utils/index.js"; import type { Networkish } from "./network.js"; import type { FetchUrlFeeDataNetworkPlugin } from "./plugins-network.js"; //import type { MaxPriorityFeePlugin } from "./plugins-network.js"; import type { BlockParams, LogParams, TransactionReceiptParams, TransactionResponseParams } from "./formatting.js"; import type { BlockTag, EventFilter, Filter, FilterByBlockHash, OrphanFilter, PreparedTransactionRequest, Provider, ProviderEvent, TransactionRequest } from "./provider.js"; type Timer = ReturnType; // Constants const BN_2 = BigInt(2); const MAX_CCIP_REDIRECTS = 10; function isPromise(value: any): value is Promise { return (value && typeof(value.then) === "function"); } function getTag(prefix: string, value: any): string { return prefix + ":" + JSON.stringify(value, (k, v) => { if (v == null) { return "null"; } if (typeof(v) === "bigint") { return `bigint:${ v.toString() }`} if (typeof(v) === "string") { return v.toLowerCase(); } // Sort object keys if (typeof(v) === "object" && !Array.isArray(v)) { const keys = Object.keys(v); keys.sort(); return keys.reduce((accum, key) => { accum[key] = v[key]; return accum; }, { }); } return v; }); } /** * The types of additional event values that can be emitted for the * ``"debug"`` event. */ export type DebugEventAbstractProvider = { action: "sendCcipReadFetchRequest", request: FetchRequest index: number urls: Array } | { action: "receiveCcipReadFetchResult", request: FetchRequest, result: any } | { action: "receiveCcipReadFetchError", request: FetchRequest, result: any } | { action: "sendCcipReadCall", transaction: { to: string, data: string } } | { action: "receiveCcipReadCallResult", transaction: { to: string, data: string } result: string } | { action: "receiveCcipReadCallError", transaction: { to: string, data: string } error: Error }; /** * The value passed to the [[AbstractProvider-_getSubscriber]] method. * * Only developers sub-classing [[AbstractProvider[[ will care about this, * if they are modifying a low-level feature of how subscriptions operate. */ export type Subscription = { type: "block" | "close" | "debug" | "error" | "finalized" | "network" | "pending" | "safe", tag: string } | { type: "transaction", tag: string, hash: string } | { type: "event", tag: string, filter: EventFilter } | { type: "orphan", tag: string, filter: OrphanFilter }; /** * A **Subscriber** manages a subscription. * * Only developers sub-classing [[AbstractProvider[[ will care about this, * if they are modifying a low-level feature of how subscriptions operate. */ export interface Subscriber { /** * Called initially when a subscriber is added the first time. */ start(): void; /** * Called when there are no more subscribers to the event. */ stop(): void; /** * Called when the subscription should pause. * * If %%dropWhilePaused%%, events that occur while paused should not * be emitted [[resume]]. */ pause(dropWhilePaused?: boolean): void; /** * Resume a paused subscriber. */ resume(): void; /** * The frequency (in ms) to poll for events, if polling is used by * the subscriber. * * For non-polling subscribers, this must return ``undefined``. */ pollingInterval?: number; } /** * An **UnmanagedSubscriber** is useful for events which do not require * any additional management, such as ``"debug"`` which only requires * emit in synchronous event loop triggered calls. */ export class UnmanagedSubscriber implements Subscriber { /** * The name fof the event. */ name!: string; /** * Create a new UnmanagedSubscriber with %%name%%. */ constructor(name: string) { defineProperties(this, { name }); } start(): void { } stop(): void { } pause(dropWhilePaused?: boolean): void { } resume(): void { } } type Sub = { tag: string; nameMap: Map addressableMap: WeakMap; listeners: Array<{ listener: Listener, once: boolean }>; // @TODO: get rid of this, as it is (and has to be) // tracked in subscriber started: boolean; subscriber: Subscriber; }; function copy(value: T): T { return JSON.parse(JSON.stringify(value)); } function concisify(items: Array): Array { items = Array.from((new Set(items)).values()) items.sort(); return items; } async function getSubscription(_event: ProviderEvent, provider: AbstractProvider): Promise { if (_event == null) { throw new Error("invalid event"); } // Normalize topic array info an EventFilter if (Array.isArray(_event)) { _event = { topics: _event }; } if (typeof(_event) === "string") { switch (_event) { case "block": case "debug": case "error": case "finalized": case "network": case "pending": case "safe": { return { type: _event, tag: _event }; } } } if (isHexString(_event, 32)) { const hash = _event.toLowerCase(); return { type: "transaction", tag: getTag("tx", { hash }), hash }; } if ((_event).orphan) { const event = _event; // @TODO: Should lowercase and whatnot things here instead of copy... return { type: "orphan", tag: getTag("orphan", event), filter: copy(event) }; } if (((_event).address || (_event).topics)) { const event = _event; const filter: any = { topics: ((event.topics || []).map((t) => { if (t == null) { return null; } if (Array.isArray(t)) { return concisify(t.map((t) => t.toLowerCase())); } return t.toLowerCase(); })) }; if (event.address) { const addresses: Array = [ ]; const promises: Array> = [ ]; const addAddress = (addr: AddressLike) => { if (isHexString(addr)) { addresses.push(addr); } else { promises.push((async () => { addresses.push(await resolveAddress(addr, provider)); })()); } } if (Array.isArray(event.address)) { event.address.forEach(addAddress); } else { addAddress(event.address); } if (promises.length) { await Promise.all(promises); } filter.address = concisify(addresses.map((a) => a.toLowerCase())); } return { filter, tag: getTag("event", filter), type: "event" }; } assertArgument(false, "unknown ProviderEvent", "event", _event); } function getTime(): number { return (new Date()).getTime(); } /** * An **AbstractPlugin** is used to provide additional internal services * to an [[AbstractProvider]] without adding backwards-incompatible changes * to method signatures or other internal and complex logic. */ export interface AbstractProviderPlugin { /** * The reverse domain notation of the plugin. */ readonly name: string; /** * Creates a new instance of the plugin, connected to %%provider%%. */ connect(provider: AbstractProvider): AbstractProviderPlugin; } /** * A normalized filter used for [[PerformActionRequest]] objects. */ export type PerformActionFilter = { address?: string | Array; topics?: Array>; fromBlock?: BlockTag; toBlock?: BlockTag; } | { address?: string | Array; topics?: Array>; blockHash?: string; }; /** * A normalized transactions used for [[PerformActionRequest]] objects. */ export interface PerformActionTransaction extends PreparedTransactionRequest { /** * The ``to`` address of the transaction. */ to?: string; /** * The sender of the transaction. */ from?: string; } /** * The [[AbstractProvider]] methods will normalize all values and pass this * type to [[AbstractProvider-_perform]]. */ export type PerformActionRequest = { method: "broadcastTransaction", signedTransaction: string } | { method: "call", transaction: PerformActionTransaction, blockTag: BlockTag } | { method: "chainId" } | { method: "estimateGas", transaction: PerformActionTransaction } | { method: "getBalance", address: string, blockTag: BlockTag } | { method: "getBlock", blockTag: BlockTag, includeTransactions: boolean } | { method: "getBlock", blockHash: string, includeTransactions: boolean } | { method: "getBlockNumber" } | { method: "getCode", address: string, blockTag: BlockTag } | { method: "getGasPrice" } | { method: "getLogs", filter: PerformActionFilter } | { method: "getPriorityFee" } | { method: "getStorage", address: string, position: bigint, blockTag: BlockTag } | { method: "getTransaction", hash: string } | { method: "getTransactionCount", address: string, blockTag: BlockTag } | { method: "getTransactionReceipt", hash: string } | { method: "getTransactionResult", hash: string }; type _PerformAccountRequest = { method: "getBalance" | "getTransactionCount" | "getCode" } | { method: "getStorage", position: bigint } /** * Options for configuring some internal aspects of an [[AbstractProvider]]. * * **``cacheTimeout``** - how long to cache a low-level ``_perform`` * for, based on input parameters. This reduces the number of calls * to getChainId and getBlockNumber, but may break test chains which * can perform operations (internally) synchronously. Use ``-1`` to * disable, ``0`` will only buffer within the same event loop and * any other value is in ms. (default: ``250``) */ export type AbstractProviderOptions = { cacheTimeout?: number; pollingInterval?: number; }; const defaultOptions = { cacheTimeout: 250, pollingInterval: 4000 }; type CcipArgs = { sender: string; urls: Array; calldata: string; selector: string; extraData: string; errorArgs: Array }; /** * An **AbstractProvider** provides a base class for other sub-classes to * implement the [[Provider]] API by normalizing input arguments and * formatting output results as well as tracking events for consistent * behaviour on an eventually-consistent network. */ export class AbstractProvider implements Provider { #subs: Map; #plugins: Map; // null=unpaused, true=paused+dropWhilePaused, false=paused #pausedState: null | boolean; #destroyed: boolean; #networkPromise: null | Promise; readonly #anyNetwork: boolean; #performCache: Map>; // The most recent block number if running an event or -1 if no "block" event #lastBlockNumber: number; #nextTimer: number; #timers: Map void, time: number }>; #disableCcipRead: boolean; #options: Required; /** * Create a new **AbstractProvider** connected to %%network%%, or * use the various network detection capabilities to discover the * [[Network]] if necessary. */ constructor(_network?: "any" | Networkish, options?: AbstractProviderOptions) { this.#options = Object.assign({ }, defaultOptions, options || { }); if (_network === "any") { this.#anyNetwork = true; this.#networkPromise = null; } else if (_network) { const network = Network.from(_network); this.#anyNetwork = false; this.#networkPromise = Promise.resolve(network); setTimeout(() => { this.emit("network", network, null); }, 0); } else { this.#anyNetwork = false; this.#networkPromise = null; } this.#lastBlockNumber = -1; this.#performCache = new Map(); this.#subs = new Map(); this.#plugins = new Map(); this.#pausedState = null; this.#destroyed = false; this.#nextTimer = 1; this.#timers = new Map(); this.#disableCcipRead = false; } get pollingInterval(): number { return this.#options.pollingInterval; } /** * Returns ``this``, to allow an **AbstractProvider** to implement * the [[ContractRunner]] interface. */ get provider(): this { return this; } /** * Returns all the registered plug-ins. */ get plugins(): Array { return Array.from(this.#plugins.values()); } /** * Attach a new plug-in. */ attachPlugin(plugin: AbstractProviderPlugin): this { if (this.#plugins.get(plugin.name)) { throw new Error(`cannot replace existing plugin: ${ plugin.name } `); } this.#plugins.set(plugin.name, plugin.connect(this)); return this; } /** * Get a plugin by name. */ getPlugin(name: string): null | T { return (this.#plugins.get(name)) || null; } /** * Prevent any CCIP-read operation, regardless of whether requested * in a [[call]] using ``enableCcipRead``. */ get disableCcipRead(): boolean { return this.#disableCcipRead; } set disableCcipRead(value: boolean) { this.#disableCcipRead = !!value; } // Shares multiple identical requests made during the same 250ms async #perform(req: PerformActionRequest): Promise { const timeout = this.#options.cacheTimeout; // Caching disabled if (timeout < 0) { return await this._perform(req); } // Create a tag const tag = getTag(req.method, req); let perform = this.#performCache.get(tag); if (!perform) { perform = this._perform(req); this.#performCache.set(tag, perform); setTimeout(() => { if (this.#performCache.get(tag) === perform) { this.#performCache.delete(tag); } }, timeout); } return await perform; } /** * Resolves to the data for executing the CCIP-read operations. */ async ccipReadFetch(tx: PerformActionTransaction, calldata: string, urls: Array): Promise { if (this.disableCcipRead || urls.length === 0 || tx.to == null) { return null; } const sender = tx.to.toLowerCase(); const data = calldata.toLowerCase(); const errorMessages: Array = [ ]; for (let i = 0; i < urls.length; i++) { const url = urls[i]; // URL expansion const href = url.replace("{sender}", sender).replace("{data}", data); // If no {data} is present, use POST; otherwise GET //const json: string | null = (url.indexOf("{data}") >= 0) ? null: JSON.stringify({ data, sender }); //const result = await fetchJson({ url: href, errorPassThrough: true }, json, (value, response) => { // value.status = response.statusCode; // return value; //}); const request = new FetchRequest(href); if (url.indexOf("{data}") === -1) { request.body = { data, sender }; } this.emit("debug", { action: "sendCcipReadFetchRequest", request, index: i, urls }); let errorMessage = "unknown error"; const resp = await request.send(); try { const result = resp.bodyJson; if (result.data) { this.emit("debug", { action: "receiveCcipReadFetchResult", request, result }); return result.data; } if (result.message) { errorMessage = result.message; } this.emit("debug", { action: "receiveCcipReadFetchError", request, result }); } catch (error) { } // 4xx indicates the result is not present; stop assert(resp.statusCode < 400 || resp.statusCode >= 500, `response not found during CCIP fetch: ${ errorMessage }`, "OFFCHAIN_FAULT", { reason: "404_MISSING_RESOURCE", transaction: tx, info: { url, errorMessage } }); // 5xx indicates server issue; try the next url errorMessages.push(errorMessage); } assert(false, `error encountered during CCIP fetch: ${ errorMessages.map((m) => JSON.stringify(m)).join(", ") }`, "OFFCHAIN_FAULT", { reason: "500_SERVER_ERROR", transaction: tx, info: { urls, errorMessages } }); } /** * Provides the opportunity for a sub-class to wrap a block before * returning it, to add additional properties or an alternate * sub-class of [[Block]]. */ _wrapBlock(value: BlockParams, network: Network): Block { return new Block(formatBlock(value), this); } /** * Provides the opportunity for a sub-class to wrap a log before * returning it, to add additional properties or an alternate * sub-class of [[Log]]. */ _wrapLog(value: LogParams, network: Network): Log { return new Log(formatLog(value), this); } /** * Provides the opportunity for a sub-class to wrap a transaction * receipt before returning it, to add additional properties or an * alternate sub-class of [[TransactionReceipt]]. */ _wrapTransactionReceipt(value: TransactionReceiptParams, network: Network): TransactionReceipt { return new TransactionReceipt(formatTransactionReceipt(value), this); } /** * Provides the opportunity for a sub-class to wrap a transaction * response before returning it, to add additional properties or an * alternate sub-class of [[TransactionResponse]]. */ _wrapTransactionResponse(tx: TransactionResponseParams, network: Network): TransactionResponse { return new TransactionResponse(formatTransactionResponse(tx), this); } /** * Resolves to the Network, forcing a network detection using whatever * technique the sub-class requires. * * Sub-classes **must** override this. */ _detectNetwork(): Promise { assert(false, "sub-classes must implement this", "UNSUPPORTED_OPERATION", { operation: "_detectNetwork" }); } /** * Sub-classes should use this to perform all built-in operations. All * methods sanitizes and normalizes the values passed into this. * * Sub-classes **must** override this. */ async _perform(req: PerformActionRequest): Promise { assert(false, `unsupported method: ${ req.method }`, "UNSUPPORTED_OPERATION", { operation: req.method, info: req }); } // State async getBlockNumber(): Promise { const blockNumber = getNumber(await this.#perform({ method: "getBlockNumber" }), "%response"); if (this.#lastBlockNumber >= 0) { this.#lastBlockNumber = blockNumber; } return blockNumber; } /** * Returns or resolves to the address for %%address%%, resolving ENS * names and [[Addressable]] objects and returning if already an * address. */ _getAddress(address: AddressLike): string | Promise { return resolveAddress(address, this); } /** * Returns or resolves to a valid block tag for %%blockTag%%, resolving * negative values and returning if already a valid block tag. */ _getBlockTag(blockTag?: BlockTag): string | Promise { if (blockTag == null) { return "latest"; } switch (blockTag) { case "earliest": return "0x0"; case "finalized": case "latest": case "pending": case "safe": return blockTag; } if (isHexString(blockTag)) { if (isHexString(blockTag, 32)) { return blockTag; } return toQuantity(blockTag); } if (typeof(blockTag) === "bigint") { blockTag = getNumber(blockTag, "blockTag"); } if (typeof(blockTag) === "number") { if (blockTag >= 0) { return toQuantity(blockTag); } if (this.#lastBlockNumber >= 0) { return toQuantity(this.#lastBlockNumber + blockTag); } return this.getBlockNumber().then((b) => toQuantity(b + blockTag)); } assertArgument(false, "invalid blockTag", "blockTag", blockTag); } /** * Returns or resolves to a filter for %%filter%%, resolving any ENS * names or [[Addressable]] object and returning if already a valid * filter. */ _getFilter(filter: Filter | FilterByBlockHash): PerformActionFilter | Promise { // Create a canonical representation of the topics const topics = (filter.topics || [ ]).map((t) => { if (t == null) { return null; } if (Array.isArray(t)) { return concisify(t.map((t) => t.toLowerCase())); } return t.toLowerCase(); }); const blockHash = ("blockHash" in filter) ? filter.blockHash: undefined; const resolve = (_address: Array, fromBlock?: string, toBlock?: string) => { let address: undefined | string | Array = undefined; switch (_address.length) { case 0: break; case 1: address = _address[0]; break; default: _address.sort(); address = _address; } if (blockHash) { if (fromBlock != null || toBlock != null) { throw new Error("invalid filter"); } } const filter = { }; if (address) { filter.address = address; } if (topics.length) { filter.topics = topics; } if (fromBlock) { filter.fromBlock = fromBlock; } if (toBlock) { filter.toBlock = toBlock; } if (blockHash) { filter.blockHash = blockHash; } return filter; }; // Addresses could be async (ENS names or Addressables) let address: Array> = [ ]; if (filter.address) { if (Array.isArray(filter.address)) { for (const addr of filter.address) { address.push(this._getAddress(addr)); } } else { address.push(this._getAddress(filter.address)); } } let fromBlock: undefined | string | Promise = undefined; if ("fromBlock" in filter) { fromBlock = this._getBlockTag(filter.fromBlock); } let toBlock: undefined | string | Promise = undefined; if ("toBlock" in filter) { toBlock = this._getBlockTag(filter.toBlock); } if (address.filter((a) => (typeof(a) !== "string")).length || (fromBlock != null && typeof(fromBlock) !== "string") || (toBlock != null && typeof(toBlock) !== "string")) { return Promise.all([ Promise.all(address), fromBlock, toBlock ]).then((result) => { return resolve(result[0], result[1], result[2]); }); } return resolve(>address, fromBlock, toBlock); } /** * Returns or resovles to a transaction for %%request%%, resolving * any ENS names or [[Addressable]] and returning if already a valid * transaction. */ _getTransactionRequest(_request: TransactionRequest): PerformActionTransaction | Promise { const request = copyRequest(_request); const promises: Array> = [ ]; [ "to", "from" ].forEach((key) => { if ((request)[key] == null) { return; } const addr = resolveAddress((request)[key], this); if (isPromise(addr)) { promises.push((async function() { (request)[key] = await addr; })()); } else { (request)[key] = addr; } }); if (request.blockTag != null) { const blockTag = this._getBlockTag(request.blockTag); if (isPromise(blockTag)) { promises.push((async function() { request.blockTag = await blockTag; })()); } else { request.blockTag = blockTag; } } if (promises.length) { return (async function() { await Promise.all(promises); return request; })(); } return request; } async getNetwork(): Promise { // No explicit network was set and this is our first time if (this.#networkPromise == null) { // Detect the current network (shared with all calls) const detectNetwork = (async () => { try { const network = await this._detectNetwork(); this.emit("network", network, null); return network; } catch (error) { if (this.#networkPromise === detectNetwork!) { this.#networkPromise = null; } throw error; } })(); this.#networkPromise = detectNetwork; return (await detectNetwork).clone(); } const networkPromise = this.#networkPromise; const [ expected, actual ] = await Promise.all([ networkPromise, // Possibly an explicit Network this._detectNetwork() // The actual connected network ]); if (expected.chainId !== actual.chainId) { if (this.#anyNetwork) { // The "any" network can change, so notify listeners this.emit("network", actual, expected); // Update the network if something else hasn't already changed it if (this.#networkPromise === networkPromise) { this.#networkPromise = Promise.resolve(actual); } } else { // Otherwise, we do not allow changes to the underlying network assert(false, `network changed: ${ expected.chainId } => ${ actual.chainId } `, "NETWORK_ERROR", { event: "changed" }); } } return expected.clone(); } async getFeeData(): Promise { const network = await this.getNetwork(); const getFeeDataFunc = async () => { const { _block, gasPrice, priorityFee } = await resolveProperties({ _block: this.#getBlock("latest", false), gasPrice: ((async () => { try { const value = await this.#perform({ method: "getGasPrice" }); return getBigInt(value, "%response"); } catch (error) { } return null })()), priorityFee: ((async () => { try { const value = await this.#perform({ method: "getPriorityFee" }); return getBigInt(value, "%response"); } catch (error) { } return null; })()) }); let maxFeePerGas: null | bigint = null; let maxPriorityFeePerGas: null | bigint = null; // These are the recommended EIP-1559 heuristics for fee data const block = this._wrapBlock(_block, network); if (block && block.baseFeePerGas) { maxPriorityFeePerGas = (priorityFee != null) ? priorityFee: BigInt("1000000000"); maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas; } return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); }; // Check for a FeeDataNetWorkPlugin const plugin = network.getPlugin("org.ethers.plugins.network.FetchUrlFeeDataPlugin"); if (plugin) { const req = new FetchRequest(plugin.url); const feeData = await plugin.processFunc(getFeeDataFunc, this, req); return new FeeData(feeData.gasPrice, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas); } return await getFeeDataFunc(); } async estimateGas(_tx: TransactionRequest): Promise { let tx = this._getTransactionRequest(_tx); if (isPromise(tx)) { tx = await tx; } return getBigInt(await this.#perform({ method: "estimateGas", transaction: tx }), "%response"); } async #call(tx: PerformActionTransaction, blockTag: string, attempt: number): Promise { assert (attempt < MAX_CCIP_REDIRECTS, "CCIP read exceeded maximum redirections", "OFFCHAIN_FAULT", { reason: "TOO_MANY_REDIRECTS", transaction: Object.assign({ }, tx, { blockTag, enableCcipRead: true }) }); // This came in as a PerformActionTransaction, so to/from are safe; we can cast const transaction = copyRequest(tx); try { return hexlify(await this._perform({ method: "call", transaction, blockTag })); } catch (error: any) { // CCIP Read OffchainLookup if (!this.disableCcipRead && isCallException(error) && error.data && attempt >= 0 && blockTag === "latest" && transaction.to != null && dataSlice(error.data, 0, 4) === "0x556f1830") { const data = error.data; const txSender = await resolveAddress(transaction.to, this); // Parse the CCIP Read Arguments let ccipArgs: CcipArgs; try { ccipArgs = parseOffchainLookup(dataSlice(error.data, 4)); } catch (error: any) { assert(false, error.message, "OFFCHAIN_FAULT", { reason: "BAD_DATA", transaction, info: { data } }); } // Check the sender of the OffchainLookup matches the transaction assert(ccipArgs.sender.toLowerCase() === txSender.toLowerCase(), "CCIP Read sender mismatch", "CALL_EXCEPTION", { action: "call", data, reason: "OffchainLookup", transaction: transaction, // @TODO: populate data? invocation: null, revert: { signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)", name: "OffchainLookup", args: ccipArgs.errorArgs } }); const ccipResult = await this.ccipReadFetch(transaction, ccipArgs.calldata, ccipArgs.urls); assert(ccipResult != null, "CCIP Read failed to fetch data", "OFFCHAIN_FAULT", { reason: "FETCH_FAILED", transaction, info: { data: error.data, errorArgs: ccipArgs.errorArgs } }); const tx = { to: txSender, data: concat([ ccipArgs.selector, encodeBytes([ ccipResult, ccipArgs.extraData ]) ]) }; this.emit("debug", { action: "sendCcipReadCall", transaction: tx }); try { const result = await this.#call(tx, blockTag, attempt + 1); this.emit("debug", { action: "receiveCcipReadCallResult", transaction: Object.assign({ }, tx), result }); return result; } catch (error) { this.emit("debug", { action: "receiveCcipReadCallError", transaction: Object.assign({ }, tx), error }); throw error; } } throw error; } } async #checkNetwork(promise: Promise): Promise { const { value } = await resolveProperties({ network: this.getNetwork(), value: promise }); return value; } async call(_tx: TransactionRequest): Promise { const { tx, blockTag } = await resolveProperties({ tx: this._getTransactionRequest(_tx), blockTag: this._getBlockTag(_tx.blockTag) }); return await this.#checkNetwork(this.#call(tx, blockTag, _tx.enableCcipRead ? 0: -1)); } // Account async #getAccountValue(request: _PerformAccountRequest, _address: AddressLike, _blockTag?: BlockTag): Promise { let address: string | Promise = this._getAddress(_address); let blockTag: string | Promise = this._getBlockTag(_blockTag); if (typeof(address) !== "string" || typeof(blockTag) !== "string") { [ address, blockTag ] = await Promise.all([ address, blockTag ]); } return await this.#checkNetwork(this.#perform(Object.assign(request, { address, blockTag }))); } async getBalance(address: AddressLike, blockTag?: BlockTag): Promise { return getBigInt(await this.#getAccountValue({ method: "getBalance" }, address, blockTag), "%response"); } async getTransactionCount(address: AddressLike, blockTag?: BlockTag): Promise { return getNumber(await this.#getAccountValue({ method: "getTransactionCount" }, address, blockTag), "%response"); } async getCode(address: AddressLike, blockTag?: BlockTag): Promise { return hexlify(await this.#getAccountValue({ method: "getCode" }, address, blockTag)); } async getStorage(address: AddressLike, _position: BigNumberish, blockTag?: BlockTag): Promise { const position = getBigInt(_position, "position"); return hexlify(await this.#getAccountValue({ method: "getStorage", position }, address, blockTag)); } // Write async broadcastTransaction(signedTx: string): Promise { const { blockNumber, hash, network } = await resolveProperties({ blockNumber: this.getBlockNumber(), hash: this._perform({ method: "broadcastTransaction", signedTransaction: signedTx }), network: this.getNetwork() }); const tx = Transaction.from(signedTx); if (tx.hash !== hash) { throw new Error("@TODO: the returned hash did not match"); } return this._wrapTransactionResponse(tx, network).replaceableTransaction(blockNumber); } async #getBlock(block: BlockTag | string, includeTransactions: boolean): Promise { // @TODO: Add CustomBlockPlugin check if (isHexString(block, 32)) { return await this.#perform({ method: "getBlock", blockHash: block, includeTransactions }); } let blockTag = this._getBlockTag(block); if (typeof(blockTag) !== "string") { blockTag = await blockTag; } return await this.#perform({ method: "getBlock", blockTag, includeTransactions }); } // Queries async getBlock(block: BlockTag | string, prefetchTxs?: boolean): Promise { const { network, params } = await resolveProperties({ network: this.getNetwork(), params: this.#getBlock(block, !!prefetchTxs) }); if (params == null) { return null; } return this._wrapBlock(params, network); } async getTransaction(hash: string): Promise { const { network, params } = await resolveProperties({ network: this.getNetwork(), params: this.#perform({ method: "getTransaction", hash }) }); if (params == null) { return null; } return this._wrapTransactionResponse(params, network); } async getTransactionReceipt(hash: string): Promise { const { network, params } = await resolveProperties({ network: this.getNetwork(), params: this.#perform({ method: "getTransactionReceipt", hash }) }); if (params == null) { return null; } // Some backends did not backfill the effectiveGasPrice into old transactions // in the receipt, so we look it up manually and inject it. if (params.gasPrice == null && params.effectiveGasPrice == null) { const tx = await this.#perform({ method: "getTransaction", hash }); if (tx == null) { throw new Error("report this; could not find tx or effectiveGasPrice"); } params.effectiveGasPrice = tx.gasPrice; } return this._wrapTransactionReceipt(params, network); } async getTransactionResult(hash: string): Promise { const { result } = await resolveProperties({ network: this.getNetwork(), result: this.#perform({ method: "getTransactionResult", hash }) }); if (result == null) { return null; } return hexlify(result); } // Bloom-filter Queries async getLogs(_filter: Filter | FilterByBlockHash): Promise> { let filter = this._getFilter(_filter); if (isPromise(filter)) { filter = await filter; } const { network, params } = await resolveProperties({ network: this.getNetwork(), params: this.#perform>({ method: "getLogs", filter }) }); return params.map((p) => this._wrapLog(p, network)); } // ENS _getProvider(chainId: number): AbstractProvider { assert(false, "provider cannot connect to target network", "UNSUPPORTED_OPERATION", { operation: "_getProvider()" }); } async getResolver(name: string): Promise { return await EnsResolver.fromName(this, name); } async getAvatar(name: string): Promise { const resolver = await this.getResolver(name); if (resolver) { return await resolver.getAvatar(); } return null; } async resolveName(name: string): Promise{ const resolver = await this.getResolver(name); if (resolver) { return await resolver.getAddress(); } return null; } async lookupAddress(address: string): Promise { address = getAddress(address); const node = namehash(address.substring(2).toLowerCase() + ".addr.reverse"); try { const ensAddr = await EnsResolver.getEnsAddress(this); const ensContract = new Contract(ensAddr, [ "function resolver(bytes32) view returns (address)" ], this); const resolver = await ensContract.resolver(node); if (resolver == null || resolver === ZeroAddress) { return null; } const resolverContract = new Contract(resolver, [ "function name(bytes32) view returns (string)" ], this); const name = await resolverContract.name(node); // Failed forward resolution const check = await this.resolveName(name); if (check !== address) { return null; } return name; } catch (error) { // No data was returned from the resolver if (isError(error, "BAD_DATA") && error.value === "0x") { return null; } // Something reerted if (isError(error, "CALL_EXCEPTION")) { return null; } throw error; } return null; } async waitForTransaction(hash: string, _confirms?: null | number, timeout?: null | number): Promise { const confirms = (_confirms != null) ? _confirms: 1; if (confirms === 0) { return this.getTransactionReceipt(hash); } return new Promise(async (resolve, reject) => { let timer: null | Timer = null; const listener = (async (blockNumber: number) => { try { const receipt = await this.getTransactionReceipt(hash); if (receipt != null) { if (blockNumber - receipt.blockNumber + 1 >= confirms) { resolve(receipt); //this.off("block", listener); if (timer) { clearTimeout(timer); timer = null; } return; } } } catch (error) { console.log("EEE", error); } this.once("block", listener); }); if (timeout != null) { timer = setTimeout(() => { if (timer == null) { return; } timer = null; this.off("block", listener); reject(makeError("timeout", "TIMEOUT", { reason: "timeout" })); }, timeout); } listener(await this.getBlockNumber()); }); } async waitForBlock(blockTag?: BlockTag): Promise { assert(false, "not implemented yet", "NOT_IMPLEMENTED", { operation: "waitForBlock" }); } /** * Clear a timer created using the [[_setTimeout]] method. */ _clearTimeout(timerId: number): void { const timer = this.#timers.get(timerId); if (!timer) { return; } if (timer.timer) { clearTimeout(timer.timer); } this.#timers.delete(timerId); } /** * Create a timer that will execute %%func%% after at least %%timeout%% * (in ms). If %%timeout%% is unspecified, then %%func%% will execute * in the next event loop. * * [Pausing](AbstractProvider-paused) the provider will pause any * associated timers. */ _setTimeout(_func: () => void, timeout?: number): number { if (timeout == null) { timeout = 0; } const timerId = this.#nextTimer++; const func = () => { this.#timers.delete(timerId); _func(); }; if (this.paused) { this.#timers.set(timerId, { timer: null, func, time: timeout }); } else { const timer = setTimeout(func, timeout); this.#timers.set(timerId, { timer, func, time: getTime() }); } return timerId; } /** * Perform %%func%% on each subscriber. */ _forEachSubscriber(func: (s: Subscriber) => void): void { for (const sub of this.#subs.values()) { func(sub.subscriber); } } /** * Sub-classes may override this to customize subscription * implementations. */ _getSubscriber(sub: Subscription): Subscriber { switch (sub.type) { case "debug": case "error": case "network": return new UnmanagedSubscriber(sub.type); case "block": { const subscriber = new PollingBlockSubscriber(this); subscriber.pollingInterval = this.pollingInterval; return subscriber; } case "safe": case "finalized": return new PollingBlockTagSubscriber(this, sub.type); case "event": return new PollingEventSubscriber(this, sub.filter); case "transaction": return new PollingTransactionSubscriber(this, sub.hash); case "orphan": return new PollingOrphanSubscriber(this, sub.filter); } throw new Error(`unsupported event: ${ sub.type }`); } /** * If a [[Subscriber]] fails and needs to replace itself, this * method may be used. * * For example, this is used for providers when using the * ``eth_getFilterChanges`` method, which can return null if state * filters are not supported by the backend, allowing the Subscriber * to swap in a [[PollingEventSubscriber]]. */ _recoverSubscriber(oldSub: Subscriber, newSub: Subscriber): void { for (const sub of this.#subs.values()) { if (sub.subscriber === oldSub) { if (sub.started) { sub.subscriber.stop(); } sub.subscriber = newSub; if (sub.started) { newSub.start(); } if (this.#pausedState != null) { newSub.pause(this.#pausedState); } break; } } } async #hasSub(event: ProviderEvent, emitArgs?: Array): Promise { let sub = await getSubscription(event, this); // This is a log that is removing an existing log; we actually want // to emit an orphan event for the removed log if (sub.type === "event" && emitArgs && emitArgs.length > 0 && emitArgs[0].removed === true) { sub = await getSubscription({ orphan: "drop-log", log: emitArgs[0] }, this); } return this.#subs.get(sub.tag) || null; } async #getSub(event: ProviderEvent): Promise { const subscription = await getSubscription(event, this); // Prevent tampering with our tag in any subclass' _getSubscriber const tag = subscription.tag; let sub = this.#subs.get(tag); if (!sub) { const subscriber = this._getSubscriber(subscription); const addressableMap = new WeakMap(); const nameMap = new Map(); sub = { subscriber, tag, addressableMap, nameMap, started: false, listeners: [ ] }; this.#subs.set(tag, sub); } return sub; } async on(event: ProviderEvent, listener: Listener): Promise { const sub = await this.#getSub(event); sub.listeners.push({ listener, once: false }); if (!sub.started) { sub.subscriber.start(); sub.started = true; if (this.#pausedState != null) { sub.subscriber.pause(this.#pausedState); } } return this; } async once(event: ProviderEvent, listener: Listener): Promise { const sub = await this.#getSub(event); sub.listeners.push({ listener, once: true }); if (!sub.started) { sub.subscriber.start(); sub.started = true; if (this.#pausedState != null) { sub.subscriber.pause(this.#pausedState); } } return this; } async emit(event: ProviderEvent, ...args: Array): Promise { const sub = await this.#hasSub(event, args); // If there is not subscription or if a recent emit removed // the last of them (which also deleted the sub) do nothing if (!sub || sub.listeners.length === 0) { return false; }; const count = sub.listeners.length; sub.listeners = sub.listeners.filter(({ listener, once }) => { const payload = new EventPayload(this, (once ? null: listener), event); try { listener.call(this, ...args, payload); } catch(error) { } return !once; }); if (sub.listeners.length === 0) { if (sub.started) { sub.subscriber.stop(); } this.#subs.delete(sub.tag); } return (count > 0); } async listenerCount(event?: ProviderEvent): Promise { if (event) { const sub = await this.#hasSub(event); if (!sub) { return 0; } return sub.listeners.length; } let total = 0; for (const { listeners } of this.#subs.values()) { total += listeners.length; } return total; } async listeners(event?: ProviderEvent): Promise> { if (event) { const sub = await this.#hasSub(event); if (!sub) { return [ ]; } return sub.listeners.map(({ listener }) => listener); } let result: Array = [ ]; for (const { listeners } of this.#subs.values()) { result = result.concat(listeners.map(({ listener }) => listener)); } return result; } async off(event: ProviderEvent, listener?: Listener): Promise { const sub = await this.#hasSub(event); if (!sub) { return this; } if (listener) { const index = sub.listeners.map(({ listener }) => listener).indexOf(listener); if (index >= 0) { sub.listeners.splice(index, 1); } } if (!listener || sub.listeners.length === 0) { if (sub.started) { sub.subscriber.stop(); } this.#subs.delete(sub.tag); } return this; } async removeAllListeners(event?: ProviderEvent): Promise { if (event) { const { tag, started, subscriber } = await this.#getSub(event); if (started) { subscriber.stop(); } this.#subs.delete(tag); } else { for (const [ tag, { started, subscriber } ] of this.#subs) { if (started) { subscriber.stop(); } this.#subs.delete(tag); } } return this; } // Alias for "on" async addListener(event: ProviderEvent, listener: Listener): Promise { return await this.on(event, listener); } // Alias for "off" async removeListener(event: ProviderEvent, listener: Listener): Promise { return this.off(event, listener); } /** * If this provider has been destroyed using the [[destroy]] method. * * Once destroyed, all resources are reclaimed, internal event loops * and timers are cleaned up and no further requests may be sent to * the provider. */ get destroyed(): boolean { return this.#destroyed; } /** * Sub-classes may use this to shutdown any sockets or release their * resources and reject any pending requests. * * Sub-classes **must** call ``super.destroy()``. */ destroy(): void { // Stop all listeners this.removeAllListeners(); // Shut down all tiemrs for (const timerId of this.#timers.keys()) { this._clearTimeout(timerId); } this.#destroyed = true; } /** * Whether the provider is currently paused. * * A paused provider will not emit any events, and generally should * not make any requests to the network, but that is up to sub-classes * to manage. * * Setting ``paused = true`` is identical to calling ``.pause(false)``, * which will buffer any events that occur while paused until the * provider is unpaused. */ get paused(): boolean { return (this.#pausedState != null); } set paused(pause: boolean) { if (!!pause === this.paused) { return; } if (this.paused) { this.resume(); } else { this.pause(false); } } /** * Pause the provider. If %%dropWhilePaused%%, any events that occur * while paused are dropped, otherwise all events will be emitted once * the provider is unpaused. */ pause(dropWhilePaused?: boolean): void { this.#lastBlockNumber = -1; if (this.#pausedState != null) { if (this.#pausedState == !!dropWhilePaused) { return; } assert(false, "cannot change pause type; resume first", "UNSUPPORTED_OPERATION", { operation: "pause" }); } this._forEachSubscriber((s) => s.pause(dropWhilePaused)); this.#pausedState = !!dropWhilePaused; for (const timer of this.#timers.values()) { // Clear the timer if (timer.timer) { clearTimeout(timer.timer); } // Remaining time needed for when we become unpaused timer.time = getTime() - timer.time; } } /** * Resume the provider. */ resume(): void { if (this.#pausedState == null) { return; } this._forEachSubscriber((s) => s.resume()); this.#pausedState = null; for (const timer of this.#timers.values()) { // Remaining time when we were paused let timeout = timer.time; if (timeout < 0) { timeout = 0; } // Start time (in cause paused, so we con compute remaininf time) timer.time = getTime(); // Start the timer setTimeout(timer.func, timeout); } } } function _parseString(result: string, start: number): null | string { try { const bytes = _parseBytes(result, start); if (bytes) { return toUtf8String(bytes); } } catch(error) { } return null; } function _parseBytes(result: string, start: number): null | string { if (result === "0x") { return null; } try { const offset = getNumber(dataSlice(result, start, start + 32)); const length = getNumber(dataSlice(result, offset, offset + 32)); return dataSlice(result, offset + 32, offset + 32 + length); } catch (error) { } return null; } function numPad(value: number): Uint8Array { const result = toBeArray(value); if (result.length > 32) { throw new Error("internal; should not happen"); } const padded = new Uint8Array(32); padded.set(result, 32 - result.length); return padded; } function bytesPad(value: Uint8Array): Uint8Array { if ((value.length % 32) === 0) { return value; } const result = new Uint8Array(Math.ceil(value.length / 32) * 32); result.set(value); return result; } const empty: Uint8Array = new Uint8Array([ ]); // ABI Encodes a series of (bytes, bytes, ...) function encodeBytes(datas: Array): string { const result: Array = [ ]; let byteCount = 0; // Add place-holders for pointers as we add items for (let i = 0; i < datas.length; i++) { result.push(empty); byteCount += 32; } for (let i = 0; i < datas.length; i++) { const data = getBytes(datas[i]); // Update the bytes offset result[i] = numPad(byteCount); // The length and padded value of data result.push(numPad(data.length)); result.push(bytesPad(data)); byteCount += 32 + Math.ceil(data.length / 32) * 32; } return concat(result); } const zeros = "0x0000000000000000000000000000000000000000000000000000000000000000" function parseOffchainLookup(data: string): CcipArgs { const result: CcipArgs = { sender: "", urls: [ ], calldata: "", selector: "", extraData: "", errorArgs: [ ] }; assert(dataLength(data) >= 5 * 32, "insufficient OffchainLookup data", "OFFCHAIN_FAULT", { reason: "insufficient OffchainLookup data" }); const sender = dataSlice(data, 0, 32); assert(dataSlice(sender, 0, 12) === dataSlice(zeros, 0, 12), "corrupt OffchainLookup sender", "OFFCHAIN_FAULT", { reason: "corrupt OffchainLookup sender" }); result.sender = dataSlice(sender, 12); // Read the URLs from the response try { const urls: Array = []; const urlsOffset = getNumber(dataSlice(data, 32, 64)); const urlsLength = getNumber(dataSlice(data, urlsOffset, urlsOffset + 32)); const urlsData = dataSlice(data, urlsOffset + 32); for (let u = 0; u < urlsLength; u++) { const url = _parseString(urlsData, u * 32); if (url == null) { throw new Error("abort"); } urls.push(url); } result.urls = urls; } catch (error) { assert(false, "corrupt OffchainLookup urls", "OFFCHAIN_FAULT", { reason: "corrupt OffchainLookup urls" }); } // Get the CCIP calldata to forward try { const calldata = _parseBytes(data, 64); if (calldata == null) { throw new Error("abort"); } result.calldata = calldata; } catch (error) { assert(false, "corrupt OffchainLookup calldata", "OFFCHAIN_FAULT", { reason: "corrupt OffchainLookup calldata" }); } // Get the callbackSelector (bytes4) assert(dataSlice(data, 100, 128) === dataSlice(zeros, 0, 28), "corrupt OffchainLookup callbaackSelector", "OFFCHAIN_FAULT", { reason: "corrupt OffchainLookup callbaackSelector" }); result.selector = dataSlice(data, 96, 100); // Get the extra data to send back to the contract as context try { const extraData = _parseBytes(data, 128); if (extraData == null) { throw new Error("abort"); } result.extraData = extraData; } catch (error) { assert(false, "corrupt OffchainLookup extraData", "OFFCHAIN_FAULT", { reason: "corrupt OffchainLookup extraData" }); } result.errorArgs = "sender,urls,calldata,selector,extraData".split(/,/).map((k) => (result)[k]) return result; }