//! Synor Contract SDK client use reqwest::Client as HttpClient; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use super::error::{ContractError, Result}; use super::types::*; /// Contract SDK configuration #[derive(Debug, Clone)] pub struct ContractConfig { /// API key for authentication pub api_key: String, /// API endpoint URL pub endpoint: String, /// Request timeout in milliseconds pub timeout_ms: u64, /// Number of retry attempts pub retries: u32, } impl ContractConfig { /// Create a new configuration with the given API key pub fn new(api_key: impl Into) -> Self { Self { api_key: api_key.into(), endpoint: "https://contract.synor.io".to_string(), timeout_ms: 30000, retries: 3, } } /// Set the endpoint URL pub fn endpoint(mut self, endpoint: impl Into) -> Self { self.endpoint = endpoint.into(); self } /// Set the timeout in milliseconds pub fn timeout_ms(mut self, timeout_ms: u64) -> Self { self.timeout_ms = timeout_ms; self } /// Set the number of retries pub fn retries(mut self, retries: u32) -> Self { self.retries = retries; self } } /// Synor Contract SDK client pub struct SynorContract { config: ContractConfig, client: HttpClient, closed: Arc, } impl SynorContract { /// Create a new Contract client pub fn new(config: ContractConfig) -> Result { let client = HttpClient::builder() .timeout(std::time::Duration::from_millis(config.timeout_ms)) .build() .map_err(|e| ContractError::Request(e.to_string()))?; Ok(Self { config, client, closed: Arc::new(AtomicBool::new(false)), }) } fn check_closed(&self) -> Result<()> { if self.closed.load(Ordering::SeqCst) { return Err(ContractError::Closed); } Ok(()) } async fn request( &self, method: reqwest::Method, path: &str, body: Option, ) -> Result { self.check_closed()?; let url = format!("{}{}", self.config.endpoint, path); let mut last_error = None; for attempt in 0..self.config.retries { let mut req = self .client .request(method.clone(), &url) .header("Authorization", format!("Bearer {}", self.config.api_key)) .header("Content-Type", "application/json") .header("X-SDK-Version", format!("rust/{}", env!("CARGO_PKG_VERSION"))); if let Some(ref b) = body { req = req.json(b); } match req.send().await { Ok(response) => { let status = response.status(); let text = response.text().await.map_err(|e| ContractError::Response(e.to_string()))?; if status.is_success() { return serde_json::from_str(&text).map_err(ContractError::from); } // Try to parse error response if let Ok(error_response) = serde_json::from_str::(&text) { let message = error_response .get("message") .or_else(|| error_response.get("error")) .and_then(|v| v.as_str()) .unwrap_or("Unknown error") .to_string(); let code = error_response .get("code") .and_then(|v| v.as_str()) .map(|s| s.to_string()); last_error = Some(ContractError::Api { message, code, status_code: Some(status.as_u16()), }); } else { last_error = Some(ContractError::Response(text)); } } Err(e) => { last_error = Some(ContractError::Request(e.to_string())); } } if attempt < self.config.retries - 1 { tokio::time::sleep(std::time::Duration::from_millis( 2u64.pow(attempt) * 1000, )) .await; } } Err(last_error.unwrap_or_else(|| ContractError::Request("Unknown error".to_string()))) } async fn get(&self, path: &str) -> Result { self.request(reqwest::Method::GET, path, None).await } async fn post( &self, path: &str, body: serde_json::Value, ) -> Result { self.request(reqwest::Method::POST, path, Some(body)).await } // ==================== Contract Deployment ==================== /// Deploy a new smart contract pub async fn deploy(&self, options: DeployContractOptions) -> Result { let mut body = serde_json::json!({ "bytecode": options.bytecode, }); if let Some(abi) = &options.abi { body["abi"] = serde_json::to_value(abi)?; } if let Some(args) = &options.constructor_args { body["constructor_args"] = serde_json::to_value(args)?; } if let Some(value) = &options.value { body["value"] = serde_json::json!(value); } if let Some(gas_limit) = options.gas_limit { body["gas_limit"] = serde_json::json!(gas_limit); } if let Some(gas_price) = &options.gas_price { body["gas_price"] = serde_json::json!(gas_price); } if let Some(nonce) = options.nonce { body["nonce"] = serde_json::json!(nonce); } self.post("/contract/deploy", body).await } /// Deploy a contract using CREATE2 for deterministic addresses pub async fn deploy_create2(&self, options: DeployContractOptions, salt: &str) -> Result { let mut body = serde_json::json!({ "bytecode": options.bytecode, "salt": salt, }); if let Some(abi) = &options.abi { body["abi"] = serde_json::to_value(abi)?; } if let Some(args) = &options.constructor_args { body["constructor_args"] = serde_json::to_value(args)?; } if let Some(value) = &options.value { body["value"] = serde_json::json!(value); } if let Some(gas_limit) = options.gas_limit { body["gas_limit"] = serde_json::json!(gas_limit); } if let Some(gas_price) = &options.gas_price { body["gas_price"] = serde_json::json!(gas_price); } self.post("/contract/deploy/create2", body).await } /// Predict the address from a CREATE2 deployment pub async fn predict_address(&self, bytecode: &str, salt: &str, deployer: Option<&str>) -> Result { let mut body = serde_json::json!({ "bytecode": bytecode, "salt": salt, }); if let Some(d) = deployer { body["deployer"] = serde_json::json!(d); } let response: serde_json::Value = self.post("/contract/predict-address", body).await?; response["address"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| ContractError::Response("Missing address in response".to_string())) } // ==================== Contract Interaction ==================== /// Call a view/pure function (read-only, no gas) pub async fn call(&self, options: CallContractOptions) -> Result { let body = serde_json::json!({ "contract": options.contract, "method": options.method, "args": options.args, "abi": options.abi, }); self.post("/contract/call", body).await } /// Send a state-changing transaction pub async fn send(&self, options: SendContractOptions) -> Result { let mut body = serde_json::json!({ "contract": options.contract, "method": options.method, "args": options.args, "abi": options.abi, }); if let Some(value) = &options.value { body["value"] = serde_json::json!(value); } if let Some(gas_limit) = options.gas_limit { body["gas_limit"] = serde_json::json!(gas_limit); } if let Some(gas_price) = &options.gas_price { body["gas_price"] = serde_json::json!(gas_price); } if let Some(nonce) = options.nonce { body["nonce"] = serde_json::json!(nonce); } self.post("/contract/send", body).await } // ==================== Events ==================== /// Get events from a contract pub async fn get_events(&self, filter: EventFilter) -> Result> { let mut body = serde_json::json!({ "contract": filter.contract, }); if let Some(event) = &filter.event { body["event"] = serde_json::json!(event); } if let Some(from_block) = filter.from_block { body["from_block"] = serde_json::json!(from_block); } if let Some(to_block) = filter.to_block { body["to_block"] = serde_json::json!(to_block); } if let Some(topics) = &filter.topics { body["topics"] = serde_json::to_value(topics)?; } if let Some(abi) = &filter.abi { body["abi"] = serde_json::to_value(abi)?; } self.post("/contract/events", body).await } /// Get the logs for a contract pub async fn get_logs(&self, contract: &str, from_block: Option, to_block: Option) -> Result> { let mut params = format!("contract={}", contract); if let Some(from) = from_block { params.push_str(&format!("&from_block={}", from)); } if let Some(to) = to_block { params.push_str(&format!("&to_block={}", to)); } self.get(&format!("/contract/logs?{}", params)).await } /// Decode event logs using an ABI pub async fn decode_logs(&self, logs: &[EventLog], abi: &Abi) -> Result> { let body = serde_json::json!({ "logs": logs, "abi": abi, }); self.post("/contract/decode-logs", body).await } // ==================== ABI Utilities ==================== /// Encode a function call pub async fn encode_call(&self, options: EncodeCallOptions) -> Result { let body = serde_json::json!({ "method": options.method, "args": options.args, "abi": options.abi, }); let response: serde_json::Value = self.post("/contract/encode", body).await?; response["data"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| ContractError::Response("Missing data in response".to_string())) } /// Decode a function result pub async fn decode_result(&self, options: DecodeResultOptions) -> Result { let body = serde_json::json!({ "data": options.data, "method": options.method, "abi": options.abi, }); let response: serde_json::Value = self.post("/contract/decode", body).await?; Ok(response["result"].clone()) } /// Get the function selector for a method pub async fn get_selector(&self, signature: &str) -> Result { let response: serde_json::Value = self .get(&format!("/contract/selector?signature={}", urlencoding::encode(signature))) .await?; response["selector"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| ContractError::Response("Missing selector in response".to_string())) } // ==================== Gas Estimation ==================== /// Estimate gas for a contract interaction pub async fn estimate_gas(&self, options: EstimateGasOptions) -> Result { let mut body = serde_json::json!({ "contract": options.contract, "method": options.method, "args": options.args, "abi": options.abi, }); if let Some(value) = &options.value { body["value"] = serde_json::json!(value); } self.post("/contract/estimate-gas", body).await } // ==================== Contract Information ==================== /// Get bytecode deployed at an address pub async fn get_bytecode(&self, address: &str) -> Result { self.get(&format!("/contract/{}/bytecode", address)).await } /// Verify contract source code pub async fn verify(&self, options: VerifyContractOptions) -> Result { let mut body = serde_json::json!({ "address": options.address, "source_code": options.source_code, "compiler_version": options.compiler_version, }); if let Some(ctor_args) = &options.constructor_args { body["constructor_args"] = serde_json::json!(ctor_args); } if let Some(optimization) = options.optimization { body["optimization"] = serde_json::json!(optimization); } if let Some(runs) = options.optimization_runs { body["optimization_runs"] = serde_json::json!(runs); } if let Some(license) = &options.license { body["license"] = serde_json::json!(license); } self.post("/contract/verify", body).await } /// Get verification status for a contract pub async fn get_verification_status(&self, address: &str) -> Result { self.get(&format!("/contract/{}/verification", address)).await } // ==================== Multicall ==================== /// Execute multiple calls in a single request pub async fn multicall(&self, requests: &[MulticallRequest]) -> Result> { let body = serde_json::json!({ "calls": requests, }); self.post("/contract/multicall", body).await } // ==================== Storage ==================== /// Read a storage slot from a contract pub async fn read_storage(&self, options: ReadStorageOptions) -> Result { let mut params = format!("contract={}&slot={}", options.contract, options.slot); if let Some(block) = options.block_number { params.push_str(&format!("&block={}", block)); } let response: serde_json::Value = self.get(&format!("/contract/storage?{}", params)).await?; response["value"] .as_str() .map(|s| s.to_string()) .ok_or_else(|| ContractError::Response("Missing value in response".to_string())) } // ==================== Lifecycle ==================== /// Check if the service is healthy pub async fn health_check(&self) -> bool { if self.closed.load(Ordering::SeqCst) { return false; } match self.get::("/health").await { Ok(response) => response.get("status").and_then(|s| s.as_str()) == Some("healthy"), Err(_) => false, } } /// Close the client pub fn close(&self) { self.closed.store(true, Ordering::SeqCst); } /// Check if the client is closed pub fn is_closed(&self) -> bool { self.closed.load(Ordering::SeqCst) } }