Skip to main content

builder_relayer_client_rust/
client.rs

1use crate::builder::create::AbstractSignerForCreate;
2use crate::builder::proxy::{
3    ProxyContractConfig, build_proxy_transaction_request, is_proxy_contract_config_valid,
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        // Read the response body as text first to enable better error diagnostics
165        let text = resp
166            .text()
167            .await
168            .map_err(|e| RelayClientError::Http(e.to_string()))?;
169        serde_json::from_str::<T>(&text).map_err(|e| {
170            // Include part of the response body in the error for debugging
171            let snippet = if text.len() > 300 {
172                format!("{}...(truncated, total {} bytes)", &text[..300], text.len())
173            } else {
174                text.clone()
175            };
176            RelayClientError::Serde(format!("{} | response body: {}", e, snippet))
177        })
178    }
179
180    /// Best-effort gas estimation for proxy txns, mirroring TS SDK behavior.
181    /// Uses BLOCK_RPC_URL env or defaults to https://polygon-rpc.com.
182    async fn estimate_gas_limit(&self, from: &str, to: &str, data: &str) -> Result<String> {
183        let rpc = if let Some(r) = self.gas_estimate_rpc.as_ref() {
184            r.clone()
185        } else {
186            std::env::var("BLOCK_RPC_URL").unwrap_or_else(|_| "https://polygon-rpc.com".to_string())
187        };
188        let payload = json!({
189            "jsonrpc": "2.0",
190            "id": 1,
191            "method": "eth_estimateGas",
192            "params": [json!({
193                "from": from,
194                "to": to,
195                "data": data,
196            })],
197        });
198
199        let resp = self
200            .http
201            .post(&rpc)
202            .json(&payload)
203            .send()
204            .await
205            .map_err(|e| RelayClientError::Http(e.to_string()))?;
206
207        let value: serde_json::Value = resp
208            .json()
209            .await
210            .map_err(|e| RelayClientError::Serde(e.to_string()))?;
211
212        if let Some(err) = value.get("error") {
213            return Err(RelayClientError::Http(format!("estimateGas error: {err}")));
214        }
215        let res = value
216            .get("result")
217            .and_then(|v| v.as_str())
218            .ok_or_else(|| RelayClientError::Http("missing result in estimateGas".into()))?;
219
220        // result is hex string
221        let gas_limit = u128::from_str_radix(res.trim_start_matches("0x"), 16)
222            .unwrap_or(0)
223            .to_string();
224        Ok(gas_limit)
225    }
226
227    pub async fn get_nonce(&self, signer_address: &str, signer_type: &str) -> Result<NoncePayload> {
228        self.send(
229            GET_NONCE,
230            "GET",
231            None,
232            Some(vec![
233                ("address".into(), signer_address.into()),
234                ("type".into(), signer_type.into()),
235            ]),
236            None,
237        )
238        .await
239    }
240
241    pub async fn get_relay_payload(
242        &self,
243        signer_address: &str,
244        signer_type: &str,
245    ) -> Result<RelayPayload> {
246        self.send(
247            GET_RELAY_PAYLOAD,
248            "GET",
249            None,
250            Some(vec![
251                ("address".into(), signer_address.into()),
252                ("type".into(), signer_type.into()),
253            ]),
254            None,
255        )
256        .await
257    }
258
259    pub async fn get_transaction(&self, transaction_id: &str) -> Result<Vec<RelayerTransaction>> {
260        self.send(
261            GET_TRANSACTION,
262            "GET",
263            None,
264            Some(vec![("id".into(), transaction_id.into())]),
265            None,
266        )
267        .await
268    }
269
270    pub async fn get_transactions(&self) -> Result<Vec<RelayerTransaction>> {
271        self.authed_get(GET_TRANSACTIONS).await
272    }
273
274    async fn authed_get<T: for<'de> serde::Deserialize<'de>>(&self, path: &str) -> Result<T> {
275        if let Some(bs) = &self.builder_signer {
276            let headers = bs
277                .create_builder_header_payload("GET", path, None, None)
278                .map_err(RelayClientError::Http)?;
279            return self.send(path, "GET", None, None, Some(&headers)).await;
280        }
281        self.send(path, "GET", None, None, None).await
282    }
283
284    async fn authed_post<T: for<'de> serde::Deserialize<'de>>(
285        &self,
286        path: &str,
287        body: &str,
288    ) -> Result<T> {
289        if let Some(bs) = &self.builder_signer {
290            let headers = bs
291                .create_builder_header_payload("POST", path, Some(body), None)
292                .map_err(RelayClientError::Http)?;
293            return self
294                .send(path, "POST", Some(body.to_string()), None, Some(&headers))
295                .await;
296        }
297        self.send(path, "POST", Some(body.to_string()), None, None)
298            .await
299    }
300
301    fn ensure_signer(&self) -> Result<()> {
302        if self.signer.is_none() {
303            Err(RelayClientError::SignerUnavailable)
304        } else {
305            Ok(())
306        }
307    }
308
309    pub async fn get_deployed(&self, safe_address: &str) -> Result<bool> {
310        let resp: GetDeployedResponse = self
311            .send(
312                GET_DEPLOYED,
313                "GET",
314                None,
315                Some(vec![("address".into(), safe_address.into())]),
316                None,
317            )
318            .await?;
319        Ok(resp.deployed)
320    }
321
322    pub async fn deploy(&self) -> Result<RelayerTransactionResponse> {
323        self.ensure_signer()?;
324        let signer = self.signer.as_ref().unwrap();
325        let factory = self
326            .contract_config
327            .safe
328            .safe_factory
329            .parse()
330            .map_err(|_| RelayClientError::InvalidAddress)?;
331        let safe_addr = derive_safe(factory, signer.address())?;
332        let safe = safe_addr.to_string();
333
334        if self.get_deployed(&safe).await? {
335            return Err(RelayClientError::SafeDeployed);
336        }
337        self._deploy().await
338    }
339
340    async fn _deploy(&self) -> Result<RelayerTransactionResponse> {
341        self.ensure_signer()?;
342        let signer = self.signer.as_ref().unwrap();
343        let typed = self
344            .typed_signer
345            .as_ref()
346            .ok_or(RelayClientError::SignerUnavailable)?;
347        let from = signer.address().to_string();
348        let args = SafeCreateTransactionArgs {
349            from: from.clone(),
350            chain_id: self.chain_id,
351            payment_token: "0x0000000000000000000000000000000000000000".into(),
352            payment: "0".into(),
353            payment_receiver: "0x0000000000000000000000000000000000000000".into(),
354        };
355        let req = build_safe_create_transaction_request(
356            typed.as_ref(),
357            &self.contract_config.safe.safe_factory,
358            args,
359        )
360        .await?;
361        let payload =
362            serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
363        let resp: RelayerTransactionResponse =
364            self.authed_post(SUBMIT_TRANSACTION, &payload).await?;
365        Ok(resp)
366    }
367
368    /// 按 relayTxType 分发执行(对齐 TS execute 行为)
369    pub async fn execute_transactions(
370        &self,
371        txns: Vec<Transaction>,
372        metadata: Option<String>,
373    ) -> Result<RelayerTransactionResponse> {
374        match self.relay_tx_type {
375            RelayerTxType::Safe => {
376                let safe_txns: Vec<SafeTransaction> = txns
377                    .into_iter()
378                    .map(|t| SafeTransaction {
379                        to: t.to,
380                        operation: OperationType::Call,
381                        data: t.data,
382                        // TS execute() 对 SAFE 分支将 value 固定为 "0"
383                        value: "0".to_string(),
384                    })
385                    .collect();
386                self.execute_with_safe(safe_txns, metadata, None).await
387            }
388            RelayerTxType::Proxy => {
389                let proxy_txns: Vec<ProxyTransaction> = txns
390                    .into_iter()
391                    .map(|t| ProxyTransaction {
392                        to: t.to,
393                        type_code: CallType::Call,
394                        data: t.data,
395                        value: t.value,
396                    })
397                    .collect();
398                self.execute_proxy_transactions(proxy_txns, metadata).await
399            }
400        }
401    }
402
403    pub async fn execute(
404        &self,
405        txns: Vec<SafeTransaction>,
406        metadata: Option<String>,
407    ) -> Result<RelayerTransactionResponse> {
408        self.execute_with_safe(txns, metadata, None).await
409    }
410
411    /// Execute transactions with an optional explicit Safe address
412    ///
413    /// If `safe_address` is provided, it will be used directly instead of deriving from signer address.
414    /// This is useful when the Safe address is already known (e.g., from Polymarket account).
415    pub async fn execute_with_safe(
416        &self,
417        txns: Vec<SafeTransaction>,
418        metadata: Option<String>,
419        safe_address: Option<String>,
420    ) -> Result<RelayerTransactionResponse> {
421        self.ensure_signer()?;
422        let signer = self.signer.as_ref().unwrap();
423        let from = signer.address().to_string();
424
425        // Debug: compare derived safe and provided safe (if any)
426        let factory = self
427            .contract_config
428            .safe
429            .safe_factory
430            .parse()
431            .map_err(|_| RelayClientError::InvalidAddress)?;
432        let derived_safe_addr = derive_safe(factory, signer.address())?;
433        let derived_safe = derived_safe_addr.to_string();
434        let safe_to_check = if let Some(ref provided_safe) = safe_address {
435            eprintln!(
436                "[RelayClient][execute] derived_safe={} provided_safe={} equal? {}",
437                derived_safe,
438                provided_safe,
439                (derived_safe.to_lowercase() == provided_safe.to_lowercase())
440            );
441            provided_safe.clone()
442        } else {
443            eprintln!(
444                "[RelayClient][execute] derived_safe={} (no provided safe)",
445                derived_safe
446            );
447            derived_safe.clone()
448        };
449
450        // Check if Safe is deployed
451        if !self.get_deployed(&safe_to_check).await? {
452            return Err(RelayClientError::SafeNotDeployed);
453        }
454
455        // Use EOA `from` to fetch nonce, matching the TypeScript SDK behaviour.
456        // The relayer expects the nonce query to be done against the signer EOA
457        // even when a proxyWallet (Safe address) is provided in the request.
458        let nonce_payload = self.get_nonce(&from, "SAFE").await?;
459
460        let args = SafeTransactionArgs {
461            from: from.clone(),
462            nonce: nonce_payload.nonce.clone(),
463            chain_id: self.chain_id,
464            transactions: txns,
465            safe_address,
466        };
467        // Resolve signature mode from env
468        // let mode_env = std::env::var("RELAYER_SIG_MODE").unwrap_or_else(|_| "auto".into());
469        // let initial_mode = match mode_env.to_lowercase().as_str() {
470        //     "structhash" => SignatureMode::Eip191StructHash,
471        //     "digest" => SignatureMode::Eip712Digest,
472        //     "eip191_digest" => SignatureMode::Eip191Digest,
473        //     _ => SignatureMode::Eip191Digest, // 默认与 TS signMessage(hashTypedData digest) 行为一致
474        // };
475
476        // TS implementation: signMessage(hashTypedData(...)) => EIP-191 over the EIP-712 digest
477        let initial_mode = SignatureMode::Eip191Digest;
478
479        let mut _last_err: Option<crate::errors::RelayClientError> = None;
480        let mut attempt = 0;
481        // let max_attempts = if mode_env.to_lowercase() == "auto" {
482        //     3
483        // } else {
484        //     1
485        // };
486        let max_attempts = 1; // Disable retry loop
487
488        loop {
489            attempt += 1;
490            let mode = initial_mode;
491            // let mode = if attempt == 1 {
492            //     initial_mode
493            // } else {
494            //     match (initial_mode, attempt) {
495            //         (SignatureMode::Eip191Digest, 2) => SignatureMode::Eip712Digest,
496            //         (SignatureMode::Eip191Digest, 3) => SignatureMode::Eip191StructHash,
497            //         (SignatureMode::Eip712Digest, 2) => SignatureMode::Eip191Digest,
498            //         (SignatureMode::Eip712Digest, 3) => SignatureMode::Eip191StructHash,
499            //         (SignatureMode::Eip191StructHash, 2) => SignatureMode::Eip191Digest,
500            //         (SignatureMode::Eip191StructHash, 3) => SignatureMode::Eip712Digest,
501            //         _ => initial_mode,
502            //     }
503            // };
504
505            let req = build_safe_transaction_request(
506                signer.as_ref(),
507                &args.clone(),
508                &self.contract_config.safe,
509                metadata.clone(),
510                mode,
511            )
512            .await?;
513            let body =
514                serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
515            eprintln!("[RelayClient][execute] outbound body: {}", body);
516            let res: Result<RelayerTransactionResponse> =
517                self.authed_post(SUBMIT_TRANSACTION, &body).await;
518            match res {
519                Ok(resp) => {
520                    eprintln!(
521                        "[RelayClient][execute] Transaction submitted successfully using signature mode: {:?}",
522                        mode
523                    );
524                    // Check if the response indicates an error (relayer may return 200 with error in body)
525                    if let Some(ref err) = resp.error
526                        && !err.is_empty()
527                    {
528                        let msg = resp
529                            .message
530                            .as_deref()
531                            .or(resp.reason.as_deref())
532                            .unwrap_or(err);
533                        return Err(RelayClientError::Http(format!("relayer error: {}", msg)));
534                    }
535                    return Ok(resp);
536                }
537                Err(e) => {
538                    let msg = format!("{}", e);
539                    let is_invalid_sig =
540                        msg.contains("invalid signature") || msg.contains("validation error");
541                    if attempt < max_attempts && is_invalid_sig {
542                        eprintln!(
543                            "[RelayClient][execute] invalid signature, retrying with alternate signature mode..."
544                        );
545                        _last_err = Some(e);
546                        continue;
547                    } else {
548                        return Err(e);
549                    }
550                }
551            }
552        }
553    }
554
555    pub async fn execute_proxy_transactions(
556        &self,
557        txns: Vec<ProxyTransaction>,
558        metadata: Option<String>,
559    ) -> Result<RelayerTransactionResponse> {
560        self.ensure_signer()?;
561        if !is_proxy_contract_config_valid(&self.contract_config.proxy) {
562            return Err(RelayClientError::InvalidNetwork);
563        }
564
565        let signer = self.signer.as_ref().unwrap();
566        let from = signer.address().to_string();
567
568        let relay_payload = self.get_relay_payload(&from, "PROXY").await?;
569        let encoded_data = crate::encode::proxy::encode_proxy_transaction_data(&txns);
570
571        // Try to mirror TS SDK behavior: estimate gas if not provided by caller.
572        let gas_limit_est = self
573            .estimate_gas_limit(
574                &from,
575                &self.contract_config.proxy.proxy_factory,
576                &encoded_data,
577            )
578            .await
579            .ok();
580
581        let args = ProxyTransactionArgs {
582            from: from.clone(),
583            nonce: relay_payload.nonce.clone(),
584            gas_price: "0".to_string(),
585            gas_limit: gas_limit_est,
586            data: encoded_data,
587            relay: relay_payload.address.clone(),
588        };
589
590        let req = build_proxy_transaction_request(
591            signer.as_ref(),
592            &args,
593            &self.contract_config.proxy,
594            metadata.clone(),
595            &txns,
596        )
597        .await?;
598
599        let body =
600            serde_json::to_string(&req).map_err(|e| RelayClientError::Serde(e.to_string()))?;
601        let resp: RelayerTransactionResponse = self.authed_post(SUBMIT_TRANSACTION, &body).await?;
602
603        // Check if the response indicates an error (relayer may return 200 with error in body)
604        if let Some(ref err) = resp.error
605            && !err.is_empty()
606        {
607            let msg = resp
608                .message
609                .as_deref()
610                .or(resp.reason.as_deref())
611                .unwrap_or(err);
612            return Err(RelayClientError::Http(format!("relayer error: {}", msg)));
613        }
614
615        Ok(resp)
616    }
617
618    pub async fn poll_until_state(
619        &self,
620        transaction_id: &str,
621        states: &[RelayerTransactionState],
622        fail_state: Option<RelayerTransactionState>,
623        max_polls: usize,
624        poll_freq_ms: u64,
625    ) -> Result<Option<RelayerTransaction>> {
626        let mut count = 0usize;
627        while count < max_polls {
628            let txns = self.get_transaction(transaction_id).await?;
629            if let Some(first) = txns.first() {
630                if states.iter().any(|s| first.state == format!("{:?}", s)) {
631                    return Ok(Some(first.clone()));
632                }
633                if let Some(ref fail) = fail_state
634                    && first.state == format!("{:?}", fail)
635                {
636                    return Ok(None);
637                }
638            }
639            count += 1;
640            sleep_ms(poll_freq_ms).await;
641        }
642        Ok(None)
643    }
644}