More robust blockchain error detection (#1047)
This commit is contained in:
parent
9ee685df46
commit
49f71574f4
@ -85,15 +85,45 @@ function checkLogTag(blockTag: string): number | "latest" {
|
||||
|
||||
const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";
|
||||
|
||||
function checkGasError(error: any, transaction: any): never {
|
||||
function checkError(method: string, error: any, transaction: any): never {
|
||||
|
||||
// Get the message from any nested error structure
|
||||
let message = error.message;
|
||||
if (error.code === Logger.errors.SERVER_ERROR && error.error && typeof(error.error.message) === "string") {
|
||||
message = error.error.message;
|
||||
if (error.code === Logger.errors.SERVER_ERROR) {
|
||||
if (error.error && typeof(error.error.message) === "string") {
|
||||
message = error.error.message;
|
||||
} else if (typeof(error.body) === "string") {
|
||||
message = error.body;
|
||||
} else if (typeof(error.responseText) === "string") {
|
||||
message = error.responseText;
|
||||
}
|
||||
}
|
||||
message = (message || "").toLowerCase();
|
||||
|
||||
// "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
|
||||
if (message.match(/insufficient funds/)) {
|
||||
logger.throwError("insufficient funds for intrinsic transaction cost", Logger.errors.INSUFFICIENT_FUNDS, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
// "Transaction with the same hash was already imported."
|
||||
if (message.match(/same hash was already imported|transaction nonce is too low/)) {
|
||||
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
// "Transaction gas price is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce."
|
||||
if (message.match(/another transaction with same nonce/)) {
|
||||
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
if (message.match(/execution failed due to an exception/)) {
|
||||
logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, {
|
||||
error, transaction
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
@ -215,21 +245,7 @@ export class EtherscanProvider extends BaseProvider{
|
||||
url += "/api?module=proxy&action=eth_sendRawTransaction&hex=" + params.signedTransaction;
|
||||
url += apiKey;
|
||||
return get(url).catch((error) => {
|
||||
if (error.responseText) {
|
||||
// "Insufficient funds. The account you tried to send transaction from does not have enough funds. Required 21464000000000 and got: 0"
|
||||
if (error.responseText.toLowerCase().indexOf("insufficient funds") >= 0) {
|
||||
logger.throwError("insufficient funds", Logger.errors.INSUFFICIENT_FUNDS, { });
|
||||
}
|
||||
// "Transaction with the same hash was already imported."
|
||||
if (error.responseText.indexOf("same hash was already imported") >= 0) {
|
||||
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, { });
|
||||
}
|
||||
// "Transaction gas price is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce."
|
||||
if (error.responseText.indexOf("another transaction with same nonce") >= 0) {
|
||||
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, { });
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
return checkError("sendTransaction", error, params.signedTransaction);
|
||||
});
|
||||
|
||||
case "getBlock":
|
||||
@ -268,7 +284,7 @@ export class EtherscanProvider extends BaseProvider{
|
||||
try {
|
||||
return await get(url);
|
||||
} catch (error) {
|
||||
return checkGasError(error, params.transaction);
|
||||
return checkError("call", error, params.transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,7 +296,7 @@ export class EtherscanProvider extends BaseProvider{
|
||||
try {
|
||||
return await get(url);
|
||||
} catch (error) {
|
||||
return checkGasError(error, params.transaction);
|
||||
return checkError("estimateGas", error, params.transaction);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,16 +18,49 @@ const logger = new Logger(version);
|
||||
import { BaseProvider, Event } from "./base-provider";
|
||||
|
||||
|
||||
const ErrorGas = [ "call", "estimateGas" ];
|
||||
const errorGas = [ "call", "estimateGas" ];
|
||||
|
||||
function getMessage(error: any): string {
|
||||
function checkError(method: string, error: any, params: any): never {
|
||||
let message = error.message;
|
||||
if (error.code === Logger.errors.SERVER_ERROR && error.error && typeof(error.error.message) === "string") {
|
||||
message = error.error.message;
|
||||
} else if (typeof(error.body) === "string") {
|
||||
message = error.body;
|
||||
} else if (typeof(error.responseText) === "string") {
|
||||
message = error.responseText;
|
||||
}
|
||||
return message || "";
|
||||
message = (message || "").toLowerCase();
|
||||
|
||||
const transaction = params.transaction || params.signedTransaction;
|
||||
|
||||
// "insufficient funds for gas * price + value + cost(data)"
|
||||
if (message.match(/insufficient funds/)) {
|
||||
logger.throwError("insufficient funds for intrinsic transaction cost", Logger.errors.INSUFFICIENT_FUNDS, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
// "nonce too low"
|
||||
if (message.match(/nonce too low/)) {
|
||||
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
// "replacement transaction underpriced"
|
||||
if (message.match(/replacement transaction underpriced/)) {
|
||||
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
if (errorGas.indexOf(method) >= 0 && message.match(/gas required exceeds allowance|always failing transaction|execution reverted/)) {
|
||||
logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, {
|
||||
error, method, transaction
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
function timer(timeout: number): Promise<any> {
|
||||
@ -145,25 +178,7 @@ export class JsonRpcSigner extends Signer {
|
||||
return this.provider.send("eth_sendTransaction", [ hexTx ]).then((hash) => {
|
||||
return hash;
|
||||
}, (error) => {
|
||||
if (error.responseText) {
|
||||
// See: JsonRpcProvider.sendTransaction (@TODO: Expose a ._throwError??)
|
||||
if (error.responseText.indexOf("insufficient funds") >= 0) {
|
||||
logger.throwError("insufficient funds", Logger.errors.INSUFFICIENT_FUNDS, {
|
||||
transaction: tx
|
||||
});
|
||||
}
|
||||
if (error.responseText.indexOf("nonce too low") >= 0) {
|
||||
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, {
|
||||
transaction: tx
|
||||
});
|
||||
}
|
||||
if (error.responseText.indexOf("replacement transaction underpriced") >= 0) {
|
||||
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, {
|
||||
transaction: tx
|
||||
});
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
return checkError("sendTransaction", error, hexTx);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -420,42 +435,10 @@ export class JsonRpcProvider extends BaseProvider {
|
||||
logger.throwError(method + " not implemented", Logger.errors.NOT_IMPLEMENTED, { operation: method });
|
||||
}
|
||||
|
||||
// We need a little extra logic to process errors from sendTransaction
|
||||
if (method === "sendTransaction") {
|
||||
try {
|
||||
return await this.send(args[0], args[1]);
|
||||
} catch (error) {
|
||||
const message = getMessage(error);
|
||||
|
||||
// "insufficient funds for gas * price + value"
|
||||
if (message.match(/insufficient funds/)) {
|
||||
logger.throwError("insufficient funds", Logger.errors.INSUFFICIENT_FUNDS, { });
|
||||
}
|
||||
|
||||
// "nonce too low"
|
||||
if (message.match(/nonce too low/)) {
|
||||
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, { });
|
||||
}
|
||||
|
||||
// "replacement transaction underpriced"
|
||||
if (message.match(/replacement transaction underpriced/)) {
|
||||
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, { });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.send(args[0], args[1])
|
||||
} catch (error) {
|
||||
if (ErrorGas.indexOf(method) >= 0 && getMessage(error).match(/gas required exceeds allowance|always failing transaction|execution reverted/)) {
|
||||
logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.errors.UNPREDICTABLE_GAS_LIMIT, {
|
||||
transaction: params.transaction,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
return checkError(method, error, params);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -564,6 +564,110 @@ function testProvider(providerName: string, networkName: string) {
|
||||
});
|
||||
});
|
||||
|
||||
if (networkName === "ropsten") {
|
||||
|
||||
it("throws correct NONCE_EXPIRED errors", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
try {
|
||||
const tx = await provider.sendTransaction("0xf86480850218711a0082520894000000000000000000000000000000000000000002801ba038aaddcaaae7d3fa066dfd6f196c8348e1bb210f2c121d36cb2c24ef20cea1fba008ae378075d3cd75aae99ab75a70da82161dffb2c8263dabc5d8adecfa9447fa");
|
||||
console.log(tx);
|
||||
assert.ok(false);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, ethers.utils.Logger.errors.NONCE_EXPIRED);
|
||||
}
|
||||
|
||||
await waiter(delay);
|
||||
});
|
||||
|
||||
it("throws correct INSUFFICIENT_FUNDS errors", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const txProps = {
|
||||
to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
|
||||
gasPrice: 9000000000,
|
||||
gasLimit: 21000,
|
||||
value: 1
|
||||
};
|
||||
|
||||
const wallet = ethers.Wallet.createRandom();
|
||||
const tx = await wallet.signTransaction(txProps);
|
||||
|
||||
try {
|
||||
await provider.sendTransaction(tx);
|
||||
assert.ok(false);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, ethers.utils.Logger.errors.INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
await waiter(delay);
|
||||
});
|
||||
|
||||
it("throws correct INSUFFICIENT_FUNDS errors (signer)", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const txProps = {
|
||||
to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
|
||||
gasPrice: 9000000000,
|
||||
gasLimit: 21000,
|
||||
value: 1
|
||||
};
|
||||
|
||||
const wallet = ethers.Wallet.createRandom().connect(provider);
|
||||
|
||||
try {
|
||||
await wallet.sendTransaction(txProps);
|
||||
assert.ok(false);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, ethers.utils.Logger.errors.INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
await waiter(delay);
|
||||
});
|
||||
|
||||
it("throws correct UNPREDICTABLE_GAS_LIMIT errors", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
try {
|
||||
await provider.estimateGas({
|
||||
to: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" // ENS; no payable fallback
|
||||
});
|
||||
assert.ok(false);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, ethers.utils.Logger.errors.UNPREDICTABLE_GAS_LIMIT);
|
||||
}
|
||||
|
||||
await waiter(delay);
|
||||
});
|
||||
|
||||
it("sends a transaction", async function() {
|
||||
this.timeout(360000);
|
||||
|
||||
const wallet = ethers.Wallet.createRandom().connect(provider);
|
||||
const funder = await ethers.utils.fetchJson(`https:/\/api.ethers.io/api/v1/?action=fundAccount&address=${ wallet.address.toLowerCase() }`);
|
||||
await provider.waitForTransaction(funder.hash);
|
||||
|
||||
const addr = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
|
||||
const gasPrice = 9000000000;
|
||||
|
||||
let balance = await provider.getBalance(wallet.address);
|
||||
assert.ok(balance.eq(ethers.utils.parseEther("3.141592653589793238")), "balance is pi after funding");
|
||||
|
||||
const tx = await wallet.sendTransaction({
|
||||
to: addr,
|
||||
gasPrice: gasPrice,
|
||||
value: balance.sub(21000 * gasPrice)
|
||||
});
|
||||
|
||||
await tx.wait();
|
||||
|
||||
balance = await provider.getBalance(wallet.address);
|
||||
assert.ok(balance.eq(ethers.constants.Zero), "balance is zero after after sweeping");
|
||||
|
||||
await waiter(delay);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Obviously many more cases to add here
|
||||
// - getTransactionCount
|
||||
|
Loading…
Reference in New Issue
Block a user