//! Bridge client implementation use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; use serde::{de::DeserializeOwned, Serialize}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use super::error::{BridgeError, Result}; use super::types::*; /// Synor Bridge Client pub struct BridgeClient { config: BridgeConfig, http_client: reqwest::Client, closed: Arc, } impl BridgeClient { /// Create a new bridge client pub fn new(config: BridgeConfig) -> Self { let mut headers = HeaderMap::new(); headers.insert( AUTHORIZATION, HeaderValue::from_str(&format!("Bearer {}", config.api_key)).unwrap(), ); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); headers.insert("X-SDK-Version", HeaderValue::from_static("rust/0.1.0")); let http_client = reqwest::Client::builder() .default_headers(headers) .timeout(Duration::from_secs(config.timeout_secs)) .build() .expect("Failed to create HTTP client"); Self { config, http_client, closed: Arc::new(AtomicBool::new(false)), } } // ==================== Chain Operations ==================== /// Get all supported chains pub async fn get_supported_chains(&self) -> Result> { #[derive(Deserialize)] struct Response { chains: Vec, } let resp: Response = self.request("GET", "/chains", Option::<()>::None).await?; Ok(resp.chains) } /// Get chain by ID pub async fn get_chain(&self, chain_id: ChainId) -> Result { self.request("GET", &format!("/chains/{:?}", chain_id).to_lowercase(), Option::<()>::None).await } /// Check if chain is supported pub async fn is_chain_supported(&self, chain_id: ChainId) -> bool { match self.get_chain(chain_id).await { Ok(chain) => chain.supported, Err(_) => false, } } // ==================== Asset Operations ==================== /// Get supported assets for a chain pub async fn get_supported_assets(&self, chain_id: ChainId) -> Result> { #[derive(Deserialize)] struct Response { assets: Vec, } let resp: Response = self.request( "GET", &format!("/chains/{:?}/assets", chain_id).to_lowercase(), Option::<()>::None, ).await?; Ok(resp.assets) } /// Get asset by ID pub async fn get_asset(&self, asset_id: &str) -> Result { self.request("GET", &format!("/assets/{}", urlencoding::encode(asset_id)), Option::<()>::None).await } /// Get wrapped asset mapping pub async fn get_wrapped_asset(&self, original_asset_id: &str, target_chain: ChainId) -> Result { self.request( "GET", &format!("/assets/{}/wrapped/{:?}", urlencoding::encode(original_asset_id), target_chain).to_lowercase(), Option::<()>::None, ).await } /// Get all wrapped assets for a chain pub async fn get_wrapped_assets(&self, chain_id: ChainId) -> Result> { #[derive(Deserialize)] struct Response { assets: Vec, } let resp: Response = self.request( "GET", &format!("/chains/{:?}/wrapped", chain_id).to_lowercase(), Option::<()>::None, ).await?; Ok(resp.assets) } // ==================== Fee & Rate Operations ==================== /// Estimate bridge fee pub async fn estimate_fee( &self, asset: &str, amount: &str, source_chain: ChainId, target_chain: ChainId, ) -> Result { #[derive(Serialize)] struct Request { asset: String, amount: String, #[serde(rename = "sourceChain")] source_chain: ChainId, #[serde(rename = "targetChain")] target_chain: ChainId, } self.request("POST", "/fees/estimate", Some(Request { asset: asset.to_string(), amount: amount.to_string(), source_chain, target_chain, })).await } /// Get exchange rate between assets pub async fn get_exchange_rate(&self, from_asset: &str, to_asset: &str) -> Result { self.request( "GET", &format!("/rates/{}/{}", urlencoding::encode(from_asset), urlencoding::encode(to_asset)), Option::<()>::None, ).await } // ==================== Lock-Mint Flow ==================== /// Lock assets on source chain pub async fn lock( &self, asset: &str, amount: &str, target_chain: ChainId, options: Option, ) -> Result { #[derive(Serialize)] struct Request { asset: String, amount: String, #[serde(rename = "targetChain")] target_chain: ChainId, #[serde(flatten)] options: Option, } self.request("POST", "/transfers/lock", Some(Request { asset: asset.to_string(), amount: amount.to_string(), target_chain, options, })).await } /// Get lock proof for minting pub async fn get_lock_proof(&self, lock_receipt_id: &str) -> Result { self.request( "GET", &format!("/transfers/lock/{}/proof", urlencoding::encode(lock_receipt_id)), Option::<()>::None, ).await } /// Wait for lock proof to be ready pub async fn wait_for_lock_proof( &self, lock_receipt_id: &str, poll_interval: Option, max_wait: Option, ) -> Result { let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5)); let max_wait = max_wait.unwrap_or(Duration::from_secs(600)); let deadline = std::time::Instant::now() + max_wait; loop { if std::time::Instant::now() >= deadline { return Err(BridgeError::new("Timeout waiting for lock proof") .with_code("CONFIRMATIONS_PENDING")); } match self.get_lock_proof(lock_receipt_id).await { Ok(proof) => return Ok(proof), Err(e) if e.is_confirmations_pending() => { tokio::time::sleep(poll_interval).await; } Err(e) => return Err(e), } } } /// Mint wrapped tokens on target chain pub async fn mint( &self, proof: &LockProof, target_address: &str, options: Option, ) -> Result { #[derive(Serialize)] struct Request<'a> { proof: &'a LockProof, #[serde(rename = "targetAddress")] target_address: String, #[serde(flatten)] options: Option, } self.request("POST", "/transfers/mint", Some(Request { proof, target_address: target_address.to_string(), options, })).await } // ==================== Burn-Unlock Flow ==================== /// Burn wrapped tokens pub async fn burn( &self, wrapped_asset: &str, amount: &str, options: Option, ) -> Result { #[derive(Serialize)] struct Request { #[serde(rename = "wrappedAsset")] wrapped_asset: String, amount: String, #[serde(flatten)] options: Option, } self.request("POST", "/transfers/burn", Some(Request { wrapped_asset: wrapped_asset.to_string(), amount: amount.to_string(), options, })).await } /// Get burn proof for unlocking pub async fn get_burn_proof(&self, burn_receipt_id: &str) -> Result { self.request( "GET", &format!("/transfers/burn/{}/proof", urlencoding::encode(burn_receipt_id)), Option::<()>::None, ).await } /// Wait for burn proof to be ready pub async fn wait_for_burn_proof( &self, burn_receipt_id: &str, poll_interval: Option, max_wait: Option, ) -> Result { let poll_interval = poll_interval.unwrap_or(Duration::from_secs(5)); let max_wait = max_wait.unwrap_or(Duration::from_secs(600)); let deadline = std::time::Instant::now() + max_wait; loop { if std::time::Instant::now() >= deadline { return Err(BridgeError::new("Timeout waiting for burn proof") .with_code("CONFIRMATIONS_PENDING")); } match self.get_burn_proof(burn_receipt_id).await { Ok(proof) => return Ok(proof), Err(e) if e.is_confirmations_pending() => { tokio::time::sleep(poll_interval).await; } Err(e) => return Err(e), } } } /// Unlock original tokens pub async fn unlock(&self, proof: &BurnProof, options: Option) -> Result { #[derive(Serialize)] struct Request<'a> { proof: &'a BurnProof, #[serde(flatten)] options: Option, } self.request("POST", "/transfers/unlock", Some(Request { proof, options })).await } // ==================== Transfer Management ==================== /// Get transfer by ID pub async fn get_transfer(&self, transfer_id: &str) -> Result { self.request( "GET", &format!("/transfers/{}", urlencoding::encode(transfer_id)), Option::<()>::None, ).await } /// Get transfer status pub async fn get_transfer_status(&self, transfer_id: &str) -> Result { let transfer = self.get_transfer(transfer_id).await?; Ok(transfer.status) } /// List transfers pub async fn list_transfers(&self, filter: Option) -> Result> { let mut params = vec![]; if let Some(f) = &filter { if let Some(status) = f.status { params.push(format!("status={:?}", status).to_lowercase()); } if let Some(chain) = f.source_chain { params.push(format!("sourceChain={:?}", chain).to_lowercase()); } if let Some(chain) = f.target_chain { params.push(format!("targetChain={:?}", chain).to_lowercase()); } if let Some(ref asset) = f.asset { params.push(format!("asset={}", urlencoding::encode(asset))); } if let Some(ref sender) = f.sender { params.push(format!("sender={}", urlencoding::encode(sender))); } if let Some(ref recipient) = f.recipient { params.push(format!("recipient={}", urlencoding::encode(recipient))); } if let Some(from_date) = f.from_date { params.push(format!("fromDate={}", from_date)); } if let Some(to_date) = f.to_date { params.push(format!("toDate={}", to_date)); } if let Some(limit) = f.limit { params.push(format!("limit={}", limit)); } if let Some(offset) = f.offset { params.push(format!("offset={}", offset)); } } let path = if params.is_empty() { "/transfers".to_string() } else { format!("/transfers?{}", params.join("&")) }; #[derive(Deserialize)] struct Response { transfers: Vec, } let resp: Response = self.request("GET", &path, Option::<()>::None).await?; Ok(resp.transfers) } /// Wait for transfer to complete pub async fn wait_for_transfer( &self, transfer_id: &str, poll_interval: Option, max_wait: Option, ) -> Result { let poll_interval = poll_interval.unwrap_or(Duration::from_secs(10)); let max_wait = max_wait.unwrap_or(Duration::from_secs(1800)); let deadline = std::time::Instant::now() + max_wait; loop { if std::time::Instant::now() >= deadline { return Err(BridgeError::new("Timeout waiting for transfer completion")); } let transfer = self.get_transfer(transfer_id).await?; match transfer.status { TransferStatus::Completed | TransferStatus::Failed | TransferStatus::Refunded => { return Ok(transfer); } _ => { tokio::time::sleep(poll_interval).await; } } } } // ==================== Convenience Methods ==================== /// Execute complete lock-mint transfer pub async fn bridge_to( &self, asset: &str, amount: &str, target_chain: ChainId, target_address: &str, lock_options: Option, mint_options: Option, ) -> Result { // Lock on source chain let lock_receipt = self.lock(asset, amount, target_chain, lock_options).await?; if self.config.debug { eprintln!("Locked: {}, waiting for confirmations...", lock_receipt.id); } // Wait for proof let proof = self.wait_for_lock_proof(&lock_receipt.id, None, None).await?; if self.config.debug { eprintln!("Proof ready, minting on {:?}...", target_chain); } // Mint on target chain self.mint(&proof, target_address, mint_options).await?; // Return final transfer status self.wait_for_transfer(&lock_receipt.id, None, None).await } /// Execute complete burn-unlock transfer pub async fn bridge_back( &self, wrapped_asset: &str, amount: &str, burn_options: Option, unlock_options: Option, ) -> Result { // Burn wrapped tokens let burn_receipt = self.burn(wrapped_asset, amount, burn_options).await?; if self.config.debug { eprintln!("Burned: {}, waiting for confirmations...", burn_receipt.id); } // Wait for proof let proof = self.wait_for_burn_proof(&burn_receipt.id, None, None).await?; if self.config.debug { eprintln!("Proof ready, unlocking on {:?}...", burn_receipt.target_chain); } // Unlock on original chain self.unlock(&proof, unlock_options).await?; // Return final transfer status self.wait_for_transfer(&burn_receipt.id, None, None).await } // ==================== Lifecycle ==================== /// Close the client pub fn close(&self) { self.closed.store(true, Ordering::SeqCst); } /// Check if client is closed pub fn is_closed(&self) -> bool { self.closed.load(Ordering::SeqCst) } /// Health check pub async fn health_check(&self) -> bool { #[derive(Deserialize)] struct Response { status: String, } match self.request::<_, Response>("GET", "/health", Option::<()>::None).await { Ok(resp) => resp.status == "healthy", Err(_) => false, } } async fn request( &self, method: &str, path: &str, body: Option, ) -> Result { if self.is_closed() { return Err(BridgeError::new("Client has been closed")); } let mut last_error = None; for attempt in 0..self.config.retries { match self.do_request(method, path, &body).await { Ok(result) => return Ok(result), Err(e) => { if self.config.debug { eprintln!("Attempt {} failed: {}", attempt + 1, e); } last_error = Some(e); if attempt < self.config.retries - 1 { tokio::time::sleep(Duration::from_secs((attempt + 1) as u64)).await; } } } } Err(last_error.unwrap_or_else(|| BridgeError::new("Unknown error after retries"))) } async fn do_request( &self, method: &str, path: &str, body: &Option, ) -> Result { let url = format!("{}{}", self.config.endpoint, path); let mut request = match method { "GET" => self.http_client.get(&url), "POST" => self.http_client.post(&url), "PUT" => self.http_client.put(&url), "PATCH" => self.http_client.patch(&url), "DELETE" => self.http_client.delete(&url), _ => return Err(BridgeError::new(format!("Unknown method: {}", method))), }; if let Some(b) = body { request = request.json(b); } let response = request.send().await?; let status = response.status(); if !status.is_success() { let error_body: serde_json::Value = response.json().await.unwrap_or_default(); let message = error_body["message"] .as_str() .or_else(|| error_body["error"].as_str()) .unwrap_or(&format!("HTTP {}", status.as_u16())); return Err(BridgeError::new(message) .with_status(status.as_u16()) .with_code(error_body["code"].as_str().unwrap_or("").to_string())); } let result = response.json().await?; Ok(result) } }