Skip to main content

cast/cmd/safe/
transaction.rs

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