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#[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 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 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 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 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 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 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 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 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 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 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 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 if !self.get_deployed(&safe_to_check).await? {
452 return Err(RelayClientError::SafeNotDeployed);
453 }
454
455 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 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 = 1; loop {
489 attempt += 1;
490 let mode = initial_mode;
491 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 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 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 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}