1use crate::{
7 call_spec::CallSpec,
8 cmd::auth::{confirm_auth_rpc_disclosure, confirm_auth_rpc_disclosure_during_build},
9 tempo,
10 tx::{self, CastTxBuilder},
11};
12use alloy_consensus::SignableTransaction;
13use alloy_eips::eip2718::Encodable2718;
14use alloy_network::{EthereumWallet, NetworkTransactionBuilder, TransactionBuilder};
15use alloy_primitives::Address;
16use alloy_provider::Provider;
17use alloy_signer::Signer;
18use clap::Parser;
19use eyre::{Result, eyre};
20use foundry_cli::{
21 opts::{EthereumOpts, TempoOpts, TransactionOpts},
22 utils::{self, LoadConfig, maybe_print_resolved_lane, resolve_lane},
23};
24use foundry_common::{
25 FoundryTransactionBuilder,
26 provider::ProviderBuilder,
27 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
28};
29use foundry_wallets::{TempoAccountsWallet, WalletOpts, WalletSigner};
30use tempo_alloy::TempoNetwork;
31
32#[derive(Debug, Parser)]
36pub struct BatchMakeTxArgs {
37 #[arg(long = "call", value_name = "SPEC", required = true)]
44 pub calls: Vec<String>,
45
46 #[command(flatten)]
47 pub tx: TransactionOpts,
48
49 #[arg(long)]
51 pub force: bool,
52
53 #[command(flatten)]
54 pub eth: EthereumOpts,
55
56 #[arg(long)]
58 pub raw_unsigned: bool,
59
60 #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
62 pub ethsign: bool,
63}
64
65impl BatchMakeTxArgs {
66 pub async fn run(self) -> Result<()> {
67 let Self { calls, mut tx, force, eth, raw_unsigned, ethsign } = self;
68 let has_nonce = tx.nonce.is_some();
69 let has_session = tx.tempo.session_id()?.is_some();
70 let expires_at = tx.tempo.resolve_expires();
71
72 if calls.is_empty() {
73 return Err(eyre!("No calls specified. Use --call to specify at least one call."));
74 }
75
76 if has_session && raw_unsigned {
77 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --raw-unsigned");
78 }
79 if has_session && ethsign {
80 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --ethsign");
81 }
82
83 let config = eth.load_config()?;
84 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
85
86 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
89
90 let call_specs: Vec<CallSpec> =
92 calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
93
94 let chain = utils::get_chain(config.chain, &provider).await?;
96 let (signer, tempo_access_key) =
97 resolve_signer(&tx.tempo, ð.wallet, chain.id(), raw_unsigned).await?;
98 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
99 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
100 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
101
102 let mut tempo_calls = Vec::with_capacity(call_specs.len());
103 for (i, spec) in call_specs.iter().enumerate() {
104 tempo_calls.push(
105 spec.resolve(
106 i,
107 chain,
108 &provider,
109 etherscan_api_key.as_deref(),
110 etherscan_api_url.as_deref(),
111 )
112 .await?,
113 );
114 }
115
116 sh_status!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
117 tempo::print_expires(expires_at)?;
118
119 if let Some(ref access_key) = tempo_access_key {
121 tx.tempo.key_id = Some(access_key.key_id()?);
122 }
123
124 let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
126
127 builder.tx.calls = tempo_calls;
129
130 let first_call_to = call_specs.first().map(|s| s.to);
132 let builder = builder.with_to(first_call_to.map(Into::into)).await?;
133 let tx_builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
134
135 if raw_unsigned {
136 if eth.wallet.from.is_none() && !has_nonce {
137 eyre::bail!(
138 "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
139 );
140 }
141
142 let from = eth.wallet.from.unwrap_or(Address::ZERO);
143 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, from, force)? {
144 return Ok(());
145 }
146 let (mut tx, _) = tx_builder.build(from).await?;
147 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
148 let fee_token = resolve_and_set_fee_token(
149 (!config.eth_rpc_curl).then_some(&provider),
150 Some(chain),
151 &mut tx,
152 Some(from),
153 )
154 .await?;
155 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
156 let raw_tx =
157 alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
158 sh_println!("{raw_tx}")?;
159 return Ok(());
160 }
161
162 if ethsign {
163 let sender = config.sender.into();
164 if tx_builder.has_auth() && !confirm_auth_rpc_disclosure(&tx_builder, &sender, force)? {
165 return Ok(());
166 }
167 let (mut tx, _) = tx_builder.build(config.sender).await?;
168 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
169 let fee_token = resolve_and_set_fee_token(
170 (!config.eth_rpc_curl).then_some(&provider),
171 Some(chain),
172 &mut tx,
173 Some(config.sender),
174 )
175 .await?;
176 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
177 let signed_tx = provider.sign_transaction(tx).await?;
178 sh_println!("{signed_tx}")?;
179 return Ok(());
180 }
181
182 let signed_tx = if let Some(ref access_key) = tempo_access_key {
183 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, access_key.account(), force)?
184 {
185 return Ok(());
186 }
187 let (mut tx, _, prepared) = tx_builder.build_with_tempo_wallet(access_key).await?;
188 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
189 let fee_token = resolve_and_set_fee_token(
190 (!config.eth_rpc_curl).then_some(&provider),
191 Some(chain),
192 &mut tx,
193 Some(prepared.account()),
194 )
195 .await?;
196 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
197 let raw_tx = tx.sign_with_tempo_wallet(&prepared).await?;
198 alloy_primitives::hex::encode(raw_tx)
199 } else {
200 let signer = match signer {
201 Some(s) => s,
202 None => eth.wallet.signer().await?,
203 };
204 tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?;
205 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, &signer, force)? {
206 return Ok(());
207 }
208 let (mut tx, _) = tx_builder.build(&signer).await?;
209 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
210 let fee_token = resolve_and_set_fee_token(
211 (!config.eth_rpc_curl).then_some(&provider),
212 Some(chain),
213 &mut tx,
214 Some(Signer::address(&signer)),
215 )
216 .await?;
217 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
218 let envelope = tx.build(&EthereumWallet::new(signer)).await?;
219 alloy_primitives::hex::encode(envelope.encoded_2718())
220 };
221
222 sh_println!("0x{signed_tx}")?;
223
224 Ok(())
225 }
226}
227
228async fn resolve_signer(
229 tempo: &TempoOpts,
230 wallet: &WalletOpts,
231 chain_id: u64,
232 raw_unsigned: bool,
233) -> Result<(Option<WalletSigner>, Option<TempoAccountsWallet>)> {
234 if raw_unsigned {
235 let (_, access_key) = wallet.maybe_signer_for_chain(chain_id).await?;
236 return Ok((None, access_key));
237 }
238
239 tempo::resolve_session_or_wallet_signer(tempo, wallet, chain_id).await
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use alloy_primitives::address;
246
247 #[test]
248 fn raw_unsigned_resolver_discards_signer_but_keeps_access_key_metadata() {
249 let runtime = tokio::runtime::Runtime::new().unwrap();
250 runtime.block_on(async {
251 let wallet = WalletOpts {
252 tempo_access_key: Some(
253 "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
254 .to_string(),
255 ),
256 tempo_root_account: Some(address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")),
257 ..Default::default()
258 };
259
260 let (signer, access_key) =
261 resolve_signer(&TempoOpts::default(), &wallet, 31337, true).await.unwrap();
262
263 assert!(signer.is_none());
264 let access_key = access_key.expect("access-key metadata");
265 assert_eq!(
266 access_key.account(),
267 address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
268 );
269 assert_eq!(
270 access_key.key_id().unwrap(),
271 address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
272 );
273 });
274 }
275}