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