Sync testnets with v5 updates and update to new CALL_EXCEPTION model.
This commit is contained in:
parent
f4539e5675
commit
a667dcbebe
@ -7,6 +7,7 @@
|
||||
// of Signer/ENS name to address so we can sync respond to listenerCount.
|
||||
|
||||
import { resolveAddress } from "../address/index.js";
|
||||
import { Transaction } from "../transaction/index.js";
|
||||
import {
|
||||
concat, dataLength, dataSlice, hexlify, isHexString,
|
||||
getBigInt, getBytes, getNumber,
|
||||
@ -709,7 +710,7 @@ export class AbstractProvider implements Provider {
|
||||
|
||||
} catch (error) {
|
||||
// CCIP Read OffchainLookup
|
||||
if (!this.disableCcipRead && isCallException(error) && attempt >= 0 && blockTag === "latest" && transaction.to != null && dataSlice(error.data, 0, 4) === "0x556f1830") {
|
||||
if (!this.disableCcipRead && isCallException(error) && error.data && attempt >= 0 && blockTag === "latest" && transaction.to != null && dataSlice(error.data, 0, 4) === "0x556f1830") {
|
||||
const data = error.data;
|
||||
|
||||
const txSender = await resolveAddress(transaction.to, this);
|
||||
@ -728,10 +729,16 @@ export class AbstractProvider implements Provider {
|
||||
// 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
|
||||
action: "call",
|
||||
data,
|
||||
reason: "OffchainLookup",
|
||||
transaction: <any>transaction, // @TODO: populate data?
|
||||
invocation: null,
|
||||
revert: {
|
||||
signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
||||
name: "OffchainLookup",
|
||||
args: ccipArgs.errorArgs
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -802,8 +809,21 @@ export class AbstractProvider implements Provider {
|
||||
|
||||
// Write
|
||||
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
|
||||
throw new Error();
|
||||
return <TransactionResponse><unknown>{ };
|
||||
const { blockNumber, hash, network } = await resolveProperties({
|
||||
blockNumber: this.getBlockNumber(),
|
||||
hash: this._perform({
|
||||
method: "broadcastTransaction",
|
||||
signedTransaction: signedTx
|
||||
}),
|
||||
network: this.getNetwork()
|
||||
});
|
||||
|
||||
const tx = Transaction.from(signedTx);
|
||||
if (tx.hash !== hash) {
|
||||
throw new Error("@TODO: the returned hash did not match");
|
||||
}
|
||||
|
||||
return this._wrapTransactionResponse(<any>tx, network).replaceableTransaction(blockNumber);
|
||||
}
|
||||
|
||||
async #getBlock(block: BlockTag | string, includeTransactions: boolean): Promise<any> {
|
||||
|
@ -32,7 +32,7 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
||||
return this.#checkProvider("getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
|
||||
}
|
||||
|
||||
async #populate(op: string, tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
async #populate(op: string, tx: TransactionRequest): Promise<{ provider: Provider, pop: TransactionLike<string> }> {
|
||||
const provider = this.#checkProvider(op);
|
||||
|
||||
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
|
||||
@ -60,17 +60,17 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
||||
});
|
||||
}
|
||||
|
||||
return pop;
|
||||
return { pop: await resolveProperties(pop), provider };
|
||||
}
|
||||
|
||||
async populateCall(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
const pop = await this.#populate("populateCall", tx);
|
||||
const { pop } = await this.#populate("populateCall", tx);
|
||||
|
||||
return pop;
|
||||
}
|
||||
|
||||
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
const pop = await this.#populate("populateTransaction", tx);
|
||||
const { pop, provider } = await this.#populate("populateTransaction", tx);
|
||||
|
||||
if (pop.nonce == null) {
|
||||
pop.nonce = await this.getNonce("pending");
|
||||
@ -91,10 +91,109 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
||||
pop.chainId = network.chainId;
|
||||
}
|
||||
|
||||
// Do not allow mixing pre-eip-1559 and eip-1559 properties
|
||||
const hasEip1559 = (pop.maxFeePerGas != null || pop.maxPriorityFeePerGas != null);
|
||||
if (pop.gasPrice != null && (pop.type === 2 || hasEip1559)) {
|
||||
throwArgumentError("eip-1559 transaction do not support gasPrice", "tx", tx);
|
||||
} else if ((pop.type === 0 || pop.type === 1) && hasEip1559) {
|
||||
throwArgumentError("pre-eip-1559 transaction do not support maxFeePerGas/maxPriorityFeePerGas", "tx", tx);
|
||||
}
|
||||
|
||||
if ((pop.type === 2 || pop.type == null) && (pop.maxFeePerGas != null && pop.maxPriorityFeePerGas != null)) {
|
||||
// Fully-formed EIP-1559 transaction (skip getFeeData)
|
||||
pop.type = 2;
|
||||
|
||||
} else if (pop.type === 0 || pop.type === 1) {
|
||||
// Explicit Legacy or EIP-2930 transaction
|
||||
|
||||
// We need to get fee data to determine things
|
||||
const feeData = await provider.getFeeData();
|
||||
|
||||
if (feeData.gasPrice == null) {
|
||||
throwError("network does not support gasPrice", "UNSUPPORTED_OPERATION", {
|
||||
operation: "getGasPrice"
|
||||
});
|
||||
}
|
||||
|
||||
// Populate missing gasPrice
|
||||
if (pop.gasPrice == null) { pop.gasPrice = feeData.gasPrice; }
|
||||
|
||||
} else {
|
||||
|
||||
// We need to get fee data to determine things
|
||||
const feeData = await provider.getFeeData();
|
||||
|
||||
if (pop.type == null) {
|
||||
// We need to auto-detect the intended type of this transaction...
|
||||
|
||||
if (feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null) {
|
||||
// The network supports EIP-1559!
|
||||
|
||||
// Upgrade transaction from null to eip-1559
|
||||
pop.type = 2;
|
||||
|
||||
if (pop.gasPrice != null) {
|
||||
// Using legacy gasPrice property on an eip-1559 network,
|
||||
// so use gasPrice as both fee properties
|
||||
const gasPrice = pop.gasPrice;
|
||||
delete pop.gasPrice;
|
||||
pop.maxFeePerGas = gasPrice;
|
||||
pop.maxPriorityFeePerGas = gasPrice;
|
||||
|
||||
} else {
|
||||
// Populate missing fee data
|
||||
|
||||
if (pop.maxFeePerGas == null) {
|
||||
pop.maxFeePerGas = feeData.maxFeePerGas;
|
||||
}
|
||||
|
||||
if (pop.maxPriorityFeePerGas == null) {
|
||||
pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (feeData.gasPrice != null) {
|
||||
// Network doesn't support EIP-1559...
|
||||
|
||||
// ...but they are trying to use EIP-1559 properties
|
||||
if (hasEip1559) {
|
||||
throwError("network does not support EIP-1559", "UNSUPPORTED_OPERATION", {
|
||||
operation: "populateTransaction"
|
||||
});
|
||||
}
|
||||
|
||||
// Populate missing fee data
|
||||
if (pop.gasPrice == null) {
|
||||
pop.gasPrice = feeData.gasPrice;
|
||||
}
|
||||
|
||||
// Explicitly set untyped transaction to legacy
|
||||
// @TODO: Maybe this shold allow type 1?
|
||||
pop.type = 0;
|
||||
|
||||
} else {
|
||||
// getFeeData has failed us.
|
||||
throwError("failed to get consistent fee data", "UNSUPPORTED_OPERATION", {
|
||||
operation: "signer.getFeeData"
|
||||
});
|
||||
}
|
||||
|
||||
} else if (pop.type === 2) {
|
||||
// Explicitly using EIP-1559
|
||||
|
||||
// Populate missing fee data
|
||||
if (pop.maxFeePerGas == null) {
|
||||
pop.maxFeePerGas = feeData.maxFeePerGas;
|
||||
}
|
||||
|
||||
if (pop.maxPriorityFeePerGas == null) {
|
||||
pop.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@TOOD: Don't await all over the place; save them up for
|
||||
// the end for better batching
|
||||
//@TODO: Copy type logic from AbstractSigner in v5
|
||||
// Test how many batches is actually sent for sending a tx; compare before/after
|
||||
return await resolveProperties(pop);
|
||||
}
|
||||
|
||||
@ -114,7 +213,8 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
||||
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||
const provider = this.#checkProvider("sendTransaction");
|
||||
|
||||
const txObj = Transaction.from(await this.populateTransaction(tx));
|
||||
const pop = await this.populateTransaction(tx);
|
||||
const txObj = Transaction.from(pop);
|
||||
return await provider.broadcastTransaction(await this.signTransaction(txObj));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getAddress } from "../address/index.js";
|
||||
import { ZeroHash } from "../constants/hashes.js";
|
||||
import { ZeroAddress, ZeroHash } from "../constants/index.js";
|
||||
import { dnsEncode, namehash } from "../hash/index.js";
|
||||
import {
|
||||
concat, dataSlice, getBytes, hexlify, zeroPadValue,
|
||||
@ -197,6 +197,7 @@ export class EnsResolver {
|
||||
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
||||
const tx: TransactionRequest = {
|
||||
to: this.address,
|
||||
from: ZeroAddress,
|
||||
enableCcipRead: true,
|
||||
data: addrData
|
||||
};
|
||||
@ -213,8 +214,9 @@ export class EnsResolver {
|
||||
try {
|
||||
let data = await this.provider.call(tx);
|
||||
if ((getBytes(data).length % 32) === 4) {
|
||||
return throwError("resolver threw error", "CALL_EXCEPTION", {
|
||||
transaction: tx, data
|
||||
return throwError("execution reverted during JSON-RPC call (could not parse reason; invalid data length)", "CALL_EXCEPTION", {
|
||||
action: "call", data, reason: null, transaction: <any>tx,
|
||||
invocation: null, revert: null
|
||||
});
|
||||
}
|
||||
if (wrapped) { return parseBytes(data, 0); }
|
||||
|
@ -1,6 +1,7 @@
|
||||
|
||||
import {
|
||||
defineProperties, FetchRequest, throwArgumentError, throwError
|
||||
defineProperties, resolveProperties, throwArgumentError, throwError,
|
||||
FetchRequest
|
||||
} from "../utils/index.js";
|
||||
|
||||
import { showThrottleMessage } from "./community.js";
|
||||
@ -18,26 +19,21 @@ function getHost(name: string): string {
|
||||
switch(name) {
|
||||
case "mainnet":
|
||||
return "eth-mainnet.alchemyapi.io";
|
||||
case "ropsten":
|
||||
return "eth-ropsten.alchemyapi.io";
|
||||
case "rinkeby":
|
||||
return "eth-rinkeby.alchemyapi.io";
|
||||
case "goerli":
|
||||
return "eth-goerli.alchemyapi.io";
|
||||
case "kovan":
|
||||
return "eth-kovan.alchemyapi.io";
|
||||
return "eth-goerli.g.alchemy.com";
|
||||
|
||||
case "arbitrum":
|
||||
return "arb-mainnet.g.alchemy.com";
|
||||
case "arbitrum-goerli":
|
||||
return "arb-goerli.g.alchemy.com";
|
||||
case "matic":
|
||||
return "polygon-mainnet.g.alchemy.com";
|
||||
case "maticmum":
|
||||
return "polygon-mumbai.g.alchemy.com";
|
||||
case "arbitrum":
|
||||
return "arb-mainnet.g.alchemy.com";
|
||||
case "arbitrum-rinkeby":
|
||||
return "arb-rinkeby.g.alchemy.com";
|
||||
case "optimism":
|
||||
return "opt-mainnet.g.alchemy.com";
|
||||
case "optimism-kovan":
|
||||
return "opt-kovan.g.alchemy.com";
|
||||
case "optimism-goerli":
|
||||
return "opt-goerli.g.alchemy.com";
|
||||
}
|
||||
|
||||
return throwArgumentError("unsupported network", "network", name);
|
||||
@ -67,8 +63,11 @@ export class AlchemyProvider extends JsonRpcProvider implements CommunityResourc
|
||||
|
||||
// https://docs.alchemy.com/reference/trace-transaction
|
||||
if (req.method === "getTransactionResult") {
|
||||
const trace = await this.send("trace_transaction", [ req.hash ]);
|
||||
if (trace == null) { return null; }
|
||||
const { trace, tx } = await resolveProperties({
|
||||
trace: this.send("trace_transaction", [ req.hash ]),
|
||||
tx: this.getTransaction(req.hash)
|
||||
});
|
||||
if (trace == null || tx == null) { return null; }
|
||||
|
||||
let data: undefined | string;
|
||||
let error = false;
|
||||
@ -80,7 +79,12 @@ export class AlchemyProvider extends JsonRpcProvider implements CommunityResourc
|
||||
if (data) {
|
||||
if (error) {
|
||||
throwError("an error occurred during transaction executions", "CALL_EXCEPTION", {
|
||||
data
|
||||
action: "getTransactionResult",
|
||||
data,
|
||||
reason: null,
|
||||
transaction: tx,
|
||||
invocation: null,
|
||||
revert: null // @TODO
|
||||
});
|
||||
}
|
||||
return data;
|
||||
|
120
src.ts/providers/provider-browser.ts
Normal file
120
src.ts/providers/provider-browser.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { assertArgument } from "../utils/index.js";
|
||||
|
||||
import { JsonRpcApiPollingProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type {
|
||||
JsonRpcError, JsonRpcPayload, JsonRpcResult,
|
||||
JsonRpcSigner
|
||||
} from "./provider-jsonrpc.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
export interface Eip1193Provider {
|
||||
request(request: { method: string, params?: Array<any> | Record<string, any> }): Promise<any>;
|
||||
};
|
||||
|
||||
export type DebugEventJsonRpcApiProvider = {
|
||||
action: "sendEip1193Payload",
|
||||
payload: { method: string, params: Array<any> }
|
||||
} | {
|
||||
action: "receiveEip1193Result",
|
||||
result: any
|
||||
} | {
|
||||
action: "receiveEip1193Error",
|
||||
error: Error
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class BrowserProvider extends JsonRpcApiPollingProvider {
|
||||
#request: (method: string, params: Array<any> | Record<string, any>) => Promise<any>;
|
||||
|
||||
constructor(ethereum: Eip1193Provider, network?: Networkish) {
|
||||
super(network, { batchMaxCount: 1 });
|
||||
|
||||
this.#request = async (method: string, params: Array<any> | Record<string, any>) => {
|
||||
const payload = { method, params };
|
||||
this.emit("debug", { action: "sendEip1193Request", payload });
|
||||
try {
|
||||
const result = await ethereum.request(payload);
|
||||
this.emit("debug", { action: "receiveEip1193Result", result });
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
const error = new Error(e.message);
|
||||
(<any>error).code = e.code;
|
||||
(<any>error).data = e.data;
|
||||
(<any>error).payload = payload;
|
||||
this.emit("debug", { action: "receiveEip1193Error", error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
|
||||
await this._start();
|
||||
|
||||
return await super.send(method, params);
|
||||
}
|
||||
|
||||
async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>> {
|
||||
assertArgument(!Array.isArray(payload), "EIP-1193 does not support batch request", "payload", payload);
|
||||
|
||||
try {
|
||||
const result = await this.#request(payload.method, payload.params || [ ]);
|
||||
return [ { id: payload.id, result } ];
|
||||
} catch (e: any) {
|
||||
return [ {
|
||||
id: payload.id,
|
||||
error: { code: e.code, data: e.data, message: e.message }
|
||||
} ];
|
||||
}
|
||||
}
|
||||
|
||||
getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error {
|
||||
|
||||
error = JSON.parse(JSON.stringify(error));
|
||||
|
||||
// EIP-1193 gives us some machine-readable error codes, so rewrite
|
||||
// them into
|
||||
switch (error.error.code || -1) {
|
||||
case 4001:
|
||||
error.error.message = `ethers-user-denied: ${ error.error.message }`;
|
||||
break;
|
||||
case 4200:
|
||||
error.error.message = `ethers-unsupported: ${ error.error.message }`;
|
||||
break;
|
||||
}
|
||||
|
||||
return super.getRpcError(payload, error);
|
||||
}
|
||||
|
||||
async hasSigner(address: number | string): Promise<boolean> {
|
||||
if (address == null) { address = 0; }
|
||||
|
||||
const accounts = await this.send("eth_accounts", [ ]);
|
||||
if (typeof(address) === "number") {
|
||||
return (accounts.length > address);
|
||||
}
|
||||
|
||||
address = address.toLowerCase();
|
||||
return accounts.filter((a: string) => (a.toLowerCase() === address)).length !== 0;
|
||||
}
|
||||
|
||||
async getSigner(address?: number | string): Promise<JsonRpcSigner> {
|
||||
if (address == null) { address = 0; }
|
||||
|
||||
if (!(await this.hasSigner(address))) {
|
||||
try {
|
||||
//const resp =
|
||||
await this.#request("eth_requestAccounts", [ ]);
|
||||
//console.log("RESP", resp);
|
||||
|
||||
} catch (error: any) {
|
||||
const payload = error.payload;
|
||||
throw this.getRpcError(payload, { id: payload.id, error });
|
||||
}
|
||||
}
|
||||
|
||||
return await super.getSigner(address);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { getBuiltinCallException } from "../abi/index.js";
|
||||
import { accessListify } from "../transaction/index.js";
|
||||
import {
|
||||
defineProperties,
|
||||
@ -82,14 +83,23 @@ export class BaseEtherscanProvider extends AbstractProvider {
|
||||
switch(this.network.name) {
|
||||
case "mainnet":
|
||||
return "https:/\/api.etherscan.io";
|
||||
case "ropsten":
|
||||
return "https:/\/api-ropsten.etherscan.io";
|
||||
case "rinkeby":
|
||||
return "https:/\/api-rinkeby.etherscan.io";
|
||||
case "kovan":
|
||||
return "https:/\/api-kovan.etherscan.io";
|
||||
case "goerli":
|
||||
return "https:/\/api-goerli.etherscan.io";
|
||||
case "sepolia":
|
||||
return "https:/\/api-sepolia.etherscan.io";
|
||||
|
||||
case "arbitrum":
|
||||
return "https:/\/api.arbiscan.io";
|
||||
case "arbitrum-goerli":
|
||||
return "https:/\/api-goerli.arbiscan.io";
|
||||
case "matic":
|
||||
return "https:/\/api.polygonscan.com";
|
||||
case "maticmum":
|
||||
return "https:/\/api-testnet.polygonscan.com";
|
||||
case "optimism":
|
||||
return "https:/\/api-optimistic.etherscan.io";
|
||||
case "optimism-goerli":
|
||||
return "https:/\/api-goerli-optimistic.etherscan.io";
|
||||
default:
|
||||
}
|
||||
|
||||
@ -246,6 +256,13 @@ export class BaseEtherscanProvider extends AbstractProvider {
|
||||
|
||||
|
||||
_checkError(req: PerformActionRequest, error: Error, transaction: any): never {
|
||||
if (req.method === "call" || req.method === "estimateGas") {
|
||||
if (error.message.match(/execution reverted/i)) {
|
||||
const e = getBuiltinCallException(req.method, <any>req.transaction, (<any>error).data);
|
||||
e.info = { request: req, error }
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
/*
|
||||
let body = "";
|
||||
if (isError(error, Logger.Errors.SERVER_ERROR) && error.response && error.response.hasBody()) {
|
||||
|
@ -18,26 +18,23 @@ function getHost(name: string): string {
|
||||
switch(name) {
|
||||
case "mainnet":
|
||||
return "mainnet.infura.io";
|
||||
case "ropsten":
|
||||
return "ropsten.infura.io";
|
||||
case "rinkeby":
|
||||
return "rinkeby.infura.io";
|
||||
case "kovan":
|
||||
return "kovan.infura.io";
|
||||
case "goerli":
|
||||
return "goerli.infura.io";
|
||||
case "sepolia":
|
||||
return "sepolia.infura.io";
|
||||
|
||||
case "arbitrum":
|
||||
return "arbitrum-mainnet.infura.io";
|
||||
case "arbitrum-goerli":
|
||||
return "arbitrum-goerli.infura.io";
|
||||
case "matic":
|
||||
return "polygon-mainnet.infura.io";
|
||||
case "maticmum":
|
||||
return "polygon-mumbai.infura.io";
|
||||
case "optimism":
|
||||
return "optimism-mainnet.infura.io";
|
||||
case "optimism-kovan":
|
||||
return "optimism-kovan.infura.io";
|
||||
case "arbitrum":
|
||||
return "arbitrum-mainnet.infura.io";
|
||||
case "arbitrum-rinkeby":
|
||||
return "arbitrum-rinkeby.infura.io";
|
||||
case "optimism-goerli":
|
||||
return "optimism-goerli.infura.io";
|
||||
}
|
||||
|
||||
return throwArgumentError("unsupported network", "network", name);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
// 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 { getBuiltinCallException } from "../abi/index.js";
|
||||
import { getAddress, resolveAddress } from "../address/index.js";
|
||||
import { TypedDataEncoder } from "../hash/index.js";
|
||||
import { accessListify } from "../transaction/index.js";
|
||||
@ -295,7 +296,7 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
|
||||
}
|
||||
|
||||
const hexTx = this.provider.getRpcTransaction(tx);
|
||||
return await this.provider.send("eth_sign_Transaction", [ hexTx ]);
|
||||
return await this.provider.send("eth_signTransaction", [ hexTx ]);
|
||||
}
|
||||
|
||||
|
||||
@ -782,38 +783,113 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
||||
* that different nodes return, coercing them into a machine-readable
|
||||
* standardized error.
|
||||
*/
|
||||
getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error {
|
||||
getRpcError(payload: JsonRpcPayload, _error: JsonRpcError): Error {
|
||||
const { method } = payload;
|
||||
const { error } = _error;
|
||||
|
||||
if (method === "eth_call") {
|
||||
const transaction = <TransactionLike<string>>((<any>payload).params[0]);
|
||||
|
||||
if (method === "eth_call" || method === "eth_estimateGas") {
|
||||
const result = spelunkData(error);
|
||||
|
||||
const e = getBuiltinCallException(
|
||||
(method === "eth_call") ? "call": "estimateGas",
|
||||
((<any>payload).params[0]),
|
||||
(result ? result.data: null)
|
||||
);
|
||||
e.info = { error, payload };
|
||||
return e;
|
||||
/*
|
||||
let message = "missing revert data during JSON-RPC call";
|
||||
|
||||
const action = <"call" | "estimateGas" | "unknown">(({ eth_call: "call", eth_estimateGas: "estimateGas" })[method] || "unknown");
|
||||
let data: null | string = null;
|
||||
let reason: null | string = null;
|
||||
const transaction = <{ from: string, to: string, data: string }>((<any>payload).params[0]);
|
||||
const invocation = null;
|
||||
let revert: null | { signature: string, name: string, args: Array<any> } = null;
|
||||
|
||||
if (result) {
|
||||
// @TODO: Extract errorSignature, errorName, errorArgs, reason if
|
||||
// it is Error(string) or Panic(uint25)
|
||||
return makeError("execution reverted during JSON-RPC call", "CALL_EXCEPTION", {
|
||||
data: result.data,
|
||||
transaction
|
||||
});
|
||||
message = "execution reverted during JSON-RPC call";
|
||||
data = result.data;
|
||||
|
||||
let bytes = getBytes(data);
|
||||
if (bytes.length % 32 !== 4) {
|
||||
message += " (could not parse reason; invalid data length)";
|
||||
|
||||
} else if (data.substring(0, 10) === "0x08c379a0") {
|
||||
// Error(string)
|
||||
try {
|
||||
if (bytes.length < 68) { throw new Error("bad length"); }
|
||||
bytes = bytes.slice(4);
|
||||
const pointer = getNumber(hexlify(bytes.slice(0, 32)));
|
||||
bytes = bytes.slice(pointer);
|
||||
if (bytes.length < 32) { throw new Error("overrun"); }
|
||||
const length = getNumber(hexlify(bytes.slice(0, 32)));
|
||||
bytes = bytes.slice(32);
|
||||
if (bytes.length < length) { throw new Error("overrun"); }
|
||||
reason = toUtf8String(bytes.slice(0, length));
|
||||
revert = {
|
||||
signature: "Error(string)",
|
||||
name: "Error",
|
||||
args: [ reason ]
|
||||
};
|
||||
message += `: ${ JSON.stringify(reason) }`;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
message += " (could not parse reason; invalid data length)";
|
||||
}
|
||||
|
||||
return makeError("missing revert data during JSON-RPC call", "CALL_EXCEPTION", {
|
||||
data: "0x", transaction, info: { error }
|
||||
});
|
||||
} else if (data.substring(0, 10) === "0x4e487b71") {
|
||||
// Panic(uint256)
|
||||
try {
|
||||
if (bytes.length !== 36) { throw new Error("bad length"); }
|
||||
const arg = getNumber(hexlify(bytes.slice(4)));
|
||||
revert = {
|
||||
signature: "Panic(uint256)",
|
||||
name: "Panic",
|
||||
args: [ arg ]
|
||||
};
|
||||
reason = `Panic due to ${ PanicReasons.get(Number(arg)) || "UNKNOWN" }(${ arg })`;
|
||||
message += `: ${ reason }`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
message += " (could not parse panic reason)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return makeError(message, "CALL_EXCEPTION", {
|
||||
action, data, reason, transaction, invocation, revert,
|
||||
info: { payload, error }
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
// 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 (method === "eth_estimateGas") {
|
||||
const transaction = <TransactionLike<string>>((<any>payload).params[0]);
|
||||
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",
|
||||
};
|
||||
|
||||
if (message.match(/gas required exceeds allowance|always failing transaction|execution reverted/)) {
|
||||
return makeError("cannot estimate gas; transaction may fail or may require manual gas limit", "UNPREDICTABLE_GAS_LIMIT", {
|
||||
transaction
|
||||
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]);
|
||||
@ -840,6 +916,12 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (message.match(/the method .* does not exist/i)) {
|
||||
return makeError("unsupported operation", "UNSUPPORTED_OPERATION", {
|
||||
operation: payload.method
|
||||
});
|
||||
}
|
||||
|
||||
return makeError("could not coalesce error", "UNKNOWN_ERROR", { error });
|
||||
}
|
||||
|
||||
@ -893,7 +975,7 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
||||
// Account index
|
||||
if (typeof(address) === "number") {
|
||||
const accounts = <Array<string>>(await accountsPromise);
|
||||
if (address > accounts.length) { throw new Error("no such account"); }
|
||||
if (address >= accounts.length) { throw new Error("no such account"); }
|
||||
return new JsonRpcSigner(this, accounts[address]);
|
||||
}
|
||||
|
||||
@ -914,6 +996,37 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonRpcApiPollingProvider extends JsonRpcApiProvider {
|
||||
#pollingInterval: number;
|
||||
constructor(network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
||||
super(network, options);
|
||||
|
||||
this.#pollingInterval = 4000;
|
||||
}
|
||||
|
||||
_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.
|
||||
@ -922,11 +1035,9 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
||||
* number; when it advances, all block-base events are then checked
|
||||
* for updates.
|
||||
*/
|
||||
export class JsonRpcProvider extends JsonRpcApiProvider {
|
||||
export class JsonRpcProvider extends JsonRpcApiPollingProvider {
|
||||
#connect: FetchRequest;
|
||||
|
||||
#pollingInterval: number;
|
||||
|
||||
constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
||||
if (url == null) { url = "http:/\/localhost:8545"; }
|
||||
super(network, options);
|
||||
@ -936,8 +1047,6 @@ export class JsonRpcProvider extends JsonRpcApiProvider {
|
||||
} else {
|
||||
this.#connect = url.clone();
|
||||
}
|
||||
|
||||
this.#pollingInterval = 4000;
|
||||
}
|
||||
|
||||
_getConnection(): FetchRequest {
|
||||
@ -966,20 +1075,6 @@ export class JsonRpcProvider extends JsonRpcApiProvider {
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function spelunkData(value: any): null | { message: string, data: string } {
|
||||
|
@ -124,13 +124,13 @@ export function copyRequest(req: TransactionRequest): PreparedTransactionRequest
|
||||
if (req.data) { result.data = hexlify(req.data); }
|
||||
|
||||
const bigIntKeys = "chainId,gasLimit,gasPrice,maxFeePerGas, maxPriorityFeePerGas,value".split(/,/);
|
||||
for (const key in bigIntKeys) {
|
||||
for (const key of bigIntKeys) {
|
||||
if (!(key in req) || (<any>req)[key] == null) { continue; }
|
||||
result[key] = getBigInt((<any>req)[key], `request.${ key }`);
|
||||
}
|
||||
|
||||
const numberKeys = "type,nonce".split(/,/);
|
||||
for (const key in numberKeys) {
|
||||
for (const key of numberKeys) {
|
||||
if (!(key in req) || (<any>req)[key] == null) { continue; }
|
||||
result[key] = getNumber((<any>req)[key], `request.${ key }`);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user