//! Transaction pool errors

use reth_primitives::{Address, InvalidTransactionError, TxHash};

/// Transaction pool result type.
pub type PoolResult<T> = Result<T, PoolError>;

/// All errors the Transaction pool can throw.
#[derive(Debug, thiserror::Error)]
pub enum PoolError {
    /// Same transaction already imported
    #[error("[{0:?}] Already imported")]
    AlreadyImported(TxHash),
    /// Thrown if a replacement transaction's gas price is below the already imported transaction
    #[error("[{0:?}]: insufficient gas price to replace existing transaction.")]
    ReplacementUnderpriced(TxHash),
    /// The fee cap of the transaction is below the minimum fee cap determined by the protocol
    #[error("[{0:?}] Transaction feeCap {1} below chain minimum.")]
    FeeCapBelowMinimumProtocolFeeCap(TxHash, u128),
    /// Thrown when the number of unique transactions of a sender exceeded the slot capacity.
    #[error("{0:?} identified as spammer. Transaction {1:?} rejected.")]
    SpammerExceededCapacity(Address, TxHash),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the size limits of the pool.
    #[error("[{0:?}] Transaction discarded outright due to pool size constraints.")]
    DiscardedOnInsert(TxHash),
    /// Thrown when the transaction is considered invalid.
    #[error("[{0:?}] {1:?}")]
    InvalidTransaction(TxHash, InvalidPoolTransactionError),
    /// Any other error that occurred while inserting/validating a transaction. e.g. IO database
    /// error
    #[error("[{0:?}] {1:?}")]
    Other(TxHash, Box<dyn std::error::Error + Send + Sync>),
}

// === impl PoolError ===

impl PoolError {
    /// Returns the hash of the transaction that resulted in this error.
    pub fn hash(&self) -> &TxHash {
        match self {
            PoolError::AlreadyImported(hash) => hash,
            PoolError::ReplacementUnderpriced(hash) => hash,
            PoolError::FeeCapBelowMinimumProtocolFeeCap(hash, _) => hash,
            PoolError::SpammerExceededCapacity(_, hash) => hash,
            PoolError::DiscardedOnInsert(hash) => hash,
            PoolError::InvalidTransaction(hash, _) => hash,
            PoolError::Other(hash, _) => hash,
        }
    }

    /// Returns `true` if the error was caused by a transaction that is considered bad in the
    /// context of the transaction pool and warrants peer penalization.
    ///
    /// Not all error variants are caused by the incorrect composition of the transaction (See also
    /// [InvalidPoolTransactionError]) and can be caused by the current state of the transaction
    /// pool. For example the transaction pool is already full or the error was caused my an
    /// internal error, such as database errors.
    ///
    /// This function returns true only if the transaction will never make it into the pool because
    /// its composition is invalid and the original sender should have detected this as well. This
    /// is used to determine whether the original sender should be penalized for sending an
    /// erroneous transaction.
    #[inline]
    pub fn is_bad_transaction(&self) -> bool {
        match self {
            PoolError::AlreadyImported(_) => {
                // already imported but not bad
                false
            }
            PoolError::ReplacementUnderpriced(_) => {
                // already imported but not bad
                false
            }
            PoolError::FeeCapBelowMinimumProtocolFeeCap(_, _) => {
                // fee cap of the tx below the technical minimum determined by the protocol, see
                // [MINIMUM_PROTOCOL_FEE_CAP](reth_primitives::constants::MIN_PROTOCOL_BASE_FEE)
                // although this transaction will always be invalid, we do not want to penalize the
                // sender because this check simply could not be implemented by the client
                false
            }
            PoolError::SpammerExceededCapacity(_, _) => {
                // the sender exceeded the slot capacity, we should not penalize the peer for
                // sending the tx because we don't know if all the transactions are sent from the
                // same peer, there's also a chance that old transactions haven't been cleared yet
                // (pool lags behind) and old transaction still occupy a slot in the pool
                false
            }
            PoolError::DiscardedOnInsert(_) => {
                // valid tx but dropped due to size constraints
                false
            }
            PoolError::InvalidTransaction(_, err) => {
                // transaction rejected because it violates constraints
                err.is_bad_transaction()
            }
            PoolError::Other(_, _) => {
                // internal error unrelated to the transaction
                false
            }
        }
    }
}

/// Represents errors that can happen when validating transactions for the pool
///
/// See [TransactionValidator](crate::TransactionValidator).
#[derive(Debug, Clone, thiserror::Error)]
pub enum InvalidPoolTransactionError {
    /// Hard consensus errors
    #[error(transparent)]
    Consensus(#[from] InvalidTransactionError),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the size limits of the pool.
    #[error("Transaction's gas limit {0} exceeds block's gas limit {1}.")]
    ExceedsGasLimit(u64, u64),
    /// Thrown when a new transaction is added to the pool, but then immediately discarded to
    /// respect the max_init_code_size.
    #[error("Transaction's size {0} exceeds max_init_code_size {1}.")]
    ExceedsMaxInitCodeSize(usize, usize),
    /// Thrown if the input data of a transaction is greater
    /// than some meaningful limit a user might use. This is not a consensus error
    /// making the transaction invalid, rather a DOS protection.
    #[error("Input data too large")]
    OversizedData(usize, usize),
    /// Thrown if the transaction's fee is below the minimum fee
    #[error("transaction underpriced")]
    Underpriced,
}

// === impl InvalidPoolTransactionError ===

impl InvalidPoolTransactionError {
    /// Returns `true` if the error was caused by a transaction that is considered bad in the
    /// context of the transaction pool and warrants peer penalization.
    ///
    /// See [PoolError::is_bad_transaction].
    #[inline]
    fn is_bad_transaction(&self) -> bool {
        match self {
            InvalidPoolTransactionError::Consensus(err) => {
                // transaction considered invalid by the consensus rules
                // We do not consider the following errors to be erroneous transactions, since they
                // depend on dynamic environmental conditions and should not be assumed to have been
                // intentionally caused by the sender
                match err {
                    InvalidTransactionError::InsufficientFunds { .. } |
                    InvalidTransactionError::NonceNotConsistent => {
                        // transaction could just have arrived late/early
                        false
                    }
                    InvalidTransactionError::GasTooLow |
                    InvalidTransactionError::GasTooHigh |
                    InvalidTransactionError::TipAboveFeeCap => {
                        // these are technically not invalid
                        false
                    }
                    InvalidTransactionError::FeeCapTooLow => {
                        // dynamic, but not used during validation
                        false
                    }
                    InvalidTransactionError::Eip2930Disabled |
                    InvalidTransactionError::Eip1559Disabled => {
                        // settings
                        false
                    }
                    InvalidTransactionError::OldLegacyChainId => true,
                    InvalidTransactionError::ChainIdMismatch => true,
                    InvalidTransactionError::GasUintOverflow => true,
                    InvalidTransactionError::TxTypeNotSupported => true,
                    InvalidTransactionError::SignerAccountHasBytecode => true,
                }
            }
            InvalidPoolTransactionError::ExceedsGasLimit(_, _) => true,
            InvalidPoolTransactionError::ExceedsMaxInitCodeSize(_, _) => true,
            InvalidPoolTransactionError::OversizedData(_, _) => true,
            InvalidPoolTransactionError::Underpriced => {
                // local setting
                false
            }
        }
    }
}
