1use super::auth::{confirm_auth_rpc_disclosure, confirm_auth_rpc_disclosure_during_build};
2use crate::{
3 tempo,
4 tx::{self, CastTxBuilder},
5};
6use alloy_consensus::{SignableTransaction, Signed};
7use alloy_eips::Encodable2718;
8use alloy_ens::NameOrAddress;
9use alloy_network::{
10 Ethereum, EthereumWallet, Network, NetworkTransactionBuilder, TransactionBuilder,
11};
12use alloy_primitives::{Address, hex};
13use alloy_provider::Provider;
14use alloy_signer::{Signature, Signer};
15use clap::Parser;
16use eyre::Result;
17use foundry_cli::{
18 json::print_scalar,
19 opts::{EthereumOpts, TransactionOpts},
20 utils::{LoadConfig, maybe_print_resolved_lane, resolve_lane},
21};
22use foundry_common::{
23 FoundryTransactionBuilder,
24 provider::ProviderBuilder,
25 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
26};
27use foundry_wallets::{TempoAccountsWallet, WalletSigner};
28use std::{path::PathBuf, str::FromStr};
29use tempo_alloy::TempoNetwork;
30
31#[derive(Debug, Parser)]
33pub struct MakeTxArgs {
34 #[arg(value_parser = NameOrAddress::from_str)]
38 to: Option<NameOrAddress>,
39
40 sig: Option<String>,
42
43 #[arg(allow_negative_numbers = true)]
45 args: Vec<String>,
46
47 #[command(subcommand)]
48 command: Option<MakeTxSubcommands>,
49
50 #[command(flatten)]
51 tx: TransactionOpts,
52
53 #[arg(long)]
55 force: bool,
56
57 #[arg(
59 long,
60 value_name = "BLOB_DATA_PATH",
61 conflicts_with = "legacy",
62 requires = "blob",
63 help_heading = "Transaction options"
64 )]
65 path: Option<PathBuf>,
66
67 #[command(flatten)]
68 eth: EthereumOpts,
69
70 #[arg(long)]
74 raw_unsigned: bool,
75
76 #[arg(long, requires = "from", conflicts_with = "raw_unsigned")]
78 ethsign: bool,
79
80 #[arg(
82 long,
83 value_name = "SIGNATURE",
84 requires = "from",
85 conflicts_with_all = ["raw_unsigned", "ethsign"]
86 )]
87 signature: Option<Signature>,
88}
89
90#[derive(Debug, Parser)]
91pub enum MakeTxSubcommands {
92 #[command(name = "--create")]
94 Create {
95 code: String,
97
98 sig: Option<String>,
100
101 #[arg(allow_negative_numbers = true)]
103 args: Vec<String>,
104 },
105}
106
107impl MakeTxArgs {
108 pub async fn run(self) -> Result<()> {
109 if self.tx.tempo.sponsor_url.is_some() {
110 eyre::bail!(
111 "--sponsor-url is not supported by cast mktx; use --tempo.sponsor with \
112 --tempo.sponsor-signer or --tempo.sponsor-sig"
113 );
114 }
115
116 if self.tx.tempo.session_id()?.is_some() {
117 return self.run_generic::<TempoNetwork>(None, None).await;
118 }
119
120 let (is_tempo, signer, access_key) =
121 tempo::resolve_transaction_network_and_signer(&self.tx.tempo, &self.eth).await?;
122 if is_tempo {
123 self.run_generic::<TempoNetwork>(signer, access_key).await
124 } else {
125 self.run_generic::<Ethereum>(signer, None).await
126 }
127 }
128
129 pub async fn run_generic<N: Network>(
130 self,
131 pre_resolved_signer: Option<WalletSigner>,
132 pre_resolved_access_key: Option<TempoAccountsWallet>,
133 ) -> Result<()>
134 where
135 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
136 N::UnsignedTx: SignableTransaction<Signature>,
137 N::TransactionRequest: FoundryTransactionBuilder<N>,
138 {
139 let Self {
140 to,
141 mut sig,
142 mut args,
143 command,
144 mut tx,
145 force,
146 path,
147 eth,
148 raw_unsigned,
149 ethsign,
150 signature,
151 } = self;
152 let has_session = tx.tempo.session_id()?.is_some();
153
154 let print_sponsor_hash = tx.tempo.print_sponsor_hash;
155 let sponsor_fee_payer = tx.tempo.sponsor;
156 let expires_at = tx.tempo.resolve_expires();
157 let tempo_sponsor =
158 if print_sponsor_hash { None } else { tx.tempo.sponsor_config().await? };
159
160 let blob_data = if let Some(path) = path { Some(std::fs::read(path)?) } else { None };
161
162 let code = if let Some(MakeTxSubcommands::Create {
163 code,
164 sig: constructor_sig,
165 args: constructor_args,
166 }) = command
167 {
168 sig = constructor_sig;
169 args = constructor_args;
170 Some(code)
171 } else {
172 None
173 };
174
175 let config = eth.load_config()?;
176
177 let provider = ProviderBuilder::<N>::from_config(&config)?.build()?;
178
179 let resolved_lane = resolve_lane(&mut tx.tempo, &config.root)?;
183
184 let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config)
185 .await?
186 .with_to(to)
187 .await?
188 .with_code_sig_and_args(code, sig, args)
189 .await?
190 .with_blob_data(blob_data)?;
191 let chain = tx_builder.chain();
192 let (signer, access_key) = if has_session || pre_resolved_access_key.is_some() {
193 tempo::resolve_session_or_wallet_signer(&tx.tempo, ð.wallet, chain.id()).await?
194 } else {
195 (pre_resolved_signer, None)
196 };
197
198 if print_sponsor_hash {
200 let (mut tx, from) = if let Some(access_key) = access_key {
204 if !confirm_auth_rpc_disclosure_during_build(
205 &tx_builder,
206 access_key.account(),
207 force,
208 )? {
209 return Ok(());
210 }
211 let (tx, _, prepared) = tx_builder.build_with_tempo_wallet(&access_key).await?;
212 (tx, prepared.account())
213 } else {
214 let signer = match signer {
215 Some(signer) => signer,
216 None => eth.wallet.signer().await?,
217 };
218 let from = signer.address();
219 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, &signer, force)? {
220 return Ok(());
221 }
222 let (tx, _) = tx_builder.build(&signer).await?;
223 (tx, from)
224 };
225 if let Some(fee_payer) = sponsor_fee_payer {
226 resolve_and_set_fee_token(
227 (!config.eth_rpc_curl).then_some(&provider),
228 Some(chain),
229 &mut tx,
230 Some(fee_payer),
231 )
232 .await?;
233 }
234 let hash = tx.compute_sponsor_hash(from).ok_or_else(|| {
235 eyre::eyre!("This network does not support sponsored transactions")
236 })?;
237 print_scalar(format!("{hash:?}"))?;
238 return Ok(());
239 }
240
241 if let Some(ts) = expires_at {
242 sh_status!("Transaction expires at unix timestamp {ts}")?;
243 }
244
245 if raw_unsigned {
246 if eth.wallet.from.is_none() && tx.nonce.is_none() {
250 eyre::bail!(
251 "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce"
252 );
253 }
254 if tempo_sponsor.is_some() && eth.wallet.from.is_none() {
255 eyre::bail!(
256 "--tempo.sponsor requires --from for --raw-unsigned because the sponsor digest commits to the sender"
257 );
258 }
259
260 let from = eth.wallet.from.unwrap_or(Address::ZERO);
262
263 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, from, force)? {
264 return Ok(());
265 }
266 let (mut tx, _) = tx_builder.build(from).await?;
267 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
268 if let Some(sponsor) = &tempo_sponsor {
269 sponsor
270 .resolve_and_set_fee_token(
271 (!config.eth_rpc_curl).then_some(&provider),
272 Some(chain),
273 &mut tx,
274 )
275 .await?;
276 sponsor.attach_and_print::<N>(&mut tx, from).await?;
277 } else {
278 let fee_token = resolve_and_set_fee_token(
279 (!config.eth_rpc_curl).then_some(&provider),
280 Some(chain),
281 &mut tx,
282 Some(from),
283 )
284 .await?;
285 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
286 .await?;
287 }
288 let raw_tx = hex::encode_prefixed(tx.build_unsigned()?.encoded_for_signing());
289
290 print_scalar(raw_tx)?;
291 return Ok(());
292 }
293
294 if let Some(signature) = signature {
295 let signature = signature.normalized_s();
296 let from = eth.wallet.from.expect("required by clap");
297 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, from, force)? {
298 return Ok(());
299 }
300 let (mut tx, _) = tx_builder.build(from).await?;
301 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
302 if let Some(sponsor) = &tempo_sponsor {
303 sponsor
304 .resolve_and_set_fee_token(
305 (!config.eth_rpc_curl).then_some(&provider),
306 Some(chain),
307 &mut tx,
308 )
309 .await?;
310 sponsor.attach_and_print::<N>(&mut tx, from).await?;
311 } else {
312 let fee_token = resolve_and_set_fee_token(
313 (!config.eth_rpc_curl).then_some(&provider),
314 Some(chain),
315 &mut tx,
316 Some(from),
317 )
318 .await?;
319 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
320 .await?;
321 }
322
323 let tx = tx.build_unsigned()?;
324 let recovered = signature.recover_address_from_prehash(&tx.signature_hash())?;
325 if recovered != from {
326 eyre::bail!(
327 "The provided signature recovers to {recovered}, which does not match the specified sender {from}"
328 );
329 }
330
331 let tx = N::TxEnvelope::from(tx.into_signed(signature));
332 print_scalar(hex::encode_prefixed(tx.encoded_2718()))?;
333 return Ok(());
334 }
335
336 if ethsign {
337 let sender = config.sender.into();
340 if tx_builder.has_auth() && !confirm_auth_rpc_disclosure(&tx_builder, &sender, force)? {
341 return Ok(());
342 }
343 let (mut tx, _) = tx_builder.build(config.sender).await?;
344 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
345 if let Some(sponsor) = &tempo_sponsor {
346 sponsor
347 .resolve_and_set_fee_token(
348 (!config.eth_rpc_curl).then_some(&provider),
349 Some(chain),
350 &mut tx,
351 )
352 .await?;
353 sponsor.attach_and_print::<N>(&mut tx, config.sender).await?;
354 } else {
355 let fee_token = resolve_and_set_fee_token(
356 (!config.eth_rpc_curl).then_some(&provider),
357 Some(chain),
358 &mut tx,
359 Some(config.sender),
360 )
361 .await?;
362 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
363 .await?;
364 }
365 let signed_tx = provider.sign_transaction(tx).await?;
366
367 print_scalar(signed_tx)?;
368 return Ok(());
369 }
370
371 let signed_tx = if let Some(access_key) = access_key {
373 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, access_key.account(), force)?
374 {
375 return Ok(());
376 }
377 let (mut tx, _, prepared) = tx_builder.build_with_tempo_wallet(&access_key).await?;
378 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
379 if let Some(sponsor) = &tempo_sponsor {
380 sponsor
381 .resolve_and_set_fee_token(
382 (!config.eth_rpc_curl).then_some(&provider),
383 Some(chain),
384 &mut tx,
385 )
386 .await?;
387 sponsor.attach_and_print::<N>(&mut tx, prepared.account()).await?;
388 } else {
389 let fee_token = resolve_and_set_fee_token(
390 (!config.eth_rpc_curl).then_some(&provider),
391 Some(chain),
392 &mut tx,
393 Some(prepared.account()),
394 )
395 .await?;
396 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
397 .await?;
398 }
399 tx.sign_with_tempo_wallet(&prepared).await?
400 } else {
401 let signer = match signer {
403 Some(signer) => signer,
404 None => eth.wallet.signer().await?,
405 };
406 let from = signer.address();
407
408 tx::validate_from_address(eth.wallet.from, from)?;
409
410 if !confirm_auth_rpc_disclosure_during_build(&tx_builder, &signer, force)? {
411 return Ok(());
412 }
413 let (mut tx, _) = tx_builder.build(&signer).await?;
414 maybe_print_resolved_lane(resolved_lane.as_ref(), tx.nonce().unwrap_or_default())?;
415 if let Some(sponsor) = &tempo_sponsor {
416 sponsor
417 .resolve_and_set_fee_token(
418 (!config.eth_rpc_curl).then_some(&provider),
419 Some(chain),
420 &mut tx,
421 )
422 .await?;
423 sponsor.attach_and_print::<N>(&mut tx, from).await?;
424 } else {
425 let fee_token = resolve_and_set_fee_token(
426 (!config.eth_rpc_curl).then_some(&provider),
427 Some(chain),
428 &mut tx,
429 Some(from),
430 )
431 .await?;
432 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token)
433 .await?;
434 }
435
436 tx.build(&EthereumWallet::new(signer)).await?.encoded_2718()
437 };
438
439 print_scalar(hex::encode_prefixed(signed_tx))?;
440 Ok(())
441 }
442}