Skip to content

Commit

Permalink
Better error forwarding from FallbackProvider (#1021).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Sep 11, 2020
1 parent 042b74e commit bc3eeec
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 20 deletions.
27 changes: 25 additions & 2 deletions packages/providers/src.ts/etherscan-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ function checkLogTag(blockTag: string): number | "latest" {

const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";

function checkGasError(error: any, transaction: any): never {
let message = error.message;
if (error.code === Logger.errors.SERVER_ERROR && error.error && typeof(error.error.message) === "string") {
message = error.error.message;
}

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
});
}

throw error;
}

export class EtherscanProvider extends BaseProvider{
readonly baseUrl: string;
readonly apiKey: string;
Expand Down Expand Up @@ -250,15 +265,23 @@ export class EtherscanProvider extends BaseProvider{
throw new Error("EtherscanProvider does not support blockTag for call");
}
url += apiKey;
return get(url);
try {
return await get(url);
} catch (error) {
return checkGasError(error, params.transaction);
}
}

case "estimateGas": {
let transaction = getTransactionString(params.transaction);
if (transaction) { transaction = "&" + transaction; }
url += "/api?module=proxy&action=eth_estimateGas&" + transaction;
url += apiKey;
return get(url);
try {
return await get(url);
} catch (error) {
return checkGasError(error, params.transaction);
}
}

case "getLogs": {
Expand Down
54 changes: 53 additions & 1 deletion packages/providers/src.ts/fallback-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@ function stall(duration: number): Staller {
return { cancel, getPromise, wait };
}

const ForwardErrors = [
Logger.errors.CALL_EXCEPTION,
Logger.errors.INSUFFICIENT_FUNDS,
Logger.errors.NONCE_EXPIRED,
Logger.errors.REPLACEMENT_UNDERPRICED,
Logger.errors.UNPREDICTABLE_GAS_LIMIT
];

const ForwardProperties = [
"address",
"args",
"errorArgs",
"errorSignature",
"method",
"transaction",
];


// @TODO: Make this an object with staller and cancel built-in
interface RunningConfig extends FallbackProviderConfig {
start?: number;
Expand All @@ -161,9 +179,9 @@ interface RunningConfig extends FallbackProviderConfig {

function exposeDebugConfig(config: RunningConfig, now?: number): any {
const result: any = {
provider: config.provider,
weight: config.weight
};
Object.defineProperty(result, "provider", { get: () => config.provider });
if (config.start) { result.start = config.start; }
if (now) { result.duration = (now - config.start); }
if (config.done) {
Expand Down Expand Up @@ -574,6 +592,40 @@ export class FallbackProvider extends BaseProvider {
first = false;
}

// No result, check for errors that should be forwarded
const errors = configs.reduce((accum, c) => {
if (!c.done || c.error == null) { return accum; }

const code = (<any>(c.error)).code;
if (ForwardErrors.indexOf(code) >= 0) {
if (!accum[code]) { accum[code] = { error: c.error, weight: 0 }; }
accum[code].weight += c.weight;
}

return accum;
}, <{ [ code: string ]: { error: Error, weight: number } }>({ }));

Object.keys(errors).forEach((errorCode: string) => {
const tally = errors[errorCode];
if (tally.weight < this.quorum) { return; }

// Shut down any stallers
configs.forEach(c => {
if (c.staller) { c.staller.cancel(); }
c.cancelled = true;
});

const e = <any>(tally.error);

const props: { [ name: string ]: any } = { };
ForwardProperties.forEach((name) => {
if (e[name] == null) { return; }
props[name] = e[name];
});

logger.throwError(e.reason || e.message, <any>errorCode, props);
});

// All configs have run to completion; we will never get more data
if (configs.filter((c) => !c.done).length === 0) { break; }
}
Expand Down
61 changes: 44 additions & 17 deletions packages/providers/src.ts/json-rpc-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ const logger = new Logger(version);
import { BaseProvider, Event } from "./base-provider";


const ErrorGas = [ "call", "estimateGas" ];

function getMessage(error: any): string {
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.responseText) === "string") {
message = error.responseText;
}
return message || "";
}

function timer(timeout: number): Promise<any> {
return new Promise(function(resolve) {
setTimeout(resolve, timeout);
Expand Down Expand Up @@ -401,7 +413,7 @@ export class JsonRpcProvider extends BaseProvider {
return null;
}

perform(method: string, params: any): Promise<any> {
async perform(method: string, params: any): Promise<any> {
const args = this.prepareRequest(method, params);

if (args == null) {
Expand All @@ -410,26 +422,41 @@ export class JsonRpcProvider extends BaseProvider {

// We need a little extra logic to process errors from sendTransaction
if (method === "sendTransaction") {
return this.send(args[0], args[1]).catch((error) => {
if (error.responseText) {
// "insufficient funds for gas * price + value"
if (error.responseText.indexOf("insufficient funds") > 0) {
logger.throwError("insufficient funds", Logger.errors.INSUFFICIENT_FUNDS, { });
}
// "nonce too low"
if (error.responseText.indexOf("nonce too low") > 0) {
logger.throwError("nonce has already been used", Logger.errors.NONCE_EXPIRED, { });
}
// "replacement transaction underpriced"
if (error.responseText.indexOf("replacement transaction underpriced") > 0) {
logger.throwError("replacement fee too low", Logger.errors.REPLACEMENT_UNDERPRICED, { });
}
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;
});
}
}

return this.send(args[0], args[1])
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;
}
}

_startEvent(event: Event): void {
Expand Down

0 comments on commit bc3eeec

Please sign in to comment.