1use crate::auth::AuthMethod;
2use crate::builder::{create, deposit_wallet, derive, proxy, safe};
3use crate::contracts;
4use crate::error::{RelayerError, Result};
5use crate::types::*;
6use ethers::signers::{LocalWallet, Signer};
7use reqwest::Client;
8use std::sync::Arc;
9use tokio::time::{sleep, Duration};
10use tracing::{debug, info, warn};
11
12const DEFAULT_PROXY_GAS_LIMIT: u64 = 200_000;
21const POLL_INTERVAL: Duration = Duration::from_secs(2);
22const MAX_POLL_ATTEMPTS: u32 = 100;
23
24#[derive(Clone)]
26pub struct RelayClient {
27 http: Client,
28 base_url: String,
29 chain_id: u64,
30 signer: Arc<LocalWallet>,
31 auth: AuthMethod,
32 tx_type: RelayerTxType,
33 rpc_url: Option<String>,
35}
36
37impl RelayClient {
38 pub async fn new(
46 chain_id: u64,
47 signer: LocalWallet,
48 auth: AuthMethod,
49 tx_type: RelayerTxType,
50 ) -> Result<Self> {
51 let http = Client::builder()
52 .timeout(Duration::from_secs(30))
53 .build()?;
54
55 Ok(Self {
56 http,
57 base_url: contracts::RELAYER_URL.trim_end_matches('/').to_string(),
58 chain_id,
59 signer: Arc::new(signer),
60 auth,
61 tx_type,
62 rpc_url: None,
63 })
64 }
65
66 pub fn set_url(&mut self, url: String) {
68 self.base_url = url.trim_end_matches('/').to_string();
69 }
70
71 pub fn set_rpc_url(&mut self, url: String) {
80 self.rpc_url = Some(url);
81 }
82
83 pub fn signer_address(&self) -> ethers::types::Address {
85 self.signer.address()
86 }
87
88 pub fn wallet_address(&self) -> Result<ethers::types::Address> {
90 match self.tx_type {
91 RelayerTxType::Eoa => Ok(self.signer.address()),
92 RelayerTxType::Safe => derive::derive_safe_address(self.signer.address()),
93 RelayerTxType::Proxy => derive::derive_proxy_address(self.signer.address()),
94 }
95 }
96
97 pub async fn is_deployed(&self) -> Result<bool> {
99 let wallet = self.wallet_address()?;
100 let url = format!("{}/deployed?address={:?}", self.base_url, wallet);
101 let resp = self.http.get(&url).send().await?;
102 if !resp.status().is_success() {
103 let status = resp.status().as_u16();
104 let body = resp.text().await.unwrap_or_default();
105 return Err(RelayerError::Api { status, message: body });
106 }
107 let text = resp.text().await?;
108 let body: serde_json::Value = serde_json::from_str(&text)
109 .map_err(|e| RelayerError::Other(format!("Parse Error on {}: {}", text, e)))?;
110 Ok(body.as_bool()
115 .or_else(|| body.as_str().map(|s| s == "true"))
116 .or_else(|| body.get("deployed").and_then(|v| v.as_bool()))
117 .unwrap_or(false))
118 }
119
120 pub async fn get_nonce(&self) -> Result<u64> {
126 if self.tx_type == RelayerTxType::Safe {
128 if let Some(ref rpc_url) = self.rpc_url {
129 match self.read_safe_nonce_onchain(rpc_url).await {
130 Ok(nonce) => {
131 debug!(nonce, source = "on-chain", "Safe nonce");
132 return Ok(nonce);
133 }
134 Err(e) => {
135 warn!(error = %e, "Failed to read on-chain nonce, falling back to relayer API");
136 }
137 }
138 }
139 }
140
141 let nonce = self.get_nonce_from_relayer().await?;
143 debug!(nonce, source = "relayer-api", "Nonce");
144 Ok(nonce)
145 }
146
147 async fn read_safe_nonce_onchain(&self, rpc_url: &str) -> Result<u64> {
149 let safe_address = self.wallet_address()?;
150
151 let selector = ðers::utils::keccak256(b"nonce()")[..4];
153 let calldata = format!("0x{}", hex::encode(selector));
154
155 let body = serde_json::json!({
156 "jsonrpc": "2.0",
157 "method": "eth_call",
158 "params": [{
159 "to": format!("{:?}", safe_address),
160 "data": calldata,
161 }, "latest"],
162 "id": 1
163 });
164
165 let resp = self
166 .http
167 .post(rpc_url)
168 .json(&body)
169 .send()
170 .await
171 .map_err(|e| RelayerError::Other(format!("RPC request failed: {e}")))?;
172
173 let text = resp.text().await
174 .map_err(|e| RelayerError::Other(format!("RPC response read failed: {e}")))?;
175
176 let json: serde_json::Value = serde_json::from_str(&text)
177 .map_err(|e| RelayerError::Other(format!("RPC parse error on {}: {e}", text)))?;
178
179 if let Some(error) = json.get("error") {
181 let msg = error.get("message").and_then(|m| m.as_str()).unwrap_or("unknown");
182 return Err(RelayerError::Other(format!("RPC error: {msg}")));
183 }
184
185 let result_hex = json.get("result")
186 .and_then(|r| r.as_str())
187 .ok_or_else(|| RelayerError::Other(format!("No result in RPC response: {text}")))?;
188
189 let result_hex = result_hex.strip_prefix("0x").unwrap_or(result_hex);
191 let nonce = u64::from_str_radix(result_hex, 16)
192 .map_err(|e| RelayerError::Other(format!("Invalid nonce hex '{}': {e}", result_hex)))?;
193
194 Ok(nonce)
195 }
196
197 async fn get_nonce_from_relayer(&self) -> Result<u64> {
199 let url = format!(
200 "{}/nonce?address={:?}&type={}",
201 self.base_url,
202 self.signer.address(),
203 self.tx_type.as_str()
204 );
205 let resp = self.http.get(&url).send().await?;
206 if !resp.status().is_success() {
207 let status = resp.status().as_u16();
208 let body = resp.text().await.unwrap_or_default();
209 return Err(RelayerError::Api { status, message: body });
210 }
211 let text = resp.text().await?;
212 debug!(raw_response = %text, "Relayer nonce response");
213
214 let body: serde_json::Value = serde_json::from_str(&text)
215 .map_err(|e| RelayerError::Other(format!("Nonce parse error on {}: {}", text, e)))?;
216 let nonce = body
217 .as_u64()
218 .or_else(|| body.as_str().and_then(|s| s.parse().ok()))
219 .unwrap_or(0);
220 Ok(nonce)
221 }
222
223 async fn get_relay_payload(&self) -> Result<RelayPayload> {
225 let url = format!(
226 "{}/relay-payload?address={:?}&type=PROXY",
227 self.base_url,
228 self.signer.address()
229 );
230 let resp = self.http.get(&url).send().await?;
231 if !resp.status().is_success() {
232 let status = resp.status().as_u16();
233 let body = resp.text().await.unwrap_or_default();
234 return Err(RelayerError::Api { status, message: body });
235 }
236 let text = resp.text().await?;
237 Ok(serde_json::from_str(&text).map_err(|e| RelayerError::Other(format!("Payload Parse Error on {}: {}", text, e)))?)
238 }
239
240 pub async fn get_transaction(&self, tx_id: &str) -> Result<TxResult> {
242 let url = format!("{}/transaction?id={}", self.base_url, tx_id);
243 let resp = self.http.get(&url).send().await?;
244 if !resp.status().is_success() {
245 let status = resp.status().as_u16();
246 let body = resp.text().await.unwrap_or_default();
247 return Err(RelayerError::Api { status, message: body });
248 }
249 let text = resp.text().await?;
250 debug!(raw_response = %text, "Relayer get_transaction response");
251
252 let data = parse_relayer_response(&text)?;
253 let state = parse_tx_state(&data.state);
254
255 let error = if state == TxState::Failed || state == TxState::Invalid {
257 extract_error_from_response(&text)
258 } else {
259 None
260 };
261
262 Ok(TxResult {
263 state,
264 tx_hash: data.transaction_hash.or(data.hash),
265 proxy_address: None,
266 error,
267 })
268 }
269
270 pub async fn deploy(&self) -> Result<TxResult> {
272 if self.tx_type != RelayerTxType::Safe {
273 return Err(RelayerError::Other(
274 "deploy() is only for Safe wallet type".to_string(),
275 ));
276 }
277
278 if self.is_deployed().await? {
279 let wallet = self.wallet_address()?;
280 return Err(RelayerError::WalletAlreadyDeployed(format!("{:?}", wallet)));
281 }
282
283 let safe_address = self.wallet_address()?;
284 let (signature, params) =
285 create::build_create_transaction(self.signer.as_ref(), self.chain_id).await?;
286
287 let request = TransactionRequest {
288 tx_type: "SAFE-CREATE".to_string(),
289 from: format!("{:?}", self.signer.address()),
290 to: contracts::SAFE_FACTORY.to_string(),
291 proxy_wallet: Some(format!("{:?}", safe_address)),
292 data: Some("0x".to_string()),
293 signature: Some(signature),
294 nonce: None,
295 signature_params: Some(
296 serde_json::to_value(¶ms).map_err(|e| RelayerError::Abi(e.to_string()))?,
297 ),
298 metadata: Some("Deploy Safe wallet".to_string()),
299 value: Some("0".to_string()),
300 deposit_wallet_params: None,
301 };
302
303 let response = self.submit(request).await?;
304 info!(tx_id = %response.transaction_id, "Safe deploy submitted");
305
306 let result = self.wait_for_tx(&response.transaction_id).await?;
307 Ok(TxResult {
308 proxy_address: Some(format!("{:?}", safe_address)),
309 ..result
310 })
311 }
312
313 pub async fn execute(
315 &self,
316 txs: Vec<Transaction>,
317 description: &str,
318 ) -> Result<TransactionResponseHandle> {
319 if txs.is_empty() {
320 return Err(RelayerError::Other("No transactions to execute".to_string()));
321 }
322
323 let request = match self.tx_type {
324 RelayerTxType::Eoa => {
325 return Err(RelayerError::Other(
326 "EOA wallets cannot use the gasless relayer — send transactions directly".to_string(),
327 ));
328 }
329 RelayerTxType::Safe => self.build_safe_request(&txs, description).await?,
330 RelayerTxType::Proxy => self.build_proxy_request(&txs, description).await?,
331 };
332
333 let response = self.submit(request).await?;
334 info!(tx_id = %response.transaction_id, description, "Transaction submitted");
335
336 Ok(TransactionResponseHandle {
337 tx_id: response.transaction_id,
338 client: self.clone(),
339 })
340 }
341
342 pub async fn execute_sequential(
355 &self,
356 batches: Vec<Vec<Transaction>>,
357 delay: Option<Duration>,
358 on_progress: Option<&dyn Fn(usize, usize)>,
359 ) -> Result<Vec<TxResult>> {
360 let delay = delay.unwrap_or(Duration::from_secs(5));
361 let total = batches.len();
362 let mut results = Vec::with_capacity(total);
363
364 for (i, txs) in batches.into_iter().enumerate() {
365 let desc = format!("Batch {}/{}", i + 1, total);
366 info!(batch = i + 1, total, "Submitting sequential batch");
367
368 let handle = self.execute(txs, &desc).await?;
369 let result = handle.wait().await?;
370 results.push(result);
371
372 if let Some(cb) = on_progress {
373 cb(i + 1, total);
374 }
375
376 if i + 1 < total {
378 debug!(delay_secs = delay.as_secs(), "Waiting between batches");
379 sleep(delay).await;
380 }
381 }
382
383 Ok(results)
384 }
385
386 pub async fn execute_batch(
397 &self,
398 txs: Vec<Transaction>,
399 description: &str,
400 ) -> Result<TxResult> {
401 if txs.is_empty() {
402 return Err(RelayerError::Other("No transactions to batch".to_string()));
403 }
404
405 info!(count = txs.len(), description, "Submitting batch transaction");
406
407 let request = match self.tx_type {
408 RelayerTxType::Eoa => {
409 return Err(RelayerError::Other(
410 "EOA wallets cannot use the gasless relayer".to_string(),
411 ));
412 }
413 RelayerTxType::Safe => {
414 self.build_safe_request(&txs, description).await?
416 }
417 RelayerTxType::Proxy => {
418 self.build_proxy_request(&txs, description).await?
420 }
421 };
422
423 let response = self.submit(request).await?;
424 info!(tx_id = %response.transaction_id, description, "Batch submitted");
425
426 self.wait_for_tx(&response.transaction_id).await
427 }
428
429
430
431 async fn build_safe_request(
433 &self,
434 txs: &[Transaction],
435 metadata: &str,
436 ) -> Result<TransactionRequest> {
437 let safe_address = self.wallet_address()?;
438
439 let nonce = self.get_nonce().await?;
443
444 let (data, to, signature, sig_params) = safe::build_safe_transaction(
445 self.signer.as_ref(),
446 self.chain_id,
447 safe_address,
448 txs,
449 nonce,
450 )
451 .await?;
452
453 Ok(TransactionRequest {
454 tx_type: "SAFE".to_string(),
455 from: format!("{:?}", self.signer.address()),
456 to: format!("{:?}", to),
457 proxy_wallet: Some(format!("{:?}", safe_address)),
458 data: Some(data),
459 signature: Some(signature),
460 nonce: Some(nonce.to_string()),
461 signature_params: Some(
462 serde_json::to_value(&sig_params)
463 .map_err(|e| RelayerError::Abi(e.to_string()))?,
464 ),
465 metadata: Some(metadata.to_string()),
466 value: Some("0".to_string()),
467 deposit_wallet_params: None,
468 })
469 }
470
471 async fn build_proxy_request(
473 &self,
474 txs: &[Transaction],
475 metadata: &str,
476 ) -> Result<TransactionRequest> {
477 let proxy_address = self.wallet_address()?;
478 let relay_payload = self.get_relay_payload().await?;
479
480 let extra = txs.len().saturating_sub(1) as u64;
482 let gas_limit = (DEFAULT_PROXY_GAS_LIMIT + extra * 80_000).min(400_000);
483 debug!(gas_limit, tx_count = txs.len(), "Dynamic proxy gas limit");
484
485 let (data, signature, sig_params) = proxy::build_proxy_transaction(
486 self.signer.as_ref(),
487 self.signer.address(),
488 txs,
489 &relay_payload,
490 gas_limit,
491 )
492 .await?;
493
494 Ok(TransactionRequest {
495 tx_type: "PROXY".to_string(),
496 from: format!("{:?}", self.signer.address()),
497 to: contracts::PROXY_FACTORY.to_string(),
498 proxy_wallet: Some(format!("{:?}", proxy_address)),
499 data: Some(data),
500 signature: Some(signature),
501 nonce: Some(relay_payload.nonce),
502 signature_params: Some(
503 serde_json::to_value(&sig_params)
504 .map_err(|e| RelayerError::Abi(e.to_string()))?,
505 ),
506 metadata: Some(metadata.to_string()),
507 value: Some("0".to_string()),
508 deposit_wallet_params: None,
509 })
510 }
511
512 async fn submit(&self, request: TransactionRequest) -> Result<RelayerTransactionResponse> {
514 let url = format!("{}/submit", self.base_url);
515 let body = serde_json::to_string(&request)
516 .map_err(|e| RelayerError::Abi(e.to_string()))?;
517
518 debug!(url = %url, body_len = body.len(), "Submitting to relayer");
519
520 let auth_headers = self.auth.headers("POST", "/submit", &body)?;
521
522 debug!(
523 headers = ?auth_headers.keys().map(|k| k.as_str()).collect::<Vec<_>>(),
524 "Auth headers"
525 );
526
527 let resp = self
528 .http
529 .post(&url)
530 .headers(auth_headers)
531 .header("Content-Type", "application/json")
532 .body(body)
533 .send()
534 .await?;
535
536 if !resp.status().is_success() {
537 let status = resp.status().as_u16();
538 let err = resp.text().await.unwrap_or_default();
539 if status == 429 {
540 return Err(RelayerError::QuotaExhausted);
541 }
542 return Err(RelayerError::Api { status, message: err });
543 }
544
545 let text = resp.text().await?;
546 debug!(raw_response = %text, "Relayer submit response");
547
548 parse_relayer_response(&text)
549 }
550
551 async fn wait_for_tx(&self, tx_id: &str) -> Result<TxResult> {
553 for attempt in 0..MAX_POLL_ATTEMPTS {
554 sleep(POLL_INTERVAL).await;
555 let result = self.get_transaction(tx_id).await?;
556 debug!(attempt, state = ?result.state, tx_id, "Polling transaction");
557
558 if result.state.is_terminal() {
559 let tx_hash_str = result.tx_hash.as_deref().unwrap_or("no hash");
560 let error_str = result.error.as_deref().unwrap_or("no details");
561 if result.state == TxState::Failed {
562 return Err(RelayerError::TransactionFailed(format!(
563 "Transaction {} failed | tx: {} | reason: {}",
564 tx_id, tx_hash_str, error_str
565 )));
566 }
567 if result.state == TxState::Invalid {
568 return Err(RelayerError::TransactionInvalid(format!(
569 "Transaction {} rejected | tx: {} | reason: {}",
570 tx_id, tx_hash_str, error_str
571 )));
572 }
573 return Ok(result);
574 }
575 }
576 Err(RelayerError::Timeout)
577 }
578
579 pub async fn approve_usdc_for_ctf(&self) -> Result<TransactionResponseHandle> {
583 let tx = crate::operations::approve_usdc_for_ctf_exchange();
584 self.execute(vec![tx], "Approve USDC for CTF Exchange").await
585 }
586
587 pub async fn approve_usdc_for_negrisk(&self) -> Result<TransactionResponseHandle> {
589 let tx = crate::operations::approve_usdc_for_neg_risk_exchange();
590 self.execute(vec![tx], "Approve USDC for NegRisk Exchange").await
591 }
592
593 pub async fn approve_ctf_for_exchange(&self) -> Result<TransactionResponseHandle> {
595 let tx = crate::operations::approve_ctf_for_ctf_exchange();
596 self.execute(vec![tx], "Approve CTF for Exchange").await
597 }
598
599 pub async fn setup_approvals(&self) -> Result<TransactionResponseHandle> {
601 let txs = vec![
602 crate::operations::approve_usdc_for_ctf_exchange(),
603 crate::operations::approve_usdc_for_neg_risk_exchange(),
604 crate::operations::approve_ctf_for_ctf_exchange(),
605 crate::operations::approve_ctf_for_neg_risk_exchange(),
606 crate::operations::approve_ctf_for_neg_risk_adapter(),
607 ];
608 self.execute(txs, "Setup all approvals").await
609 }
610
611 pub fn derive_deposit_wallet_address(&self) -> Result<ethers::types::Address> {
617 derive::derive_deposit_wallet_address_for_chain(self.signer.address(), self.chain_id)
618 }
619
620 pub async fn deploy_deposit_wallet(&self) -> Result<TxResult> {
628 let owner = self.signer.address();
629 let request = deposit_wallet::build_create_request(owner);
630 let response = self.submit(request).await?;
631 info!(tx_id = %response.transaction_id, "Deposit wallet deploy submitted");
632 self.wait_for_tx(&response.transaction_id).await
633 }
634
635 pub async fn is_deposit_wallet_deployed(&self) -> Result<bool> {
638 let wallet = self.derive_deposit_wallet_address()?;
639 let url = format!("{}/deployed?address={:?}&type=WALLET", self.base_url, wallet);
640 let resp = self.http.get(&url).send().await?;
641 if !resp.status().is_success() {
642 let status = resp.status().as_u16();
643 let body = resp.text().await.unwrap_or_default();
644 return Err(RelayerError::Api { status, message: body });
645 }
646 let text = resp.text().await?;
647 let body: serde_json::Value = serde_json::from_str(&text)
648 .map_err(|e| RelayerError::Other(format!("Parse Error on {}: {}", text, e)))?;
649 Ok(body
650 .as_bool()
651 .or_else(|| body.as_str().map(|s| s == "true"))
652 .or_else(|| body.get("deployed").and_then(|v| v.as_bool()))
653 .unwrap_or(false))
654 }
655
656 pub async fn get_deposit_wallet_nonce(&self) -> Result<u64> {
658 let url = format!(
659 "{}/nonce?address={:?}&type=WALLET",
660 self.base_url,
661 self.signer.address()
662 );
663 let resp = self.http.get(&url).send().await?;
664 if !resp.status().is_success() {
665 let status = resp.status().as_u16();
666 let body = resp.text().await.unwrap_or_default();
667 return Err(RelayerError::Api { status, message: body });
668 }
669 let text = resp.text().await?;
670 debug!(raw_response = %text, "Deposit wallet nonce response");
671 let body: serde_json::Value = serde_json::from_str(&text)
672 .map_err(|e| RelayerError::Other(format!("Nonce parse error on {}: {}", text, e)))?;
673 let nonce = body
674 .as_u64()
675 .or_else(|| body.get("nonce").and_then(|v| v.as_u64()))
676 .or_else(|| {
677 body.get("nonce")
678 .and_then(|v| v.as_str())
679 .and_then(|s| s.parse().ok())
680 })
681 .or_else(|| body.as_str().and_then(|s| s.parse().ok()))
682 .ok_or_else(|| RelayerError::Other(format!("No nonce in response: {text}")))?;
683 Ok(nonce)
684 }
685
686 pub async fn execute_deposit_wallet_batch(
702 &self,
703 calls: Vec<DepositWalletCall>,
704 deposit_wallet_addr: Option<ethers::types::Address>,
705 deadline: u64,
706 metadata: Option<&str>,
707 ) -> Result<TransactionResponseHandle> {
708 if calls.is_empty() {
709 return Err(RelayerError::Other(
710 "No calls to execute in deposit wallet batch".to_string(),
711 ));
712 }
713
714 let owner = self.signer.address();
715 let wallet_addr = match deposit_wallet_addr {
716 Some(w) => w,
717 None => self.derive_deposit_wallet_address()?,
718 };
719
720 let nonce = self.get_deposit_wallet_nonce().await?;
721
722 let request = deposit_wallet::build_batch_request(
723 self.signer.as_ref(),
724 self.chain_id,
725 owner,
726 wallet_addr,
727 nonce,
728 deadline,
729 calls,
730 metadata.map(|s| s.to_string()),
731 )?;
732
733 let response = self.submit(request).await?;
734 info!(
735 tx_id = %response.transaction_id,
736 wallet = ?wallet_addr,
737 nonce,
738 "Deposit wallet batch submitted"
739 );
740
741 Ok(TransactionResponseHandle {
742 tx_id: response.transaction_id,
743 client: self.clone(),
744 })
745 }
746
747 pub async fn setup_approvals_v2(&self) -> Result<TransactionResponseHandle> {
762 let txs = vec![
763 crate::operations::approve_pusd_for_ctf_exchange_v2(),
764 crate::operations::approve_pusd_for_neg_risk_exchange_v2(),
765 crate::operations::approve_pusd_for_ctf_adapter(),
766 crate::operations::approve_pusd_for_neg_risk_ctf_adapter(),
767 crate::operations::approve_ctf_for_ctf_exchange_v2(),
768 crate::operations::approve_ctf_for_neg_risk_exchange_v2(),
769 crate::operations::approve_ctf_for_ctf_adapter(),
770 crate::operations::approve_ctf_for_neg_risk_ctf_adapter(),
771 crate::operations::approve_ctf_for_neg_risk_adapter(),
772 ];
773 self.execute(txs, "Setup V2 approvals (pUSD)").await
774 }
775}
776
777pub struct TransactionResponseHandle {
779 pub tx_id: String,
780 client: RelayClient,
781}
782
783impl TransactionResponseHandle {
784 pub async fn wait(self) -> Result<TxResult> {
786 self.client.wait_for_tx(&self.tx_id).await
787 }
788
789 pub fn id(&self) -> &str {
791 &self.tx_id
792 }
793}
794
795fn parse_relayer_response(text: &str) -> Result<RelayerTransactionResponse> {
800 if let Ok(resp) = serde_json::from_str::<RelayerTransactionResponse>(text) {
802 return Ok(resp);
803 }
804
805 if let Ok(value) = serde_json::from_str::<serde_json::Value>(text) {
807 if let Some(first) = value.as_array().and_then(|a| a.first()) {
809 if let Ok(resp) = serde_json::from_value::<RelayerTransactionResponse>(first.clone()) {
810 warn!("Relayer returned JSON array; extracted first element");
811 return Ok(resp);
812 }
813 return parse_relayer_value(first);
815 }
816
817 return parse_relayer_value(&value);
818 }
819
820 Err(RelayerError::Other(format!(
821 "Failed to parse relayer response: {}", text
822 )))
823}
824
825fn parse_relayer_value(value: &serde_json::Value) -> Result<RelayerTransactionResponse> {
827 for key in &["data", "result", "transaction"] {
829 if let Some(inner) = value.get(key) {
830 if let Ok(resp) = serde_json::from_value::<RelayerTransactionResponse>(inner.clone()) {
831 warn!(wrapper_key = key, "Relayer returned wrapped response");
832 return Ok(resp);
833 }
834 }
835 }
836
837 let tx_id = value.get("transactionId")
839 .or_else(|| value.get("transactionID"))
840 .and_then(|v| v.as_str())
841 .map(|s| s.to_string());
842
843 if let Some(id) = tx_id {
844 let state = value.get("state")
845 .and_then(|v| v.as_str())
846 .unwrap_or("NEW")
847 .to_string();
848 let hash = value.get("hash")
849 .and_then(|v| v.as_str())
850 .map(|s| s.to_string());
851 let transaction_hash = value.get("transactionHash")
852 .and_then(|v| v.as_str())
853 .map(|s| s.to_string());
854
855 warn!("Relayer response required manual field extraction");
856 return Ok(RelayerTransactionResponse {
857 transaction_id: id,
858 state,
859 hash,
860 transaction_hash,
861 });
862 }
863
864 Err(RelayerError::Other(format!(
865 "Value is not a valid relayer response: {}", value
866 )))
867}
868
869fn parse_tx_state(s: &str) -> TxState {
875 let normalized = s.to_uppercase();
877 let key = normalized.strip_prefix("STATE_").unwrap_or(&normalized);
878 match key {
879 "NEW" => TxState::New,
880 "EXECUTED" => TxState::Executed,
881 "MINED" => TxState::Mined,
882 "CONFIRMED" => TxState::Confirmed,
883 "FAILED" => TxState::Failed,
884 "INVALID" => TxState::Invalid,
885 _ => {
886 warn!(raw_state = s, "Unknown transaction state, treating as New");
887 TxState::New
888 }
889 }
890}
891
892fn extract_error_from_response(text: &str) -> Option<String> {
896 let value: serde_json::Value = serde_json::from_str(text).ok()?;
897
898 let obj = if let Some(first) = value.as_array().and_then(|a| a.first()) {
900 first
901 } else {
902 &value
903 };
904
905 for key in &["errorMsg", "error", "reason", "failureReason", "revertReason", "message", "statusMessage"] {
907 if let Some(v) = obj.get(key) {
908 let s = if let Some(s) = v.as_str() {
909 s.to_string()
910 } else {
911 v.to_string()
912 };
913 if !s.is_empty() && s != "\"\"" && s != "null" {
914 return Some(s);
915 }
916 }
917 }
918
919 if let Some(meta) = obj.get("derivedMetadata") {
921 for key in &["error", "reason", "revertReason"] {
922 if let Some(v) = meta.get(key) {
923 if let Some(s) = v.as_str() {
924 if !s.is_empty() {
925 return Some(s.to_string());
926 }
927 }
928 }
929 }
930 }
931
932 None
933}