Refactored JsonRpcApiProvider and the Provider model.

This commit is contained in:
Richard Moore 2022-09-17 23:17:52 -04:00
parent 40fad8bbc0
commit ee06989ba3
21 changed files with 1066 additions and 562 deletions

@ -230,7 +230,9 @@ class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | Co
}
const tx = await runner.sendTransaction(await this.populateTransaction(...args));
const provider = getProvider(this._contract.runner);
return new ContractTransactionResponse(this._contract.interface, provider, tx);
// @TODO: the provider can be null; make a custom dummy provider that will throw a
// meaningful error
return new ContractTransactionResponse(this._contract.interface, <Provider>provider, tx);
}
async estimateGas(...args: ContractMethodArgs<A>): Promise<bigint> {
@ -491,7 +493,9 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
let deployTx: null | ContractTransactionResponse = null;
if (_deployTx) {
const provider = getProvider(runner);
deployTx = new ContractTransactionResponse(this.interface, provider, _deployTx);
// @TODO: the provider can be null; make a custom dummy provider that will throw a
// meaningful error
deployTx = new ContractTransactionResponse(this.interface, <Provider>provider, _deployTx);
}
let subs = new Map();

@ -2,7 +2,7 @@ import type {
EventFragment, FunctionFragment, Result, Typed
} from "../abi/index.js";
import type {
CallRequest, PreparedRequest, TopicFilter
TransactionRequest, PreparedTransactionRequest, TopicFilter
} from "../providers/index.js";
import type { ContractTransactionResponse } from "./wrappers.js";
@ -20,7 +20,7 @@ export interface DeferredTopicFilter {
fragment: EventFragment;
}
export interface ContractTransaction extends PreparedRequest {
export interface ContractTransaction extends PreparedTransactionRequest {
// These are populated by contract methods and cannot bu null
to: string;
data: string;
@ -30,7 +30,7 @@ export interface ContractTransaction extends PreparedRequest {
export interface ContractDeployTransaction extends Omit<ContractTransaction, "to"> { }
// Overrides; cannot override `to` or `data` as Contract populates these
export interface Overrides extends Omit<CallRequest, "to" | "data"> { };
export interface Overrides extends Omit<TransactionRequest, "to" | "data"> { };
// Arguments for methods; with an optional (n+1)th Override

@ -31,7 +31,7 @@ export class EventLog extends Log {
export class ContractTransactionReceipt extends TransactionReceipt {
readonly #interface: Interface;
constructor(iface: Interface, provider: null | Provider, tx: TransactionReceipt) {
constructor(iface: Interface, provider: Provider, tx: TransactionReceipt) {
super(tx, provider);
this.#interface = iface;
}
@ -52,7 +52,7 @@ export class ContractTransactionReceipt extends TransactionReceipt {
export class ContractTransactionResponse extends TransactionResponse {
readonly #interface: Interface;
constructor(iface: Interface, provider: null | Provider, tx: TransactionResponse) {
constructor(iface: Interface, provider: Provider, tx: TransactionResponse) {
super(tx, provider);
this.#interface = iface;
}

@ -18,22 +18,26 @@ import {
} from "../utils/index.js";
import { EnsResolver } from "./ens-resolver.js";
import {
formatBlock, formatBlockWithTransactions, formatLog, formatTransactionReceipt,
formatTransactionResponse
} from "./format.js";
import { Network } from "./network.js";
import { Block, FeeData, Log, TransactionReceipt, TransactionResponse } from "./provider.js";
import { copyRequest, Block, FeeData, Log, TransactionReceipt, TransactionResponse } from "./provider.js";
import {
PollingBlockSubscriber, 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 { Frozen, Listener } from "../utils/index.js";
import type { AccessList } from "../transaction/index.js";
import type { Listener } from "../utils/index.js";
import type { Networkish } from "./network.js";
//import type { MaxPriorityFeePlugin } from "./plugins-network.js";
import type {
BlockTag, CallRequest, EventFilter, Filter, FilterByBlockHash,
LogParams, OrphanFilter, Provider, ProviderEvent, TransactionRequest,
BlockParams, BlockTag, EventFilter, Filter, FilterByBlockHash, LogParams, OrphanFilter,
PreparedTransactionRequest, Provider, ProviderEvent,
TransactionReceiptParams, TransactionRequest, TransactionResponseParams
} from "./provider.js";
@ -42,6 +46,9 @@ const BN_2 = BigInt(2);
const MAX_CCIP_REDIRECTS = 10;
function isPromise<T = any>(value: any): value is Promise<T> {
return (value && typeof(value.then) === "function");
}
function getTag(prefix: string, value: any): string {
return prefix + ":" + JSON.stringify(value, (k, v) => {
@ -124,10 +131,6 @@ function concisify(items: Array<string>): Array<string> {
return items;
}
// Normalize a ProviderEvent into a Subscription
// @TODO: Make events sync if possible; like block
//function getSyncSubscription(_event: ProviderEvent): Subscription {
//}
async function getSubscription(_event: ProviderEvent, provider: AbstractProvider): Promise<Subscription> {
if (_event == null) { throw new Error("invalid event"); }
@ -215,26 +218,10 @@ export type PerformActionFilter = {
blockHash?: string;
};
export type PerformActionTransaction = {
type?: number;
export interface PerformActionTransaction extends PreparedTransactionRequest {
to?: string;
from?: string;
nonce?: number;
gasLimit?: bigint;
gasPrice?: bigint;
maxPriorityFeePerGas?: bigint;
maxFeePerGas?: bigint;
data?: string;
value?: bigint;
chainId?: bigint;
accessList?: AccessList;
};
}
export type PerformActionRequest = {
method: "call",
@ -289,11 +276,6 @@ type _PerformAccountRequest = {
method: "getStorageAt", position: bigint
}
export function copyRequest<T extends PerformActionTransaction>(tx: T): T {
// @TODO: copy the copy from contracts and use it from this
return tx;
}
type CcipArgs = {
sender: string;
urls: Array<string>;
@ -304,7 +286,6 @@ type CcipArgs = {
};
export class AbstractProvider implements Provider {
#subs: Map<string, Sub>;
@ -313,11 +294,14 @@ export class AbstractProvider implements Provider {
// null=unpaused, true=paused+dropWhilePaused, false=paused
#pausedState: null | boolean;
#networkPromise: null | Promise<Frozen<Network>>;
#networkPromise: null | Promise<Network>;
readonly #anyNetwork: boolean;
#performCache: Map<string, Promise<any>>;
// The most recent block number if running an event or -1 if no "block" event
#lastBlockNumber: number;
#nextTimer: number;
#timers: Map<number, { timer: null | NodeJS.Timer, func: () => void, time: number }>;
@ -326,6 +310,7 @@ export class AbstractProvider implements Provider {
// @TODO: This should be a () => Promise<Network> so network can be
// done when needed; or rely entirely on _detectNetwork?
constructor(_network?: "any" | Networkish) {
if (_network === "any") {
this.#anyNetwork = true;
this.#networkPromise = null;
@ -339,6 +324,8 @@ export class AbstractProvider implements Provider {
this.#networkPromise = null;
}
this.#lastBlockNumber = -1;
this.#performCache = new Map();
this.#subs = new Map();
@ -445,11 +432,27 @@ export class AbstractProvider implements Provider {
});
}
_wrapTransaction(tx: TransactionResponse, hash: string, blockNumber: number): TransactionResponse {
return tx;
_wrapBlock(value: BlockParams<string>, network: Network): Block<string> {
return new Block(formatBlock(value), this);
}
_detectNetwork(): Promise<Frozen<Network>> {
_wrapBlockWithTransactions(value: BlockParams<TransactionResponseParams>, network: Network): Block<TransactionResponse> {
return new Block(formatBlock(value), this);
}
_wrapLog(value: LogParams, network: Network): Log {
return new Log(formatLog(value), this);
}
_wrapTransactionReceipt(value: TransactionReceiptParams, network: Network): TransactionReceipt {
return new TransactionReceipt(formatTransactionReceipt(value), this);
}
_wrapTransactionResponse(tx: TransactionResponseParams, network: Network): TransactionResponse {
return new TransactionResponse(tx, this);
}
_detectNetwork(): Promise<Network> {
return throwError("sub-classes must implement this", "UNSUPPORTED_OPERATION", {
operation: "_detectNetwork"
});
@ -466,21 +469,13 @@ export class AbstractProvider implements Provider {
// State
async getBlockNumber(): Promise<number> {
return getNumber(await this.#perform({ method: "getBlockNumber" }), "%response");
const blockNumber = getNumber(await this.#perform({ method: "getBlockNumber" }), "%response");
if (this.#lastBlockNumber >= 0) { this.#lastBlockNumber = blockNumber; }
return blockNumber;
}
// @TODO: Make this string | Promsie<string> so no await needed if sync is possible
_getAddress(address: AddressLike): string | Promise<string> {
return resolveAddress(address, this);
/*
if (typeof(address) === "string") {
if (address.match(/^0x[0-9a-f]+$/i)) { return address; }
const resolved = await this.resolveName(address);
if (resolved == null) { throw new Error("not confiugred @TODO"); }
return resolved;
}
return address.getAddress();
*/
}
_getBlockTag(blockTag?: BlockTag): string | Promise<string> {
@ -500,276 +495,13 @@ export class AbstractProvider implements Provider {
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));
}
return throwArgumentError("invalid blockTag", "blockTag", blockTag);
}
async getNetwork(): Promise<Frozen<Network>> {
// 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 = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
if (this.#networkPromise === detectNetwork) {
this.#networkPromise = null;
}
throw error;
});
this.#networkPromise = detectNetwork;
return await detectNetwork;
}
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
throwError(`network changed: ${ expected.chainId } => ${ actual.chainId } `, "NETWORK_ERROR", {
event: "changed"
});
}
}
return expected.clone().freeze();
}
async getFeeData(): Promise<FeeData> {
const { block, gasPrice } = await resolveProperties({
block: this.getBlock("latest"),
gasPrice: ((async () => {
try {
const gasPrice = await this.#perform({ method: "getGasPrice" });
return getBigInt(gasPrice, "%response");
} catch (error) { }
return null
})())
});
let maxFeePerGas = null, maxPriorityFeePerGas = null;
if (block && block.baseFeePerGas) {
// We may want to compute this more accurately in the future,
// using the formula "check if the base fee is correct".
// See: https://eips.ethereum.org/EIPS/eip-1559
maxPriorityFeePerGas = BigInt("1500000000");
// Allow a network to override their maximum priority fee per gas
//const priorityFeePlugin = (await this.getNetwork()).getPlugin<MaxPriorityFeePlugin>("org.ethers.plugins.max-priority-fee");
//if (priorityFeePlugin) {
// maxPriorityFeePerGas = await priorityFeePlugin.getPriorityFee(this);
//}
maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas;
}
return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
}
async _getTransaction(_request: CallRequest): Promise<PerformActionTransaction> {
const network = await this.getNetwork();
// Fill in any addresses
const request = Object.assign({}, _request, await resolveProperties({
to: (_request.to ? resolveAddress(_request.to, this): undefined),
from: (_request.from ? resolveAddress(_request.from, this): undefined),
}));
return network.formatter.transactionRequest(request);
}
async estimateGas(_tx: TransactionRequest): Promise<bigint> {
const transaction = await this._getTransaction(_tx);
return getBigInt(await this.#perform({
method: "estimateGas", transaction
}), "%response");
}
async #call(tx: PerformActionTransaction, blockTag: string, attempt: number): Promise<string> {
if (attempt >= MAX_CCIP_REDIRECTS) {
throwError("CCIP read exceeded maximum redirections", "OFFCHAIN_FAULT", {
reason: "TOO_MANY_REDIRECTS",
transaction: Object.assign({ }, tx, { blockTag, enableCcipRead: true })
});
}
const transaction = copyRequest(tx);
try {
return hexlify(await this._perform({ method: "call", transaction, blockTag }));
} catch (error) {
// CCIP Read OffchainLookup
if (!this.disableCcipRead && isCallException(error) && 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) {
return throwError(error.message, "OFFCHAIN_FAULT", {
reason: "BAD_DATA",
transaction, info: { data }
});
}
// Check the sender of the OffchainLookup matches the transaction
if (ccipArgs.sender.toLowerCase() !== txSender.toLowerCase()) {
return throwError("CCIP Read sender mismatch", "CALL_EXCEPTION", {
data, transaction,
errorSignature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
errorName: "OffchainLookup",
errorArgs: ccipArgs.errorArgs
});
}
const ccipResult = await this.ccipReadFetch(transaction, ccipArgs.calldata, ccipArgs.urls);
if (ccipResult == null) {
return throwError("CCIP Read failed to fetch data", "OFFCHAIN_FAULT", {
reason: "FETCH_FAILED",
transaction, info: { data: error.data, errorArgs: ccipArgs.errorArgs }
});
}
return this.#call({
to: txSender,
data: concat([
ccipArgs.selector, encodeBytes([ ccipResult, ccipArgs.extraData ])
]),
}, blockTag, attempt + 1);
}
throw error;
}
}
async call(_tx: CallRequest): Promise<string> {
const [ tx, blockTag ] = await Promise.all([
this._getTransaction(_tx), this._getBlockTag(_tx.blockTag)
]);
return this.#call(tx, blockTag, _tx.enableCcipRead ? 0: -1);
}
// Account
async #getAccountValue(request: _PerformAccountRequest, _address: AddressLike, _blockTag?: BlockTag): Promise<any> {
let address: string | Promise<string> = this._getAddress(_address);
let blockTag: string | Promise<string> = this._getBlockTag(_blockTag);
if (typeof(address) !== "string" || typeof(blockTag) !== "string") {
[ address, blockTag ] = await Promise.all([ address, blockTag ]);
}
return await this.#perform(Object.assign(request, { address, blockTag }));
}
async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
return getBigInt(await this.#getAccountValue({ method: "getBalance" }, address, blockTag), "%response");
}
async getTransactionCount(address: AddressLike, blockTag?: BlockTag): Promise<number> {
return getNumber(await this.#getAccountValue({ method: "getTransactionCount" }, address, blockTag), "%response");
}
async getCode(address: AddressLike, blockTag?: BlockTag): Promise<string> {
return hexlify(await this.#getAccountValue({ method: "getCode" }, address, blockTag));
}
async getStorageAt(address: AddressLike, _position: BigNumberish, blockTag?: BlockTag): Promise<string> {
const position = getBigInt(_position, "position");
return hexlify(await this.#getAccountValue({ method: "getStorageAt", position }, address, blockTag));
}
// Write
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
throw new Error();
return <TransactionResponse><unknown>{ };
}
async #getBlock(block: BlockTag | string, includeTransactions: boolean): Promise<any> {
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): Promise<null | Block<string>> {
const [ network, params ] = await Promise.all([
this.getNetwork(), this.#getBlock(block, false)
]);
if (params == null) { return null; }
return network.formatter.block(params, this);
}
async getBlockWithTransactions(block: BlockTag | string): Promise<null | Block<TransactionResponse>> {
const format = (await this.getNetwork()).formatter;
const params = this.#getBlock(block, true);
if (params == null) { return null; }
return format.blockWithTransactions(params, this);
}
async getTransaction(hash: string): Promise<null | TransactionResponse> {
const format = (await this.getNetwork()).formatter;
const params = await this.#perform({ method: "getTransaction", hash });
return format.transactionResponse(params, this);
}
async getTransactionReceipt(hash: string): Promise<null | TransactionReceipt> {
const format = (await this.getNetwork()).formatter;
const receipt = await this.#perform({ method: "getTransactionReceipt", hash });
if (receipt == 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 (receipt.gasPrice == null && receipt.effectiveGasPrice == null) {
const tx = await this.#perform({ method: "getTransaction", hash });
receipt.effectiveGasPrice = tx.gasPrice;
}
return format.receipt(receipt, this);
}
async getTransactionResult(hash: string): Promise<null | string> {
const result = await this.#perform({ method: "getTransactionResult", hash });
if (result == null) { return null; }
return hexlify(result);
}
_getFilter(filter: Filter | FilterByBlockHash): PerformActionFilter | Promise<PerformActionFilter> {
// Create a canonical representation of the topics
@ -839,16 +571,327 @@ export class AbstractProvider implements Provider {
return resolve(<Array<string>>address, fromBlock, toBlock);
}
// Bloom-filter Queries
async getLogs(_filter: Filter | FilterByBlockHash): Promise<Array<Log>> {
const { network, filter } = await resolveProperties({
network: this.getNetwork(),
filter: this._getFilter(_filter)
_getTransactionRequest(_request: TransactionRequest): PerformActionTransaction | Promise<PerformActionTransaction> {
const request = <PerformActionTransaction>copyRequest(_request);
const promises: Array<Promise<void>> = [ ];
[ "to", "from" ].forEach((key) => {
if ((<any>request)[key] == null) { return; }
const addr = resolveAddress((<any>request)[key]);
if (isPromise(addr)) {
promises.push((async function() { (<any>request)[key] = await addr; })());
} else {
(<any>request)[key] = addr;
}
});
return (await this.#perform<Array<LogParams>>({ method: "getLogs", filter })).map((l) => {
return network.formatter.log(l, this);
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<Network> {
// 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 = this._detectNetwork().then((network) => {
this.emit("network", network, null);
return network;
}, (error) => {
// Reset the networkPromise on failure, so we will try again
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
throwError(`network changed: ${ expected.chainId } => ${ actual.chainId } `, "NETWORK_ERROR", {
event: "changed"
});
}
}
return expected.clone();
}
async getFeeData(): Promise<FeeData> {
const { block, gasPrice } = await resolveProperties({
block: this.getBlock("latest"),
gasPrice: ((async () => {
try {
const gasPrice = await this.#perform({ method: "getGasPrice" });
return getBigInt(gasPrice, "%response");
} catch (error) { }
return null
})())
});
let maxFeePerGas = null, maxPriorityFeePerGas = null;
if (block && block.baseFeePerGas) {
// We may want to compute this more accurately in the future,
// using the formula "check if the base fee is correct".
// See: https://eips.ethereum.org/EIPS/eip-1559
maxPriorityFeePerGas = BigInt("1500000000");
// Allow a network to override their maximum priority fee per gas
//const priorityFeePlugin = (await this.getNetwork()).getPlugin<MaxPriorityFeePlugin>("org.ethers.plugins.max-priority-fee");
//if (priorityFeePlugin) {
// maxPriorityFeePerGas = await priorityFeePlugin.getPriorityFee(this);
//}
maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas;
}
return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
}
async estimateGas(_tx: TransactionRequest): Promise<bigint> {
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<string> {
if (attempt >= MAX_CCIP_REDIRECTS) {
throwError("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 = <PerformActionTransaction>copyRequest(tx);
try {
return hexlify(await this._perform({ method: "call", transaction, blockTag }));
} catch (error) {
// CCIP Read OffchainLookup
if (!this.disableCcipRead && isCallException(error) && 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) {
return throwError(error.message, "OFFCHAIN_FAULT", {
reason: "BAD_DATA",
transaction, info: { data }
});
}
// Check the sender of the OffchainLookup matches the transaction
if (ccipArgs.sender.toLowerCase() !== txSender.toLowerCase()) {
return throwError("CCIP Read sender mismatch", "CALL_EXCEPTION", {
data, transaction,
errorSignature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
errorName: "OffchainLookup",
errorArgs: ccipArgs.errorArgs
});
}
const ccipResult = await this.ccipReadFetch(transaction, ccipArgs.calldata, ccipArgs.urls);
if (ccipResult == null) {
return throwError("CCIP Read failed to fetch data", "OFFCHAIN_FAULT", {
reason: "FETCH_FAILED",
transaction, info: { data: error.data, errorArgs: ccipArgs.errorArgs }
});
}
return this.#call({
to: txSender,
data: concat([
ccipArgs.selector, encodeBytes([ ccipResult, ccipArgs.extraData ])
]),
}, blockTag, attempt + 1);
}
throw error;
}
}
async #checkNetwork<T>(promise: Promise<T>): Promise<T> {
const { value } = await resolveProperties({
network: this.getNetwork(),
value: promise
});
return value;
}
async call(_tx: TransactionRequest): Promise<string> {
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<any> {
let address: string | Promise<string> = this._getAddress(_address);
let blockTag: string | Promise<string> = 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<bigint> {
return getBigInt(await this.#getAccountValue({ method: "getBalance" }, address, blockTag), "%response");
}
async getTransactionCount(address: AddressLike, blockTag?: BlockTag): Promise<number> {
return getNumber(await this.#getAccountValue({ method: "getTransactionCount" }, address, blockTag), "%response");
}
async getCode(address: AddressLike, blockTag?: BlockTag): Promise<string> {
return hexlify(await this.#getAccountValue({ method: "getCode" }, address, blockTag));
}
async getStorageAt(address: AddressLike, _position: BigNumberish, blockTag?: BlockTag): Promise<string> {
const position = getBigInt(_position, "position");
return hexlify(await this.#getAccountValue({ method: "getStorageAt", position }, address, blockTag));
}
// Write
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
throw new Error();
return <TransactionResponse><unknown>{ };
}
async #getBlock(block: BlockTag | string, includeTransactions: boolean): Promise<any> {
// @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): Promise<null | Block<string>> {
const { network, params } = await resolveProperties({
network: this.getNetwork(),
params: this.#getBlock(block, false)
});
if (params == null) { return null; }
return this._wrapBlock(formatBlock(params), network);
}
async getBlockWithTransactions(block: BlockTag | string): Promise<null | Block<TransactionResponse>> {
const { network, params } = await resolveProperties({
network: this.getNetwork(),
params: this.#getBlock(block, true)
});
if (params == null) { return null; }
return this._wrapBlockWithTransactions(formatBlockWithTransactions(params), network);
}
async getTransaction(hash: string): Promise<null | TransactionResponse> {
const { network, params } = await resolveProperties({
network: this.getNetwork(),
params: this.#perform({ method: "getTransaction", hash })
});
if (params == null) { return null; }
return this._wrapTransactionResponse(formatTransactionResponse(params), network);
}
async getTransactionReceipt(hash: string): Promise<null | TransactionReceipt> {
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(formatTransactionReceipt(params), network);
}
async getTransactionResult(hash: string): Promise<null | string> {
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<Array<Log>> {
let filter = this._getFilter(_filter);
if (isPromise(filter)) { filter = await filter; }
const { network, params } = await resolveProperties({
network: this.getNetwork(),
params: this.#perform<Array<LogParams>>({ method: "getLogs", filter })
});
return params.map((p) => this._wrapLog(formatLog(p), network));
}
// ENS
@ -1153,6 +1196,8 @@ export class AbstractProvider implements Provider {
}
pause(dropWhilePaused?: boolean): void {
this.#lastBlockNumber = -1;
if (this.#pausedState != null) {
if (this.#pausedState == !!dropWhilePaused) { return; }
return throwError("cannot change pause type; resume first", "UNSUPPORTED_OPERATION", {

@ -8,7 +8,7 @@ import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
import type { TransactionLike } from "../transaction/index.js";
import type {
BlockTag, CallRequest, Provider, TransactionRequest, TransactionResponse
BlockTag, Provider, TransactionRequest, TransactionResponse
} from "./provider.js";
import type { Signer } from "./signer.js";
@ -32,7 +32,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
return this.#checkProvider("getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
}
async #populate(op: string, tx: CallRequest | TransactionRequest): Promise<TransactionLike<string>> {
async #populate(op: string, tx: TransactionRequest): Promise<TransactionLike<string>> {
const provider = this.#checkProvider(op);
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
@ -63,7 +63,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
return pop;
}
async populateCall(tx: CallRequest): Promise<TransactionLike<string>> {
async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
const pop = await this.#populate("populateCall", tx);
return pop;
@ -98,11 +98,11 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
return await resolveProperties(pop);
}
async estimateGas(tx: CallRequest): Promise<bigint> {
async estimateGas(tx: TransactionRequest): Promise<bigint> {
return this.#checkProvider("estimateGas").estimateGas(await this.populateCall(tx));
}
async call(tx: CallRequest): Promise<string> {
async call(tx: TransactionRequest): Promise<string> {
return this.#checkProvider("call").call(await this.populateCall(tx));
}
@ -176,7 +176,7 @@ export class WrappedSigner extends AbstractSigner {
return await this.#signer.getNonce(blockTag);
}
async populateCall(tx: CallRequest): Promise<TransactionLike<string>> {
async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
return await this.#signer.populateCall(tx);
}
@ -184,11 +184,11 @@ export class WrappedSigner extends AbstractSigner {
return await this.#signer.populateTransaction(tx);
}
async estimateGas(tx: CallRequest): Promise<bigint> {
async estimateGas(tx: TransactionRequest): Promise<bigint> {
return await this.#signer.estimateGas(tx);
}
async call(tx: CallRequest): Promise<string> {
async call(tx: TransactionRequest): Promise<string> {
return await this.#signer.call(tx);
}

@ -1,5 +1,5 @@
import type {
CallRequest, Provider, TransactionRequest, TransactionResponse
Provider, TransactionRequest, TransactionResponse
} from "./provider.js";
// The object that will be used to run Contracts. The Signer and Provider
@ -11,7 +11,7 @@ export interface ContractRunner {
estimateGas?: (tx: TransactionRequest) => Promise<bigint>;
// Required for pure, view or static calls to contracts; usually a Signer or Provider
call?: (tx: CallRequest) => Promise<string>;
call?: (tx: TransactionRequest) => Promise<string>;
// Required to support ENS names; usually a Signer or Provider
resolveName?: (name: string) => Promise<null | string>;

@ -1,3 +1,4 @@
import { getAddress } from "../address/index.js";
import { ZeroHash } from "../constants/hashes.js";
import { dnsEncode, namehash } from "../hash/index.js";
import {
@ -12,7 +13,7 @@ import type { BigNumberish, BytesLike, EthersError } from "../utils/index.js";
import type { AbstractProvider, ProviderPlugin } from "./abstract-provider.js";
import type { EnsPlugin } from "./plugins-network.js";
import type { CallRequest, Provider } from "./provider.js";
import type { TransactionRequest, Provider } from "./provider.js";
const BN_1 = BigInt(1);
@ -79,6 +80,13 @@ function encodeBytes(datas: Array<BytesLike>) {
return concat(result);
}
function callAddress(value: string): string {
if (value.length !== 66 || dataSlice(value, 0, 12) !== "0x000000000000000000000000") {
throwArgumentError("invalid call address", "value", value);
}
return getAddress("0x" + value.substring(26));
}
// @TODO: This should use the fetch-data:ipfs gateway
// Trim off the ipfs:// prefix and return the default gateway URL
function getIpfsLink(link: string): string {
@ -187,7 +195,7 @@ export class EnsResolver {
// e.g. keccak256("addr(bytes32,uint256)")
const addrData = concat([ selector, namehash(this.name), parameters ]);
const tx: CallRequest = {
const tx: TransactionRequest = {
to: this.address,
enableCcipRead: true,
data: addrData
@ -225,10 +233,9 @@ export class EnsResolver {
const result = await this._fetch("0x3b3b57de");
// No address
if (result === "0x" || result === ZeroHash) { return null; }
if (result == null || result === "0x" || result === ZeroHash) { return null; }
const network = await this.provider.getNetwork();
return network.formatter.callAddress(result);
return callAddress(result);
} catch (error: any) {
if ((error as EthersError).code === "CALL_EXCEPTION") { return null; }
throw error;
@ -359,15 +366,13 @@ export class EnsResolver {
throw new Error("!caip");
}
const formatter = (await this.provider.getNetwork()).formatter;
const addr = formatter.address(comps[0]);
const addr = getAddress(comps[0]);
const tokenId = numPad(comps[1]);
// Check that this account owns the token
if (scheme === "erc721") {
// ownerOf(uint256 tokenId)
const tokenOwner = formatter.callAddress(await this.provider.call({
const tokenOwner = callAddress(await this.provider.call({
to: addr, data: concat([ "0x6352211e", tokenId ])
}));
if (owner !== tokenOwner) {
@ -493,7 +498,7 @@ export class EnsResolver {
enableCcipRead: true
});
const addr = network.formatter.callAddress(addrData);
const addr = callAddress(addrData);
if (addr === dataSlice(ZeroHash, 0, 20)) { return null; }
return addr;

263
src.ts/providers/format.ts Normal file

@ -0,0 +1,263 @@
import { getAddress, getCreateAddress } from "../address/index.js";
import { accessListify } from "../transaction/index.js";
import {
getBigInt, getNumber, hexlify, isHexString, zeroPadValue,
throwArgumentError, throwError
} from "../utils/index.js";
const BN_0 = BigInt(0);
export type FormatFunc = (value: any) => any;
export function allowNull(format: FormatFunc, nullValue?: any): FormatFunc {
return (function(value: any) {
if (value == null) { return nullValue; }
return format(value);
});
}
export function arrayOf(format: FormatFunc): FormatFunc {
return ((array: any) => {
if (!Array.isArray(array)) { throw new Error("not an array"); }
return array.map((i) => format(i));
});
}
// Requires an object which matches a fleet of other formatters
// Any FormatFunc may return `undefined` to have the value omitted
// from the result object. Calls preserve `this`.
export function object(format: Record<string, FormatFunc>, altNames?: Record<string, Array<string>>): FormatFunc {
return ((value: any) => {
const result: any = { };
for (const key in format) {
let srcKey = key;
if (altNames && key in altNames && !(srcKey in value)) {
for (const altKey of altNames[key]) {
if (altKey in value) {
srcKey = altKey;
break;
}
}
}
try {
const nv = format[key](value[srcKey]);
if (nv !== undefined) { result[key] = nv; }
} catch (error) {
const message = (error instanceof Error) ? error.message: "not-an-error";
throwError(`invalid value for value.${ key } (${ message })`, "BAD_DATA", { value })
}
}
return result;
});
}
export function formatBoolean(value: any): boolean {
switch (value) {
case true: case "true":
return true;
case false: case "false":
return false;
}
return throwArgumentError(`invalid boolean; ${ JSON.stringify(value) }`, "value", value);
}
export function formatData(value: string): string {
if (!isHexString(value, true)) {
throwArgumentError("", "value", value);
}
return value;
}
export function formatHash(value: any): string {
if (!isHexString(value, 32)) {
throwArgumentError("", "value", value);
}
return value;
}
export function formatUint256(value: any): string {
if (!isHexString(value)) {
throw new Error("invalid uint256");
}
return zeroPadValue(value, 32);
}
export const formatLog = object({
address: getAddress,
blockHash: formatHash,
blockNumber: getNumber,
data: formatData,
index: getNumber,
removed: formatBoolean,
topics: arrayOf(formatHash),
transactionHash: formatHash,
transactionIndex: getNumber,
}, {
index: [ "logIndex" ]
});
function _formatBlock(txFunc: FormatFunc): FormatFunc {
return object({
hash: allowNull(formatHash),
parentHash: formatHash,
number: getNumber,
timestamp: getNumber,
nonce: allowNull(formatData),
difficulty: getBigInt,
gasLimit: getBigInt,
gasUsed: getBigInt,
miner: allowNull(getAddress),
extraData: formatData,
transactions: arrayOf(txFunc),
baseFeePerGas: allowNull(getBigInt)
});
}
export const formatBlock = _formatBlock(formatHash);
export const formatBlockWithTransactions = _formatBlock(formatTransactionResponse);
export const formatReceiptLog = object({
transactionIndex: getNumber,
blockNumber: getNumber,
transactionHash: formatHash,
address: getAddress,
topics: arrayOf(formatHash),
data: formatData,
logIndex: getNumber,
blockHash: formatHash,
});
export const formatTransactionReceipt = object({
to: allowNull(getAddress, null),
from: allowNull(getAddress, null),
contractAddress: allowNull(getAddress, null),
transactionIndex: getNumber,
// should be allowNull(hash), but broken-EIP-658 support is handled in receipt
root: allowNull(hexlify),
gasUsed: getBigInt,
logsBloom: allowNull(formatData),
blockHash: formatHash,
transactionHash: formatHash,
logs: arrayOf(formatReceiptLog),
blockNumber: getNumber,
confirmations: allowNull(getNumber, null),
cumulativeGasUsed: getBigInt,
effectiveGasPrice: allowNull(getBigInt),
status: allowNull(getNumber),
type: getNumber
}, {
effectiveGasPrice: [ "gasPrice" ]
});
export function formatTransactionResponse(value: any) {
// Some clients (TestRPC) do strange things like return 0x0 for the
// 0 address; correct this to be a real address
if (value.to && getBigInt(value.to) === BN_0) {
value.to = "0x0000000000000000000000000000000000000000";
}
const result = object({
hash: formatHash,
type: (value: any) => {
if (value === "0x" || value == null) { return 0; }
return getNumber(value);
},
accessList: allowNull(accessListify, null),
blockHash: allowNull(formatHash, null),
blockNumber: allowNull(getNumber, null),
transactionIndex: allowNull(getNumber, null),
confirmations: allowNull(getNumber, null),
from: getAddress,
// either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas) must be set
gasPrice: allowNull(getBigInt),
maxPriorityFeePerGas: allowNull(getBigInt),
maxFeePerGas: allowNull(getBigInt),
gasLimit: getBigInt,
to: allowNull(getAddress, null),
value: getBigInt,
nonce: getNumber,
data: formatData,
r: allowNull(formatUint256),
s: allowNull(formatUint256),
v: allowNull(getNumber),
creates: allowNull(getAddress, null),
chainId: allowNull(getBigInt, null)
}, {
data: [ "input" ],
gasLimit: [ "gas" ]
})(value);
// If to and creates are empty, populate the creates from the value
if (result.to == null && result.creates == null) {
result.creates = getCreateAddress(result);
}
// @TODO: Check fee data
// Add an access list to supported transaction types
if ((value.type === 1 || value.type === 2) && value.accessList == null) {
value.accessList = [ ];
}
// @TODO: check chainID
/*
if (value.chainId != null) {
let chainId = value.chainId;
if (isHexString(chainId)) {
chainId = BigNumber.from(chainId).toNumber();
}
result.chainId = chainId;
} else {
let chainId = value.networkId;
// geth-etc returns chainId
if (chainId == null && result.v == null) {
chainId = value.chainId;
}
if (isHexString(chainId)) {
chainId = BigNumber.from(chainId).toNumber();
}
if (typeof(chainId) !== "number" && result.v != null) {
chainId = (result.v - 35) / 2;
if (chainId < 0) { chainId = 0; }
chainId = parseInt(chainId);
}
if (typeof(chainId) !== "number") { chainId = 0; }
result.chainId = chainId;
}
*/
// 0x0000... should actually be null
if (result.blockHash && getBigInt(result.blockHash) === BN_0) {
result.blockHash = null;
}
return result;
}

@ -14,7 +14,7 @@
* Network object this allows exotic (non-Ethereum) networks
* to be fairly simple to adapt to ethers.
*/
/*
import { getAddress, getCreateAddress } from "../address/index.js";
import {
dataLength, dataSlice, getBigInt, getNumber, isHexString, toQuantity,
@ -441,3 +441,4 @@ export class Formatter {
});
}
}
*/

@ -22,8 +22,6 @@ export { getDefaultProvider } from "./default-provider.js";
export { EnsResolver } from "./ens-resolver.js";
*/
export { Formatter } from "./formatter.js";
export { Network } from "./common-networks.js";
export {
@ -42,8 +40,6 @@ export {
TransactionReceipt,
TransactionResponse,
dummyProvider,
copyRequest,
//resolveTransactionRequest,
} from "./provider.js";
@ -83,15 +79,13 @@ export type {
AvatarLinkageType, AvatarLinkage, AvatarResult
} from "./ens-resolver.js";
*/
export type { FormatFunc } from "./formatter.js";
export type { Networkish } from "./network.js";
export type { GasCostParameters } from "./plugins-network.js";
export type {
BlockTag,
CallRequest, TransactionRequest, PreparedRequest,
TransactionRequest, PreparedTransactionRequest,
EventFilter, Filter, FilterByBlockHash, OrphanFilter, ProviderEvent, TopicFilter,
Provider,
} from "./provider.js";

@ -1,12 +1,11 @@
import { accessListify } from "../transaction/index.js";
import {
getStore, getBigInt, setStore, throwArgumentError
} from "../utils/index.js";
import { Formatter } from "./formatter.js";
import { EnsPlugin, GasCostPlugin } from "./plugins-network.js";
import type { BigNumberish } from "../utils/index.js";
import type { Freezable, Frozen } from "../utils/index.js";
import type { TransactionLike } from "../transaction/index.js";
import type { NetworkPlugin } from "./plugins-network.js";
@ -84,23 +83,20 @@ export class CcipPreflightPlugin extends NetworkPlugin {
const Networks: Map<string | bigint, () => Network> = new Map();
const defaultFormatter = new Formatter();
// @TODO: Add a _ethersNetworkObj variable to better detect network ovjects
export class Network implements Freezable<Network> {
export class Network {
#props: {
name: string,
chainId: bigint,
formatter: Formatter,
plugins: Map<string, NetworkPlugin>
};
constructor(name: string, _chainId: BigNumberish, formatter?: Formatter) {
constructor(name: string, _chainId: BigNumberish) {
const chainId = getBigInt(_chainId);
if (formatter == null) { formatter = defaultFormatter; }
const plugins = new Map();
this.#props = { name, chainId, formatter, plugins };
this.#props = { name, chainId, plugins };
}
toJSON(): any {
@ -113,19 +109,15 @@ export class Network implements Freezable<Network> {
get chainId(): bigint { return getStore(this.#props, "chainId"); }
set chainId(value: BigNumberish) { setStore(this.#props, "chainId", getBigInt(value, "chainId")); }
get formatter(): Formatter { return getStore(this.#props, "formatter"); }
set formatter(value: Formatter) { setStore(this.#props, "formatter", value); }
get plugins(): Array<NetworkPlugin> {
return Array.from(this.#props.plugins.values());
}
attachPlugin(plugin: NetworkPlugin): this {
if (this.isFrozen()) { throw new Error("frozen"); }
if (this.#props.plugins.get(plugin.name)) {
throw new Error(`cannot replace existing plugin: ${ plugin.name } `);
}
this.#props.plugins.set(plugin.name, plugin.validate(this));
this.#props.plugins.set(plugin.name, plugin.clone());
return this;
}
@ -139,13 +131,13 @@ export class Network implements Freezable<Network> {
}
clone(): Network {
const clone = new Network(this.name, this.chainId, this.formatter);
const clone = new Network(this.name, this.chainId);
this.plugins.forEach((plugin) => {
clone.attachPlugin(plugin.clone());
});
return clone;
}
/*
freeze(): Frozen<Network> {
Object.freeze(this.#props);
return this;
@ -154,7 +146,7 @@ export class Network implements Freezable<Network> {
isFrozen(): boolean {
return Object.isFrozen(this.#props);
}
*/
computeIntrinsicGas(tx: TransactionLike): number {
const costs = this.getPlugin<GasCostPlugin>("org.ethers.gas-cost") || (new GasCostPlugin());
@ -171,7 +163,7 @@ export class Network implements Freezable<Network> {
}
if (tx.accessList) {
const accessList = this.formatter.accessList(tx.accessList);
const accessList = accessListify(tx.accessList);
for (const addr in accessList) {
gas += costs.txAccessListAddress + costs.txAccessListStorageKey * accessList[addr].storageKeys.length;
}

@ -2,9 +2,6 @@ import { defineProperties } from "../utils/properties.js";
import { throwArgumentError } from "../utils/index.js";
//import { BigNumberish } from "../math/index.js";
import type { Network } from "./network.js";
import type { FeeData, Provider } from "./provider.js";
@ -21,9 +18,9 @@ export class NetworkPlugin {
return new NetworkPlugin(this.name);
}
validate(network: Network): NetworkPlugin {
return this;
}
// validate(network: Network): NetworkPlugin {
// return this;
// }
}
// Networks can use this plugin to override calculations for the
@ -38,7 +35,7 @@ export type GasCostParameters = {
txAccessListAddress?: number;
};
export class GasCostPlugin extends NetworkPlugin {
export class GasCostPlugin extends NetworkPlugin implements GasCostParameters {
readonly effectiveBlock!: number;
readonly txBase!: number;
@ -98,10 +95,10 @@ export class EnsPlugin extends NetworkPlugin {
return new EnsPlugin(this.address, this.targetNetwork);
}
validate(network: Network): this {
network.formatter.address(this.address);
return this;
}
// validate(network: Network): this {
// network.formatter.address(this.address);
// return this;
// }
}
/*
export class MaxPriorityFeePlugin extends NetworkPlugin {
@ -141,3 +138,28 @@ export class FeeDataNetworkPlugin extends NetworkPlugin {
return new FeeDataNetworkPlugin(this.#feeDataFunc);
}
}
import type { Block, BlockParams, TransactionResponse, TransactionResponseParams } from "./provider.js";
export class CustomBlockNetworkPlugin extends NetworkPlugin {
readonly #blockFunc: (provider: Provider, block: BlockParams<string>) => Block<string>;
readonly #blockWithTxsFunc: (provider: Provider, block: BlockParams<TransactionResponseParams>) => Block<TransactionResponse>;
constructor(blockFunc: (provider: Provider, block: BlockParams<string>) => Block<string>, blockWithTxsFunc: (provider: Provider, block: BlockParams<TransactionResponseParams>) => Block<TransactionResponse>) {
super("org.ethers.network-plugins.custom-block");
this.#blockFunc = blockFunc;
this.#blockWithTxsFunc = blockWithTxsFunc;
}
async getBlock(provider: Provider, block: BlockParams<string>): Promise<Block<string>> {
return await this.#blockFunc(provider, block);
}
async getBlockWithTransactions(provider: Provider, block: BlockParams<TransactionResponseParams>): Promise<Block<TransactionResponse>> {
return await this.#blockWithTxsFunc(provider, block);
}
clone(): CustomBlockNetworkPlugin {
return new CustomBlockNetworkPlugin(this.#blockFunc, this.#blockWithTxsFunc);
}
}

@ -1,3 +1,4 @@
import { accessListify } from "../transaction/index.js";
import {
defineProperties,
hexlify, toQuantity,
@ -210,7 +211,7 @@ export class EtherscanProvider extends AbstractProvider {
if ((<any>{ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true })[key]) {
value = toQuantity(hexlify(value));
} else if (key === "accessList") {
value = "[" + this.network.formatter.accessList(value).map((set) => {
value = "[" + accessListify(value).map((set) => {
return `{address:"${ set.address }",storageKeys:["${ set.storageKeys.join('","') }"]}`;
}).join(",") + "]";
} else {

@ -4,10 +4,12 @@ import {
} from "../utils/index.js";
import { AbstractProvider } from "./abstract-provider.js";
import {
formatBlock, formatBlockWithTransactions, formatLog, formatTransactionReceipt,
formatTransactionResponse
} from "./format.js";
import { Network } from "./network.js"
import type { Frozen } from "../utils/index.js";
import type { PerformActionRequest } from "./abstract-provider.js";
import type { Networkish } from "./network.js"
@ -79,7 +81,7 @@ export interface FallbackProviderState extends Required<FallbackProviderConfig>
interface Config extends FallbackProviderState {
_updateNumber: null | Promise<any>;
_network: null | Frozen<Network>;
_network: null | Network;
_totalTime: number;
}
@ -132,7 +134,7 @@ type RunningState = {
// Normalizes a result to a string that can be used to compare against
// other results using normal string equality
function normalize(network: Frozen<Network>, value: any, req: PerformActionRequest): string {
function normalize(provider: AbstractProvider, value: any, req: PerformActionRequest): string {
switch (req.method) {
case "chainId":
return getBigInt(value).toString();
@ -150,19 +152,19 @@ function normalize(network: Frozen<Network>, value: any, req: PerformActionReque
return hexlify(value);
case "getBlock":
if (req.includeTransactions) {
return JSON.stringify(network.formatter.blockWithTransactions(value));
return JSON.stringify(formatBlockWithTransactions(value));
}
return JSON.stringify(network.formatter.block(value));
return JSON.stringify(formatBlock(value));
case "getTransaction":
return JSON.stringify(network.formatter.transactionResponse(value));
return JSON.stringify(formatTransactionResponse(value));
case "getTransactionReceipt":
return JSON.stringify(network.formatter.receipt(value));
return JSON.stringify(formatTransactionReceipt(value));
case "call":
return hexlify(value);
case "estimateGas":
return getBigInt(value).toString();
case "getLogs":
return JSON.stringify(value.map((v: any) => network.formatter.log(v)));
return JSON.stringify(value.map((v: any) => formatLog(v)));
}
return throwError("unsupported method", "UNSUPPORTED_OPERATION", {
@ -292,8 +294,8 @@ export class FallbackProvider extends AbstractProvider {
return this.#configs.slice();
}
async _detectNetwork(): Promise<Frozen<Network>> {
return Network.from(getBigInt(await this._perform({ method: "chainId" }))).freeze();
async _detectNetwork(): Promise<Network> {
return Network.from(getBigInt(await this._perform({ method: "chainId" })));
}
// @TODO: Add support to select providers to be the event subscriber
@ -379,7 +381,7 @@ export class FallbackProvider extends AbstractProvider {
// Check all the networks match
let chainId: null | bigint = null;
for (const config of this.#configs) {
const network = <Frozen<Network>>(config._network);
const network = <Network>(config._network);
if (chainId == null) {
chainId = network.chainId;
} else if (network.chainId !== chainId) {
@ -403,7 +405,7 @@ export class FallbackProvider extends AbstractProvider {
const result = runner.result.result;
results.push({
result,
normal: normalize(<Frozen<Network>>(runner.config._network), result, req),
normal: normalize(runner.config.provider, result, req),
weight: runner.config.weight
});
}

@ -30,7 +30,14 @@ export class IpcSocketProvider extends SocketProvider {
super(network);
this.#socket = connect(path);
this.socket.on("ready", () => { this._start(); });
this.socket.on("ready", async () => {
try {
await this._start();
} catch (error) {
console.log("failed to start IpcSocketProvider", error);
// @TODO: Now what? Restart?
}
});
let response = Buffer.alloc(0);
this.socket.on("data", (data) => {

@ -3,13 +3,13 @@
// https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json&uiSchema%5BappBar%5D%5Bui:splitView%5D=true&uiSchema%5BappBar%5D%5Bui:input%5D=false&uiSchema%5BappBar%5D%5Bui:examplesDropdown%5D=false
import { resolveAddress } from "../address/index.js";
import { getAddress, resolveAddress } from "../address/index.js";
import { TypedDataEncoder } from "../hash/index.js";
import { accessListify } from "../transaction/index.js";
import {
defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
makeError, throwArgumentError, throwError,
FetchRequest
FetchRequest, resolveProperties
} from "../utils/index.js";
import { AbstractProvider, UnmanagedSubscriber } from "./abstract-provider.js";
@ -50,6 +50,10 @@ function deepCopy<T = any>(value: T): T {
throw new Error(`should not happen: ${ value } (${ typeof(value) })`);
}
function stall(duration: number): Promise<void> {
return new Promise((resolve) => { setTimeout(resolve, duration); });
}
function getLowerCase(value: string): string {
if (value) { return value.toLowerCase(); }
return value;
@ -265,7 +269,7 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
// Try getting the transaction
const tx = await this.provider.getTransaction(hash);
if (tx != null) {
resolve(this.provider._wrapTransaction(tx, hash, blockNumber));
resolve(tx.replaceableTransaction(blockNumber));
return;
}
@ -342,6 +346,10 @@ type Payload = { payload: JsonRpcPayload, resolve: ResolveFunc, reject: RejectFu
* sub-classed.
*
* It provides the base for all JSON-RPC-based Provider interaction.
*
* Sub-classing Notes:
* - a sub-class MUST override _send
* - a sub-class MUST call the `_start()` method once connected
*/
export class JsonRpcApiProvider extends AbstractProvider {
@ -349,21 +357,35 @@ export class JsonRpcApiProvider extends AbstractProvider {
#nextId: number;
#payloads: Array<Payload>;
#ready: boolean;
#starting: null | Promise<void>;
#drainTimer: null | NodeJS.Timer;
#network: null | Network;
constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
super(network);
this.#ready = false;
this.#starting = null;
this.#nextId = 1;
this.#options = Object.assign({ }, defaultOptions, options || { });
this.#payloads = [ ];
this.#drainTimer = null;
this.#network = null;
// This could be relaxed in the future to just check equivalent networks
const staticNetwork = this._getOption("staticNetwork");
if (staticNetwork && staticNetwork !== network) {
throwArgumentError("staticNetwork MUST match network object", "options", options);
if (staticNetwork) {
if (staticNetwork !== network) {
throwArgumentError("staticNetwork MUST match network object", "options", options);
}
this.#network = staticNetwork;
}
}
@ -376,8 +398,43 @@ export class JsonRpcApiProvider extends AbstractProvider {
return this.#options[key];
}
get _network(): Network {
if (!this.#network) {
throwError("network is not available yet", "NETWORK_ERROR");
}
return this.#network;
}
get ready(): boolean { return this.#ready; }
async _start(): Promise<void> {
if (this.#ready) { return; }
if (this.#starting) { return this.#starting; }
this.#starting = (async () => {
// Bootstrap the network
if (this.#network == null) {
try {
this.#network = await this._detectNetwork();
} catch (error) {
console.log("JsonRpcProvider failed to startup; retry in 1s");
await stall(1000);
this.#starting = null;
}
}
this.#ready = true;
this.#starting = null;
// Start dispatching requests
this.#scheduleDrain();
})();
}
#scheduleDrain(): void {
if (this.#drainTimer) { return; }
if (this.#drainTimer || !this.ready) { return; }
// If we aren't using batching, no hard in sending it immeidately
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0: this._getOption("batchStallTime");
@ -446,7 +503,6 @@ export class JsonRpcApiProvider extends AbstractProvider {
}, stallTime);
}
// Sub-classes should **NOT** override this
/**
* Requests the %%method%% with %%params%% via the JSON-RPC protocol
* over the underlying channel. This can be used to call methods
@ -511,12 +567,15 @@ export class JsonRpcApiProvider extends AbstractProvider {
return new JsonRpcSigner(this, accounts[address]);
}
const [ network, accounts ] = await Promise.all([ this.getNetwork(), accountsPromise ]);
const { accounts } = await resolveProperties({
network: this.getNetwork(),
accounts: accountsPromise
});
// Account address
address = network.formatter.address(address);
address = getAddress(address);
for (const account of accounts) {
if (network.formatter.address(account) === account) {
if (getAddress(account) === account) {
return new JsonRpcSigner(this, account);
}
}
@ -524,14 +583,35 @@ export class JsonRpcApiProvider extends AbstractProvider {
throw new Error("invalid account");
}
// Sub-classes can override this; it detects the *actual* network we
// are connected to
/** Sub-classes can override this; it detects the *actual* network that
* we are **currently** connected to.
*
* Keep in mind that [[send]] may only be used once [[ready]].
*/
async _detectNetwork(): Promise<Network> {
// We have a static network (like INFURA)
const network = this._getOption("staticNetwork");
if (network) { return network; }
return Network.from(getBigInt(await this._perform({ method: "chainId" })));
// If we are ready, use ``send``, which enabled requests to be batched
if (this.ready) {
return Network.from(getBigInt(await this.send("eth_chainId", [ ])));
}
// We are not ready yet; use the primitive _send
const payload: JsonRpcPayload = {
id: this.#nextId++, method: "eth_chainId", params: [ ], jsonrpc: "2.0"
};
this.emit("debug", { action: "sendRpcPayload", payload });
const result = (await this._send(payload))[0];
this.emit("debug", { action: "receiveRpcResult", result });
if ("result" in result) {
return Network.from(getBigInt(result.result));
}
throw this.getRpcError(payload, result);
}
/**
@ -817,6 +897,15 @@ export class JsonRpcProvider extends JsonRpcApiProvider {
this.#pollingInterval = 4000;
}
async send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
// All requests are over HTTP, so we can just start handling requests
// We do this here rather than the constructor so that we don't send any
// requests to the network until we absolutely have to.
await this._start();
return await super.send(method, params);
}
async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult>> {
// Configure a POST connection for the requested method
const request = this.#connect.clone();

@ -14,7 +14,6 @@ import { assertArgument, makeError, throwError } from "../utils/index.js";
import { JsonRpcApiProvider } from "./provider-jsonrpc.js";
import type { Subscriber, Subscription } from "./abstract-provider.js";
import type { Formatter } from "./formatter.js";
import type { EventFilter } from "./provider.js";
import type { JsonRpcError, JsonRpcPayload, JsonRpcResult } from "./provider-jsonrpc.js";
import type { Networkish } from "./network.js";
@ -124,25 +123,19 @@ export class SocketEventSubscriber extends SocketSubscriber {
#logFilter: string;
get logFilter(): EventFilter { return JSON.parse(this.#logFilter); }
#formatter: Promise<Readonly<Formatter>>;
constructor(provider: SocketProvider, filter: EventFilter) {
super(provider, [ "logs", filter ]);
this.#logFilter = JSON.stringify(filter);
this.#formatter = provider.getNetwork().then((network) => network.formatter);
}
async _emit(provider: SocketProvider, message: any): Promise<void> {
const formatter = await this.#formatter;
provider.emit(this.#logFilter, formatter.log(message, provider));
provider.emit(this.#logFilter, provider._wrapLog(message, provider._network));
}
}
export class SocketProvider extends JsonRpcApiProvider {
#callbacks: Map<number, { payload: JsonRpcPayload, resolve: (r: any) => void, reject: (e: Error) => void }>;
#ready: boolean;
// Maps each filterId to its subscriber
#subs: Map<number | string, SocketSubscriber>;
@ -153,11 +146,20 @@ export class SocketProvider extends JsonRpcApiProvider {
constructor(network?: Networkish) {
super(network, { batchMaxCount: 1 });
this.#callbacks = new Map();
this.#ready = false;
this.#subs = new Map();
this.#pending = new Map();
}
// This value is only valid after _start has been called
/*
get _network(): Network {
if (this.#network == null) {
throw new Error("this shouldn't happen");
}
return this.#network.clone();
}
*/
_getSubscriber(sub: Subscription): Subscriber {
switch (sub.type) {
case "close":
@ -198,21 +200,25 @@ export class SocketProvider extends JsonRpcApiProvider {
this.#callbacks.set(payload.id, { payload, resolve, reject });
});
if (this.#ready) {
await this._write(JSON.stringify(payload));
}
await this._write(JSON.stringify(payload));
return <Array<JsonRpcResult | JsonRpcError>>[ await promise ];
}
// Sub-classes must call this once they are connected
/*
async _start(): Promise<void> {
if (this.#ready) { return; }
this.#ready = true;
for (const { payload } of this.#callbacks.values()) {
await this._write(JSON.stringify(payload));
}
this.#ready = (async function() {
await super._start();
})();
}
*/
// Sub-classes must call this for each message
async _processMessage(message: string): Promise<void> {

@ -31,8 +31,13 @@ export class WebSocketProvider extends SocketProvider {
this.#websocket = url;
}
this.websocket.onopen = () => {
this._start();
this.websocket.onopen = async () => {
try {
await this._start()
} catch (error) {
console.log("failed to start WebsocketProvider", error);
// @TODO: now what? Attempt reconnect?
}
};
this.websocket.onmessage = (message: { data: string }) => {

@ -1,11 +1,12 @@
//import { resolveAddress } from "@ethersproject/address";
import {
defineProperties, getBigInt, getNumber, hexlify, throwError
defineProperties, getBigInt, getNumber, hexlify, resolveProperties,
assertArgument, isError, makeError, throwError
} from "../utils/index.js";
import { accessListify } from "../transaction/index.js";
import type { AddressLike, NameResolver } from "../address/index.js";
import type { BigNumberish, EventEmitterable, Frozen, Listener } from "../utils/index.js";
import type { BigNumberish, EventEmitterable } from "../utils/index.js";
import type { Signature } from "../crypto/index.js";
import type { AccessList, AccessListish, TransactionLike } from "../transaction/index.js";
@ -13,6 +14,8 @@ import type { ContractRunner } from "./contracts.js";
import type { Network } from "./network.js";
const BN_0 = BigInt(0);
export type BlockTag = number | string;
// -----------------------
@ -55,7 +58,6 @@ export class FeeData {
}
export interface TransactionRequest {
type?: null | number;
@ -78,17 +80,15 @@ export interface TransactionRequest {
customData?: any;
// Only meaningful when used for call
blockTag?: BlockTag;
enableCcipRead?: boolean;
// Todo?
//gasMultiplier?: number;
};
export interface CallRequest extends TransactionRequest {
blockTag?: BlockTag;
enableCcipRead?: boolean;
}
export interface PreparedRequest {
export interface PreparedTransactionRequest {
type?: number;
to?: AddressLike;
@ -114,7 +114,7 @@ export interface PreparedRequest {
enableCcipRead?: boolean;
}
export function copyRequest(req: CallRequest): PreparedRequest {
export function copyRequest(req: TransactionRequest): PreparedTransactionRequest {
const result: any = { };
// These could be addresses, ENS names or Addressables
@ -176,7 +176,7 @@ export async function resolveTransactionRequest(tx: TransactionRequest, provider
//////////////////////
// Block
export interface BlockParams<T extends string | TransactionResponse> {
export interface BlockParams<T extends string | TransactionResponseParams> {
hash?: null | string;
number: number;
@ -228,8 +228,7 @@ export class Block<T extends string | TransactionResponse> implements BlockParam
readonly #transactions: ReadonlyArray<T>;
constructor(block: BlockParams<T>, provider?: null | Provider) {
if (provider == null) { provider = dummyProvider; }
constructor(block: BlockParams<T>, provider: Provider) {
this.#transactions = Object.freeze(block.transactions.map((tx) => {
if (typeof(tx) !== "string" && tx.provider !== provider) {
@ -363,8 +362,7 @@ export class Log implements LogParams {
readonly transactionIndex!: number;
constructor(log: LogParams, provider?: null | Provider) {
if (provider == null) { provider = dummyProvider; }
constructor(log: LogParams, provider: Provider) {
this.provider = provider;
const topics = Object.freeze(log.topics.slice());
@ -486,8 +484,7 @@ export class TransactionReceipt implements TransactionReceiptParams, Iterable<Lo
readonly #logs: ReadonlyArray<Log>;
constructor(tx: TransactionReceiptParams, provider?: null | Provider) {
if (provider == null) { provider = dummyProvider; }
constructor(tx: TransactionReceiptParams, provider: Provider) {
this.#logs = Object.freeze(tx.logs.map((log) => {
if (provider !== log.provider) {
//return log.connect(provider);
@ -635,7 +632,14 @@ export interface MinedTransactionResponse extends TransactionResponse {
date: Date;
}
export type ReplacementDetectionSetup = {
to: string;
from: string;
value: bigint;
data: string;
nonce: number;
block: number;
};
export class TransactionResponse implements TransactionLike<string>, TransactionResponseParams {
readonly provider: Provider;
@ -669,8 +673,9 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
readonly accessList!: null | AccessList;
constructor(tx: TransactionResponseParams, provider?: null | Provider) {
if (provider == null) { provider = dummyProvider; }
#startBlock: number;
constructor(tx: TransactionResponseParams, provider: Provider) {
this.provider = provider;
this.blockNumber = (tx.blockNumber != null) ? tx.blockNumber: null;
@ -697,11 +702,9 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
this.signature = tx.signature;
this.accessList = (tx.accessList != null) ? tx.accessList: null;
}
//connect(provider: Provider): TransactionResponse {
// return new TransactionResponse(this, provider);
//}
this.#startBlock = -1;
}
toJSON(): any {
const {
@ -740,8 +743,155 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
return this.provider.getTransaction(this.hash);
}
async wait(confirms?: number): Promise<null | TransactionReceipt> {
return this.provider.waitForTransaction(this.hash, confirms);
async wait(_confirms?: number, _timeout?: number): Promise<null | TransactionReceipt> {
const confirms = (_confirms == null) ? 1: _confirms;
const timeout = (_timeout == null) ? 0: _timeout;
let startBlock = this.#startBlock
let nextScan = -1;
let stopScanning = (startBlock === -1) ? true: false;
const checkReplacement = async () => {
// Get the current transaction count for this sender
if (stopScanning) { return null; }
const { blockNumber, nonce } = await resolveProperties({
blockNumber: this.provider.getBlockNumber(),
nonce: this.provider.getTransactionCount(this.from)
});
// No transaction for our nonce has been mined yet; but we can start
// scanning later when we do start
if (nonce < this.nonce) {
startBlock = blockNumber;
return;
}
// We were mined; no replacement
if (stopScanning) { return null; }
const mined = await this.getTransaction();
if (mined && mined.blockNumber != null) { return; }
// We were replaced; start scanning for that transaction
// Starting to scan; look back a few extra blocks for safety
if (nextScan === -1) {
nextScan = startBlock - 3;
if (nextScan < this.#startBlock) { nextScan = this.#startBlock; }
}
while (nextScan <= blockNumber) {
// Get the next block to scan
if (stopScanning) { return null; }
const block = await this.provider.getBlockWithTransactions(nextScan);
// This should not happen; but we'll try again shortly
if (block == null) { return; }
for (const tx of block.transactions) {
// We were mined; no replacement
if (tx.hash === this.hash) { return; }
if (tx.from === this.from && tx.nonce === this.nonce) {
// Get the receipt
if (stopScanning) { return null; }
const receipt = await this.provider.getTransactionReceipt(tx.hash);
// This should not happen; but we'll try again shortly
if (receipt == null) { return; }
// We will retry this on the next block (this case could be optimized)
if ((blockNumber - receipt.blockNumber + 1) < confirms) { return; }
// The reason we were replaced
let reason: "replaced" | "repriced" | "cancelled" = "replaced";
if (tx.data === this.data && tx.to === this.to && tx.value === this.value) {
reason = "repriced";
} else if (tx.data === "0x" && tx.from === tx.to && tx.value === BN_0) {
reason = "cancelled"
}
throwError("transaction was replaced", "TRANSACTION_REPLACED", {
cancelled: (reason === "replaced" || reason === "cancelled"),
reason,
replacement: tx.replaceableTransaction(startBlock),
hash: tx.hash,
receipt
});
}
}
nextScan++;
}
return;
};
const receipt = await this.provider.getTransactionReceipt(this.hash);
if (receipt) {
if ((await receipt.confirmations()) >= confirms) { return receipt; }
} else {
// Check for a replacement; throws if a replacement was found
await checkReplacement();
// Allow null only when the confirms is 0
if (confirms === 0) { return null; }
}
const waiter = new Promise((resolve, reject) => {
// List of things to cancel when we have a result (one way or the other)
const cancellers: Array<() => void> = [ ];
const cancel = () => { cancellers.forEach((c) => c()); };
// On cancel, stop scanning for replacements
cancellers.push(() => { stopScanning = true; });
// Set up any timeout requested
if (timeout > 0) {
const timer = setTimeout(() => {
cancel();
reject(makeError("wait for transaction timeout", "TIMEOUT"));
}, timeout);
cancellers.push(() => { clearTimeout(timer); });
}
const txListener = async (receipt: TransactionReceipt) => {
// Done; return it!
if ((await receipt.confirmations()) >= confirms) {
cancel();
resolve(receipt);
}
};
cancellers.push(() => { this.provider.off(this.hash, txListener); });
this.provider.on(this.hash, txListener);
// We support replacement detection; start checking
if (startBlock >= 0) {
const replaceListener = async () => {
try {
// Check for a replacement; this throws only if one is found
await checkReplacement();
} catch (error) {
// We were replaced (with enough confirms); re-throw the error
if (isError(error, "TRANSACTION_REPLACED")) {
cancel();
reject(error);
return;
}
}
// Rescheudle a check on the next block
this.provider.once("block", replaceListener);
};
cancellers.push(() => { this.provider.off("block", replaceListener); });
this.provider.once("block", replaceListener);
}
});
return await <Promise<TransactionReceipt>>waiter;
}
isMined(): this is MinedTransactionResponse {
@ -779,6 +929,22 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
}
return createReorderedTransactionFilter(this, other);
}
/**
* Returns a new TransactionResponse instance which has the ability to
* detect (and throw an error) if the transaction is replaced, which
* will begin scanning at %%startBlock%%.
*
* This should generally not be used by developers and is intended
* primarily for internal use. Setting an incorrect %%startBlock%% can
* have devastating performance consequences if used incorrectly.
*/
replaceableTransaction(startBlock: number): TransactionResponse {
assertArgument(Number.isInteger(startBlock) && startBlock >= 0, "invalid startBlock", "startBlock", startBlock);
const tx = new TransactionResponse(this, this.provider);
tx.#startBlock = startBlock;
return tx;
}
}
@ -881,7 +1047,7 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
/**
* Get the connected [[Network]].
*/
getNetwork(): Promise<Frozen<Network>>;
getNetwork(): Promise<Network>;
/**
* Get the best guess at the recommended [[FeeData]].
@ -957,7 +1123,7 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
*
* @param tx - The transaction to simulate
*/
call(tx: CallRequest): Promise<string>
call(tx: TransactionRequest): Promise<string>
/**
* Broadcasts the %%signedTx%% to the network, adding it to the memory pool
@ -993,96 +1159,3 @@ export interface Provider extends ContractRunner, EventEmitterable<ProviderEvent
waitForTransaction(hash: string, confirms?: number, timeout?: number): Promise<null | TransactionReceipt>;
waitForBlock(blockTag?: BlockTag): Promise<Block<string>>;
}
// @TODO: I think I can drop T
function fail<T>(): T {
throw new Error("this provider should not be used");
}
class DummyProvider implements Provider {
get provider(): this { return this; }
async getNetwork(): Promise<Frozen<Network>> { return fail<Frozen<Network>>(); }
async getFeeData(): Promise<FeeData> { return fail<FeeData>(); }
async estimateGas(tx: TransactionRequest): Promise<bigint> { return fail<bigint>(); }
async call(tx: CallRequest): Promise<string> { return fail<string>(); }
async resolveName(name: string): Promise<null | string> { return fail<null | string>(); }
// State
async getBlockNumber(): Promise<number> { return fail<number>(); }
// Account
async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
return fail<bigint>();
}
async getTransactionCount(address: AddressLike, blockTag?: BlockTag): Promise<number> {
return fail<number>();
}
async getCode(address: AddressLike, blockTag?: BlockTag): Promise<string> {
return fail<string>();
}
async getStorageAt(address: AddressLike, position: BigNumberish, blockTag?: BlockTag): Promise<string> {
return fail<string>();
}
// Write
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> { return fail<TransactionResponse>(); }
// Queries
async getBlock(blockHashOrBlockTag: BlockTag | string): Promise<null | Block<string>>{
return fail<null | Block<string>>();
}
async getBlockWithTransactions(blockHashOrBlockTag: BlockTag | string): Promise<null | Block<TransactionResponse>> {
return fail<null | Block<TransactionResponse>>();
}
async getTransaction(hash: string): Promise<null | TransactionResponse> {
return fail<null | TransactionResponse>();
}
async getTransactionReceipt(hash: string): Promise<null | TransactionReceipt> {
return fail<null | TransactionReceipt>();
}
async getTransactionResult(hash: string): Promise<null | string> {
return fail<null | string>();
}
// Bloom-filter Queries
async getLogs(filter: Filter | FilterByBlockHash): Promise<Array<Log>> {
return fail<Array<Log>>();
}
// ENS
async lookupAddress(address: string): Promise<null | string> {
return fail<null | string>();
}
async waitForTransaction(hash: string, confirms?: number, timeout?: number): Promise<null | TransactionReceipt> {
return fail<null | TransactionReceipt>();
}
async waitForBlock(blockTag?: BlockTag): Promise<Block<string>> {
return fail<Block<string>>();
}
// EventEmitterable
async on(event: ProviderEvent, listener: Listener): Promise<this> { return fail(); }
async once(event: ProviderEvent, listener: Listener): Promise<this> { return fail(); }
async emit(event: ProviderEvent, ...args: Array<any>): Promise<boolean> { return fail(); }
async listenerCount(event?: ProviderEvent): Promise<number> { return fail(); }
async listeners(event?: ProviderEvent): Promise<Array<Listener>> { return fail(); }
async off(event: ProviderEvent, listener?: Listener): Promise<this> { return fail(); }
async removeAllListeners(event?: ProviderEvent): Promise<this> { return fail(); }
async addListener(event: ProviderEvent, listener: Listener): Promise<this> { return fail(); }
async removeListener(event: ProviderEvent, listener: Listener): Promise<this> { return fail(); }
}
/**
* A singleton [[Provider]] instance that can be used as a placeholder. This
* allows API that have a Provider added later to not require a null check.
*
* All operations performed on this [[Provider]] will throw.
*/
export const dummyProvider: Provider = new DummyProvider();

@ -4,7 +4,7 @@ import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
import type { TransactionLike } from "../transaction/index.js";
import type { ContractRunner } from "./contracts.js";
import type { BlockTag, CallRequest, Provider, TransactionRequest, TransactionResponse } from "./provider.js";
import type { BlockTag, Provider, TransactionRequest, TransactionResponse } from "./provider.js";
/**
* A Signer represents an account on the Ethereum Blockchain, and is most often
@ -49,13 +49,13 @@ export interface Signer extends Addressable, ContractRunner, NameResolver {
// Preparation
/**
* Prepares a {@link CallRequest} for calling:
* Prepares a {@link TransactionRequest} for calling:
* - resolves ``to`` and ``from`` addresses
* - if ``from`` is specified , check that it matches this Signer
*
* @param tx - The call to prepare
*/
populateCall(tx: CallRequest): Promise<TransactionLike<string>>;
populateCall(tx: TransactionRequest): Promise<TransactionLike<string>>;
/**
* Prepares a {@link TransactionRequest} for sending to the network by
@ -96,7 +96,7 @@ export interface Signer extends Addressable, ContractRunner, NameResolver {
* node to take into account. In these cases, a manually determined ``gasLimit``
* will need to be made.
*/
estimateGas(tx: CallRequest): Promise<bigint>;
estimateGas(tx: TransactionRequest): Promise<bigint>;
/**
* Evaluates the //tx// by running it against the current Blockchain state. This
@ -107,7 +107,7 @@ export interface Signer extends Addressable, ContractRunner, NameResolver {
* (e.g. running a Contract's getters) or to simulate the effect of a transaction
* before actually performing an operation.
*/
call(tx: CallRequest): Promise<string>;
call(tx: TransactionRequest): Promise<string>;
/**
* Resolves an [[Address]] or ENS Name to an [[Address]].

@ -1,7 +1,5 @@
import { PollingEventSubscriber } from "./subscriber-polling.js";
import type { Frozen } from "../utils/index.js";
import type { AbstractProvider, Subscriber } from "./abstract-provider.js";
import type { Network } from "./network.js";
import type { EventFilter } from "./provider.js";
@ -18,7 +16,7 @@ export class FilterIdSubscriber implements Subscriber {
#filterIdPromise: null | Promise<string>;
#poller: (b: number) => Promise<void>;
#network: null | Frozen<Network>;
#network: null | Network;
constructor(provider: JsonRpcApiProvider) {
this.#provider = provider;
@ -111,10 +109,8 @@ export class FilterIdEventSubscriber extends FilterIdSubscriber {
}
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
const network = await provider.getNetwork();
for (const result of results) {
const log = network.formatter.log(result, provider);
provider.emit(this.#event, log);
provider.emit(this.#event, provider._wrapLog(result, provider._network));
}
}
}
@ -125,9 +121,8 @@ export class FilterIdPendingSubscriber extends FilterIdSubscriber {
}
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
const network = await provider.getNetwork();
for (const result of results) {
provider.emit("pending", network.formatter.hash(result));
provider.emit("pending", result);
}
}
}