Skip to main content

builder_relayer_client_rust/builder/
safe.rs

1use crate::builder::derive::derive_safe;
2use crate::encode::safe::create_safe_multisend_transaction;
3use crate::errors::Result;
4pub use crate::signer::AbstractSigner;
5use crate::types::{
6    SafeTransaction, SafeTransactionArgs, SignatureParams, TransactionRequest, TransactionType,
7};
8use crate::utils::split_and_pack_sig;
9use alloy::primitives::{Address, Bytes, U256, keccak256};
10use alloy::sol;
11use alloy::sol_types::SolStruct;
12use std::str::FromStr;
13
14#[derive(Clone, Debug)]
15pub struct SafeContractConfig {
16    pub safe_factory: String,
17    pub safe_multisend: String,
18}
19
20// Define EIP-712 structs using alloy::sol!
21sol! {
22    #[derive(Debug)]
23    struct SafeTx {
24        address to;
25        uint256 value;
26        bytes data;
27        uint8 operation;
28        uint256 safeTxGas;
29        uint256 baseGas;
30        uint256 gasPrice;
31        address gasToken;
32        address refundReceiver;
33        uint256 nonce;
34    }
35
36    #[derive(Debug)]
37    struct EIP712Domain {
38        uint256 chainId;
39        address verifyingContract;
40    }
41}
42
43pub fn eip712_domain_separator(chain_id: U256, verifying_contract: Address) -> [u8; 32] {
44    let domain = EIP712Domain {
45        chainId: chain_id,
46        verifyingContract: verifying_contract,
47    };
48    domain.eip712_hash_struct().into()
49}
50
51pub fn safe_tx_struct_hash(
52    to: Address,
53    value: U256,
54    data: &[u8],
55    operation: u8,
56    safe_tx_gas: U256,
57    base_gas: U256,
58    gas_price: U256,
59    gas_token: Address,
60    refund_receiver: Address,
61    nonce: U256,
62) -> [u8; 32] {
63    let tx = SafeTx {
64        to,
65        value,
66        data: Bytes::from(data.to_vec()),
67        operation,
68        safeTxGas: safe_tx_gas,
69        baseGas: base_gas,
70        gasPrice: gas_price,
71        gasToken: gas_token,
72        refundReceiver: refund_receiver,
73        nonce,
74    };
75    tx.eip712_hash_struct().into()
76}
77
78fn aggregate_transaction(txns: &[SafeTransaction], safe_multisend: &str) -> SafeTransaction {
79    if txns.len() == 1 {
80        txns[0].clone()
81    } else {
82        create_safe_multisend_transaction(txns, safe_multisend)
83    }
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum SignatureMode {
88    /// EIP-191 over structHash (ethers.js signMessage on 32-byte struct hash)
89    Eip191StructHash,
90    /// Directly sign the EIP-712 digest (0x1901||domainSeparator||structHash)
91    Eip712Digest,
92    Eip191Digest,
93}
94
95pub async fn build_safe_transaction_request(
96    signer: &dyn AbstractSigner,
97    args: &SafeTransactionArgs,
98    config: &SafeContractConfig,
99    metadata: Option<String>,
100    mode: SignatureMode,
101) -> Result<TransactionRequest> {
102    let safe_address = if let Some(addr) = &args.safe_address {
103        Address::from_str(addr).map_err(|_| crate::errors::RelayClientError::InvalidAddress)?
104    } else {
105        let owner = Address::from_str(&args.from)
106            .map_err(|_| crate::errors::RelayClientError::InvalidAddress)?;
107        let factory = Address::from_str(&config.safe_factory)
108            .map_err(|_| crate::errors::RelayClientError::InvalidAddress)?;
109        derive_safe(factory, owner)?
110    };
111
112    let tx = aggregate_transaction(&args.transactions, &config.safe_multisend);
113
114    let to =
115        Address::from_str(&tx.to).map_err(|_| crate::errors::RelayClientError::InvalidAddress)?;
116    let value = U256::from_str(&tx.value).unwrap_or(U256::ZERO);
117    let data = hex::decode(tx.data.trim_start_matches("0x")).unwrap_or_default();
118    let operation = tx.operation as u8;
119
120    // TS client sends zeroed gas fields by default; mirror that exactly.
121    let safe_tx_gas = U256::ZERO;
122    let base_gas = U256::ZERO;
123    let gas_price = U256::ZERO;
124    let gas_token = Address::ZERO;
125    let refund_receiver = Address::ZERO;
126
127    let nonce = U256::from_str(&args.nonce).unwrap_or(U256::ZERO);
128
129    let struct_hash = safe_tx_struct_hash(
130        to,
131        value,
132        &data,
133        operation,
134        safe_tx_gas,
135        base_gas,
136        gas_price,
137        gas_token,
138        refund_receiver,
139        nonce,
140    );
141
142    let chain_id = U256::from(args.chain_id);
143    let domain_separator = eip712_domain_separator(chain_id, safe_address);
144
145    // digest = keccak256("\x19\x01" || domainSeparator || structHash)
146    let mut digest_input = Vec::with_capacity(2 + 32 + 32);
147    digest_input.extend_from_slice(&[0x19, 0x01]);
148    digest_input.extend_from_slice(&domain_separator);
149    digest_input.extend_from_slice(&struct_hash);
150    let digest = keccak256(&digest_input);
151
152    let signature = match mode {
153        SignatureMode::Eip191StructHash => {
154            let mut msg = Vec::with_capacity(60);
155            msg.extend_from_slice(b"\x19Ethereum Signed Message:\n32");
156            msg.extend_from_slice(&struct_hash);
157            let hash = keccak256(&msg);
158
159            signer.sign_hash(hash).await?
160        }
161        SignatureMode::Eip712Digest => signer.sign_eip712_digest(digest).await?,
162        SignatureMode::Eip191Digest => {
163            // EIP-191 over the EIP-712 digest (TS signMessage(hashTypedData(...)))
164            let mut msg = Vec::with_capacity(60);
165            msg.extend_from_slice(b"\x19Ethereum Signed Message:\n32");
166            msg.extend_from_slice(digest.as_slice());
167            let hash = keccak256(&msg);
168            signer.sign_hash(hash).await?
169        }
170    };
171
172    // Convert signature to hex string for split_and_pack_sig
173    let sig_hex = format!("0x{}", hex::encode(signature.as_bytes()));
174    let packed_sig = split_and_pack_sig(&sig_hex);
175
176    let signature_params = SignatureParams {
177        gas_price: Some(gas_price.to_string()),
178        relayer_fee: None,
179        gas_limit: None,
180        relay_hub: None,
181        relay: None,
182        operation: Some(operation.to_string()),
183        safe_txn_gas: Some(safe_tx_gas.to_string()),
184        base_gas: Some(base_gas.to_string()),
185        gas_token: Some(format!("{:#x}", gas_token)),
186        refund_receiver: Some(format!("{:#x}", refund_receiver)),
187        payment_token: None,
188        payment: None,
189        payment_receiver: None,
190    };
191
192    Ok(TransactionRequest {
193        r#type: TransactionType::SAFE,
194        from: args.from.clone(),
195        to: tx.to.clone(),
196        proxy_wallet: Some(format!("{:#x}", safe_address)),
197        data: tx.data.clone(),
198        nonce: Some(args.nonce.clone()),
199        signature: packed_sig,
200        signature_params,
201        metadata,
202    })
203}