tempo_node/rpc/
simulate.rs1use 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
27static TRANSFER_TOPIC: LazyLock<B256> =
29 LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)"));
30
31#[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#[derive(Clone, Debug, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct TempoSimulateV1Response<B> {
49 pub blocks: Vec<SimulatedBlock<B>>,
51 pub token_metadata: BTreeMap<Address, Tip20TokenMetadata>,
53}
54
55#[rpc(server, namespace = "tempo")]
56pub trait TempoSimulateApi {
57 #[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#[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
84fn 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 if let Some(to) = call.to.as_ref().and_then(|k| k.to())
97 && to.is_tip20()
98 {
99 addrs.insert(*to);
100 }
101 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 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 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 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 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 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}