1use crate::tempo;
2use alloy_consensus::{SignableTransaction, Signed};
3use alloy_network::{Ethereum, EthereumWallet, Network, ReceiptResponse};
4use alloy_primitives::{Address, B256, Bytes, hex};
5use alloy_provider::{Provider, fillers::RecommendedFillers};
6use alloy_rpc_types::Log;
7use alloy_signer::{Signature, Signer};
8use eyre::{Context, Result, ensure};
9use foundry_cli::{
10 opts::{EthereumOpts, RpcOpts, TransactionOpts},
11 utils::{LoadConfig, resolve_lane},
12};
13use foundry_common::{
14 FoundryTransactionBuilder,
15 provider::ProviderBuilder,
16 tempo::{maybe_print_fee_token, resolve_and_set_fee_token},
17};
18use foundry_wallets::WalletOpts;
19use serde::Serialize;
20use std::time::Duration;
21use tempo_alloy::TempoNetwork;
22
23use crate::tx::CastTxBuilder;
24
25pub(super) struct SafeSendResult {
26 pub(super) tx_hash: B256,
27 pub(super) logs: Vec<Log>,
28}
29
30#[allow(clippy::too_many_arguments)]
31pub(super) async fn send_safe_call(
32 to: Address,
33 data: Bytes,
34 confirmations: u64,
35 timeout: Option<u64>,
36 poll_interval: Option<u64>,
37 rpc: RpcOpts,
38 wallet: WalletOpts,
39 tx: TransactionOpts,
40) -> Result<SafeSendResult> {
41 let eth = EthereumOpts { rpc, wallet, ..Default::default() };
42 let (is_tempo, signer, access_key) =
43 tempo::resolve_transaction_network_and_signer(&tx.tempo, ð).await?;
44 ensure!(
45 access_key.is_none(),
46 "Tempo Accounts sessions are not yet supported by `cast safe create` or `cast safe execute`"
47 );
48 if is_tempo {
49 send_safe_call_generic::<TempoNetwork>(
50 to,
51 data,
52 confirmations,
53 timeout,
54 poll_interval,
55 eth.rpc,
56 eth.wallet,
57 tx,
58 signer,
59 )
60 .await
61 } else {
62 send_safe_call_generic::<Ethereum>(
63 to,
64 data,
65 confirmations,
66 timeout,
67 poll_interval,
68 eth.rpc,
69 eth.wallet,
70 tx,
71 signer,
72 )
73 .await
74 }
75}
76
77#[allow(clippy::too_many_arguments)]
78async fn send_safe_call_generic<N>(
79 to: Address,
80 data: Bytes,
81 confirmations: u64,
82 timeout: Option<u64>,
83 poll_interval: Option<u64>,
84 rpc: RpcOpts,
85 wallet_opts: WalletOpts,
86 mut tx_opts: TransactionOpts,
87 signer: Option<foundry_wallets::WalletSigner>,
88) -> Result<SafeSendResult>
89where
90 N: Network + RecommendedFillers,
91 N::TxEnvelope: From<Signed<N::UnsignedTx>>,
92 N::UnsignedTx: SignableTransaction<Signature>,
93 N::TransactionRequest: Default + FoundryTransactionBuilder<N>,
94 N::ReceiptResponse: Serialize,
95{
96 ensure!(
97 tx_opts.value.is_none_or(|value| value.is_zero()),
98 "Safe outer transaction value must be zero"
99 );
100 ensure!(!tx_opts.blob, "blob transactions are not supported by `cast safe`");
101 ensure!(tx_opts.auth.is_empty(), "EIP-7702 authorizations are not supported by `cast safe`");
102 ensure!(
103 !tx_opts.tempo.has_sponsor_submission()
104 && tx_opts.tempo.sponsor_url.is_none()
105 && !tx_opts.tempo.print_sponsor_hash,
106 "Tempo sponsorship is not yet supported by `cast safe create` or `cast safe execute`"
107 );
108 ensure!(
109 tx_opts.tempo.session_id()?.is_none(),
110 "Tempo Accounts sessions are not yet supported by `cast safe create` or `cast safe execute`"
111 );
112
113 let config = rpc.load_config()?;
114 let timeout = timeout.unwrap_or(config.transaction_timeout);
115 resolve_lane(&mut tx_opts.tempo, &config.root)?;
116 tempo::print_expires(tx_opts.tempo.resolve_expires())?;
117 let signer = match signer {
118 Some(signer) => signer,
119 None => wallet_opts.signer().await?,
120 };
121 crate::tx::validate_from_address(wallet_opts.from, signer.address())?;
122 let from = signer.address();
123 let wallet = EthereumWallet::from(signer);
124 let provider = ProviderBuilder::<N>::from_config(&config)?.build_with_wallet(wallet)?;
125 if let Some(interval) = poll_interval {
126 provider.client().set_poll_interval(Duration::from_secs(interval));
127 }
128 let builder = CastTxBuilder::new(&provider, tx_opts, &config)
129 .await?
130 .with_to(Some(to.into()))
131 .await?
132 .with_code_sig_and_args(None, Some(hex::encode_prefixed(data)), Vec::new())
133 .await?;
134 let chain = builder.chain();
135 let (mut request, _) = builder.build(from).await?;
136 let fee_token = resolve_and_set_fee_token(
137 (!config.eth_rpc_curl).then_some(&provider),
138 Some(chain),
139 &mut request,
140 Some(from),
141 )
142 .await?;
143 maybe_print_fee_token((!config.eth_rpc_curl).then_some(&provider), fee_token).await?;
144
145 let receipt = provider
146 .send_transaction(request)
147 .await?
148 .with_required_confirmations(confirmations)
149 .with_timeout(Some(Duration::from_secs(timeout)))
150 .get_receipt()
151 .await?;
152 ensure!(receipt.status(), "Safe transaction reverted");
153 let tx_hash = receipt.transaction_hash();
154 let receipt = serde_json::to_value(receipt)?;
155 let logs = serde_json::from_value(receipt["logs"].clone())
156 .wrap_err("invalid logs in transaction receipt")?;
157 Ok(SafeSendResult { tx_hash, logs })
158}