synor/sdk/rust/src/contract/client.rs
Gulshan Yadav e65ea40af2 feat: implement Privacy and Contract SDKs for all 12 languages (Phase 5)
Privacy SDK features:
- Confidential transactions with Pedersen commitments
- Bulletproof range proofs for value validation
- Ring signatures for anonymous signing with key images
- Stealth addresses for unlinkable payments
- Blinding factor generation and value operations

Contract SDK features:
- Smart contract deployment (standard and CREATE2)
- Call (view/pure) and Send (state-changing) operations
- Event log filtering, subscription, and decoding
- ABI encoding/decoding utilities
- Gas estimation and contract verification
- Multicall for batched operations
- Storage slot reading

Languages implemented:
- JavaScript/TypeScript
- Python (async with httpx)
- Go
- Rust (async with reqwest/tokio)
- Java (async with OkHttp)
- Kotlin (coroutines with Ktor)
- Swift (async/await with URLSession)
- Flutter/Dart
- C (header-only interface)
- C++ (header-only with std::future)
- C#/.NET (async with HttpClient)
- Ruby (Faraday HTTP client)

All SDKs follow consistent patterns:
- Configuration with API key, endpoint, timeout, retries
- Custom exception types with error codes
- Retry logic with exponential backoff
- Health check endpoints
- Closed state management
2026-01-28 09:03:34 +05:30

471 lines
16 KiB
Rust

//! 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<String>) -> 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<String>) -> 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<AtomicBool>,
}
impl SynorContract {
/// Create a new Contract client
pub fn new(config: ContractConfig) -> Result<Self> {
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<T: serde::de::DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<T> {
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::<serde_json::Value>(&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<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
self.request(reqwest::Method::GET, path, None).await
}
async fn post<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: serde_json::Value,
) -> Result<T> {
self.request(reqwest::Method::POST, path, Some(body)).await
}
// ==================== Contract Deployment ====================
/// Deploy a new smart contract
pub async fn deploy(&self, options: DeployContractOptions) -> Result<DeploymentResult> {
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<DeploymentResult> {
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<String> {
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<serde_json::Value> {
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<TransactionResult> {
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<Vec<DecodedEvent>> {
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<u64>, to_block: Option<u64>) -> Result<Vec<EventLog>> {
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<Vec<DecodedEvent>> {
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<String> {
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<serde_json::Value> {
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<String> {
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<GasEstimation> {
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<BytecodeInfo> {
self.get(&format!("/contract/{}/bytecode", address)).await
}
/// Verify contract source code
pub async fn verify(&self, options: VerifyContractOptions) -> Result<VerificationResult> {
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<VerificationResult> {
self.get(&format!("/contract/{}/verification", address)).await
}
// ==================== Multicall ====================
/// Execute multiple calls in a single request
pub async fn multicall(&self, requests: &[MulticallRequest]) -> Result<Vec<MulticallResult>> {
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<String> {
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::<serde_json::Value>("/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)
}
}