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}