259 lines
9.1 KiB
Swift
259 lines
9.1 KiB
Swift
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 {}
|
||
}
|
||
}
|