1use crate::{
8 call_spec::CallSpec,
9 cmd::{
10 auth::confirm_auth_rpc_disclosure_during_build,
11 send::{cast_send, cast_send_with_tempo_wallet},
12 },
13 tempo,
14 tx::{self, CastTxBuilder, SendTxOpts},
15};
16use alloy_network::{EthereumWallet, TransactionBuilder};
17use alloy_provider::{Provider, ProviderBuilder as AlloyProviderBuilder};
18use alloy_signer::Signer;
19use clap::Parser;
20use eyre::{Result, eyre};
21use foundry_cli::{
22 opts::TransactionOpts,
23 utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane},
24};
25use foundry_common::provider::ProviderBuilder;
26use std::time::Duration;
27use tempo_alloy::TempoNetwork;
28
29#[derive(Debug, Parser)]
33pub struct BatchSendArgs {
34 #[arg(long = "call", value_name = "SPEC", required = true)]
42 pub calls: Vec<String>,
43
44 #[command(flatten)]
45 pub send_tx: SendTxOpts,
46
47 #[command(flatten)]
48 pub tx: TransactionOpts,
49
50 #[arg(long)]
52 pub force: bool,
53
54 #[arg(long, requires = "from")]
56 pub unlocked: bool,
57}
58
59impl BatchSendArgs {
60 pub async fn run(self) -> Result<()> {
61 let Self { calls, send_tx, mut tx, force, unlocked } = self;
62 let has_session = tx.tempo.session_id()?.is_some();
63 if has_session && unlocked {
66 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --unlocked");
67 }
68 if has_session && send_tx.browser.browser {
69 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --browser");
70 }
71
72 let expires_at = tx.tempo.resolve_expires();
73
74 if calls.is_empty() {
75 return Err(eyre!("No calls specified. Use --call to specify at least one call."));
76 }
77
78 let config = send_tx.eth.load_config()?;
79 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
80
81 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
84
85 if let Some(interval) = send_tx.poll_interval {
86 provider.client().set_poll_interval(Duration::from_secs(interval))
87 }
88
89 let call_specs: Vec<CallSpec> =
91 calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
92
93 let chain = utils::get_chain(config.chain, &provider).await?;
95 let (signer, tempo_access_key) =
96 tempo::resolve_session_or_wallet_signer(&tx.tempo, &send_tx.eth.wallet, chain.id())
97 .await?;
98
99 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
100 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
101 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
102
103 let mut tempo_calls = Vec::with_capacity(call_specs.len());
105 for (i, spec) in call_specs.iter().enumerate() {
106 tempo_calls.push(
107 spec.resolve(
108 i,
109 chain,
110 &provider,
111 etherscan_api_key.as_deref(),
112 etherscan_api_url.as_deref(),
113 )
114 .await?,
115 );
116 }
117
118 sh_status!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
119 tempo::print_expires(expires_at)?;
120
121 if let Some(ref access_key) = tempo_access_key {
123 tx.tempo.key_id = Some(access_key.key_id()?);
124 }
125
126 let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
128
129 builder.tx.calls = tempo_calls;
131
132 let first_call_to = call_specs.first().map(|s| s.to);
135 let builder = builder.with_to(first_call_to.map(Into::into)).await?;
136
137 let builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
139
140 let timeout = send_tx.timeout.unwrap_or(config.transaction_timeout);
141
142 if unlocked {
143 if !confirm_auth_rpc_disclosure_during_build(&builder, config.sender, force)? {
144 return Ok(());
145 }
146 let (tx, _) = builder.build(config.sender).await?;
147 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
148 cast_send(
149 provider,
150 tx,
151 Some(chain),
152 None,
153 send_tx.cast_async,
154 send_tx.sync,
155 send_tx.confirmations,
156 timeout,
157 !config.eth_rpc_curl,
158 )
159 .await
160 .map(drop)
161 } else {
162 if let Some(ref access_key) = tempo_access_key {
163 if !confirm_auth_rpc_disclosure_during_build(&builder, access_key.account(), force)?
164 {
165 return Ok(());
166 }
167 let (tx_request, _, prepared) = builder.build_with_tempo_wallet(access_key).await?;
168 maybe_print_resolved_lane(
169 resolved_lane.as_ref(),
170 tx_request.nonce().unwrap_or_default(),
171 )?;
172 cast_send_with_tempo_wallet(
173 &provider,
174 tx_request,
175 &prepared,
176 Some(chain),
177 None,
178 send_tx.cast_async,
179 send_tx.sync,
180 send_tx.confirmations,
181 timeout,
182 !config.eth_rpc_curl,
183 )
184 .await?;
185 } else {
186 let signer = match signer {
187 Some(s) => s,
188 None => send_tx.eth.wallet.signer().await?,
189 };
190 tx::validate_from_address(send_tx.eth.wallet.from, Signer::address(&signer))?;
191 if !confirm_auth_rpc_disclosure_during_build(&builder, &signer, force)? {
192 return Ok(());
193 }
194 let (tx_request, _) = builder.build(&signer).await?;
195 maybe_print_resolved_lane(
196 resolved_lane.as_ref(),
197 tx_request.nonce().unwrap_or_default(),
198 )?;
199 let wallet = EthereumWallet::from(signer);
200 let provider = AlloyProviderBuilder::<_, _, TempoNetwork>::default()
201 .wallet(wallet)
202 .connect_provider(&provider);
203
204 cast_send(
205 provider,
206 tx_request,
207 Some(chain),
208 None,
209 send_tx.cast_async,
210 send_tx.sync,
211 send_tx.confirmations,
212 timeout,
213 !config.eth_rpc_curl,
214 )
215 .await?;
216 }
217
218 Ok(())
219 }
220 }
221}