synor/sdk/swift/Sources/Synor/Bridge/SynorBridge.swift
Gulshan Yadav a874faef13 feat: complete Phase 3 SDKs for Swift, C, C++, C#, and Ruby
Implements Database, Hosting, and Bridge SDKs for remaining languages:

Swift SDKs:
- SynorDatabase with KV, Document, Vector, TimeSeries stores
- SynorHosting with domain, DNS, deployment, SSL operations
- SynorBridge with lock-mint and burn-unlock cross-chain flows

C SDKs:
- database.h/c - multi-model database client
- hosting.h/c - hosting and domain management
- bridge.h/c - cross-chain asset transfers

C++ SDKs:
- database.hpp - modern C++17 with std::future async
- hosting.hpp - domain and deployment operations
- bridge.hpp - cross-chain bridge with wait operations

C# SDKs:
- SynorDatabase.cs - async/await with inner store classes
- SynorHosting.cs - domain management and analytics
- SynorBridge.cs - cross-chain with BridgeException handling

Ruby SDKs:
- synor_database - Struct-based types with Faraday HTTP
- synor_hosting - domain, DNS, SSL, analytics
- synor_bridge - lock-mint/burn-unlock with retry logic

Phase 3 complete: Database/Hosting/Bridge now available in all 12 languages.
2026-01-27 02:23:07 +05:30

467 lines
16 KiB
Swift

import Foundation
/// Synor Bridge SDK client for Swift.
///
/// Provides cross-chain asset transfers with lock-mint and burn-unlock patterns.
///
/// Example:
/// ```swift
/// let bridge = SynorBridge(config: BridgeConfig(apiKey: "your-api-key"))
///
/// // Get supported chains
/// let chains = try await bridge.getSupportedChains()
///
/// // Estimate fee
/// let fee = try await bridge.estimateFee(
/// asset: "SYNR",
/// amount: "1000000000000000000",
/// sourceChain: .synor,
/// targetChain: .ethereum
/// )
///
/// // Full bridge flow
/// let transfer = try await bridge.bridgeTo(
/// asset: "SYNR",
/// amount: "1000000000000000000",
/// targetChain: .ethereum,
/// targetAddress: "0x..."
/// )
/// print("Transfer completed: \(transfer.id)")
/// ```
public class SynorBridge {
private let config: BridgeConfig
private let session: URLSession
private let decoder: JSONDecoder
private let encoder: JSONEncoder
private var closed = false
private static let finalStatuses: Set<TransferStatus> = [.completed, .failed, .refunded]
public init(config: BridgeConfig) {
self.config = config
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = config.timeout
sessionConfig.timeoutIntervalForResource = config.timeout * 2
self.session = URLSession(configuration: sessionConfig)
self.decoder = JSONDecoder()
self.encoder = JSONEncoder()
}
// MARK: - Chain Operations
/// Get all supported chains.
public func getSupportedChains() async throws -> [Chain] {
let response: ChainsResponse = try await get(path: "/chains")
return response.chains ?? []
}
/// Get chain details.
public func getChain(chainId: ChainId) async throws -> Chain {
return try await get(path: "/chains/\(chainId.rawValue)")
}
/// Check if a chain is supported.
public func isChainSupported(chainId: ChainId) async -> Bool {
do {
let chain = try await getChain(chainId: chainId)
return chain.supported
} catch {
return false
}
}
// MARK: - Asset Operations
/// Get supported assets for a chain.
public func getSupportedAssets(chainId: ChainId) async throws -> [Asset] {
let response: AssetsResponse = try await get(path: "/chains/\(chainId.rawValue)/assets")
return response.assets ?? []
}
/// Get asset details.
public func getAsset(assetId: String) async throws -> Asset {
return try await get(path: "/assets/\(assetId.urlEncoded)")
}
/// Get wrapped asset on a target chain.
public func getWrappedAsset(originalAssetId: String, targetChain: ChainId) async throws -> WrappedAsset {
return try await get(path: "/assets/\(originalAssetId.urlEncoded)/wrapped/\(targetChain.rawValue)")
}
// MARK: - Fee & Rate Operations
/// Estimate transfer fee.
public func estimateFee(
asset: String,
amount: String,
sourceChain: ChainId,
targetChain: ChainId
) async throws -> BridgeFeeEstimate {
let body: [String: String] = [
"asset": asset,
"amount": amount,
"sourceChain": sourceChain.rawValue,
"targetChain": targetChain.rawValue
]
return try await post(path: "/fees/estimate", body: body)
}
/// Get exchange rate between assets.
public func getExchangeRate(fromAsset: String, toAsset: String) async throws -> ExchangeRate {
return try await get(path: "/rates/\(fromAsset.urlEncoded)/\(toAsset.urlEncoded)")
}
// MARK: - Lock-Mint Flow
/// Lock assets on the source chain.
public func lock(
asset: String,
amount: String,
targetChain: ChainId,
options: LockOptions? = nil
) async throws -> LockReceipt {
var body: [String: Any] = [
"asset": asset,
"amount": amount,
"targetChain": targetChain.rawValue
]
if let recipient = options?.recipient { body["recipient"] = recipient }
if let deadline = options?.deadline { body["deadline"] = deadline }
if let slippage = options?.slippage { body["slippage"] = slippage }
return try await post(path: "/transfers/lock", body: body)
}
/// Get lock proof.
public func getLockProof(lockReceiptId: String) async throws -> LockProof {
return try await get(path: "/transfers/lock/\(lockReceiptId.urlEncoded)/proof")
}
/// Wait for lock proof with confirmations.
public func waitForLockProof(
lockReceiptId: String,
pollInterval: TimeInterval = 5,
maxWait: TimeInterval = 600
) async throws -> LockProof {
let deadline = Date().addingTimeInterval(maxWait)
while Date() < deadline {
do {
return try await getLockProof(lockReceiptId: lockReceiptId)
} catch let error as BridgeError {
if error.isConfirmationsPending {
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
continue
}
throw error
}
}
throw BridgeError.timeout("Waiting for lock proof")
}
/// Mint assets on the target chain.
public func mint(
proof: LockProof,
targetAddress: String,
options: MintOptions? = nil
) async throws -> BridgeSignedTransaction {
var body: [String: Any] = [
"proof": try proofToDictionary(proof),
"targetAddress": targetAddress
]
if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
if let maxFeePerGas = options?.maxFeePerGas { body["maxFeePerGas"] = maxFeePerGas }
if let maxPriorityFeePerGas = options?.maxPriorityFeePerGas { body["maxPriorityFeePerGas"] = maxPriorityFeePerGas }
return try await post(path: "/transfers/mint", body: body)
}
// MARK: - Burn-Unlock Flow
/// Burn wrapped assets.
public func burn(
wrappedAsset: String,
amount: String,
options: BurnOptions? = nil
) async throws -> BurnReceipt {
var body: [String: Any] = [
"wrappedAsset": wrappedAsset,
"amount": amount
]
if let recipient = options?.recipient { body["recipient"] = recipient }
if let deadline = options?.deadline { body["deadline"] = deadline }
return try await post(path: "/transfers/burn", body: body)
}
/// Get burn proof.
public func getBurnProof(burnReceiptId: String) async throws -> BurnProof {
return try await get(path: "/transfers/burn/\(burnReceiptId.urlEncoded)/proof")
}
/// Wait for burn proof with confirmations.
public func waitForBurnProof(
burnReceiptId: String,
pollInterval: TimeInterval = 5,
maxWait: TimeInterval = 600
) async throws -> BurnProof {
let deadline = Date().addingTimeInterval(maxWait)
while Date() < deadline {
do {
return try await getBurnProof(burnReceiptId: burnReceiptId)
} catch let error as BridgeError {
if error.isConfirmationsPending {
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
continue
}
throw error
}
}
throw BridgeError.timeout("Waiting for burn proof")
}
/// Unlock original assets.
public func unlock(
proof: BurnProof,
options: UnlockOptions? = nil
) async throws -> BridgeSignedTransaction {
var body: [String: Any] = [
"proof": try burnProofToDictionary(proof)
]
if let gasLimit = options?.gasLimit { body["gasLimit"] = gasLimit }
if let gasPrice = options?.gasPrice { body["gasPrice"] = gasPrice }
return try await post(path: "/transfers/unlock", body: body)
}
// MARK: - Transfer Management
/// Get transfer details.
public func getTransfer(transferId: String) async throws -> Transfer {
return try await get(path: "/transfers/\(transferId.urlEncoded)")
}
/// Get transfer status.
public func getTransferStatus(transferId: String) async throws -> TransferStatus {
let transfer = try await getTransfer(transferId: transferId)
return transfer.status
}
/// List transfers with optional filter.
public func listTransfers(filter: TransferFilter? = nil) async throws -> [Transfer] {
var params: [String] = []
if let status = filter?.status { params.append("status=\(status.rawValue)") }
if let sourceChain = filter?.sourceChain { params.append("sourceChain=\(sourceChain.rawValue)") }
if let targetChain = filter?.targetChain { params.append("targetChain=\(targetChain.rawValue)") }
if let limit = filter?.limit { params.append("limit=\(limit)") }
if let offset = filter?.offset { params.append("offset=\(offset)") }
var path = "/transfers"
if !params.isEmpty {
path += "?\(params.joined(separator: "&"))"
}
let response: TransfersResponse = try await get(path: path)
return response.transfers ?? []
}
/// Wait for transfer to complete.
public func waitForTransfer(
transferId: String,
pollInterval: TimeInterval = 10,
maxWait: TimeInterval = 1800
) async throws -> Transfer {
let deadline = Date().addingTimeInterval(maxWait)
while Date() < deadline {
let transfer = try await getTransfer(transferId: transferId)
if Self.finalStatuses.contains(transfer.status) {
return transfer
}
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
}
throw BridgeError.timeout("Waiting for transfer completion")
}
// MARK: - Convenience Methods
/// Complete lock-mint bridge flow.
public func bridgeTo(
asset: String,
amount: String,
targetChain: ChainId,
targetAddress: String,
lockOptions: LockOptions? = nil,
mintOptions: MintOptions? = nil
) async throws -> Transfer {
let lockReceipt = try await lock(asset: asset, amount: amount, targetChain: targetChain, options: lockOptions)
if config.debug {
print("Locked: \(lockReceipt.id), waiting for confirmations...")
}
let proof = try await waitForLockProof(lockReceiptId: lockReceipt.id)
if config.debug {
print("Proof ready, minting on \(targetChain.rawValue)...")
}
_ = try await mint(proof: proof, targetAddress: targetAddress, options: mintOptions)
return try await waitForTransfer(transferId: lockReceipt.id)
}
/// Complete burn-unlock bridge flow.
public func bridgeBack(
wrappedAsset: String,
amount: String,
burnOptions: BurnOptions? = nil,
unlockOptions: UnlockOptions? = nil
) async throws -> Transfer {
let burnReceipt = try await burn(wrappedAsset: wrappedAsset, amount: amount, options: burnOptions)
if config.debug {
print("Burned: \(burnReceipt.id), waiting for confirmations...")
}
let proof = try await waitForBurnProof(burnReceiptId: burnReceipt.id)
if config.debug {
print("Proof ready, unlocking on \(burnReceipt.targetChain.rawValue)...")
}
_ = try await unlock(proof: proof, options: unlockOptions)
return try await waitForTransfer(transferId: burnReceipt.id)
}
// MARK: - Lifecycle
/// Close the client.
public func close() {
closed = true
session.invalidateAndCancel()
}
/// Check if the client is closed.
public var isClosed: Bool { closed }
/// Perform a health check.
public func healthCheck() async -> Bool {
do {
let response: BridgeHealthResponse = try await get(path: "/health")
return response.status == "healthy"
} catch {
return false
}
}
// MARK: - Private HTTP Methods
private func get<T: Decodable>(path: String) async throws -> T {
return try await executeWithRetry {
try await self.performRequest(method: "GET", path: path, body: nil as [String: Any]?)
}
}
private func post<T: Decodable>(path: String, body: [String: Any]) async throws -> T {
return try await executeWithRetry {
try await self.performRequest(method: "POST", path: path, body: body)
}
}
private func performRequest<T: Decodable>(
method: String,
path: String,
body: [String: Any]?
) async throws -> T {
if closed { throw BridgeError.clientClosed }
guard let url = URL(string: "\(config.endpoint)\(path)") else {
throw BridgeError.invalidResponse
}
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
if let body = body {
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
} catch {
throw BridgeError.encodingError(error)
}
}
let (data, response): (Data, URLResponse)
do {
(data, response) = try await session.data(for: request)
} catch {
throw BridgeError.networkError(error)
}
guard let httpResponse = response as? HTTPURLResponse else {
throw BridgeError.invalidResponse
}
if httpResponse.statusCode >= 200 && httpResponse.statusCode < 300 {
if data.isEmpty {
throw BridgeError.invalidResponse
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw BridgeError.decodingError(error)
}
}
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
let message = errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)"
let code = errorInfo?["code"] as? String
throw BridgeError.httpError(statusCode: httpResponse.statusCode, message: message, code: code)
}
private func executeWithRetry<T>(_ operation: () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 0..<config.retries {
do {
return try await operation()
} catch {
lastError = error
if config.debug {
print("Attempt \(attempt + 1) failed: \(error)")
}
if attempt < config.retries - 1 {
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * (1 << attempt)))
}
}
}
throw lastError ?? BridgeError.invalidResponse
}
// MARK: - Private Helpers
private func proofToDictionary(_ proof: LockProof) throws -> [String: Any] {
let data = try encoder.encode(proof)
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
}
return dict
}
private func burnProofToDictionary(_ proof: BurnProof) throws -> [String: Any] {
let data = try encoder.encode(proof)
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw BridgeError.encodingError(NSError(domain: "BridgeError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode proof"]))
}
return dict
}
}
// MARK: - Private Helpers
private extension String {
var urlEncoded: String {
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
}
}