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 tx = await runner.sendTransaction(await this.populateTransaction(...args));
|
||||||
const provider = getProvider(this._contract.runner);
|
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> {
|
async estimateGas(...args: ContractMethodArgs<A>): Promise<bigint> {
|
||||||
@ -491,7 +493,9 @@ export class BaseContract implements Addressable, EventEmitterable<ContractEvent
|
|||||||
let deployTx: null | ContractTransactionResponse = null;
|
let deployTx: null | ContractTransactionResponse = null;
|
||||||
if (_deployTx) {
|
if (_deployTx) {
|
||||||
const provider = getProvider(runner);
|
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();
|
let subs = new Map();
|
||||||
|
@ -2,7 +2,7 @@ import type {
|
|||||||
EventFragment, FunctionFragment, Result, Typed
|
EventFragment, FunctionFragment, Result, Typed
|
||||||
} from "../abi/index.js";
|
} from "../abi/index.js";
|
||||||
import type {
|
import type {
|
||||||
CallRequest, PreparedRequest, TopicFilter
|
TransactionRequest, PreparedTransactionRequest, TopicFilter
|
||||||
} from "../providers/index.js";
|
} from "../providers/index.js";
|
||||||
|
|
||||||
import type { ContractTransactionResponse } from "./wrappers.js";
|
import type { ContractTransactionResponse } from "./wrappers.js";
|
||||||
@ -20,7 +20,7 @@ export interface DeferredTopicFilter {
|
|||||||
fragment: EventFragment;
|
fragment: EventFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractTransaction extends PreparedRequest {
|
export interface ContractTransaction extends PreparedTransactionRequest {
|
||||||
// These are populated by contract methods and cannot bu null
|
// These are populated by contract methods and cannot bu null
|
||||||
to: string;
|
to: string;
|
||||||
data: string;
|
data: string;
|
||||||
@ -30,7 +30,7 @@ export interface ContractTransaction extends PreparedRequest {
|
|||||||
export interface ContractDeployTransaction extends Omit<ContractTransaction, "to"> { }
|
export interface ContractDeployTransaction extends Omit<ContractTransaction, "to"> { }
|
||||||
|
|
||||||
// Overrides; cannot override `to` or `data` as Contract populates these
|
// 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
|
// Arguments for methods; with an optional (n+1)th Override
|
||||||
|
@ -31,7 +31,7 @@ export class EventLog extends Log {
|
|||||||
export class ContractTransactionReceipt extends TransactionReceipt {
|
export class ContractTransactionReceipt extends TransactionReceipt {
|
||||||
readonly #interface: Interface;
|
readonly #interface: Interface;
|
||||||
|
|
||||||
constructor(iface: Interface, provider: null | Provider, tx: TransactionReceipt) {
|
constructor(iface: Interface, provider: Provider, tx: TransactionReceipt) {
|
||||||
super(tx, provider);
|
super(tx, provider);
|
||||||
this.#interface = iface;
|
this.#interface = iface;
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ export class ContractTransactionReceipt extends TransactionReceipt {
|
|||||||
export class ContractTransactionResponse extends TransactionResponse {
|
export class ContractTransactionResponse extends TransactionResponse {
|
||||||
readonly #interface: Interface;
|
readonly #interface: Interface;
|
||||||
|
|
||||||
constructor(iface: Interface, provider: null | Provider, tx: TransactionResponse) {
|
constructor(iface: Interface, provider: Provider, tx: TransactionResponse) {
|
||||||
super(tx, provider);
|
super(tx, provider);
|
||||||
this.#interface = iface;
|
this.#interface = iface;
|
||||||
}
|
}
|
||||||
|
@ -18,22 +18,26 @@ import {
|
|||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { EnsResolver } from "./ens-resolver.js";
|
import { EnsResolver } from "./ens-resolver.js";
|
||||||
|
import {
|
||||||
|
formatBlock, formatBlockWithTransactions, formatLog, formatTransactionReceipt,
|
||||||
|
formatTransactionResponse
|
||||||
|
} from "./format.js";
|
||||||
import { Network } from "./network.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 {
|
import {
|
||||||
PollingBlockSubscriber, PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber
|
PollingBlockSubscriber, PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber
|
||||||
} from "./subscriber-polling.js";
|
} from "./subscriber-polling.js";
|
||||||
|
|
||||||
import type { Addressable, AddressLike } from "../address/index.js";
|
import type { Addressable, AddressLike } from "../address/index.js";
|
||||||
import type { BigNumberish, BytesLike } from "../utils/index.js";
|
import type { BigNumberish, BytesLike } from "../utils/index.js";
|
||||||
import type { Frozen, Listener } from "../utils/index.js";
|
import type { Listener } from "../utils/index.js";
|
||||||
import type { AccessList } from "../transaction/index.js";
|
|
||||||
|
|
||||||
import type { Networkish } from "./network.js";
|
import type { Networkish } from "./network.js";
|
||||||
//import type { MaxPriorityFeePlugin } from "./plugins-network.js";
|
//import type { MaxPriorityFeePlugin } from "./plugins-network.js";
|
||||||
import type {
|
import type {
|
||||||
BlockTag, CallRequest, EventFilter, Filter, FilterByBlockHash,
|
BlockParams, BlockTag, EventFilter, Filter, FilterByBlockHash, LogParams, OrphanFilter,
|
||||||
LogParams, OrphanFilter, Provider, ProviderEvent, TransactionRequest,
|
PreparedTransactionRequest, Provider, ProviderEvent,
|
||||||
|
TransactionReceiptParams, TransactionRequest, TransactionResponseParams
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
|
|
||||||
|
|
||||||
@ -42,6 +46,9 @@ const BN_2 = BigInt(2);
|
|||||||
|
|
||||||
const MAX_CCIP_REDIRECTS = 10;
|
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 {
|
function getTag(prefix: string, value: any): string {
|
||||||
return prefix + ":" + JSON.stringify(value, (k, v) => {
|
return prefix + ":" + JSON.stringify(value, (k, v) => {
|
||||||
@ -124,10 +131,6 @@ function concisify(items: Array<string>): Array<string> {
|
|||||||
return items;
|
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> {
|
async function getSubscription(_event: ProviderEvent, provider: AbstractProvider): Promise<Subscription> {
|
||||||
if (_event == null) { throw new Error("invalid event"); }
|
if (_event == null) { throw new Error("invalid event"); }
|
||||||
@ -215,26 +218,10 @@ export type PerformActionFilter = {
|
|||||||
blockHash?: string;
|
blockHash?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PerformActionTransaction = {
|
export interface PerformActionTransaction extends PreparedTransactionRequest {
|
||||||
type?: number;
|
|
||||||
|
|
||||||
to?: string;
|
to?: string;
|
||||||
from?: string;
|
from?: string;
|
||||||
|
}
|
||||||
nonce?: number;
|
|
||||||
|
|
||||||
gasLimit?: bigint;
|
|
||||||
gasPrice?: bigint;
|
|
||||||
|
|
||||||
maxPriorityFeePerGas?: bigint;
|
|
||||||
maxFeePerGas?: bigint;
|
|
||||||
|
|
||||||
data?: string;
|
|
||||||
value?: bigint;
|
|
||||||
chainId?: bigint;
|
|
||||||
|
|
||||||
accessList?: AccessList;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PerformActionRequest = {
|
export type PerformActionRequest = {
|
||||||
method: "call",
|
method: "call",
|
||||||
@ -289,11 +276,6 @@ type _PerformAccountRequest = {
|
|||||||
method: "getStorageAt", position: bigint
|
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 = {
|
type CcipArgs = {
|
||||||
sender: string;
|
sender: string;
|
||||||
urls: Array<string>;
|
urls: Array<string>;
|
||||||
@ -304,7 +286,6 @@ type CcipArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class AbstractProvider implements Provider {
|
export class AbstractProvider implements Provider {
|
||||||
|
|
||||||
#subs: Map<string, Sub>;
|
#subs: Map<string, Sub>;
|
||||||
@ -313,11 +294,14 @@ export class AbstractProvider implements Provider {
|
|||||||
// null=unpaused, true=paused+dropWhilePaused, false=paused
|
// null=unpaused, true=paused+dropWhilePaused, false=paused
|
||||||
#pausedState: null | boolean;
|
#pausedState: null | boolean;
|
||||||
|
|
||||||
#networkPromise: null | Promise<Frozen<Network>>;
|
#networkPromise: null | Promise<Network>;
|
||||||
readonly #anyNetwork: boolean;
|
readonly #anyNetwork: boolean;
|
||||||
|
|
||||||
#performCache: Map<string, Promise<any>>;
|
#performCache: Map<string, Promise<any>>;
|
||||||
|
|
||||||
|
// The most recent block number if running an event or -1 if no "block" event
|
||||||
|
#lastBlockNumber: number;
|
||||||
|
|
||||||
#nextTimer: number;
|
#nextTimer: number;
|
||||||
#timers: Map<number, { timer: null | NodeJS.Timer, func: () => void, time: 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
|
// @TODO: This should be a () => Promise<Network> so network can be
|
||||||
// done when needed; or rely entirely on _detectNetwork?
|
// done when needed; or rely entirely on _detectNetwork?
|
||||||
constructor(_network?: "any" | Networkish) {
|
constructor(_network?: "any" | Networkish) {
|
||||||
|
|
||||||
if (_network === "any") {
|
if (_network === "any") {
|
||||||
this.#anyNetwork = true;
|
this.#anyNetwork = true;
|
||||||
this.#networkPromise = null;
|
this.#networkPromise = null;
|
||||||
@ -339,6 +324,8 @@ export class AbstractProvider implements Provider {
|
|||||||
this.#networkPromise = null;
|
this.#networkPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#lastBlockNumber = -1;
|
||||||
|
|
||||||
this.#performCache = new Map();
|
this.#performCache = new Map();
|
||||||
|
|
||||||
this.#subs = new Map();
|
this.#subs = new Map();
|
||||||
@ -445,11 +432,27 @@ export class AbstractProvider implements Provider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_wrapTransaction(tx: TransactionResponse, hash: string, blockNumber: number): TransactionResponse {
|
_wrapBlock(value: BlockParams<string>, network: Network): Block<string> {
|
||||||
return tx;
|
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", {
|
return throwError("sub-classes must implement this", "UNSUPPORTED_OPERATION", {
|
||||||
operation: "_detectNetwork"
|
operation: "_detectNetwork"
|
||||||
});
|
});
|
||||||
@ -466,21 +469,13 @@ export class AbstractProvider implements Provider {
|
|||||||
|
|
||||||
// State
|
// State
|
||||||
async getBlockNumber(): Promise<number> {
|
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> {
|
_getAddress(address: AddressLike): string | Promise<string> {
|
||||||
return resolveAddress(address, this);
|
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> {
|
_getBlockTag(blockTag?: BlockTag): string | Promise<string> {
|
||||||
@ -500,276 +495,13 @@ export class AbstractProvider implements Provider {
|
|||||||
|
|
||||||
if (typeof(blockTag) === "number") {
|
if (typeof(blockTag) === "number") {
|
||||||
if (blockTag >= 0) { return toQuantity(blockTag); }
|
if (blockTag >= 0) { return toQuantity(blockTag); }
|
||||||
|
if (this.#lastBlockNumber >= 0) { return toQuantity(this.#lastBlockNumber + blockTag); }
|
||||||
return this.getBlockNumber().then((b) => toQuantity(b + blockTag));
|
return this.getBlockNumber().then((b) => toQuantity(b + blockTag));
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwArgumentError("invalid blockTag", "blockTag", 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> {
|
_getFilter(filter: Filter | FilterByBlockHash): PerformActionFilter | Promise<PerformActionFilter> {
|
||||||
|
|
||||||
// Create a canonical representation of the topics
|
// Create a canonical representation of the topics
|
||||||
@ -839,16 +571,327 @@ export class AbstractProvider implements Provider {
|
|||||||
return resolve(<Array<string>>address, fromBlock, toBlock);
|
return resolve(<Array<string>>address, fromBlock, toBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloom-filter Queries
|
_getTransactionRequest(_request: TransactionRequest): PerformActionTransaction | Promise<PerformActionTransaction> {
|
||||||
async getLogs(_filter: Filter | FilterByBlockHash): Promise<Array<Log>> {
|
const request = <PerformActionTransaction>copyRequest(_request);
|
||||||
const { network, filter } = await resolveProperties({
|
|
||||||
network: this.getNetwork(),
|
const promises: Array<Promise<void>> = [ ];
|
||||||
filter: this._getFilter(_filter)
|
[ "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) => {
|
if (request.blockTag != null) {
|
||||||
return network.formatter.log(l, this);
|
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
|
// ENS
|
||||||
@ -1153,6 +1196,8 @@ export class AbstractProvider implements Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pause(dropWhilePaused?: boolean): void {
|
pause(dropWhilePaused?: boolean): void {
|
||||||
|
this.#lastBlockNumber = -1;
|
||||||
|
|
||||||
if (this.#pausedState != null) {
|
if (this.#pausedState != null) {
|
||||||
if (this.#pausedState == !!dropWhilePaused) { return; }
|
if (this.#pausedState == !!dropWhilePaused) { return; }
|
||||||
return throwError("cannot change pause type; resume first", "UNSUPPORTED_OPERATION", {
|
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 { TransactionLike } from "../transaction/index.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BlockTag, CallRequest, Provider, TransactionRequest, TransactionResponse
|
BlockTag, Provider, TransactionRequest, TransactionResponse
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
import type { Signer } from "./signer.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);
|
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);
|
const provider = this.#checkProvider(op);
|
||||||
|
|
||||||
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
|
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
|
||||||
@ -63,7 +63,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
|||||||
return pop;
|
return pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
async populateCall(tx: CallRequest): Promise<TransactionLike<string>> {
|
async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||||
const pop = await this.#populate("populateCall", tx);
|
const pop = await this.#populate("populateCall", tx);
|
||||||
|
|
||||||
return pop;
|
return pop;
|
||||||
@ -98,11 +98,11 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
|||||||
return await resolveProperties(pop);
|
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));
|
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));
|
return this.#checkProvider("call").call(await this.populateCall(tx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ export class WrappedSigner extends AbstractSigner {
|
|||||||
return await this.#signer.getNonce(blockTag);
|
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);
|
return await this.#signer.populateCall(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +184,11 @@ export class WrappedSigner extends AbstractSigner {
|
|||||||
return await this.#signer.populateTransaction(tx);
|
return await this.#signer.populateTransaction(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async estimateGas(tx: CallRequest): Promise<bigint> {
|
async estimateGas(tx: TransactionRequest): Promise<bigint> {
|
||||||
return await this.#signer.estimateGas(tx);
|
return await this.#signer.estimateGas(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async call(tx: CallRequest): Promise<string> {
|
async call(tx: TransactionRequest): Promise<string> {
|
||||||
return await this.#signer.call(tx);
|
return await this.#signer.call(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CallRequest, Provider, TransactionRequest, TransactionResponse
|
Provider, TransactionRequest, TransactionResponse
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
|
|
||||||
// The object that will be used to run Contracts. The Signer and Provider
|
// 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>;
|
estimateGas?: (tx: TransactionRequest) => Promise<bigint>;
|
||||||
|
|
||||||
// Required for pure, view or static calls to contracts; usually a Signer or Provider
|
// 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
|
// Required to support ENS names; usually a Signer or Provider
|
||||||
resolveName?: (name: string) => Promise<null | string>;
|
resolveName?: (name: string) => Promise<null | string>;
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getAddress } from "../address/index.js";
|
||||||
import { ZeroHash } from "../constants/hashes.js";
|
import { ZeroHash } from "../constants/hashes.js";
|
||||||
import { dnsEncode, namehash } from "../hash/index.js";
|
import { dnsEncode, namehash } from "../hash/index.js";
|
||||||
import {
|
import {
|
||||||
@ -12,7 +13,7 @@ import type { BigNumberish, BytesLike, EthersError } from "../utils/index.js";
|
|||||||
|
|
||||||
import type { AbstractProvider, ProviderPlugin } from "./abstract-provider.js";
|
import type { AbstractProvider, ProviderPlugin } from "./abstract-provider.js";
|
||||||
import type { EnsPlugin } from "./plugins-network.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);
|
const BN_1 = BigInt(1);
|
||||||
|
|
||||||
@ -79,6 +80,13 @@ function encodeBytes(datas: Array<BytesLike>) {
|
|||||||
return concat(result);
|
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
|
// @TODO: This should use the fetch-data:ipfs gateway
|
||||||
// Trim off the ipfs:// prefix and return the default gateway URL
|
// Trim off the ipfs:// prefix and return the default gateway URL
|
||||||
function getIpfsLink(link: string): string {
|
function getIpfsLink(link: string): string {
|
||||||
@ -187,7 +195,7 @@ export class EnsResolver {
|
|||||||
|
|
||||||
// e.g. keccak256("addr(bytes32,uint256)")
|
// e.g. keccak256("addr(bytes32,uint256)")
|
||||||
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
||||||
const tx: CallRequest = {
|
const tx: TransactionRequest = {
|
||||||
to: this.address,
|
to: this.address,
|
||||||
enableCcipRead: true,
|
enableCcipRead: true,
|
||||||
data: addrData
|
data: addrData
|
||||||
@ -225,10 +233,9 @@ export class EnsResolver {
|
|||||||
const result = await this._fetch("0x3b3b57de");
|
const result = await this._fetch("0x3b3b57de");
|
||||||
|
|
||||||
// No address
|
// 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 callAddress(result);
|
||||||
return network.formatter.callAddress(result);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if ((error as EthersError).code === "CALL_EXCEPTION") { return null; }
|
if ((error as EthersError).code === "CALL_EXCEPTION") { return null; }
|
||||||
throw error;
|
throw error;
|
||||||
@ -359,15 +366,13 @@ export class EnsResolver {
|
|||||||
throw new Error("!caip");
|
throw new Error("!caip");
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatter = (await this.provider.getNetwork()).formatter;
|
const addr = getAddress(comps[0]);
|
||||||
|
|
||||||
const addr = formatter.address(comps[0]);
|
|
||||||
const tokenId = numPad(comps[1]);
|
const tokenId = numPad(comps[1]);
|
||||||
|
|
||||||
// Check that this account owns the token
|
// Check that this account owns the token
|
||||||
if (scheme === "erc721") {
|
if (scheme === "erc721") {
|
||||||
// ownerOf(uint256 tokenId)
|
// ownerOf(uint256 tokenId)
|
||||||
const tokenOwner = formatter.callAddress(await this.provider.call({
|
const tokenOwner = callAddress(await this.provider.call({
|
||||||
to: addr, data: concat([ "0x6352211e", tokenId ])
|
to: addr, data: concat([ "0x6352211e", tokenId ])
|
||||||
}));
|
}));
|
||||||
if (owner !== tokenOwner) {
|
if (owner !== tokenOwner) {
|
||||||
@ -493,7 +498,7 @@ export class EnsResolver {
|
|||||||
enableCcipRead: true
|
enableCcipRead: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const addr = network.formatter.callAddress(addrData);
|
const addr = callAddress(addrData);
|
||||||
if (addr === dataSlice(ZeroHash, 0, 20)) { return null; }
|
if (addr === dataSlice(ZeroHash, 0, 20)) { return null; }
|
||||||
return addr;
|
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
|
* Network object this allows exotic (non-Ethereum) networks
|
||||||
* to be fairly simple to adapt to ethers.
|
* to be fairly simple to adapt to ethers.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
import { getAddress, getCreateAddress } from "../address/index.js";
|
import { getAddress, getCreateAddress } from "../address/index.js";
|
||||||
import {
|
import {
|
||||||
dataLength, dataSlice, getBigInt, getNumber, isHexString, toQuantity,
|
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 { EnsResolver } from "./ens-resolver.js";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Formatter } from "./formatter.js";
|
|
||||||
|
|
||||||
export { Network } from "./common-networks.js";
|
export { Network } from "./common-networks.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -42,8 +40,6 @@ export {
|
|||||||
TransactionReceipt,
|
TransactionReceipt,
|
||||||
TransactionResponse,
|
TransactionResponse,
|
||||||
|
|
||||||
dummyProvider,
|
|
||||||
|
|
||||||
copyRequest,
|
copyRequest,
|
||||||
//resolveTransactionRequest,
|
//resolveTransactionRequest,
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
@ -83,15 +79,13 @@ export type {
|
|||||||
AvatarLinkageType, AvatarLinkage, AvatarResult
|
AvatarLinkageType, AvatarLinkage, AvatarResult
|
||||||
} from "./ens-resolver.js";
|
} from "./ens-resolver.js";
|
||||||
*/
|
*/
|
||||||
export type { FormatFunc } from "./formatter.js";
|
|
||||||
|
|
||||||
export type { Networkish } from "./network.js";
|
export type { Networkish } from "./network.js";
|
||||||
|
|
||||||
export type { GasCostParameters } from "./plugins-network.js";
|
export type { GasCostParameters } from "./plugins-network.js";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
BlockTag,
|
BlockTag,
|
||||||
CallRequest, TransactionRequest, PreparedRequest,
|
TransactionRequest, PreparedTransactionRequest,
|
||||||
EventFilter, Filter, FilterByBlockHash, OrphanFilter, ProviderEvent, TopicFilter,
|
EventFilter, Filter, FilterByBlockHash, OrphanFilter, ProviderEvent, TopicFilter,
|
||||||
Provider,
|
Provider,
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
|
import { accessListify } from "../transaction/index.js";
|
||||||
import {
|
import {
|
||||||
getStore, getBigInt, setStore, throwArgumentError
|
getStore, getBigInt, setStore, throwArgumentError
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { Formatter } from "./formatter.js";
|
|
||||||
import { EnsPlugin, GasCostPlugin } from "./plugins-network.js";
|
import { EnsPlugin, GasCostPlugin } from "./plugins-network.js";
|
||||||
|
|
||||||
import type { BigNumberish } from "../utils/index.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 { TransactionLike } from "../transaction/index.js";
|
||||||
|
|
||||||
import type { NetworkPlugin } from "./plugins-network.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 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: {
|
#props: {
|
||||||
name: string,
|
name: string,
|
||||||
chainId: bigint,
|
chainId: bigint,
|
||||||
|
|
||||||
formatter: Formatter,
|
|
||||||
|
|
||||||
plugins: Map<string, NetworkPlugin>
|
plugins: Map<string, NetworkPlugin>
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(name: string, _chainId: BigNumberish, formatter?: Formatter) {
|
constructor(name: string, _chainId: BigNumberish) {
|
||||||
const chainId = getBigInt(_chainId);
|
const chainId = getBigInt(_chainId);
|
||||||
if (formatter == null) { formatter = defaultFormatter; }
|
|
||||||
const plugins = new Map();
|
const plugins = new Map();
|
||||||
this.#props = { name, chainId, formatter, plugins };
|
this.#props = { name, chainId, plugins };
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): any {
|
||||||
@ -113,19 +109,15 @@ export class Network implements Freezable<Network> {
|
|||||||
get chainId(): bigint { return getStore(this.#props, "chainId"); }
|
get chainId(): bigint { return getStore(this.#props, "chainId"); }
|
||||||
set chainId(value: BigNumberish) { setStore(this.#props, "chainId", getBigInt(value, "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> {
|
get plugins(): Array<NetworkPlugin> {
|
||||||
return Array.from(this.#props.plugins.values());
|
return Array.from(this.#props.plugins.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
attachPlugin(plugin: NetworkPlugin): this {
|
attachPlugin(plugin: NetworkPlugin): this {
|
||||||
if (this.isFrozen()) { throw new Error("frozen"); }
|
|
||||||
if (this.#props.plugins.get(plugin.name)) {
|
if (this.#props.plugins.get(plugin.name)) {
|
||||||
throw new Error(`cannot replace existing plugin: ${ 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,13 +131,13 @@ export class Network implements Freezable<Network> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone(): 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) => {
|
this.plugins.forEach((plugin) => {
|
||||||
clone.attachPlugin(plugin.clone());
|
clone.attachPlugin(plugin.clone());
|
||||||
});
|
});
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
freeze(): Frozen<Network> {
|
freeze(): Frozen<Network> {
|
||||||
Object.freeze(this.#props);
|
Object.freeze(this.#props);
|
||||||
return this;
|
return this;
|
||||||
@ -154,7 +146,7 @@ export class Network implements Freezable<Network> {
|
|||||||
isFrozen(): boolean {
|
isFrozen(): boolean {
|
||||||
return Object.isFrozen(this.#props);
|
return Object.isFrozen(this.#props);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
computeIntrinsicGas(tx: TransactionLike): number {
|
computeIntrinsicGas(tx: TransactionLike): number {
|
||||||
const costs = this.getPlugin<GasCostPlugin>("org.ethers.gas-cost") || (new GasCostPlugin());
|
const costs = this.getPlugin<GasCostPlugin>("org.ethers.gas-cost") || (new GasCostPlugin());
|
||||||
|
|
||||||
@ -171,7 +163,7 @@ export class Network implements Freezable<Network> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tx.accessList) {
|
if (tx.accessList) {
|
||||||
const accessList = this.formatter.accessList(tx.accessList);
|
const accessList = accessListify(tx.accessList);
|
||||||
for (const addr in accessList) {
|
for (const addr in accessList) {
|
||||||
gas += costs.txAccessListAddress + costs.txAccessListStorageKey * accessList[addr].storageKeys.length;
|
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 { throwArgumentError } from "../utils/index.js";
|
||||||
|
|
||||||
//import { BigNumberish } from "../math/index.js";
|
|
||||||
|
|
||||||
import type { Network } from "./network.js";
|
|
||||||
import type { FeeData, Provider } from "./provider.js";
|
import type { FeeData, Provider } from "./provider.js";
|
||||||
|
|
||||||
|
|
||||||
@ -21,9 +18,9 @@ export class NetworkPlugin {
|
|||||||
return new NetworkPlugin(this.name);
|
return new NetworkPlugin(this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(network: Network): NetworkPlugin {
|
// validate(network: Network): NetworkPlugin {
|
||||||
return this;
|
// return this;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Networks can use this plugin to override calculations for the
|
// Networks can use this plugin to override calculations for the
|
||||||
@ -38,7 +35,7 @@ export type GasCostParameters = {
|
|||||||
txAccessListAddress?: number;
|
txAccessListAddress?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class GasCostPlugin extends NetworkPlugin {
|
export class GasCostPlugin extends NetworkPlugin implements GasCostParameters {
|
||||||
readonly effectiveBlock!: number;
|
readonly effectiveBlock!: number;
|
||||||
|
|
||||||
readonly txBase!: number;
|
readonly txBase!: number;
|
||||||
@ -98,10 +95,10 @@ export class EnsPlugin extends NetworkPlugin {
|
|||||||
return new EnsPlugin(this.address, this.targetNetwork);
|
return new EnsPlugin(this.address, this.targetNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(network: Network): this {
|
// validate(network: Network): this {
|
||||||
network.formatter.address(this.address);
|
// network.formatter.address(this.address);
|
||||||
return this;
|
// return this;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
export class MaxPriorityFeePlugin extends NetworkPlugin {
|
export class MaxPriorityFeePlugin extends NetworkPlugin {
|
||||||
@ -141,3 +138,28 @@ export class FeeDataNetworkPlugin extends NetworkPlugin {
|
|||||||
return new FeeDataNetworkPlugin(this.#feeDataFunc);
|
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 {
|
import {
|
||||||
defineProperties,
|
defineProperties,
|
||||||
hexlify, toQuantity,
|
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]) {
|
if ((<any>{ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true })[key]) {
|
||||||
value = toQuantity(hexlify(value));
|
value = toQuantity(hexlify(value));
|
||||||
} else if (key === "accessList") {
|
} 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('","') }"]}`;
|
return `{address:"${ set.address }",storageKeys:["${ set.storageKeys.join('","') }"]}`;
|
||||||
}).join(",") + "]";
|
}).join(",") + "]";
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,10 +4,12 @@ import {
|
|||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { AbstractProvider } from "./abstract-provider.js";
|
import { AbstractProvider } from "./abstract-provider.js";
|
||||||
|
import {
|
||||||
|
formatBlock, formatBlockWithTransactions, formatLog, formatTransactionReceipt,
|
||||||
|
formatTransactionResponse
|
||||||
|
} from "./format.js";
|
||||||
import { Network } from "./network.js"
|
import { Network } from "./network.js"
|
||||||
|
|
||||||
import type { Frozen } from "../utils/index.js";
|
|
||||||
|
|
||||||
import type { PerformActionRequest } from "./abstract-provider.js";
|
import type { PerformActionRequest } from "./abstract-provider.js";
|
||||||
import type { Networkish } from "./network.js"
|
import type { Networkish } from "./network.js"
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ export interface FallbackProviderState extends Required<FallbackProviderConfig>
|
|||||||
|
|
||||||
interface Config extends FallbackProviderState {
|
interface Config extends FallbackProviderState {
|
||||||
_updateNumber: null | Promise<any>;
|
_updateNumber: null | Promise<any>;
|
||||||
_network: null | Frozen<Network>;
|
_network: null | Network;
|
||||||
_totalTime: number;
|
_totalTime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +134,7 @@ type RunningState = {
|
|||||||
|
|
||||||
// Normalizes a result to a string that can be used to compare against
|
// Normalizes a result to a string that can be used to compare against
|
||||||
// other results using normal string equality
|
// 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) {
|
switch (req.method) {
|
||||||
case "chainId":
|
case "chainId":
|
||||||
return getBigInt(value).toString();
|
return getBigInt(value).toString();
|
||||||
@ -150,19 +152,19 @@ function normalize(network: Frozen<Network>, value: any, req: PerformActionReque
|
|||||||
return hexlify(value);
|
return hexlify(value);
|
||||||
case "getBlock":
|
case "getBlock":
|
||||||
if (req.includeTransactions) {
|
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":
|
case "getTransaction":
|
||||||
return JSON.stringify(network.formatter.transactionResponse(value));
|
return JSON.stringify(formatTransactionResponse(value));
|
||||||
case "getTransactionReceipt":
|
case "getTransactionReceipt":
|
||||||
return JSON.stringify(network.formatter.receipt(value));
|
return JSON.stringify(formatTransactionReceipt(value));
|
||||||
case "call":
|
case "call":
|
||||||
return hexlify(value);
|
return hexlify(value);
|
||||||
case "estimateGas":
|
case "estimateGas":
|
||||||
return getBigInt(value).toString();
|
return getBigInt(value).toString();
|
||||||
case "getLogs":
|
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", {
|
return throwError("unsupported method", "UNSUPPORTED_OPERATION", {
|
||||||
@ -292,8 +294,8 @@ export class FallbackProvider extends AbstractProvider {
|
|||||||
return this.#configs.slice();
|
return this.#configs.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _detectNetwork(): Promise<Frozen<Network>> {
|
async _detectNetwork(): Promise<Network> {
|
||||||
return Network.from(getBigInt(await this._perform({ method: "chainId" }))).freeze();
|
return Network.from(getBigInt(await this._perform({ method: "chainId" })));
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO: Add support to select providers to be the event subscriber
|
// @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
|
// Check all the networks match
|
||||||
let chainId: null | bigint = null;
|
let chainId: null | bigint = null;
|
||||||
for (const config of this.#configs) {
|
for (const config of this.#configs) {
|
||||||
const network = <Frozen<Network>>(config._network);
|
const network = <Network>(config._network);
|
||||||
if (chainId == null) {
|
if (chainId == null) {
|
||||||
chainId = network.chainId;
|
chainId = network.chainId;
|
||||||
} else if (network.chainId !== chainId) {
|
} else if (network.chainId !== chainId) {
|
||||||
@ -403,7 +405,7 @@ export class FallbackProvider extends AbstractProvider {
|
|||||||
const result = runner.result.result;
|
const result = runner.result.result;
|
||||||
results.push({
|
results.push({
|
||||||
result,
|
result,
|
||||||
normal: normalize(<Frozen<Network>>(runner.config._network), result, req),
|
normal: normalize(runner.config.provider, result, req),
|
||||||
weight: runner.config.weight
|
weight: runner.config.weight
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,14 @@ export class IpcSocketProvider extends SocketProvider {
|
|||||||
super(network);
|
super(network);
|
||||||
this.#socket = connect(path);
|
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);
|
let response = Buffer.alloc(0);
|
||||||
this.socket.on("data", (data) => {
|
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
|
// 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 { TypedDataEncoder } from "../hash/index.js";
|
||||||
import { accessListify } from "../transaction/index.js";
|
import { accessListify } from "../transaction/index.js";
|
||||||
import {
|
import {
|
||||||
defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
|
defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
|
||||||
makeError, throwArgumentError, throwError,
|
makeError, throwArgumentError, throwError,
|
||||||
FetchRequest
|
FetchRequest, resolveProperties
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { AbstractProvider, UnmanagedSubscriber } from "./abstract-provider.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) })`);
|
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 {
|
function getLowerCase(value: string): string {
|
||||||
if (value) { return value.toLowerCase(); }
|
if (value) { return value.toLowerCase(); }
|
||||||
return value;
|
return value;
|
||||||
@ -265,7 +269,7 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
|
|||||||
// Try getting the transaction
|
// Try getting the transaction
|
||||||
const tx = await this.provider.getTransaction(hash);
|
const tx = await this.provider.getTransaction(hash);
|
||||||
if (tx != null) {
|
if (tx != null) {
|
||||||
resolve(this.provider._wrapTransaction(tx, hash, blockNumber));
|
resolve(tx.replaceableTransaction(blockNumber));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,6 +346,10 @@ type Payload = { payload: JsonRpcPayload, resolve: ResolveFunc, reject: RejectFu
|
|||||||
* sub-classed.
|
* sub-classed.
|
||||||
*
|
*
|
||||||
* It provides the base for all JSON-RPC-based Provider interaction.
|
* 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 {
|
export class JsonRpcApiProvider extends AbstractProvider {
|
||||||
|
|
||||||
@ -349,22 +357,36 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
|||||||
|
|
||||||
#nextId: number;
|
#nextId: number;
|
||||||
#payloads: Array<Payload>;
|
#payloads: Array<Payload>;
|
||||||
|
|
||||||
|
#ready: boolean;
|
||||||
|
#starting: null | Promise<void>;
|
||||||
|
|
||||||
#drainTimer: null | NodeJS.Timer;
|
#drainTimer: null | NodeJS.Timer;
|
||||||
|
|
||||||
|
#network: null | Network;
|
||||||
|
|
||||||
constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
||||||
super(network);
|
super(network);
|
||||||
|
|
||||||
|
this.#ready = false;
|
||||||
|
this.#starting = null;
|
||||||
|
|
||||||
this.#nextId = 1;
|
this.#nextId = 1;
|
||||||
this.#options = Object.assign({ }, defaultOptions, options || { });
|
this.#options = Object.assign({ }, defaultOptions, options || { });
|
||||||
|
|
||||||
this.#payloads = [ ];
|
this.#payloads = [ ];
|
||||||
this.#drainTimer = null;
|
this.#drainTimer = null;
|
||||||
|
|
||||||
|
this.#network = null;
|
||||||
|
|
||||||
// This could be relaxed in the future to just check equivalent networks
|
// This could be relaxed in the future to just check equivalent networks
|
||||||
const staticNetwork = this._getOption("staticNetwork");
|
const staticNetwork = this._getOption("staticNetwork");
|
||||||
if (staticNetwork && staticNetwork !== network) {
|
if (staticNetwork) {
|
||||||
|
if (staticNetwork !== network) {
|
||||||
throwArgumentError("staticNetwork MUST match network object", "options", options);
|
throwArgumentError("staticNetwork MUST match network object", "options", options);
|
||||||
}
|
}
|
||||||
|
this.#network = staticNetwork;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -376,8 +398,43 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
|||||||
return this.#options[key];
|
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 {
|
#scheduleDrain(): void {
|
||||||
if (this.#drainTimer) { return; }
|
if (this.#drainTimer || !this.ready) { return; }
|
||||||
|
|
||||||
// If we aren't using batching, no hard in sending it immeidately
|
// If we aren't using batching, no hard in sending it immeidately
|
||||||
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0: this._getOption("batchStallTime");
|
const stallTime = (this._getOption("batchMaxCount") === 1) ? 0: this._getOption("batchStallTime");
|
||||||
@ -446,7 +503,6 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
|||||||
}, stallTime);
|
}, stallTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-classes should **NOT** override this
|
|
||||||
/**
|
/**
|
||||||
* Requests the %%method%% with %%params%% via the JSON-RPC protocol
|
* Requests the %%method%% with %%params%% via the JSON-RPC protocol
|
||||||
* over the underlying channel. This can be used to call methods
|
* 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]);
|
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
|
// Account address
|
||||||
address = network.formatter.address(address);
|
address = getAddress(address);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
if (network.formatter.address(account) === account) {
|
if (getAddress(account) === account) {
|
||||||
return new JsonRpcSigner(this, account);
|
return new JsonRpcSigner(this, account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -524,14 +583,35 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
|||||||
throw new Error("invalid account");
|
throw new Error("invalid account");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-classes can override this; it detects the *actual* network we
|
/** Sub-classes can override this; it detects the *actual* network that
|
||||||
// are connected to
|
* we are **currently** connected to.
|
||||||
|
*
|
||||||
|
* Keep in mind that [[send]] may only be used once [[ready]].
|
||||||
|
*/
|
||||||
async _detectNetwork(): Promise<Network> {
|
async _detectNetwork(): Promise<Network> {
|
||||||
// We have a static network (like INFURA)
|
|
||||||
const network = this._getOption("staticNetwork");
|
const network = this._getOption("staticNetwork");
|
||||||
if (network) { return network; }
|
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;
|
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>> {
|
async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult>> {
|
||||||
// Configure a POST connection for the requested method
|
// Configure a POST connection for the requested method
|
||||||
const request = this.#connect.clone();
|
const request = this.#connect.clone();
|
||||||
|
@ -14,7 +14,6 @@ import { assertArgument, makeError, throwError } from "../utils/index.js";
|
|||||||
import { JsonRpcApiProvider } from "./provider-jsonrpc.js";
|
import { JsonRpcApiProvider } from "./provider-jsonrpc.js";
|
||||||
|
|
||||||
import type { Subscriber, Subscription } from "./abstract-provider.js";
|
import type { Subscriber, Subscription } from "./abstract-provider.js";
|
||||||
import type { Formatter } from "./formatter.js";
|
|
||||||
import type { EventFilter } from "./provider.js";
|
import type { EventFilter } from "./provider.js";
|
||||||
import type { JsonRpcError, JsonRpcPayload, JsonRpcResult } from "./provider-jsonrpc.js";
|
import type { JsonRpcError, JsonRpcPayload, JsonRpcResult } from "./provider-jsonrpc.js";
|
||||||
import type { Networkish } from "./network.js";
|
import type { Networkish } from "./network.js";
|
||||||
@ -124,25 +123,19 @@ export class SocketEventSubscriber extends SocketSubscriber {
|
|||||||
#logFilter: string;
|
#logFilter: string;
|
||||||
get logFilter(): EventFilter { return JSON.parse(this.#logFilter); }
|
get logFilter(): EventFilter { return JSON.parse(this.#logFilter); }
|
||||||
|
|
||||||
#formatter: Promise<Readonly<Formatter>>;
|
|
||||||
|
|
||||||
constructor(provider: SocketProvider, filter: EventFilter) {
|
constructor(provider: SocketProvider, filter: EventFilter) {
|
||||||
super(provider, [ "logs", filter ]);
|
super(provider, [ "logs", filter ]);
|
||||||
this.#logFilter = JSON.stringify(filter);
|
this.#logFilter = JSON.stringify(filter);
|
||||||
this.#formatter = provider.getNetwork().then((network) => network.formatter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _emit(provider: SocketProvider, message: any): Promise<void> {
|
async _emit(provider: SocketProvider, message: any): Promise<void> {
|
||||||
const formatter = await this.#formatter;
|
provider.emit(this.#logFilter, provider._wrapLog(message, provider._network));
|
||||||
provider.emit(this.#logFilter, formatter.log(message, provider));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SocketProvider extends JsonRpcApiProvider {
|
export class SocketProvider extends JsonRpcApiProvider {
|
||||||
#callbacks: Map<number, { payload: JsonRpcPayload, resolve: (r: any) => void, reject: (e: Error) => void }>;
|
#callbacks: Map<number, { payload: JsonRpcPayload, resolve: (r: any) => void, reject: (e: Error) => void }>;
|
||||||
|
|
||||||
#ready: boolean;
|
|
||||||
|
|
||||||
// Maps each filterId to its subscriber
|
// Maps each filterId to its subscriber
|
||||||
#subs: Map<number | string, SocketSubscriber>;
|
#subs: Map<number | string, SocketSubscriber>;
|
||||||
|
|
||||||
@ -153,11 +146,20 @@ export class SocketProvider extends JsonRpcApiProvider {
|
|||||||
constructor(network?: Networkish) {
|
constructor(network?: Networkish) {
|
||||||
super(network, { batchMaxCount: 1 });
|
super(network, { batchMaxCount: 1 });
|
||||||
this.#callbacks = new Map();
|
this.#callbacks = new Map();
|
||||||
this.#ready = false;
|
|
||||||
this.#subs = new Map();
|
this.#subs = new Map();
|
||||||
this.#pending = 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 {
|
_getSubscriber(sub: Subscription): Subscriber {
|
||||||
switch (sub.type) {
|
switch (sub.type) {
|
||||||
case "close":
|
case "close":
|
||||||
@ -198,21 +200,25 @@ export class SocketProvider extends JsonRpcApiProvider {
|
|||||||
this.#callbacks.set(payload.id, { payload, resolve, reject });
|
this.#callbacks.set(payload.id, { payload, resolve, reject });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.#ready) {
|
|
||||||
await this._write(JSON.stringify(payload));
|
await this._write(JSON.stringify(payload));
|
||||||
}
|
|
||||||
|
|
||||||
return <Array<JsonRpcResult | JsonRpcError>>[ await promise ];
|
return <Array<JsonRpcResult | JsonRpcError>>[ await promise ];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-classes must call this once they are connected
|
// Sub-classes must call this once they are connected
|
||||||
|
/*
|
||||||
async _start(): Promise<void> {
|
async _start(): Promise<void> {
|
||||||
if (this.#ready) { return; }
|
if (this.#ready) { return; }
|
||||||
this.#ready = true;
|
|
||||||
for (const { payload } of this.#callbacks.values()) {
|
for (const { payload } of this.#callbacks.values()) {
|
||||||
await this._write(JSON.stringify(payload));
|
await this._write(JSON.stringify(payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#ready = (async function() {
|
||||||
|
await super._start();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Sub-classes must call this for each message
|
// Sub-classes must call this for each message
|
||||||
async _processMessage(message: string): Promise<void> {
|
async _processMessage(message: string): Promise<void> {
|
||||||
|
@ -31,8 +31,13 @@ export class WebSocketProvider extends SocketProvider {
|
|||||||
this.#websocket = url;
|
this.#websocket = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
this.websocket.onopen = async () => {
|
||||||
this._start();
|
try {
|
||||||
|
await this._start()
|
||||||
|
} catch (error) {
|
||||||
|
console.log("failed to start WebsocketProvider", error);
|
||||||
|
// @TODO: now what? Attempt reconnect?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.onmessage = (message: { data: string }) => {
|
this.websocket.onmessage = (message: { data: string }) => {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
//import { resolveAddress } from "@ethersproject/address";
|
//import { resolveAddress } from "@ethersproject/address";
|
||||||
import {
|
import {
|
||||||
defineProperties, getBigInt, getNumber, hexlify, throwError
|
defineProperties, getBigInt, getNumber, hexlify, resolveProperties,
|
||||||
|
assertArgument, isError, makeError, throwError
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
import { accessListify } from "../transaction/index.js";
|
import { accessListify } from "../transaction/index.js";
|
||||||
|
|
||||||
import type { AddressLike, NameResolver } from "../address/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 { Signature } from "../crypto/index.js";
|
||||||
import type { AccessList, AccessListish, TransactionLike } from "../transaction/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";
|
import type { Network } from "./network.js";
|
||||||
|
|
||||||
|
|
||||||
|
const BN_0 = BigInt(0);
|
||||||
|
|
||||||
export type BlockTag = number | string;
|
export type BlockTag = number | string;
|
||||||
|
|
||||||
// -----------------------
|
// -----------------------
|
||||||
@ -55,7 +58,6 @@ export class FeeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface TransactionRequest {
|
export interface TransactionRequest {
|
||||||
type?: null | number;
|
type?: null | number;
|
||||||
|
|
||||||
@ -78,17 +80,15 @@ export interface TransactionRequest {
|
|||||||
|
|
||||||
customData?: any;
|
customData?: any;
|
||||||
|
|
||||||
|
// Only meaningful when used for call
|
||||||
|
blockTag?: BlockTag;
|
||||||
|
enableCcipRead?: boolean;
|
||||||
|
|
||||||
// Todo?
|
// Todo?
|
||||||
//gasMultiplier?: number;
|
//gasMultiplier?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface PreparedTransactionRequest {
|
||||||
export interface CallRequest extends TransactionRequest {
|
|
||||||
blockTag?: BlockTag;
|
|
||||||
enableCcipRead?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PreparedRequest {
|
|
||||||
type?: number;
|
type?: number;
|
||||||
|
|
||||||
to?: AddressLike;
|
to?: AddressLike;
|
||||||
@ -114,7 +114,7 @@ export interface PreparedRequest {
|
|||||||
enableCcipRead?: boolean;
|
enableCcipRead?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyRequest(req: CallRequest): PreparedRequest {
|
export function copyRequest(req: TransactionRequest): PreparedTransactionRequest {
|
||||||
const result: any = { };
|
const result: any = { };
|
||||||
|
|
||||||
// These could be addresses, ENS names or Addressables
|
// These could be addresses, ENS names or Addressables
|
||||||
@ -176,7 +176,7 @@ export async function resolveTransactionRequest(tx: TransactionRequest, provider
|
|||||||
//////////////////////
|
//////////////////////
|
||||||
// Block
|
// Block
|
||||||
|
|
||||||
export interface BlockParams<T extends string | TransactionResponse> {
|
export interface BlockParams<T extends string | TransactionResponseParams> {
|
||||||
hash?: null | string;
|
hash?: null | string;
|
||||||
|
|
||||||
number: number;
|
number: number;
|
||||||
@ -228,8 +228,7 @@ export class Block<T extends string | TransactionResponse> implements BlockParam
|
|||||||
|
|
||||||
readonly #transactions: ReadonlyArray<T>;
|
readonly #transactions: ReadonlyArray<T>;
|
||||||
|
|
||||||
constructor(block: BlockParams<T>, provider?: null | Provider) {
|
constructor(block: BlockParams<T>, provider: Provider) {
|
||||||
if (provider == null) { provider = dummyProvider; }
|
|
||||||
|
|
||||||
this.#transactions = Object.freeze(block.transactions.map((tx) => {
|
this.#transactions = Object.freeze(block.transactions.map((tx) => {
|
||||||
if (typeof(tx) !== "string" && tx.provider !== provider) {
|
if (typeof(tx) !== "string" && tx.provider !== provider) {
|
||||||
@ -363,8 +362,7 @@ export class Log implements LogParams {
|
|||||||
readonly transactionIndex!: number;
|
readonly transactionIndex!: number;
|
||||||
|
|
||||||
|
|
||||||
constructor(log: LogParams, provider?: null | Provider) {
|
constructor(log: LogParams, provider: Provider) {
|
||||||
if (provider == null) { provider = dummyProvider; }
|
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
|
|
||||||
const topics = Object.freeze(log.topics.slice());
|
const topics = Object.freeze(log.topics.slice());
|
||||||
@ -486,8 +484,7 @@ export class TransactionReceipt implements TransactionReceiptParams, Iterable<Lo
|
|||||||
|
|
||||||
readonly #logs: ReadonlyArray<Log>;
|
readonly #logs: ReadonlyArray<Log>;
|
||||||
|
|
||||||
constructor(tx: TransactionReceiptParams, provider?: null | Provider) {
|
constructor(tx: TransactionReceiptParams, provider: Provider) {
|
||||||
if (provider == null) { provider = dummyProvider; }
|
|
||||||
this.#logs = Object.freeze(tx.logs.map((log) => {
|
this.#logs = Object.freeze(tx.logs.map((log) => {
|
||||||
if (provider !== log.provider) {
|
if (provider !== log.provider) {
|
||||||
//return log.connect(provider);
|
//return log.connect(provider);
|
||||||
@ -635,7 +632,14 @@ export interface MinedTransactionResponse extends TransactionResponse {
|
|||||||
date: Date;
|
date: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReplacementDetectionSetup = {
|
||||||
|
to: string;
|
||||||
|
from: string;
|
||||||
|
value: bigint;
|
||||||
|
data: string;
|
||||||
|
nonce: number;
|
||||||
|
block: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class TransactionResponse implements TransactionLike<string>, TransactionResponseParams {
|
export class TransactionResponse implements TransactionLike<string>, TransactionResponseParams {
|
||||||
readonly provider: Provider;
|
readonly provider: Provider;
|
||||||
@ -669,8 +673,9 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
|
|||||||
|
|
||||||
readonly accessList!: null | AccessList;
|
readonly accessList!: null | AccessList;
|
||||||
|
|
||||||
constructor(tx: TransactionResponseParams, provider?: null | Provider) {
|
#startBlock: number;
|
||||||
if (provider == null) { provider = dummyProvider; }
|
|
||||||
|
constructor(tx: TransactionResponseParams, provider: Provider) {
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
|
|
||||||
this.blockNumber = (tx.blockNumber != null) ? tx.blockNumber: null;
|
this.blockNumber = (tx.blockNumber != null) ? tx.blockNumber: null;
|
||||||
@ -697,11 +702,9 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
|
|||||||
this.signature = tx.signature;
|
this.signature = tx.signature;
|
||||||
|
|
||||||
this.accessList = (tx.accessList != null) ? tx.accessList: null;
|
this.accessList = (tx.accessList != null) ? tx.accessList: null;
|
||||||
}
|
|
||||||
|
|
||||||
//connect(provider: Provider): TransactionResponse {
|
this.#startBlock = -1;
|
||||||
// return new TransactionResponse(this, provider);
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
toJSON(): any {
|
toJSON(): any {
|
||||||
const {
|
const {
|
||||||
@ -740,8 +743,155 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
|
|||||||
return this.provider.getTransaction(this.hash);
|
return this.provider.getTransaction(this.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
async wait(confirms?: number): Promise<null | TransactionReceipt> {
|
async wait(_confirms?: number, _timeout?: number): Promise<null | TransactionReceipt> {
|
||||||
return this.provider.waitForTransaction(this.hash, confirms);
|
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 {
|
isMined(): this is MinedTransactionResponse {
|
||||||
@ -779,6 +929,22 @@ export class TransactionResponse implements TransactionLike<string>, Transaction
|
|||||||
}
|
}
|
||||||
return createReorderedTransactionFilter(this, other);
|
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]].
|
* Get the connected [[Network]].
|
||||||
*/
|
*/
|
||||||
getNetwork(): Promise<Frozen<Network>>;
|
getNetwork(): Promise<Network>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the best guess at the recommended [[FeeData]].
|
* 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
|
* @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
|
* 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>;
|
waitForTransaction(hash: string, confirms?: number, timeout?: number): Promise<null | TransactionReceipt>;
|
||||||
waitForBlock(blockTag?: BlockTag): Promise<Block<string>>;
|
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 { TransactionLike } from "../transaction/index.js";
|
||||||
|
|
||||||
import type { ContractRunner } from "./contracts.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
|
* 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
|
// Preparation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares a {@link CallRequest} for calling:
|
* Prepares a {@link TransactionRequest} for calling:
|
||||||
* - resolves ``to`` and ``from`` addresses
|
* - resolves ``to`` and ``from`` addresses
|
||||||
* - if ``from`` is specified , check that it matches this Signer
|
* - if ``from`` is specified , check that it matches this Signer
|
||||||
*
|
*
|
||||||
* @param tx - The call to prepare
|
* @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
|
* 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``
|
* node to take into account. In these cases, a manually determined ``gasLimit``
|
||||||
* will need to be made.
|
* 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
|
* 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
|
* (e.g. running a Contract's getters) or to simulate the effect of a transaction
|
||||||
* before actually performing an operation.
|
* before actually performing an operation.
|
||||||
*/
|
*/
|
||||||
call(tx: CallRequest): Promise<string>;
|
call(tx: TransactionRequest): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves an [[Address]] or ENS Name to an [[Address]].
|
* Resolves an [[Address]] or ENS Name to an [[Address]].
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { PollingEventSubscriber } from "./subscriber-polling.js";
|
import { PollingEventSubscriber } from "./subscriber-polling.js";
|
||||||
|
|
||||||
import type { Frozen } from "../utils/index.js";
|
|
||||||
|
|
||||||
import type { AbstractProvider, Subscriber } from "./abstract-provider.js";
|
import type { AbstractProvider, Subscriber } from "./abstract-provider.js";
|
||||||
import type { Network } from "./network.js";
|
import type { Network } from "./network.js";
|
||||||
import type { EventFilter } from "./provider.js";
|
import type { EventFilter } from "./provider.js";
|
||||||
@ -18,7 +16,7 @@ export class FilterIdSubscriber implements Subscriber {
|
|||||||
#filterIdPromise: null | Promise<string>;
|
#filterIdPromise: null | Promise<string>;
|
||||||
#poller: (b: number) => Promise<void>;
|
#poller: (b: number) => Promise<void>;
|
||||||
|
|
||||||
#network: null | Frozen<Network>;
|
#network: null | Network;
|
||||||
|
|
||||||
constructor(provider: JsonRpcApiProvider) {
|
constructor(provider: JsonRpcApiProvider) {
|
||||||
this.#provider = provider;
|
this.#provider = provider;
|
||||||
@ -111,10 +109,8 @@ export class FilterIdEventSubscriber extends FilterIdSubscriber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
|
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
|
||||||
const network = await provider.getNetwork();
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
const log = network.formatter.log(result, provider);
|
provider.emit(this.#event, provider._wrapLog(result, provider._network));
|
||||||
provider.emit(this.#event, log);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,9 +121,8 @@ export class FilterIdPendingSubscriber extends FilterIdSubscriber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
|
async _emitResults(provider: JsonRpcApiProvider, results: Array<any>): Promise<void> {
|
||||||
const network = await provider.getNetwork();
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
provider.emit("pending", network.formatter.hash(result));
|
provider.emit("pending", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user