UNPKG

@lifi/rpc-wrapper

Version:
434 lines (433 loc) 19.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseError = exports.TransactionProcessingError = exports.InitialSubmitFailure = exports.ConfigurationError = exports.ProviderNotConfigured = exports.ChainNotSupported = exports.GasEstimateInvalid = exports.NotEnoughConfirmations = exports.MaxAttemptsReached = exports.TransactionKilled = exports.TransactionAlreadyKnown = exports.ServerError = exports.BadNonce = exports.UnpredictableGasLimit = exports.TransactionBackfilled = exports.OperationTimeout = exports.TransactionReplaced = exports.TransactionReverted = exports.TransactionReadError = exports.RpcError = exports.StallTimeout = exports.MaxBufferLengthError = void 0; const error_1 = require("./error"); const utils_1 = require("ethers/lib/utils"); class MaxBufferLengthError extends error_1.CustomError { constructor(context = {}) { super('Inflight transaction buffer is full.', context, MaxBufferLengthError.type); this.context = context; } } exports.MaxBufferLengthError = MaxBufferLengthError; /** * Thrown if a backfill transaction fails and other txs are attempted */ MaxBufferLengthError.type = MaxBufferLengthError.name; class StallTimeout extends error_1.CustomError { constructor(context = {}) { super('Request stalled and timed out.', context, StallTimeout.type); this.context = context; } } exports.StallTimeout = StallTimeout; StallTimeout.type = StallTimeout.name; class RpcError extends error_1.CustomError { constructor(reason, context = {}) { const errors = (context.errors ? context.errors : []) .map((e, i) => `-${i}: ${e}`) .join(';\n'); const stringifiedContext = Object.entries(Object.assign(Object.assign({}, context), { errors })) .map((k, v) => `${k}: ${v}`) .join('\n'); super(reason + `\n{${stringifiedContext}}`, context, RpcError.type); this.reason = reason; this.context = context; } } exports.RpcError = RpcError; RpcError.type = RpcError.name; /** * Indicates the RPC Providers are malfunctioning. If errors of this type persist, * ensure you have a sufficient number of backup providers configured. */ RpcError.reasons = { OutOfSync: 'All providers for this chain fell out of sync with the chain.', FailedToSend: 'Failed to send RPC transaction.', NetworkError: 'An RPC network error occurred.', ConnectionReset: 'Connection was reset by peer.', }; class TransactionReadError extends error_1.CustomError { constructor(reason, context = {}) { super(reason, context, TransactionReadError.type); this.reason = reason; this.context = context; } } exports.TransactionReadError = TransactionReadError; /** * An error that indicates that a read transaction failed. */ TransactionReadError.type = TransactionReadError.name; TransactionReadError.reasons = { ContractReadError: 'An exception occurred while trying to read from the contract.', }; class TransactionReverted extends error_1.CustomError { constructor(reason, receipt, context = {}) { super(reason, context, TransactionReverted.type); this.reason = reason; this.receipt = receipt; this.context = context; } } exports.TransactionReverted = TransactionReverted; /** * An error that indicates that the transaction was reverted on-chain. * * Could be harmless if this was from a subsuquent attempt, e.g. if the tx * was already mined (NonceExpired, AlreadyMined) * * Alternatively, if this is from the first attempt, it must be thrown as the reversion * was for a legitimate reason. */ TransactionReverted.type = TransactionReverted.name; TransactionReverted.reasons = { GasEstimateFailed: 'Operation for gas estimate failed; transaction was reverted on-chain.', InsufficientFunds: 'Not enough funds in wallet.', /** * From ethers docs: * If the transaction execution failed (i.e. the receipt status is 0), a CALL_EXCEPTION error will be rejected with the following properties: * error.transaction - the original transaction * error.transactionHash - the hash of the transaction * error.receipt - the actual receipt, with the status of 0 */ CallException: 'An exception occurred during this contract call.', /** * No difference between the following two errors, except to distinguish a message we * get back from providers on execution failure. */ ExecutionFailed: 'Transaction would fail on chain.', AlwaysFailingTransaction: 'Transaction would always fail on chain.', GasExceedsAllowance: 'Transaction gas exceeds allowance.', }; class TransactionReplaced extends error_1.CustomError { constructor(receipt, replacement, context = {}) { super('Transaction replaced.', context, TransactionReplaced.type); this.receipt = receipt; this.replacement = replacement; this.context = context; } } exports.TransactionReplaced = TransactionReplaced; /** * From ethers docs: * If the transaction is replaced by another transaction, a TRANSACTION_REPLACED error will be rejected with the following properties: * error.hash - the hash of the original transaction which was replaced * error.reason - a string reason; one of "repriced", "cancelled" or "replaced" * error.cancelled - a boolean; a "repriced" transaction is not considered cancelled, but "cancelled" and "replaced" are * error.replacement - the replacement transaction (a TransactionResponse) * error.receipt - the receipt of the replacement transaction (a TransactionReceipt) */ TransactionReplaced.type = TransactionReplaced.name; // TODO: #144 Some of these error classes are a bit of an antipattern with the whole "reason" argument structure // being missing. They won't function as proper CustomErrors, essentially. class OperationTimeout extends error_1.CustomError { constructor(context = {}) { super('Operation timed out.', context, OperationTimeout.type); this.context = context; } } exports.OperationTimeout = OperationTimeout; /** * An error indicating that an operation (typically confirmation) timed out. */ OperationTimeout.type = OperationTimeout.name; class TransactionBackfilled extends error_1.CustomError { constructor(context = {}) { super('Transaction was replaced by a backfill.', context, TransactionBackfilled.type); this.context = context; } } exports.TransactionBackfilled = TransactionBackfilled; /** * An error indicating that a transaction was replaced by a backfill, likely because it * was unresponsive. */ TransactionBackfilled.type = TransactionBackfilled.name; class UnpredictableGasLimit extends error_1.CustomError { constructor(context = {}) { super('The gas estimate could not be determined.', context, UnpredictableGasLimit.type); this.context = context; } } exports.UnpredictableGasLimit = UnpredictableGasLimit; /** * An error that we get back from ethers when we try to do a gas estimate, but this * may need to be handled differently. */ UnpredictableGasLimit.type = UnpredictableGasLimit.name; class BadNonce extends error_1.CustomError { constructor(reason, context = {}) { super(reason, context, BadNonce.type); this.reason = reason; this.context = context; } } exports.BadNonce = BadNonce; /** * An error indicating that we got a "nonce expired"-like message back from * ethers while conducting sendTransaction. */ BadNonce.type = BadNonce.name; BadNonce.reasons = { NonceExpired: 'Nonce for this transaction is already expired.', ReplacementUnderpriced: "Gas for replacement tx was insufficient (must be greater than previous transaction's gas).", NonceIncorrect: "Transaction doesn't have the correct nonce", }; class ServerError extends error_1.CustomError { constructor(reason, context = {}) { const stringifiedContext = Object.entries(context) .map((k, v) => `${k}: ${v}`) .join(';'); super((reason !== null && reason !== void 0 ? reason : 'Server error occurred.') + `{${stringifiedContext}}`, context, ServerError.type); this.reason = reason; this.context = context; } } exports.ServerError = ServerError; /** * An error indicating that an operation on the node server (such as validation * before submitting a transaction) occurred. * * This error could directly come from geth, or be altered by the node server, * depending on which service is used. As a result, we coerce this to a single error * type. */ ServerError.type = ServerError.name; ServerError.reasons = { BadResponse: 'Received bad response from provider.', }; class TransactionAlreadyKnown extends error_1.CustomError { constructor(context = {}) { super('Transaction is already indexed by provider.', context, TransactionAlreadyKnown.type); this.context = context; } } exports.TransactionAlreadyKnown = TransactionAlreadyKnown; /** * This one occurs (usually) when we try to send a transaction to multiple providers * and one or more of them already has the transaction in their mempool. */ TransactionAlreadyKnown.type = TransactionAlreadyKnown.name; class TransactionKilled extends error_1.CustomError { constructor(context = {}) { super('Transaction was killed by monitor loop.', context, TransactionKilled.type); this.context = context; } } exports.TransactionKilled = TransactionKilled; /** * An error indicating that the transaction was killed by the monitor loop due to * it taking too long, and blocking (potentially too many) transactions in the pending * queue. * * It will be replaced with a backfill transaction at max gas. */ TransactionKilled.type = TransactionKilled.name; class MaxAttemptsReached extends error_1.CustomError { constructor(attempts, context = {}) { super(MaxAttemptsReached.getMessage(attempts), context, MaxAttemptsReached.type); this.context = context; } static getMessage(attempts) { return `Reached maximum attempts ${attempts}.`; } } exports.MaxAttemptsReached = MaxAttemptsReached; MaxAttemptsReached.type = MaxAttemptsReached.name; class NotEnoughConfirmations extends error_1.CustomError { constructor(required, hash, confs, context = {}) { super(NotEnoughConfirmations.getMessage(required, hash, confs), context, NotEnoughConfirmations.type); this.context = context; } static getMessage(required, hash, confs) { return `Never reached the required amount of confirmations (${required}) on ${hash} (got: ${confs}). Did a reorg occur?`; } } exports.NotEnoughConfirmations = NotEnoughConfirmations; NotEnoughConfirmations.type = NotEnoughConfirmations.name; class GasEstimateInvalid extends error_1.CustomError { constructor(returned, context = {}) { super(GasEstimateInvalid.getMessage(returned), context, GasEstimateInvalid.type); this.context = context; } static getMessage(returned) { return `The gas estimate returned was an invalid value. Got: ${returned}`; } } exports.GasEstimateInvalid = GasEstimateInvalid; GasEstimateInvalid.type = GasEstimateInvalid.name; class ChainNotSupported extends error_1.CustomError { constructor(chainId, context = {}) { super(ChainNotSupported.getMessage(chainId), context, ChainNotSupported.type); this.chainId = chainId; this.context = context; } static getMessage(chainId) { return `Request for chain ${chainId} cannot be handled: resources not configured.`; } } exports.ChainNotSupported = ChainNotSupported; ChainNotSupported.type = ChainNotSupported.name; // TODO: ProviderNotConfigured is essentially a more specific ChainNotSupported error. Should they be combined? class ProviderNotConfigured extends error_1.CustomError { constructor(chainId, context = {}) { super(ProviderNotConfigured.getMessage(chainId), context, ProviderNotConfigured.type); this.chainId = chainId; this.context = context; } static getMessage(chainId) { return `No provider(s) configured for chain ${chainId}. Make sure this chain's providers are configured.`; } } exports.ProviderNotConfigured = ProviderNotConfigured; ProviderNotConfigured.type = ProviderNotConfigured.name; class ConfigurationError extends error_1.CustomError { constructor(invalidParameters, context = {}) { super('Configuration paramater(s) were invalid.', Object.assign(Object.assign({}, context), { invalidParameters }), ConfigurationError.type); this.invalidParameters = invalidParameters; this.context = context; } } exports.ConfigurationError = ConfigurationError; ConfigurationError.type = ConfigurationError.name; class InitialSubmitFailure extends error_1.CustomError { constructor(context = {}) { super('Transaction never submitted: exceeded maximum iterations in initial submit loop.', context, InitialSubmitFailure.type); this.context = context; } } exports.InitialSubmitFailure = InitialSubmitFailure; InitialSubmitFailure.type = InitialSubmitFailure.name; // These errors should essentially never happen; they are only used within the block of sanity checks. class TransactionProcessingError extends error_1.CustomError { constructor(reason, method, context = {}) { super(reason, Object.assign(Object.assign({}, context), { method }), TransactionProcessingError.type); this.reason = reason; this.method = method; this.context = context; } } exports.TransactionProcessingError = TransactionProcessingError; TransactionProcessingError.type = TransactionProcessingError.name; TransactionProcessingError.reasons = { SubmitOutOfOrder: 'Submit was called but transaction is already completed.', MineOutOfOrder: 'Transaction mine or confirm was called, but no transaction has been sent.', ConfirmOutOfOrder: "Tried to confirm but tansaction did not complete 'mine' step; no receipt was found.", DuplicateHash: 'Received a transaction response with a duplicate hash!', NoReceipt: 'No receipt was returned from the transaction.', NullReceipt: 'Unable to obtain receipt: ethers responded with null.', ReplacedButNoReplacement: 'Transaction was replaced, but no replacement transaction and/or receipt was returned.', DidNotThrowRevert: 'Transaction was reverted but TransactionReverted error was not thrown.', InsufficientConfirmations: 'Receipt did not have enough confirmations, should have timed out!', }; /** * Parses error strings into strongly typed CustomError. * @param error from ethers.js package * @returns CustomError */ const parseError = (error, ctx = {}) => { var _a, _b, _c; if (error.isCustomError) { // If the error has already been parsed into a native error, just return it. return error; } let message = error.message; 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; } // Preserve error data, if applicable. let data = ''; if (error.data) { if (error.data.data) { data = error.data.data.toString(); } else { data = error.data.toString(); } } else if ((_a = error.error) === null || _a === void 0 ? void 0 : _a.data) { if (error.error.data.data) { data = error.error.data.data; } else { data = error.error.data; } } else if (error.body) { if (error.body.data) { if (error.body.data.data) { data = error.body.data.data; } else { data = error.body.data; } } } // Preserve the original message before making it lower case. const originalMessage = message; message = (message || '').toLowerCase(); let args = 'n/a'; if (ctx.args) args = JSON.stringify(ctx.args, null, 2); const context = Object.assign(Object.assign({}, ctx), { args, data: data !== null && data !== void 0 ? data : 'n/a', message: originalMessage, code: (_b = error.code) !== null && _b !== void 0 ? _b : 'n/a', reason: (_c = error.reason) !== null && _c !== void 0 ? _c : 'n/a' }); if (message.match(/execution reverted/)) { return new TransactionReverted(TransactionReverted.reasons.ExecutionFailed, undefined, context); } else if (message.match(/always failing transaction/)) { return new TransactionReverted(TransactionReverted.reasons.AlwaysFailingTransaction, undefined, context); } else if (message.match(/gas required exceeds allowance/)) { return new TransactionReverted(TransactionReverted.reasons.GasExceedsAllowance, undefined, context); } else if (message.match(/another transaction with same nonce|same hash was already imported|transaction nonce is too low|nonce too low|oldnonce/)) { return new BadNonce(BadNonce.reasons.NonceExpired, context); } else if (message.match(/replacement transaction underpriced/)) { return new BadNonce(BadNonce.reasons.ReplacementUnderpriced, context); } else if (message.match(/tx doesn't have the correct nonce|invalid transaction nonce/)) { return new BadNonce(BadNonce.reasons.NonceIncorrect, context); } else if (message.match(/econnreset|eaddrinuse|econnrefused|epipe|enotfound|enetunreach|eai_again/)) { // Common connection errors: ECONNRESET, EADDRINUSE, ECONNREFUSED, EPIPE, ENOTFOUND, ENETUNREACH, EAI_AGAIN // TODO: Should also take in certain HTTP Status Codes: 429, 500, 502, 503, 504, 521, 522, 524; but need to be sure they // are status codes and not just part of a hash string, id number, etc. return new RpcError(RpcError.reasons.ConnectionReset, context); } else if (message.match(/already known|alreadyknown/)) { return new TransactionAlreadyKnown(context); } else if (message.match(/insufficient funds/)) { return new TransactionReverted(TransactionReverted.reasons.InsufficientFunds, error.receipt, context); } switch (error.code) { case utils_1.Logger.errors.TRANSACTION_REPLACED: return new TransactionReplaced(error.receipt, error.replacement, Object.assign(Object.assign({}, context), { hash: error.hash, reason: error.reason, cancelled: error.cancelled })); case utils_1.Logger.errors.INSUFFICIENT_FUNDS: return new TransactionReverted(TransactionReverted.reasons.InsufficientFunds, error.receipt, context); case utils_1.Logger.errors.CALL_EXCEPTION: return new TransactionReverted(TransactionReverted.reasons.CallException, error.receipt, context); case utils_1.Logger.errors.NONCE_EXPIRED: return new BadNonce(BadNonce.reasons.NonceExpired, context); case utils_1.Logger.errors.REPLACEMENT_UNDERPRICED: return new BadNonce(BadNonce.reasons.ReplacementUnderpriced, context); case utils_1.Logger.errors.UNPREDICTABLE_GAS_LIMIT: return new UnpredictableGasLimit(context); case utils_1.Logger.errors.TIMEOUT: return new OperationTimeout(context); case utils_1.Logger.errors.NETWORK_ERROR: return new RpcError(RpcError.reasons.NetworkError, context); case utils_1.Logger.errors.SERVER_ERROR: return new ServerError(ServerError.reasons.BadResponse, context); default: return error; } }; exports.parseError = parseError;