@lifi/rpc-wrapper
Version:
LI.FI rpc-wrapper
434 lines (433 loc) • 19.8 kB
JavaScript
;
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;