1use crate::tx::fill_transaction_gas_fees;
4use alloy_network::{Ethereum, Network, TransactionBuilder};
5use alloy_primitives::{Address, B256};
6use alloy_provider::Provider;
7use alloy_rpc_client::BuiltInConnectionString;
8use alloy_transport::{BoxTransport, TransportConnect, TransportError};
9use eyre::Result;
10use foundry_cli::{
11 json::print_json_success,
12 opts::{EthereumOpts, TempoOpts},
13 utils::{LoadConfig, get_chain},
14};
15use foundry_common::{
16 FoundryTransactionBuilder,
17 provider::{ProviderBuilder, RetryProvider},
18 shell,
19 tempo::{is_tempo_hardfork_active, maybe_print_fee_token, resolve_and_set_fee_token},
20};
21use foundry_config::{Chain, Config, Eip1559FeeEstimatePreset};
22use foundry_evm::hardfork::TempoHardfork;
23use foundry_evm_networks::NetworkVariant;
24use foundry_wallets::{TempoAccountsWallet, WalletOpts, WalletSigner};
25use serde_json::Value;
26use std::str::FromStr;
27use tempo_alloy::{
28 TempoNetwork,
29 rpc::TempoTransactionRequest,
30 transport::{RelayConnector, SponsorshipMode},
31};
32
33pub use foundry_common::tempo::TempoSponsor;
34
35pub(crate) fn tempo_provider(
37 opts: &impl LoadConfig,
38) -> Result<(Config, RetryProvider<TempoNetwork>)> {
39 let config = opts.load_config()?;
40 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
41 Ok((config, provider))
42}
43
44pub(crate) async fn apply_fee_payment<N, P>(
47 sponsor: Option<&TempoSponsor>,
48 provider: Option<&P>,
49 tx: &mut N::TransactionRequest,
50 payer: Address,
51) -> Result<()>
52where
53 N: Network,
54 N::TransactionRequest: FoundryTransactionBuilder<N>,
55 P: Provider<N>,
56{
57 if tx.gas_price().is_some() {
58 eyre::ensure!(
59 sponsor.is_none() && !tx.is_tempo_aa(),
60 "Tempo transaction options cannot be combined with a legacy transaction"
61 );
62 return Ok(());
63 }
64
65 if sponsor.is_some() {
66 maybe_attach_sponsor(sponsor, provider, tx, payer).await
67 } else {
68 resolve_and_print_fee_token(provider, tx, Some(payer)).await
69 }
70}
71
72pub(crate) async fn maybe_attach_sponsor<N, P>(
75 sponsor: Option<&TempoSponsor>,
76 provider: Option<&P>,
77 tx: &mut N::TransactionRequest,
78 payer: Address,
79) -> Result<()>
80where
81 N: Network,
82 N::TransactionRequest: FoundryTransactionBuilder<N>,
83 P: Provider<N>,
84{
85 if let Some(sponsor) = sponsor {
86 let provider = provider.map(|p| p as &dyn Provider<N>);
87 sponsor.resolve_and_set_fee_token(provider, tx).await?;
88 sponsor.attach_and_print::<N>(tx, payer).await?;
89 }
90 Ok(())
91}
92
93pub(crate) async fn resolve_and_print_fee_token<N, P>(
95 provider: Option<&P>,
96 tx: &mut N::TransactionRequest,
97 fee_payer: Option<Address>,
98) -> Result<()>
99where
100 N: Network,
101 N::TransactionRequest: FoundryTransactionBuilder<N>,
102 P: Provider<N>,
103{
104 let dyn_provider = provider.map(|p| p as &dyn Provider<N>);
105 let fee_token = resolve_and_set_fee_token(dyn_provider, tx, fee_payer).await?;
106 maybe_print_fee_token(provider, fee_token).await
107}
108
109pub(crate) async fn sponsor_hash<N, P>(
112 provider: Option<&P>,
113 tx: &mut N::TransactionRequest,
114 from: Address,
115 fee_payer: Option<Address>,
116) -> Result<B256>
117where
118 N: Network,
119 N::TransactionRequest: FoundryTransactionBuilder<N>,
120 P: Provider<N>,
121{
122 if fee_payer.is_some() {
123 let provider = provider.map(|p| p as &dyn Provider<N>);
124 resolve_and_set_fee_token(provider, tx, fee_payer).await?;
125 }
126 tx.compute_sponsor_hash(from)
127 .ok_or_else(|| eyre::eyre!("This network does not support sponsored transactions"))
128}
129
130pub(crate) fn print_payload<F>(payload: Value, human: F) -> Result<()>
132where
133 F: FnOnce(&Value) -> Result<()>,
134{
135 if shell::is_json() {
136 print_json_success(payload)?;
137 } else {
138 human(&payload)?;
139 }
140 Ok(())
141}
142
143pub(crate) fn print_expires(expires_at: Option<u64>) -> Result<()> {
144 if let Some(ts) = expires_at {
145 sh_status!("Transaction expires at unix timestamp {ts}")?;
146 }
147 Ok(())
148}
149
150pub(crate) async fn resolve_session_or_wallet_signer(
156 tempo: &TempoOpts,
157 wallet: &WalletOpts,
158 chain_id: u64,
159) -> Result<(Option<WalletSigner>, Option<TempoAccountsWallet>)> {
160 let (signer, access_key) = match tempo.session_signer_for_wallet(wallet, chain_id)? {
161 Some(session) => (None, Some(session.access_key)),
162 None => wallet.maybe_signer_for_chain(chain_id).await?,
163 };
164 if let (Some(from), Some(access_key)) = (wallet.from, &access_key) {
165 eyre::ensure!(
166 access_key.account() == from,
167 "sender {from} does not match Tempo account {}",
168 access_key.account()
169 );
170 }
171 Ok((signer, access_key))
172}
173
174pub(crate) fn ensure_session_not_browser(tempo: &TempoOpts, browser: bool) -> Result<()> {
175 if browser && tempo.session_id()?.is_some() {
176 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --browser");
177 }
178 Ok(())
179}
180
181pub(crate) async fn require_hardfork<P: Provider<TempoNetwork>>(
183 provider: &P,
184 hardfork: TempoHardfork,
185 message: &str,
186) -> Result<()> {
187 if !is_tempo_hardfork_active(provider, hardfork).await? {
188 eyre::bail!("{message}");
189 }
190 Ok(())
191}
192
193pub(crate) async fn ensure_tempo_precompile_active<P: Provider<TempoNetwork>>(
197 provider: &P,
198 hardfork: TempoHardfork,
199 precompile: Address,
200 requirement: &str,
201) -> Result<()> {
202 let active = match is_tempo_hardfork_active(provider, hardfork).await {
203 Ok(active) => active,
204 Err(_) => !provider.get_code_at(precompile).await?.is_empty(),
205 };
206 eyre::ensure!(active, "{requirement}");
207 Ok(())
208}
209
210#[derive(Clone, Debug)]
215pub(crate) struct ExistingTransportConnector {
216 transport: BoxTransport,
217 is_local: bool,
218}
219
220impl TransportConnect for ExistingTransportConnector {
221 fn is_local(&self) -> bool {
222 self.is_local
223 }
224
225 async fn get_transport(&self) -> Result<BoxTransport, TransportError> {
226 Ok(self.transport.clone())
227 }
228}
229
230pub(crate) fn sponsor_relay_connector<N: Network>(
231 provider: &impl Provider<N>,
232 sponsor_url: &str,
233) -> Result<RelayConnector<ExistingTransportConnector, BuiltInConnectionString>> {
234 let default = ExistingTransportConnector {
235 transport: provider.client().transport().clone(),
236 is_local: provider.client().is_local(),
237 };
238 let relay = BuiltInConnectionString::from_str(sponsor_url)?;
239 Ok(RelayConnector::with_config(default, relay, SponsorshipMode::SignOnly, false))
240}
241
242pub(crate) async fn resolve_transaction_network_and_signer(
249 tempo: &TempoOpts,
250 eth: &EthereumOpts,
251) -> Result<(NetworkVariant, Option<WalletSigner>, Option<TempoAccountsWallet>)> {
252 let config = eth.load_config()?;
253 let has_session = tempo.session_id()?.is_some();
254 let requires_tempo = tempo.is_tempo() || has_session;
255 crate::cmd::validate_tempo_network(&config, requires_tempo)?;
256 if has_session {
257 return Ok((NetworkVariant::Tempo, None, None));
258 }
259
260 let mut explicit_wallet = eth.wallet.clone();
261 explicit_wallet.from = None;
262 let (signer, access_key) = explicit_wallet.maybe_signer().await?;
263 let network =
264 crate::cmd::resolve_transaction_network(&config, requires_tempo || access_key.is_some())
265 .await?;
266 if let (Some(from), Some(access_key)) = (eth.wallet.from, &access_key) {
267 eyre::ensure!(
268 access_key.account() == from,
269 "sender {from} does not match Tempo account {}",
270 access_key.account()
271 );
272 }
273 if !network.is_tempo() || signer.is_some() || access_key.is_some() || eth.wallet.from.is_none()
274 {
275 return Ok((network, signer, access_key));
276 }
277
278 let provider = ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
280 let chain = get_chain(config.chain, &provider).await?;
281 let (signer, access_key) = eth.wallet.maybe_signer_for_chain(chain.id()).await?;
282 if let Some(access_key) = &access_key {
283 let from = eth.wallet.from.expect("checked above");
284 eyre::ensure!(
285 access_key.account() == from,
286 "sender {from} does not match active Tempo account {}",
287 access_key.account()
288 );
289 }
290 Ok((network, signer, access_key))
291}
292
293pub(crate) async fn fill_access_key_transaction<P>(
296 provider: &P,
297 tx: &mut TempoTransactionRequest,
298 access_key: &TempoAccountsWallet,
299 chain: Chain,
300 eip1559_fee_estimate: Eip1559FeeEstimatePreset,
301) -> Result<TempoAccountsWallet>
302where
303 P: Provider<TempoNetwork>,
304{
305 tx.set_chain_id(chain.id());
306 let prepared = access_key.prepare_request(provider, tx).await?;
307
308 if tx.nonce().is_none() {
309 tx.set_nonce(provider.get_transaction_count(prepared.account()).await?);
310 }
311 fill_transaction_gas_fees(provider, tx, chain.is_legacy(), false, eip1559_fee_estimate).await?;
312 if tx.gas_limit().is_none() {
313 tx.set_gas_limit(provider.estimate_gas(tx.clone()).await?);
314 }
315
316 Ok(prepared)
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use alloy_provider::{ProviderBuilder as AlloyProviderBuilder, mock::Asserter};
323 use alloy_rpc_client::RpcClient;
324
325 #[tokio::test]
326 async fn legacy_fee_payment_skips_stored_token_and_rejects_tempo_fields() {
327 let asserter = Asserter::new();
328 let provider =
329 AlloyProviderBuilder::new().network::<TempoNetwork>().connect_mocked_client(asserter);
330 let payer = Address::repeat_byte(0x11);
331 let mut tx = TempoTransactionRequest::default();
332 tx.set_gas_price(1);
333
334 apply_fee_payment(None, Some(&provider), &mut tx, payer).await.unwrap();
335 assert!(tx.fee_token().is_none());
336
337 tx.set_fee_token(Address::repeat_byte(0x22));
338 assert_eq!(
339 apply_fee_payment(None, Some(&provider), &mut tx, payer).await.unwrap_err().to_string(),
340 "Tempo transaction options cannot be combined with a legacy transaction"
341 );
342 }
343
344 #[tokio::test]
345 async fn sponsor_relay_reuses_existing_default_transport() {
346 let asserter = Asserter::new();
347 asserter.push_success(&alloy_primitives::U64::from(42));
348 let provider = AlloyProviderBuilder::new().connect_mocked_client(asserter);
349 let connector =
350 sponsor_relay_connector(&provider, "http://127.0.0.1:1").expect("valid relay");
351 let transport = connector.get_transport().await.expect("relay transport");
352 let client = RpcClient::builder().transport(transport, true);
353 let relayed = AlloyProviderBuilder::new().connect_client(client);
354
355 assert_eq!(relayed.get_block_number().await.unwrap(), 42);
356 }
357}