import Foundation /// Synor Economics SDK client for Swift. /// /// Provides pricing, billing, staking, and discount operations. /// /// Example: /// ```swift /// let economics = SynorEconomics(config: EconomicsConfig(apiKey: "your-api-key")) /// /// // Get price for compute usage /// let usage = UsageMetrics(computeHours: 100, gpuHours: 10) /// let price = try await economics.getPrice(service: .compute, usage: usage) /// /// // Stake tokens /// let receipt = try await economics.stake(amount: "1000000000000000000") /// ``` public class SynorEconomics { private let config: EconomicsConfig private let session: URLSession private let decoder: JSONDecoder private let encoder: JSONEncoder private var closed = false public init(config: EconomicsConfig) { 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: - Pricing Operations /// Get price for service usage. public func getPrice(service: ServiceType, usage: UsageMetrics) async throws -> Price { let body: [String: Any] = [ "service": service.rawValue, "usage": [ "computeHours": usage.computeHours, "storageGb": usage.storageGb, "requests": usage.requests, "bandwidthGb": usage.bandwidthGb, "gpuHours": usage.gpuHours ] ] return try await post(path: "/pricing/calculate", body: body) } /// Estimate cost for a usage plan. public func estimateCost(plan: UsagePlan) async throws -> CostEstimate { let body: [String: Any] = [ "service": plan.service.rawValue, "estimatedUsage": [ "computeHours": plan.estimatedUsage.computeHours, "storageGb": plan.estimatedUsage.storageGb, "requests": plan.estimatedUsage.requests, "bandwidthGb": plan.estimatedUsage.bandwidthGb, "gpuHours": plan.estimatedUsage.gpuHours ], "period": plan.period.rawValue ] return try await post(path: "/pricing/estimate", body: body) } /// Get pricing tiers for a service. public func getPricingTiers(service: ServiceType) async throws -> [PricingTier] { let response: TiersResponse = try await get(path: "/pricing/tiers/\(service.rawValue)") return response.tiers ?? [] } // MARK: - Billing Operations /// Get usage for a billing period. public func getUsage(period: BillingPeriod? = nil) async throws -> Usage { var path = "/billing/usage" if let period = period { path += "?period=\(period.rawValue)" } return try await get(path: path) } /// Get usage by date range. public func getUsageByDateRange(startDate: Int64, endDate: Int64) async throws -> Usage { return try await get(path: "/billing/usage?startDate=\(startDate)&endDate=\(endDate)") } /// Get all invoices. public func getInvoices() async throws -> [Invoice] { let response: InvoicesResponse = try await get(path: "/billing/invoices") return response.invoices ?? [] } /// Get invoice by ID. public func getInvoice(invoiceId: String) async throws -> Invoice { return try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)") } /// Download invoice as PDF data. public func downloadInvoice(invoiceId: String) async throws -> Data { let response: [String: String] = try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)/pdf") guard let base64 = response["data"], let data = Data(base64Encoded: base64) else { throw EconomicsError(message: "Invalid PDF data") } return data } /// Get account balance. public func getBalance() async throws -> AccountBalance { return try await get(path: "/billing/balance") } /// Add credits to account. public func addCredits(amount: String, paymentMethod: String) async throws { let body = ["amount": amount, "paymentMethod": paymentMethod] let _: [String: Any] = try await post(path: "/billing/credits", body: body) } // MARK: - Staking Operations /// Stake tokens. public func stake(amount: String, durationDays: Int? = nil) async throws -> StakeReceipt { var body: [String: Any] = ["amount": amount] if let duration = durationDays { body["durationDays"] = duration } return try await post(path: "/staking/stake", body: body) } /// Unstake tokens. public func unstake(stakeId: String) async throws -> UnstakeReceipt { return try await post(path: "/staking/unstake/\(stakeId.urlEncoded)", body: [:] as [String: String]) } /// Get all stakes. public func getStakes() async throws -> [StakeInfo] { let response: StakesResponse = try await get(path: "/staking/stakes") return response.stakes ?? [] } /// Get stake by ID. public func getStake(stakeId: String) async throws -> StakeInfo { return try await get(path: "/staking/stakes/\(stakeId.urlEncoded)") } /// Get staking rewards. public func getStakingRewards() async throws -> Rewards { return try await get(path: "/staking/rewards") } /// Claim staking rewards. public func claimRewards() async throws -> String { let response: [String: String] = try await post(path: "/staking/rewards/claim", body: [:] as [String: String]) return response["txHash"] ?? "" } /// Get current APY. public func getCurrentApy() async throws -> Double { let response: [String: Double] = try await get(path: "/staking/apy") return response["apy"] ?? 0 } // MARK: - Discount Operations /// Apply a discount code. public func applyDiscount(code: String) async throws -> AppliedDiscount { return try await post(path: "/discounts/apply", body: ["code": code]) } /// Get available discounts. public func getAvailableDiscounts() async throws -> [Discount] { let response: DiscountsResponse = try await get(path: "/discounts/available") return response.discounts ?? [] } /// Validate a discount code. public func validateDiscount(code: String) async throws -> Discount { return try await get(path: "/discounts/validate/\(code.urlEncoded)") } /// Get applied discounts. public func getAppliedDiscounts() async throws -> [AppliedDiscount] { let response: AppliedDiscountsResponse = try await get(path: "/discounts/applied") return response.discounts ?? [] } /// Remove a discount. public func removeDiscount(discountId: String) async throws { let _: [String: Any] = try await delete(path: "/discounts/\(discountId.urlEncoded)") } // MARK: - Lifecycle /// Close the client. public func close() { closed = true } /// Check if client is closed. public var isClosed: Bool { closed } /// Health check. public func healthCheck() async -> Bool { do { let response: [String: String] = try await get(path: "/health") return response["status"] == "healthy" } catch { return false } } // MARK: - Private Methods private func get(path: String) async throws -> T { return try await request(method: "GET", path: path) } private func post(path: String, body: Any) async throws -> T { return try await request(method: "POST", path: path, body: body) } private func delete(path: String) async throws -> T { return try await request(method: "DELETE", path: path) } private func request(method: String, path: String, body: Any? = nil) async throws -> T { guard !closed else { throw EconomicsError(message: "Client has been closed") } var lastError: Error? for attempt in 0..(method: String, path: String, body: Any?) async throws -> T { guard let url = URL(string: config.endpoint + path) else { throw EconomicsError(message: "Invalid URL") } 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 { request.httpBody = try JSONSerialization.data(withJSONObject: body) } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw EconomicsError(message: "Invalid response") } if httpResponse.statusCode >= 400 { let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any] throw EconomicsError( message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)", code: errorInfo?["code"] as? String, statusCode: httpResponse.statusCode ) } return try decoder.decode(T.self, from: data) } } // MARK: - Extensions extension String { var urlEncoded: String { addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self } }