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
471 lines
16 KiB
Rust
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)
|
|
}
|
|
}
|