synor/sdk/swift/Sources/SynorContract/ContractClient.swift
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

314 lines
12 KiB
Swift

import Foundation
/// Synor Contract SDK client for Swift.
/// Smart contract deployment, interaction, and event handling.
public actor ContractClient {
public static let version = "0.1.0"
private let config: ContractConfig
private let session: URLSession
private var isClosed = false
public init(config: ContractConfig) {
self.config = config
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeoutMs) / 1000.0
self.session = URLSession(configuration: sessionConfig)
}
// MARK: - Contract Deployment
public func deploy(_ options: DeployContractOptions) async throws -> DeploymentResult {
var body: [String: Any] = ["bytecode": options.bytecode]
if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } }
if let args = options.constructorArgs { body["constructor_args"] = args }
if let value = options.value { body["value"] = value }
if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
if let nonce = options.nonce { body["nonce"] = nonce }
return try await post("/contract/deploy", body: body)
}
public func deployCreate2(_ options: DeployContractOptions, salt: String) async throws -> DeploymentResult {
var body: [String: Any] = ["bytecode": options.bytecode, "salt": salt]
if let abi = options.abi { body["abi"] = abi.map { $0.toDictionary() } }
if let args = options.constructorArgs { body["constructor_args"] = args }
if let value = options.value { body["value"] = value }
if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
return try await post("/contract/deploy/create2", body: body)
}
public func predictAddress(bytecode: String, salt: String, deployer: String? = nil) async throws -> String {
var body: [String: Any] = ["bytecode": bytecode, "salt": salt]
if let deployer = deployer { body["deployer"] = deployer }
let response: [String: Any] = try await post("/contract/predict-address", body: body)
guard let address = response["address"] as? String else {
throw ContractError.response("Missing address")
}
return address
}
// MARK: - Contract Interaction
public func call(_ options: CallContractOptions) async throws -> Any {
let body: [String: Any] = [
"contract": options.contract,
"method": options.method,
"args": options.args,
"abi": options.abi.map { $0.toDictionary() }
]
return try await post("/contract/call", body: body) as [String: Any]
}
public func send(_ options: SendContractOptions) async throws -> TransactionResult {
var body: [String: Any] = [
"contract": options.contract,
"method": options.method,
"args": options.args,
"abi": options.abi.map { $0.toDictionary() }
]
if let value = options.value { body["value"] = value }
if let gasLimit = options.gasLimit { body["gas_limit"] = gasLimit }
if let gasPrice = options.gasPrice { body["gas_price"] = gasPrice }
if let nonce = options.nonce { body["nonce"] = nonce }
return try await post("/contract/send", body: body)
}
// MARK: - Events
public func getEvents(_ filter: EventFilter) async throws -> [DecodedEvent] {
var body: [String: Any] = ["contract": filter.contract]
if let event = filter.event { body["event"] = event }
if let fromBlock = filter.fromBlock { body["from_block"] = fromBlock }
if let toBlock = filter.toBlock { body["to_block"] = toBlock }
if let topics = filter.topics { body["topics"] = topics }
if let abi = filter.abi { body["abi"] = abi.map { $0.toDictionary() } }
return try await post("/contract/events", body: body)
}
public func getLogs(contract: String, fromBlock: Int64? = nil, toBlock: Int64? = nil) async throws -> [EventLog] {
var path = "/contract/logs?contract=\(contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? contract)"
if let fromBlock = fromBlock { path += "&from_block=\(fromBlock)" }
if let toBlock = toBlock { path += "&to_block=\(toBlock)" }
return try await get(path)
}
public func decodeLogs(_ logs: [EventLog], abi: [AbiEntry]) async throws -> [DecodedEvent] {
let body: [String: Any] = [
"logs": logs.map { $0.toDictionary() },
"abi": abi.map { $0.toDictionary() }
]
return try await post("/contract/decode-logs", body: body)
}
// MARK: - ABI Utilities
public func encodeCall(_ options: EncodeCallOptions) async throws -> String {
let body: [String: Any] = [
"method": options.method,
"args": options.args,
"abi": options.abi.map { $0.toDictionary() }
]
let response: [String: Any] = try await post("/contract/encode", body: body)
guard let data = response["data"] as? String else {
throw ContractError.response("Missing data")
}
return data
}
public func decodeResult(_ options: DecodeResultOptions) async throws -> Any {
let body: [String: Any] = [
"data": options.data,
"method": options.method,
"abi": options.abi.map { $0.toDictionary() }
]
let response: [String: Any] = try await post("/contract/decode", body: body)
return response["result"] as Any
}
public func getSelector(_ signature: String) async throws -> String {
let encoded = signature.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signature
let response: [String: Any] = try await get("/contract/selector?signature=\(encoded)")
guard let selector = response["selector"] as? String else {
throw ContractError.response("Missing selector")
}
return selector
}
// MARK: - Gas Estimation
public func estimateGas(_ options: EstimateGasOptions) async throws -> GasEstimation {
var body: [String: Any] = [
"contract": options.contract,
"method": options.method,
"args": options.args,
"abi": options.abi.map { $0.toDictionary() }
]
if let value = options.value { body["value"] = value }
return try await post("/contract/estimate-gas", body: body)
}
// MARK: - Contract Information
public func getBytecode(_ address: String) async throws -> BytecodeInfo {
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
return try await get("/contract/\(encoded)/bytecode")
}
public func verify(_ options: VerifyContractOptions) async throws -> VerificationResult {
var body: [String: Any] = [
"address": options.address,
"source_code": options.sourceCode,
"compiler_version": options.compilerVersion
]
if let args = options.constructorArgs { body["constructor_args"] = args }
if let optimization = options.optimization { body["optimization"] = optimization }
if let runs = options.optimizationRuns { body["optimization_runs"] = runs }
if let license = options.license { body["license"] = license }
return try await post("/contract/verify", body: body)
}
public func getVerificationStatus(_ address: String) async throws -> VerificationResult {
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
return try await get("/contract/\(encoded)/verification")
}
// MARK: - Multicall
public func multicall(_ requests: [MulticallRequest]) async throws -> [MulticallResult] {
let body: [String: Any] = ["calls": requests.map { $0.toDictionary() }]
return try await post("/contract/multicall", body: body)
}
// MARK: - Storage
public func readStorage(_ options: ReadStorageOptions) async throws -> String {
let contract = options.contract.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.contract
let slot = options.slot.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? options.slot
var path = "/contract/storage?contract=\(contract)&slot=\(slot)"
if let block = options.blockNumber { path += "&block=\(block)" }
let response: [String: Any] = try await get(path)
guard let value = response["value"] as? String else {
throw ContractError.response("Missing value")
}
return value
}
// MARK: - Lifecycle
public func healthCheck() async -> Bool {
if isClosed { return false }
do {
let response: [String: Any] = try await get("/health")
return response["status"] as? String == "healthy"
} catch {
return false
}
}
public func close() {
isClosed = true
session.invalidateAndCancel()
}
// MARK: - HTTP Helpers
private func get<T: Decodable>(_ path: String) async throws -> T {
try checkClosed()
return try await executeWithRetry {
let url = URL(string: "\(self.config.endpoint)\(path)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
self.addHeaders(to: &request)
return try await self.execute(request)
}
}
private func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
try checkClosed()
return try await executeWithRetry {
let url = URL(string: "\(self.config.endpoint)\(path)")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = try JSONSerialization.data(withJSONObject: body)
self.addHeaders(to: &request)
return try await self.execute(request)
}
}
private func addHeaders(to request: inout URLRequest) {
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("swift/\(Self.version)", forHTTPHeaderField: "X-SDK-Version")
}
private func execute<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ContractError.request("Invalid response")
}
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
return try JSONDecoder().decode(T.self, from: data)
}
if let errorJson = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
let message = errorJson["message"] as? String ?? errorJson["error"] as? String ?? "Unknown error"
let code = errorJson["code"] as? String
throw ContractError.api(message: message, code: code, statusCode: httpResponse.statusCode)
}
throw ContractError.response("HTTP \(httpResponse.statusCode)")
}
private func executeWithRetry<T>(_ block: () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 0..<config.retries {
do {
return try await block()
} catch {
lastError = error
if attempt < config.retries - 1 {
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
}
}
}
throw lastError ?? ContractError.request("Request failed")
}
private func checkClosed() throws {
if isClosed {
throw ContractError.closed
}
}
}
/// Contract SDK configuration.
public struct ContractConfig {
public let apiKey: String
public var endpoint: String
public var timeoutMs: Int
public var retries: Int
public init(
apiKey: String,
endpoint: String = "https://contract.synor.io",
timeoutMs: Int = 30000,
retries: Int = 3
) {
self.apiKey = apiKey
self.endpoint = endpoint
self.timeoutMs = timeoutMs
self.retries = retries
}
}
/// Contract SDK errors.
public enum ContractError: Error {
case request(String)
case response(String)
case api(message: String, code: String?, statusCode: Int)
case closed
}