polymarket_sdk/
safe.rs

1//! Polymarket Relayer Module
2//!
3//! This module provides comprehensive Safe wallet deployment and management
4//! functionality for interacting with Polymarket's Relayer API.
5//!
6//! ## Features
7//!
8//! - **Safe Address Derivation**: Deterministic Safe address computation using CREATE2
9//! - **SafeCreate Signing**: EIP-712 typed data for Safe deployment
10//! - **RelayerClient**: Full client for deploying and managing Safe wallets
11//! - **Builder API Authentication**: HMAC-based authentication for Relayer API
12//!
13//! ## Example
14//!
15//! ```rust,ignore
16//! use polymarket_sdk::{RelayerClient, RelayerConfig, derive_safe_address};
17//!
18//! // Derive Safe address deterministically
19//! let owner = "0x1234...";
20//! let safe_address = derive_safe_address(owner)?;
21//!
22//! // Create client and deploy Safe
23//! let client = RelayerClient::new(RelayerConfig::default())?;
24//! let result = client.deploy_safe_with_signature(owner, &signature).await?;
25//! ```
26
27use std::collections::HashMap;
28use std::num::NonZeroU32;
29use std::sync::Arc;
30use std::time::Duration;
31
32use alloy_primitives::Signature as AlloySignature;
33use alloy_primitives::{hex, keccak256, Address, B256, U256};
34use alloy_provider::{Provider, ProviderBuilder};
35use base64::engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD};
36use base64::Engine;
37use governor::{Quota, RateLimiter as GovRateLimiter};
38use hmac::{Hmac, Mac};
39use reqwest::Client;
40use serde::{Deserialize, Serialize};
41use sha2::Sha256;
42use tracing::{debug, info, instrument, warn};
43
44use crate::core::{data_api_url, relayer_api_url};
45use crate::core::{PolymarketError, Result};
46
47type RateLimiter = GovRateLimiter<
48    governor::state::NotKeyed,
49    governor::state::InMemoryState,
50    governor::clock::DefaultClock,
51>;
52
53// ============================================================================
54// Constants
55// ============================================================================
56
57/// Safe Factory address on Polygon
58pub const SAFE_FACTORY: &str = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b";
59
60/// Safe init code hash for CREATE2 derivation
61pub const SAFE_INIT_CODE_HASH: &str =
62    "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
63
64/// Default Polygon RPC (used for on-chain checks, can be overridden by env `POLYGON_RPC_URL`)
65pub const DEFAULT_POLYGON_RPC: &str = "https://polygon-rpc.com";
66
67/// USDC contract address on Polygon (PoS bridged USDC.e)
68pub const USDC_CONTRACT_ADDRESS: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
69
70/// Native USDC contract address on Polygon (Circle's native USDC)
71pub const NATIVE_USDC_CONTRACT_ADDRESS: &str = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
72
73/// Polymarket ConditionalTokens (CTF) ERC1155 contract address on Polygon
74/// This holds the outcome tokens for prediction markets
75/// Reference: <https://polygonscan.com/address/0x4d97dcd97ec945f40cf65f87097ace5ea0476045>
76pub const CONDITIONAL_TOKENS_ADDRESS: &str = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
77
78/// Alias for backward compatibility
79pub const CTF_EXCHANGE_ADDRESS: &str = CONDITIONAL_TOKENS_ADDRESS;
80
81/// Polymarket Exchange contract address on Polygon (for standard markets)
82/// Reference: <https://polygonscan.com/address/0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e>
83pub const EXCHANGE_ADDRESS: &str = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E";
84
85/// Polymarket NegRisk CTF Exchange contract address on Polygon (for neg-risk markets)
86/// Reference: <https://polygonscan.com/address/0xc5d563a36ae78145c45a50134d48a1215220f80a>
87pub const NEG_RISK_CTF_EXCHANGE_ADDRESS: &str = "0xC5d563A36AE78145C45a50134d48A1215220f80a";
88
89/// Polymarket NegRiskAdapter contract address on Polygon
90/// Used for split/merge operations in neg-risk markets
91/// Reference: <https://polygonscan.com/address/0xd91e80cf2e7be2e162c6513ced06f1dd0da35296>
92pub const NEG_RISK_ADAPTER_ADDRESS: &str = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";
93
94/// CreateProxy EIP-712 type string (Polymarket SafeProxyFactory)
95/// Reference: https://polygonscan.com/address/0xaacfeea03eb1561c4e67d661e40682bd20e3541b
96const CREATE_PROXY_TYPE_STR: &str =
97    "CreateProxy(address paymentToken,uint256 payment,address paymentReceiver)";
98
99/// EIP712Domain type string for Polymarket SafeProxyFactory (includes name field)
100const DOMAIN_TYPE_STR: &str = "EIP712Domain(string name,uint256 chainId,address verifyingContract)";
101
102/// Domain name for Polymarket SafeProxyFactory
103const DOMAIN_NAME: &str = "Polymarket Contract Proxy Factory";
104
105/// Default chain ID for Polygon
106const DEFAULT_CHAIN_ID: u64 = 137;
107
108// ============================================================================
109// EIP-712 Digest Computation
110// ============================================================================
111
112/// Compute the EIP-712 digest for CreateProxy (Polymarket SafeProxyFactory)
113///
114/// This is used to verify signatures before sending to the Relayer.
115/// The digest matches the Polymarket SafeProxyFactory contract's expected format.
116fn compute_safe_create_digest_internal(_owner_address: &str, chain_id: u64) -> Result<B256> {
117    let factory_addr: Address = SAFE_FACTORY
118        .parse()
119        .map_err(|e| PolymarketError::validation(format!("Invalid factory address: {e}")))?;
120
121    let payment_token: Address = Address::ZERO;
122    let payment = U256::ZERO;
123    let payment_receiver: Address = Address::ZERO;
124
125    // Domain type hash
126    let domain_type_hash = keccak256(DOMAIN_TYPE_STR.as_bytes());
127
128    // CreateProxy type hash
129    let create_proxy_type_hash = keccak256(CREATE_PROXY_TYPE_STR.as_bytes());
130
131    // Name hash
132    let name_hash = keccak256(DOMAIN_NAME.as_bytes());
133
134    // Domain separator: keccak256(domainTypeHash || keccak256(name) || chainId || verifyingContract)
135    let mut domain_encoded = Vec::with_capacity(128);
136    domain_encoded.extend_from_slice(domain_type_hash.as_slice());
137    domain_encoded.extend_from_slice(name_hash.as_slice());
138    domain_encoded.extend_from_slice(&U256::from(chain_id).to_be_bytes::<32>());
139    let mut factory_bytes = [0u8; 32];
140    factory_bytes[12..].copy_from_slice(factory_addr.as_slice());
141    domain_encoded.extend_from_slice(&factory_bytes);
142    let domain_separator = keccak256(&domain_encoded);
143
144    // Struct hash: keccak256(typeHash || paymentToken || payment || paymentReceiver)
145    // Note: Polymarket's CreateProxy does NOT include owner or nonce
146    let mut struct_encoded = Vec::with_capacity(128);
147    struct_encoded.extend_from_slice(create_proxy_type_hash.as_slice());
148    let mut payment_token_bytes = [0u8; 32];
149    payment_token_bytes[12..].copy_from_slice(payment_token.as_slice());
150    struct_encoded.extend_from_slice(&payment_token_bytes);
151    struct_encoded.extend_from_slice(&payment.to_be_bytes::<32>());
152    let mut payment_receiver_bytes = [0u8; 32];
153    payment_receiver_bytes[12..].copy_from_slice(payment_receiver.as_slice());
154    struct_encoded.extend_from_slice(&payment_receiver_bytes);
155    let struct_hash = keccak256(&struct_encoded);
156
157    // Final digest: keccak256(0x1901 || domainSeparator || structHash)
158    let mut bytes = Vec::with_capacity(66);
159    bytes.push(0x19);
160    bytes.push(0x01);
161    bytes.extend_from_slice(domain_separator.as_slice());
162    bytes.extend_from_slice(struct_hash.as_slice());
163
164    Ok(keccak256(&bytes))
165}
166
167// ============================================================================
168// Types
169// ============================================================================
170
171/// Transaction type for Relayer API
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum TransactionType {
174    /// Standard Safe transaction
175    #[serde(rename = "SAFE")]
176    Safe,
177    /// Safe creation transaction
178    #[serde(rename = "SAFE-CREATE")]
179    SafeCreate,
180}
181
182/// Transaction state from Relayer API
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184pub enum TransactionState {
185    /// Transaction is new/pending
186    #[serde(rename = "STATE_NEW")]
187    New,
188    /// Transaction has been executed
189    #[serde(rename = "STATE_EXECUTED")]
190    Executed,
191    /// Transaction has been mined
192    #[serde(rename = "STATE_MINED")]
193    Mined,
194    /// Transaction is confirmed
195    #[serde(rename = "STATE_CONFIRMED")]
196    Confirmed,
197    /// Transaction failed
198    #[serde(rename = "STATE_FAILED")]
199    Failed,
200    /// Transaction is invalid
201    #[serde(rename = "STATE_INVALID")]
202    Invalid,
203}
204
205impl TransactionState {
206    /// Check if this is a terminal state
207    ///
208    /// For Polymarket Relayer, `Mined` is considered terminal because the transaction
209    /// has been included in a block. `Confirmed` indicates additional block confirmations.
210    #[must_use]
211    pub const fn is_terminal(&self) -> bool {
212        matches!(
213            self,
214            Self::Mined | Self::Confirmed | Self::Failed | Self::Invalid
215        )
216    }
217
218    /// Check if this is a success state
219    ///
220    /// Both `Mined` and `Confirmed` are considered successful.
221    /// - `Mined`: Transaction included in a block (sufficient for Safe deployment)
222    /// - `Confirmed`: Additional block confirmations received
223    #[must_use]
224    pub const fn is_success(&self) -> bool {
225        matches!(self, Self::Mined | Self::Confirmed)
226    }
227}
228
229// ====================================================================================
230// Manual debug helper (not part of library API)
231// ====================================================================================
232#[cfg(test)]
233mod manual_debug {
234    use super::*;
235
236    /// 手动从 .env 读取配置,调用 /transaction?id=... 打印回执
237    ///
238    /// 注意:这会访问网络,仅用于本地调试,不会在 CI 运行。
239    #[tokio::test]
240    async fn fetch_transaction_status_from_env() {
241        // 目标 tx_id(来自当前排查)
242        let tx_id = "019ad6a5-fe80-7b44-a075-2af31ea399dd";
243
244        // 用环境变量构建 relayer 客户端
245        let cfg = RelayerConfig::from_env();
246        let mut client = RelayerClient::new(cfg).expect("create relayer client");
247
248        // NOTE: 仅用于本地调试,按需求硬编码 Builder 凭据
249        let hardcoded_creds = BuilderApiCredentials::new(
250            "019acb98-c6b1-7bd3-b31a-a62881ee200e",
251            "IRYvSFDwdGcG67cmpXFoqV_l9vWmi8n40x0j5UwkSpA=",
252            "67e95965fca9af2eff7700c768e40406efad1610324fd94e5005be8300f63d10",
253        );
254        client = client.with_builder_credentials(hardcoded_creds);
255
256        // 调用 /transaction?id=...
257        let receipt = client
258            .get_transaction_status(tx_id)
259            .await
260            .expect("fetch transaction status");
261
262        eprintln!("=== Transaction Receipt ===\n{:#?}", receipt);
263    }
264}
265
266/// Transaction request for Relayer API
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct TransactionRequest {
269    /// Transaction type
270    #[serde(rename = "type")]
271    pub r#type: TransactionType,
272    /// From address (signer)
273    pub from: String,
274    /// To address (target contract)
275    pub to: String,
276    /// Proxy wallet address (for Safe transactions)
277    #[serde(skip_serializing_if = "Option::is_none", rename = "proxyWallet")]
278    pub proxy_wallet: Option<String>,
279    /// Transaction data
280    pub data: String,
281    /// Nonce (optional)
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub nonce: Option<String>,
284    /// Signature
285    pub signature: String,
286    /// Signature parameters
287    #[serde(rename = "signatureParams")]
288    pub signature_params: SignatureParams,
289    /// Optional metadata
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub metadata: Option<String>,
292}
293
294/// Signature parameters for transaction
295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296pub struct SignatureParams {
297    /// Payment token address (for SafeCreate)
298    #[serde(skip_serializing_if = "Option::is_none", rename = "paymentToken")]
299    pub payment_token: Option<String>,
300    /// Payment amount (for SafeCreate)
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub payment: Option<String>,
303    /// Payment receiver (for SafeCreate)
304    #[serde(skip_serializing_if = "Option::is_none", rename = "paymentReceiver")]
305    pub payment_receiver: Option<String>,
306    /// Operation type (0 = Call, 1 = DelegateCall) - for Safe transactions
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub operation: Option<String>,
309    /// Safe transaction gas - for Safe transactions
310    #[serde(skip_serializing_if = "Option::is_none", rename = "safeTxnGas")]
311    pub safe_tx_gas: Option<String>,
312    /// Base gas - for Safe transactions
313    #[serde(skip_serializing_if = "Option::is_none", rename = "baseGas")]
314    pub base_gas: Option<String>,
315    /// Gas price - for Safe transactions
316    #[serde(skip_serializing_if = "Option::is_none", rename = "gasPrice")]
317    pub gas_price: Option<String>,
318    /// Gas token - for Safe transactions
319    #[serde(skip_serializing_if = "Option::is_none", rename = "gasToken")]
320    pub gas_token: Option<String>,
321    /// Refund receiver - for Safe transactions
322    #[serde(skip_serializing_if = "Option::is_none", rename = "refundReceiver")]
323    pub refund_receiver: Option<String>,
324}
325
326/// Transaction receipt from Relayer API
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct TransactionReceipt {
329    /// Transaction ID (Polymarket uses "transactionID", we alias it)
330    #[serde(
331        alias = "transactionID",
332        alias = "transactionId",
333        alias = "transaction_id",
334        alias = "id"
335    )]
336    pub id: String,
337    /// Transaction state
338    #[serde(alias = "status")]
339    pub state: TransactionState,
340    /// Transaction hash (if submitted)
341    #[serde(
342        alias = "transactionHash",
343        alias = "txHash",
344        skip_serializing_if = "Option::is_none"
345    )]
346    pub transaction_hash: Option<String>,
347    /// Some relayer responses include `hash` field (alias of transaction_hash)
348    #[serde(alias = "hash", skip_serializing_if = "Option::is_none")]
349    pub hash: Option<String>,
350    /// Proxy address (for Safe creation)
351    #[serde(alias = "proxyWallet", skip_serializing_if = "Option::is_none")]
352    pub proxy_address: Option<String>,
353    /// From / to (for debugging / compatibility with builder-relayer types)
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub from: Option<String>,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub to: Option<String>,
358    /// Error message (if failed)
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub error: Option<String>,
361    /// Created timestamp
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub created_at: Option<String>,
364    /// Updated timestamp
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub updated_at: Option<String>,
367}
368
369/// Builder API credentials for authenticated requests
370#[derive(Debug, Clone)]
371pub struct BuilderApiCredentials {
372    /// API key
373    pub api_key: String,
374    /// API secret (base64 encoded)
375    pub secret: String,
376    /// Passphrase
377    pub passphrase: String,
378}
379
380impl BuilderApiCredentials {
381    /// Create new credentials
382    #[must_use]
383    pub fn new(
384        api_key: impl Into<String>,
385        secret: impl Into<String>,
386        passphrase: impl Into<String>,
387    ) -> Self {
388        Self {
389            api_key: api_key.into(),
390            secret: secret.into(),
391            passphrase: passphrase.into(),
392        }
393    }
394
395    /// Load from environment variables
396    ///
397    /// Expected env vars:
398    /// - `POLY_BUILDER_API_KEY`
399    /// - `POLY_BUILDER_SECRET`
400    /// - `POLY_BUILDER_PASSPHRASE`
401    pub fn from_env() -> std::result::Result<Self, std::env::VarError> {
402        Ok(Self {
403            api_key: std::env::var("POLY_BUILDER_API_KEY")?,
404            secret: std::env::var("POLY_BUILDER_SECRET")?,
405            passphrase: std::env::var("POLY_BUILDER_PASSPHRASE")?,
406        })
407    }
408}
409
410/// Nonce type for Relayer API
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "lowercase")]
413pub enum NonceType {
414    /// Standard transaction nonce
415    Transaction,
416    /// Safe creation nonce
417    SafeCreate,
418}
419
420// ============================================================================
421// Safe Address Derivation
422// ============================================================================
423
424/// Derive Safe address from owner address using CREATE2
425///
426/// The Safe address is deterministically computed based on:
427/// - Factory address
428/// - Salt (keccak256 of owner address)
429/// - Init code hash
430///
431/// This means the same owner will always produce the same Safe address.
432///
433/// # Arguments
434/// * `owner` - The owner's wallet address (0x prefixed hex string)
435///
436/// # Returns
437/// The derived Safe address as a 0x prefixed hex string
438///
439/// # Example
440///
441/// ```rust,ignore
442/// use polymarket_sdk::derive_safe_address;
443///
444/// let owner = "0x1234567890123456789012345678901234567890";
445/// let safe_address = derive_safe_address(owner)?;
446/// println!("Safe address: {}", safe_address);
447/// ```
448pub fn derive_safe_address(owner: &str) -> Result<String> {
449    derive_safe_address_with_factory(owner, SAFE_FACTORY)
450}
451
452/// Derive Safe address with custom factory
453///
454/// # Arguments
455/// * `owner` - The owner's wallet address
456/// * `factory` - The Safe factory address
457pub fn derive_safe_address_with_factory(owner: &str, factory: &str) -> Result<String> {
458    let factory_addr: Address = factory
459        .parse()
460        .map_err(|e| PolymarketError::validation(format!("Invalid factory address: {e}")))?;
461
462    let owner_addr: Address = owner
463        .parse()
464        .map_err(|e| PolymarketError::validation(format!("Invalid owner address: {e}")))?;
465
466    let init_code_hash: B256 = SAFE_INIT_CODE_HASH
467        .parse()
468        .map_err(|e| PolymarketError::validation(format!("Invalid init code hash: {e}")))?;
469
470    // Compute salt = keccak256(abi.encode(owner))
471    let mut salt_input = [0u8; 32];
472    salt_input[12..32].copy_from_slice(owner_addr.as_slice());
473    let salt = keccak256(salt_input);
474
475    // CREATE2 address computation
476    let safe_addr = compute_create2_address(factory_addr, salt, init_code_hash);
477
478    Ok(format!("{safe_addr:?}"))
479}
480
481/// Compute CREATE2 address
482fn compute_create2_address(deployer: Address, salt: B256, init_code_hash: B256) -> Address {
483    let mut bytes = Vec::with_capacity(1 + 20 + 32 + 32);
484    bytes.push(0xff);
485    bytes.extend_from_slice(deployer.as_slice());
486    bytes.extend_from_slice(salt.as_slice());
487    bytes.extend_from_slice(init_code_hash.as_slice());
488
489    let hash = keccak256(&bytes);
490    Address::from_slice(&hash[12..])
491}
492
493// ============================================================================
494// Signature Utilities
495// ============================================================================
496
497/// Pack an ECDSA signature for the Relayer API
498///
499/// The Relayer API expects signatures with transformed v values:
500/// - v=0 or v=1 → v=31 or v=32
501/// - v=27 or v=28 → v=31 or v=32
502///
503/// Input format: `0x{r(32)}{s(32)}{v(1)}` (65 bytes hex = 130 chars + 0x)
504/// Output format: `0x{r(32)}{s(32)}{v(1)}` with transformed v
505///
506/// # Arguments
507/// * `signature` - The original ECDSA signature (hex with 0x prefix)
508///
509/// # Returns
510/// The packed signature ready for Relayer API
511pub fn pack_signature(signature: &str) -> Result<String> {
512    let sig = signature.trim_start_matches("0x");
513
514    debug!(sig_len = sig.len(), "Packing signature");
515
516    if sig.len() < 130 {
517        return Err(PolymarketError::validation(format!(
518            "Signature too short: {} chars, expected at least 130",
519            sig.len()
520        )));
521    }
522
523    let r_hex = &sig[0..64];
524    let s_hex = &sig[64..128];
525    let v_hex = &sig[128..130];
526
527    let original_v = u8::from_str_radix(v_hex, 16)
528        .map_err(|e| PolymarketError::validation(format!("Invalid v value in signature: {e}")))?;
529
530    let mut v = original_v;
531
532    // Transform v value for Polymarket Relayer API
533    //
534    // The Polymarket SafeProxyFactory uses OpenZeppelin's ECDSA.recover which expects
535    // standard Ethereum v values (27/28). Privy's sign_secp256k1 returns raw recovery id (0/1).
536    //
537    // For raw recovery ids (0/1), we convert to standard Ethereum format (27/28).
538    // For v=27/28, we keep as-is.
539    match v {
540        0 | 1 => v += 27, // 0/1 → 27/28 (raw k256 recovery id → standard Ethereum)
541        27 | 28 => {}     // Standard Ethereum format, keep as-is
542        _ => {
543            warn!(v = v, "Unexpected v value in signature, using as-is");
544        }
545    }
546
547    debug!(
548        original_v = original_v,
549        transformed_v = v,
550        v_hex = %v_hex,
551        r_hex_prefix = %&r_hex[0..8],
552        s_hex_prefix = %&s_hex[0..8],
553        "Signature v value transformation"
554    );
555
556    // Parse r and s as U256 for proper padding
557    let r = U256::from_str_radix(r_hex, 16)
558        .map_err(|e| PolymarketError::validation(format!("Invalid r value in signature: {e}")))?;
559    let s = U256::from_str_radix(s_hex, 16)
560        .map_err(|e| PolymarketError::validation(format!("Invalid s value in signature: {e}")))?;
561
562    // Pack as 32-byte r, 32-byte s, 1-byte v
563    let mut packed = Vec::with_capacity(65);
564    packed.extend_from_slice(&r.to_be_bytes::<32>());
565    packed.extend_from_slice(&s.to_be_bytes::<32>());
566    packed.push(v);
567
568    let packed_hex = format!("0x{}", hex::encode(&packed));
569
570    // Log full details for debugging
571    debug!(
572        original_sig = %signature,
573        packed_sig = %packed_hex,
574        packed_len = packed.len(),
575        packed_v = packed[64],
576        "Packed signature for Relayer"
577    );
578
579    Ok(packed_hex)
580}
581
582/// Pack a signature for Safe execTransaction (SafeTx)
583///
584/// Safe's `checkNSignatures` function uses a special signature format where the v value
585/// must be transformed to indicate an eth_sign signature type:
586/// - v=0 → v=31 (0x1f)
587/// - v=1 → v=32 (0x20)
588/// - v=27 → v=31 (0x1f)
589/// - v=28 → v=32 (0x20)
590///
591/// This matches the official Polymarket builder-relayer-client-rust implementation:
592/// <https://github.com/Polymarket/builder-relayer-client-rust>
593///
594/// This is different from SafeCreate which uses standard v=27/28 format.
595///
596/// # Arguments
597/// * `signature` - The ECDSA signature (hex string with optional 0x prefix)
598///
599/// # Returns
600/// The packed signature with transformed v for Safe execTransaction
601pub fn pack_signature_for_safe_tx(signature: &str) -> Result<String> {
602    let sig = signature.trim_start_matches("0x");
603
604    debug!(sig_len = sig.len(), "Packing signature for SafeTx");
605
606    if sig.len() < 130 {
607        return Err(PolymarketError::validation(format!(
608            "Signature too short: {} chars, expected at least 130",
609            sig.len()
610        )));
611    }
612
613    let r_hex = &sig[0..64];
614    let s_hex = &sig[64..128];
615    let v_hex = &sig[128..130];
616
617    let original_v = u8::from_str_radix(v_hex, 16)
618        .map_err(|e| PolymarketError::validation(format!("Invalid v value in signature: {e}")))?;
619
620    // Transform v value for Safe execTransaction (eth_sign format)
621    // This matches Polymarket's official implementation:
622    // - 0/1 → 31/32 (raw recovery id + 31)
623    // - 27/28 → 31/32 (standard Ethereum + 4)
624    let v = match original_v {
625        0 | 1 => original_v + 31,  // 0→31, 1→32 (raw k256 recovery id)
626        27 | 28 => original_v + 4, // 27→31, 28→32 (standard Ethereum)
627        31 | 32 => original_v,     // Already in Safe eth_sign format
628        _ => {
629            warn!(
630                v = original_v,
631                "Unexpected v value in signature, using as-is"
632            );
633            original_v
634        }
635    };
636
637    debug!(
638        original_v = original_v,
639        transformed_v = v,
640        v_hex = %v_hex,
641        r_hex_prefix = %&r_hex[0..8],
642        s_hex_prefix = %&s_hex[0..8],
643        "Signature v value transformation for SafeTx"
644    );
645
646    // Parse r and s as U256 for proper padding
647    let r = U256::from_str_radix(r_hex, 16)
648        .map_err(|e| PolymarketError::validation(format!("Invalid r value in signature: {e}")))?;
649    let s = U256::from_str_radix(s_hex, 16)
650        .map_err(|e| PolymarketError::validation(format!("Invalid s value in signature: {e}")))?;
651
652    // Pack as 32-byte r, 32-byte s, 1-byte v
653    let mut packed = Vec::with_capacity(65);
654    packed.extend_from_slice(&r.to_be_bytes::<32>());
655    packed.extend_from_slice(&s.to_be_bytes::<32>());
656    packed.push(v);
657
658    let packed_hex = format!("0x{}", hex::encode(&packed));
659
660    debug!(
661        original_sig = %signature,
662        packed_sig = %packed_hex,
663        packed_len = packed.len(),
664        packed_v = packed[64],
665        "Packed signature for SafeTx"
666    );
667
668    Ok(packed_hex)
669}
670
671/// Verify a signature can recover to the expected address
672///
673/// This function verifies that an EIP-712 signature was created by the expected signer
674/// by recovering the address from the signature and comparing it.
675///
676/// # Arguments
677/// * `signature` - The ECDSA signature (hex string with 0x prefix)
678/// * `digest` - The EIP-712 digest that was signed (hex string with 0x prefix)
679/// * `expected_address` - The expected signer address (hex string with 0x prefix)
680///
681/// # Returns
682/// Ok(recovered_address) if verification succeeds, Err if it fails
683pub fn verify_signature(signature: &str, digest: &str, expected_address: &str) -> Result<String> {
684    // Parse the digest as B256
685    let digest_bytes: B256 = digest
686        .parse()
687        .map_err(|e| PolymarketError::validation(format!("Invalid digest: {e}")))?;
688
689    // Parse the signature - alloy can handle both v=0/1 and v=27/28 formats
690    let sig: AlloySignature = signature
691        .parse()
692        .map_err(|e| PolymarketError::validation(format!("Invalid signature format: {e}")))?;
693
694    // Recover the signer address
695    let recovered = sig
696        .recover_address_from_prehash(&digest_bytes)
697        .map_err(|e| {
698            PolymarketError::validation(format!("Failed to recover address from signature: {e}"))
699        })?;
700
701    let recovered_str = format!("{recovered:#x}");
702    let expected_lower = expected_address.to_lowercase();
703
704    debug!(
705        recovered_address = %recovered_str,
706        expected_address = %expected_address,
707        signature_v = sig.v(),
708        "Signature verification"
709    );
710
711    if recovered_str.to_lowercase() != expected_lower {
712        return Err(PolymarketError::validation(format!(
713            "Signature verification failed: recovered {} but expected {}",
714            recovered_str, expected_address
715        )));
716    }
717
718    Ok(recovered_str)
719}
720
721// ============================================================================
722// SafeCreate EIP-712 Typed Data
723// ============================================================================
724
725/// CreateProxy typed data for EIP-712 signing (Polymarket SafeProxyFactory)
726#[derive(Debug, Clone, Serialize, Deserialize)]
727pub struct SafeCreateTypedData {
728    /// Domain data
729    pub domain: SafeCreateDomain,
730    /// Message data
731    pub message: SafeCreateMessage,
732    /// Primary type name
733    pub primary_type: String,
734    /// Type definitions
735    pub types: SafeCreateTypes,
736}
737
738/// EIP-712 Domain for Polymarket SafeProxyFactory
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct SafeCreateDomain {
741    /// Domain name (required for Polymarket)
742    pub name: String,
743    /// Chain ID (137 for Polygon)
744    #[serde(rename = "chainId")]
745    pub chain_id: u64,
746    /// Safe Factory address
747    #[serde(rename = "verifyingContract")]
748    pub verifying_contract: String,
749}
750
751impl Default for SafeCreateDomain {
752    fn default() -> Self {
753        Self {
754            name: DOMAIN_NAME.to_string(),
755            chain_id: 137,
756            verifying_contract: SAFE_FACTORY.to_string(),
757        }
758    }
759}
760
761/// CreateProxy message for EIP-712 signing (Polymarket SafeProxyFactory)
762/// Note: The owner is NOT part of the signed message - it's recovered from the signature
763#[derive(Debug, Clone, Serialize, Deserialize)]
764pub struct SafeCreateMessage {
765    /// Payment token address (usually zero address)
766    #[serde(rename = "paymentToken")]
767    pub payment_token: String,
768    /// Payment amount (usually 0)
769    pub payment: String,
770    /// Payment receiver address (usually zero address)
771    #[serde(rename = "paymentReceiver")]
772    pub payment_receiver: String,
773}
774
775impl SafeCreateMessage {
776    /// Create a standard CreateProxy message with no payment
777    #[must_use]
778    pub fn new(_owner: &str) -> Self {
779        // Note: owner is not part of the message for Polymarket CreateProxy
780        Self {
781            payment_token: "0x0000000000000000000000000000000000000000".to_string(),
782            payment: "0".to_string(),
783            payment_receiver: "0x0000000000000000000000000000000000000000".to_string(),
784        }
785    }
786
787    /// Create with custom payment parameters
788    #[must_use]
789    pub fn with_payment(
790        _owner: &str,
791        payment_token: &str,
792        payment: &str,
793        payment_receiver: &str,
794    ) -> Self {
795        Self {
796            payment_token: payment_token.to_string(),
797            payment: payment.to_string(),
798            payment_receiver: payment_receiver.to_string(),
799        }
800    }
801}
802
803/// Type definitions for CreateProxy (Polymarket SafeProxyFactory)
804#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct SafeCreateTypes {
806    /// EIP712Domain type
807    #[serde(rename = "EIP712Domain")]
808    pub eip712_domain: Vec<TypedDataField>,
809    /// CreateProxy type
810    #[serde(rename = "CreateProxy")]
811    pub safe_create: Vec<TypedDataField>,
812}
813
814impl Default for SafeCreateTypes {
815    fn default() -> Self {
816        Self {
817            eip712_domain: vec![
818                TypedDataField {
819                    name: "name".to_string(),
820                    r#type: "string".to_string(),
821                },
822                TypedDataField {
823                    name: "chainId".to_string(),
824                    r#type: "uint256".to_string(),
825                },
826                TypedDataField {
827                    name: "verifyingContract".to_string(),
828                    r#type: "address".to_string(),
829                },
830            ],
831            safe_create: vec![
832                TypedDataField {
833                    name: "paymentToken".to_string(),
834                    r#type: "address".to_string(),
835                },
836                TypedDataField {
837                    name: "payment".to_string(),
838                    r#type: "uint256".to_string(),
839                },
840                TypedDataField {
841                    name: "paymentReceiver".to_string(),
842                    r#type: "address".to_string(),
843                },
844            ],
845        }
846    }
847}
848
849/// A single field in a type definition
850#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct TypedDataField {
852    /// Field name
853    pub name: String,
854    /// Field type
855    pub r#type: String,
856}
857
858/// Build CreateProxy typed data for EIP-712 signing (Polymarket SafeProxyFactory)
859///
860/// # Arguments
861/// * `owner` - The owner address for the Safe (not included in signed message, but validated)
862/// * `chain_id` - The chain ID (default 137 for Polygon)
863///
864/// # Returns
865/// Complete typed data structure ready for signing
866///
867/// # Example
868///
869/// ```rust,ignore
870/// use polymarket_sdk::build_safe_create_typed_data;
871///
872/// let owner = "0x1234567890123456789012345678901234567890";
873/// let typed_data = build_safe_create_typed_data(owner, None)?;
874///
875/// // Sign with Privy or other signer
876/// let signature = signer.sign_typed_data(&typed_data).await?;
877/// ```
878pub fn build_safe_create_typed_data(
879    owner: &str,
880    chain_id: Option<u64>,
881) -> Result<SafeCreateTypedData> {
882    // Validate owner address format (even though it's not part of the signed message)
883    let _: Address = owner
884        .parse()
885        .map_err(|e| PolymarketError::validation(format!("Invalid owner address: {e}")))?;
886
887    Ok(SafeCreateTypedData {
888        domain: SafeCreateDomain {
889            chain_id: chain_id.unwrap_or(137),
890            ..Default::default()
891        },
892        message: SafeCreateMessage::new(owner),
893        primary_type: "CreateProxy".to_string(),
894        types: SafeCreateTypes::default(),
895    })
896}
897
898/// Compute the EIP-712 digest for SafeCreate
899///
900/// This computes the hash that needs to be signed:
901/// `keccak256(0x1901 || domainSeparator || structHash)`
902///
903/// # Arguments
904/// * `typed_data` - The SafeCreate typed data
905///
906/// # Returns
907/// The 32-byte digest to be signed
908pub fn compute_safe_create_digest(typed_data: &SafeCreateTypedData) -> Result<B256> {
909    let domain_separator = compute_domain_separator(typed_data)?;
910    let struct_hash = compute_struct_hash(typed_data)?;
911
912    let mut bytes = Vec::with_capacity(2 + 32 + 32);
913    bytes.push(0x19);
914    bytes.push(0x01);
915    bytes.extend_from_slice(domain_separator.as_slice());
916    bytes.extend_from_slice(struct_hash.as_slice());
917
918    Ok(keccak256(&bytes))
919}
920
921fn compute_domain_separator(typed_data: &SafeCreateTypedData) -> Result<B256> {
922    let domain_type_hash = keccak256(DOMAIN_TYPE_STR.as_bytes());
923
924    let chain_id = U256::from(typed_data.domain.chain_id);
925    let verifying_contract: Address = typed_data
926        .domain
927        .verifying_contract
928        .parse()
929        .map_err(|e| PolymarketError::validation(format!("Invalid verifying contract: {e}")))?;
930
931    let mut encoded = Vec::with_capacity(32 + 32 + 32);
932    encoded.extend_from_slice(domain_type_hash.as_slice());
933    encoded.extend_from_slice(&chain_id.to_be_bytes::<32>());
934
935    let mut addr_bytes = [0u8; 32];
936    addr_bytes[12..].copy_from_slice(verifying_contract.as_slice());
937    encoded.extend_from_slice(&addr_bytes);
938
939    Ok(keccak256(&encoded))
940}
941
942fn compute_struct_hash(typed_data: &SafeCreateTypedData) -> Result<B256> {
943    // Use Polymarket CreateProxy type hash
944    let type_hash = keccak256(CREATE_PROXY_TYPE_STR.as_bytes());
945
946    let payment_token: Address = typed_data
947        .message
948        .payment_token
949        .parse()
950        .map_err(|e| PolymarketError::validation(format!("Invalid payment token: {e}")))?;
951
952    let payment: U256 = typed_data
953        .message
954        .payment
955        .parse()
956        .map_err(|e| PolymarketError::validation(format!("Invalid payment: {e}")))?;
957
958    let payment_receiver: Address = typed_data
959        .message
960        .payment_receiver
961        .parse()
962        .map_err(|e| PolymarketError::validation(format!("Invalid payment receiver: {e}")))?;
963
964    // Polymarket CreateProxy only has 4 fields: typeHash + paymentToken + payment + paymentReceiver
965    let mut encoded = Vec::with_capacity(32 * 4);
966    encoded.extend_from_slice(type_hash.as_slice());
967
968    let mut payment_token_bytes = [0u8; 32];
969    payment_token_bytes[12..].copy_from_slice(payment_token.as_slice());
970    encoded.extend_from_slice(&payment_token_bytes);
971
972    encoded.extend_from_slice(&payment.to_be_bytes::<32>());
973
974    let mut payment_receiver_bytes = [0u8; 32];
975    payment_receiver_bytes[12..].copy_from_slice(payment_receiver.as_slice());
976    encoded.extend_from_slice(&payment_receiver_bytes);
977
978    Ok(keccak256(&encoded))
979}
980
981// ============================================================================
982// Relayer Client
983// ============================================================================
984
985/// Relayer API configuration
986#[derive(Debug, Clone)]
987pub struct RelayerConfig {
988    /// Relayer API base URL
989    pub base_url: String,
990    /// Data API base URL (for profile queries)
991    pub data_api_base_url: String,
992    /// Request timeout
993    pub timeout: Duration,
994    /// Rate limit (requests per second)
995    pub rate_limit_per_second: u32,
996    /// User agent string
997    pub user_agent: String,
998}
999
1000impl Default for RelayerConfig {
1001    fn default() -> Self {
1002        Self {
1003            // Use helper functions to support env var overrides
1004            base_url: relayer_api_url(),
1005            data_api_base_url: data_api_url(),
1006            timeout: Duration::from_secs(60),
1007            rate_limit_per_second: 2,
1008            user_agent: "polymarket-sdk/0.1.0".to_string(),
1009        }
1010    }
1011}
1012
1013impl RelayerConfig {
1014    /// Create a new configuration builder
1015    #[must_use]
1016    pub fn builder() -> Self {
1017        Self::default()
1018    }
1019
1020    /// Set base URL
1021    #[must_use]
1022    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
1023        self.base_url = url.into();
1024        self
1025    }
1026
1027    /// Set Data API base URL
1028    #[must_use]
1029    pub fn with_data_api_base_url(mut self, url: impl Into<String>) -> Self {
1030        self.data_api_base_url = url.into();
1031        self
1032    }
1033
1034    /// Set request timeout
1035    #[must_use]
1036    pub fn with_timeout(mut self, timeout: Duration) -> Self {
1037        self.timeout = timeout;
1038        self
1039    }
1040
1041    /// Set rate limit (requests per second)
1042    #[must_use]
1043    pub fn with_rate_limit(mut self, rate_limit: u32) -> Self {
1044        self.rate_limit_per_second = rate_limit;
1045        self
1046    }
1047
1048    /// Set user agent string
1049    #[must_use]
1050    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
1051        self.user_agent = user_agent.into();
1052        self
1053    }
1054
1055    /// Create config from environment variables.
1056    ///
1057    /// **Deprecated**: Use `RelayerConfig::default()` instead.
1058    /// The default implementation already supports env var overrides.
1059    #[must_use]
1060    #[deprecated(
1061        since = "0.1.0",
1062        note = "Use RelayerConfig::default() instead. URL overrides via \
1063                POLYMARKET_RELAYER_URL and POLYMARKET_DATA_URL env vars are already supported."
1064    )]
1065    pub fn from_env() -> Self {
1066        Self::default()
1067    }
1068}
1069
1070/// Deploy Safe request
1071#[derive(Debug, Serialize)]
1072#[allow(dead_code)]
1073struct DeploySafeRequest {
1074    owner: String,
1075}
1076
1077/// Deploy Safe response
1078#[derive(Debug, Deserialize)]
1079pub struct DeploySafeResponse {
1080    /// Transaction hash if deployment was submitted
1081    /// Relayer v2 API may return this as "hash" or "transactionHash"
1082    #[serde(alias = "transactionHash", alias = "hash")]
1083    pub transaction_hash: Option<String>,
1084    /// Proxy wallet address if already deployed or immediately available
1085    #[serde(alias = "proxyAddress", alias = "proxy_address")]
1086    pub proxy_address: Option<String>,
1087    /// Status of the deployment
1088    pub status: Option<String>,
1089    /// Error message if failed
1090    pub error: Option<String>,
1091}
1092
1093/// Relayer API client for Safe wallet deployment and proxy wallet management
1094#[derive(Clone)]
1095pub struct RelayerClient {
1096    config: RelayerConfig,
1097    client: Client,
1098    rate_limiter: Arc<RateLimiter>,
1099    builder_credentials: Option<BuilderApiCredentials>,
1100    /// Optional default RPC endpoint for on-chain checks (e.g., eth_getCode)
1101    default_rpc: Option<String>,
1102}
1103
1104impl RelayerClient {
1105    /// Create a new Relayer API client
1106    pub fn new(config: RelayerConfig) -> Result<Self> {
1107        let client = Client::builder()
1108            .timeout(config.timeout)
1109            .user_agent(&config.user_agent)
1110            .gzip(true)
1111            .build()
1112            .map_err(|e| PolymarketError::config(format!("Failed to create HTTP client: {e}")))?;
1113
1114        let quota = Quota::per_second(
1115            NonZeroU32::new(config.rate_limit_per_second).unwrap_or(NonZeroU32::new(2).unwrap()),
1116        );
1117        let rate_limiter = Arc::new(GovRateLimiter::direct(quota));
1118
1119        Ok(Self {
1120            config,
1121            client,
1122            rate_limiter,
1123            builder_credentials: None,
1124            default_rpc: None,
1125        })
1126    }
1127
1128    /// Create client with default configuration
1129    pub fn with_defaults() -> Result<Self> {
1130        Self::new(RelayerConfig::default())
1131    }
1132
1133    /// Create client from environment variables.
1134    ///
1135    /// **Deprecated**: Use `RelayerClient::with_defaults()` instead.
1136    #[deprecated(since = "0.1.0", note = "Use RelayerClient::with_defaults() instead")]
1137    #[allow(deprecated)]
1138    pub fn from_env() -> Result<Self> {
1139        Self::new(RelayerConfig::from_env())
1140    }
1141
1142    /// Add Builder API credentials for authenticated requests
1143    #[must_use]
1144    pub fn with_builder_credentials(mut self, credentials: BuilderApiCredentials) -> Self {
1145        self.builder_credentials = Some(credentials);
1146        self
1147    }
1148
1149    /// Set a default RPC endpoint for on-chain checks (e.g., proxy deployment detection)
1150    #[must_use]
1151    pub fn with_default_rpc(mut self, rpc_url: impl Into<String>) -> Self {
1152        self.default_rpc = Some(rpc_url.into());
1153        self
1154    }
1155
1156    /// Check whether the CREATE2-derived proxy wallet for `owner_address` is already deployed.
1157    ///
1158    /// - Computes the expected proxy address via SafeProxyFactory CREATE2 rules.
1159    /// - Uses alloy provider to call `eth_getCode` on the given RPC.
1160    /// - Returns `Some(proxy_address)` if code exists, otherwise `None`.
1161    pub async fn check_proxy_deployed(
1162        &self,
1163        owner_address: &str,
1164        rpc_url: Option<&str>,
1165    ) -> Result<Option<String>> {
1166        let rpc = rpc_url
1167            .map(|s| s.to_string())
1168            .or_else(|| self.default_rpc.clone())
1169            .or_else(|| std::env::var("POLYGON_RPC_URL").ok())
1170            .unwrap_or_else(|| DEFAULT_POLYGON_RPC.to_string());
1171
1172        let proxy_address = derive_safe_address(owner_address)?;
1173
1174        // Parse proxy address to alloy Address type
1175        let addr: Address = proxy_address
1176            .parse()
1177            .map_err(|e| PolymarketError::validation(format!("Invalid proxy address: {e}")))?;
1178
1179        // Build provider using alloy
1180        let rpc_url: url::Url = rpc
1181            .parse()
1182            .map_err(|e| PolymarketError::validation(format!("Invalid RPC URL {rpc}: {e}")))?;
1183        let provider = ProviderBuilder::new().connect_http(rpc_url);
1184
1185        // Use alloy provider to get code at address
1186        let code = provider
1187            .get_code_at(addr)
1188            .await
1189            .map_err(|e| PolymarketError::internal(format!("eth_getCode failed: {e}")))?;
1190
1191        let deployed = !code.is_empty();
1192
1193        debug!(
1194            owner = %owner_address,
1195            proxy = %proxy_address,
1196            rpc = %rpc,
1197            code_len = code.len(),
1198            deployed = deployed,
1199            "Checked proxy deployment via alloy provider"
1200        );
1201
1202        if deployed {
1203            Ok(Some(proxy_address))
1204        } else {
1205            Ok(None)
1206        }
1207    }
1208
1209    /// Get USDC balance for an address on Polygon
1210    ///
1211    /// Returns the balance in USDC (with 6 decimals precision).
1212    /// Queries both bridged USDC.e and native USDC contracts.
1213    ///
1214    /// # Arguments
1215    /// * `address` - The wallet address to check balance for
1216    /// * `rpc_url` - Optional RPC URL (defaults to POLYGON_RPC_URL env or DEFAULT_POLYGON_RPC)
1217    ///
1218    /// # Returns
1219    /// `(usdc_e_balance, native_usdc_balance)` as f64 values (human readable, e.g., 100.50 = $100.50)
1220    pub async fn get_usdc_balance(
1221        &self,
1222        address: &str,
1223        rpc_url: Option<&str>,
1224    ) -> Result<(f64, f64)> {
1225        let rpc = rpc_url
1226            .map(|s| s.to_string())
1227            .or_else(|| self.default_rpc.clone())
1228            .or_else(|| std::env::var("POLYGON_RPC_URL").ok())
1229            .unwrap_or_else(|| DEFAULT_POLYGON_RPC.to_string());
1230
1231        // Parse wallet address to validate format
1232        let wallet_addr: Address = address
1233            .parse()
1234            .map_err(|e| PolymarketError::validation(format!("Invalid wallet address: {e}")))?;
1235
1236        // Query both USDC contracts using raw JSON-RPC
1237        let usdc_e_balance = self
1238            .query_erc20_balance_rpc(&rpc, USDC_CONTRACT_ADDRESS, &wallet_addr)
1239            .await
1240            .unwrap_or(0.0);
1241
1242        let native_usdc_balance = self
1243            .query_erc20_balance_rpc(&rpc, NATIVE_USDC_CONTRACT_ADDRESS, &wallet_addr)
1244            .await
1245            .unwrap_or(0.0);
1246
1247        debug!(
1248            address = %address,
1249            usdc_e = %usdc_e_balance,
1250            native_usdc = %native_usdc_balance,
1251            "USDC balance query completed"
1252        );
1253
1254        Ok((usdc_e_balance, native_usdc_balance))
1255    }
1256
1257    /// Query ERC20 balanceOf for a specific token contract using raw JSON-RPC
1258    async fn query_erc20_balance_rpc(
1259        &self,
1260        rpc_url: &str,
1261        token_contract: &str,
1262        wallet: &Address,
1263    ) -> Result<f64> {
1264        // Build balanceOf call data: 0x70a08231 + address (padded to 32 bytes)
1265        // Function selector for balanceOf(address) = keccak256("balanceOf(address)")[:4]
1266        let mut call_data = vec![0x70, 0xa0, 0x82, 0x31]; // balanceOf selector
1267        let mut addr_padded = [0u8; 32];
1268        addr_padded[12..].copy_from_slice(wallet.as_slice());
1269        call_data.extend_from_slice(&addr_padded);
1270        let call_data_hex = format!("0x{}", hex::encode(&call_data));
1271
1272        // Build JSON-RPC request for eth_call
1273        let request = serde_json::json!({
1274            "jsonrpc": "2.0",
1275            "method": "eth_call",
1276            "params": [
1277                {
1278                    "to": token_contract,
1279                    "data": call_data_hex
1280                },
1281                "latest"
1282            ],
1283            "id": 1
1284        });
1285
1286        let response = self
1287            .client
1288            .post(rpc_url)
1289            .header("Content-Type", "application/json")
1290            .json(&request)
1291            .send()
1292            .await
1293            .map_err(|e| PolymarketError::internal(format!("RPC request failed: {e}")))?;
1294
1295        if !response.status().is_success() {
1296            return Err(PolymarketError::api(
1297                response.status().as_u16(),
1298                "RPC call failed".to_string(),
1299            ));
1300        }
1301
1302        let json: serde_json::Value = response
1303            .json()
1304            .await
1305            .map_err(|e| PolymarketError::parse(format!("Failed to parse RPC response: {e}")))?;
1306
1307        // Check for RPC error
1308        if let Some(error) = json.get("error") {
1309            return Err(PolymarketError::internal(format!("RPC error: {}", error)));
1310        }
1311
1312        // Parse result
1313        let result_hex = json["result"]
1314            .as_str()
1315            .ok_or_else(|| PolymarketError::parse("Missing result in RPC response"))?;
1316
1317        // Remove 0x prefix and parse as hex
1318        let result_bytes = hex::decode(result_hex.trim_start_matches("0x"))
1319            .map_err(|e| PolymarketError::parse(format!("Invalid hex result: {e}")))?;
1320
1321        if result_bytes.len() < 32 {
1322            return Ok(0.0);
1323        }
1324
1325        let balance_raw = U256::from_be_slice(&result_bytes[..32]);
1326
1327        // Convert to f64 with 6 decimals (USDC has 6 decimals)
1328        let balance = balance_raw.to::<u128>() as f64 / 1_000_000.0;
1329
1330        Ok(balance)
1331    }
1332
1333    /// Create Builder API authentication headers using HMAC-SHA256
1334    fn create_builder_headers(
1335        &self,
1336        method: &str,
1337        path: &str,
1338        body: Option<&str>,
1339    ) -> Result<HashMap<String, String>> {
1340        let credentials = self
1341            .builder_credentials
1342            .as_ref()
1343            .ok_or_else(|| PolymarketError::config("Builder API credentials not configured"))?;
1344
1345        let timestamp = std::time::SystemTime::now()
1346            .duration_since(std::time::UNIX_EPOCH)
1347            .map_err(|e| PolymarketError::config(format!("Failed to get timestamp: {e}")))?
1348            .as_secs() as i64;
1349
1350        let mut message = format!("{timestamp}{method}{path}");
1351        if let Some(b) = body {
1352            message.push_str(b);
1353        }
1354
1355        // Try URL-safe base64 first (handles _ and - characters), fallback to standard
1356        let secret_bytes = URL_SAFE
1357            .decode(&credentials.secret)
1358            .or_else(|_| URL_SAFE_NO_PAD.decode(&credentials.secret))
1359            .or_else(|_| STANDARD.decode(&credentials.secret))
1360            .map_err(|e| PolymarketError::config(format!("Invalid base64 secret: {e}")))?;
1361
1362        type HmacSha256 = Hmac<Sha256>;
1363        let mut mac = HmacSha256::new_from_slice(&secret_bytes)
1364            .map_err(|e| PolymarketError::config(format!("Invalid HMAC key: {e}")))?;
1365        mac.update(message.as_bytes());
1366        let signature_bytes = mac.finalize().into_bytes();
1367
1368        let signature = STANDARD
1369            .encode(&signature_bytes)
1370            .replace('+', "-")
1371            .replace('/', "_");
1372
1373        let mut headers = HashMap::new();
1374        headers.insert(
1375            "POLY_BUILDER_API_KEY".to_string(),
1376            credentials.api_key.clone(),
1377        );
1378        headers.insert(
1379            "POLY_BUILDER_PASSPHRASE".to_string(),
1380            credentials.passphrase.clone(),
1381        );
1382        headers.insert("POLY_BUILDER_SIGNATURE".to_string(), signature);
1383        headers.insert("POLY_BUILDER_TIMESTAMP".to_string(), timestamp.to_string());
1384
1385        Ok(headers)
1386    }
1387
1388    async fn wait_for_rate_limit(&self) {
1389        self.rate_limiter.until_ready().await;
1390    }
1391
1392    /// Deploy a Safe wallet for a user via Relayer v2 API
1393    ///
1394    /// This method requires Builder API credentials to be configured.
1395    /// The Relayer v2 API uses HMAC authentication with Builder credentials.
1396    #[instrument(skip(self), fields(owner = %owner_address))]
1397    pub async fn deploy_safe(&self, owner_address: &str) -> Result<DeploySafeResponse> {
1398        self.wait_for_rate_limit().await;
1399
1400        // Relayer v2 API uses /submit endpoint for Safe creation
1401        let endpoint = "/submit";
1402        let url = format!("{}{}", self.config.base_url, endpoint);
1403
1404        info!(owner = %owner_address, url = %url, "Deploying Safe wallet via Relayer");
1405
1406        // Build SafeCreate request body
1407        let request_body = serde_json::json!({
1408            "type": "SAFE-CREATE",
1409            "from": owner_address,
1410            "chainId": 137,
1411            "paymentToken": "0x0000000000000000000000000000000000000000",
1412            "payment": "0",
1413            "paymentReceiver": "0x0000000000000000000000000000000000000000"
1414        });
1415        let body_str = serde_json::to_string(&request_body)
1416            .map_err(|e| PolymarketError::config(format!("Failed to serialize request: {e}")))?;
1417
1418        // Build request with Builder authentication headers
1419        let mut req_builder = self.client.post(&url);
1420
1421        // Add Builder API authentication headers (required for Relayer v2)
1422        if self.builder_credentials.is_some() {
1423            let headers = self.create_builder_headers("POST", endpoint, Some(&body_str))?;
1424            for (key, value) in headers {
1425                req_builder = req_builder.header(&key, &value);
1426            }
1427        } else {
1428            return Err(PolymarketError::config(
1429                "Builder API credentials required for Safe deployment",
1430            ));
1431        }
1432
1433        // Add POLY_ADDRESS header with the owner wallet address
1434        req_builder = req_builder
1435            .header("POLY_ADDRESS", owner_address)
1436            .header("Content-Type", "application/json")
1437            .body(body_str.clone());
1438
1439        debug!(body = %body_str, "Sending SafeCreate request");
1440
1441        let response = req_builder.send().await?;
1442        let status = response.status();
1443        let response_body = response.text().await.unwrap_or_default();
1444
1445        debug!(status = %status, response = %response_body, "Relayer response received");
1446
1447        if !status.is_success() {
1448            warn!(
1449                status = %status,
1450                endpoint = %endpoint,
1451                body = %response_body,
1452                "Relayer SafeCreate request failed"
1453            );
1454            return Err(PolymarketError::api(status.as_u16(), response_body));
1455        }
1456
1457        let result: DeploySafeResponse = serde_json::from_str(&response_body).map_err(|e| {
1458            PolymarketError::parse_with_source(
1459                format!("Failed to parse Relayer response: {e}. Body: {response_body}"),
1460                e,
1461            )
1462        })?;
1463
1464        info!(
1465            owner = %owner_address,
1466            proxy_address = ?result.proxy_address,
1467            tx_hash = ?result.transaction_hash,
1468            "Safe deployment response received"
1469        );
1470
1471        Ok(result)
1472    }
1473
1474    /// Get proxy wallet address for an owner address from Data API
1475    #[instrument(skip(self), fields(owner = %owner_address))]
1476    pub async fn get_proxy_wallet_address(&self, owner_address: &str) -> Result<Option<String>> {
1477        self.wait_for_rate_limit().await;
1478
1479        let endpoint = format!("/profile/{owner_address}");
1480        let url = format!("{}{}", self.config.data_api_base_url, endpoint);
1481
1482        debug!(owner = %owner_address, "Querying proxy wallet address");
1483
1484        let response = self.client.get(&url).send().await?;
1485        let status = response.status();
1486
1487        if status.as_u16() == 404 {
1488            debug!(owner = %owner_address, "No proxy wallet found (404)");
1489            return Ok(None);
1490        }
1491
1492        if !status.is_success() {
1493            let body = response.text().await.unwrap_or_default();
1494            warn!(status = %status.as_u16(), response_body = %body, "Data API profile query failed");
1495            return Ok(None);
1496        }
1497
1498        let body = response.text().await.unwrap_or_default();
1499        let json: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
1500
1501        let proxy_address = json["proxyWallet"]
1502            .as_str()
1503            .or_else(|| json["polyProxy"].as_str())
1504            .or_else(|| json["safeAddress"].as_str())
1505            .or_else(|| json["proxy_wallet"].as_str())
1506            .map(String::from);
1507
1508        if let Some(ref addr) = proxy_address {
1509            info!(owner = %owner_address, proxy = %addr, "Found proxy wallet");
1510        }
1511
1512        Ok(proxy_address)
1513    }
1514
1515    /// Deploy Safe and wait for proxy address
1516    #[instrument(skip(self), fields(owner = %owner_address))]
1517    pub async fn ensure_proxy_wallet(
1518        &self,
1519        owner_address: &str,
1520        max_wait_secs: Option<u64>,
1521    ) -> Result<Option<String>> {
1522        let max_wait = Duration::from_secs(max_wait_secs.unwrap_or(60));
1523        let poll_interval = Duration::from_secs(3);
1524        let start = std::time::Instant::now();
1525
1526        if let Some(proxy_address) = self.get_proxy_wallet_address(owner_address).await? {
1527            info!(owner = %owner_address, proxy = %proxy_address, "Proxy wallet already exists");
1528            return Ok(Some(proxy_address));
1529        }
1530
1531        info!(owner = %owner_address, "No existing proxy wallet, deploying new Safe");
1532
1533        let deploy_result = self.deploy_safe(owner_address).await?;
1534
1535        if let Some(proxy_address) = deploy_result.proxy_address {
1536            info!(owner = %owner_address, proxy = %proxy_address, "Safe deployed immediately");
1537            return Ok(Some(proxy_address));
1538        }
1539
1540        if let Some(ref tx_hash) = deploy_result.transaction_hash {
1541            info!(owner = %owner_address, tx_hash = %tx_hash, "Safe deployment submitted, polling");
1542        }
1543
1544        while start.elapsed() < max_wait {
1545            tokio::time::sleep(poll_interval).await;
1546
1547            match self.get_proxy_wallet_address(owner_address).await {
1548                Ok(Some(proxy_address)) => {
1549                    info!(owner = %owner_address, proxy = %proxy_address, "Proxy wallet now available");
1550                    return Ok(Some(proxy_address));
1551                }
1552                Ok(None) => {
1553                    debug!(owner = %owner_address, "Proxy wallet not yet available");
1554                }
1555                Err(e) => {
1556                    warn!(owner = %owner_address, error = %e, "Error polling for proxy wallet");
1557                }
1558            }
1559        }
1560
1561        warn!(owner = %owner_address, "Proxy wallet not available after max wait time");
1562        Ok(None)
1563    }
1564
1565    /// Deploy Safe with an EIP-712 signature
1566    ///
1567    /// This method requires the user's EIP-712 signature on SafeCreate typed data.
1568    /// The signature should be created by signing the data from `build_safe_create_typed_data`.
1569    ///
1570    /// # Arguments
1571    /// * `owner_address` - The owner's embedded wallet address
1572    /// * `signature` - The EIP-712 signature (hex string with 0x prefix)
1573    #[instrument(skip(self, signature), fields(owner = %owner_address))]
1574    pub async fn deploy_safe_with_signature(
1575        &self,
1576        owner_address: &str,
1577        signature: &str,
1578    ) -> Result<TransactionReceipt> {
1579        self.wait_for_rate_limit().await;
1580
1581        let safe_address = derive_safe_address(owner_address)?;
1582
1583        info!(owner = %owner_address, safe_address = %safe_address, "Deploying Safe with signature");
1584
1585        // Compute the digest and verify signature BEFORE sending to relayer
1586        let digest = compute_safe_create_digest_internal(owner_address, DEFAULT_CHAIN_ID)?;
1587        let digest_hex = format!("{digest:#x}");
1588
1589        debug!(digest = %digest_hex, owner = %owner_address, "Computed SafeCreate digest for verification");
1590
1591        // Verify the signature recovers to the expected owner address
1592        match verify_signature(signature, &digest_hex, owner_address) {
1593            Ok(recovered) => {
1594                debug!(recovered_address = %recovered, owner_address = %owner_address, "Signature verification PASSED");
1595            }
1596            Err(e) => {
1597                // Log the error but continue for now to see what the relayer says
1598                // In production, we might want to fail fast here
1599                warn!(
1600                    error = %e,
1601                    signature = %signature,
1602                    digest = %digest_hex,
1603                    owner = %owner_address,
1604                    "Signature verification FAILED - this will likely cause relayer rejection"
1605                );
1606            }
1607        }
1608
1609        // Pack the signature for Relayer API (transforms v value)
1610        let packed_signature = pack_signature(signature)?;
1611        debug!(original_sig = %signature, packed_sig = %packed_signature, "Signature packed");
1612
1613        // Build SignatureParams for SafeCreate (no payment)
1614        let sig_params = SignatureParams {
1615            payment_token: Some("0x0000000000000000000000000000000000000000".to_string()),
1616            payment: Some("0".to_string()),
1617            payment_receiver: Some("0x0000000000000000000000000000000000000000".to_string()),
1618            ..Default::default()
1619        };
1620
1621        let tx_request = TransactionRequest {
1622            r#type: TransactionType::SafeCreate,
1623            from: owner_address.to_string(),
1624            to: SAFE_FACTORY.to_string(),
1625            proxy_wallet: Some(safe_address.clone()),
1626            data: "0x".to_string(),
1627            nonce: None,
1628            signature: packed_signature,
1629            signature_params: sig_params,
1630            metadata: None,
1631        };
1632
1633        // Use /submit endpoint for SafeCreate
1634        let receipt = self.submit_safe_create(&tx_request).await?;
1635
1636        info!(owner = %owner_address, tx_id = %receipt.id, state = ?receipt.state, "Safe deployment submitted");
1637
1638        Ok(receipt)
1639    }
1640
1641    /// Submit a SafeCreate transaction to the Relayer /submit endpoint
1642    #[instrument(skip(self, request))]
1643    async fn submit_safe_create(&self, request: &TransactionRequest) -> Result<TransactionReceipt> {
1644        self.wait_for_rate_limit().await;
1645
1646        let endpoint = "/submit";
1647        let url = format!("{}{}", self.config.base_url, endpoint);
1648
1649        let body = serde_json::to_string(request)
1650            .map_err(|e| PolymarketError::config(format!("Failed to serialize request: {e}")))?;
1651
1652        // Log the full request body for debugging
1653        debug!(
1654            endpoint = %endpoint,
1655            from = %request.from,
1656            to = %request.to,
1657            proxy_wallet = ?request.proxy_wallet,
1658            signature_len = %request.signature.len(),
1659            "Submitting SafeCreate to Relayer"
1660        );
1661        debug!(body = %body, "SafeCreate request body");
1662
1663        let mut req_builder = self.client.post(&url);
1664
1665        if self.builder_credentials.is_some() {
1666            let headers = self.create_builder_headers("POST", endpoint, Some(&body))?;
1667            let header_keys: Vec<String> = headers.iter().map(|(k, _)| k.to_string()).collect();
1668            debug!(headers = ?header_keys, "Applying builder headers for SafeCreate");
1669            for (key, value) in headers {
1670                req_builder = req_builder.header(&key, &value);
1671            }
1672        } else {
1673            return Err(PolymarketError::config(
1674                "Builder API credentials required for Safe deployment",
1675            ));
1676        }
1677
1678        // Add POLY_ADDRESS header
1679        req_builder = req_builder
1680            .header("POLY_ADDRESS", &request.from)
1681            .header("Content-Type", "application/json")
1682            .body(body);
1683
1684        let response = req_builder.send().await?;
1685        let status = response.status();
1686        let response_body = response.text().await.unwrap_or_default();
1687
1688        debug!(status = %status, response = %response_body, "Relayer /submit response");
1689
1690        if !status.is_success() {
1691            warn!(status = %status, endpoint = %endpoint, body = %response_body, "Relayer /submit failed");
1692            return Err(PolymarketError::api(status.as_u16(), response_body));
1693        }
1694
1695        let receipt: TransactionReceipt = serde_json::from_str(&response_body).map_err(|e| {
1696            PolymarketError::parse_with_source(
1697                format!("Failed to parse receipt: {e}. Body: {response_body}"),
1698                e,
1699            )
1700        })?;
1701
1702        Ok(receipt)
1703    }
1704
1705    /// Submit a transaction to the Relayer API
1706    ///
1707    /// Uses the `/submit` endpoint for both SafeCreate and Safe execution transactions.
1708    /// Note: `/transaction` is only for querying transaction status (GET), not submitting.
1709    #[instrument(skip(self, request))]
1710    pub async fn submit_transaction(
1711        &self,
1712        request: &TransactionRequest,
1713    ) -> Result<TransactionReceipt> {
1714        self.wait_for_rate_limit().await;
1715
1716        let endpoint = "/submit";
1717        let url = format!("{}{}", self.config.base_url, endpoint);
1718
1719        let body = serde_json::to_string(request)
1720            .map_err(|e| PolymarketError::config(format!("Failed to serialize request: {e}")))?;
1721
1722        debug!(endpoint = %endpoint, "Submitting transaction to Relayer");
1723
1724        let mut req_builder = self.client.post(&url);
1725
1726        if self.builder_credentials.is_some() {
1727            let headers = self.create_builder_headers("POST", endpoint, Some(&body))?;
1728            for (key, value) in headers {
1729                req_builder = req_builder.header(&key, &value);
1730            }
1731        }
1732
1733        // Add POLY_ADDRESS header (required for Safe transactions)
1734        let response = req_builder
1735            .header("POLY_ADDRESS", &request.from)
1736            .header("Content-Type", "application/json")
1737            .body(body)
1738            .send()
1739            .await?;
1740
1741        let status = response.status();
1742
1743        if !status.is_success() {
1744            let body = response.text().await.unwrap_or_default();
1745            warn!(status = %status, url = %url, body = %body, "Relayer /submit request failed");
1746            return Err(PolymarketError::api(status.as_u16(), body));
1747        }
1748
1749        let receipt: TransactionReceipt = response.json().await.map_err(|e| {
1750            PolymarketError::parse_with_source(format!("Failed to parse receipt: {e}"), e)
1751        })?;
1752
1753        Ok(receipt)
1754    }
1755
1756    /// Get the status of a transaction
1757    ///
1758    /// Uses the `/transaction?id=xxx` endpoint (query parameter, not path parameter)
1759    /// as per the official Polymarket builder-relayer-client SDK.
1760    #[instrument(skip(self), fields(tx_id = %transaction_id))]
1761    pub async fn get_transaction_status(&self, transaction_id: &str) -> Result<TransactionReceipt> {
1762        self.wait_for_rate_limit().await;
1763
1764        // Official SDK uses query parameter: /transaction?id=xxx
1765        let endpoint = "/transaction";
1766        let url = format!("{}{}?id={}", self.config.base_url, endpoint, transaction_id);
1767
1768        debug!(tx_id = %transaction_id, url = %url, "Querying transaction status");
1769
1770        let mut req_builder = self.client.get(&url);
1771
1772        if self.builder_credentials.is_some() {
1773            let headers = self.create_builder_headers("GET", endpoint, None)?;
1774            for (key, value) in headers {
1775                req_builder = req_builder.header(&key, &value);
1776            }
1777        }
1778
1779        let response = req_builder.send().await?;
1780        let status = response.status();
1781
1782        if !status.is_success() {
1783            let body = response.text().await.unwrap_or_default();
1784            return Err(PolymarketError::api(status.as_u16(), body));
1785        }
1786
1787        // The API returns an array of transactions, we need the first one
1788        let receipts: Vec<TransactionReceipt> = response.json().await.map_err(|e| {
1789            PolymarketError::parse_with_source(format!("Failed to parse status: {e}"), e)
1790        })?;
1791
1792        receipts.into_iter().next().ok_or_else(|| {
1793            PolymarketError::api(404, format!("Transaction not found: {}", transaction_id))
1794        })
1795    }
1796
1797    /// Poll until a transaction is confirmed or reaches a terminal state
1798    #[instrument(skip(self), fields(tx_id = %transaction_id))]
1799    pub async fn poll_until_confirmed(
1800        &self,
1801        transaction_id: &str,
1802        max_wait_secs: Option<u64>,
1803        poll_interval_secs: Option<u64>,
1804    ) -> Result<TransactionReceipt> {
1805        let max_wait = Duration::from_secs(max_wait_secs.unwrap_or(120));
1806        let poll_interval = Duration::from_secs(poll_interval_secs.unwrap_or(3));
1807        let start = std::time::Instant::now();
1808
1809        info!(tx_id = %transaction_id, max_wait_secs = %max_wait.as_secs(), "Polling until confirmed");
1810
1811        loop {
1812            let receipt = self.get_transaction_status(transaction_id).await?;
1813
1814            if receipt.state.is_terminal() {
1815                if receipt.state.is_success() {
1816                    info!(tx_id = %transaction_id, "Transaction confirmed");
1817                } else {
1818                    warn!(
1819                        tx_id = %transaction_id,
1820                        state = ?receipt.state,
1821                        tx_hash = ?receipt.transaction_hash,
1822                        error = ?receipt.error,
1823                        "Transaction failed"
1824                    );
1825                    debug!(tx_id = %transaction_id, receipt = ?receipt, "Full transaction receipt");
1826                }
1827                return Ok(receipt);
1828            }
1829
1830            if start.elapsed() >= max_wait {
1831                warn!(tx_id = %transaction_id, "Polling timeout reached");
1832                return Ok(receipt);
1833            }
1834
1835            debug!(tx_id = %transaction_id, state = ?receipt.state, "Pending, continuing poll");
1836            tokio::time::sleep(poll_interval).await;
1837        }
1838    }
1839
1840    /// Get the next nonce for an address
1841    ///
1842    /// # Arguments
1843    /// * `address` - The signer address (EOA/embedded wallet, NOT proxy wallet)
1844    /// * `nonce_type` - The type of nonce to query (SAFE for transactions, SAFECREATE for deployment)
1845    ///
1846    /// # API Format
1847    /// `GET /nonce?address={address}&type={SAFE|SAFECREATE}`
1848    #[instrument(skip(self), fields(address = %address))]
1849    pub async fn get_next_nonce(&self, address: &str, nonce_type: NonceType) -> Result<u64> {
1850        self.wait_for_rate_limit().await;
1851
1852        // Polymarket Relayer API uses uppercase type values
1853        let nonce_type_str = match nonce_type {
1854            NonceType::Transaction => "SAFE",
1855            NonceType::SafeCreate => "SAFECREATE",
1856        };
1857
1858        // Correct API format: /nonce?address=...&type=...
1859        let endpoint = format!("/nonce?address={address}&type={nonce_type_str}");
1860        let url = format!("{}{}", self.config.base_url, endpoint);
1861
1862        debug!(address = %address, nonce_type = %nonce_type_str, url = %url, "Getting next nonce");
1863
1864        let mut req_builder = self.client.get(&url);
1865
1866        if self.builder_credentials.is_some() {
1867            // For GET requests with query params, sign with just the path portion
1868            let sign_endpoint = format!("/nonce?address={address}&type={nonce_type_str}");
1869            let headers = self.create_builder_headers("GET", &sign_endpoint, None)?;
1870            for (key, value) in headers {
1871                req_builder = req_builder.header(&key, &value);
1872            }
1873        }
1874
1875        let response = req_builder.send().await?;
1876        let status = response.status();
1877
1878        if !status.is_success() {
1879            let body = response.text().await.unwrap_or_default();
1880            return Err(PolymarketError::api(status.as_u16(), body));
1881        }
1882
1883        // Polymarket returns nonce as string, need to parse
1884        #[derive(Deserialize)]
1885        struct NonceResponse {
1886            nonce: String,
1887        }
1888
1889        let nonce_resp: NonceResponse = response.json().await.map_err(|e| {
1890            PolymarketError::parse_with_source(format!("Failed to parse nonce response: {e}"), e)
1891        })?;
1892
1893        let nonce: u64 = nonce_resp.nonce.parse().map_err(|e| {
1894            PolymarketError::parse(format!(
1895                "Failed to parse nonce value '{}': {}",
1896                nonce_resp.nonce, e
1897            ))
1898        })?;
1899
1900        debug!(address = %address, nonce = %nonce, "Got next nonce");
1901
1902        Ok(nonce)
1903    }
1904
1905    /// Deploy Safe with signature and wait for confirmation
1906    #[instrument(skip(self, signature), fields(owner = %owner_address))]
1907    pub async fn deploy_safe_and_wait(
1908        &self,
1909        owner_address: &str,
1910        signature: &str,
1911        max_wait_secs: Option<u64>,
1912    ) -> Result<String> {
1913        let receipt = self
1914            .deploy_safe_with_signature(owner_address, signature)
1915            .await?;
1916        let final_receipt = self
1917            .poll_until_confirmed(&receipt.id, max_wait_secs, None)
1918            .await?;
1919
1920        if !final_receipt.state.is_success() {
1921            return Err(PolymarketError::api(
1922                500,
1923                format!(
1924                    "Safe deployment failed: {:?} - {:?}",
1925                    final_receipt.state, final_receipt.error
1926                ),
1927            ));
1928        }
1929
1930        final_receipt
1931            .proxy_address
1932            .ok_or_else(|| PolymarketError::api(500, "No proxy address returned"))
1933    }
1934
1935    // ========================================================================
1936    // Approve Service Methods
1937    // ========================================================================
1938
1939    /// Check ERC20 token allowance via RPC
1940    ///
1941    /// # Arguments
1942    /// * `token_address` - ERC20 token contract address (USDC)
1943    /// * `owner` - Token owner address (proxy wallet)
1944    /// * `spender` - Spender address (Exchange contract)
1945    ///
1946    /// # Returns
1947    /// Current allowance amount in raw units (6 decimals for USDC)
1948    #[instrument(skip(self), fields(owner = %owner, spender = %spender))]
1949    pub async fn check_erc20_allowance(
1950        &self,
1951        token_address: &str,
1952        owner: &str,
1953        spender: &str,
1954    ) -> Result<U256> {
1955        let rpc = self
1956            .default_rpc
1957            .as_ref()
1958            .ok_or_else(|| PolymarketError::config("RPC URL not configured"))?;
1959
1960        let calldata = encode_erc20_allowance_query(owner, spender)?;
1961
1962        let params = serde_json::json!([
1963            {
1964                "to": token_address,
1965                "data": calldata
1966            },
1967            "latest"
1968        ]);
1969
1970        let response: serde_json::Value = self
1971            .client
1972            .post(rpc)
1973            .json(&serde_json::json!({
1974                "jsonrpc": "2.0",
1975                "method": "eth_call",
1976                "params": params,
1977                "id": 1
1978            }))
1979            .send()
1980            .await?
1981            .json()
1982            .await?;
1983
1984        let result = response["result"]
1985            .as_str()
1986            .ok_or_else(|| PolymarketError::api(500, "Invalid RPC response"))?;
1987
1988        // Parse hex result to U256
1989        let result_bytes = hex::decode(result.trim_start_matches("0x"))
1990            .map_err(|e| PolymarketError::validation(format!("Invalid hex: {e}")))?;
1991
1992        if result_bytes.len() != 32 {
1993            return Err(PolymarketError::validation(
1994                "Invalid allowance response length",
1995            ));
1996        }
1997
1998        Ok(U256::from_be_slice(&result_bytes))
1999    }
2000
2001    /// Check ERC1155 approval status via RPC
2002    ///
2003    /// # Arguments
2004    /// * `token_address` - ERC1155 token contract address (CTF)
2005    /// * `owner` - Token owner address (proxy wallet)
2006    /// * `operator` - Operator address (Exchange contract)
2007    ///
2008    /// # Returns
2009    /// Whether the operator is approved for all tokens
2010    #[instrument(skip(self), fields(owner = %owner, operator = %operator))]
2011    pub async fn check_erc1155_approval(
2012        &self,
2013        token_address: &str,
2014        owner: &str,
2015        operator: &str,
2016    ) -> Result<bool> {
2017        let rpc = self
2018            .default_rpc
2019            .as_ref()
2020            .ok_or_else(|| PolymarketError::config("RPC URL not configured"))?;
2021
2022        let calldata = encode_erc1155_is_approved_for_all(owner, operator)?;
2023
2024        let params = serde_json::json!([
2025            {
2026                "to": token_address,
2027                "data": calldata
2028            },
2029            "latest"
2030        ]);
2031
2032        let response: serde_json::Value = self
2033            .client
2034            .post(rpc)
2035            .json(&serde_json::json!({
2036                "jsonrpc": "2.0",
2037                "method": "eth_call",
2038                "params": params,
2039                "id": 1
2040            }))
2041            .send()
2042            .await?
2043            .json()
2044            .await?;
2045
2046        let result = response["result"]
2047            .as_str()
2048            .ok_or_else(|| PolymarketError::api(500, "Invalid RPC response"))?;
2049
2050        // Parse hex result - returns true (1) or false (0)
2051        let result_bytes = hex::decode(result.trim_start_matches("0x"))
2052            .map_err(|e| PolymarketError::validation(format!("Invalid hex: {e}")))?;
2053
2054        if result_bytes.is_empty() || result_bytes.len() > 32 {
2055            return Ok(false);
2056        }
2057
2058        // Check if last byte is 1 (true)
2059        Ok(result_bytes.last() == Some(&1))
2060    }
2061
2062    /// Check if proxy wallet has sufficient USDC allowance for trading
2063    ///
2064    /// # Arguments
2065    /// * `proxy_wallet` - Proxy wallet address
2066    /// * `spender` - Exchange contract address
2067    /// * `required_amount` - Required allowance amount in raw USDC (6 decimals)
2068    /// * `use_native_usdc` - Whether to use native USDC or bridged USDC.e
2069    ///
2070    /// # Returns
2071    /// (has_sufficient_allowance, current_allowance)
2072    pub async fn check_usdc_allowance(
2073        &self,
2074        proxy_wallet: &str,
2075        spender: &str,
2076        required_amount: U256,
2077        use_native_usdc: bool,
2078    ) -> Result<(bool, U256)> {
2079        let token = if use_native_usdc {
2080            NATIVE_USDC_CONTRACT_ADDRESS
2081        } else {
2082            USDC_CONTRACT_ADDRESS
2083        };
2084
2085        let current = self
2086            .check_erc20_allowance(token, proxy_wallet, spender)
2087            .await?;
2088        let sufficient = current >= required_amount;
2089
2090        debug!(
2091            proxy_wallet = %proxy_wallet,
2092            spender = %spender,
2093            required = %required_amount,
2094            current = %current,
2095            sufficient = %sufficient,
2096            "Checked USDC allowance"
2097        );
2098
2099        Ok((sufficient, current))
2100    }
2101
2102    /// Check if proxy wallet has CTF approval for trading
2103    ///
2104    /// # Arguments
2105    /// * `proxy_wallet` - Proxy wallet address
2106    /// * `operator` - Exchange contract address
2107    ///
2108    /// # Returns
2109    /// Whether the operator is approved
2110    pub async fn check_ctf_approval(&self, proxy_wallet: &str, operator: &str) -> Result<bool> {
2111        let approved = self
2112            .check_erc1155_approval(CONDITIONAL_TOKENS_ADDRESS, proxy_wallet, operator)
2113            .await?;
2114
2115        debug!(
2116            proxy_wallet = %proxy_wallet,
2117            operator = %operator,
2118            approved = %approved,
2119            "Checked CTF approval"
2120        );
2121
2122        Ok(approved)
2123    }
2124
2125    /// Check all required approvals for a market type
2126    ///
2127    /// # Arguments
2128    /// * `proxy_wallet` - Proxy wallet address
2129    /// * `market_type` - Standard or NegRisk market
2130    /// * `use_native_usdc` - Whether to use native USDC
2131    ///
2132    /// # Returns
2133    /// ApprovalStatus indicating which approvals are missing
2134    pub async fn check_approvals(
2135        &self,
2136        proxy_wallet: &str,
2137        market_type: MarketType,
2138        use_native_usdc: bool,
2139    ) -> Result<ApprovalStatus> {
2140        let targets = ApprovalTargets::for_market_type(market_type);
2141
2142        // Check USDC allowance for exchange
2143        let (usdc_approved, usdc_allowance) = self
2144            .check_usdc_allowance(
2145                proxy_wallet,
2146                targets.usdc_spender,
2147                U256::from(1),
2148                use_native_usdc,
2149            )
2150            .await?;
2151
2152        // Check CTF approval for exchange
2153        let ctf_approved = self
2154            .check_ctf_approval(proxy_wallet, targets.ctf_operator)
2155            .await?;
2156
2157        // For neg-risk markets, also check adapter approval
2158        let adapter_approved = if let Some(adapter) = targets.ctf_adapter_operator {
2159            self.check_ctf_approval(proxy_wallet, adapter).await?
2160        } else {
2161            true
2162        };
2163
2164        Ok(ApprovalStatus {
2165            usdc_approved,
2166            usdc_allowance,
2167            ctf_approved,
2168            adapter_approved,
2169            all_approved: usdc_approved && ctf_approved && adapter_approved,
2170        })
2171    }
2172}
2173
2174/// Status of token approvals for trading
2175#[derive(Debug, Clone)]
2176pub struct ApprovalStatus {
2177    /// Whether USDC is approved for exchange
2178    pub usdc_approved: bool,
2179    /// Current USDC allowance
2180    pub usdc_allowance: U256,
2181    /// Whether CTF is approved for exchange
2182    pub ctf_approved: bool,
2183    /// Whether CTF is approved for adapter (neg-risk only)
2184    pub adapter_approved: bool,
2185    /// Whether all required approvals are in place
2186    pub all_approved: bool,
2187}
2188
2189impl ApprovalStatus {
2190    /// Get list of missing approvals
2191    pub fn missing_approvals(&self) -> Vec<&'static str> {
2192        let mut missing = Vec::new();
2193        if !self.usdc_approved {
2194            missing.push("USDC → Exchange");
2195        }
2196        if !self.ctf_approved {
2197            missing.push("CTF → Exchange");
2198        }
2199        if !self.adapter_approved {
2200            missing.push("CTF → Adapter");
2201        }
2202        missing
2203    }
2204}
2205
2206// ============================================================================
2207// Safe Transaction EIP-712 (for USDC transfers)
2208// ============================================================================
2209
2210/// Safe transaction domain name
2211#[allow(dead_code)]
2212const SAFE_DOMAIN_NAME: &str = "Gnosis Safe";
2213
2214/// Safe transaction domain version
2215#[allow(dead_code)]
2216const SAFE_DOMAIN_VERSION: &str = "1.3.0";
2217
2218/// SafeTx type string for EIP-712
2219const SAFE_TX_TYPE_STR: &str = "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)";
2220
2221/// EIP712Domain type string for Safe (includes version)
2222const SAFE_DOMAIN_TYPE_STR: &str = "EIP712Domain(uint256 chainId,address verifyingContract)";
2223
2224/// ERC20 transfer function selector: keccak256("transfer(address,uint256)")[:4]
2225const ERC20_TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
2226
2227/// ERC20 approve function selector: keccak256("approve(address,uint256)")[:4]
2228const ERC20_APPROVE_SELECTOR: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
2229
2230/// ERC20 allowance function selector: keccak256("allowance(address,address)")[:4]
2231const ERC20_ALLOWANCE_SELECTOR: [u8; 4] = [0xdd, 0x62, 0xed, 0x3e];
2232
2233/// ERC1155 setApprovalForAll function selector: keccak256("setApprovalForAll(address,bool)")[:4]
2234const ERC1155_SET_APPROVAL_FOR_ALL_SELECTOR: [u8; 4] = [0xa2, 0x2c, 0xb4, 0x65];
2235
2236/// ERC1155 isApprovedForAll function selector: keccak256("isApprovedForAll(address,address)")[:4]
2237const ERC1155_IS_APPROVED_FOR_ALL_SELECTOR: [u8; 4] = [0xe9, 0x85, 0xe9, 0xc5];
2238
2239/// Safe Transaction typed data for EIP-712 signing
2240#[derive(Debug, Clone, Serialize, Deserialize)]
2241pub struct SafeTxTypedData {
2242    /// Domain data
2243    pub domain: SafeTxDomain,
2244    /// Message data
2245    pub message: SafeTxMessage,
2246    /// Primary type name (must be "primaryType" for EIP-712)
2247    #[serde(rename = "primaryType")]
2248    pub primary_type: String,
2249    /// Type definitions
2250    pub types: SafeTxTypes,
2251}
2252
2253/// EIP-712 Domain for Safe transactions
2254#[derive(Debug, Clone, Serialize, Deserialize)]
2255pub struct SafeTxDomain {
2256    /// Chain ID (137 for Polygon)
2257    #[serde(rename = "chainId")]
2258    pub chain_id: u64,
2259    /// Safe wallet address (proxy wallet)
2260    #[serde(rename = "verifyingContract")]
2261    pub verifying_contract: String,
2262}
2263
2264/// SafeTx message for EIP-712 signing
2265#[derive(Debug, Clone, Serialize, Deserialize)]
2266pub struct SafeTxMessage {
2267    /// Target contract address (USDC contract for transfers)
2268    pub to: String,
2269    /// ETH value (0 for ERC20 transfers)
2270    pub value: String,
2271    /// Encoded function call data
2272    pub data: String,
2273    /// Operation type (0 = Call, 1 = DelegateCall)
2274    pub operation: u8,
2275    /// Gas for Safe internal call
2276    #[serde(rename = "safeTxGas")]
2277    pub safe_tx_gas: String,
2278    /// Base gas for transaction
2279    #[serde(rename = "baseGas")]
2280    pub base_gas: String,
2281    /// Gas price (0 for gasless)
2282    #[serde(rename = "gasPrice")]
2283    pub gas_price: String,
2284    /// Gas token address (zero for ETH)
2285    #[serde(rename = "gasToken")]
2286    pub gas_token: String,
2287    /// Refund receiver address
2288    #[serde(rename = "refundReceiver")]
2289    pub refund_receiver: String,
2290    /// Safe nonce
2291    pub nonce: String,
2292}
2293
2294/// Type definitions for SafeTx
2295#[derive(Debug, Clone, Serialize, Deserialize)]
2296pub struct SafeTxTypes {
2297    /// EIP712Domain type
2298    #[serde(rename = "EIP712Domain")]
2299    pub eip712_domain: Vec<TypedDataField>,
2300    /// SafeTx type
2301    #[serde(rename = "SafeTx")]
2302    pub safe_tx: Vec<TypedDataField>,
2303}
2304
2305impl Default for SafeTxTypes {
2306    fn default() -> Self {
2307        Self {
2308            eip712_domain: vec![
2309                TypedDataField {
2310                    name: "chainId".to_string(),
2311                    r#type: "uint256".to_string(),
2312                },
2313                TypedDataField {
2314                    name: "verifyingContract".to_string(),
2315                    r#type: "address".to_string(),
2316                },
2317            ],
2318            safe_tx: vec![
2319                TypedDataField {
2320                    name: "to".to_string(),
2321                    r#type: "address".to_string(),
2322                },
2323                TypedDataField {
2324                    name: "value".to_string(),
2325                    r#type: "uint256".to_string(),
2326                },
2327                TypedDataField {
2328                    name: "data".to_string(),
2329                    r#type: "bytes".to_string(),
2330                },
2331                TypedDataField {
2332                    name: "operation".to_string(),
2333                    r#type: "uint8".to_string(),
2334                },
2335                TypedDataField {
2336                    name: "safeTxGas".to_string(),
2337                    r#type: "uint256".to_string(),
2338                },
2339                TypedDataField {
2340                    name: "baseGas".to_string(),
2341                    r#type: "uint256".to_string(),
2342                },
2343                TypedDataField {
2344                    name: "gasPrice".to_string(),
2345                    r#type: "uint256".to_string(),
2346                },
2347                TypedDataField {
2348                    name: "gasToken".to_string(),
2349                    r#type: "address".to_string(),
2350                },
2351                TypedDataField {
2352                    name: "refundReceiver".to_string(),
2353                    r#type: "address".to_string(),
2354                },
2355                TypedDataField {
2356                    name: "nonce".to_string(),
2357                    r#type: "uint256".to_string(),
2358                },
2359            ],
2360        }
2361    }
2362}
2363
2364/// Encode ERC20 transfer calldata
2365///
2366/// # Arguments
2367/// * `recipient` - The recipient address
2368/// * `amount` - The amount in token units (e.g., USDC with 6 decimals: 1 USDC = 1_000_000)
2369///
2370/// # Returns
2371/// Hex-encoded calldata with 0x prefix
2372pub fn encode_erc20_transfer(recipient: &str, amount: u128) -> Result<String> {
2373    let recipient_addr: Address = recipient
2374        .parse()
2375        .map_err(|e| PolymarketError::validation(format!("Invalid recipient address: {e}")))?;
2376
2377    let mut calldata = Vec::with_capacity(68);
2378    // Function selector
2379    calldata.extend_from_slice(&ERC20_TRANSFER_SELECTOR);
2380    // Recipient address (padded to 32 bytes)
2381    let mut addr_bytes = [0u8; 32];
2382    addr_bytes[12..].copy_from_slice(recipient_addr.as_slice());
2383    calldata.extend_from_slice(&addr_bytes);
2384    // Amount (32 bytes)
2385    let amount_u256 = U256::from(amount);
2386    calldata.extend_from_slice(&amount_u256.to_be_bytes::<32>());
2387
2388    Ok(format!("0x{}", hex::encode(&calldata)))
2389}
2390
2391/// Encode ERC20 approve function call
2392///
2393/// # Arguments
2394/// * `spender` - The address authorized to spend tokens
2395/// * `amount` - Amount to approve (use U256::MAX for unlimited)
2396///
2397/// # Returns
2398/// Encoded calldata for approve(address,uint256)
2399pub fn encode_erc20_approve(spender: &str, amount: U256) -> Result<String> {
2400    let spender_addr: Address = spender
2401        .parse()
2402        .map_err(|e| PolymarketError::validation(format!("Invalid spender address: {e}")))?;
2403
2404    let mut calldata = Vec::with_capacity(68);
2405    // Function selector
2406    calldata.extend_from_slice(&ERC20_APPROVE_SELECTOR);
2407    // Spender address (padded to 32 bytes)
2408    let mut addr_bytes = [0u8; 32];
2409    addr_bytes[12..].copy_from_slice(spender_addr.as_slice());
2410    calldata.extend_from_slice(&addr_bytes);
2411    // Amount (32 bytes)
2412    calldata.extend_from_slice(&amount.to_be_bytes::<32>());
2413
2414    Ok(format!("0x{}", hex::encode(&calldata)))
2415}
2416
2417/// Encode ERC20 allowance query function call
2418///
2419/// # Arguments
2420/// * `owner` - The token owner address
2421/// * `spender` - The spender address
2422///
2423/// # Returns
2424/// Encoded calldata for allowance(address,address)
2425pub fn encode_erc20_allowance_query(owner: &str, spender: &str) -> Result<String> {
2426    let owner_addr: Address = owner
2427        .parse()
2428        .map_err(|e| PolymarketError::validation(format!("Invalid owner address: {e}")))?;
2429    let spender_addr: Address = spender
2430        .parse()
2431        .map_err(|e| PolymarketError::validation(format!("Invalid spender address: {e}")))?;
2432
2433    let mut calldata = Vec::with_capacity(68);
2434    // Function selector
2435    calldata.extend_from_slice(&ERC20_ALLOWANCE_SELECTOR);
2436    // Owner address (padded to 32 bytes)
2437    let mut owner_bytes = [0u8; 32];
2438    owner_bytes[12..].copy_from_slice(owner_addr.as_slice());
2439    calldata.extend_from_slice(&owner_bytes);
2440    // Spender address (padded to 32 bytes)
2441    let mut spender_bytes = [0u8; 32];
2442    spender_bytes[12..].copy_from_slice(spender_addr.as_slice());
2443    calldata.extend_from_slice(&spender_bytes);
2444
2445    Ok(format!("0x{}", hex::encode(&calldata)))
2446}
2447
2448/// Encode ERC1155 setApprovalForAll function call
2449///
2450/// # Arguments
2451/// * `operator` - The address to grant/revoke approval for all tokens
2452/// * `approved` - Whether to approve or revoke
2453///
2454/// # Returns
2455/// Encoded calldata for setApprovalForAll(address,bool)
2456pub fn encode_erc1155_set_approval_for_all(operator: &str, approved: bool) -> Result<String> {
2457    let operator_addr: Address = operator
2458        .parse()
2459        .map_err(|e| PolymarketError::validation(format!("Invalid operator address: {e}")))?;
2460
2461    let mut calldata = Vec::with_capacity(68);
2462    // Function selector
2463    calldata.extend_from_slice(&ERC1155_SET_APPROVAL_FOR_ALL_SELECTOR);
2464    // Operator address (padded to 32 bytes)
2465    let mut addr_bytes = [0u8; 32];
2466    addr_bytes[12..].copy_from_slice(operator_addr.as_slice());
2467    calldata.extend_from_slice(&addr_bytes);
2468    // Approved bool (padded to 32 bytes, 1 or 0 in last byte)
2469    let mut bool_bytes = [0u8; 32];
2470    bool_bytes[31] = if approved { 1 } else { 0 };
2471    calldata.extend_from_slice(&bool_bytes);
2472
2473    Ok(format!("0x{}", hex::encode(&calldata)))
2474}
2475
2476/// Encode ERC1155 isApprovedForAll query function call
2477///
2478/// # Arguments
2479/// * `owner` - The token owner address
2480/// * `operator` - The operator address to check approval for
2481///
2482/// # Returns
2483/// Encoded calldata for isApprovedForAll(address,address)
2484pub fn encode_erc1155_is_approved_for_all(owner: &str, operator: &str) -> Result<String> {
2485    let owner_addr: Address = owner
2486        .parse()
2487        .map_err(|e| PolymarketError::validation(format!("Invalid owner address: {e}")))?;
2488    let operator_addr: Address = operator
2489        .parse()
2490        .map_err(|e| PolymarketError::validation(format!("Invalid operator address: {e}")))?;
2491
2492    let mut calldata = Vec::with_capacity(68);
2493    // Function selector
2494    calldata.extend_from_slice(&ERC1155_IS_APPROVED_FOR_ALL_SELECTOR);
2495    // Owner address (padded to 32 bytes)
2496    let mut owner_bytes = [0u8; 32];
2497    owner_bytes[12..].copy_from_slice(owner_addr.as_slice());
2498    calldata.extend_from_slice(&owner_bytes);
2499    // Operator address (padded to 32 bytes)
2500    let mut operator_bytes = [0u8; 32];
2501    operator_bytes[12..].copy_from_slice(operator_addr.as_slice());
2502    calldata.extend_from_slice(&operator_bytes);
2503
2504    Ok(format!("0x{}", hex::encode(&calldata)))
2505}
2506
2507/// Build SafeTx typed data for USDC transfer
2508///
2509/// # Arguments
2510/// * `proxy_wallet` - The Safe (proxy wallet) address
2511/// * `recipient` - The recipient address for USDC
2512/// * `amount_usdc` - Amount in USDC (human readable, e.g., 100.50 = $100.50)
2513/// * `nonce` - Safe nonce
2514/// * `use_native_usdc` - Whether to use native USDC (true) or bridged USDC.e (false)
2515/// * `chain_id` - Chain ID (default 137 for Polygon)
2516///
2517/// # Returns
2518/// Complete SafeTx typed data ready for EIP-712 signing
2519pub fn build_usdc_transfer_typed_data(
2520    proxy_wallet: &str,
2521    recipient: &str,
2522    amount_usdc: f64,
2523    nonce: u64,
2524    use_native_usdc: bool,
2525    chain_id: Option<u64>,
2526) -> Result<SafeTxTypedData> {
2527    // Validate addresses
2528    let _: Address = proxy_wallet
2529        .parse()
2530        .map_err(|e| PolymarketError::validation(format!("Invalid proxy wallet address: {e}")))?;
2531    let _: Address = recipient
2532        .parse()
2533        .map_err(|e| PolymarketError::validation(format!("Invalid recipient address: {e}")))?;
2534
2535    // Convert USDC amount to raw units (6 decimals)
2536    let amount_raw = (amount_usdc * 1_000_000.0) as u128;
2537
2538    // Select USDC contract
2539    let usdc_contract = if use_native_usdc {
2540        NATIVE_USDC_CONTRACT_ADDRESS
2541    } else {
2542        USDC_CONTRACT_ADDRESS
2543    };
2544
2545    // Encode transfer calldata
2546    let calldata = encode_erc20_transfer(recipient, amount_raw)?;
2547
2548    Ok(SafeTxTypedData {
2549        domain: SafeTxDomain {
2550            chain_id: chain_id.unwrap_or(137),
2551            verifying_contract: proxy_wallet.to_string(),
2552        },
2553        message: SafeTxMessage {
2554            to: usdc_contract.to_string(),
2555            value: "0".to_string(),
2556            data: calldata,
2557            operation: 0, // Call
2558            safe_tx_gas: "0".to_string(),
2559            base_gas: "0".to_string(),
2560            gas_price: "0".to_string(),
2561            gas_token: "0x0000000000000000000000000000000000000000".to_string(),
2562            refund_receiver: "0x0000000000000000000000000000000000000000".to_string(),
2563            nonce: nonce.to_string(),
2564        },
2565        primary_type: "SafeTx".to_string(),
2566        types: SafeTxTypes::default(),
2567    })
2568}
2569
2570/// Build EIP-712 typed data for token approval (ERC20 approve)
2571///
2572/// Creates a Safe transaction that approves a spender (typically Exchange contract)
2573/// to spend unlimited USDC tokens from the proxy wallet.
2574///
2575/// # Arguments
2576/// * `proxy_wallet` - Safe proxy wallet address performing the approval
2577/// * `spender` - Address to approve (Exchange contract)
2578/// * `nonce` - Safe nonce from Relayer API
2579/// * `use_native_usdc` - Whether to use native USDC (true) or bridged USDC.e (false)
2580/// * `chain_id` - Chain ID (default: 137 for Polygon)
2581///
2582/// # Returns
2583/// EIP-712 typed data ready for user signature
2584pub fn build_token_approve_typed_data(
2585    proxy_wallet: &str,
2586    spender: &str,
2587    nonce: u64,
2588    use_native_usdc: bool,
2589    chain_id: Option<u64>,
2590) -> Result<SafeTxTypedData> {
2591    use alloy_primitives::U256;
2592
2593    // Validate addresses
2594    let _: Address = proxy_wallet
2595        .parse()
2596        .map_err(|e| PolymarketError::validation(format!("Invalid proxy wallet address: {e}")))?;
2597    let _: Address = spender
2598        .parse()
2599        .map_err(|e| PolymarketError::validation(format!("Invalid spender address: {e}")))?;
2600
2601    // Select USDC contract
2602    let usdc_contract = if use_native_usdc {
2603        NATIVE_USDC_CONTRACT_ADDRESS
2604    } else {
2605        USDC_CONTRACT_ADDRESS
2606    };
2607
2608    // Encode approve calldata with unlimited approval (U256::MAX)
2609    let calldata = encode_erc20_approve(spender, U256::MAX)?;
2610
2611    Ok(SafeTxTypedData {
2612        domain: SafeTxDomain {
2613            chain_id: chain_id.unwrap_or(137),
2614            verifying_contract: proxy_wallet.to_string(),
2615        },
2616        message: SafeTxMessage {
2617            to: usdc_contract.to_string(),
2618            value: "0".to_string(),
2619            data: calldata,
2620            operation: 0, // Call
2621            safe_tx_gas: "0".to_string(),
2622            base_gas: "0".to_string(),
2623            gas_price: "0".to_string(),
2624            gas_token: "0x0000000000000000000000000000000000000000".to_string(),
2625            refund_receiver: "0x0000000000000000000000000000000000000000".to_string(),
2626            nonce: nonce.to_string(),
2627        },
2628        primary_type: "SafeTx".to_string(),
2629        types: SafeTxTypes::default(),
2630    })
2631}
2632
2633/// Market type for determining which contracts to approve
2634#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2635pub enum MarketType {
2636    /// Standard prediction markets
2637    Standard,
2638    /// Negative risk markets (multi-outcome)
2639    NegRisk,
2640}
2641
2642/// Build EIP-712 typed data for CTF (ERC1155) approval
2643///
2644/// Creates a Safe transaction that approves an operator (Exchange or NegRiskExchange)
2645/// to transfer all CTF tokens (outcome shares) from the proxy wallet.
2646///
2647/// # Arguments
2648/// * `proxy_wallet` - Safe proxy wallet address performing the approval
2649/// * `operator` - Address to approve (Exchange or NegRiskExchange contract)
2650/// * `nonce` - Safe nonce from Relayer API
2651/// * `chain_id` - Chain ID (default: 137 for Polygon)
2652///
2653/// # Returns
2654/// EIP-712 typed data ready for user signature
2655pub fn build_ctf_approve_typed_data(
2656    proxy_wallet: &str,
2657    operator: &str,
2658    nonce: u64,
2659    chain_id: Option<u64>,
2660) -> Result<SafeTxTypedData> {
2661    // Validate addresses
2662    let _: Address = proxy_wallet
2663        .parse()
2664        .map_err(|e| PolymarketError::validation(format!("Invalid proxy wallet address: {e}")))?;
2665    let _: Address = operator
2666        .parse()
2667        .map_err(|e| PolymarketError::validation(format!("Invalid operator address: {e}")))?;
2668
2669    // Encode setApprovalForAll calldata
2670    let calldata = encode_erc1155_set_approval_for_all(operator, true)?;
2671
2672    Ok(SafeTxTypedData {
2673        domain: SafeTxDomain {
2674            chain_id: chain_id.unwrap_or(137),
2675            verifying_contract: proxy_wallet.to_string(),
2676        },
2677        message: SafeTxMessage {
2678            to: CONDITIONAL_TOKENS_ADDRESS.to_string(),
2679            value: "0".to_string(),
2680            data: calldata,
2681            operation: 0, // Call
2682            safe_tx_gas: "0".to_string(),
2683            base_gas: "0".to_string(),
2684            gas_price: "0".to_string(),
2685            gas_token: "0x0000000000000000000000000000000000000000".to_string(),
2686            refund_receiver: "0x0000000000000000000000000000000000000000".to_string(),
2687            nonce: nonce.to_string(),
2688        },
2689        primary_type: "SafeTx".to_string(),
2690        types: SafeTxTypes::default(),
2691    })
2692}
2693
2694/// Approval target configuration based on market type
2695#[derive(Debug, Clone)]
2696pub struct ApprovalTargets {
2697    /// USDC spender (Exchange or NegRiskExchange)
2698    pub usdc_spender: &'static str,
2699    /// CTF operator (Exchange or NegRiskExchange)
2700    pub ctf_operator: &'static str,
2701    /// Additional USDC spender for split/merge (CTF or NegRiskAdapter)
2702    pub usdc_split_spender: Option<&'static str>,
2703    /// Additional CTF operator for neg-risk (NegRiskAdapter)
2704    pub ctf_adapter_operator: Option<&'static str>,
2705}
2706
2707impl ApprovalTargets {
2708    /// Get approval targets for standard markets
2709    pub fn standard() -> Self {
2710        Self {
2711            usdc_spender: EXCHANGE_ADDRESS,
2712            ctf_operator: EXCHANGE_ADDRESS,
2713            usdc_split_spender: Some(CONDITIONAL_TOKENS_ADDRESS),
2714            ctf_adapter_operator: None,
2715        }
2716    }
2717
2718    /// Get approval targets for neg-risk markets
2719    pub fn neg_risk() -> Self {
2720        Self {
2721            usdc_spender: NEG_RISK_CTF_EXCHANGE_ADDRESS,
2722            ctf_operator: NEG_RISK_CTF_EXCHANGE_ADDRESS,
2723            usdc_split_spender: Some(NEG_RISK_ADAPTER_ADDRESS),
2724            ctf_adapter_operator: Some(NEG_RISK_ADAPTER_ADDRESS),
2725        }
2726    }
2727
2728    /// Get all approval targets (supports both market types)
2729    pub fn all() -> Self {
2730        Self {
2731            usdc_spender: EXCHANGE_ADDRESS, // Standard exchange
2732            ctf_operator: EXCHANGE_ADDRESS,
2733            usdc_split_spender: Some(CONDITIONAL_TOKENS_ADDRESS),
2734            ctf_adapter_operator: None,
2735        }
2736    }
2737
2738    /// Get targets for a specific market type
2739    pub fn for_market_type(market_type: MarketType) -> Self {
2740        match market_type {
2741            MarketType::Standard => Self::standard(),
2742            MarketType::NegRisk => Self::neg_risk(),
2743        }
2744    }
2745}
2746
2747/// Compute the EIP-712 domain separator for Safe transactions
2748fn compute_safe_domain_separator(typed_data: &SafeTxTypedData) -> Result<B256> {
2749    let domain_type_hash = keccak256(SAFE_DOMAIN_TYPE_STR.as_bytes());
2750
2751    let chain_id = U256::from(typed_data.domain.chain_id);
2752    let verifying_contract: Address = typed_data
2753        .domain
2754        .verifying_contract
2755        .parse()
2756        .map_err(|e| PolymarketError::validation(format!("Invalid verifying contract: {e}")))?;
2757
2758    // Domain separator: keccak256(typeHash || chainId || verifyingContract)
2759    let mut encoded = Vec::with_capacity(96);
2760    encoded.extend_from_slice(domain_type_hash.as_slice());
2761    encoded.extend_from_slice(&chain_id.to_be_bytes::<32>());
2762    let mut addr_bytes = [0u8; 32];
2763    addr_bytes[12..].copy_from_slice(verifying_contract.as_slice());
2764    encoded.extend_from_slice(&addr_bytes);
2765
2766    Ok(keccak256(&encoded))
2767}
2768
2769/// Compute the struct hash for SafeTx
2770fn compute_safe_tx_struct_hash(typed_data: &SafeTxTypedData) -> Result<B256> {
2771    let type_hash = keccak256(SAFE_TX_TYPE_STR.as_bytes());
2772
2773    let to: Address = typed_data
2774        .message
2775        .to
2776        .parse()
2777        .map_err(|e| PolymarketError::validation(format!("Invalid to address: {e}")))?;
2778
2779    let value: U256 = typed_data
2780        .message
2781        .value
2782        .parse()
2783        .map_err(|e| PolymarketError::validation(format!("Invalid value: {e}")))?;
2784
2785    // Decode data and hash it
2786    let data_bytes = hex::decode(typed_data.message.data.trim_start_matches("0x"))
2787        .map_err(|e| PolymarketError::validation(format!("Invalid data hex: {e}")))?;
2788    let data_hash = keccak256(&data_bytes);
2789
2790    let operation = U256::from(typed_data.message.operation);
2791    let safe_tx_gas: U256 = typed_data
2792        .message
2793        .safe_tx_gas
2794        .parse()
2795        .map_err(|e| PolymarketError::validation(format!("Invalid safeTxGas: {e}")))?;
2796    let base_gas: U256 = typed_data
2797        .message
2798        .base_gas
2799        .parse()
2800        .map_err(|e| PolymarketError::validation(format!("Invalid baseGas: {e}")))?;
2801    let gas_price: U256 = typed_data
2802        .message
2803        .gas_price
2804        .parse()
2805        .map_err(|e| PolymarketError::validation(format!("Invalid gasPrice: {e}")))?;
2806    let gas_token: Address = typed_data
2807        .message
2808        .gas_token
2809        .parse()
2810        .map_err(|e| PolymarketError::validation(format!("Invalid gasToken: {e}")))?;
2811    let refund_receiver: Address = typed_data
2812        .message
2813        .refund_receiver
2814        .parse()
2815        .map_err(|e| PolymarketError::validation(format!("Invalid refundReceiver: {e}")))?;
2816    let nonce: U256 = typed_data
2817        .message
2818        .nonce
2819        .parse()
2820        .map_err(|e| PolymarketError::validation(format!("Invalid nonce: {e}")))?;
2821
2822    // Encode struct: typeHash || to || value || dataHash || operation || safeTxGas || baseGas || gasPrice || gasToken || refundReceiver || nonce
2823    let mut encoded = Vec::with_capacity(352);
2824    encoded.extend_from_slice(type_hash.as_slice());
2825
2826    let mut to_bytes = [0u8; 32];
2827    to_bytes[12..].copy_from_slice(to.as_slice());
2828    encoded.extend_from_slice(&to_bytes);
2829
2830    encoded.extend_from_slice(&value.to_be_bytes::<32>());
2831    encoded.extend_from_slice(data_hash.as_slice());
2832    encoded.extend_from_slice(&operation.to_be_bytes::<32>());
2833    encoded.extend_from_slice(&safe_tx_gas.to_be_bytes::<32>());
2834    encoded.extend_from_slice(&base_gas.to_be_bytes::<32>());
2835    encoded.extend_from_slice(&gas_price.to_be_bytes::<32>());
2836
2837    let mut gas_token_bytes = [0u8; 32];
2838    gas_token_bytes[12..].copy_from_slice(gas_token.as_slice());
2839    encoded.extend_from_slice(&gas_token_bytes);
2840
2841    let mut refund_receiver_bytes = [0u8; 32];
2842    refund_receiver_bytes[12..].copy_from_slice(refund_receiver.as_slice());
2843    encoded.extend_from_slice(&refund_receiver_bytes);
2844
2845    encoded.extend_from_slice(&nonce.to_be_bytes::<32>());
2846
2847    Ok(keccak256(&encoded))
2848}
2849
2850/// Compute the EIP-712 digest for SafeTx
2851///
2852/// This computes the hash that needs to be signed:
2853/// `keccak256(0x1901 || domainSeparator || structHash)`
2854///
2855/// # Arguments
2856/// * `typed_data` - The SafeTx typed data
2857///
2858/// # Returns
2859/// The 32-byte digest to be signed
2860pub fn compute_safe_tx_digest(typed_data: &SafeTxTypedData) -> Result<B256> {
2861    let domain_separator = compute_safe_domain_separator(typed_data)?;
2862    let struct_hash = compute_safe_tx_struct_hash(typed_data)?;
2863
2864    let mut bytes = Vec::with_capacity(66);
2865    bytes.push(0x19);
2866    bytes.push(0x01);
2867    bytes.extend_from_slice(domain_separator.as_slice());
2868    bytes.extend_from_slice(struct_hash.as_slice());
2869
2870    Ok(keccak256(&bytes))
2871}
2872
2873/// Build TransactionRequest for submitting signed Safe transaction to Relayer
2874///
2875/// Uses `pack_signature_for_safe_tx` which adds +4 to v value for Safe's
2876/// `checkNSignatures` eth_sign format (v=31/32 instead of v=27/28).
2877///
2878/// # Arguments
2879/// * `typed_data` - The SafeTx typed data that was signed
2880/// * `signer` - The address that signed the transaction (server wallet)
2881/// * `signature` - The ECDSA signature (will be packed with v+4)
2882/// * `nonce` - The Safe nonce
2883///
2884/// # Returns
2885/// TransactionRequest ready for Relayer API
2886pub fn build_safe_tx_request(
2887    typed_data: &SafeTxTypedData,
2888    signer: &str,
2889    signature: &str,
2890    nonce: u64,
2891) -> Result<TransactionRequest> {
2892    // Use pack_signature_for_safe_tx which adds +4 to v for Safe execTransaction
2893    let packed_signature = pack_signature_for_safe_tx(signature)?;
2894
2895    Ok(TransactionRequest {
2896        r#type: TransactionType::Safe,
2897        from: signer.to_string(),
2898        to: typed_data.message.to.clone(),
2899        proxy_wallet: Some(typed_data.domain.verifying_contract.clone()),
2900        data: typed_data.message.data.clone(),
2901        nonce: Some(nonce.to_string()),
2902        signature: packed_signature,
2903        signature_params: SignatureParams {
2904            operation: Some(typed_data.message.operation.to_string()),
2905            safe_tx_gas: Some(typed_data.message.safe_tx_gas.clone()),
2906            base_gas: Some(typed_data.message.base_gas.clone()),
2907            gas_price: Some(typed_data.message.gas_price.clone()),
2908            gas_token: Some(typed_data.message.gas_token.clone()),
2909            refund_receiver: Some(typed_data.message.refund_receiver.clone()),
2910            ..Default::default()
2911        },
2912        metadata: None,
2913    })
2914}
2915
2916// ============================================================================
2917// Tests
2918// ============================================================================
2919
2920#[cfg(test)]
2921mod tests {
2922    use super::*;
2923
2924    // --- Derive Tests ---
2925
2926    #[test]
2927    fn test_derive_safe_address() {
2928        let owner = "0x1234567890123456789012345678901234567890";
2929        let result = derive_safe_address(owner);
2930
2931        assert!(result.is_ok());
2932        let safe_addr = result.unwrap();
2933        assert!(safe_addr.starts_with("0x"));
2934        assert_eq!(safe_addr.len(), 42);
2935    }
2936
2937    #[test]
2938    fn test_derive_safe_address_deterministic() {
2939        let owner = "0xabcdef1234567890abcdef1234567890abcdef12";
2940
2941        let result1 = derive_safe_address(owner).unwrap();
2942        let result2 = derive_safe_address(owner).unwrap();
2943
2944        assert_eq!(result1, result2);
2945    }
2946
2947    #[test]
2948    fn test_derive_safe_address_different_owners() {
2949        let owner1 = "0x1234567890123456789012345678901234567890";
2950        let owner2 = "0x0987654321098765432109876543210987654321";
2951
2952        let safe1 = derive_safe_address(owner1).unwrap();
2953        let safe2 = derive_safe_address(owner2).unwrap();
2954
2955        assert_ne!(safe1, safe2);
2956    }
2957
2958    #[test]
2959    fn test_invalid_owner_address() {
2960        let invalid = "not-a-valid-address";
2961        let result = derive_safe_address(invalid);
2962        assert!(result.is_err());
2963    }
2964
2965    #[test]
2966    fn test_constants() {
2967        let factory: std::result::Result<Address, _> = SAFE_FACTORY.parse();
2968        assert!(factory.is_ok());
2969
2970        let hash: std::result::Result<B256, _> = SAFE_INIT_CODE_HASH.parse();
2971        assert!(hash.is_ok());
2972    }
2973
2974    // --- SafeCreate Tests ---
2975
2976    #[test]
2977    fn test_build_safe_create_typed_data() {
2978        let owner = "0x1234567890123456789012345678901234567890";
2979        let result = build_safe_create_typed_data(owner, None);
2980
2981        assert!(result.is_ok());
2982        let typed_data = result.unwrap();
2983
2984        assert_eq!(typed_data.primary_type, "CreateProxy");
2985        assert_eq!(typed_data.domain.chain_id, 137);
2986    }
2987
2988    #[test]
2989    fn test_build_safe_create_typed_data_custom_chain() {
2990        let owner = "0x1234567890123456789012345678901234567890";
2991        let result = build_safe_create_typed_data(owner, Some(80001));
2992
2993        assert!(result.is_ok());
2994        let typed_data = result.unwrap();
2995        assert_eq!(typed_data.domain.chain_id, 80001);
2996    }
2997
2998    #[test]
2999    fn test_compute_digest() {
3000        let owner = "0x1234567890123456789012345678901234567890";
3001        let typed_data = build_safe_create_typed_data(owner, None).unwrap();
3002
3003        let result = compute_safe_create_digest(&typed_data);
3004        assert!(result.is_ok());
3005
3006        let digest = result.unwrap();
3007        assert_eq!(digest.len(), 32);
3008    }
3009
3010    #[test]
3011    fn test_digest_deterministic() {
3012        let owner = "0x1234567890123456789012345678901234567890";
3013        let typed_data = build_safe_create_typed_data(owner, None).unwrap();
3014
3015        let digest1 = compute_safe_create_digest(&typed_data).unwrap();
3016        let digest2 = compute_safe_create_digest(&typed_data).unwrap();
3017
3018        assert_eq!(digest1, digest2);
3019    }
3020
3021    #[test]
3022    fn test_invalid_owner_typed_data() {
3023        let result = build_safe_create_typed_data("invalid-address", None);
3024        assert!(result.is_err());
3025    }
3026
3027    #[test]
3028    fn test_safe_create_message() {
3029        let msg = SafeCreateMessage::new("0x1234567890123456789012345678901234567890");
3030
3031        assert_eq!(msg.payment, "0");
3032        assert_eq!(
3033            msg.payment_receiver,
3034            "0x0000000000000000000000000000000000000000"
3035        );
3036        assert_eq!(
3037            msg.payment_token,
3038            "0x0000000000000000000000000000000000000000"
3039        );
3040    }
3041
3042    // --- Client Tests ---
3043
3044    #[test]
3045    fn test_relayer_config_default() {
3046        let config = RelayerConfig::default();
3047        assert_eq!(config.base_url, RELAYER_API_BASE);
3048        assert_eq!(config.timeout, Duration::from_secs(60));
3049        assert_eq!(config.rate_limit_per_second, 2);
3050    }
3051
3052    #[test]
3053    fn test_relayer_config_builder() {
3054        let config = RelayerConfig::builder()
3055            .with_base_url("https://custom.example.com")
3056            .with_timeout(Duration::from_secs(120))
3057            .with_rate_limit(5);
3058
3059        assert_eq!(config.base_url, "https://custom.example.com");
3060        assert_eq!(config.timeout, Duration::from_secs(120));
3061        assert_eq!(config.rate_limit_per_second, 5);
3062    }
3063}