1use crate::{FoundryTransactionBuilder, provider::is_rpc_method_not_found};
4use alloy_network::{Network, TransactionBuilder};
5use alloy_provider::Provider;
6use alloy_transport::{TransportError, TransportErrorKind};
7use eyre::Result;
8use serde::Deserialize;
9use tempo_alloy::{chainspec::hardfork::TempoHardfork, rpc::ForkSchedule};
10use tempo_primitives::transaction::TEMPO_EXPIRING_NONCE_KEY;
11
12#[derive(Debug, Deserialize)]
13#[serde(rename_all = "camelCase")]
14struct AnvilNodeInfo {
15 hard_fork: Option<String>,
16 network: Option<String>,
17}
18
19pub async fn is_tempo_hardfork_active<N: Network, P: Provider<N>>(
24 provider: &P,
25 hardfork: TempoHardfork,
26) -> Result<bool> {
27 Ok(active_tempo_hardfork(provider).await? >= hardfork)
28}
29
30pub async fn active_tempo_hardfork<N: Network, P: Provider<N>>(
33 provider: &P,
34) -> Result<TempoHardfork> {
35 match fork_schedule_hardfork(provider).await {
36 Ok(hardfork) => Ok(hardfork),
37 Err(err) if is_rpc_method_not_found(&err) => match anvil_tempo_hardfork(provider).await {
38 Ok(Some(hardfork)) => Ok(hardfork),
39 _ => Err(err.into()),
40 },
41 Err(err) => Err(err.into()),
42 }
43}
44
45pub async fn ensure_expiring_nonce_discriminator_active<N: Network, P: Provider<N>>(
51 provider: &P,
52 tx: &N::TransactionRequest,
53) -> Result<()>
54where
55 N::TransactionRequest: FoundryTransactionBuilder<N>,
56{
57 if tx.nonce_key() == Some(TEMPO_EXPIRING_NONCE_KEY)
58 && let Some(nonce) = tx.nonce().filter(|nonce| *nonce != 0)
59 && matches!(is_tempo_hardfork_active(provider, TempoHardfork::T12).await, Ok(false))
60 {
61 eyre::bail!(
62 "expiring nonce transactions must use nonce 0 before the Tempo T12 hardfork, got nonce {nonce}; non-zero expiring nonce discriminators (TIP-1106) are not active on this chain"
63 );
64 }
65 Ok(())
66}
67
68async fn fork_schedule_hardfork<N: Network, P: Provider<N>>(
69 provider: &P,
70) -> Result<TempoHardfork, TransportError> {
71 let schedule = provider.raw_request::<_, ForkSchedule>("tempo_forkSchedule".into(), ()).await?;
72 schedule.active.parse::<TempoHardfork>().map_err(TransportErrorKind::custom)
73}
74
75async fn anvil_tempo_hardfork<N: Network, P: Provider<N>>(
76 provider: &P,
77) -> Result<Option<TempoHardfork>, TransportError> {
78 let info = provider.raw_request::<_, AnvilNodeInfo>("anvil_nodeInfo".into(), ()).await?;
79 Ok(hardfork_from_anvil_node_info(&info))
80}
81
82fn hardfork_from_anvil_node_info(info: &AnvilNodeInfo) -> Option<TempoHardfork> {
83 if info.network.as_deref() != Some("tempo") {
84 return None;
85 }
86 info.hard_fork.as_deref()?.parse().ok()
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use alloy_json_rpc::ErrorPayload;
93 use alloy_primitives::U256;
94 use alloy_provider::{ProviderBuilder, mock::Asserter};
95 use tempo_alloy::{TempoNetwork, rpc::TempoTransactionRequest};
96
97 fn mocked_provider(asserter: Asserter) -> impl Provider<TempoNetwork> {
98 ProviderBuilder::new().network::<TempoNetwork>().connect_mocked_client(asserter)
99 }
100
101 #[tokio::test]
102 async fn tempo_fork_schedule_detects_t3_activation() {
103 for (active, expected) in [("T2", false), ("T3", true), ("T13", true), ("T14", true)] {
104 let asserter = Asserter::new();
105 asserter.push_success(&serde_json::json!({ "active": active, "schedule": [] }));
106 let provider = mocked_provider(asserter);
107 assert_eq!(
108 is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.unwrap(),
109 expected
110 );
111 }
112 }
113
114 #[tokio::test]
115 async fn tempo_fork_schedule_rejects_unknown_hardfork() {
116 let asserter = Asserter::new();
117 asserter.push_success(&serde_json::json!({ "active": "FutureFork", "schedule": [] }));
118 let provider = mocked_provider(asserter);
119 assert!(is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.is_err());
120 }
121
122 #[test]
123 fn hardfork_from_anvil_node_info_requires_tempo_network() {
124 let info = |network: &str, hard_fork: &str| AnvilNodeInfo {
125 network: Some(network.to_string()),
126 hard_fork: Some(hard_fork.to_string()),
127 };
128 assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "T3")), Some(TempoHardfork::T3));
129 assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "T11")), Some(TempoHardfork::T11));
130 assert_eq!(hardfork_from_anvil_node_info(&info("tempo", "FutureFork")), None);
131 assert_eq!(hardfork_from_anvil_node_info(&info("ethereum", "T3")), None);
132 }
133
134 #[tokio::test]
135 async fn anvil_node_info_fallback_detects_hardfork_activation() {
136 let asserter = Asserter::new();
137 for _ in 0..2 {
138 asserter.push_failure(ErrorPayload {
139 code: -32601,
140 message: "Method not found".into(),
141 data: None,
142 });
143 asserter.push_success(&serde_json::json!({ "network": "tempo", "hardFork": "T3" }));
144 }
145 let provider =
146 ProviderBuilder::new().network::<TempoNetwork>().connect_mocked_client(asserter);
147 assert!(is_tempo_hardfork_active(&provider, TempoHardfork::T3).await.unwrap());
148 assert!(!is_tempo_hardfork_active(&provider, TempoHardfork::T4).await.unwrap());
149 }
150
151 #[tokio::test]
152 async fn expiring_nonce_discriminator_requires_t12_when_hardfork_is_known() {
153 let request = |nonce_key, nonce| {
154 let mut tx = TempoTransactionRequest::default();
155 tx.set_nonce_key(nonce_key);
156 tx.set_nonce(nonce);
157 tx
158 };
159 let discriminator = request(TEMPO_EXPIRING_NONCE_KEY, 7);
160
161 for (tx, active, accepted) in [
163 (&discriminator, Some("T11"), false),
164 (&discriminator, Some("T12"), true),
165 (&discriminator, Some("FutureFork"), true),
166 (&discriminator, None, true),
167 (&request(TEMPO_EXPIRING_NONCE_KEY, 0), None, true),
168 (&request(U256::from(1), 7), None, true),
169 ] {
170 let asserter = Asserter::new();
171 if let Some(active) = active {
172 asserter.push_success(&serde_json::json!({ "active": active, "schedule": [] }));
173 }
174 let provider = mocked_provider(asserter);
175 let result = ensure_expiring_nonce_discriminator_active(&provider, tx).await;
176 assert_eq!(
177 result.is_ok(),
178 accepted,
179 "nonce {:?} at {active:?}: {result:?}",
180 tx.nonce()
181 );
182 }
183 }
184}