Skip to main content

tempo_node/rpc/
simulate.rs

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