cast/cmd/safe/
transaction.rs1use 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#[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 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, ð).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}