Skip to main content

tempo_node/rpc/
error.rs

1use std::convert::Infallible;
2
3use alloy_primitives::Bytes;
4use alloy_rpc_types_eth::error::EthRpcErrorCode;
5use jsonrpsee::types::error::ErrorObject;
6use reth_errors::ProviderError;
7use reth_evm::revm::context::result::EVMError;
8use reth_node_core::rpc::result::rpc_err;
9use reth_rpc_eth_api::AsEthApiError;
10use reth_rpc_eth_types::{
11    EthApiError,
12    error::{
13        RpcPoolError,
14        api::{FromEvmHalt, FromRevert},
15    },
16};
17use tempo_evm::{TempoHaltReason, TempoInvalidTransaction};
18use tempo_transaction_pool::transaction::TempoPoolTransactionError;
19
20#[derive(Debug, thiserror::Error)]
21pub enum TempoEthApiError {
22    #[error(transparent)]
23    EthApiError(EthApiError),
24}
25
26impl From<TempoEthApiError> for jsonrpsee::types::error::ErrorObject<'static> {
27    fn from(error: TempoEthApiError) -> Self {
28        if let TempoEthApiError::EthApiError(EthApiError::PoolError(
29            RpcPoolError::PoolTransactionError(err),
30        )) = &error
31            && let Some(TempoPoolTransactionError::Evm(err)) =
32                err.as_any().downcast_ref::<TempoPoolTransactionError>()
33            && let Some(rpc_error) = fee_token_rpc_error(err)
34        {
35            return rpc_error;
36        }
37
38        match error {
39            TempoEthApiError::EthApiError(err) => err.into(),
40        }
41    }
42}
43impl From<Infallible> for TempoEthApiError {
44    fn from(_: Infallible) -> Self {
45        unreachable!()
46    }
47}
48
49impl AsEthApiError for TempoEthApiError {
50    fn as_err(&self) -> Option<&EthApiError> {
51        match self {
52            Self::EthApiError(err) => Some(err),
53        }
54    }
55}
56
57impl From<EthApiError> for TempoEthApiError {
58    fn from(error: EthApiError) -> Self {
59        Self::EthApiError(error)
60    }
61}
62
63impl From<ProviderError> for TempoEthApiError {
64    fn from(error: ProviderError) -> Self {
65        EthApiError::from(error).into()
66    }
67}
68impl<T> From<EVMError<T, TempoInvalidTransaction>> for TempoEthApiError
69where
70    T: Into<EthApiError>,
71{
72    fn from(error: EVMError<T, TempoInvalidTransaction>) -> Self {
73        if let EVMError::Transaction(err) = &error
74            && let Some(rpc_error) = fee_token_rpc_error(err)
75        {
76            return Self::EthApiError(EthApiError::Other(Box::new(rpc_error)));
77        }
78
79        EthApiError::from(error).into()
80    }
81}
82
83fn fee_token_rpc_error(err: &TempoInvalidTransaction) -> Option<ErrorObject<'static>> {
84    let data = match err {
85        TempoInvalidTransaction::FeeTokenNotTip20 { address } => serde_json::json!({
86            "name": "FeeTokenNotTip20Error",
87            "token": address.to_string(),
88        }),
89        TempoInvalidTransaction::FeeTokenNotUsdCurrency { address, currency } => {
90            serde_json::json!({
91                "name": "FeeTokenNotUsdError",
92                "token": address.to_string(),
93                "currency": currency,
94            })
95        }
96        TempoInvalidTransaction::FeeTokenPaused { address } => serde_json::json!({
97            "name": "FeeTokenPausedError",
98            "token": address.to_string(),
99        }),
100        _ => return None,
101    };
102
103    Some(ErrorObject::owned(
104        EthRpcErrorCode::TransactionRejected.code(),
105        err.to_string(),
106        Some(data),
107    ))
108}
109
110impl FromEvmHalt<TempoHaltReason> for TempoEthApiError {
111    fn from_evm_halt(halt: TempoHaltReason, gas_limit: u64) -> Self {
112        EthApiError::from_evm_halt(halt, gas_limit).into()
113    }
114}
115
116impl FromRevert for TempoEthApiError {
117    fn from_revert(revert: Bytes) -> Self {
118        match tempo_precompiles::error::decode_error(&revert.0) {
119            Some(error) => Self::EthApiError(EthApiError::Other(Box::new(rpc_err(
120                3,
121                format!("execution reverted: {}", error.error),
122                Some(error.revert_bytes),
123            )))),
124            None => Self::EthApiError(EthApiError::from_revert(revert)),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use alloy_primitives::Address;
132
133    use super::*;
134
135    fn into_rpc_error(err: TempoInvalidTransaction) -> ErrorObject<'static> {
136        let api_error = TempoEthApiError::from(EVMError::<ProviderError, _>::Transaction(err));
137        api_error.into()
138    }
139
140    fn rpc_error_data(error: &ErrorObject<'static>) -> serde_json::Value {
141        serde_json::from_str(error.data().expect("rpc error has data").get())
142            .expect("rpc error data is valid json")
143    }
144
145    #[test]
146    fn fee_token_errors_are_transaction_rejected_rpc_errors() {
147        let address = Address::repeat_byte(0x20);
148        let cases = [
149            (
150                TempoInvalidTransaction::FeeTokenNotTip20 { address },
151                "is not a TIP-20 token",
152                serde_json::json!({
153                    "name": "FeeTokenNotTip20Error",
154                    "token": address.to_string(),
155                }),
156            ),
157            (
158                TempoInvalidTransaction::FeeTokenNotUsdCurrency {
159                    address,
160                    currency: "EUR".to_string(),
161                },
162                "uses currency",
163                serde_json::json!({
164                    "name": "FeeTokenNotUsdError",
165                    "token": address.to_string(),
166                    "currency": "EUR",
167                }),
168            ),
169            (
170                TempoInvalidTransaction::FeeTokenPaused { address },
171                "is paused",
172                serde_json::json!({
173                    "name": "FeeTokenPausedError",
174                    "token": address.to_string(),
175                }),
176            ),
177        ];
178
179        for (err, message, expected_data) in cases {
180            let rpc_error = into_rpc_error(err);
181
182            assert_eq!(
183                rpc_error.code(),
184                EthRpcErrorCode::TransactionRejected.code()
185            );
186            assert!(rpc_error.message().contains(message));
187            assert_eq!(rpc_error_data(&rpc_error), expected_data);
188        }
189    }
190
191    #[test]
192    fn pool_fee_token_errors_are_transaction_rejected_rpc_errors() {
193        let address = Address::repeat_byte(0x20);
194        let error = TempoEthApiError::EthApiError(EthApiError::PoolError(
195            RpcPoolError::PoolTransactionError(Box::new(TempoPoolTransactionError::Evm(
196                TempoInvalidTransaction::FeeTokenNotTip20 { address },
197            ))),
198        ));
199
200        let rpc_error: ErrorObject<'static> = error.into();
201
202        assert_eq!(
203            rpc_error.code(),
204            EthRpcErrorCode::TransactionRejected.code()
205        );
206        assert!(rpc_error.message().contains("is not a TIP-20 token"));
207        assert_eq!(
208            rpc_error_data(&rpc_error),
209            serde_json::json!({
210                "name": "FeeTokenNotTip20Error",
211                "token": address.to_string(),
212            })
213        );
214    }
215}