1pub mod admin;
2pub mod consensus;
3pub mod error;
4pub mod eth_ext;
5pub mod token;
6
7pub use admin::{TempoAdminApi, TempoAdminApiServer};
8use alloy_primitives::B256;
9use alloy_rpc_types_eth::{Log, ReceiptWithBloom};
10pub use consensus::{TempoConsensusApiServer, TempoConsensusRpc};
11pub use eth_ext::{TempoEthExt, TempoEthExtApiServer};
12use futures::{TryFutureExt, future::Either};
13use reth_errors::RethError;
14use reth_primitives_traits::{
15 Recovered, TransactionMeta, TxTy, WithEncoded, transaction::TxHashRef,
16};
17use reth_rpc_eth_api::{FromEthApiError, RpcTxReq};
18use reth_transaction_pool::{PoolPooledTx, TransactionOrigin};
19use std::sync::Arc;
20pub use tempo_alloy::rpc::TempoTransactionRequest;
21use tempo_chainspec::TempoChainSpec;
22use tempo_evm::TempoStateAccess;
23use tempo_precompiles::{NONCE_PRECOMPILE_ADDRESS, nonce::NonceManager};
24use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
25pub use token::{TempoToken, TempoTokenApiServer};
26
27use crate::{node::TempoNode, rpc::error::TempoEthApiError};
28use alloy::primitives::{U256, uint};
29use reth_ethereum::tasks::{
30 Runtime,
31 pool::{BlockingTaskGuard, BlockingTaskPool},
32};
33use reth_evm::{
34 EvmEnvFor, TxEnvFor,
35 revm::{Database, context::result::EVMError},
36};
37use reth_node_api::{FullNodeComponents, FullNodeTypes, HeaderTy, PrimitivesTy};
38use reth_node_builder::{
39 NodeAdapter,
40 rpc::{EthApiBuilder, EthApiCtx},
41};
42use reth_provider::{ChainSpecProvider, ProviderError};
43use reth_rpc::{DynRpcConverter, eth::EthApi};
44use reth_rpc_eth_api::{
45 EthApiTypes, RpcConverter, RpcNodeCore, RpcNodeCoreExt,
46 helpers::{
47 Call, EthApiSpec, EthBlocks, EthCall, EthFees, EthState, EthTransactions, LoadBlock,
48 LoadFee, LoadPendingBlock, LoadReceipt, LoadState, LoadTransaction, SpawnBlocking, Trace,
49 estimate::EstimateCall, pending_block::PendingEnvBuilder, spec::SignersForRpc,
50 },
51 transaction::{ConvertReceiptInput, ReceiptConverter},
52};
53use reth_rpc_eth_types::{
54 EthApiError, EthStateCache, FeeHistoryCache, FillTransaction, GasPriceOracle, PendingBlock,
55 builder::config::PendingBlockKind, receipt::EthReceiptConverter,
56};
57use tempo_alloy::{TempoNetwork, rpc::TempoTransactionReceipt};
58use tempo_evm::TempoEvmConfig;
59use tempo_primitives::{
60 TEMPO_GAS_PRICE_SCALING_FACTOR, TempoPrimitives, TempoReceipt, TempoTxEnvelope,
61 subblock::PartialValidatorKey,
62};
63use tokio::sync::{Mutex, broadcast};
64
65pub const NATIVE_BALANCE_PLACEHOLDER: U256 =
68 uint!(4242424242424242424242424242424242424242424242424242424242424242424242424242_U256);
69
70pub const SUBBLOCK_TX_CHANNEL_CAPACITY: usize = 10_000;
76
77#[derive(Clone)]
88pub struct TempoEthApi<N: FullNodeTypes<Types = TempoNode>> {
89 inner: EthApi<NodeAdapter<N>, DynRpcConverter<TempoEvmConfig, TempoNetwork>>,
91
92 subblock_transactions_tx: broadcast::Sender<Recovered<TempoTxEnvelope>>,
94
95 validator_key: Option<B256>,
101}
102
103impl<N: FullNodeTypes<Types = TempoNode>> TempoEthApi<N> {
104 pub fn new(
106 eth_api: EthApi<NodeAdapter<N>, DynRpcConverter<TempoEvmConfig, TempoNetwork>>,
107 validator_key: Option<B256>,
108 ) -> Self {
109 Self {
110 inner: eth_api,
111 subblock_transactions_tx: broadcast::channel(SUBBLOCK_TX_CHANNEL_CAPACITY).0,
112 validator_key,
113 }
114 }
115
116 pub fn subblock_transactions_rx(&self) -> broadcast::Receiver<Recovered<TempoTxEnvelope>> {
118 self.subblock_transactions_tx.subscribe()
119 }
120
121 fn matches_validator_key(&self, partial_key: &PartialValidatorKey) -> bool {
126 self.validator_key
127 .is_some_and(|key| partial_key.matches(key.as_slice()))
128 }
129}
130
131impl<N: FullNodeTypes<Types = TempoNode>> EthApiTypes for TempoEthApi<N> {
132 type Error = TempoEthApiError;
133 type NetworkTypes = TempoNetwork;
134 type RpcConvert = DynRpcConverter<TempoEvmConfig, TempoNetwork>;
135
136 fn converter(&self) -> &Self::RpcConvert {
137 self.inner.converter()
138 }
139}
140
141impl<N: FullNodeTypes<Types = TempoNode>> RpcNodeCore for TempoEthApi<N> {
142 type Primitives = PrimitivesTy<N::Types>;
143 type Provider = N::Provider;
144 type Pool = <NodeAdapter<N> as FullNodeComponents>::Pool;
145 type Evm = <NodeAdapter<N> as FullNodeComponents>::Evm;
146 type Network = <NodeAdapter<N> as FullNodeComponents>::Network;
147
148 #[inline]
149 fn pool(&self) -> &Self::Pool {
150 self.inner.pool()
151 }
152
153 #[inline]
154 fn evm_config(&self) -> &Self::Evm {
155 self.inner.evm_config()
156 }
157
158 #[inline]
159 fn network(&self) -> &Self::Network {
160 self.inner.network()
161 }
162
163 #[inline]
164 fn provider(&self) -> &Self::Provider {
165 self.inner.provider()
166 }
167}
168
169impl<N: FullNodeTypes<Types = TempoNode>> RpcNodeCoreExt for TempoEthApi<N> {
170 #[inline]
171 fn cache(&self) -> &EthStateCache<PrimitivesTy<N::Types>> {
172 self.inner.cache()
173 }
174}
175
176impl<N: FullNodeTypes<Types = TempoNode>> EthApiSpec for TempoEthApi<N> {
177 #[inline]
178 fn starting_block(&self) -> U256 {
179 self.inner.starting_block()
180 }
181}
182
183impl<N: FullNodeTypes<Types = TempoNode>> SpawnBlocking for TempoEthApi<N> {
184 #[inline]
185 fn io_task_spawner(&self) -> &Runtime {
186 self.inner.task_spawner()
187 }
188
189 #[inline]
190 fn tracing_task_pool(&self) -> &BlockingTaskPool {
191 self.inner.blocking_task_pool()
192 }
193
194 #[inline]
195 fn tracing_task_guard(&self) -> &BlockingTaskGuard {
196 self.inner.blocking_task_guard()
197 }
198
199 #[inline]
200 fn blocking_io_task_guard(&self) -> &Arc<tokio::sync::Semaphore> {
201 self.inner.blocking_io_task_guard()
202 }
203}
204
205impl<N: FullNodeTypes<Types = TempoNode>> LoadPendingBlock for TempoEthApi<N> {
206 #[inline]
207 fn pending_block(&self) -> &Mutex<Option<PendingBlock<Self::Primitives>>> {
208 self.inner.pending_block()
209 }
210
211 #[inline]
212 fn pending_env_builder(&self) -> &dyn PendingEnvBuilder<Self::Evm> {
213 self.inner.pending_env_builder()
214 }
215
216 #[inline]
217 fn pending_block_kind(&self) -> PendingBlockKind {
218 PendingBlockKind::None
220 }
221}
222
223impl<N: FullNodeTypes<Types = TempoNode>> LoadFee for TempoEthApi<N> {
224 #[inline]
225 fn gas_oracle(&self) -> &GasPriceOracle<Self::Provider> {
226 self.inner.gas_oracle()
227 }
228
229 #[inline]
230 fn fee_history_cache(&self) -> &FeeHistoryCache<HeaderTy<N::Types>> {
231 self.inner.fee_history_cache()
232 }
233}
234
235impl<N: FullNodeTypes<Types = TempoNode>> LoadState for TempoEthApi<N> {}
236
237impl<N: FullNodeTypes<Types = TempoNode>> EthState for TempoEthApi<N> {
238 #[inline]
239 async fn balance(
240 &self,
241 _address: alloy_primitives::Address,
242 _block_id: Option<alloy_eips::BlockId>,
243 ) -> Result<U256, Self::Error> {
244 Ok(NATIVE_BALANCE_PLACEHOLDER)
245 }
246
247 #[inline]
248 fn max_proof_window(&self) -> u64 {
249 self.inner.eth_proof_window()
250 }
251}
252
253impl<N: FullNodeTypes<Types = TempoNode>> EthFees for TempoEthApi<N> {}
254
255impl<N: FullNodeTypes<Types = TempoNode>> Trace for TempoEthApi<N> {}
256
257impl<N: FullNodeTypes<Types = TempoNode>> EthCall for TempoEthApi<N> {}
258
259impl<N: FullNodeTypes<Types = TempoNode>> Call for TempoEthApi<N> {
260 #[inline]
261 fn call_gas_limit(&self) -> u64 {
262 self.inner.gas_cap()
263 }
264
265 #[inline]
266 fn max_simulate_blocks(&self) -> u64 {
267 self.inner.max_simulate_blocks()
268 }
269
270 #[inline]
271 fn evm_memory_limit(&self) -> u64 {
272 self.inner.evm_memory_limit()
273 }
274
275 fn caller_gas_allowance(
277 &self,
278 mut db: impl Database<Error: Into<EthApiError>>,
279 evm_env: &EvmEnvFor<Self::Evm>,
280 tx_env: &TxEnvFor<Self::Evm>,
281 ) -> Result<u64, Self::Error> {
282 let fee_payer = tx_env
283 .fee_payer()
284 .map_err(EVMError::<ProviderError, _>::from)?;
285
286 let fee_token = db
287 .get_fee_token(tx_env, fee_payer, evm_env.cfg_env.spec)
288 .map_err(ProviderError::other)?;
289 let fee_token_balance = db
290 .get_token_balance(fee_token, fee_payer, evm_env.cfg_env.spec)
291 .map_err(ProviderError::other)?;
292
293 Ok(fee_token_balance
294 .saturating_mul(TEMPO_GAS_PRICE_SCALING_FACTOR)
296 .checked_div(U256::from(tx_env.inner.gas_price))
298 .unwrap_or_default()
300 .saturating_to())
301 }
302
303 fn create_txn_env(
304 &self,
305 evm_env: &EvmEnvFor<Self::Evm>,
306 mut request: TempoTransactionRequest,
307 mut db: impl Database<Error: Into<EthApiError>>,
308 ) -> Result<TxEnvFor<Self::Evm>, Self::Error> {
309 if let Some(nonce_key) = request.nonce_key
310 && !nonce_key.is_zero()
311 && request.nonce.is_none()
312 {
313 let nonce = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
314 0 } else {
316 let slot =
318 NonceManager::new().nonces[request.from.unwrap_or_default()][nonce_key].slot();
319 db.storage(NONCE_PRECOMPILE_ADDRESS, slot)
320 .map_err(Into::into)?
321 .saturating_to()
322 };
323 request.nonce = Some(nonce);
324 }
325
326 Ok(self.inner.create_txn_env(evm_env, request, db)?)
327 }
328}
329
330impl<N: FullNodeTypes<Types = TempoNode>> EstimateCall for TempoEthApi<N> {}
331impl<N: FullNodeTypes<Types = TempoNode>> LoadBlock for TempoEthApi<N> {}
332impl<N: FullNodeTypes<Types = TempoNode>> LoadReceipt for TempoEthApi<N> {}
333impl<N: FullNodeTypes<Types = TempoNode>> EthBlocks for TempoEthApi<N> {}
334impl<N: FullNodeTypes<Types = TempoNode>> LoadTransaction for TempoEthApi<N> {}
335
336impl<N: FullNodeTypes<Types = TempoNode>> EthTransactions for TempoEthApi<N> {
337 fn signers(&self) -> &SignersForRpc<Self::Provider, Self::NetworkTypes> {
338 self.inner.signers()
339 }
340
341 fn send_raw_transaction_sync_timeout(&self) -> std::time::Duration {
342 self.inner.send_raw_transaction_sync_timeout()
343 }
344
345 fn send_transaction(
346 &self,
347 origin: TransactionOrigin,
348 tx: WithEncoded<Recovered<PoolPooledTx<Self::Pool>>>,
349 ) -> impl Future<Output = Result<B256, Self::Error>> + Send {
350 match tx.value().inner().subblock_proposer() {
351 Some(proposer) if self.matches_validator_key(&proposer) => {
352 let subblock_tx = self.subblock_transactions_tx.clone();
353 Either::Left(Either::Left(async move {
354 let tx_hash = *tx.value().tx_hash();
355
356 subblock_tx.send(tx.into_value()).map_err(|_| {
357 EthApiError::from(RethError::msg("subblocks service channel closed"))
358 })?;
359
360 Ok(tx_hash)
361 }))
362 }
363 Some(_) => Either::Left(Either::Right(futures::future::err(
364 EthApiError::from(RethError::msg(
365 "subblock transaction rejected: target validator mismatch",
366 ))
367 .into(),
368 ))),
369 None => Either::Right(self.inner.send_transaction(origin, tx).map_err(Into::into)),
370 }
371 }
372
373 async fn fill_transaction(
374 &self,
375 mut request: RpcTxReq<Self::NetworkTypes>,
376 ) -> Result<FillTransaction<TxTy<Self::Primitives>>, Self::Error> {
377 if let Some(nonce_key) = request.nonce_key
378 && !nonce_key.is_zero()
379 {
380 if request.nonce.is_none() {
381 let nonce = if nonce_key == TEMPO_EXPIRING_NONCE_KEY {
382 0 } else {
384 let slot = NonceManager::new().nonces[request.from.unwrap_or_default()]
386 [nonce_key]
387 .slot();
388 self.spawn_blocking_io(move |this| {
389 this.latest_state()?
390 .storage(NONCE_PRECOMPILE_ADDRESS, slot.into())
391 .map_err(Self::Error::from_eth_err)
392 })
393 .await?
394 .unwrap_or_default()
395 .saturating_to()
396 };
397 request.nonce = Some(nonce);
398 }
399
400 if request.gas.is_none() {
402 let gas = EstimateCall::estimate_gas_at(
403 self,
404 request.clone(),
405 alloy_eips::BlockId::pending(),
406 None,
407 )
408 .await?;
409 request.gas = Some(gas.to());
410 }
411 }
412
413 Ok(self.inner.fill_transaction(request).await?)
414 }
415}
416
417#[derive(Debug, Clone)]
419#[expect(clippy::type_complexity)]
420pub struct TempoReceiptConverter {
421 inner: EthReceiptConverter<
422 TempoChainSpec,
423 fn(TempoReceipt, usize, TransactionMeta) -> ReceiptWithBloom<TempoReceipt<Log>>,
424 >,
425}
426
427impl TempoReceiptConverter {
428 pub fn new(chain_spec: Arc<TempoChainSpec>) -> Self {
429 Self {
430 inner: EthReceiptConverter::new(chain_spec).with_builder(
431 |receipt: TempoReceipt, next_log_index, meta| {
432 let mut log_index = next_log_index;
433 receipt
434 .map_logs(|log| {
435 let idx = log_index;
436 log_index += 1;
437 Log {
438 inner: log,
439 block_hash: Some(meta.block_hash),
440 block_number: Some(meta.block_number),
441 block_timestamp: Some(meta.timestamp),
442 transaction_hash: Some(meta.tx_hash),
443 transaction_index: Some(meta.index),
444 log_index: Some(idx as u64),
445 removed: false,
446 }
447 })
448 .into()
449 },
450 ),
451 }
452 }
453}
454
455impl ReceiptConverter<TempoPrimitives> for TempoReceiptConverter {
456 type RpcReceipt = TempoTransactionReceipt;
457 type Error = EthApiError;
458
459 fn convert_receipts(
460 &self,
461 receipts: Vec<ConvertReceiptInput<'_, TempoPrimitives>>,
462 ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
463 let txs = receipts.iter().map(|r| r.tx).collect::<Vec<_>>();
464 self.inner
465 .convert_receipts(receipts)?
466 .into_iter()
467 .zip(txs)
468 .map(|(inner, tx)| {
469 let mut receipt = TempoTransactionReceipt {
470 inner,
471 fee_token: None,
472 fee_payer: tx
474 .fee_payer(tx.signer())
475 .map_err(|_| EthApiError::InvalidTransactionSignature)?,
476 };
477 if receipt.effective_gas_price == 0 || receipt.gas_used == 0 {
478 return Ok(receipt);
479 }
480
481 receipt.fee_token = receipt.logs().last().map(|log| log.address());
486 Ok(receipt)
487 })
488 .collect()
489 }
490}
491
492#[derive(Debug, Default)]
493pub struct TempoEthApiBuilder {
494 pub validator_key: Option<B256>,
496}
497
498impl TempoEthApiBuilder {
499 pub fn new(validator_key: Option<B256>) -> Self {
501 Self { validator_key }
502 }
503}
504
505impl<N> EthApiBuilder<NodeAdapter<N>> for TempoEthApiBuilder
506where
507 N: FullNodeTypes<Types = TempoNode>,
508{
509 type EthApi = TempoEthApi<N>;
510
511 async fn build_eth_api(self, ctx: EthApiCtx<'_, NodeAdapter<N>>) -> eyre::Result<Self::EthApi> {
512 let chain_spec = ctx.components.provider.chain_spec();
513 let eth_api = ctx
514 .eth_api_builder()
515 .modify_gas_oracle_config(|config| config.default_suggested_fee = Some(U256::ZERO))
516 .map_converter(|_| RpcConverter::new(TempoReceiptConverter::new(chain_spec)).erased())
517 .build();
518
519 Ok(TempoEthApi::new(eth_api, self.validator_key))
520 }
521}