Skip to main content

tempo_node/rpc/
simulate.rs

1use crate::{node::TempoNode, rpc::TempoEthApi};
2use alloy_primitives::{Address, B256, keccak256};
3use alloy_rpc_types_eth::simulate::SimulatedBlock;
4use jsonrpsee::{core::RpcResult, proc_macros::rpc};
5use reth_ethereum::evm::revm::database::StateProviderDatabase;
6use reth_node_api::FullNodeTypes;
7use reth_primitives_traits::AlloyBlockHeader as _;
8use reth_provider::{BlockIdReader, ChainSpecProvider, HeaderProvider};
9use reth_rpc_eth_api::{
10    RpcBlock, RpcNodeCore,
11    helpers::{EthCall, LoadState, SpawnBlocking},
12};
13use reth_tracing::tracing;
14use serde::{Deserialize, Serialize};
15use std::{
16    collections::{BTreeMap, HashSet},
17    sync::LazyLock,
18};
19use tempo_chainspec::hardfork::TempoHardforks;
20use tempo_evm::TempoStateAccess;
21use tempo_precompiles::{error::TempoPrecompileError, tip20::TIP20Token};
22use tempo_primitives::TempoAddressExt;
23
24/// keccak256("Transfer(address,address,uint256)")
25static TRANSFER_TOPIC: LazyLock<B256> =
26    LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)"));
27
28/// TIP-20 token metadata returned alongside simulation results.
29///
30/// `decimals` is omitted because all TIP-20 tokens use a fixed decimal count.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Tip20TokenMetadata {
34    pub name: String,
35    pub symbol: String,
36    pub currency: String,
37}
38
39/// Response for `tempo_simulateV1`.
40///
41/// Wraps the standard `eth_simulateV1` response with a top-level `tokenMetadata` map
42/// containing TIP-20 token info for all tokens involved in transfer logs.
43#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TempoSimulateV1Response<B> {
46    /// Standard simulation results (one per simulated block).
47    pub blocks: Vec<SimulatedBlock<B>>,
48    /// Token metadata for TIP-20 addresses that appear in Transfer logs.
49    pub token_metadata: BTreeMap<Address, Tip20TokenMetadata>,
50}
51
52#[rpc(server, namespace = "tempo")]
53pub trait TempoSimulateApi {
54    /// Simulates transactions like `eth_simulateV1` but enriches the response with
55    /// TIP-20 token metadata for all tokens involved in Transfer events.
56    ///
57    /// This eliminates the need for a second roundtrip to fetch token symbols/decimals
58    /// after simulation.
59    #[method(name = "simulateV1")]
60    async fn simulate_v1(
61        &self,
62        payload: alloy_rpc_types_eth::simulate::SimulatePayload<
63            tempo_alloy::rpc::TempoTransactionRequest,
64        >,
65        block: Option<alloy_eips::BlockId>,
66    ) -> RpcResult<TempoSimulateV1Response<RpcBlock<tempo_alloy::TempoNetwork>>>;
67}
68
69/// Implementation of `tempo_simulateV1`.
70#[derive(Debug, Clone)]
71pub struct TempoSimulate<N: FullNodeTypes<Types = TempoNode>> {
72    eth_api: TempoEthApi<N>,
73}
74
75impl<N: FullNodeTypes<Types = TempoNode>> TempoSimulate<N> {
76    pub fn new(eth_api: TempoEthApi<N>) -> Self {
77        Self { eth_api }
78    }
79}
80
81/// Extract TIP-20 addresses from the simulation request's call targets.
82///
83/// This allows metadata resolution to start before simulation completes.
84fn extract_tip20_targets(
85    payload: &alloy_rpc_types_eth::simulate::SimulatePayload<
86        tempo_alloy::rpc::TempoTransactionRequest,
87    >,
88) -> Vec<Address> {
89    let mut addrs = std::collections::BTreeSet::new();
90    for block in &payload.block_state_calls {
91        for call in &block.calls {
92            // Standard `to` field
93            if let Some(to) = call.to.as_ref().and_then(|k| k.to())
94                && to.is_tip20()
95            {
96                addrs.insert(*to);
97            }
98            // AA calls array
99            for c in &call.calls {
100                if let Some(to) = c.to.to()
101                    && to.is_tip20()
102                {
103                    addrs.insert(*to);
104                }
105            }
106            // Fee token
107            if let Some(ft) = call.fee_token
108                && ft.is_tip20()
109            {
110                addrs.insert(ft);
111            }
112        }
113    }
114    addrs.into_iter().collect()
115}
116
117#[async_trait::async_trait]
118impl<N: FullNodeTypes<Types = TempoNode>> TempoSimulateApiServer for TempoSimulate<N> {
119    async fn simulate_v1(
120        &self,
121        payload: alloy_rpc_types_eth::simulate::SimulatePayload<
122            tempo_alloy::rpc::TempoTransactionRequest,
123        >,
124        block: Option<alloy_eips::BlockId>,
125    ) -> RpcResult<TempoSimulateV1Response<RpcBlock<tempo_alloy::TempoNetwork>>> {
126        // Pre-extract TIP-20 addresses from call targets so we can start
127        // metadata resolution concurrently with the simulation.
128        let prefetched = extract_tip20_targets(&payload);
129
130        // Run simulation and metadata prefetch concurrently
131        let (sim_result, mut token_metadata) = tokio::join!(
132            self.eth_api.simulate_v1(payload, block),
133            self.resolve_token_metadata(prefetched, block),
134        );
135
136        let blocks = sim_result.map_err(|e| {
137            let err: jsonrpsee::types::ErrorObject<'static> = e.into();
138            err
139        })?;
140
141        // Scan simulation logs for any additional TIP-20 addresses not in the
142        // prefetched set (e.g. tokens touched indirectly via contract calls).
143        let mut extra = HashSet::new();
144        for sim_block in &blocks {
145            for call in &sim_block.calls {
146                for log in &call.logs {
147                    if log.address().is_tip20()
148                        && log.topics().first() == Some(&*TRANSFER_TOPIC)
149                        && !token_metadata.contains_key(&log.address())
150                    {
151                        extra.insert(log.address());
152                    }
153                }
154            }
155        }
156
157        if !extra.is_empty() {
158            let extra_metadata = self
159                .resolve_token_metadata(extra.into_iter().collect(), block)
160                .await;
161            token_metadata.extend(extra_metadata);
162        }
163
164        Ok(TempoSimulateV1Response {
165            blocks,
166            token_metadata,
167        })
168    }
169}
170
171impl<N: FullNodeTypes<Types = TempoNode>> TempoSimulate<N> {
172    /// Resolves TIP-20 token metadata for the given addresses using state at the target block.
173    async fn resolve_token_metadata(
174        &self,
175        addresses: Vec<Address>,
176        block: Option<alloy_eips::BlockId>,
177    ) -> BTreeMap<Address, Tip20TokenMetadata> {
178        if addresses.is_empty() {
179            return BTreeMap::new();
180        }
181
182        let result = self
183            .eth_api
184            .spawn_blocking_io_fut(async move |this| {
185                let state = this.state_at_block_id_or_latest(block).await?;
186
187                // Derive hardfork spec from the target block's timestamp.
188                let timestamp = block
189                    .and_then(|id| {
190                        this.provider()
191                            .block_number_for_id(id)
192                            .ok()
193                            .flatten()
194                            .and_then(|num| {
195                                this.provider()
196                                    .header_by_number(num)
197                                    .ok()
198                                    .flatten()
199                                    .map(|h| h.timestamp())
200                            })
201                    })
202                    .unwrap_or(u64::MAX);
203
204                let spec = this.provider().chain_spec().tempo_hardfork_at(timestamp);
205                let mut db = StateProviderDatabase::new(state);
206
207                let mut metadata = BTreeMap::new();
208                for addr in &addresses {
209                    let result = db.with_read_only_storage_ctx(spec, || {
210                        let token = TIP20Token::from_address(*addr)?;
211                        Ok::<_, TempoPrecompileError>((
212                            token.name()?,
213                            token.symbol()?,
214                            token.currency()?,
215                        ))
216                    });
217
218                    match result {
219                        Ok((name, symbol, currency)) => {
220                            metadata.insert(
221                                *addr,
222                                Tip20TokenMetadata {
223                                    name,
224                                    symbol,
225                                    currency,
226                                },
227                            );
228                        }
229                        Err(e) => {
230                            tracing::warn!(
231                                token = %addr,
232                                error = %e,
233                                "failed to resolve TIP-20 metadata, skipping"
234                            );
235                        }
236                    }
237                }
238
239                Ok(metadata)
240            })
241            .await;
242
243        match result {
244            Ok(m) => m,
245            Err(e) => {
246                tracing::warn!(error = ?e, "failed to resolve token metadata");
247                BTreeMap::new()
248            }
249        }
250    }
251}