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