montana/macOS/MontanaPresence/MontanaAPIClient.swift

806 lines
38 KiB
Swift
Raw Normal View History

import Foundation
class MontanaAPIClient: NSObject, URLSessionDelegate {
private let endpoints: [(name: String, url: String)] = [
("Primary", "https://efir.org"),
("Amsterdam", "http://72.56.102.240:8889"),
("Moscow", "http://176.124.208.93:8889"),
("Almaty", "http://91.200.148.93:8889")
]
// Certificate pinning: HTTPS endpoints only (efir.org)
// HTTP nodes (Timeweb IPs) on trusted network - no pinning required
private lazy var secureSession: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
// URLSessionDelegate: Certificate validation for HTTPS endpoints
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Only validate HTTPS connections
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
challenge.protectionSpace.host == "efir.org",
let serverTrust = challenge.protectionSpace.serverTrust else {
// HTTP or non-HTTPS challenge - use default handling
completionHandler(.performDefaultHandling, nil)
return
}
// Validate certificate chain for efir.org
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
}
func reportPresence(address: String, seconds: Int) async throws -> Int {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/presence") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(address, forHTTPHeaderField: "X-Device-ID")
request.setValue(address, forHTTPHeaderField: "X-Address")
request.timeoutInterval = 10
request.httpBody = try JSONSerialization.data(withJSONObject: ["seconds": seconds])
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let balance = json["balance"] as? Int else {
throw URLError(.cannotParseResponse)
}
return balance
}
}
func fetchBalance(address: String) async throws -> Int {
return try await tryAllEndpoints { endpoint in
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
guard let url = URL(string: "\(endpoint)/api/balance/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let balance = json["balance"] as? Int else {
throw URLError(.cannotParseResponse)
}
return balance
}
}
func lookupWallet(identifier: String) async throws -> (address: String, alias: String) {
return try await tryAllEndpoints { endpoint in
let encoded = identifier.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? identifier
guard let url = URL(string: "\(endpoint)/api/wallet/lookup/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
// Extract mt address from crypto_hash
let cryptoHash = json["crypto_hash"] as? String ?? ""
let mtAddress = "mt" + cryptoHash
let alias = json["alias"] as? String ?? ""
return (mtAddress, alias)
}
}
func transfer(from: String, to: String, amount: Int) async throws {
try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/transfer") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
let timestamp = ISO8601DateFormatter().string(from: Date())
let body: [String: Any] = [
"from_address": from,
"to_address": to,
"amount": amount,
"timestamp": timestamp
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.cannotParseResponse)
}
if httpResponse.statusCode != 200 {
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? String {
throw NSError(domain: "Montana", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: error])
}
throw URLError(.cannotParseResponse)
}
return ()
}
}
struct NetworkStatus {
var nodes: [(name: String, location: String, online: Bool)] = []
var onlineCount: Int = 0
var totalNodes: Int = 0
var health: String = "0%"
}
struct ProtocolStatus {
var version: String = ""
var mode: String = ""
var crypto: String = ""
}
struct LedgerVerification {
var ledgerBalance: Int = 0
var cachedBalance: Int = 0
var verified: Bool = false
}
func fetchStatus() async throws -> (network: NetworkStatus, protocol_: ProtocolStatus) {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/status") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
var net = NetworkStatus()
if let network = json["network"] as? [String: Any],
let nodes = network["nodes"] as? [String: [String: Any]],
let summary = network["summary"] as? [String: Any] {
net.onlineCount = summary["online_nodes"] as? Int ?? 0
net.totalNodes = summary["total_nodes"] as? Int ?? 0
net.health = summary["network_health"] as? String ?? "0%"
for (_, info) in nodes {
net.nodes.append((
name: info["name"] as? String ?? "",
location: info["location"] as? String ?? "",
online: info["online"] as? Bool ?? false
))
}
net.nodes.sort { $0.name < $1.name }
}
var proto = ProtocolStatus()
if let montana = json["montana"] as? [String: Any] {
proto.version = montana["version"] as? String ?? ""
proto.mode = montana["mode"] as? String ?? ""
proto.crypto = montana["crypto"] as? String ?? ""
}
return (net, proto)
}
}
func fetchLedgerVerify(address: String) async throws -> LedgerVerification {
return try await tryAllEndpoints { endpoint in
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
guard let url = URL(string: "\(endpoint)/api/ledger/verify/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return LedgerVerification(
ledgerBalance: json["ledger_balance"] as? Int ?? 0,
cachedBalance: json["cached_balance"] as? Int ?? 0,
verified: json["verified"] as? Bool ?? false
)
}
}
//
// AI Agent API register wallet, manage balance
//
struct AgentWallet {
var number: Int
var alias: String
var address: String
var cryptoHash: String
}
/// Register AI agent wallet by address, get sequential number
func registerAgentWallet(address: String, alias: String? = nil) async throws -> AgentWallet {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/wallet/register") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 10
var body: [String: Any] = ["address": address]
if let alias = alias { body["alias"] = alias }
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return AgentWallet(
number: json["number"] as? Int ?? 0,
alias: json["alias"] as? String ?? "",
address: json["address"] as? String ?? "",
cryptoHash: json["crypto_hash"] as? String ?? ""
)
}
}
/// Register AI agent wallet with public key (ML-DSA-65)
func registerWithPublicKey(publicKey: String) async throws -> (address: String, balance: Int) {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/register") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 10
request.httpBody = try JSONSerialization.data(withJSONObject: ["public_key": publicKey])
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return (
json["address"] as? String ?? "",
Int(json["balance"] as? Double ?? 0)
)
}
}
//
// TimeChain Core API τ/τ/τ/τ blocks, UTXO, verification
//
func fetchTimeChainStats() async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/timechain/stats") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
func fetchTimeChainWindows(layer: String = "tau1", limit: Int = 20) async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/timechain/windows?layer=\(layer)&limit=\(limit)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let windows = json["windows"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return windows
}
}
func fetchTimeChainVerify() async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/timechain/verify") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 15
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
func fetchTimeChainBalances() async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/timechain/balances") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let balances = json["balances"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return balances
}
}
//
// NTS Anchor API 36 Global Atomic Time Servers
//
func fetchNTSServers() async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/nts/servers") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
func fetchNTSStatus() async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/nts/status") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
func fetchNTSAnchor(tau2Number: Int) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/nts/anchor/\(tau2Number)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
//
// TimeChain Explorer API events & addresses (legacy EventLedger)
//
func fetchMyEvents(address: String, limit: Int = 100) async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? address
guard let url = URL(string: "\(endpoint)/api/node/events?address=\(encoded)&limit=\(limit)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let events = json["events"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return events
}
}
func fetchEvents(limit: Int = 50) async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/node/events?limit=\(limit)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let events = json["events"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return events
}
}
func fetchAddresses() async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/addresses") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let addresses = json["addresses"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return addresses
}
}
/// Query balance from ALL endpoints in parallel, return array of (name, balance) for consensus check
func fetchBalanceFromAll(address: String) async -> [(name: String, balance: Int)] {
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
return await withTaskGroup(of: (String, Int)?.self) { group in
for ep in endpoints {
group.addTask { [weak self] in
guard let self else { return nil }
guard let url = URL(string: "\(ep.url)/api/balance/\(encoded)") else { return nil }
var request = URLRequest(url: url)
request.timeoutInterval = 8
do {
let session = ep.url.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let balance = json["balance"] as? Int else { return nil }
return (ep.name, balance)
} catch { return nil }
}
}
var results: [(name: String, balance: Int)] = []
for await result in group {
if let r = result { results.append(r) }
}
return results
}
}
func fetchAddressBalance(query: String) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? query
guard let url = URL(string: "\(endpoint)/api/address/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return json
}
}
func fetchAddressTransactions(query: String) async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? query
guard let url = URL(string: "\(endpoint)/api/address/\(encoded)/transactions") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let transactions = json["transactions"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return transactions
}
}
//
// Auction & Domain API
//
func fetchAuctionPrice(serviceType: String) async throws -> (nextPrice: Int, totalSold: Int) {
return try await tryAllEndpoints { endpoint in
let encoded = serviceType.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? serviceType
guard let url = URL(string: "\(endpoint)/api/auction/price/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return (
json["next_price"] as? Int ?? 1,
json["total_sold"] as? Int ?? 0
)
}
}
func checkDomainAvailability(domain: String) async throws -> (available: Bool, price: Int) {
return try await tryAllEndpoints { endpoint in
let encoded = domain.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? domain
guard let url = URL(string: "\(endpoint)/api/domain/available/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return (
json["available"] as? Bool ?? false,
json["price"] as? Int ?? 1
)
}
}
func registerDomain(domain: String, ownerAddress: String, amount: Int) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/domain/register") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"domain": domain,
"owner_address": ownerAddress,
"amount": amount
])
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
if httpResponse.statusCode != 200 {
let error = json["error"] as? String ?? "Unknown error"
throw NSError(domain: "Montana", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: error])
}
return json
}
}
//
// Phone Service API
//
func registerPhone(number: Int, ownerAddress: String, amount: Int) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/phone/register") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"number": number,
"owner_address": ownerAddress,
"amount": amount
])
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
if httpResponse.statusCode != 200 {
let error = json["error"] as? String ?? "Unknown error"
throw NSError(domain: "Montana", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: error])
}
return json
}
}
func requestPhoneBind(phone: String, montanaAddress: String) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/phone/bind/request") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"phone": phone,
"montana_address": montanaAddress
])
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
if httpResponse.statusCode != 200 {
let error = json["error"] as? String ?? "Unknown error"
throw NSError(domain: "Montana", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: error])
}
return json
}
}
func verifyPhoneBind(phone: String, montanaAddress: String, code: String) async throws -> [String: Any] {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/phone/bind/verify") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15
request.httpBody = try JSONSerialization.data(withJSONObject: [
"phone": phone,
"montana_address": montanaAddress,
"code": code
])
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
if httpResponse.statusCode != 200 {
let error = json["error"] as? String ?? "Unknown error"
throw NSError(domain: "Montana", code: httpResponse.statusCode,
userInfo: [NSLocalizedDescriptionKey: error])
}
return json
}
}
func fetchBoundPhones(address: String) async throws -> [[String: Any]] {
return try await tryAllEndpoints { endpoint in
let encoded = address.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? address
guard let url = URL(string: "\(endpoint)/api/phone/bind/address/\(encoded)") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let phones = json["phones"] as? [[String: Any]] else {
throw URLError(.cannotParseResponse)
}
return phones
}
}
func fetchCallPricing() async throws -> (audioPerSecond: Int, videoPerSecond: Int) {
return try await tryAllEndpoints { endpoint in
guard let url = URL(string: "\(endpoint)/api/call/pricing") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = 10
let session = endpoint.hasPrefix("https://") ? self.secureSession : URLSession.shared
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw URLError(.cannotParseResponse)
}
return (
json["audio_per_second"] as? Int ?? 1,
json["video_per_second"] as? Int ?? 1
)
}
}
private func tryAllEndpoints<T>(_ operation: (String) async throws -> T) async throws -> T {
var lastError: Error = URLError(.cannotConnectToHost)
for endpoint in endpoints {
do {
return try await operation(endpoint.url)
} catch {
lastError = error
continue
}
}
throw lastError
}
}