1use 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
53pub const SAFE_FACTORY: &str = "0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b";
59
60pub const SAFE_INIT_CODE_HASH: &str =
62 "0x2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
63
64pub const DEFAULT_POLYGON_RPC: &str = "https://polygon-rpc.com";
66
67pub const USDC_CONTRACT_ADDRESS: &str = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
69
70pub const NATIVE_USDC_CONTRACT_ADDRESS: &str = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359";
72
73pub const CONDITIONAL_TOKENS_ADDRESS: &str = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045";
77
78pub const CTF_EXCHANGE_ADDRESS: &str = CONDITIONAL_TOKENS_ADDRESS;
80
81pub const EXCHANGE_ADDRESS: &str = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E";
84
85pub const NEG_RISK_CTF_EXCHANGE_ADDRESS: &str = "0xC5d563A36AE78145C45a50134d48A1215220f80a";
88
89pub const NEG_RISK_ADAPTER_ADDRESS: &str = "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296";
93
94const CREATE_PROXY_TYPE_STR: &str =
97 "CreateProxy(address paymentToken,uint256 payment,address paymentReceiver)";
98
99const DOMAIN_TYPE_STR: &str = "EIP712Domain(string name,uint256 chainId,address verifyingContract)";
101
102const DOMAIN_NAME: &str = "Polymarket Contract Proxy Factory";
104
105const DEFAULT_CHAIN_ID: u64 = 137;
107
108fn 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 let domain_type_hash = keccak256(DOMAIN_TYPE_STR.as_bytes());
127
128 let create_proxy_type_hash = keccak256(CREATE_PROXY_TYPE_STR.as_bytes());
130
131 let name_hash = keccak256(DOMAIN_NAME.as_bytes());
133
134 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173pub enum TransactionType {
174 #[serde(rename = "SAFE")]
176 Safe,
177 #[serde(rename = "SAFE-CREATE")]
179 SafeCreate,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184pub enum TransactionState {
185 #[serde(rename = "STATE_NEW")]
187 New,
188 #[serde(rename = "STATE_EXECUTED")]
190 Executed,
191 #[serde(rename = "STATE_MINED")]
193 Mined,
194 #[serde(rename = "STATE_CONFIRMED")]
196 Confirmed,
197 #[serde(rename = "STATE_FAILED")]
199 Failed,
200 #[serde(rename = "STATE_INVALID")]
202 Invalid,
203}
204
205impl TransactionState {
206 #[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 #[must_use]
224 pub const fn is_success(&self) -> bool {
225 matches!(self, Self::Mined | Self::Confirmed)
226 }
227}
228
229#[cfg(test)]
233mod manual_debug {
234 use super::*;
235
236 #[tokio::test]
240 async fn fetch_transaction_status_from_env() {
241 let tx_id = "019ad6a5-fe80-7b44-a075-2af31ea399dd";
243
244 let cfg = RelayerConfig::from_env();
246 let mut client = RelayerClient::new(cfg).expect("create relayer client");
247
248 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct TransactionRequest {
269 #[serde(rename = "type")]
271 pub r#type: TransactionType,
272 pub from: String,
274 pub to: String,
276 #[serde(skip_serializing_if = "Option::is_none", rename = "proxyWallet")]
278 pub proxy_wallet: Option<String>,
279 pub data: String,
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub nonce: Option<String>,
284 pub signature: String,
286 #[serde(rename = "signatureParams")]
288 pub signature_params: SignatureParams,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub metadata: Option<String>,
292}
293
294#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296pub struct SignatureParams {
297 #[serde(skip_serializing_if = "Option::is_none", rename = "paymentToken")]
299 pub payment_token: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub payment: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none", rename = "paymentReceiver")]
305 pub payment_receiver: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub operation: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none", rename = "safeTxnGas")]
311 pub safe_tx_gas: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none", rename = "baseGas")]
314 pub base_gas: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none", rename = "gasPrice")]
317 pub gas_price: Option<String>,
318 #[serde(skip_serializing_if = "Option::is_none", rename = "gasToken")]
320 pub gas_token: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none", rename = "refundReceiver")]
323 pub refund_receiver: Option<String>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct TransactionReceipt {
329 #[serde(
331 alias = "transactionID",
332 alias = "transactionId",
333 alias = "transaction_id",
334 alias = "id"
335 )]
336 pub id: String,
337 #[serde(alias = "status")]
339 pub state: TransactionState,
340 #[serde(
342 alias = "transactionHash",
343 alias = "txHash",
344 skip_serializing_if = "Option::is_none"
345 )]
346 pub transaction_hash: Option<String>,
347 #[serde(alias = "hash", skip_serializing_if = "Option::is_none")]
349 pub hash: Option<String>,
350 #[serde(alias = "proxyWallet", skip_serializing_if = "Option::is_none")]
352 pub proxy_address: Option<String>,
353 #[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 #[serde(skip_serializing_if = "Option::is_none")]
360 pub error: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub created_at: Option<String>,
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub updated_at: Option<String>,
367}
368
369#[derive(Debug, Clone)]
371pub struct BuilderApiCredentials {
372 pub api_key: String,
374 pub secret: String,
376 pub passphrase: String,
378}
379
380impl BuilderApiCredentials {
381 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "lowercase")]
413pub enum NonceType {
414 Transaction,
416 SafeCreate,
418}
419
420pub fn derive_safe_address(owner: &str) -> Result<String> {
449 derive_safe_address_with_factory(owner, SAFE_FACTORY)
450}
451
452pub 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 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 let safe_addr = compute_create2_address(factory_addr, salt, init_code_hash);
477
478 Ok(format!("{safe_addr:?}"))
479}
480
481fn 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
493pub 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 match v {
540 0 | 1 => v += 27, 27 | 28 => {} _ => {
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 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 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 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
582pub 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 let v = match original_v {
625 0 | 1 => original_v + 31, 27 | 28 => original_v + 4, 31 | 32 => original_v, _ => {
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 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 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
671pub fn verify_signature(signature: &str, digest: &str, expected_address: &str) -> Result<String> {
684 let digest_bytes: B256 = digest
686 .parse()
687 .map_err(|e| PolymarketError::validation(format!("Invalid digest: {e}")))?;
688
689 let sig: AlloySignature = signature
691 .parse()
692 .map_err(|e| PolymarketError::validation(format!("Invalid signature format: {e}")))?;
693
694 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#[derive(Debug, Clone, Serialize, Deserialize)]
727pub struct SafeCreateTypedData {
728 pub domain: SafeCreateDomain,
730 pub message: SafeCreateMessage,
732 pub primary_type: String,
734 pub types: SafeCreateTypes,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct SafeCreateDomain {
741 pub name: String,
743 #[serde(rename = "chainId")]
745 pub chain_id: u64,
746 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
764pub struct SafeCreateMessage {
765 #[serde(rename = "paymentToken")]
767 pub payment_token: String,
768 pub payment: String,
770 #[serde(rename = "paymentReceiver")]
772 pub payment_receiver: String,
773}
774
775impl SafeCreateMessage {
776 #[must_use]
778 pub fn new(_owner: &str) -> Self {
779 Self {
781 payment_token: "0x0000000000000000000000000000000000000000".to_string(),
782 payment: "0".to_string(),
783 payment_receiver: "0x0000000000000000000000000000000000000000".to_string(),
784 }
785 }
786
787 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
805pub struct SafeCreateTypes {
806 #[serde(rename = "EIP712Domain")]
808 pub eip712_domain: Vec<TypedDataField>,
809 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
851pub struct TypedDataField {
852 pub name: String,
854 pub r#type: String,
856}
857
858pub fn build_safe_create_typed_data(
879 owner: &str,
880 chain_id: Option<u64>,
881) -> Result<SafeCreateTypedData> {
882 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
898pub 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 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 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#[derive(Debug, Clone)]
987pub struct RelayerConfig {
988 pub base_url: String,
990 pub data_api_base_url: String,
992 pub timeout: Duration,
994 pub rate_limit_per_second: u32,
996 pub user_agent: String,
998}
999
1000impl Default for RelayerConfig {
1001 fn default() -> Self {
1002 Self {
1003 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 #[must_use]
1016 pub fn builder() -> Self {
1017 Self::default()
1018 }
1019
1020 #[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 #[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 #[must_use]
1036 pub fn with_timeout(mut self, timeout: Duration) -> Self {
1037 self.timeout = timeout;
1038 self
1039 }
1040
1041 #[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 #[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 #[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#[derive(Debug, Serialize)]
1072#[allow(dead_code)]
1073struct DeploySafeRequest {
1074 owner: String,
1075}
1076
1077#[derive(Debug, Deserialize)]
1079pub struct DeploySafeResponse {
1080 #[serde(alias = "transactionHash", alias = "hash")]
1083 pub transaction_hash: Option<String>,
1084 #[serde(alias = "proxyAddress", alias = "proxy_address")]
1086 pub proxy_address: Option<String>,
1087 pub status: Option<String>,
1089 pub error: Option<String>,
1091}
1092
1093#[derive(Clone)]
1095pub struct RelayerClient {
1096 config: RelayerConfig,
1097 client: Client,
1098 rate_limiter: Arc<RateLimiter>,
1099 builder_credentials: Option<BuilderApiCredentials>,
1100 default_rpc: Option<String>,
1102}
1103
1104impl RelayerClient {
1105 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 pub fn with_defaults() -> Result<Self> {
1130 Self::new(RelayerConfig::default())
1131 }
1132
1133 #[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 #[must_use]
1144 pub fn with_builder_credentials(mut self, credentials: BuilderApiCredentials) -> Self {
1145 self.builder_credentials = Some(credentials);
1146 self
1147 }
1148
1149 #[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 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 let addr: Address = proxy_address
1176 .parse()
1177 .map_err(|e| PolymarketError::validation(format!("Invalid proxy address: {e}")))?;
1178
1179 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 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 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 let wallet_addr: Address = address
1233 .parse()
1234 .map_err(|e| PolymarketError::validation(format!("Invalid wallet address: {e}")))?;
1235
1236 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 async fn query_erc20_balance_rpc(
1259 &self,
1260 rpc_url: &str,
1261 token_contract: &str,
1262 wallet: &Address,
1263 ) -> Result<f64> {
1264 let mut call_data = vec![0x70, 0xa0, 0x82, 0x31]; 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 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 if let Some(error) = json.get("error") {
1309 return Err(PolymarketError::internal(format!("RPC error: {}", error)));
1310 }
1311
1312 let result_hex = json["result"]
1314 .as_str()
1315 .ok_or_else(|| PolymarketError::parse("Missing result in RPC response"))?;
1316
1317 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 let balance = balance_raw.to::<u128>() as f64 / 1_000_000.0;
1329
1330 Ok(balance)
1331 }
1332
1333 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 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 #[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 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 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 let mut req_builder = self.client.post(&url);
1420
1421 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 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 #[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 #[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 #[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 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 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 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 let packed_signature = pack_signature(signature)?;
1611 debug!(original_sig = %signature, packed_sig = %packed_signature, "Signature packed");
1612
1613 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 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 #[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 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 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 #[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 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 #[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 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 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 #[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 #[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 let nonce_type_str = match nonce_type {
1854 NonceType::Transaction => "SAFE",
1855 NonceType::SafeCreate => "SAFECREATE",
1856 };
1857
1858 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 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 #[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 #[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 #[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 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 #[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 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 Ok(result_bytes.last() == Some(&1))
2060 }
2061
2062 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 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 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 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 let ctf_approved = self
2154 .check_ctf_approval(proxy_wallet, targets.ctf_operator)
2155 .await?;
2156
2157 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#[derive(Debug, Clone)]
2176pub struct ApprovalStatus {
2177 pub usdc_approved: bool,
2179 pub usdc_allowance: U256,
2181 pub ctf_approved: bool,
2183 pub adapter_approved: bool,
2185 pub all_approved: bool,
2187}
2188
2189impl ApprovalStatus {
2190 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#[allow(dead_code)]
2212const SAFE_DOMAIN_NAME: &str = "Gnosis Safe";
2213
2214#[allow(dead_code)]
2216const SAFE_DOMAIN_VERSION: &str = "1.3.0";
2217
2218const 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
2221const SAFE_DOMAIN_TYPE_STR: &str = "EIP712Domain(uint256 chainId,address verifyingContract)";
2223
2224const ERC20_TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];
2226
2227const ERC20_APPROVE_SELECTOR: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];
2229
2230const ERC20_ALLOWANCE_SELECTOR: [u8; 4] = [0xdd, 0x62, 0xed, 0x3e];
2232
2233const ERC1155_SET_APPROVAL_FOR_ALL_SELECTOR: [u8; 4] = [0xa2, 0x2c, 0xb4, 0x65];
2235
2236const ERC1155_IS_APPROVED_FOR_ALL_SELECTOR: [u8; 4] = [0xe9, 0x85, 0xe9, 0xc5];
2238
2239#[derive(Debug, Clone, Serialize, Deserialize)]
2241pub struct SafeTxTypedData {
2242 pub domain: SafeTxDomain,
2244 pub message: SafeTxMessage,
2246 #[serde(rename = "primaryType")]
2248 pub primary_type: String,
2249 pub types: SafeTxTypes,
2251}
2252
2253#[derive(Debug, Clone, Serialize, Deserialize)]
2255pub struct SafeTxDomain {
2256 #[serde(rename = "chainId")]
2258 pub chain_id: u64,
2259 #[serde(rename = "verifyingContract")]
2261 pub verifying_contract: String,
2262}
2263
2264#[derive(Debug, Clone, Serialize, Deserialize)]
2266pub struct SafeTxMessage {
2267 pub to: String,
2269 pub value: String,
2271 pub data: String,
2273 pub operation: u8,
2275 #[serde(rename = "safeTxGas")]
2277 pub safe_tx_gas: String,
2278 #[serde(rename = "baseGas")]
2280 pub base_gas: String,
2281 #[serde(rename = "gasPrice")]
2283 pub gas_price: String,
2284 #[serde(rename = "gasToken")]
2286 pub gas_token: String,
2287 #[serde(rename = "refundReceiver")]
2289 pub refund_receiver: String,
2290 pub nonce: String,
2292}
2293
2294#[derive(Debug, Clone, Serialize, Deserialize)]
2296pub struct SafeTxTypes {
2297 #[serde(rename = "EIP712Domain")]
2299 pub eip712_domain: Vec<TypedDataField>,
2300 #[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
2364pub 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 calldata.extend_from_slice(&ERC20_TRANSFER_SELECTOR);
2380 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 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
2391pub 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 calldata.extend_from_slice(&ERC20_APPROVE_SELECTOR);
2407 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 calldata.extend_from_slice(&amount.to_be_bytes::<32>());
2413
2414 Ok(format!("0x{}", hex::encode(&calldata)))
2415}
2416
2417pub 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 calldata.extend_from_slice(&ERC20_ALLOWANCE_SELECTOR);
2436 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 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
2448pub 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 calldata.extend_from_slice(&ERC1155_SET_APPROVAL_FOR_ALL_SELECTOR);
2464 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 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
2476pub 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 calldata.extend_from_slice(&ERC1155_IS_APPROVED_FOR_ALL_SELECTOR);
2495 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 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
2507pub 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 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 let amount_raw = (amount_usdc * 1_000_000.0) as u128;
2537
2538 let usdc_contract = if use_native_usdc {
2540 NATIVE_USDC_CONTRACT_ADDRESS
2541 } else {
2542 USDC_CONTRACT_ADDRESS
2543 };
2544
2545 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, 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
2570pub 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 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 let usdc_contract = if use_native_usdc {
2603 NATIVE_USDC_CONTRACT_ADDRESS
2604 } else {
2605 USDC_CONTRACT_ADDRESS
2606 };
2607
2608 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, 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2635pub enum MarketType {
2636 Standard,
2638 NegRisk,
2640}
2641
2642pub 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 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 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, 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#[derive(Debug, Clone)]
2696pub struct ApprovalTargets {
2697 pub usdc_spender: &'static str,
2699 pub ctf_operator: &'static str,
2701 pub usdc_split_spender: Option<&'static str>,
2703 pub ctf_adapter_operator: Option<&'static str>,
2705}
2706
2707impl ApprovalTargets {
2708 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 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 pub fn all() -> Self {
2730 Self {
2731 usdc_spender: EXCHANGE_ADDRESS, ctf_operator: EXCHANGE_ADDRESS,
2733 usdc_split_spender: Some(CONDITIONAL_TOKENS_ADDRESS),
2734 ctf_adapter_operator: None,
2735 }
2736 }
2737
2738 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
2747fn 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 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
2769fn 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 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 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
2850pub 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
2873pub fn build_safe_tx_request(
2887 typed_data: &SafeTxTypedData,
2888 signer: &str,
2889 signature: &str,
2890 nonce: u64,
2891) -> Result<TransactionRequest> {
2892 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#[cfg(test)]
2921mod tests {
2922 use super::*;
2923
2924 #[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 #[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 #[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}