synor/sdk/swift/Sources/SynorIbc/SynorIbc.swift
Gulshan Yadav 97add23062 feat(sdk): implement IBC SDK for all 12 languages
Implement Inter-Blockchain Communication (IBC) SDK with full ICS
protocol support across all 12 programming languages:
- JavaScript/TypeScript, Python, Go, Rust
- Java, Kotlin, Swift, Flutter/Dart
- C, C++, C#/.NET, Ruby

Features:
- Light client management (Tendermint, Solo Machine, WASM)
- Connection handshake (4-way: Init, Try, Ack, Confirm)
- Channel management with ordered/unordered support
- ICS-20 fungible token transfers
- HTLC atomic swaps with hashlock (SHA256) and timelock
- Packet relay with timeout handling
2026-01-28 12:53:46 +05:30

342 lines
12 KiB
Swift

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) }
}
}
}