diff --git a/packages/providers/src.ts/etherscan-provider.ts b/packages/providers/src.ts/etherscan-provider.ts index dff4f82482..50f62d1450 100644 --- a/packages/providers/src.ts/etherscan-provider.ts +++ b/packages/providers/src.ts/etherscan-provider.ts @@ -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; @@ -250,7 +265,11 @@ 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": { @@ -258,7 +277,11 @@ export class EtherscanProvider extends BaseProvider{ 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": { diff --git a/packages/providers/src.ts/fallback-provider.ts b/packages/providers/src.ts/fallback-provider.ts index 278b555bce..2577930593 100644 --- a/packages/providers/src.ts/fallback-provider.ts +++ b/packages/providers/src.ts/fallback-provider.ts @@ -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; @@ -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) { @@ -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 = ((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 = (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, errorCode, props); + }); + // All configs have run to completion; we will never get more data if (configs.filter((c) => !c.done).length === 0) { break; } } diff --git a/packages/providers/src.ts/json-rpc-provider.ts b/packages/providers/src.ts/json-rpc-provider.ts index e11b51ccf3..cf331cfc31 100644 --- a/packages/providers/src.ts/json-rpc-provider.ts +++ b/packages/providers/src.ts/json-rpc-provider.ts @@ -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 { return new Promise(function(resolve) { setTimeout(resolve, timeout); @@ -401,7 +413,7 @@ export class JsonRpcProvider extends BaseProvider { return null; } - perform(method: string, params: any): Promise { + async perform(method: string, params: any): Promise { const args = this.prepareRequest(method, params); if (args == null) { @@ -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 {