Skip to main content

cast/cmd/safe/
transaction.rs

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, &eth).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}