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, provider::ProviderBuilder, tempo::print_resolved_fee_token_selection,
25};
26use foundry_wallets::{TempoAccessKeyConfig, WalletOpts, WalletSigner};
27use tempo_alloy::TempoNetwork;
28
29#[derive(Debug, Parser)]
33pub struct BatchMakeTxArgs {
34 #[arg(long = "call", value_name = "SPEC", required = true)]
41 pub calls: Vec<String>,
42
43 #[command(flatten)]
44 pub tx: TransactionOpts,
45
46 #[command(flatten)]
47 pub eth: EthereumOpts,
48
49 #[arg(long)]
51 pub raw_unsigned: bool,
52
53 #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
55 pub ethsign: bool,
56}
57
58impl BatchMakeTxArgs {
59 pub async fn run(self) -> Result<()> {
60 let Self { calls, mut tx, eth, raw_unsigned, ethsign } = self;
61 let has_nonce = tx.nonce.is_some();
62 let has_session = tx.tempo.session_id()?.is_some();
63 let expires_at = tx.tempo.resolve_expires();
64
65 if calls.is_empty() {
66 return Err(eyre!("No calls specified. Use --call to specify at least one call."));
67 }
68
69 if has_session && raw_unsigned {
70 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --raw-unsigned");
71 }
72 if has_session && ethsign {
73 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --ethsign");
74 }
75
76 let config = eth.load_config()?;
77 let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
78
79 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
82
83 let call_specs: Vec<CallSpec> =
85 calls.iter().map(|s| CallSpec::parse(s)).collect::<Result<Vec<_>>>()?;
86
87 let chain = utils::get_chain(config.chain, &provider).await?;
89 let (signer, tempo_access_key) =
90 resolve_signer(&tx.tempo, ð.wallet, chain.id(), raw_unsigned).await?;
91 let etherscan_config = config.get_etherscan_config_with_chain(Some(chain)).ok().flatten();
92 let etherscan_api_key = etherscan_config.as_ref().map(|c| c.key.clone());
93 let etherscan_api_url = etherscan_config.map(|c| c.api_url);
94
95 let mut tempo_calls = Vec::with_capacity(call_specs.len());
96 for (i, spec) in call_specs.iter().enumerate() {
97 tempo_calls.push(
98 spec.resolve(
99 i,
100 chain,
101 &provider,
102 etherscan_api_key.as_deref(),
103 etherscan_api_url.as_deref(),
104 )
105 .await?,
106 );
107 }
108
109 sh_status!("Building batch transaction with {} call(s)...", tempo_calls.len())?;
110 tempo::print_expires(expires_at)?;
111
112 if let Some(ref access_key) = tempo_access_key {
114 tx.tempo.key_id = Some(access_key.key_address);
115 }
116
117 let mut builder = CastTxBuilder::<TempoNetwork, _, _>::new(&provider, tx, &config).await?;
119
120 builder.tx.calls = tempo_calls;
122
123 let first_call_to = call_specs.first().map(|s| s.to);
125 let builder = builder.with_to(first_call_to.map(Into::into)).await?;
126 let tx_builder = builder.with_code_sig_and_args(None, None, vec![]).await?;
127
128 if raw_unsigned {
129 if eth.wallet.from.is_none() && !has_nonce {
130 eyre::bail!(
131 "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
132 );
133 }
134
135 let from = eth.wallet.from.unwrap_or(Address::ZERO);
136 let (tx, _) = tx_builder.build(from).await?;
137 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
138 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
139 let raw_tx =
140 alloy_primitives::hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
141 sh_println!("{raw_tx}")?;
142 return Ok(());
143 }
144
145 if ethsign {
146 let (tx, _) = tx_builder.build(config.sender).await?;
147 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
148 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
149 let signed_tx = provider.sign_transaction(tx).await?;
150 sh_println!("{signed_tx}")?;
151 return Ok(());
152 }
153
154 let signer = match signer {
156 Some(s) => s,
157 None => eth.wallet.signer().await?,
158 };
159
160 let signed_tx = if let Some(ref access_key) = tempo_access_key {
161 let (tx, _) =
162 tx_builder.build_with_access_key(access_key.wallet_address, access_key).await?;
163 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
164 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
165 let raw_tx = tx
166 .sign_with_access_key(
167 &provider,
168 &signer,
169 access_key.wallet_address,
170 access_key.key_address,
171 access_key.key_authorization.as_ref(),
172 )
173 .await?;
174 alloy_primitives::hex::encode(raw_tx)
175 } else {
176 tx::validate_from_address(eth.wallet.from, Signer::address(&signer))?;
177 let (tx, _) = tx_builder.build(&signer).await?;
178 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
179 print_resolved_fee_token_selection(Some(chain), tx.fee_token())?;
180 let envelope = tx.build(&EthereumWallet::new(signer)).await?;
181 alloy_primitives::hex::encode(envelope.encoded_2718())
182 };
183
184 sh_println!("0x{signed_tx}")?;
185
186 Ok(())
187 }
188}
189
190async fn resolve_signer(
191 tempo: &TempoOpts,
192 wallet: &WalletOpts,
193 chain_id: u64,
194 raw_unsigned: bool,
195) -> Result<(Option<WalletSigner>, Option<TempoAccessKeyConfig>)> {
196 if raw_unsigned {
197 let (_, access_key) = wallet.maybe_signer().await?;
198 return Ok((None, access_key));
199 }
200
201 tempo::resolve_session_or_wallet_signer(tempo, wallet, chain_id).await
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use alloy_primitives::address;
208
209 #[test]
210 fn raw_unsigned_resolver_discards_signer_but_keeps_access_key_metadata() {
211 let runtime = tokio::runtime::Runtime::new().unwrap();
212 runtime.block_on(async {
213 let wallet = WalletOpts {
214 tempo_access_key: Some(
215 "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
216 .to_string(),
217 ),
218 tempo_root_account: Some(address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")),
219 ..Default::default()
220 };
221
222 let (signer, access_key) =
223 resolve_signer(&TempoOpts::default(), &wallet, 31337, true).await.unwrap();
224
225 assert!(signer.is_none());
226 let access_key = access_key.expect("access-key metadata");
227 assert_eq!(
228 access_key.wallet_address,
229 address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
230 );
231 assert_eq!(
232 access_key.key_address,
233 address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
234 );
235 });
236 }
237}