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.
|
// of Signer/ENS name to address so we can sync respond to listenerCount.
|
||||||
|
|
||||||
import { resolveAddress } from "../address/index.js";
|
import { resolveAddress } from "../address/index.js";
|
||||||
|
import { Transaction } from "../transaction/index.js";
|
||||||
import {
|
import {
|
||||||
concat, dataLength, dataSlice, hexlify, isHexString,
|
concat, dataLength, dataSlice, hexlify, isHexString,
|
||||||
getBigInt, getBytes, getNumber,
|
getBigInt, getBytes, getNumber,
|
||||||
@ -709,7 +710,7 @@ export class AbstractProvider implements Provider {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// CCIP Read OffchainLookup
|
// 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 data = error.data;
|
||||||
|
|
||||||
const txSender = await resolveAddress(transaction.to, this);
|
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
|
// Check the sender of the OffchainLookup matches the transaction
|
||||||
if (ccipArgs.sender.toLowerCase() !== txSender.toLowerCase()) {
|
if (ccipArgs.sender.toLowerCase() !== txSender.toLowerCase()) {
|
||||||
return throwError("CCIP Read sender mismatch", "CALL_EXCEPTION", {
|
return throwError("CCIP Read sender mismatch", "CALL_EXCEPTION", {
|
||||||
data, transaction,
|
action: "call",
|
||||||
errorSignature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
|
data,
|
||||||
errorName: "OffchainLookup",
|
reason: "OffchainLookup",
|
||||||
errorArgs: ccipArgs.errorArgs
|
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
|
// Write
|
||||||
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
|
async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
|
||||||
throw new Error();
|
const { blockNumber, hash, network } = await resolveProperties({
|
||||||
return <TransactionResponse><unknown>{ };
|
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> {
|
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);
|
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);
|
const provider = this.#checkProvider(op);
|
||||||
|
|
||||||
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
|
//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>> {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
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) {
|
if (pop.nonce == null) {
|
||||||
pop.nonce = await this.getNonce("pending");
|
pop.nonce = await this.getNonce("pending");
|
||||||
@ -91,10 +91,109 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
|||||||
pop.chainId = network.chainId;
|
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
|
//@TOOD: Don't await all over the place; save them up for
|
||||||
// the end for better batching
|
// 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);
|
return await resolveProperties(pop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +213,8 @@ export abstract class AbstractSigner<P extends null | Provider = null | Provider
|
|||||||
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||||
const provider = this.#checkProvider("sendTransaction");
|
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));
|
return await provider.broadcastTransaction(await this.signTransaction(txObj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getAddress } from "../address/index.js";
|
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 { dnsEncode, namehash } from "../hash/index.js";
|
||||||
import {
|
import {
|
||||||
concat, dataSlice, getBytes, hexlify, zeroPadValue,
|
concat, dataSlice, getBytes, hexlify, zeroPadValue,
|
||||||
@ -197,6 +197,7 @@ export class EnsResolver {
|
|||||||
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
||||||
const tx: TransactionRequest = {
|
const tx: TransactionRequest = {
|
||||||
to: this.address,
|
to: this.address,
|
||||||
|
from: ZeroAddress,
|
||||||
enableCcipRead: true,
|
enableCcipRead: true,
|
||||||
data: addrData
|
data: addrData
|
||||||
};
|
};
|
||||||
@ -213,8 +214,9 @@ export class EnsResolver {
|
|||||||
try {
|
try {
|
||||||
let data = await this.provider.call(tx);
|
let data = await this.provider.call(tx);
|
||||||
if ((getBytes(data).length % 32) === 4) {
|
if ((getBytes(data).length % 32) === 4) {
|
||||||
return throwError("resolver threw error", "CALL_EXCEPTION", {
|
return throwError("execution reverted during JSON-RPC call (could not parse reason; invalid data length)", "CALL_EXCEPTION", {
|
||||||
transaction: tx, data
|
action: "call", data, reason: null, transaction: <any>tx,
|
||||||
|
invocation: null, revert: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (wrapped) { return parseBytes(data, 0); }
|
if (wrapped) { return parseBytes(data, 0); }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
defineProperties, FetchRequest, throwArgumentError, throwError
|
defineProperties, resolveProperties, throwArgumentError, throwError,
|
||||||
|
FetchRequest
|
||||||
} from "../utils/index.js";
|
} from "../utils/index.js";
|
||||||
|
|
||||||
import { showThrottleMessage } from "./community.js";
|
import { showThrottleMessage } from "./community.js";
|
||||||
@ -18,26 +19,21 @@ function getHost(name: string): string {
|
|||||||
switch(name) {
|
switch(name) {
|
||||||
case "mainnet":
|
case "mainnet":
|
||||||
return "eth-mainnet.alchemyapi.io";
|
return "eth-mainnet.alchemyapi.io";
|
||||||
case "ropsten":
|
|
||||||
return "eth-ropsten.alchemyapi.io";
|
|
||||||
case "rinkeby":
|
|
||||||
return "eth-rinkeby.alchemyapi.io";
|
|
||||||
case "goerli":
|
case "goerli":
|
||||||
return "eth-goerli.alchemyapi.io";
|
return "eth-goerli.g.alchemy.com";
|
||||||
case "kovan":
|
|
||||||
return "eth-kovan.alchemyapi.io";
|
case "arbitrum":
|
||||||
|
return "arb-mainnet.g.alchemy.com";
|
||||||
|
case "arbitrum-goerli":
|
||||||
|
return "arb-goerli.g.alchemy.com";
|
||||||
case "matic":
|
case "matic":
|
||||||
return "polygon-mainnet.g.alchemy.com";
|
return "polygon-mainnet.g.alchemy.com";
|
||||||
case "maticmum":
|
case "maticmum":
|
||||||
return "polygon-mumbai.g.alchemy.com";
|
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":
|
case "optimism":
|
||||||
return "opt-mainnet.g.alchemy.com";
|
return "opt-mainnet.g.alchemy.com";
|
||||||
case "optimism-kovan":
|
case "optimism-goerli":
|
||||||
return "opt-kovan.g.alchemy.com";
|
return "opt-goerli.g.alchemy.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwArgumentError("unsupported network", "network", name);
|
return throwArgumentError("unsupported network", "network", name);
|
||||||
@ -67,8 +63,11 @@ export class AlchemyProvider extends JsonRpcProvider implements CommunityResourc
|
|||||||
|
|
||||||
// https://docs.alchemy.com/reference/trace-transaction
|
// https://docs.alchemy.com/reference/trace-transaction
|
||||||
if (req.method === "getTransactionResult") {
|
if (req.method === "getTransactionResult") {
|
||||||
const trace = await this.send("trace_transaction", [ req.hash ]);
|
const { trace, tx } = await resolveProperties({
|
||||||
if (trace == null) { return null; }
|
trace: this.send("trace_transaction", [ req.hash ]),
|
||||||
|
tx: this.getTransaction(req.hash)
|
||||||
|
});
|
||||||
|
if (trace == null || tx == null) { return null; }
|
||||||
|
|
||||||
let data: undefined | string;
|
let data: undefined | string;
|
||||||
let error = false;
|
let error = false;
|
||||||
@ -80,7 +79,12 @@ export class AlchemyProvider extends JsonRpcProvider implements CommunityResourc
|
|||||||
if (data) {
|
if (data) {
|
||||||
if (error) {
|
if (error) {
|
||||||
throwError("an error occurred during transaction executions", "CALL_EXCEPTION", {
|
throwError("an error occurred during transaction executions", "CALL_EXCEPTION", {
|
||||||
data
|
action: "getTransactionResult",
|
||||||
|
data,
|
||||||
|
reason: null,
|
||||||
|
transaction: tx,
|
||||||
|
invocation: null,
|
||||||
|
revert: null // @TODO
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return data;
|
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 { accessListify } from "../transaction/index.js";
|
||||||
import {
|
import {
|
||||||
defineProperties,
|
defineProperties,
|
||||||
@ -82,14 +83,23 @@ export class BaseEtherscanProvider extends AbstractProvider {
|
|||||||
switch(this.network.name) {
|
switch(this.network.name) {
|
||||||
case "mainnet":
|
case "mainnet":
|
||||||
return "https:/\/api.etherscan.io";
|
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":
|
case "goerli":
|
||||||
return "https:/\/api-goerli.etherscan.io";
|
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:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,6 +256,13 @@ export class BaseEtherscanProvider extends AbstractProvider {
|
|||||||
|
|
||||||
|
|
||||||
_checkError(req: PerformActionRequest, error: Error, transaction: any): never {
|
_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 = "";
|
let body = "";
|
||||||
if (isError(error, Logger.Errors.SERVER_ERROR) && error.response && error.response.hasBody()) {
|
if (isError(error, Logger.Errors.SERVER_ERROR) && error.response && error.response.hasBody()) {
|
||||||
|
@ -18,26 +18,23 @@ function getHost(name: string): string {
|
|||||||
switch(name) {
|
switch(name) {
|
||||||
case "mainnet":
|
case "mainnet":
|
||||||
return "mainnet.infura.io";
|
return "mainnet.infura.io";
|
||||||
case "ropsten":
|
|
||||||
return "ropsten.infura.io";
|
|
||||||
case "rinkeby":
|
|
||||||
return "rinkeby.infura.io";
|
|
||||||
case "kovan":
|
|
||||||
return "kovan.infura.io";
|
|
||||||
case "goerli":
|
case "goerli":
|
||||||
return "goerli.infura.io";
|
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":
|
case "matic":
|
||||||
return "polygon-mainnet.infura.io";
|
return "polygon-mainnet.infura.io";
|
||||||
case "maticmum":
|
case "maticmum":
|
||||||
return "polygon-mumbai.infura.io";
|
return "polygon-mumbai.infura.io";
|
||||||
case "optimism":
|
case "optimism":
|
||||||
return "optimism-mainnet.infura.io";
|
return "optimism-mainnet.infura.io";
|
||||||
case "optimism-kovan":
|
case "optimism-goerli":
|
||||||
return "optimism-kovan.infura.io";
|
return "optimism-goerli.infura.io";
|
||||||
case "arbitrum":
|
|
||||||
return "arbitrum-mainnet.infura.io";
|
|
||||||
case "arbitrum-rinkeby":
|
|
||||||
return "arbitrum-rinkeby.infura.io";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwArgumentError("unsupported network", "network", name);
|
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
|
// 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 { 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";
|
||||||
@ -295,7 +296,7 @@ export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hexTx = this.provider.getRpcTransaction(tx);
|
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
|
* that different nodes return, coercing them into a machine-readable
|
||||||
* standardized error.
|
* standardized error.
|
||||||
*/
|
*/
|
||||||
getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error {
|
getRpcError(payload: JsonRpcPayload, _error: JsonRpcError): Error {
|
||||||
const { method } = payload;
|
const { method } = payload;
|
||||||
|
const { error } = _error;
|
||||||
|
|
||||||
if (method === "eth_call") {
|
if (method === "eth_call" || method === "eth_estimateGas") {
|
||||||
const transaction = <TransactionLike<string>>((<any>payload).params[0]);
|
|
||||||
|
|
||||||
const result = spelunkData(error);
|
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) {
|
if (result) {
|
||||||
// @TODO: Extract errorSignature, errorName, errorArgs, reason if
|
// @TODO: Extract errorSignature, errorName, errorArgs, reason if
|
||||||
// it is Error(string) or Panic(uint25)
|
// it is Error(string) or Panic(uint25)
|
||||||
return makeError("execution reverted during JSON-RPC call", "CALL_EXCEPTION", {
|
message = "execution reverted during JSON-RPC call";
|
||||||
data: result.data,
|
data = result.data;
|
||||||
transaction
|
|
||||||
});
|
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", {
|
} else if (data.substring(0, 10) === "0x4e487b71") {
|
||||||
data: "0x", transaction, info: { error }
|
// 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));
|
const message = JSON.stringify(spelunkMessage(error));
|
||||||
|
|
||||||
if (method === "eth_estimateGas") {
|
if (typeof(error.message) === "string" && error.message.match(/user denied|ethers-user-denied/i)) {
|
||||||
const transaction = <TransactionLike<string>>((<any>payload).params[0]);
|
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(`user rejected action`, "ACTION_REJECTED", {
|
||||||
return makeError("cannot estimate gas; transaction may fail or may require manual gas limit", "UNPREDICTABLE_GAS_LIMIT", {
|
action: (actionMap[method] || "unknown") ,
|
||||||
transaction
|
reason: "rejected",
|
||||||
|
info: { payload, error }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (method === "eth_sendRawTransaction" || method === "eth_sendTransaction") {
|
if (method === "eth_sendRawTransaction" || method === "eth_sendTransaction") {
|
||||||
const transaction = <TransactionLike<string>>((<any>payload).params[0]);
|
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 });
|
return makeError("could not coalesce error", "UNKNOWN_ERROR", { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -893,7 +975,7 @@ export class JsonRpcApiProvider extends AbstractProvider {
|
|||||||
// Account index
|
// Account index
|
||||||
if (typeof(address) === "number") {
|
if (typeof(address) === "number") {
|
||||||
const accounts = <Array<string>>(await accountsPromise);
|
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]);
|
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,
|
* The JsonRpcProvider is one of the most common Providers,
|
||||||
* which performs all operations over HTTP (or HTTPS) requests.
|
* 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
|
* number; when it advances, all block-base events are then checked
|
||||||
* for updates.
|
* for updates.
|
||||||
*/
|
*/
|
||||||
export class JsonRpcProvider extends JsonRpcApiProvider {
|
export class JsonRpcProvider extends JsonRpcApiPollingProvider {
|
||||||
#connect: FetchRequest;
|
#connect: FetchRequest;
|
||||||
|
|
||||||
#pollingInterval: number;
|
|
||||||
|
|
||||||
constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcApiProviderOptions) {
|
||||||
if (url == null) { url = "http:/\/localhost:8545"; }
|
if (url == null) { url = "http:/\/localhost:8545"; }
|
||||||
super(network, options);
|
super(network, options);
|
||||||
@ -936,8 +1047,6 @@ export class JsonRpcProvider extends JsonRpcApiProvider {
|
|||||||
} else {
|
} else {
|
||||||
this.#connect = url.clone();
|
this.#connect = url.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#pollingInterval = 4000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getConnection(): FetchRequest {
|
_getConnection(): FetchRequest {
|
||||||
@ -966,20 +1075,6 @@ export class JsonRpcProvider extends JsonRpcApiProvider {
|
|||||||
|
|
||||||
return resp;
|
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 } {
|
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); }
|
if (req.data) { result.data = hexlify(req.data); }
|
||||||
|
|
||||||
const bigIntKeys = "chainId,gasLimit,gasPrice,maxFeePerGas, maxPriorityFeePerGas,value".split(/,/);
|
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; }
|
if (!(key in req) || (<any>req)[key] == null) { continue; }
|
||||||
result[key] = getBigInt((<any>req)[key], `request.${ key }`);
|
result[key] = getBigInt((<any>req)[key], `request.${ key }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberKeys = "type,nonce".split(/,/);
|
const numberKeys = "type,nonce".split(/,/);
|
||||||
for (const key in numberKeys) {
|
for (const key of numberKeys) {
|
||||||
if (!(key in req) || (<any>req)[key] == null) { continue; }
|
if (!(key in req) || (<any>req)[key] == null) { continue; }
|
||||||
result[key] = getNumber((<any>req)[key], `request.${ key }`);
|
result[key] = getNumber((<any>req)[key], `request.${ key }`);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user