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(_ 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(_ 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(_ 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(_ block: () async throws -> T) async throws -> T { var lastError: Error? for attempt in 0..