Skip to main content

polymarket_relayer/
client.rs

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
12/// Gas limit for Proxy transactions via Relay Hub.
13///
14/// The Relay Hub requires `gasleft() >= gasLimit` before calling the inner
15/// function. The relayer bot has a fixed gas budget per transaction (~200-300K).
16/// Setting this too high causes "Not enough gasleft()" reverts.
17///
18/// A single redeemPositions uses ~100-150K gas. Set low to stay within
19/// the relayer bot's budget. The Polymarket frontend uses ~150K.
20const DEFAULT_PROXY_GAS_LIMIT: u64 = 200_000;
21const POLL_INTERVAL: Duration = Duration::from_secs(2);
22const MAX_POLL_ATTEMPTS: u32 = 100;
23
24/// Client for interacting with the Polymarket Builder Relayer.
25#[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    /// Optional RPC URL for reading nonce on-chain (recommended for Safe wallets).
34    rpc_url: Option<String>,
35}
36
37impl RelayClient {
38    /// Create a new RelayClient.
39    ///
40    /// # Arguments
41    /// * `chain_id` - Chain ID (137 for Polygon mainnet)
42    /// * `signer` - Ethers LocalWallet for signing transactions
43    /// * `auth` - Authentication method (Builder or RelayerKey)
44    /// * `tx_type` - Wallet type (Safe or Proxy)
45    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    /// Set a custom relayer URL.
67    pub fn set_url(&mut self, url: String) {
68        self.base_url = url.trim_end_matches('/').to_string();
69    }
70
71    /// Set an RPC URL for reading the Safe nonce on-chain.
72    ///
73    /// **Highly recommended** — the relayer API `/nonce` endpoint can return
74    /// stale values (e.g., 0), causing GS026 "Invalid owner" errors because
75    /// the EIP-712 hash is computed with the wrong nonce.
76    ///
77    /// When set, `get_nonce()` reads the nonce directly from the Safe contract
78    /// on-chain, falling back to the relayer API only on failure.
79    pub fn set_rpc_url(&mut self, url: String) {
80        self.rpc_url = Some(url);
81    }
82
83    /// Get the signer's EOA address.
84    pub fn signer_address(&self) -> ethers::types::Address {
85        self.signer.address()
86    }
87
88    /// Get the derived wallet address (Safe, Proxy, or EOA).
89    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    /// Check if the Safe wallet is deployed.
98    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        // Handle multiple response formats:
111        //   true / false                → bare bool
112        //   "true" / "false"            → string
113        //   {"deployed": true}          → object
114        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    /// Get the current nonce for the wallet.
121    ///
122    /// For Safe wallets: reads from on-chain `nonce()` if `rpc_url` is set,
123    /// otherwise falls back to the relayer API. The relayer API is known to
124    /// return stale nonces (e.g., 0) which causes GS026 errors.
125    pub async fn get_nonce(&self) -> Result<u64> {
126        // For Safe wallets, prefer on-chain nonce if RPC URL is available
127        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        // Fallback: relayer API
142        let nonce = self.get_nonce_from_relayer().await?;
143        debug!(nonce, source = "relayer-api", "Nonce");
144        Ok(nonce)
145    }
146
147    /// Read Safe nonce directly from on-chain via JSON-RPC eth_call.
148    async fn read_safe_nonce_onchain(&self, rpc_url: &str) -> Result<u64> {
149        let safe_address = self.wallet_address()?;
150
151        // nonce() selector = keccak256("nonce()")[..4]
152        let selector = &ethers::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        // Check for JSON-RPC error
180        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        // Parse hex result → u64
190        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    /// Get nonce from the relayer API (may be stale for Safe wallets).
198    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    /// Get relay payload (for Proxy transactions).
224    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    /// Get a transaction's status by ID.
241    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        // Extract error details from the raw response for failed transactions
256        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    /// Deploy a Safe wallet (one-time, Safe wallet type only).
271    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(&params).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    /// Execute one or more transactions through the relayer.
314    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    /// Execute multiple groups of transactions sequentially, waiting for each
343    /// to confirm before submitting the next.
344    ///
345    /// This avoids nonce collisions when the relay bot (Gelato) cannot handle
346    /// back-to-back requests for the same proxy wallet.
347    ///
348    /// # Arguments
349    /// * `batches` - Each inner `Vec<Transaction>` is submitted as one relay request.
350    /// * `delay` - Wait time between confirmed batches (default 5s if `None`).
351    /// * `on_progress` - Optional callback `(completed, total)` after each batch confirms.
352    ///
353    /// Returns a vec of `TxResult`, one per batch (in order).
354    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            // Delay between batches (skip after the last one)
377            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    /// Execute multiple transactions as a single batched relay request.
387    ///
388    /// All transactions share one nonce and one relay call, completely avoiding
389    /// nonce collisions. More gas-efficient than sequential execution.
390    ///
391    /// For Proxy wallets, the gas limit scales with the number of transactions:
392    ///   `gas_limit = 150_000 + (extra_txs * 80_000)`, capped at 400_000.
393    ///
394    /// For Safe wallets, multiple transactions are packed via multiSend
395    /// (already handled by `build_safe_request`).
396    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                // Safe already supports multisend natively
415                self.build_safe_request(&txs, description).await?
416            }
417            RelayerTxType::Proxy => {
418                // build_proxy_request now scales gas limit internally based on tx count.
419                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    /// Build a Safe transaction request with full EIP-712 signing.
432    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        // Don't block on is_deployed() — the relayer will reject if not deployed.
440        // This matches the Python SDK behavior.
441
442        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    /// Build a Proxy transaction request with dynamic gas limit scaling.
472    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        // Scale gas limit for multiple operations
481        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    /// Submit a transaction request to the relayer.
513    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    /// Poll for transaction confirmation.
552    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    // ── Convenience methods ──
580
581    /// Approve USDC.e for CTF Exchange.
582    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    /// Approve USDC.e for Neg Risk CTF Exchange.
588    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    /// Approve CTF tokens (ERC1155) for CTF Exchange.
594    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    /// Set up all standard approvals in a single batch.
600    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    // ── V2 Deposit Wallet (POLY_1271) ───────────────────────────────
612
613    /// Predict the V2 Deposit Wallet address for the current signer EOA.
614    ///
615    /// Pure CREATE2 derivation — does not query the relayer.
616    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    /// Deploy a Deposit Wallet via the relayer (gasless, type `WALLET-CREATE`).
621    ///
622    /// Idempotent on the relayer side: a second call for an already-deployed
623    /// wallet typically returns `Invalid`. Use [`is_deposit_wallet_deployed`]
624    /// to gate the call.
625    ///
626    /// [`is_deposit_wallet_deployed`]: Self::is_deposit_wallet_deployed
627    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    /// Check whether a Deposit Wallet is already deployed at the derived
636    /// address, using `GET /deployed?address=...&type=WALLET`.
637    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    /// Fetch the Deposit Wallet nonce from the relayer (`GET /nonce?type=WALLET`).
657    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    /// Execute a batch of calls through the user's Deposit Wallet (V2 flow).
687    ///
688    /// Mirrors the TypeScript SDK's `executeDepositWalletBatch`:
689    ///   1. Read the WALLET nonce from `/nonce?type=WALLET`.
690    ///   2. EIP-712-sign the `Batch{wallet, nonce, deadline, calls[]}` struct.
691    ///   3. POST `/submit` with `type: "WALLET"` and `depositWalletParams`.
692    ///
693    /// The Deposit Wallet must already be deployed (see
694    /// [`deploy_deposit_wallet`]). If `deposit_wallet` is `None` the address
695    /// is derived from the signer.
696    ///
697    /// `deadline` is a unix timestamp (seconds) after which the batch is
698    /// invalid. The TS examples typically use `now + 4 minutes`.
699    ///
700    /// [`deploy_deposit_wallet`]: Self::deploy_deposit_wallet
701    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    /// Set up all V2 approvals (pUSD + adapters + V2 exchanges) in one batch.
748    ///
749    /// After the V2 migration (2026-04-28) the user-facing collateral is pUSD,
750    /// so trading and split/merge require fresh approvals against the new
751    /// contracts. Includes:
752    ///   * pUSD → V2 CTF Exchange / V2 Neg Risk Exchange (for orderbook trades)
753    ///   * pUSD → CtfCollateralAdapter / NegRiskCtfCollateralAdapter (for split)
754    ///   * CTF (ERC-1155) → V2 exchanges + both collateral adapters (for trades / merge)
755    ///   * CTF (ERC-1155) → NegRiskAdapter (for neg-risk redeem; unchanged from V1)
756    ///
757    /// Note: for **Proxy** wallets, this is 9 inner calls and will exceed the
758    /// relayer's per-tx gas budget — call the individual approvals one by one
759    /// or use `execute_sequential` instead. Safe wallets pack these via
760    /// MultiSend and submit them as one transaction without issue.
761    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
777/// Handle for a submitted transaction, with polling support.
778pub struct TransactionResponseHandle {
779    pub tx_id: String,
780    client: RelayClient,
781}
782
783impl TransactionResponseHandle {
784    /// Poll the transaction until it reaches a terminal state.
785    pub async fn wait(self) -> Result<TxResult> {
786        self.client.wait_for_tx(&self.tx_id).await
787    }
788
789    /// Get the transaction ID.
790    pub fn id(&self) -> &str {
791        &self.tx_id
792    }
793}
794
795/// Parse a relayer response that may be either:
796///   - A flat `RelayerTransactionResponse` JSON object
797///   - A JSON array containing the response object (e.g. `[{"transactionId": "..."}]`)
798///   - A wrapper object with a nested transaction (e.g., `{"data": {...}}`)
799fn parse_relayer_response(text: &str) -> Result<RelayerTransactionResponse> {
800    // 1. Try direct deserialization
801    if let Ok(resp) = serde_json::from_str::<RelayerTransactionResponse>(text) {
802        return Ok(resp);
803    }
804
805    // 2. Try as a JSON value
806    if let Ok(value) = serde_json::from_str::<serde_json::Value>(text) {
807        // Handle JSON array (take first element)
808        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            // If it's an array of wrappers/partial objects, continue searching inside the first element
814            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
825/// Helper to parse a JSON value that might be a wrapped or partial relayer response.
826fn parse_relayer_value(value: &serde_json::Value) -> Result<RelayerTransactionResponse> {
827    // 1. Try common wrapper patterns: {"data": {...}}, {"result": {...}}, {"transaction": {...}}
828    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    // 2. Try extracting transactionId/transactionID from top-level (partial match)
838    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
869/// Parse a transaction state string from the relayer.
870///
871/// Handles both formats:
872///   - Plain: "NEW", "MINED", "CONFIRMED", "FAILED", "INVALID"
873///   - Prefixed: "STATE_NEW", "STATE_MINED", "STATE_CONFIRMED", etc.
874fn parse_tx_state(s: &str) -> TxState {
875    // Normalize: uppercase + strip "STATE_" prefix
876    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
892/// Extract error/reason from a raw relayer response JSON.
893///
894/// Looks for common error fields in the response or its array wrapper.
895fn extract_error_from_response(text: &str) -> Option<String> {
896    let value: serde_json::Value = serde_json::from_str(text).ok()?;
897
898    // If it's an array, look inside the first element
899    let obj = if let Some(first) = value.as_array().and_then(|a| a.first()) {
900        first
901    } else {
902        &value
903    };
904
905    // Try common error field names
906    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    // Try nested: derivedMetadata.error, etc.
920    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}