123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308 |
- /**
- * One of the most common ways to interact with the blockchain is
- * by a node running a JSON-RPC interface which can be connected to,
- * based on the transport, using:
- *
- * - HTTP or HTTPS - [[JsonRpcProvider]]
- * - WebSocket - [[WebSocketProvider]]
- * - IPC - [[IpcSocketProvider]]
- *
- * @_section: api/providers/jsonrpc:JSON-RPC Provider [about-jsonrpcProvider]
- */
- // @TODO:
- // - Add the batching API
- // 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 { AbiCoder } from "../abi/index.js";
- import { getAddress, resolveAddress } from "../address/index.js";
- import { TypedDataEncoder } from "../hash/index.js";
- import { accessListify } from "../transaction/index.js";
- import {
- defineProperties, getBigInt, hexlify, isHexString, toQuantity, toUtf8Bytes,
- isError, makeError, assert, assertArgument,
- FetchRequest, resolveProperties
- } from "../utils/index.js";
- import { AbstractProvider, UnmanagedSubscriber } from "./abstract-provider.js";
- import { AbstractSigner } from "./abstract-signer.js";
- import { Network } from "./network.js";
- import { FilterIdEventSubscriber, FilterIdPendingSubscriber } from "./subscriber-filterid.js";
- import { PollingEventSubscriber } from "./subscriber-polling.js";
- import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
- import type { TransactionLike } from "../transaction/index.js";
- import type { PerformActionRequest, Subscriber, Subscription } from "./abstract-provider.js";
- import type { Networkish } from "./network.js";
- import type { Provider, TransactionRequest, TransactionResponse } from "./provider.js";
- import type { Signer } from "./signer.js";
- type Timer = ReturnType<typeof setTimeout>;
- const Primitive = "bigint,boolean,function,number,string,symbol".split(/,/g);
- //const Methods = "getAddress,then".split(/,/g);
- function deepCopy<T = any>(value: T): T {
- if (value == null || Primitive.indexOf(typeof(value)) >= 0) {
- return value;
- }
- // Keep any Addressable
- if (typeof((<any>value).getAddress) === "function") {
- return value;
- }
- if (Array.isArray(value)) { return <any>(value.map(deepCopy)); }
- if (typeof(value) === "object") {
- return Object.keys(value).reduce((accum, key) => {
- accum[key] = (<any>value)[key];
- return accum;
- }, <any>{ });
- }
- throw new Error(`should not happen: ${ value } (${ typeof(value) })`);
- }
- function stall(duration: number): Promise<void> {
- return new Promise((resolve) => { setTimeout(resolve, duration); });
- }
- function getLowerCase(value: string): string {
- if (value) { return value.toLowerCase(); }
- return value;
- }
- interface Pollable {
- pollingInterval: number;
- }
- function isPollable(value: any): value is Pollable {
- return (value && typeof(value.pollingInterval) === "number");
- }
- /**
- * A JSON-RPC payload, which are sent to a JSON-RPC server.
- */
- export type JsonRpcPayload = {
- /**
- * The JSON-RPC request ID.
- */
- id: number;
- /**
- * The JSON-RPC request method.
- */
- method: string;
- /**
- * The JSON-RPC request parameters.
- */
- params: Array<any> | Record<string, any>;
- /**
- * A required constant in the JSON-RPC specification.
- */
- jsonrpc: "2.0";
- };
- /**
- * A JSON-RPC result, which are returned on success from a JSON-RPC server.
- */
- export type JsonRpcResult = {
- /**
- * The response ID to match it to the relevant request.
- */
- id: number;
- /**
- * The response result.
- */
- result: any;
- };
- /**
- * A JSON-RPC error, which are returned on failure from a JSON-RPC server.
- */
- export type JsonRpcError = {
- /**
- * The response ID to match it to the relevant request.
- */
- id: number;
- /**
- * The response error.
- */
- error: {
- code: number;
- message?: string;
- data?: any;
- }
- };
- /**
- * When subscribing to the ``"debug"`` event, the [[Listener]] will
- * receive this object as the first parameter.
- */
- export type DebugEventJsonRpcApiProvider = {
- action: "sendRpcPayload",
- payload: JsonRpcPayload | Array<JsonRpcPayload>
- } | {
- action: "receiveRpcResult",
- result: Array<JsonRpcResult | JsonRpcError>
- } | {
- action: "receiveRpcError",
- error: Error
- };
- /**
- * Options for configuring a [[JsonRpcApiProvider]]. Much of this
- * is targetted towards sub-classes, which often will not expose
- * any of these options to their consumers.
- *
- * **``polling``** - use the polling strategy is used immediately
- * for events; otherwise, attempt to use filters and fall back onto
- * polling (default: ``false``)
- *
- * **``staticNetwork``** - do not request chain ID on requests to
- * validate the underlying chain has not changed (default: ``null``)
- *
- * This should **ONLY** be used if it is **certain** that the network
- * cannot change, such as when using INFURA (since the URL dictates the
- * network). If the network is assumed static and it does change, this
- * can have tragic consequences. For example, this **CANNOT** be used
- * with MetaMask, since the user can select a new network from the
- * drop-down at any time.
- *
- * **``batchStallTime``** - how long (ms) to aggregate requests into a
- * single batch. ``0`` indicates batching will only encompass the current
- * event loop. If ``batchMaxCount = 1``, this is ignored. (default: ``10``)
- *
- * **``batchMaxSize``** - target maximum size (bytes) to allow per batch
- * request (default: 1Mb)
- *
- * **``batchMaxCount``** - maximum number of requests to allow in a batch.
- * If ``batchMaxCount = 1``, then batching is disabled. (default: ``100``)
- *
- * **``cacheTimeout``** - passed as [[AbstractProviderOptions]].
- */
- export type JsonRpcApiProviderOptions = {
- polling?: boolean;
- staticNetwork?: null | boolean | Network;
- batchStallTime?: number;
- batchMaxSize?: number;
- batchMaxCount?: number;
- cacheTimeout?: number;
- pollingInterval?: number;
- };
- const defaultOptions = {
- polling: false,
- staticNetwork: null,
- batchStallTime: 10, // 10ms
- batchMaxSize: (1 << 20), // 1Mb
- batchMaxCount: 100, // 100 requests
- cacheTimeout: 250,
- pollingInterval: 4000
- }
- /**
- * A **JsonRpcTransactionRequest** is formatted as needed by the JSON-RPC
- * Ethereum API specification.
- */
- export interface JsonRpcTransactionRequest {
- /**
- * The sender address to use when signing.
- */
- from?: string;
- /**
- * The target address.
- */
- to?: string;
- /**
- * The transaction data.
- */
- data?: string;
- /**
- * The chain ID the transaction is valid on.
- */
- chainId?: string;
- /**
- * The [[link-eip-2718]] transaction type.
- */
- type?: string;
- /**
- * The maximum amount of gas to allow a transaction to consume.
- *
- * In most other places in ethers, this is called ``gasLimit`` which
- * differs from the JSON-RPC Ethereum API specification.
- */
- gas?: string;
- /**
- * The gas price per wei for transactions prior to [[link-eip-1559]].
- */
- gasPrice?: string;
- /**
- * The maximum fee per gas for [[link-eip-1559]] transactions.
- */
- maxFeePerGas?: string;
- /**
- * The maximum priority fee per gas for [[link-eip-1559]] transactions.
- */
- maxPriorityFeePerGas?: string;
- /**
- * The nonce for the transaction.
- */
- nonce?: string;
- /**
- * The transaction value (in wei).
- */
- value?: string;
- /**
- * The transaction access list.
- */
- accessList?: Array<{ address: string, storageKeys: Array<string> }>;
- }
- // @TODO: Unchecked Signers
- export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
- address!: string;
- constructor(provider: JsonRpcApiProvider, address: string) {
- super(provider);
- address = getAddress(address);
- defineProperties<JsonRpcSigner>(this, { address });
- }
- connect(provider: null | Provider): Signer {
- assert(false, "cannot reconnect JsonRpcSigner", "UNSUPPORTED_OPERATION", {
- operation: "signer.connect"
- });
- }
- async getAddress(): Promise<string> {
- return this.address;
- }
- // JSON-RPC will automatially fill in nonce, etc. so we just check from
- async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
- return await this.populateCall(tx);
- }
- // Returns just the hash of the transaction after sent, which is what
- // the bare JSON-RPC API does;
- async sendUncheckedTransaction(_tx: TransactionRequest): Promise<string> {
- const tx = deepCopy(_tx);
- const promises: Array<Promise<void>> = [];
- // Make sure the from matches the sender
- if (tx.from) {
- const _from = tx.from;
- promises.push((async () => {
- const from = await resolveAddress(_from, this.provider);
- assertArgument(from != null && from.toLowerCase() === this.address.toLowerCase(),
- "from address mismatch", "transaction", _tx);
- tx.from = from;
- })());
- } else {
- tx.from = this.address;
- }
- // The JSON-RPC for eth_sendTransaction uses 90000 gas; if the user
- // wishes to use this, it is easy to specify explicitly, otherwise
- // we look it up for them.
- if (tx.gasLimit == null) {
- promises.push((async () => {
- tx.gasLimit = await this.provider.estimateGas({ ...tx, from: this.address});
- })());
- }
- // The address may be an ENS name or Addressable
- if (tx.to != null) {
- const _to = tx.to;
- promises.push((async () => {
- tx.to = await resolveAddress(_to, this.provider);
- })());
- }
- // Wait until all of our properties are filled in
- if (promises.length) { await Promise.all(promises); }
- const hexTx = this.provider.getRpcTransaction(tx);
- return this.provider.send("eth_sendTransaction", [ hexTx ]);
- }
- async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
- // This cannot be mined any earlier than any recent block
- const blockNumber = await this.provider.getBlockNumber();
- // Send the transaction
- const hash = await this.sendUncheckedTransaction(tx);
- // Unfortunately, JSON-RPC only provides and opaque transaction hash
- // for a response, and we need the actual transaction, so we poll
- // for it; it should show up very quickly
- return await (new Promise((resolve, reject) => {
- const timeouts = [ 1000, 100 ];
- let invalids = 0;
- const checkTx = async () => {
- try {
- // Try getting the transaction
- const tx = await this.provider.getTransaction(hash);
- if (tx != null) {
- resolve(tx.replaceableTransaction(blockNumber));
- return;
- }
- } catch (error) {
- // If we were cancelled: stop polling.
- // If the data is bad: the node returns bad transactions
- // If the network changed: calling again will also fail
- // If unsupported: likely destroyed
- if (isError(error, "CANCELLED") || isError(error, "BAD_DATA") ||
- isError(error, "NETWORK_ERROR") || isError(error, "UNSUPPORTED_OPERATION")) {
- if (error.info == null) { error.info = { }; }
- error.info.sendTransactionHash = hash;
- reject(error);
- return;
- }
- // Stop-gap for misbehaving backends; see #4513
- if (isError(error, "INVALID_ARGUMENT")) {
- invalids++;
- if (error.info == null) { error.info = { }; }
- error.info.sendTransactionHash = hash;
- if (invalids > 10) {
- reject(error);
- return;
- }
- }
- // Notify anyone that cares; but we will try again, since
- // it is likely an intermittent service error
- this.provider.emit("error", makeError("failed to fetch transation after sending (will try again)", "UNKNOWN_ERROR", { error }));
- }
- // Wait another 4 seconds
- this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
- };
- checkTx();
- }));
- }
- async signTransaction(_tx: TransactionRequest): Promise<string> {
- const tx = deepCopy(_tx);
- // Make sure the from matches the sender
- if (tx.from) {
- const from = await resolveAddress(tx.from, this.provider);
- assertArgument(from != null && from.toLowerCase() === this.address.toLowerCase(),
- "from address mismatch", "transaction", _tx);
- tx.from = from;
- } else {
- tx.from = this.address;
- }
- const hexTx = this.provider.getRpcTransaction(tx);
- return await this.provider.send("eth_signTransaction", [ hexTx ]);
- }
- async signMessage(_message: string | Uint8Array): Promise<string> {
- const message = ((typeof(_message) === "string") ? toUtf8Bytes(_message): _message);
- return await this.provider.send("personal_sign", [
- hexlify(message), this.address.toLowerCase() ]);
- }
- async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, _value: Record<string, any>): Promise<string> {
- const value = deepCopy(_value);
- // Populate any ENS names (in-place)
- const populated = await TypedDataEncoder.resolveNames(domain, types, value, async (value: string) => {
- const address = await resolveAddress(value);
- assertArgument(address != null, "TypedData does not support null address", "value", value);
- return address;
- });
- return await this.provider.send("eth_signTypedData_v4", [
- this.address.toLowerCase(),
- JSON.stringify(TypedDataEncoder.getPayload(populated.domain, types, populated.value))
- ]);
- }
- async unlock(password: string): Promise<boolean> {
- return this.provider.send("personal_unlockAccount", [
- this.address.toLowerCase(), password, null ]);
- }
- // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
- async _legacySignMessage(_message: string | Uint8Array): Promise<string> {
- const message = ((typeof(_message) === "string") ? toUtf8Bytes(_message): _message);
- return await this.provider.send("eth_sign", [
- this.address.toLowerCase(), hexlify(message) ]);
- }
- }
- type ResolveFunc = (result: JsonRpcResult) => void;
- type RejectFunc = (error: Error) => void;
- type Payload = { payload: JsonRpcPayload, resolve: ResolveFunc, reject: RejectFunc };
- /**
- * The JsonRpcApiProvider is an abstract class and **MUST** be
- * sub-classed.
- *
- * It provides the base for all JSON-RPC-based Provider interaction.
- *
- * Sub-classing Notes:
- * - a sub-class MUST override _send
- * - a sub-class MUST call the `_start()` method once connected
- */
- export abstract class JsonRpcApiProvider extends AbstractProvider {
- #options: Required<JsonRpcApiProviderOptions>;
- // The next ID to use for the JSON-RPC ID field
- #nextId: number;
- // Payloads are queued and triggered in batches using the drainTimer
- #payloads: Array<Payload>;
- #drainTimer: null | Timer;
- #notReady: null | {
- promise: Promise<void>,
- resolve: null | ((v: void) => void)
- };
- #network: null | Network;
- #pendingDetectNetwork: null | Promise<Network>;
- #scheduleDrain(): void {
- if (this.#drainTimer) { return; }
- // If we aren't using batching, no harm in sending it immediately
- const stallTime = (this._getOption("batchMaxCount") === 1) ? 0: this._getOption("batchStallTime");
- this.#drainTimer = setTimeout(() => {
- this.#drainTimer = null;
- const payloads = this.#payloads;
- this.#payloads = [ ];
- while (payloads.length) {
- // Create payload batches that satisfy our batch constraints
- const batch = [ <Payload>(payloads.shift()) ];
- while (payloads.length) {
- if (batch.length === this.#options.batchMaxCount) { break; }
- batch.push(<Payload>(payloads.shift()));
- const bytes = JSON.stringify(batch.map((p) => p.payload));
- if (bytes.length > this.#options.batchMaxSize) {
- payloads.unshift(<Payload>(batch.pop()));
- break;
- }
- }
- // Process the result to each payload
- (async () => {
- const payload = ((batch.length === 1) ? batch[0].payload: batch.map((p) => p.payload));
- this.emit("debug", { action: "sendRpcPayload", payload });
- try {
- const result = await this._send(payload);
- this.emit("debug", { action: "receiveRpcResult", result });
- // Process results in batch order
- for (const { resolve, reject, payload } of batch) {
- if (this.destroyed) {
- reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method }));
- continue;
- }
- // Find the matching result
- const resp = result.filter((r) => (r.id === payload.id))[0];
- // No result; the node failed us in unexpected ways
- if (resp == null) {
- const error = makeError("missing response for request", "BAD_DATA", {
- value: result, info: { payload }
- });
- this.emit("error", error);
- reject(error);
- continue;
- }
- // The response is an error
- if ("error" in resp) {
- reject(this.getRpcError(payload, resp));
- continue;
- }
- // All good; send the result
- resolve(resp.result);
- }
- } catch (error: any) {
- this.emit("debug", { action: "receiveRpcError", error });
- for (const { reject } of batch) {
- // @TODO: augment the error with the payload
- reject(error);
- }
- }
- })();
- }
- }, stallTime);
- }
- constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
- super(network, options);
- this.#nextId = 1;
- this.#options = Object.assign({ }, defaultOptions, options || { });
- this.#payloads = [ ];
- this.#drainTimer = null;
- this.#network = null;
- this.#pendingDetectNetwork = null;
- {
- let resolve: null | ((value: void) => void) = null;
- const promise = new Promise((_resolve: (value: void) => void) => {
- resolve = _resolve;
- });
- this.#notReady = { promise, resolve };
- }
- const staticNetwork = this._getOption("staticNetwork");
- if (typeof(staticNetwork) === "boolean") {
- assertArgument(!staticNetwork || network !== "any", "staticNetwork cannot be used on special network 'any'", "options", options);
- if (staticNetwork && network != null) {
- this.#network = Network.from(network);
- }
- } else if (staticNetwork) {
- // Make sure any static network is compatbile with the provided netwrok
- assertArgument(network == null || staticNetwork.matches(network),
- "staticNetwork MUST match network object", "options", options);
- this.#network = staticNetwork;
- }
- }
- /**
- * Returns the value associated with the option %%key%%.
- *
- * Sub-classes can use this to inquire about configuration options.
- */
- _getOption<K extends keyof JsonRpcApiProviderOptions>(key: K): JsonRpcApiProviderOptions[K] {
- return this.#options[key];
- }
- /**
- * Gets the [[Network]] this provider has committed to. On each call, the network
- * is detected, and if it has changed, the call will reject.
- */
- get _network(): Network {
- assert (this.#network, "network is not available yet", "NETWORK_ERROR");
- return this.#network;
- }
- /**
- * Sends a JSON-RPC %%payload%% (or a batch) to the underlying channel.
- *
- * Sub-classes **MUST** override this.
- */
- abstract _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>>;
- /**
- * Resolves to the non-normalized value by performing %%req%%.
- *
- * Sub-classes may override this to modify behavior of actions,
- * and should generally call ``super._perform`` as a fallback.
- */
- async _perform(req: PerformActionRequest): Promise<any> {
- // Legacy networks do not like the type field being passed along (which
- // is fair), so we delete type if it is 0 and a non-EIP-1559 network
- if (req.method === "call" || req.method === "estimateGas") {
- let tx = req.transaction;
- if (tx && tx.type != null && getBigInt(tx.type)) {
- // If there are no EIP-1559 or newer properties, it might be pre-EIP-1559
- if (tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
- const feeData = await this.getFeeData();
- if (feeData.maxFeePerGas == null && feeData.maxPriorityFeePerGas == null) {
- // Network doesn't know about EIP-1559 (and hence type)
- req = Object.assign({ }, req, {
- transaction: Object.assign({ }, tx, { type: undefined })
- });
- }
- }
- }
- }
- const request = this.getRpcRequest(req);
- if (request != null) {
- return await this.send(request.method, request.args);
- }
- return super._perform(req);
- }
- /**
- * Sub-classes may override this; it detects the *actual* network that
- * we are **currently** connected to.
- *
- * Keep in mind that [[send]] may only be used once [[ready]], otherwise the
- * _send primitive must be used instead.
- */
- async _detectNetwork(): Promise<Network> {
- const network = this._getOption("staticNetwork");
- if (network) {
- if (network === true) {
- if (this.#network) { return this.#network; }
- } else {
- return network;
- }
- }
- if (this.#pendingDetectNetwork) {
- return await this.#pendingDetectNetwork;
- }
- // If we are ready, use ``send``, which enabled requests to be batched
- if (this.ready) {
- this.#pendingDetectNetwork = (async () => {
- try {
- const result = Network.from(getBigInt(await this.send("eth_chainId", [ ])));
- this.#pendingDetectNetwork = null;
- return result;
- } catch (error) {
- this.#pendingDetectNetwork = null;
- throw error;
- }
- })();
- return await this.#pendingDetectNetwork;
- }
- // We are not ready yet; use the primitive _send
- this.#pendingDetectNetwork = (async () => {
- const payload: JsonRpcPayload = {
- id: this.#nextId++, method: "eth_chainId", params: [ ], jsonrpc: "2.0"
- };
- this.emit("debug", { action: "sendRpcPayload", payload });
- let result: JsonRpcResult | JsonRpcError;
- try {
- result = (await this._send(payload))[0];
- this.#pendingDetectNetwork = null;
- } catch (error) {
- this.#pendingDetectNetwork = null;
- this.emit("debug", { action: "receiveRpcError", error });
- throw error;
- }
- this.emit("debug", { action: "receiveRpcResult", result });
- if ("result" in result) {
- return Network.from(getBigInt(result.result));
- }
- throw this.getRpcError(payload, result);
- })();
- return await this.#pendingDetectNetwork;
- }
- /**
- * Sub-classes **MUST** call this. Until [[_start]] has been called, no calls
- * will be passed to [[_send]] from [[send]]. If it is overridden, then
- * ``super._start()`` **MUST** be called.
- *
- * Calling it multiple times is safe and has no effect.
- */
- _start(): void {
- if (this.#notReady == null || this.#notReady.resolve == null) { return; }
- this.#notReady.resolve();
- this.#notReady = null;
- (async () => {
- // Bootstrap the network
- while (this.#network == null && !this.destroyed) {
- try {
- this.#network = await this._detectNetwork();
- } catch (error) {
- if (this.destroyed) { break; }
- console.log("JsonRpcProvider failed to detect network and cannot start up; retry in 1s (perhaps the URL is wrong or the node is not started)");
- this.emit("error", makeError("failed to bootstrap network detection", "NETWORK_ERROR", { event: "initial-network-discovery", info: { error } }));
- await stall(1000);
- }
- }
- // Start dispatching requests
- this.#scheduleDrain();
- })();
- }
- /**
- * Resolves once the [[_start]] has been called. This can be used in
- * sub-classes to defer sending data until the connection has been
- * established.
- */
- async _waitUntilReady(): Promise<void> {
- if (this.#notReady == null) { return; }
- return await this.#notReady.promise;
- }
- /**
- * Return a Subscriber that will manage the %%sub%%.
- *
- * Sub-classes may override this to modify the behavior of
- * subscription management.
- */
- _getSubscriber(sub: Subscription): Subscriber {
- // Pending Filters aren't availble via polling
- if (sub.type === "pending") { return new FilterIdPendingSubscriber(this); }
- if (sub.type === "event") {
- if (this._getOption("polling")) {
- return new PollingEventSubscriber(this, sub.filter);
- }
- return new FilterIdEventSubscriber(this, sub.filter);
- }
- // Orphaned Logs are handled automatically, by the filter, since
- // logs with removed are emitted by it
- if (sub.type === "orphan" && sub.filter.orphan === "drop-log") {
- return new UnmanagedSubscriber("orphan");
- }
- return super._getSubscriber(sub);
- }
- /**
- * Returns true only if the [[_start]] has been called.
- */
- get ready(): boolean { return this.#notReady == null; }
- /**
- * Returns %%tx%% as a normalized JSON-RPC transaction request,
- * which has all values hexlified and any numeric values converted
- * to Quantity values.
- */
- getRpcTransaction(tx: TransactionRequest): JsonRpcTransactionRequest {
- const result: JsonRpcTransactionRequest = {};
- // JSON-RPC now requires numeric values to be "quantity" values
- ["chainId", "gasLimit", "gasPrice", "type", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "value"].forEach((key) => {
- if ((<any>tx)[key] == null) { return; }
- let dstKey = key;
- if (key === "gasLimit") { dstKey = "gas"; }
- (<any>result)[dstKey] = toQuantity(getBigInt((<any>tx)[key], `tx.${ key }`));
- });
- // Make sure addresses and data are lowercase
- ["from", "to", "data"].forEach((key) => {
- if ((<any>tx)[key] == null) { return; }
- (<any>result)[key] = hexlify((<any>tx)[key]);
- });
- // Normalize the access list object
- if (tx.accessList) {
- result["accessList"] = accessListify(tx.accessList);
- }
- if (tx.blobVersionedHashes) {
- // @TODO: Remove this <any> case once EIP-4844 added to prepared tx
- (<any>result)["blobVersionedHashes"] = tx.blobVersionedHashes.map(h => h.toLowerCase());
- }
- // @TODO: blobs should probably also be copied over, optionally
- // accounting for the kzg property to backfill blobVersionedHashes
- // using the commitment. Or should that be left as an exercise to
- // the caller?
- return result;
- }
- /**
- * Returns the request method and arguments required to perform
- * %%req%%.
- */
- getRpcRequest(req: PerformActionRequest): null | { method: string, args: Array<any> } {
- switch (req.method) {
- case "chainId":
- return { method: "eth_chainId", args: [ ] };
- case "getBlockNumber":
- return { method: "eth_blockNumber", args: [ ] };
- case "getGasPrice":
- return { method: "eth_gasPrice", args: [] };
- case "getPriorityFee":
- return { method: "eth_maxPriorityFeePerGas", args: [ ] };
- case "getBalance":
- return {
- method: "eth_getBalance",
- args: [ getLowerCase(req.address), req.blockTag ]
- };
- case "getTransactionCount":
- return {
- method: "eth_getTransactionCount",
- args: [ getLowerCase(req.address), req.blockTag ]
- };
- case "getCode":
- return {
- method: "eth_getCode",
- args: [ getLowerCase(req.address), req.blockTag ]
- };
- case "getStorage":
- return {
- method: "eth_getStorageAt",
- args: [
- getLowerCase(req.address),
- ("0x" + req.position.toString(16)),
- req.blockTag
- ]
- };
- case "broadcastTransaction":
- return {
- method: "eth_sendRawTransaction",
- args: [ req.signedTransaction ]
- };
- case "getBlock":
- if ("blockTag" in req) {
- return {
- method: "eth_getBlockByNumber",
- args: [ req.blockTag, !!req.includeTransactions ]
- };
- } else if ("blockHash" in req) {
- return {
- method: "eth_getBlockByHash",
- args: [ req.blockHash, !!req.includeTransactions ]
- };
- }
- break;
- case "getTransaction":
- return {
- method: "eth_getTransactionByHash",
- args: [ req.hash ]
- };
- case "getTransactionReceipt":
- return {
- method: "eth_getTransactionReceipt",
- args: [ req.hash ]
- };
- case "call":
- return {
- method: "eth_call",
- args: [ this.getRpcTransaction(req.transaction), req.blockTag ]
- };
- case "estimateGas": {
- return {
- method: "eth_estimateGas",
- args: [ this.getRpcTransaction(req.transaction) ]
- };
- }
- case "getLogs":
- if (req.filter && req.filter.address != null) {
- if (Array.isArray(req.filter.address)) {
- req.filter.address = req.filter.address.map(getLowerCase);
- } else {
- req.filter.address = getLowerCase(req.filter.address);
- }
- }
- return { method: "eth_getLogs", args: [ req.filter ] };
- }
- return null;
- }
- /**
- * Returns an ethers-style Error for the given JSON-RPC error
- * %%payload%%, coalescing the various strings and error shapes
- * that different nodes return, coercing them into a machine-readable
- * standardized error.
- */
- getRpcError(payload: JsonRpcPayload, _error: JsonRpcError): Error {
- const { method } = payload;
- const { error } = _error;
- if (method === "eth_estimateGas" && error.message) {
- const msg = error.message;
- if (!msg.match(/revert/i) && msg.match(/insufficient funds/i)) {
- return makeError("insufficient funds", "INSUFFICIENT_FUNDS", {
- transaction: ((<any>payload).params[0]),
- info: { payload, error }
- });
- }
- }
- if (method === "eth_call" || method === "eth_estimateGas") {
- const result = spelunkData(error);
- const e = AbiCoder.getBuiltinCallException(
- (method === "eth_call") ? "call": "estimateGas",
- ((<any>payload).params[0]),
- (result ? result.data: null)
- );
- e.info = { error, payload };
- return e;
- }
- // Only estimateGas and call can return arbitrary contract-defined text, so now we
- // we can process text safely.
- const message = JSON.stringify(spelunkMessage(error));
- if (typeof(error.message) === "string" && error.message.match(/user denied|ethers-user-denied/i)) {
- const actionMap: Record<string, "requestAccess" | "sendTransaction" | "signMessage" | "signTransaction" | "signTypedData"> = {
- eth_sign: "signMessage",
- personal_sign: "signMessage",
- eth_signTypedData_v4: "signTypedData",
- eth_signTransaction: "signTransaction",
- eth_sendTransaction: "sendTransaction",
- eth_requestAccounts: "requestAccess",
- wallet_requestAccounts: "requestAccess",
- };
- return makeError(`user rejected action`, "ACTION_REJECTED", {
- action: (actionMap[method] || "unknown") ,
- reason: "rejected",
- info: { payload, error }
- });
- }
- if (method === "eth_sendRawTransaction" || method === "eth_sendTransaction") {
- const transaction = <TransactionLike<string>>((<any>payload).params[0]);
- if (message.match(/insufficient funds|base fee exceeds gas limit/i)) {
- return makeError("insufficient funds for intrinsic transaction cost", "INSUFFICIENT_FUNDS", {
- transaction, info: { error }
- });
- }
- if (message.match(/nonce/i) && message.match(/too low/i)) {
- return makeError("nonce has already been used", "NONCE_EXPIRED", { transaction, info: { error } });
- }
- // "replacement transaction underpriced"
- if (message.match(/replacement transaction/i) && message.match(/underpriced/i)) {
- return makeError("replacement fee too low", "REPLACEMENT_UNDERPRICED", { transaction, info: { error } });
- }
- if (message.match(/only replay-protected/i)) {
- return makeError("legacy pre-eip-155 transactions not supported", "UNSUPPORTED_OPERATION", {
- operation: method, info: { transaction, info: { error } }
- });
- }
- }
- let unsupported = !!message.match(/the method .* does not exist/i);
- if (!unsupported) {
- if (error && (<any>error).details && (<any>error).details.startsWith("Unauthorized method:")) {
- unsupported = true;
- }
- }
- if (unsupported) {
- return makeError("unsupported operation", "UNSUPPORTED_OPERATION", {
- operation: payload.method, info: { error, payload }
- });
- }
- return makeError("could not coalesce error", "UNKNOWN_ERROR", { error, payload });
- }
- /**
- * Requests the %%method%% with %%params%% via the JSON-RPC protocol
- * over the underlying channel. This can be used to call methods
- * on the backend that do not have a high-level API within the Provider
- * API.
- *
- * This method queues requests according to the batch constraints
- * in the options, assigns the request a unique ID.
- *
- * **Do NOT override** this method in sub-classes; instead
- * override [[_send]] or force the options values in the
- * call to the constructor to modify this method's behavior.
- */
- send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
- // @TODO: cache chainId?? purge on switch_networks
- // We have been destroyed; no operations are supported anymore
- if (this.destroyed) {
- return Promise.reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: method }));
- }
- const id = this.#nextId++;
- const promise = new Promise((resolve, reject) => {
- this.#payloads.push({
- resolve, reject,
- payload: { method, params, id, jsonrpc: "2.0" }
- });
- });
- // If there is not a pending drainTimer, set one
- this.#scheduleDrain();
- return <Promise<JsonRpcResult>>promise;
- }
- /**
- * Resolves to the [[Signer]] account for %%address%% managed by
- * the client.
- *
- * If the %%address%% is a number, it is used as an index in the
- * the accounts from [[listAccounts]].
- *
- * This can only be used on clients which manage accounts (such as
- * Geth with imported account or MetaMask).
- *
- * Throws if the account doesn't exist.
- */
- async getSigner(address?: number | string): Promise<JsonRpcSigner> {
- if (address == null) { address = 0; }
- const accountsPromise = this.send("eth_accounts", [ ]);
- // Account index
- if (typeof(address) === "number") {
- const accounts = <Array<string>>(await accountsPromise);
- if (address >= accounts.length) { throw new Error("no such account"); }
- return new JsonRpcSigner(this, accounts[address]);
- }
- const { accounts } = await resolveProperties({
- network: this.getNetwork(),
- accounts: accountsPromise
- });
- // Account address
- address = getAddress(address);
- for (const account of accounts) {
- if (getAddress(account) === address) {
- return new JsonRpcSigner(this, address);
- }
- }
- throw new Error("invalid account");
- }
- async listAccounts(): Promise<Array<JsonRpcSigner>> {
- const accounts: Array<string> = await this.send("eth_accounts", [ ]);
- return accounts.map((a) => new JsonRpcSigner(this, a));
- }
- destroy(): void {
- // Stop processing requests
- if (this.#drainTimer) {
- clearTimeout(this.#drainTimer);
- this.#drainTimer = null;
- }
- // Cancel all pending requests
- for (const { payload, reject } of this.#payloads) {
- reject(makeError("provider destroyed; cancelled request", "UNSUPPORTED_OPERATION", { operation: payload.method }));
- }
- this.#payloads = [ ];
- // Parent clean-up
- super.destroy();
- }
- }
- // @TODO: remove this in v7, it is not exported because this functionality
- // is exposed in the JsonRpcApiProvider by setting polling to true. It should
- // be safe to remove regardless, because it isn't reachable, but just in case.
- /**
- * @_ignore:
- */
- export abstract class JsonRpcApiPollingProvider extends JsonRpcApiProvider {
- #pollingInterval: number;
- constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
- super(network, options);
- let pollingInterval = this._getOption("pollingInterval");
- if (pollingInterval == null) { pollingInterval = defaultOptions.pollingInterval; }
- this.#pollingInterval = pollingInterval;
- }
- _getSubscriber(sub: Subscription): Subscriber {
- const subscriber = super._getSubscriber(sub);
- if (isPollable(subscriber)) {
- subscriber.pollingInterval = this.#pollingInterval;
- }
- return subscriber;
- }
- /**
- * The polling interval (default: 4000 ms)
- */
- get pollingInterval(): number { return this.#pollingInterval; }
- set pollingInterval(value: number) {
- if (!Number.isInteger(value) || value < 0) { throw new Error("invalid interval"); }
- this.#pollingInterval = value;
- this._forEachSubscriber((sub) => {
- if (isPollable(sub)) {
- sub.pollingInterval = this.#pollingInterval;
- }
- });
- }
- }
- /**
- * The JsonRpcProvider is one of the most common Providers,
- * which performs all operations over HTTP (or HTTPS) requests.
- *
- * Events are processed by polling the backend for the current block
- * number; when it advances, all block-base events are then checked
- * for updates.
- */
- export class JsonRpcProvider extends JsonRpcApiPollingProvider {
- #connect: FetchRequest;
- constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) {
- if (url == null) { url = "http:/\/localhost:8545"; }
- super(network, options);
- if (typeof(url) === "string") {
- this.#connect = new FetchRequest(url);
- } else {
- this.#connect = url.clone();
- }
- }
- _getConnection(): FetchRequest {
- return this.#connect.clone();
- }
- 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 (i.e. eth_chainId) until we absolutely have to.
- await this._start();
- return await super.send(method, params);
- }
- async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult>> {
- // Configure a POST connection for the requested method
- const request = this._getConnection();
- request.body = JSON.stringify(payload);
- request.setHeader("content-type", "application/json");
- const response = await request.send();
- response.assertOk();
- let resp = response.bodyJson;
- if (!Array.isArray(resp)) { resp = [ resp ]; }
- return resp;
- }
- }
- function spelunkData(value: any): null | { message: string, data: string } {
- if (value == null) { return null; }
- // These *are* the droids we're looking for.
- if (typeof(value.message) === "string" && value.message.match(/revert/i) && isHexString(value.data)) {
- return { message: value.message, data: value.data };
- }
- // Spelunk further...
- if (typeof(value) === "object") {
- for (const key in value) {
- const result = spelunkData(value[key]);
- if (result) { return result; }
- }
- return null;
- }
- // Might be a JSON string we can further descend...
- if (typeof(value) === "string") {
- try {
- return spelunkData(JSON.parse(value));
- } catch (error) { }
- }
- return null;
- }
- function _spelunkMessage(value: any, result: Array<string>): void {
- if (value == null) { return; }
- // These *are* the droids we're looking for.
- if (typeof(value.message) === "string") {
- result.push(value.message);
- }
- // Spelunk further...
- if (typeof(value) === "object") {
- for (const key in value) {
- _spelunkMessage(value[key], result);
- }
- }
- // Might be a JSON string we can further descend...
- if (typeof(value) === "string") {
- try {
- return _spelunkMessage(JSON.parse(value), result);
- } catch (error) { }
- }
- }
- function spelunkMessage(value: any): Array<string> {
- const result: Array<string> = [ ];
- _spelunkMessage(value, result);
- return result;
- }
|