import Foundation import Combine import AppKit @MainActor class PresenceEngine: ObservableObject { static let shared = PresenceEngine() @Published var isTracking = false @Published var isPresent = false @Published var sessionSeconds: Int = 0 @Published var pendingSeconds: Int = 0 @Published var serverBalance: Int = 0 @Published var isOnline = false @Published var networkNodes: [(name: String, location: String, online: Bool)] = [] @Published var networkHealth: String = "0%" @Published var networkOnline: Int = 0 @Published var networkTotal: Int = 3 @Published var protocolVersion: String = "2.0.0" @Published var protocolMode: String = "MAINNET" @Published var protocolCrypto: String = "ML-DSA-65 (FIPS 204)" @Published var ledgerVerified: Bool = false @Published var ledgerBalance: Int = 0 @Published var walletSynced: Bool = false @Published var t2BlockNumber: Int = 0 @Published var t2SecondsElapsed: Int = 0 @Published var t2TrackingSeconds: Int = 0 @Published var showSymbolInMenuBar: Bool = true @Published var showBalanceInMenuBar: Bool = false @Published var genesisDate: Date = { var c = DateComponents() c.year = 2026; c.month = 1; c.day = 9; c.hour = 0; c.minute = 0 return Calendar.current.date(from: c) ?? Date() }() var displayBalance: Int { serverBalance + pendingSeconds } /// Callback fired AFTER pendingSeconds is updated in tick() — for menu bar sync var onTick: (() -> Void)? // ── GENESIS ECONOMICS ── // Montana Genesis: 09.01.2026 — network launch // Price Genesis: 12.03.2021 — BIPL (Bill Payment) price anchor // // Model: 1 second of human presence = 1 Ɉ // BIPL price: 1 second = $0.1605 USD // RUB rate at price genesis (12.03.2021): ~75 ₽/$ // 1 Ɉ = $0.1605 = ₽12.04 // // Source: https://x.com/tojesatoshi/status/2012823709858275473 // static let genesisPriceUSD: Double = 0.1605 // $0.1605 per Ɉ (BIPL) static let genesisPriceRUB: Double = 12.04 // ₽12.04 per Ɉ ($0.1605 × 75 ₽/$) static let genesisSettlementDate = "12.03.2021" var balanceUSD: Double { Double(displayBalance) * Self.genesisPriceUSD } var balanceRUB: Double { Double(displayBalance) * Self.genesisPriceRUB } var epochDays: Int { let diff = Calendar.current.dateComponents([.day], from: genesisDate, to: Date()) return max(diff.day ?? 0, 0) } var epochNumber: Int { epochDays / 7 + 1 } let t2Duration = 600 var t2SecondsRemaining: Int { max(t2Duration - t2SecondsElapsed, 0) } var t2Progress: Double { min(Double(t2SecondsElapsed) / Double(t2Duration), 1.0) } var t2PendingCoins: Int { t2TrackingSeconds } /// Available balance = everything confirmed BEFORE current T2 window var availableBalance: Int { max(displayBalance - t2PendingCoins, 0) } private var tickTimer: Timer? private var reportTimer: Timer? private var syncTimer: Timer? private var tickCount = 0 private let pendingKey = "montana_presence_pending" private let balanceKey = "montana_presence_balance" let api = MontanaAPIClient() private init() { pendingSeconds = UserDefaults.standard.integer(forKey: pendingKey) serverBalance = UserDefaults.standard.integer(forKey: balanceKey) showSymbolInMenuBar = UserDefaults.standard.object(forKey: "menubar_symbol") as? Bool ?? true showBalanceInMenuBar = UserDefaults.standard.object(forKey: "menubar_balance") as? Bool ?? false updateT2() } func toggleMenuBarSymbol() { showSymbolInMenuBar.toggle() UserDefaults.standard.set(showSymbolInMenuBar, forKey: "menubar_symbol") } func toggleMenuBarBalance() { showBalanceInMenuBar.toggle() UserDefaults.standard.set(showBalanceInMenuBar, forKey: "menubar_balance") } var address: String? { get { UserDefaults.standard.string(forKey: "montana_address") } set { UserDefaults.standard.set(newValue, forKey: "montana_address") } } var walletNumber: Int { UserDefaults.standard.integer(forKey: "wallet_number") } var displayAddress: String { let num = walletNumber if num > 0 { return "\u{0248}-\(num)" } if let addr = address { return String(addr.prefix(8)) + "..." + String(addr.suffix(4)) } return "" } func autoStart() { guard let addr = address, !addr.isEmpty, !isTracking else { return } if walletNumber == 0 { Task { do { let wallet = try await api.registerAgentWallet(address: addr) UserDefaults.standard.set(wallet.number, forKey: "wallet_number") } catch {} } } startTracking() } func startTracking() { guard !isTracking else { return } isTracking = true isPresent = true sessionSeconds = 0 t2TrackingSeconds = 0 tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in Task { @MainActor in self?.tick() } } reportTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in Task { @MainActor in await self?.reportToServer() } } syncTimer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in Task { @MainActor in await self?.syncBalance() } } Task { await syncBalance() } } func stopTracking() { isTracking = false isPresent = false tickTimer?.invalidate() tickTimer = nil reportTimer?.invalidate() reportTimer = nil syncTimer?.invalidate() syncTimer = nil Task { await reportToServer() } } /// Full reset — called on logout. Zeroes everything. func fullReset() { stopTracking() serverBalance = 0 pendingSeconds = 0 sessionSeconds = 0 tickCount = 0 isOnline = false walletSynced = false ledgerVerified = false ledgerBalance = 0 t2TrackingSeconds = 0 address = nil UserDefaults.standard.set(0, forKey: pendingKey) UserDefaults.standard.set(0, forKey: balanceKey) UserDefaults.standard.removeObject(forKey: "wallet_number") } // 1 second = 1 Ɉ. Always. private func tick() { guard isTracking else { return } sessionSeconds += 1 pendingSeconds += 1 tickCount += 1 updateT2() t2TrackingSeconds += 1 if tickCount % 10 == 0 { UserDefaults.standard.set(pendingSeconds, forKey: pendingKey) } onTick?() } func updateT2() { let secs = max(Int(Date().timeIntervalSince(genesisDate)), 0) let newBlock = secs / t2Duration if newBlock != t2BlockNumber { t2TrackingSeconds = 0 } t2BlockNumber = newBlock t2SecondsElapsed = secs % t2Duration } func reportToServer() async { guard let addr = address, !addr.isEmpty, pendingSeconds > 0 else { return } let delta = pendingSeconds do { let newBalance = try await api.reportPresence(address: addr, seconds: delta) let expectedMin = serverBalance + delta serverBalance = max(newBalance, expectedMin) pendingSeconds -= delta if pendingSeconds < 0 { pendingSeconds = 0 } isOnline = true walletSynced = pendingSeconds == 0 UserDefaults.standard.set(pendingSeconds, forKey: pendingKey) UserDefaults.standard.set(serverBalance, forKey: balanceKey) } catch { serverBalance += delta pendingSeconds -= delta if pendingSeconds < 0 { pendingSeconds = 0 } isOnline = false UserDefaults.standard.set(pendingSeconds, forKey: pendingKey) UserDefaults.standard.set(serverBalance, forKey: balanceKey) } } func syncBalance() async { guard let addr = address, !addr.isEmpty else { return } if pendingSeconds == 0 { do { let balance = try await api.fetchBalance(address: addr) serverBalance = max(balance, serverBalance) isOnline = true walletSynced = true UserDefaults.standard.set(serverBalance, forKey: balanceKey) } catch { isOnline = false walletSynced = false } } do { let (net, proto) = try await api.fetchStatus() networkNodes = net.nodes networkOnline = net.onlineCount networkTotal = net.totalNodes networkHealth = net.health protocolVersion = proto.version protocolMode = proto.mode protocolCrypto = proto.crypto } catch {} do { let verify = try await api.fetchLedgerVerify(address: addr) ledgerVerified = verify.verified ledgerBalance = verify.ledgerBalance } catch {} } }