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        // For DelegateCall operations (like MultiSend), we need to simulate through
286        // Safe's execTransaction because the target contract expects delegatecall context.
287        // For regular Call operations, we can simulate the inner call directly.
288        let result = match operation {
289            Operation::DelegateCall => {
290                // Simulate through Safe.execTransaction
291                self.simulate_via_exec_transaction(&simulator, to, value, data, operation)
292                    .await?
293            }
294            Operation::Call => {
295                simulator
296                    .simulate_call(self.safe.address, to, value, data, operation)
297                    .await?
298            }
299        };
300
301        if !result.success {
302            return Err(Error::SimulationReverted {
303                reason: result
304                    .revert_reason
305                    .unwrap_or_else(|| "Unknown".to_string()),
306            });
307        }
308
309        self.simulation_result = Some(result);
310        Ok(self)
311    }
312
313    /// Simulates by calling Safe.execTransaction
314    ///
315    /// This is needed for DelegateCall operations because the target contract
316    /// (like MultiSend) expects to be called via delegatecall.
317    async fn simulate_via_exec_transaction(
318        &self,
319        simulator: &ForkSimulator<P>,
320        to: Address,
321        value: U256,
322        data: Bytes,
323        operation: Operation,
324    ) -> Result<SimulationResult> {
325        // Get nonce
326        let nonce = self.safe.nonce().await?;
327
328        // Use a high gas estimate for simulation - we'll refine it after
329        let safe_tx_gas = U256::from(10_000_000);
330
331        // Build SafeTxParams
332        let params = SafeTxParams {
333            to,
334            value,
335            data: data.clone(),
336            operation,
337            safe_tx_gas,
338            base_gas: U256::ZERO,
339            gas_price: U256::ZERO,
340            gas_token: Address::ZERO,
341            refund_receiver: Address::ZERO,
342            nonce,
343        };
344
345        // Compute transaction hash
346        let tx_hash = compute_safe_transaction_hash(
347            self.safe.config.chain_id,
348            self.safe.address,
349            &params,
350        );
351
352        // Sign the hash
353        let signature = sign_hash(&self.safe.signer, tx_hash).await?;
354
355        // Build the execTransaction call
356        let exec_call = ISafe::execTransactionCall {
357            to: params.to,
358            value: params.value,
359            data: params.data,
360            operation: params.operation.as_u8(),
361            safeTxGas: params.safe_tx_gas,
362            baseGas: params.base_gas,
363            gasPrice: params.gas_price,
364            gasToken: params.gas_token,
365            refundReceiver: params.refund_receiver,
366            signatures: signature,
367        };
368
369        let exec_data = Bytes::from(exec_call.abi_encode());
370
371        // Simulate the execTransaction call
372        simulator
373            .simulate_call(
374                self.safe.signer.address(), // EOA calls Safe
375                self.safe.address,           // Safe address
376                U256::ZERO,                  // No ETH value for outer call
377                exec_data,
378                Operation::Call,             // Regular call to Safe
379            )
380            .await
381    }
382
383    /// Returns the simulation result if simulation was performed
384    pub fn simulation_result(&self) -> Option<&SimulationResult> {
385        self.simulation_result.as_ref()
386    }
387
388    /// Executes the multicall transaction
389    ///
390    /// If simulation was performed, uses the simulated gas + 10% buffer.
391    /// If no simulation, estimates gas via `eth_estimateGas` RPC call.
392    /// If `with_safe_tx_gas()` was called, uses that value instead.
393    pub async fn execute(self) -> Result<ExecutionResult> {
394        if self.calls.is_empty() {
395            return Err(Error::NoCalls);
396        }
397
398        let (to, value, data, operation) = self.build_call_params()?;
399
400        // Get nonce
401        let nonce = self.safe.nonce().await?;
402
403        // Determine safe_tx_gas: explicit > simulation > estimate
404        let safe_tx_gas = match (&self.simulation_result, self.safe_tx_gas) {
405            (_, Some(gas)) => gas, // User provided explicit gas
406            (Some(sim), None) => {
407                // Use simulation result + 10% buffer
408                let gas_used = sim.gas_used;
409                U256::from(gas_used + gas_used / 10)
410            }
411            (None, None) => {
412                // Estimate gas via RPC
413                use alloy::network::TransactionBuilder;
414                let tx_request = <AnyNetwork as alloy::network::Network>::TransactionRequest::default()
415                    .with_from(self.safe.address)
416                    .with_to(to)
417                    .with_value(value)
418                    .with_input(data.clone());
419
420                let estimated = self
421                    .safe
422                    .provider
423                    .estimate_gas(tx_request)
424                    .await
425                    .map_err(|e| Error::Provider(format!("gas estimation failed: {}", e)))?;
426
427                // Add 10% buffer
428                U256::from(estimated + estimated / 10)
429            }
430        };
431
432        // Build SafeTxParams
433        let params = SafeTxParams {
434            to,
435            value,
436            data: data.clone(),
437            operation,
438            safe_tx_gas,
439            base_gas: U256::ZERO,
440            gas_price: U256::ZERO,
441            gas_token: Address::ZERO,
442            refund_receiver: Address::ZERO,
443            nonce,
444        };
445
446        // Compute transaction hash
447        let tx_hash = compute_safe_transaction_hash(
448            self.safe.config.chain_id,
449            self.safe.address,
450            &params,
451        );
452
453        // Sign the hash
454        let signature = sign_hash(&self.safe.signer, tx_hash).await?;
455
456        // Build the execTransaction call
457        let exec_call = ISafe::execTransactionCall {
458            to: params.to,
459            value: params.value,
460            data: params.data,
461            operation: params.operation.as_u8(),
462            safeTxGas: params.safe_tx_gas,
463            baseGas: params.base_gas,
464            gasPrice: params.gas_price,
465            gasToken: params.gas_token,
466            refundReceiver: params.refund_receiver,
467            signatures: signature,
468        };
469
470        // Execute the transaction through the provider
471        let safe_contract = ISafe::new(self.safe.address, &self.safe.provider);
472
473        let builder = safe_contract.execTransaction(
474            exec_call.to,
475            exec_call.value,
476            exec_call.data,
477            exec_call.operation,
478            exec_call.safeTxGas,
479            exec_call.baseGas,
480            exec_call.gasPrice,
481            exec_call.gasToken,
482            exec_call.refundReceiver,
483            exec_call.signatures,
484        );
485
486        let pending_tx = builder
487            .send()
488            .await
489            .map_err(|e| Error::ExecutionFailed {
490                reason: e.to_string(),
491            })?;
492
493        let receipt = pending_tx
494            .get_receipt()
495            .await
496            .map_err(|e| Error::ExecutionFailed {
497                reason: e.to_string(),
498            })?;
499
500        // Check if Safe execution succeeded
501        let success = receipt.status();
502
503        Ok(ExecutionResult {
504            tx_hash: receipt.transaction_hash,
505            success,
506        })
507    }
508
509    fn build_call_params(&self) -> Result<(Address, U256, Bytes, Operation)> {
510        if self.calls.len() == 1 {
511            // Single call - execute directly
512            let call = &self.calls[0];
513            Ok((call.to, call.value, call.data.clone(), Operation::Call))
514        } else {
515            // Multiple calls - use MultiSend
516            let multisend_data = encode_multisend_data(&self.calls);
517
518            let (multisend_address, calldata) = if self.use_call_only {
519                let call = IMultiSendCallOnly::multiSendCall {
520                    transactions: multisend_data,
521                };
522                (
523                    self.safe.addresses().multi_send_call_only,
524                    Bytes::from(call.abi_encode()),
525                )
526            } else {
527                let call = IMultiSend::multiSendCall {
528                    transactions: multisend_data,
529                };
530                (
531                    self.safe.addresses().multi_send,
532                    Bytes::from(call.abi_encode()),
533                )
534            };
535
536            // MultiSend is called with zero value; individual call values are encoded in the data
537            Ok((multisend_address, U256::ZERO, calldata, Operation::DelegateCall))
538        }
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    #[allow(unused_imports)]
545    use super::*;
546    use alloy::primitives::address;
547
548    #[test]
549    fn test_call_params_single() {
550        // This would need a mock provider to test fully
551        // For now, just test that types compile correctly
552        let _addr = address!("0x1234567890123456789012345678901234567890");
553    }
554}