builder_relayer_client_rust/
signer.rs

1use ethers::signers::{LocalWallet, Signer};
2use ethers::types::{Signature, H256};
3use sha3::{Digest, Keccak256};
4
5use crate as builder_relayer_client_rust;
6use crate::errors::{RelayClientError, Result};
7
8/// Trait mirroring the expected abstract signer behavior in TS version.
9pub trait AbstractSigner {
10    fn address(&self) -> ethers::types::Address;
11    fn sign_hash(&self, hash: H256) -> std::result::Result<Signature, RelayClientError>;
12    fn sign_eip712_digest(&self, digest: H256) -> std::result::Result<Signature, RelayClientError>;
13}
14
15/// Trait for EIP-712 typed data signing (create proxy specific typed data). Placeholder for future struct generation.
16pub trait TypedDataSigner {
17    fn sign_typed_create_proxy(
18        &self,
19        safe_factory: &str,
20        chain_id: u64,
21        payment_token: &str,
22        payment: &str,
23        payment_receiver: &str,
24    ) -> std::result::Result<String, RelayClientError>;
25}
26
27/// DummySigner: simple local wallet wrapper from a provided private key (hex, no 0x needed or with 0x).
28#[derive(Clone)]
29pub struct DummySigner {
30    wallet: LocalWallet,
31}
32
33impl DummySigner {
34    pub fn new(priv_key_hex: &str) -> std::result::Result<Self, RelayClientError> {
35        let prefixed = if priv_key_hex.starts_with("0x") {
36            priv_key_hex.to_string()
37        } else {
38            format!("0x{}", priv_key_hex)
39        };
40        let wallet: LocalWallet = prefixed
41            .parse()
42            .map_err(|_| RelayClientError::SignerUnavailable)?;
43        Ok(Self { wallet })
44    }
45}
46
47impl AbstractSigner for DummySigner {
48    fn address(&self) -> ethers::types::Address {
49        self.wallet.address()
50    }
51    fn sign_hash(&self, hash: H256) -> std::result::Result<Signature, RelayClientError> {
52        let sig = self
53            .wallet
54            .sign_hash(hash)
55            .map_err(|_| RelayClientError::SignerUnavailable)?;
56        Ok(sig)
57    }
58    fn sign_eip712_digest(&self, digest: H256) -> std::result::Result<Signature, RelayClientError> {
59        // For RelayClient (builder/relayer), sign the EIP-712 digest directly
60        // WITHOUT adding Ethereum message prefix
61        // This is different from on-chain Safe execution which uses signMessage
62        use ethers::core::k256::ecdsa::SigningKey;
63
64        let key_bytes = &self.wallet.signer().to_bytes();
65        let signing_key =
66            SigningKey::from_slice(key_bytes).map_err(|_| RelayClientError::SignerUnavailable)?;
67        let (sig, recovery_id) = signing_key
68            .sign_prehash_recoverable(digest.as_bytes())
69            .map_err(|_| RelayClientError::SignerUnavailable)?;
70
71        let r_bytes = sig.r().to_bytes();
72        let s_bytes = sig.s().to_bytes();
73        Ok(Signature {
74            r: ethers::types::U256::from_big_endian(r_bytes.as_ref()),
75            s: ethers::types::U256::from_big_endian(s_bytes.as_ref()),
76            v: recovery_id.to_byte() as u64,
77        })
78    }
79}
80
81impl TypedDataSigner for DummySigner {
82    fn sign_typed_create_proxy(
83        &self,
84        _safe_factory: &str,
85        _chain_id: u64,
86        _payment_token: &str,
87        _payment: &str,
88        _payment_receiver: &str,
89    ) -> std::result::Result<String, RelayClientError> {
90        // Placeholder: sign zero hash for now; to be replaced with full typed data encoding.
91        let zero = H256::zero();
92        let sig = self.sign_hash(zero)?;
93        Ok(sig_to_hex(sig))
94    }
95}
96
97// Implement the builder module traits for convenience so examples can pass DummySigner directly
98impl builder_relayer_client_rust::builder::safe::AbstractSigner for DummySigner {
99    fn get_address(&self) -> Result<String> {
100        Ok(format!("0x{:x}", self.address()))
101    }
102    fn sign_message(&self, hash32_hex: &str) -> Result<String> {
103        // Sign using Ethereum Signed Message prefix (EIP-191) over the 32-byte input
104        let bytes = hex::decode(hash32_hex.trim_start_matches("0x"))
105            .map_err(|_| RelayClientError::SignerUnavailable)?;
106        let mut arr = [0u8; 32];
107        arr.copy_from_slice(&bytes);
108        // Compute keccak("\x19Ethereum Signed Message:\n32" || hash32)
109        let mut msg = b"\x19Ethereum Signed Message:\n32".to_vec();
110        msg.extend_from_slice(&arr);
111        let mut hasher = Keccak256::new();
112        hasher.update(&msg);
113        let digest = hasher.finalize();
114        let mut dig = [0u8; 32];
115        dig.copy_from_slice(&digest);
116        let sig = self
117            .sign_hash(H256::from(dig))
118            .map_err(|_| RelayClientError::SignerUnavailable)?;
119        Ok(sig_to_hex(sig))
120    }
121    fn sign_eip712_digest(&self, digest_hex: &str) -> Result<String> {
122        // For EIP-712 digest, use the trait method that signs without prefix
123        let bytes = hex::decode(digest_hex.trim_start_matches("0x"))
124            .map_err(|_| RelayClientError::SignerUnavailable)?;
125        let mut arr = [0u8; 32];
126        arr.copy_from_slice(&bytes);
127        let sig = AbstractSigner::sign_eip712_digest(self, H256::from(arr))
128            .map_err(|_| RelayClientError::SignerUnavailable)?;
129        Ok(sig_to_hex(sig))
130    }
131}
132
133impl builder_relayer_client_rust::builder::create::TypedDataSigner for DummySigner {
134    fn sign_typed_create_proxy(
135        &self,
136        _safe_factory: &str,
137        _chain_id: u64,
138        _payment_token: &str,
139        _payment: &str,
140        _payment_receiver: &str,
141    ) -> Result<String> {
142        // Placeholder same as our local trait: sign zero hash
143        let sig = self
144            .sign_hash(H256::zero())
145            .map_err(|_| RelayClientError::SignerUnavailable)?;
146        Ok(sig_to_hex(sig))
147    }
148}
149
150impl builder_relayer_client_rust::builder::create::AbstractSignerForCreate for DummySigner {
151    fn get_address(&self) -> Result<String> {
152        Ok(format!("0x{:x}", self.address()))
153    }
154    fn sign_eip712_digest(&self, digest_hex: &str) -> Result<String> {
155        // For EIP-712 digest, use the trait method that signs without prefix
156        let bytes = hex::decode(digest_hex.trim_start_matches("0x"))
157            .map_err(|_| RelayClientError::SignerUnavailable)?;
158        let mut arr = [0u8; 32];
159        arr.copy_from_slice(&bytes);
160        let sig = AbstractSigner::sign_eip712_digest(self, H256::from(arr))
161            .map_err(|_| RelayClientError::SignerUnavailable)?;
162        Ok(sig_to_hex(sig))
163    }
164}
165
166fn sig_to_hex(sig: Signature) -> String {
167    // r,s are U256; convert to 32-byte big endian
168    let mut r_bytes = [0u8; 32];
169    sig.r.to_big_endian(&mut r_bytes);
170    let mut s_bytes = [0u8; 32];
171    sig.s.to_big_endian(&mut s_bytes);
172    format!(
173        "0x{}{}{:02x}",
174        hex::encode(r_bytes),
175        hex::encode(s_bytes),
176        sig.v
177    )
178}