Refactored JsonRpcApiProvider and the Provider model.
This commit is contained in:
parent
40fad8bbc0
commit
ee06989ba3
@ -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
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,22 +357,36 @@ 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) {
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user