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.
467 lines
16 KiB
Swift
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
|
|
}
|
|
}
|