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