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
342 lines
12 KiB
Swift
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) }
|
|
}
|
|
}
|
|
}
|