1use crate::account::BuilderAccount;
2use crate::config::{get_contract_config, BuilderConfig, ContractConfig};
3use crate::error::RelayError;
4use crate::types::{
5 NonceResponse, RelayerTransactionResponse, SafeTransaction, SafeTx, TransactionStatusResponse,
6 WalletType,
7};
8use alloy::hex;
9use alloy::network::TransactionBuilder;
10use alloy::primitives::{keccak256, Address, Bytes, U256};
11use alloy::providers::{Provider, ProviderBuilder};
12use alloy::rpc::types::TransactionRequest;
13use alloy::signers::Signer;
14use alloy::sol_types::{Eip712Domain, SolCall, SolStruct, SolValue};
15use reqwest::Client;
16use serde::Serialize;
17use std::time::{Duration, Instant};
18use url::Url;
19
20const SAFE_INIT_CODE_HASH: &str =
22 "2bce2127ff07fb632d16c8347c4ebf501f4841168bed00d9e6ef715ddb6fcecf";
23
24const PROXY_INIT_CODE_HASH: &str =
26 "0xd21df8dc65880a8606f09fe0ce3df9b8869287ab0b058be05aa9e8af6330a00b";
27
28#[derive(Debug, Clone)]
29pub struct RelayClient {
30 client: Client,
31 base_url: Url,
32 chain_id: u64,
33 account: Option<BuilderAccount>,
34 contract_config: ContractConfig,
35 wallet_type: WalletType,
36}
37
38impl RelayClient {
39 pub fn new(
41 private_key: impl Into<String>,
42 config: Option<BuilderConfig>,
43 ) -> Result<Self, RelayError> {
44 let account = BuilderAccount::new(private_key, config)?;
45 Self::builder()?.with_account(account).build()
46 }
47
48 pub fn builder() -> Result<RelayClientBuilder, RelayError> {
50 RelayClientBuilder::new()
51 }
52
53 pub fn default_builder() -> Result<RelayClientBuilder, RelayError> {
55 Ok(RelayClientBuilder::default())
56 }
57
58 pub fn from_account(account: BuilderAccount) -> Result<Self, RelayError> {
60 Self::builder()?.with_account(account).build()
61 }
62
63 pub fn address(&self) -> Option<Address> {
64 self.account.as_ref().map(|a| a.address())
65 }
66
67 pub async fn ping(&self) -> Result<Duration, RelayError> {
84 let start = Instant::now();
85 let response = self.client.get(self.base_url.clone()).send().await?;
86 let latency = start.elapsed();
87
88 if !response.status().is_success() {
89 let text = response.text().await?;
90 return Err(RelayError::Api(format!("Ping failed: {}", text)));
91 }
92
93 Ok(latency)
94 }
95
96 pub async fn get_nonce(&self, address: Address) -> Result<u64, RelayError> {
97 let url = self.base_url.join(&format!(
98 "nonce?address={}&type={}",
99 address,
100 self.wallet_type.as_str()
101 ))?;
102 let resp = self.client.get(url).send().await?;
103
104 if !resp.status().is_success() {
105 let text = resp.text().await?;
106 return Err(RelayError::Api(format!("get_nonce failed: {}", text)));
107 }
108
109 let data = resp.json::<NonceResponse>().await?;
110 Ok(data.nonce)
111 }
112
113 pub async fn get_transaction(
114 &self,
115 transaction_id: &str,
116 ) -> Result<TransactionStatusResponse, RelayError> {
117 let url = self
118 .base_url
119 .join(&format!("transaction?id={}", transaction_id))?;
120 let resp = self.client.get(url).send().await?;
121
122 if !resp.status().is_success() {
123 let text = resp.text().await?;
124 return Err(RelayError::Api(format!("get_transaction failed: {}", text)));
125 }
126
127 resp.json::<TransactionStatusResponse>()
128 .await
129 .map_err(Into::into)
130 }
131
132 pub async fn get_deployed(&self, safe_address: Address) -> Result<bool, RelayError> {
133 #[derive(serde::Deserialize)]
134 struct DeployedResponse {
135 deployed: bool,
136 }
137 let url = self
138 .base_url
139 .join(&format!("deployed?address={}", safe_address))?;
140 let resp = self.client.get(url).send().await?;
141
142 if !resp.status().is_success() {
143 let text = resp.text().await?;
144 return Err(RelayError::Api(format!("get_deployed failed: {}", text)));
145 }
146
147 let data = resp.json::<DeployedResponse>().await?;
148 Ok(data.deployed)
149 }
150
151 fn derive_safe_address(&self, owner: Address) -> Address {
152 let salt = keccak256(owner.abi_encode());
153 let init_code_hash = hex::decode(SAFE_INIT_CODE_HASH).unwrap();
154
155 let mut input = Vec::new();
157 input.push(0xff);
158 input.extend_from_slice(self.contract_config.safe_factory.as_slice());
159 input.extend_from_slice(salt.as_slice());
160 input.extend_from_slice(&init_code_hash);
161
162 let hash = keccak256(input);
163 Address::from_slice(&hash[12..])
164 }
165
166 pub fn get_expected_safe(&self) -> Result<Address, RelayError> {
167 let account = self.account.as_ref().ok_or(RelayError::MissingSigner)?;
168 Ok(self.derive_safe_address(account.address()))
169 }
170
171 fn derive_proxy_wallet(&self, owner: Address) -> Result<Address, RelayError> {
172 let proxy_factory = self.contract_config.proxy_factory.ok_or_else(|| {
173 RelayError::Api("Proxy wallet not supported on this chain".to_string())
174 })?;
175
176 let salt = keccak256(owner.as_slice());
179
180 let init_code_hash = hex::decode(PROXY_INIT_CODE_HASH).unwrap();
181
182 let mut input = Vec::new();
184 input.push(0xff);
185 input.extend_from_slice(proxy_factory.as_slice());
186 input.extend_from_slice(salt.as_slice());
187 input.extend_from_slice(&init_code_hash);
188
189 let hash = keccak256(input);
190 Ok(Address::from_slice(&hash[12..]))
191 }
192
193 pub fn get_expected_proxy_wallet(&self) -> Result<Address, RelayError> {
194 let account = self.account.as_ref().ok_or(RelayError::MissingSigner)?;
195 self.derive_proxy_wallet(account.address())
196 }
197
198 pub async fn get_relay_payload(&self, address: Address) -> Result<(Address, u64), RelayError> {
200 #[derive(serde::Deserialize)]
201 struct RelayPayload {
202 address: String,
203 #[serde(deserialize_with = "crate::types::deserialize_nonce")]
204 nonce: u64,
205 }
206
207 let url = self
208 .base_url
209 .join(&format!("relay-payload?address={}&type=PROXY", address))?;
210 let resp = self.client.get(url).send().await?;
211
212 if !resp.status().is_success() {
213 let text = resp.text().await?;
214 return Err(RelayError::Api(format!(
215 "get_relay_payload failed: {}",
216 text
217 )));
218 }
219
220 let data = resp.json::<RelayPayload>().await?;
221 let relay_address: Address = data
222 .address
223 .parse()
224 .map_err(|e| RelayError::Api(format!("Invalid relay address: {}", e)))?;
225 Ok((relay_address, data.nonce))
226 }
227
228 #[allow(clippy::too_many_arguments)]
230 fn create_proxy_struct_hash(
231 &self,
232 from: Address,
233 to: Address,
234 data: &[u8],
235 tx_fee: U256,
236 gas_price: U256,
237 gas_limit: U256,
238 nonce: u64,
239 relay_hub: Address,
240 relay: Address,
241 ) -> [u8; 32] {
242 let mut message = Vec::new();
243
244 message.extend_from_slice(b"rlx:");
246 message.extend_from_slice(from.as_slice());
248 message.extend_from_slice(to.as_slice());
250 message.extend_from_slice(data);
252 message.extend_from_slice(&tx_fee.to_be_bytes::<32>());
254 message.extend_from_slice(&gas_price.to_be_bytes::<32>());
256 message.extend_from_slice(&gas_limit.to_be_bytes::<32>());
258 message.extend_from_slice(&U256::from(nonce).to_be_bytes::<32>());
260 message.extend_from_slice(relay_hub.as_slice());
262 message.extend_from_slice(relay.as_slice());
264
265 keccak256(&message).into()
266 }
267
268 fn encode_proxy_transaction_data(&self, txns: &[SafeTransaction]) -> Vec<u8> {
270 alloy::sol! {
274 struct ProxyTransaction {
275 uint8 typeCode;
276 address to;
277 uint256 value;
278 bytes data;
279 }
280 function proxy(ProxyTransaction[] txns);
281 }
282
283 let proxy_txns: Vec<ProxyTransaction> = txns
284 .iter()
285 .map(|tx| {
286 ProxyTransaction {
287 typeCode: 1, to: tx.to,
289 value: tx.value,
290 data: tx.data.clone(),
291 }
292 })
293 .collect();
294
295 let call = proxyCall { txns: proxy_txns };
297 call.abi_encode()
298 }
299
300 fn create_safe_multisend_transaction(&self, txns: &[SafeTransaction]) -> SafeTransaction {
301 if txns.len() == 1 {
302 return txns[0].clone();
303 }
304
305 let mut encoded_txns = Vec::new();
306 for tx in txns {
307 let mut packed = Vec::new();
309 packed.push(tx.operation);
310 packed.extend_from_slice(tx.to.as_slice());
311 packed.extend_from_slice(&tx.value.to_be_bytes::<32>());
312 packed.extend_from_slice(&U256::from(tx.data.len()).to_be_bytes::<32>());
313 packed.extend_from_slice(&tx.data);
314 encoded_txns.extend_from_slice(&packed);
315 }
316
317 let mut data = hex::decode("8d80ff0a").unwrap();
320
321 let multisend_data = (Bytes::from(encoded_txns),).abi_encode();
323 data.extend_from_slice(&multisend_data);
324
325 SafeTransaction {
326 to: self.contract_config.safe_multisend,
327 operation: 1, data: data.into(),
329 value: U256::ZERO,
330 }
331 }
332
333 fn split_and_pack_sig_safe(&self, sig: alloy::primitives::Signature) -> String {
334 let v_raw = if sig.v() { 1u8 } else { 0u8 };
337 let v = v_raw + 31;
338
339 let mut packed = Vec::new();
341 packed.extend_from_slice(&sig.r().to_be_bytes::<32>());
342 packed.extend_from_slice(&sig.s().to_be_bytes::<32>());
343 packed.push(v);
344
345 format!("0x{}", hex::encode(packed))
346 }
347
348 fn split_and_pack_sig_proxy(&self, sig: alloy::primitives::Signature) -> String {
349 let v = if sig.v() { 28u8 } else { 27u8 };
351
352 let mut packed = Vec::new();
354 packed.extend_from_slice(&sig.r().to_be_bytes::<32>());
355 packed.extend_from_slice(&sig.s().to_be_bytes::<32>());
356 packed.push(v);
357
358 format!("0x{}", hex::encode(packed))
359 }
360
361 pub async fn execute(
362 &self,
363 transactions: Vec<SafeTransaction>,
364 metadata: Option<String>,
365 ) -> Result<RelayerTransactionResponse, RelayError> {
366 self.execute_with_gas(transactions, metadata, None).await
367 }
368
369 pub async fn execute_with_gas(
370 &self,
371 transactions: Vec<SafeTransaction>,
372 metadata: Option<String>,
373 gas_limit: Option<u64>,
374 ) -> Result<RelayerTransactionResponse, RelayError> {
375 match self.wallet_type {
376 WalletType::Safe => self.execute_safe(transactions, metadata).await,
377 WalletType::Proxy => self.execute_proxy(transactions, metadata, gas_limit).await,
378 }
379 }
380
381 async fn execute_safe(
382 &self,
383 transactions: Vec<SafeTransaction>,
384 metadata: Option<String>,
385 ) -> Result<RelayerTransactionResponse, RelayError> {
386 let account = self.account.as_ref().ok_or(RelayError::MissingSigner)?;
387 let from_address = account.address();
388
389 let safe_address = self.derive_safe_address(from_address);
390
391 if !self.get_deployed(safe_address).await? {
392 return Err(RelayError::Api(format!(
393 "Safe {} is not deployed",
394 safe_address
395 )));
396 }
397
398 let nonce = self.get_nonce(from_address).await?;
399
400 let aggregated = self.create_safe_multisend_transaction(&transactions);
401
402 let safe_tx = SafeTx {
403 to: aggregated.to,
404 value: aggregated.value,
405 data: aggregated.data,
406 operation: aggregated.operation,
407 safeTxGas: U256::ZERO,
408 baseGas: U256::ZERO,
409 gasPrice: U256::ZERO,
410 gasToken: Address::ZERO,
411 refundReceiver: Address::ZERO,
412 nonce: U256::from(nonce),
413 };
414
415 let domain = Eip712Domain {
416 name: None,
417 version: None,
418 chain_id: Some(U256::from(self.chain_id)),
419 verifying_contract: Some(safe_address),
420 salt: None,
421 };
422
423 let struct_hash = safe_tx.eip712_signing_hash(&domain);
424 let signature = account
425 .signer()
426 .sign_message(struct_hash.as_slice())
427 .await
428 .map_err(|e| RelayError::Signer(e.to_string()))?;
429 let packed_sig = self.split_and_pack_sig_safe(signature);
430
431 #[derive(Serialize)]
432 struct SigParams {
433 #[serde(rename = "gasPrice")]
434 gas_price: String,
435 operation: String,
436 #[serde(rename = "safeTxnGas")]
437 safe_tx_gas: String,
438 #[serde(rename = "baseGas")]
439 base_gas: String,
440 #[serde(rename = "gasToken")]
441 gas_token: String,
442 #[serde(rename = "refundReceiver")]
443 refund_receiver: String,
444 }
445
446 #[derive(Serialize)]
447 struct Body {
448 #[serde(rename = "type")]
449 type_: String,
450 from: String,
451 to: String,
452 #[serde(rename = "proxyWallet")]
453 proxy_wallet: String,
454 data: String,
455 signature: String,
456 #[serde(rename = "signatureParams")]
457 signature_params: SigParams,
458 #[serde(rename = "value")]
459 value: String,
460 nonce: String,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 metadata: Option<String>,
463 }
464
465 let body = Body {
466 type_: "SAFE".to_string(),
467 from: from_address.to_string(),
468 to: safe_tx.to.to_string(),
469 proxy_wallet: safe_address.to_string(),
470 data: safe_tx.data.to_string(),
471 signature: packed_sig,
472 signature_params: SigParams {
473 gas_price: "0".to_string(),
474 operation: safe_tx.operation.to_string(),
475 safe_tx_gas: "0".to_string(),
476 base_gas: "0".to_string(),
477 gas_token: Address::ZERO.to_string(),
478 refund_receiver: Address::ZERO.to_string(),
479 },
480 value: safe_tx.value.to_string(),
481 nonce: nonce.to_string(),
482 metadata,
483 };
484
485 self._post_request("submit", &body).await
486 }
487
488 async fn execute_proxy(
489 &self,
490 transactions: Vec<SafeTransaction>,
491 metadata: Option<String>,
492 gas_limit: Option<u64>,
493 ) -> Result<RelayerTransactionResponse, RelayError> {
494 let account = self.account.as_ref().ok_or(RelayError::MissingSigner)?;
495 let from_address = account.address();
496
497 let proxy_wallet = self.derive_proxy_wallet(from_address)?;
498 let relay_hub = self
499 .contract_config
500 .relay_hub
501 .ok_or_else(|| RelayError::Api("Relay hub not configured".to_string()))?;
502 let proxy_factory = self
503 .contract_config
504 .proxy_factory
505 .ok_or_else(|| RelayError::Api("Proxy factory not configured".to_string()))?;
506
507 let (relay_address, nonce) = self.get_relay_payload(from_address).await?;
509
510 let encoded_data = self.encode_proxy_transaction_data(&transactions);
512
513 let tx_fee = U256::ZERO;
515 let gas_price = U256::ZERO;
516 let gas_limit = U256::from(gas_limit.unwrap_or(10_000_000u64));
517
518 let struct_hash = self.create_proxy_struct_hash(
524 from_address,
525 proxy_factory, &encoded_data,
527 tx_fee,
528 gas_price,
529 gas_limit,
530 nonce,
531 relay_hub,
532 relay_address,
533 );
534
535 let signature = account
537 .signer()
538 .sign_message(&struct_hash)
539 .await
540 .map_err(|e| RelayError::Signer(e.to_string()))?;
541 let packed_sig = self.split_and_pack_sig_proxy(signature);
542
543 #[derive(Serialize)]
544 struct SigParams {
545 #[serde(rename = "relayerFee")]
546 relayer_fee: String,
547 #[serde(rename = "gasLimit")]
548 gas_limit: String,
549 #[serde(rename = "gasPrice")]
550 gas_price: String,
551 #[serde(rename = "relayHub")]
552 relay_hub: String,
553 relay: String,
554 }
555
556 #[derive(Serialize)]
557 struct Body {
558 #[serde(rename = "type")]
559 type_: String,
560 from: String,
561 to: String,
562 #[serde(rename = "proxyWallet")]
563 proxy_wallet: String,
564 data: String,
565 signature: String,
566 #[serde(rename = "signatureParams")]
567 signature_params: SigParams,
568 nonce: String,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 metadata: Option<String>,
571 }
572
573 let body = Body {
574 type_: "PROXY".to_string(),
575 from: from_address.to_string(),
576 to: proxy_factory.to_string(),
577 proxy_wallet: proxy_wallet.to_string(),
578 data: format!("0x{}", hex::encode(&encoded_data)),
579 signature: packed_sig,
580 signature_params: SigParams {
581 relayer_fee: "0".to_string(),
582 gas_limit: gas_limit.to_string(),
583 gas_price: "0".to_string(),
584 relay_hub: relay_hub.to_string(),
585 relay: relay_address.to_string(),
586 },
587 nonce: nonce.to_string(),
588 metadata,
589 };
590
591 self._post_request("submit", &body).await
592 }
593
594 pub async fn estimate_redemption_gas(
632 &self,
633 condition_id: [u8; 32],
634 index_sets: Vec<U256>,
635 ) -> Result<u64, RelayError> {
636 alloy::sol! {
638 function redeemPositions(address collateral, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets);
639 }
640
641 let collateral =
643 Address::parse_checksummed("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", None)
644 .map_err(|e| RelayError::Api(format!("Invalid collateral address: {}", e)))?;
645 let ctf_exchange =
646 Address::parse_checksummed("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", None)
647 .map_err(|e| RelayError::Api(format!("Invalid CTF exchange address: {}", e)))?;
648 let parent_collection_id = [0u8; 32];
649
650 let call = redeemPositionsCall {
652 collateral,
653 parentCollectionId: parent_collection_id.into(),
654 conditionId: condition_id.into(),
655 indexSets: index_sets,
656 };
657 let redemption_calldata = Bytes::from(call.abi_encode());
658
659 let proxy_wallet = match self.wallet_type {
661 WalletType::Proxy => self.get_expected_proxy_wallet()?,
662 WalletType::Safe => self.get_expected_safe()?,
663 };
664
665 let provider = ProviderBuilder::new().connect_http(
667 self.contract_config
668 .rpc_url
669 .parse()
670 .map_err(|e| RelayError::Api(format!("Invalid RPC URL: {}", e)))?,
671 );
672
673 let tx = TransactionRequest::default()
675 .with_from(proxy_wallet)
676 .with_to(ctf_exchange)
677 .with_input(redemption_calldata);
678
679 let inner_gas_used = provider
681 .estimate_gas(tx)
682 .await
683 .map_err(|e| RelayError::Api(format!("Gas estimation failed: {}", e)))?;
684
685 let relayer_overhead: u64 = 50_000;
687 let safe_gas_limit = (inner_gas_used + relayer_overhead) * 120 / 100;
688
689 Ok(safe_gas_limit)
690 }
691
692 pub async fn submit_gasless_redemption(
693 &self,
694 condition_id: [u8; 32],
695 index_sets: Vec<alloy::primitives::U256>,
696 ) -> Result<RelayerTransactionResponse, RelayError> {
697 self.submit_gasless_redemption_with_gas_estimation(condition_id, index_sets, false)
698 .await
699 }
700
701 pub async fn submit_gasless_redemption_with_gas_estimation(
702 &self,
703 condition_id: [u8; 32],
704 index_sets: Vec<alloy::primitives::U256>,
705 estimate_gas: bool,
706 ) -> Result<RelayerTransactionResponse, RelayError> {
707 alloy::sol! {
709 function redeemPositions(address collateral, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets);
710 }
711
712 let collateral =
715 Address::parse_checksummed("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", None).unwrap();
716 let ctf_exchange =
718 Address::parse_checksummed("0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", None).unwrap();
719 let parent_collection_id = [0u8; 32];
720
721 let call = redeemPositionsCall {
723 collateral,
724 parentCollectionId: parent_collection_id.into(),
725 conditionId: condition_id.into(),
726 indexSets: index_sets.clone(),
727 };
728 let data = call.abi_encode();
729
730 let gas_limit = if estimate_gas {
732 Some(
733 self.estimate_redemption_gas(condition_id, index_sets.clone())
734 .await?,
735 )
736 } else {
737 None
738 };
739
740 let tx = SafeTransaction {
742 to: ctf_exchange,
743 value: U256::ZERO,
744 data: data.into(),
745 operation: 0, };
747
748 self.execute_with_gas(vec![tx], None, gas_limit).await
751 }
752
753 async fn _post_request<T: Serialize>(
754 &self,
755 endpoint: &str,
756 body: &T,
757 ) -> Result<RelayerTransactionResponse, RelayError> {
758 let url = self.base_url.join(endpoint)?;
759 let body_str = serde_json::to_string(body)?;
760
761 eprintln!("DEBUG POST {} path={}", url, url.path());
762 eprintln!("DEBUG body: {}", body_str);
763 tracing::debug!("POST {} with body: {}", url, body_str);
764
765 let mut headers = if let Some(account) = &self.account {
766 if let Some(config) = account.config() {
767 config
768 .generate_relayer_v2_headers("POST", url.path(), Some(&body_str))
769 .map_err(RelayError::Api)?
770 } else {
771 return Err(RelayError::Api(
772 "Builder config missing - cannot authenticate request".to_string(),
773 ));
774 }
775 } else {
776 return Err(RelayError::Api(
777 "Account missing - cannot authenticate request".to_string(),
778 ));
779 };
780
781 headers.insert(
782 reqwest::header::CONTENT_TYPE,
783 reqwest::header::HeaderValue::from_static("application/json"),
784 );
785
786 let resp = self
787 .client
788 .post(url.clone())
789 .headers(headers)
790 .body(body_str.clone())
791 .send()
792 .await?;
793
794 let status = resp.status();
795 tracing::debug!("Response status for {}: {}", endpoint, status);
796
797 if !status.is_success() {
798 let text = resp.text().await?;
799 tracing::error!(
800 "Request to {} failed with status {}: {}",
801 endpoint,
802 status,
803 text
804 );
805 return Err(RelayError::Api(format!("Request failed: {}", text)));
806 }
807
808 let response_text = resp.text().await?;
810 tracing::debug!("Raw response body from {}: {}", endpoint, response_text);
811
812 serde_json::from_str(&response_text).map_err(|e| {
814 tracing::error!(
815 "Failed to decode response from {}: {}. Raw body: {}",
816 endpoint,
817 e,
818 response_text
819 );
820 RelayError::SerdeJson(e)
821 })
822 }
823}
824
825pub struct RelayClientBuilder {
826 base_url: String,
827 chain_id: u64,
828 account: Option<BuilderAccount>,
829 wallet_type: WalletType,
830}
831
832impl Default for RelayClientBuilder {
833 fn default() -> Self {
834 let relayer_url = std::env::var("RELAYER_URL")
835 .unwrap_or_else(|_| "https://relayer-v2.polymarket.com/".to_string());
836 let chain_id = std::env::var("CHAIN_ID")
837 .unwrap_or("137".to_string())
838 .parse::<u64>()
839 .unwrap_or(137);
840
841 Self::new()
842 .unwrap()
843 .url(&relayer_url)
844 .unwrap()
845 .chain_id(chain_id)
846 }
847}
848
849impl RelayClientBuilder {
850 pub fn new() -> Result<Self, RelayError> {
851 let mut base_url = Url::parse("https://relayer-v2.polymarket.com")?;
852 if !base_url.path().ends_with('/') {
853 base_url.set_path(&format!("{}/", base_url.path()));
854 }
855
856 Ok(Self {
857 base_url: base_url.to_string(),
858 chain_id: 137,
859 account: None,
860 wallet_type: WalletType::default(),
861 })
862 }
863
864 pub fn chain_id(mut self, chain_id: u64) -> Self {
865 self.chain_id = chain_id;
866 self
867 }
868
869 pub fn url(mut self, url: &str) -> Result<Self, RelayError> {
870 let mut base_url = Url::parse(url)?;
871 if !base_url.path().ends_with('/') {
872 base_url.set_path(&format!("{}/", base_url.path()));
873 }
874 self.base_url = base_url.to_string();
875 Ok(self)
876 }
877
878 pub fn with_account(mut self, account: BuilderAccount) -> Self {
879 self.account = Some(account);
880 self
881 }
882
883 pub fn wallet_type(mut self, wallet_type: WalletType) -> Self {
884 self.wallet_type = wallet_type;
885 self
886 }
887
888 pub fn build(self) -> Result<RelayClient, RelayError> {
889 let mut base_url = Url::parse(&self.base_url)?;
890 if !base_url.path().ends_with('/') {
891 base_url.set_path(&format!("{}/", base_url.path()));
892 }
893
894 let contract_config = get_contract_config(self.chain_id)
895 .ok_or_else(|| RelayError::Api(format!("Unsupported chain ID: {}", self.chain_id)))?;
896
897 Ok(RelayClient {
898 client: Client::new(),
899 base_url,
900 chain_id: self.chain_id,
901 account: self.account,
902 contract_config,
903 wallet_type: self.wallet_type,
904 })
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[tokio::test]
913 async fn test_ping() {
914 let client = RelayClient::builder().unwrap().build().unwrap();
915 let result = client.ping().await;
916 assert!(result.is_ok(), "ping failed: {:?}", result.err());
917 }
918}