safe_rs/
safe.rs

1//! Safe client and MulticallBuilder implementation
2
3use alloy::network::AnyNetwork;
4use alloy::network::primitives::ReceiptResponse;
5use alloy::primitives::{Address, Bytes, TxHash, U256};
6use alloy::providers::Provider;
7use alloy::signers::local::PrivateKeySigner;
8use alloy::sol_types::SolCall;
9
10use crate::chain::{ChainAddresses, ChainConfig};
11use crate::contracts::{IMultiSend, IMultiSendCallOnly, ISafe};
12use crate::encoding::{compute_safe_transaction_hash, encode_multisend_data, SafeTxParams};
13use crate::error::{Error, Result};
14use crate::signing::sign_hash;
15use crate::simulation::{ForkSimulator, SimulationResult};
16use crate::types::{Call, Operation, SafeCall, TypedCall};
17
18/// Result of executing a Safe transaction
19#[derive(Debug, Clone)]
20pub struct ExecutionResult {
21    /// Transaction hash
22    pub tx_hash: TxHash,
23    /// Whether the Safe transaction succeeded (not just inclusion)
24    pub success: bool,
25}
26
27/// Safe client for interacting with Safe v1.4.1 smart accounts
28pub struct Safe<P> {
29    /// The provider for RPC calls
30    provider: P,
31    /// The signer for transactions
32    signer: PrivateKeySigner,
33    /// The Safe contract address
34    address: Address,
35    /// Chain configuration
36    config: ChainConfig,
37}
38
39impl<P> Safe<P>
40where
41    P: Provider<AnyNetwork> + Clone + 'static,
42{
43    /// Creates a new Safe client
44    pub fn new(provider: P, signer: PrivateKeySigner, address: Address, config: ChainConfig) -> Self {
45        Self {
46            provider,
47            signer,
48            address,
49            config,
50        }
51    }
52
53    /// Creates a Safe client with auto-detected chain configuration
54    pub async fn connect(provider: P, signer: PrivateKeySigner, address: Address) -> Result<Self> {
55        let chain_id = provider
56            .get_chain_id()
57            .await
58            .map_err(|e| Error::Provider(e.to_string()))?;
59
60        let config = ChainConfig::new(chain_id);
61        Ok(Self::new(provider, signer, address, config))
62    }
63
64    /// Returns the Safe address
65    pub fn address(&self) -> Address {
66        self.address
67    }
68
69    /// Returns the chain configuration
70    pub fn config(&self) -> &ChainConfig {
71        &self.config
72    }
73
74    /// Returns the chain addresses
75    pub fn addresses(&self) -> &ChainAddresses {
76        &self.config.addresses
77    }
78
79    /// Returns a reference to the provider
80    pub fn provider(&self) -> &P {
81        &self.provider
82    }
83
84    /// Returns the signer address
85    pub fn signer_address(&self) -> Address {
86        self.signer.address()
87    }
88
89    /// Gets the current nonce of the Safe
90    pub async fn nonce(&self) -> Result<U256> {
91        let safe = ISafe::new(self.address, &self.provider);
92        let nonce = safe
93            .nonce()
94            .call()
95            .await
96            .map_err(|e| Error::Fetch {
97                what: "nonce",
98                reason: e.to_string(),
99            })?;
100        Ok(nonce)
101    }
102
103    /// Gets the threshold of the Safe
104    pub async fn threshold(&self) -> Result<u64> {
105        let safe = ISafe::new(self.address, &self.provider);
106        let threshold = safe
107            .getThreshold()
108            .call()
109            .await
110            .map_err(|e| Error::Fetch {
111                what: "threshold",
112                reason: e.to_string(),
113            })?;
114        Ok(threshold.to::<u64>())
115    }
116
117    /// Gets the owners of the Safe
118    pub async fn owners(&self) -> Result<Vec<Address>> {
119        let safe = ISafe::new(self.address, &self.provider);
120        let owners = safe
121            .getOwners()
122            .call()
123            .await
124            .map_err(|e| Error::Fetch {
125                what: "owners",
126                reason: e.to_string(),
127            })?;
128        Ok(owners)
129    }
130
131    /// Checks if an address is an owner of the Safe
132    pub async fn is_owner(&self, address: Address) -> Result<bool> {
133        let safe = ISafe::new(self.address, &self.provider);
134        let is_owner = safe
135            .isOwner(address)
136            .call()
137            .await
138            .map_err(|e| Error::Fetch {
139                what: "is_owner",
140                reason: e.to_string(),
141            })?;
142        Ok(is_owner)
143    }
144
145    /// Verifies that the signer is an owner and threshold is 1
146    pub async fn verify_single_owner(&self) -> Result<()> {
147        let threshold = self.threshold().await?;
148        if threshold != 1 {
149            return Err(Error::InvalidThreshold { threshold });
150        }
151
152        let is_owner = self.is_owner(self.signer.address()).await?;
153        if !is_owner {
154            return Err(Error::NotOwner {
155                signer: self.signer.address(),
156                safe: self.address,
157            });
158        }
159
160        Ok(())
161    }
162
163    /// Creates a multicall builder
164    pub fn multicall(&self) -> MulticallBuilder<'_, P> {
165        MulticallBuilder::new(self)
166    }
167
168    /// Executes a single call through the Safe
169    pub async fn execute_single(
170        &self,
171        to: Address,
172        value: U256,
173        data: Bytes,
174        operation: Operation,
175    ) -> Result<ExecutionResult> {
176        self.multicall()
177            .add_raw(to, value, data)
178            .with_operation(operation)
179            .simulate()
180            .await?
181            .execute()
182            .await
183    }
184}
185
186/// Builder for constructing multicall transactions
187pub struct MulticallBuilder<'a, P> {
188    safe: &'a Safe<P>,
189    calls: Vec<Call>,
190    use_call_only: bool,
191    safe_tx_gas: Option<U256>,
192    operation: Operation,
193    simulation_result: Option<SimulationResult>,
194}
195
196impl<'a, P> MulticallBuilder<'a, P>
197where
198    P: Provider<AnyNetwork> + Clone + 'static,
199{
200    fn new(safe: &'a Safe<P>) -> Self {
201        MulticallBuilder {
202            safe,
203            calls: Vec::new(),
204            use_call_only: false,
205            safe_tx_gas: None,
206            operation: Operation::DelegateCall, // MultiSend is called via delegatecall
207            simulation_result: None,
208        }
209    }
210    /// Adds a typed call to the batch
211    pub fn add_typed<C: SolCall + Clone>(mut self, to: Address, call: C) -> Self {
212        let typed_call = TypedCall::new(to, call);
213        self.calls.push(Call::new(
214            typed_call.to(),
215            typed_call.value,
216            typed_call.data(),
217        ));
218        self
219    }
220
221    /// Adds a typed call with value to the batch
222    pub fn add_typed_with_value<C: SolCall + Clone>(
223        mut self,
224        to: Address,
225        call: C,
226        value: U256,
227    ) -> Self {
228        let typed_call = TypedCall::new(to, call).with_value(value);
229        self.calls.push(Call::new(
230            typed_call.to(),
231            typed_call.value,
232            typed_call.data(),
233        ));
234        self
235    }
236
237    /// Adds a raw call to the batch
238    pub fn add_raw(mut self, to: Address, value: U256, data: impl Into<Bytes>) -> Self {
239        self.calls.push(Call::new(to, value, data));
240        self
241    }
242
243    /// Adds a call implementing SafeCall to the batch
244    pub fn add(mut self, call: impl SafeCall) -> Self {
245        self.calls.push(Call {
246            to: call.to(),
247            value: call.value(),
248            data: call.data(),
249            operation: call.operation(),
250        });
251        self
252    }
253
254    /// Use MultiSendCallOnly instead of MultiSend (no delegatecall allowed)
255    pub fn call_only(mut self) -> Self {
256        self.use_call_only = true;
257        self
258    }
259
260    /// Sets the operation type for the outer call (usually DelegateCall for MultiSend)
261    pub fn with_operation(mut self, operation: Operation) -> Self {
262        self.operation = operation;
263        self
264    }
265
266    /// Manually sets the safeTxGas instead of auto-estimating
267    pub fn with_safe_tx_gas(mut self, gas: U256) -> Self {
268        self.safe_tx_gas = Some(gas);
269        self
270    }
271
272    /// Simulates the multicall and stores the result
273    ///
274    /// After simulation, you can inspect the results via `simulation_result()`
275    /// and then call `execute()` which will use the simulation gas.
276    pub async fn simulate(mut self) -> Result<Self> {
277        if self.calls.is_empty() {
278            return Err(Error::NoCalls);
279        }
280
281        let (to, value, data, operation) = self.build_call_params()?;
282
283        let simulator = ForkSimulator::new(self.safe.provider.clone(), self.safe.config.chain_id);
284
285        let result = simulator
286            .simulate_call(self.safe.address, to, value, data, operation)
287            .await?;
288
289        if !result.success {
290            return Err(Error::SimulationReverted {
291                reason: result
292                    .revert_reason
293                    .unwrap_or_else(|| "Unknown".to_string()),
294            });
295        }
296
297        self.simulation_result = Some(result);
298        Ok(self)
299    }
300
301    /// Returns the simulation result if simulation was performed
302    pub fn simulation_result(&self) -> Option<&SimulationResult> {
303        self.simulation_result.as_ref()
304    }
305
306    /// Executes the multicall transaction
307    ///
308    /// If simulation was performed, uses the simulated gas + 10% buffer.
309    /// If no simulation, estimates gas via `eth_estimateGas` RPC call.
310    /// If `with_safe_tx_gas()` was called, uses that value instead.
311    pub async fn execute(self) -> Result<ExecutionResult> {
312        if self.calls.is_empty() {
313            return Err(Error::NoCalls);
314        }
315
316        let (to, value, data, operation) = self.build_call_params()?;
317
318        // Get nonce
319        let nonce = self.safe.nonce().await?;
320
321        // Determine safe_tx_gas: explicit > simulation > estimate
322        let safe_tx_gas = match (&self.simulation_result, self.safe_tx_gas) {
323            (_, Some(gas)) => gas, // User provided explicit gas
324            (Some(sim), None) => {
325                // Use simulation result + 10% buffer
326                let gas_used = sim.gas_used;
327                U256::from(gas_used + gas_used / 10)
328            }
329            (None, None) => {
330                // Estimate gas via RPC
331                use alloy::network::TransactionBuilder;
332                let tx_request = <AnyNetwork as alloy::network::Network>::TransactionRequest::default()
333                    .with_from(self.safe.address)
334                    .with_to(to)
335                    .with_value(value)
336                    .with_input(data.clone());
337
338                let estimated = self
339                    .safe
340                    .provider
341                    .estimate_gas(tx_request)
342                    .await
343                    .map_err(|e| Error::Provider(format!("gas estimation failed: {}", e)))?;
344
345                // Add 10% buffer
346                U256::from(estimated + estimated / 10)
347            }
348        };
349
350        // Build SafeTxParams
351        let params = SafeTxParams {
352            to,
353            value,
354            data: data.clone(),
355            operation,
356            safe_tx_gas,
357            base_gas: U256::ZERO,
358            gas_price: U256::ZERO,
359            gas_token: Address::ZERO,
360            refund_receiver: Address::ZERO,
361            nonce,
362        };
363
364        // Compute transaction hash
365        let tx_hash = compute_safe_transaction_hash(
366            self.safe.config.chain_id,
367            self.safe.address,
368            &params,
369        );
370
371        // Sign the hash
372        let signature = sign_hash(&self.safe.signer, tx_hash).await?;
373
374        // Build the execTransaction call
375        let exec_call = ISafe::execTransactionCall {
376            to: params.to,
377            value: params.value,
378            data: params.data,
379            operation: params.operation.as_u8(),
380            safeTxGas: params.safe_tx_gas,
381            baseGas: params.base_gas,
382            gasPrice: params.gas_price,
383            gasToken: params.gas_token,
384            refundReceiver: params.refund_receiver,
385            signatures: signature,
386        };
387
388        // Execute the transaction through the provider
389        let safe_contract = ISafe::new(self.safe.address, &self.safe.provider);
390
391        let builder = safe_contract.execTransaction(
392            exec_call.to,
393            exec_call.value,
394            exec_call.data,
395            exec_call.operation,
396            exec_call.safeTxGas,
397            exec_call.baseGas,
398            exec_call.gasPrice,
399            exec_call.gasToken,
400            exec_call.refundReceiver,
401            exec_call.signatures,
402        );
403
404        let pending_tx = builder
405            .send()
406            .await
407            .map_err(|e| Error::ExecutionFailed {
408                reason: e.to_string(),
409            })?;
410
411        let receipt = pending_tx
412            .get_receipt()
413            .await
414            .map_err(|e| Error::ExecutionFailed {
415                reason: e.to_string(),
416            })?;
417
418        // Check if Safe execution succeeded
419        let success = receipt.status();
420
421        Ok(ExecutionResult {
422            tx_hash: receipt.transaction_hash,
423            success,
424        })
425    }
426
427    fn build_call_params(&self) -> Result<(Address, U256, Bytes, Operation)> {
428        if self.calls.len() == 1 {
429            // Single call - execute directly
430            let call = &self.calls[0];
431            Ok((call.to, call.value, call.data.clone(), Operation::Call))
432        } else {
433            // Multiple calls - use MultiSend
434            let multisend_data = encode_multisend_data(&self.calls);
435
436            let (multisend_address, calldata) = if self.use_call_only {
437                let call = IMultiSendCallOnly::multiSendCall {
438                    transactions: multisend_data,
439                };
440                (
441                    self.safe.addresses().multi_send_call_only,
442                    Bytes::from(call.abi_encode()),
443                )
444            } else {
445                let call = IMultiSend::multiSendCall {
446                    transactions: multisend_data,
447                };
448                (
449                    self.safe.addresses().multi_send,
450                    Bytes::from(call.abi_encode()),
451                )
452            };
453
454            // MultiSend is called with zero value; individual call values are encoded in the data
455            Ok((multisend_address, U256::ZERO, calldata, Operation::DelegateCall))
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    #[allow(unused_imports)]
463    use super::*;
464    use alloy::primitives::address;
465
466    #[test]
467    fn test_call_params_single() {
468        // This would need a mock provider to test fully
469        // For now, just test that types compile correctly
470        let _addr = address!("0x1234567890123456789012345678901234567890");
471    }
472}