builder_relayer_client_rust/
client.rs

1use crate::builder::create::AbstractSignerForCreate;
2use crate::builder::proxy::{
3    build_proxy_transaction_request, is_proxy_contract_config_valid, ProxyContractConfig,
4};
5use crate::builder::safe::{AbstractSigner, SafeContractConfig, SignatureMode};
6use crate::builder::{
7    build_safe_create_transaction_request, build_safe_transaction_request, derive_safe,
8};
9use crate::endpoints::*;
10use crate::errors::{RelayClientError, Result};
11use crate::types::*;
12use crate::utils::sleep_ms;
13use builder_signing_sdk_rs::{BuilderApiKeyCreds, BuilderSigner};
14use reqwest::Client as HttpClient;
15use serde_json::json;
16// use serde_json::json;
17
18#[derive(Clone, Debug)]
19pub struct ContractConfig {
20    pub safe: SafeContractConfig,
21    pub proxy: ProxyContractConfig,
22}
23
24pub struct RelayClient {
25    pub relayer_url: String,
26    pub chain_id: u64,
27    pub contract_config: ContractConfig,
28    pub relay_tx_type: RelayerTxType,
29    http: HttpClient,
30    gas_estimate_rpc: Option<String>,
31    signer: Option<Box<dyn AbstractSigner + Send + Sync>>,
32    typed_signer: Option<Box<dyn AbstractSignerForCreate + Send + Sync>>,
33    builder_signer: Option<BuilderSigner>,
34}
35
36impl RelayClient {
37    pub fn new(relayer_url: impl Into<String>, chain_id: u64) -> Self {
38        Self::new_with_type(relayer_url, chain_id, RelayerTxType::Safe)
39    }
40
41    pub fn new_with_type(
42        relayer_url: impl Into<String>,
43        chain_id: u64,
44        relay_tx_type: RelayerTxType,
45    ) -> Self {
46        let url = relayer_url.into();
47        let contract_config = match chain_id {
48            137 => ContractConfig {
49                proxy: ProxyContractConfig {
50                    proxy_factory: "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052".into(),
51                    relay_hub: "0xD216153c06E857cD7f72665E0aF1d7D82172F494".into(),
52                },
53                safe: SafeContractConfig {
54                    safe_factory: "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b".into(),
55                    safe_multisend: "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761".into(),
56                },
57            },
58            80002 => ContractConfig {
59                proxy: ProxyContractConfig {
60                    proxy_factory: String::new(),
61                    relay_hub: String::new(),
62                },
63                safe: SafeContractConfig {
64                    safe_factory: "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b".into(),
65                    safe_multisend: "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761".into(),
66                },
67            },
68            _ => ContractConfig {
69                proxy: ProxyContractConfig {
70                    proxy_factory: String::new(),
71                    relay_hub: String::new(),
72                },
73                safe: SafeContractConfig {
74                    safe_factory: String::new(),
75                    safe_multisend: String::new(),
76                },
77            },
78        };
79
80        Self {
81            relayer_url: url.trim_end_matches('/').to_string(),
82            chain_id,
83            contract_config,
84            relay_tx_type,
85            http: HttpClient::new(),
86            gas_estimate_rpc: None,
87            signer: None,
88            typed_signer: None,
89            builder_signer: None,
90        }
91    }
92
93    /// Override the relay transaction type (SAFE / PROXY) after construction.
94    /// This is useful when you want to pick the type dynamically (e.g., via config/env).
95    pub fn with_relay_tx_type(mut self, relay_tx_type: RelayerTxType) -> Self {
96        self.relay_tx_type = relay_tx_type;
97        self
98    }
99
100    pub fn with_signer(
101        mut self,
102        signer: Box<dyn AbstractSigner + Send + Sync>,
103        typed: Box<dyn AbstractSignerForCreate + Send + Sync>,
104    ) -> Self {
105        self.signer = Some(signer);
106        self.typed_signer = Some(typed);
107        self
108    }
109
110    pub fn with_builder_api_key(mut self, creds: BuilderApiKeyCreds) -> Self {
111        self.builder_signer = Some(builder_signing_sdk_rs::BuilderSigner::new(creds));
112        self
113    }
114
115    /// Override the RPC used for gas estimation (defaults to BLOCK_RPC_URL env or polygon-rpc.com).
116    pub fn with_gas_estimate_rpc(mut self, rpc: impl Into<String>) -> Self {
117        self.gas_estimate_rpc = Some(rpc.into());
118        self
119    }
120
121    async fn send<T: for<'de> serde::Deserialize<'de>>(
122        &self,
123        path: &str,
124        method: &str,
125        body: Option<String>,
126        params: Option<Vec<(String, String)>>,
127        builder_headers: Option<&std::collections::HashMap<String, String>>,
128    ) -> Result<T> {
129        let url = format!("{}{}", self.relayer_url, path);
130        let mut req = match method {
131            "GET" => self.http.get(&url),
132            "POST" => self.http.post(&url),
133            _ => return Err(RelayClientError::Http("unsupported method".into())),
134        };
135        if let Some(p) = params {
136            req = req.query(&p);
137        }
138        if let Some(b) = body {
139            req = req.body(b);
140        }
141        if let Some(h) = builder_headers {
142            for (k, v) in h {
143                req = req.header(k, v);
144            }
145        }
146        let resp = req
147            .send()
148            .await
149            .map_err(|e| RelayClientError::Http(e.to_string()))?;
150        let status = resp.status();
151        if !status.is_success() {
152            // Try to include response body for diagnostics
153            let text = resp.text().await.unwrap_or_default();
154            let snippet = if text.len() > 512 {
155                &text[..512]
156            } else {
157                &text
158            };
159            return Err(RelayClientError::Http(format!(
160                "status {} body: {}",
161                status, snippet
162            )));
163        }
164        resp.json::<T>()
165            .await
166            .map_err(|e| RelayClientError::Serde(e.to_string()))
167    }
168
169    /// Best-effort gas estimation for proxy txns, mirroring TS SDK behavior.
170    /// Uses BLOCK_RPC_URL env or defaults to https://polygon-rpc.com.
171    async fn estimate_gas_limit(&self, from: &str, to: &str, data: &str) -> Result<String> {
172        let rpc = if let Some(r) = self.gas_estimate_rpc.as_ref() {
173            r.clone()
174        } else {
175            std::env::var("BLOCK_RPC_URL").unwrap_or_else(|_| "https://polygon-rpc.com".to_string())
176        };
177        let payload = json!({
178            "jsonrpc": "2.0",
179            "id": 1,
180            "method": "eth_estimateGas",
181            "params": [json!({
182                "from": from,
183                "to": to,
184                "data": data,
185            })],
186        });
187
188        let resp = self
189            .http
190            .post(&rpc)
191            .json(&payload)
192            .send()
193            .await
194            .map_err(|e| RelayClientError::Http(e.to_string()))?;
195
196        let value: serde_json::Value = resp
197            .json()
198            .await
199            .map_err(|e| RelayClientError::Serde(e.to_string()))?;
200
201        if let Some(err) = value.get("error") {
202            return Err(RelayClientError::Http(format!("estimateGas error: {err}")));
203        }
204        let res = value
205            .get("result")
206            .and_then(|v| v.as_str())
207            .ok_or_else(|| RelayClientError::Http("missing result in estimateGas".into()))?;
208
209        // result is hex string
210        let gas_limit = u128::from_str_radix(res.trim_start_matches("0x"), 16)
211            .unwrap_or(0)
212            .to_string();
213        Ok(gas_limit)
214    }
215
216    pub async fn get_nonce(&self, signer_address: &str, signer_type: &str) -> Result<NoncePayload> {
217        self.send(
218            GET_NONCE,
219            "GET",
220            None,
221            Some(vec![
222                ("address".into(), signer_address.into()),
223                ("type".into(), signer_type.into()),
224            ]),
225            None,
226        )
227        .await
228    }
229
230    pub async fn get_relay_payload(
231        &self,
232        signer_address: &str,
233        signer_type: &str,
234    ) -> Result<RelayPayload> {
235        self.send(
236            GET_RELAY_PAYLOAD,
237            "GET",
238            None,
239            Some(vec![
240                ("address".into(), signer_address.into()),
241                ("type".into(), signer_type.into()),
242            ]),
243            None,
244        )
245        .await
246    }
247
248    pub async fn get_transaction(&self, transaction_id: &str) -> Result<Vec<RelayerTransaction>> {
249        self.send(
250            GET_TRANSACTION,
251            "GET",
252            None,
253            Some(vec![("id".into(), transaction_id.into())]),
254            None,
255        )
256        .await
257    }
258
259    pub async fn get_transactions(&self) -> Result<Vec<RelayerTransaction>> {
260        self.authed_get(GET_TRANSACTIONS).await
261    }
262
263    async fn authed_get<T: for<'de> serde::Deserialize<'de>>(&self, path: &str) -> Result<T> {
264        if let Some(bs) = &self.builder_signer {
265            let headers = bs
266                .create_builder_header_payload("GET", path, None, None)
267                .map_err(RelayClientError::Http)?;
268            return self.send(path, "GET", None, None, Some(&headers)).await;
269        }
270        self.send(path, "GET", None, None, None).await
271    }
272
273    async fn authed_post<T: for<'de> serde::Deserialize<'de>>(
274        &self,
275        path: &str,
276        body: &str,
277    ) -> Result<T> {
278        if let Some(bs) = &self.builder_signer {
279            let headers = bs
280                .create_builder_header_payload("POST", path, Some(body), None)
281                .map_err(RelayClientError::Http)?;
282            return self
283                .send(path, "POST", Some(body.to_string()), None, Some(&headers))
284                .await;
285        }
286        self.send(path, "POST", Some(body.to_string()), None, None)
287            .await
288    }
289
290    fn ensure_signer(&self) -> Result<()> {
291        if self.signer.is_none() {
292            Err(RelayClientError::SignerUnavailable)
293        } else {
294            Ok(())
295        }
296    }
297
298    pub async fn get_deployed(&self, safe_address: &str) -> Result<bool> {
299        let resp: GetDeployedResponse = self
300            .send(
301                GET_DEPLOYED,
302                "GET",
303                None,
304                Some(vec![("address".into(), safe_address.into())]),
305                None,
306            )
307            .await?;
308        Ok(resp.deployed)
309    }
310
311    pub async fn deploy(&self) -> Result<RelayerTransactionResponse> {
312        self.ensure_signer()?;
313        let signer = self.signer.as_ref().unwrap();
314        let factory = self
315            .contract_config
316            .safe
317            .safe_factory
318            .parse()
319            .map_err(|_| RelayClientError::InvalidAddress)?;
320        let safe_addr = derive_safe(factory, signer.address())?;
321        let safe = safe_addr.to_string();
322
323        if self.get_deployed(&safe).await? {
324            return Err(RelayClientError::SafeDeployed);
325        }
326        self._deploy().await
327    }
328
329    async fn _deploy(&self) -> Result<RelayerTransactionResponse> {
330        self.ensure_signer()?;
331        let signer = self.signer.as_ref().unwrap();
332        let typed = self
333            .typed_signer
334            .as_ref()
335            .ok_or(RelayClientError::SignerUnavailable)?;
336        let from = signer.address().to_string();
337        let args = SafeCreateTransactionArgs {
338            from: from.clone(),
339            chain_id: self.chain_id,
340            payment_token: "0x0000000000000000000000000000000000000000".into(),
341            payment: "0".into(),
342            payment_receiver: "0x0000000000000000000000000000000000000000".into(),
343        };
344        let req = build_safe_create_transaction_request(
345            typed.as_ref(),
346            &self.contract_config.safe.safe_factory,
347            args,
348        )
349        .await?;
350        let payload =
351            serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
352        let resp: RelayerTransactionResponse =
353            self.authed_post(SUBMIT_TRANSACTION, &payload).await?;
354        Ok(resp)
355    }
356
357    /// 按 relayTxType 分发执行(对齐 TS execute 行为)
358    pub async fn execute_transactions(
359        &self,
360        txns: Vec<Transaction>,
361        metadata: Option<String>,
362    ) -> Result<RelayerTransactionResponse> {
363        match self.relay_tx_type {
364            RelayerTxType::Safe => {
365                let safe_txns: Vec<SafeTransaction> = txns
366                    .into_iter()
367                    .map(|t| SafeTransaction {
368                        to: t.to,
369                        operation: OperationType::Call,
370                        data: t.data,
371                        // TS execute() 对 SAFE 分支将 value 固定为 "0"
372                        value: "0".to_string(),
373                    })
374                    .collect();
375                self.execute_with_safe(safe_txns, metadata, None).await
376            }
377            RelayerTxType::Proxy => {
378                let proxy_txns: Vec<ProxyTransaction> = txns
379                    .into_iter()
380                    .map(|t| ProxyTransaction {
381                        to: t.to,
382                        type_code: CallType::Call,
383                        data: t.data,
384                        value: t.value,
385                    })
386                    .collect();
387                self.execute_proxy_transactions(proxy_txns, metadata).await
388            }
389        }
390    }
391
392    pub async fn execute(
393        &self,
394        txns: Vec<SafeTransaction>,
395        metadata: Option<String>,
396    ) -> Result<RelayerTransactionResponse> {
397        self.execute_with_safe(txns, metadata, None).await
398    }
399
400    /// Execute transactions with an optional explicit Safe address
401    ///
402    /// If `safe_address` is provided, it will be used directly instead of deriving from signer address.
403    /// This is useful when the Safe address is already known (e.g., from Polymarket account).
404    pub async fn execute_with_safe(
405        &self,
406        txns: Vec<SafeTransaction>,
407        metadata: Option<String>,
408        safe_address: Option<String>,
409    ) -> Result<RelayerTransactionResponse> {
410        self.ensure_signer()?;
411        let signer = self.signer.as_ref().unwrap();
412        let from = signer.address().to_string();
413
414        // Debug: compare derived safe and provided safe (if any)
415        let factory = self
416            .contract_config
417            .safe
418            .safe_factory
419            .parse()
420            .map_err(|_| RelayClientError::InvalidAddress)?;
421        let derived_safe_addr = derive_safe(factory, signer.address())?;
422        let derived_safe = derived_safe_addr.to_string();
423        let safe_to_check = if let Some(ref provided_safe) = safe_address {
424            eprintln!(
425                "[RelayClient][execute] derived_safe={} provided_safe={} equal? {}",
426                derived_safe,
427                provided_safe,
428                (derived_safe.to_lowercase() == provided_safe.to_lowercase())
429            );
430            provided_safe.clone()
431        } else {
432            eprintln!(
433                "[RelayClient][execute] derived_safe={} (no provided safe)",
434                derived_safe
435            );
436            derived_safe.clone()
437        };
438
439        // Check if Safe is deployed
440        if !self.get_deployed(&safe_to_check).await? {
441            return Err(RelayClientError::SafeNotDeployed);
442        }
443
444        // Use EOA `from` to fetch nonce, matching the TypeScript SDK behaviour.
445        // The relayer expects the nonce query to be done against the signer EOA
446        // even when a proxyWallet (Safe address) is provided in the request.
447        let nonce_payload = self.get_nonce(&from, "SAFE").await?;
448
449        let args = SafeTransactionArgs {
450            from: from.clone(),
451            nonce: nonce_payload.nonce.clone(),
452            chain_id: self.chain_id,
453            transactions: txns,
454            safe_address,
455        };
456        // Resolve signature mode from env
457        // let mode_env = std::env::var("RELAYER_SIG_MODE").unwrap_or_else(|_| "auto".into());
458        // let initial_mode = match mode_env.to_lowercase().as_str() {
459        //     "structhash" => SignatureMode::Eip191StructHash,
460        //     "digest" => SignatureMode::Eip712Digest,
461        //     "eip191_digest" => SignatureMode::Eip191Digest,
462        //     _ => SignatureMode::Eip191Digest, // 默认与 TS signMessage(hashTypedData digest) 行为一致
463        // };
464
465        // TS implementation: signMessage(hashTypedData(...)) => EIP-191 over the EIP-712 digest
466        let initial_mode = SignatureMode::Eip191Digest;
467
468        let mut _last_err: Option<crate::errors::RelayClientError> = None;
469        let mut attempt = 0;
470        // let max_attempts = if mode_env.to_lowercase() == "auto" {
471        //     3
472        // } else {
473        //     1
474        // };
475        let max_attempts = 1; // Disable retry loop
476
477        loop {
478            attempt += 1;
479            let mode = initial_mode;
480            // let mode = if attempt == 1 {
481            //     initial_mode
482            // } else {
483            //     match (initial_mode, attempt) {
484            //         (SignatureMode::Eip191Digest, 2) => SignatureMode::Eip712Digest,
485            //         (SignatureMode::Eip191Digest, 3) => SignatureMode::Eip191StructHash,
486            //         (SignatureMode::Eip712Digest, 2) => SignatureMode::Eip191Digest,
487            //         (SignatureMode::Eip712Digest, 3) => SignatureMode::Eip191StructHash,
488            //         (SignatureMode::Eip191StructHash, 2) => SignatureMode::Eip191Digest,
489            //         (SignatureMode::Eip191StructHash, 3) => SignatureMode::Eip712Digest,
490            //         _ => initial_mode,
491            //     }
492            // };
493
494            let req = build_safe_transaction_request(
495                signer.as_ref(),
496                &args.clone(),
497                &self.contract_config.safe,
498                metadata.clone(),
499                mode,
500            )
501            .await?;
502            let body =
503                serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
504            eprintln!("[RelayClient][execute] outbound body: {}", body);
505            let res = self.authed_post(SUBMIT_TRANSACTION, &body).await;
506            match res {
507                Ok(resp) => {
508                    eprintln!(
509                        "[RelayClient][execute] Transaction submitted successfully using signature mode: {:?}",
510                        mode
511                    );
512                    return Ok(resp);
513                }
514                Err(e) => {
515                    let msg = format!("{}", e);
516                    let is_invalid_sig =
517                        msg.contains("invalid signature") || msg.contains("validation error");
518                    if attempt < max_attempts && is_invalid_sig {
519                        eprintln!("[RelayClient][execute] invalid signature, retrying with alternate signature mode...");
520                        _last_err = Some(e);
521                        continue;
522                    } else {
523                        return Err(e);
524                    }
525                }
526            }
527        }
528    }
529
530    pub async fn execute_proxy_transactions(
531        &self,
532        txns: Vec<ProxyTransaction>,
533        metadata: Option<String>,
534    ) -> Result<RelayerTransactionResponse> {
535        self.ensure_signer()?;
536        if !is_proxy_contract_config_valid(&self.contract_config.proxy) {
537            return Err(RelayClientError::InvalidNetwork);
538        }
539
540        let signer = self.signer.as_ref().unwrap();
541        let from = signer.address().to_string();
542
543        let relay_payload = self.get_relay_payload(&from, "PROXY").await?;
544        let encoded_data = crate::encode::proxy::encode_proxy_transaction_data(&txns);
545
546        // Try to mirror TS SDK behavior: estimate gas if not provided by caller.
547        let gas_limit_est = self
548            .estimate_gas_limit(
549                &from,
550                &self.contract_config.proxy.proxy_factory,
551                &encoded_data,
552            )
553            .await
554            .ok();
555
556        let args = ProxyTransactionArgs {
557            from: from.clone(),
558            nonce: relay_payload.nonce.clone(),
559            gas_price: "0".to_string(),
560            gas_limit: gas_limit_est,
561            data: encoded_data,
562            relay: relay_payload.address.clone(),
563        };
564
565        let req = build_proxy_transaction_request(
566            signer.as_ref(),
567            &args,
568            &self.contract_config.proxy,
569            metadata.clone(),
570            &txns,
571        )
572        .await?;
573
574        let body =
575            serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
576        self.authed_post(SUBMIT_TRANSACTION, &body).await
577    }
578
579    pub async fn poll_until_state(
580        &self,
581        transaction_id: &str,
582        states: &[RelayerTransactionState],
583        fail_state: Option<RelayerTransactionState>,
584        max_polls: usize,
585        poll_freq_ms: u64,
586    ) -> Result<Option<RelayerTransaction>> {
587        let mut count = 0usize;
588        while count < max_polls {
589            let txns = self.get_transaction(transaction_id).await?;
590            if let Some(first) = txns.get(0) {
591                if states.iter().any(|s| first.state == format!("{:?}", s)) {
592                    return Ok(Some(first.clone()));
593                }
594                if let Some(ref fail) = fail_state {
595                    if first.state == format!("{:?}", fail) {
596                        return Ok(None);
597                    }
598                }
599            }
600            count += 1;
601            sleep_ms(poll_freq_ms).await;
602        }
603        Ok(None)
604    }
605}