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;
4use crate::types::{
5    SafeTransaction, SafeTransactionArgs, SignatureParams, TransactionRequest, TransactionType,
6};
7use crate::utils::split_and_pack_sig;
8use ethers::abi::{encode, Token};
9use ethers::types::{Address, U256};
10use sha3::{Digest, Keccak256};
11
12#[derive(Clone, Debug)]
13pub struct SafeContractConfig {
14    pub safe_factory: String,
15    pub safe_multisend: String,
16}
17
18// Type strings (align with TS implementation & Gnosis Safe spec)
19const SAFE_TX_TYPE_STR: &str = "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)";
20const DOMAIN_TYPE_STR: &str = "EIP712Domain(uint256 chainId,address verifyingContract)";
21
22fn keccak_bytes(data: &[u8]) -> [u8; 32] {
23    let mut h = Keccak256::new();
24    h.update(data);
25    let out = h.finalize();
26    let mut arr = [0u8; 32];
27    arr.copy_from_slice(&out);
28    arr
29}
30
31fn safe_tx_type_hash() -> [u8; 32] {
32    keccak_bytes(SAFE_TX_TYPE_STR.as_bytes())
33}
34fn domain_type_hash() -> [u8; 32] {
35    keccak_bytes(DOMAIN_TYPE_STR.as_bytes())
36}
37
38fn keccak(data: &[u8]) -> [u8; 32] {
39    keccak_bytes(data)
40}
41
42pub fn eip712_domain_separator(chain_id: U256, verifying_contract: Address) -> [u8; 32] {
43    // abi.encode(typeHash, chainId, verifyingContract)
44    let encoded = encode(&[
45        Token::FixedBytes(domain_type_hash().to_vec()),
46        Token::Uint(chain_id),
47        Token::Address(verifying_contract),
48    ]);
49    keccak(&encoded)
50}
51
52pub fn safe_tx_struct_hash(
53    to: Address,
54    value: U256,
55    data: &[u8],
56    operation: u8,
57    safe_tx_gas: U256,
58    base_gas: U256,
59    gas_price: U256,
60    gas_token: Address,
61    refund_receiver: Address,
62    nonce: U256,
63) -> [u8; 32] {
64    let data_hash = keccak(data);
65    let encoded = encode(&[
66        Token::FixedBytes(safe_tx_type_hash().to_vec()),
67        Token::Address(to),
68        Token::Uint(value),
69        Token::FixedBytes(data_hash.to_vec()),
70        Token::Uint(U256::from(operation)),
71        Token::Uint(safe_tx_gas),
72        Token::Uint(base_gas),
73        Token::Uint(gas_price),
74        Token::Address(gas_token),
75        Token::Address(refund_receiver),
76        Token::Uint(nonce),
77    ]);
78    keccak(&encoded)
79}
80
81fn aggregate_transaction(txns: &[SafeTransaction], safe_multisend: &str) -> SafeTransaction {
82    if txns.len() == 1 {
83        txns[0].clone()
84    } else {
85        create_safe_multisend_transaction(txns, safe_multisend)
86    }
87}
88
89pub trait AbstractSigner: Send + Sync {
90    fn get_address(&self) -> Result<String>;
91    fn sign_message(&self, hash32_hex: &str) -> Result<String>; // legacy message signing
92    fn sign_eip712_digest(&self, digest_hex: &str) -> Result<String>; // explicit typed data digest signing
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum SignatureMode {
97    /// EIP-191 over structHash (ethers.js signMessage on 32-byte struct hash)
98    Eip191StructHash,
99    /// Directly sign the EIP-712 digest (0x1901||domainSeparator||structHash)
100    Eip712Digest,
101    /// EIP-191 over the EIP-712 digest (ethers.js signMessage on digest returned by hashTypedData)
102    Eip191Digest,
103}
104
105pub async fn build_safe_transaction_request(
106    signer: &dyn AbstractSigner,
107    args: SafeTransactionArgs,
108    safe_contract_config: SafeContractConfig,
109    metadata: Option<String>,
110    sig_mode: SignatureMode,
111) -> Result<TransactionRequest> {
112    let safe_factory = &safe_contract_config.safe_factory;
113    let safe_multisend = &safe_contract_config.safe_multisend;
114    let transaction = aggregate_transaction(&args.transactions, safe_multisend);
115    let safe_txn_gas = U256::zero();
116    let base_gas = U256::zero();
117    let gas_price = U256::zero();
118    let gas_token: Address = "0x0000000000000000000000000000000000000000"
119        .parse()
120        .unwrap();
121    let refund_receiver: Address = "0x0000000000000000000000000000000000000000"
122        .parse()
123        .unwrap();
124
125    let safe_address = args
126        .safe_address
127        .clone()
128        .unwrap_or_else(|| derive_safe(&args.from, safe_factory));
129
130    eprintln!("[DEBUG] args.safe_address input: {:?}", args.safe_address);
131    eprintln!("[DEBUG] Final safe_address used: {}", safe_address);
132
133    let to_addr: Address = transaction
134        .to
135        .parse()
136        .expect("invalid address in transaction.to");
137    let value = U256::from_dec_str(&transaction.value).unwrap_or_default();
138    let data_bytes = hex::decode(transaction.data.trim_start_matches("0x")).unwrap_or_default();
139    let nonce = U256::from_dec_str(&args.nonce).unwrap_or_default();
140    let struct_hash = safe_tx_struct_hash(
141        to_addr,
142        value,
143        &data_bytes,
144        transaction.operation as u8,
145        safe_txn_gas,
146        base_gas,
147        gas_price,
148        gas_token,
149        refund_receiver,
150        nonce,
151    );
152    let domain_separator =
153        eip712_domain_separator(U256::from(args.chain_id), safe_address.parse().unwrap());
154    let mut prefix = vec![0x19, 0x01];
155    prefix.extend_from_slice(&domain_separator);
156    prefix.extend_from_slice(&struct_hash);
157    let digest = keccak(&prefix);
158
159    eprintln!("[DEBUG] Safe address: {}", safe_address);
160    eprintln!(
161        "[DEBUG] Domain separator: 0x{}",
162        hex::encode(domain_separator)
163    );
164    eprintln!("[DEBUG] Struct hash: 0x{}", hex::encode(struct_hash));
165    eprintln!(
166        "[DEBUG] Digest (0x1901||domain||struct): 0x{}",
167        hex::encode(digest)
168    );
169
170    // Signature selection based on mode
171    let sig = match sig_mode {
172        SignatureMode::Eip191StructHash => {
173            eprintln!("[DEBUG] SignatureMode=Eip191StructHash (signMessage(structHash))");
174            signer.sign_message(&format!("0x{}", hex::encode(struct_hash)))?
175        }
176        SignatureMode::Eip712Digest => {
177            eprintln!("[DEBUG] SignatureMode=Eip712Digest (sign_eip712_digest(digest))");
178            signer.sign_eip712_digest(&format!("0x{}", hex::encode(digest)))?
179        }
180        SignatureMode::Eip191Digest => {
181            eprintln!("[DEBUG] SignatureMode=Eip191Digest (signMessage(digest))");
182            signer.sign_message(&format!("0x{}", hex::encode(digest)))?
183        }
184    };
185    eprintln!("[DEBUG] Raw signature before pack: {}", sig);
186    let packed_sig = split_and_pack_sig(&sig);
187    eprintln!("[DEBUG] Packed signature: {}", packed_sig);
188
189    // Verify signature recovers to correct address using the digest as the
190    // signed message (this mirrors the sign_eip712_digest path used above).
191    let signer_addr = signer.get_address()?;
192    eprintln!("[DEBUG] Expected signer address: {}", signer_addr);
193
194    use ethers::types::Signature as EthSig;
195    if let Ok(sig_parsed) = packed_sig.parse::<EthSig>() {
196        let verify_hash = match sig_mode {
197            SignatureMode::Eip191StructHash => {
198                let mut msg = b"\x19Ethereum Signed Message:\n32".to_vec();
199                msg.extend_from_slice(&struct_hash);
200                keccak(&msg)
201            }
202            SignatureMode::Eip712Digest => digest,
203            SignatureMode::Eip191Digest => {
204                let mut msg = b"\x19Ethereum Signed Message:\n32".to_vec();
205                msg.extend_from_slice(&digest);
206                keccak(&msg)
207            }
208        };
209        if let Ok(recovered) = sig_parsed.recover(verify_hash) {
210            eprintln!("[DEBUG] Recovered address: 0x{:x}", recovered);
211            if format!("0x{:x}", recovered).to_lowercase() != signer_addr.to_lowercase() {
212                eprintln!("[WARNING] Signature does not recover to signer address!");
213            } else {
214                eprintln!("[DEBUG] Signature recovery VERIFIED ✓");
215            }
216        }
217    }
218
219    let sig_params = SignatureParams {
220        gas_price: Some(gas_price.to_string()),
221        operation: Some((transaction.operation as u8).to_string()),
222        safe_txn_gas: Some(safe_txn_gas.to_string()),
223        base_gas: Some(base_gas.to_string()),
224        gas_token: Some(format!("0x{:x}", gas_token)),
225        refund_receiver: Some(format!("0x{:x}", refund_receiver)),
226        ..Default::default()
227    };
228
229    Ok(TransactionRequest {
230        from: args.from.clone(),
231        to: transaction.to.clone(),
232        proxy_wallet: Some(safe_address),
233        data: transaction.data.clone(),
234        nonce: Some(args.nonce.clone()),
235        signature: packed_sig,
236        signature_params: sig_params,
237        r#type: TransactionType::SAFE,
238        metadata,
239    })
240}