import Foundation /// Synor IBC SDK for Swift /// /// Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability. public class SynorIbc { private let config: IbcConfig private let session: URLSession private var _closed = false public lazy var clients = LightClientClient(ibc: self) public lazy var connections = ConnectionsClient(ibc: self) public lazy var channels = ChannelsClient(ibc: self) public lazy var transfer = TransferClient(ibc: self) public lazy var swaps = SwapsClient(ibc: self) public init(config: IbcConfig) { self.config = config let sessionConfig = URLSessionConfiguration.default sessionConfig.timeoutIntervalForRequest = TimeInterval(config.timeout) self.session = URLSession(configuration: sessionConfig) } public var chainId: String { config.chainId } public func getChainInfo() async throws -> [String: Any] { try await get("/chain") } public func getHeight() async throws -> Height { let result = try await get("/chain/height") return Height( revisionNumber: (result["revision_number"] as? UInt64) ?? 0, revisionHeight: (result["revision_height"] as? UInt64) ?? 1 ) } public func healthCheck() async -> Bool { do { let result = try await get("/health") return result["status"] as? String == "healthy" } catch { return false } } public func close() { _closed = true session.invalidateAndCancel() } public var isClosed: Bool { _closed } // Internal HTTP methods func get(_ path: String) async throws -> [String: Any] { try checkClosed() var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!) request.httpMethod = "GET" addHeaders(&request) return try await performRequest(request) } func post(_ path: String, body: [String: Any]) async throws -> [String: Any] { try checkClosed() var request = URLRequest(url: URL(string: "\(config.endpoint)\(path)")!) request.httpMethod = "POST" request.httpBody = try JSONSerialization.data(withJSONObject: body) addHeaders(&request) return try await performRequest(request) } private func addHeaders(_ request: inout URLRequest) { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version") request.setValue(config.chainId, forHTTPHeaderField: "X-Chain-Id") } private func performRequest(_ request: URLRequest) async throws -> [String: Any] { let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw IbcError("Invalid response") } if httpResponse.statusCode >= 400 { let error = try? JSONSerialization.jsonObject(with: data) as? [String: Any] throw IbcError( error?["message"] as? String ?? "HTTP \(httpResponse.statusCode)", code: error?["code"] as? String, status: httpResponse.statusCode ) } guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw IbcError("Invalid JSON response") } return json } private func checkClosed() throws { if _closed { throw IbcError("Client has been closed", code: "CLIENT_CLOSED") } } /// Light client sub-client public class LightClientClient { private let ibc: SynorIbc init(ibc: SynorIbc) { self.ibc = ibc } public func create( clientType: ClientType, clientState: ClientState, consensusState: [String: Any] ) async throws -> ClientId { let result = try await ibc.post("/clients", body: [ "client_type": clientType.rawValue, "client_state": [ "chain_id": clientState.chainId, "trust_level": [ "numerator": clientState.trustLevel.numerator, "denominator": clientState.trustLevel.denominator ], "trusting_period": clientState.trustingPeriod, "unbonding_period": clientState.unbondingPeriod, "max_clock_drift": clientState.maxClockDrift, "latest_height": [ "revision_number": clientState.latestHeight.revisionNumber, "revision_height": clientState.latestHeight.revisionHeight ] ] as [String: Any], "consensus_state": consensusState ]) guard let clientId = result["client_id"] as? String else { throw IbcError("Missing client_id in response") } return ClientId(clientId) } public func getState(clientId: ClientId) async throws -> ClientState { let result = try await ibc.get("/clients/\(clientId.id)/state") guard let chainId = result["chain_id"] as? String, let trustLevelDict = result["trust_level"] as? [String: Any], let latestHeightDict = result["latest_height"] as? [String: Any] else { throw IbcError("Invalid client state response") } return ClientState( chainId: chainId, trustLevel: TrustLevel( numerator: trustLevelDict["numerator"] as? Int ?? 1, denominator: trustLevelDict["denominator"] as? Int ?? 3 ), trustingPeriod: result["trusting_period"] as? UInt64 ?? 0, unbondingPeriod: result["unbonding_period"] as? UInt64 ?? 0, maxClockDrift: result["max_clock_drift"] as? UInt64 ?? 0, latestHeight: Height( revisionNumber: latestHeightDict["revision_number"] as? UInt64 ?? 0, revisionHeight: latestHeightDict["revision_height"] as? UInt64 ?? 1 ) ) } public func list() async throws -> [[String: Any]] { let result = try await ibc.get("/clients") return result["clients"] as? [[String: Any]] ?? [] } } /// Connections sub-client public class ConnectionsClient { private let ibc: SynorIbc init(ibc: SynorIbc) { self.ibc = ibc } public func openInit( clientId: ClientId, counterpartyClientId: ClientId ) async throws -> ConnectionId { let result = try await ibc.post("/connections/init", body: [ "client_id": clientId.id, "counterparty_client_id": counterpartyClientId.id ]) guard let connectionId = result["connection_id"] as? String else { throw IbcError("Missing connection_id in response") } return ConnectionId(connectionId) } public func get(connectionId: ConnectionId) async throws -> [String: Any] { try await ibc.get("/connections/\(connectionId.id)") } public func list() async throws -> [[String: Any]] { let result = try await ibc.get("/connections") return result["connections"] as? [[String: Any]] ?? [] } } /// Channels sub-client public class ChannelsClient { private let ibc: SynorIbc init(ibc: SynorIbc) { self.ibc = ibc } public func bindPort(portId: PortId, module: String) async throws { _ = try await ibc.post("/ports/bind", body: [ "port_id": portId.id, "module": module ]) } public func openInit( portId: PortId, ordering: ChannelOrder, connectionId: ConnectionId, counterpartyPort: PortId, version: String ) async throws -> ChannelId { let result = try await ibc.post("/channels/init", body: [ "port_id": portId.id, "ordering": ordering.rawValue, "connection_id": connectionId.id, "counterparty_port": counterpartyPort.id, "version": version ]) guard let channelId = result["channel_id"] as? String else { throw IbcError("Missing channel_id in response") } return ChannelId(channelId) } public func get(portId: PortId, channelId: ChannelId) async throws -> [String: Any] { try await ibc.get("/channels/\(portId.id)/\(channelId.id)") } public func list() async throws -> [[String: Any]] { let result = try await ibc.get("/channels") return result["channels"] as? [[String: Any]] ?? [] } } /// Transfer sub-client (ICS-20) public class TransferClient { private let ibc: SynorIbc init(ibc: SynorIbc) { self.ibc = ibc } public func transfer( sourcePort: String, sourceChannel: String, denom: String, amount: String, sender: String, receiver: String, timeout: Timeout? = nil, memo: String? = nil ) async throws -> [String: Any] { var body: [String: Any] = [ "source_port": sourcePort, "source_channel": sourceChannel, "token": ["denom": denom, "amount": amount], "sender": sender, "receiver": receiver ] if let timeout = timeout { body["timeout_height"] = [ "revision_number": timeout.height.revisionNumber, "revision_height": timeout.height.revisionHeight ] body["timeout_timestamp"] = String(timeout.timestamp) } if let memo = memo { body["memo"] = memo } return try await ibc.post("/transfer", body: body) } public func getDenomTrace(ibcDenom: String) async throws -> [String: Any] { try await ibc.get("/transfer/denom_trace/\(ibcDenom)") } } /// Swaps sub-client (HTLC) public class SwapsClient { private let ibc: SynorIbc init(ibc: SynorIbc) { self.ibc = ibc } public func initiate( responder: String, initiatorAsset: [String: Any], responderAsset: [String: Any] ) async throws -> [String: Any] { try await ibc.post("/swaps/initiate", body: [ "responder": responder, "initiator_asset": initiatorAsset, "responder_asset": responderAsset ]) } public func lock(swapId: SwapId) async throws { _ = try await ibc.post("/swaps/\(swapId.id)/lock", body: [:]) } public func respond(swapId: SwapId, asset: [String: Any]) async throws -> [String: Any] { try await ibc.post("/swaps/\(swapId.id)/respond", body: ["asset": asset]) } public func claim(swapId: SwapId, secret: Data) async throws -> [String: Any] { try await ibc.post("/swaps/\(swapId.id)/claim", body: [ "secret": secret.base64EncodedString() ]) } public func refund(swapId: SwapId) async throws -> [String: Any] { try await ibc.post("/swaps/\(swapId.id)/refund", body: [:]) } public func get(swapId: SwapId) async throws -> AtomicSwap { let result = try await ibc.get("/swaps/\(swapId.id)") guard let swap = AtomicSwap.fromJson(result) else { throw IbcError("Invalid swap response") } return swap } public func listActive() async throws -> [AtomicSwap] { let result = try await ibc.get("/swaps/active") guard let swaps = result["swaps"] as? [[String: Any]] else { return [] } return swaps.compactMap { AtomicSwap.fromJson($0) } } } }