tempo_node/rpc/
simulate.rs1use 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
24static TRANSFER_TOPIC: LazyLock<B256> =
26 LazyLock::new(|| keccak256(b"Transfer(address,address,uint256)"));
27
28#[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#[derive(Clone, Debug, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TempoSimulateV1Response<B> {
46 pub blocks: Vec<SimulatedBlock<B>>,
48 pub token_metadata: BTreeMap<Address, Tip20TokenMetadata>,
50}
51
52#[rpc(server, namespace = "tempo")]
53pub trait TempoSimulateApi {
54 #[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#[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
81fn 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 if let Some(to) = call.to.as_ref().and_then(|k| k.to())
94 && to.is_tip20()
95 {
96 addrs.insert(*to);
97 }
98 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 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 let prefetched = extract_tip20_targets(&payload);
129
130 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 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 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 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}