UNPKG

@lifi/rpc-wrapper

Version:
409 lines (408 loc) 18 kB
import { CustomError } from './error'; import { Logger } from 'ethers/lib/utils'; export class MaxBufferLengthError extends CustomError { constructor(context = {}) { super('Inflight transaction buffer is full.', context, MaxBufferLengthError.type); this.context = context; } } /** * Thrown if a backfill transaction fails and other txs are attempted */ MaxBufferLengthError.type = MaxBufferLengthError.name; export class StallTimeout extends CustomError { constructor(context = {}) { super('Request stalled and timed out.', context, StallTimeout.type); this.context = context; } } StallTimeout.type = StallTimeout.name; export class RpcError extends 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; } } 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.', }; export class TransactionReadError extends CustomError { constructor(reason, context = {}) { super(reason, context, TransactionReadError.type); this.reason = reason; this.context = context; } } /** * 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.', }; export class TransactionReverted extends CustomError { constructor(reason, receipt, context = {}) { super(reason, context, TransactionReverted.type); this.reason = reason; this.receipt = receipt; this.context = context; } } /** * 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.', }; export class TransactionReplaced extends CustomError { constructor(receipt, replacement, context = {}) { super('Transaction replaced.', context, TransactionReplaced.type); this.receipt = receipt; this.replacement = replacement; this.context = context; } } /** * 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. export class OperationTimeout extends CustomError { constructor(context = {}) { super('Operation timed out.', context, OperationTimeout.type); this.context = context; } } /** * An error indicating that an operation (typically confirmation) timed out. */ OperationTimeout.type = OperationTimeout.name; export class TransactionBackfilled extends CustomError { constructor(context = {}) { super('Transaction was replaced by a backfill.', context, TransactionBackfilled.type); this.context = context; } } /** * An error indicating that a transaction was replaced by a backfill, likely because it * was unresponsive. */ TransactionBackfilled.type = TransactionBackfilled.name; export class UnpredictableGasLimit extends CustomError { constructor(context = {}) { super('The gas estimate could not be determined.', context, UnpredictableGasLimit.type); this.context = context; } } /** * 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; export class BadNonce extends CustomError { constructor(reason, context = {}) { super(reason, context, BadNonce.type); this.reason = reason; this.context = context; } } /** * 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", }; export class ServerError extends 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; } } /** * 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.', }; export class TransactionAlreadyKnown extends CustomError { constructor(context = {}) { super('Transaction is already indexed by provider.', context, TransactionAlreadyKnown.type); this.context = context; } } /** * 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; export class TransactionKilled extends CustomError { constructor(context = {}) { super('Transaction was killed by monitor loop.', context, TransactionKilled.type); this.context = context; } } /** * 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; export class MaxAttemptsReached extends CustomError { constructor(attempts, context = {}) { super(MaxAttemptsReached.getMessage(attempts), context, MaxAttemptsReached.type); this.context = context; } static getMessage(attempts) { return `Reached maximum attempts ${attempts}.`; } } MaxAttemptsReached.type = MaxAttemptsReached.name; export class NotEnoughConfirmations extends 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?`; } } NotEnoughConfirmations.type = NotEnoughConfirmations.name; export class GasEstimateInvalid extends 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}`; } } GasEstimateInvalid.type = GasEstimateInvalid.name; export class ChainNotSupported extends 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.`; } } ChainNotSupported.type = ChainNotSupported.name; // TODO: ProviderNotConfigured is essentially a more specific ChainNotSupported error. Should they be combined? export class ProviderNotConfigured extends 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.`; } } ProviderNotConfigured.type = ProviderNotConfigured.name; export class ConfigurationError extends CustomError { constructor(invalidParameters, context = {}) { super('Configuration paramater(s) were invalid.', Object.assign(Object.assign({}, context), { invalidParameters }), ConfigurationError.type); this.invalidParameters = invalidParameters; this.context = context; } } ConfigurationError.type = ConfigurationError.name; export class InitialSubmitFailure extends CustomError { constructor(context = {}) { super('Transaction never submitted: exceeded maximum iterations in initial submit loop.', context, InitialSubmitFailure.type); this.context = context; } } InitialSubmitFailure.type = InitialSubmitFailure.name; // These errors should essentially never happen; they are only used within the block of sanity checks. export class TransactionProcessingError extends CustomError { constructor(reason, method, context = {}) { super(reason, Object.assign(Object.assign({}, context), { method }), TransactionProcessingError.type); this.reason = reason; this.method = method; this.context = context; } } 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 */ export 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 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 Logger.errors.INSUFFICIENT_FUNDS: return new TransactionReverted(TransactionReverted.reasons.InsufficientFunds, error.receipt, context); case Logger.errors.CALL_EXCEPTION: return new TransactionReverted(TransactionReverted.reasons.CallException, error.receipt, context); case Logger.errors.NONCE_EXPIRED: return new BadNonce(BadNonce.reasons.NonceExpired, context); case Logger.errors.REPLACEMENT_UNDERPRICED: return new BadNonce(BadNonce.reasons.ReplacementUnderpriced, context); case Logger.errors.UNPREDICTABLE_GAS_LIMIT: return new UnpredictableGasLimit(context); case Logger.errors.TIMEOUT: return new OperationTimeout(context); case Logger.errors.NETWORK_ERROR: return new RpcError(RpcError.reasons.NetworkError, context); case Logger.errors.SERVER_ERROR: return new ServerError(ServerError.reasons.BadResponse, context); default: return error; } };