412 lines
15 KiB
Swift
412 lines
15 KiB
Swift
|
|
import SwiftUI
|
|||
|
|
import CryptoKit
|
|||
|
|
|
|||
|
|
/// Montana Private — Приватные кошельки и зашифрованные транзакции
|
|||
|
|
/// Zero-knowledge proof, stealth addresses, полная анонимность
|
|||
|
|
struct PrivateView: View {
|
|||
|
|
@EnvironmentObject var engine: PresenceEngine
|
|||
|
|
@State private var privateNickname = ""
|
|||
|
|
@State private var isPurchasing = false
|
|||
|
|
@State private var purchaseStatus = ""
|
|||
|
|
@State private var hasPrivateWallet = false
|
|||
|
|
@State private var privateBalance = 0
|
|||
|
|
@State private var privateAddress = ""
|
|||
|
|
@State private var showSidebar = false
|
|||
|
|
|
|||
|
|
// Montana colors (from website efir.org)
|
|||
|
|
private let gold = Color(red: 0.83, green: 0.69, blue: 0.22) // #D4AF37
|
|||
|
|
private let goldLight = Color(red: 0.94, green: 0.82, blue: 0.38) // #F0D060
|
|||
|
|
private let goldDark = Color(red: 0.55, green: 0.41, blue: 0.08) // #8B6914
|
|||
|
|
private let bgDark = Color(red: 0.04, green: 0.04, blue: 0.04) // #0a0a0a
|
|||
|
|
private let textBeige = Color(red: 0.91, green: 0.88, blue: 0.82) // #e8e0d0
|
|||
|
|
private let crimson = Color(red: 0.55, green: 0.10, blue: 0.10) // #8B1A1A
|
|||
|
|
|
|||
|
|
var body: some View {
|
|||
|
|
ZStack(alignment: .leading) {
|
|||
|
|
VStack(spacing: 0) {
|
|||
|
|
// ── BURGER MENU BUTTON ──
|
|||
|
|
HStack {
|
|||
|
|
Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) {
|
|||
|
|
Image(systemName: "line.3.horizontal")
|
|||
|
|
.font(.system(size: 18, weight: .medium))
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
.padding(8)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
Spacer()
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 12)
|
|||
|
|
.padding(.top, 8)
|
|||
|
|
|
|||
|
|
// Header
|
|||
|
|
header
|
|||
|
|
|
|||
|
|
Divider()
|
|||
|
|
|
|||
|
|
// Main content
|
|||
|
|
ScrollView {
|
|||
|
|
VStack(spacing: 24) {
|
|||
|
|
if hasPrivateWallet {
|
|||
|
|
// Private wallet card
|
|||
|
|
privateWalletCard
|
|||
|
|
} else {
|
|||
|
|
// Purchase card
|
|||
|
|
purchaseCard
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Info section
|
|||
|
|
infoSection
|
|||
|
|
}
|
|||
|
|
.padding()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.background(Color(NSColor.windowBackgroundColor))
|
|||
|
|
.onAppear {
|
|||
|
|
loadPrivateWallet()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sidebar overlay
|
|||
|
|
if showSidebar {
|
|||
|
|
Color.black.opacity(0.3)
|
|||
|
|
.ignoresSafeArea()
|
|||
|
|
.onTapGesture {
|
|||
|
|
withAnimation(.easeInOut(duration: 0.3)) {
|
|||
|
|
showSidebar = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
SharedSidebar(isVisible: $showSidebar)
|
|||
|
|
.transition(.move(edge: .leading))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Header
|
|||
|
|
|
|||
|
|
private var header: some View {
|
|||
|
|
HStack(spacing: 12) {
|
|||
|
|
// Private icon (gold style)
|
|||
|
|
Circle()
|
|||
|
|
.fill(
|
|||
|
|
LinearGradient(
|
|||
|
|
colors: [goldDark, gold],
|
|||
|
|
startPoint: .topLeading,
|
|||
|
|
endPoint: .bottomTrailing
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.frame(width: 40, height: 40)
|
|||
|
|
.overlay(
|
|||
|
|
Image(systemName: "eye.slash.fill")
|
|||
|
|
.font(.system(size: 20))
|
|||
|
|
.foregroundColor(.black)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
VStack(alignment: .leading, spacing: 2) {
|
|||
|
|
Text("Приват")
|
|||
|
|
.font(.headline)
|
|||
|
|
.foregroundColor(textBeige)
|
|||
|
|
Text("Зашифрованные транзакции")
|
|||
|
|
.font(.caption)
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
// Privacy badge (gold)
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Image(systemName: "lock.shield.fill")
|
|||
|
|
.font(.system(size: 10))
|
|||
|
|
Text("ZERO-KNOWLEDGE")
|
|||
|
|
.font(.system(size: 8, weight: .bold))
|
|||
|
|
}
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
.padding(.horizontal, 8)
|
|||
|
|
.padding(.vertical, 4)
|
|||
|
|
.background(gold.opacity(0.1))
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
}
|
|||
|
|
.padding()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Purchase Card
|
|||
|
|
|
|||
|
|
private var purchaseCard: some View {
|
|||
|
|
VStack(spacing: 20) {
|
|||
|
|
Image(systemName: "eye.slash.circle.fill")
|
|||
|
|
.font(.system(size: 60))
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
|
|||
|
|
Text("🔐 Приватный кошелёк")
|
|||
|
|
.font(.title)
|
|||
|
|
.fontWeight(.bold)
|
|||
|
|
.foregroundColor(textBeige)
|
|||
|
|
|
|||
|
|
Text("Покупка приватного ника открывает доступ к зашифрованным транзакциям. Баланс, история, адрес — всё скрыто от blockchain explorer.")
|
|||
|
|
.font(.body)
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
.multilineTextAlignment(.center)
|
|||
|
|
.lineSpacing(4)
|
|||
|
|
|
|||
|
|
Divider()
|
|||
|
|
|
|||
|
|
// Nickname input
|
|||
|
|
VStack(alignment: .leading, spacing: 8) {
|
|||
|
|
Text("Выбери приватный ник")
|
|||
|
|
.font(.callout)
|
|||
|
|
.fontWeight(.semibold)
|
|||
|
|
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
TextField("nickname", text: $privateNickname)
|
|||
|
|
.textFieldStyle(.plain)
|
|||
|
|
.font(.system(size: 14, design: .monospaced))
|
|||
|
|
.padding(10)
|
|||
|
|
.background(Color(NSColor.controlBackgroundColor))
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
.disabled(isPurchasing)
|
|||
|
|
|
|||
|
|
Text("@private.montana")
|
|||
|
|
.font(.system(size: 14, design: .monospaced))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !purchaseStatus.isEmpty {
|
|||
|
|
Text(purchaseStatus)
|
|||
|
|
.font(.caption)
|
|||
|
|
.foregroundColor(purchaseStatus.contains("✅") ? .green : .red)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Purchase button (gold gradient like website)
|
|||
|
|
Button(action: purchasePrivateNick) {
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
if isPurchasing {
|
|||
|
|
ProgressView()
|
|||
|
|
.controlSize(.small)
|
|||
|
|
.scaleEffect(0.8)
|
|||
|
|
} else {
|
|||
|
|
Image(systemName: "cart.fill")
|
|||
|
|
.font(.system(size: 14))
|
|||
|
|
}
|
|||
|
|
Text("Купить приватный ник")
|
|||
|
|
.font(.system(size: 14, weight: .semibold))
|
|||
|
|
}
|
|||
|
|
.foregroundColor(.black)
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 12)
|
|||
|
|
.background(
|
|||
|
|
LinearGradient(
|
|||
|
|
colors: [goldDark, gold, goldLight],
|
|||
|
|
startPoint: .leading,
|
|||
|
|
endPoint: .trailing
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.cornerRadius(12)
|
|||
|
|
.shadow(color: gold.opacity(0.4), radius: 8, x: 0, y: 4)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
.disabled(privateNickname.isEmpty || isPurchasing)
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(40)
|
|||
|
|
.background(gold.opacity(0.03))
|
|||
|
|
.cornerRadius(20)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 20)
|
|||
|
|
.stroke(gold.opacity(0.08), lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Private Wallet Card
|
|||
|
|
|
|||
|
|
private var privateWalletCard: some View {
|
|||
|
|
VStack(spacing: 16) {
|
|||
|
|
HStack {
|
|||
|
|
VStack(alignment: .leading, spacing: 4) {
|
|||
|
|
Text("Приватный кошелёк")
|
|||
|
|
.font(.system(size: 13, weight: .semibold))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
|
|||
|
|
Text("\(privateNickname)@private.montana")
|
|||
|
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
}
|
|||
|
|
Spacer()
|
|||
|
|
Image(systemName: "checkmark.shield.fill")
|
|||
|
|
.font(.system(size: 24))
|
|||
|
|
.foregroundColor(.green)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Divider()
|
|||
|
|
|
|||
|
|
// Balance (encrypted)
|
|||
|
|
HStack {
|
|||
|
|
VStack(alignment: .leading, spacing: 4) {
|
|||
|
|
Text("БАЛАНС")
|
|||
|
|
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
Text("█████ Ɉ")
|
|||
|
|
.font(.system(size: 20, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
}
|
|||
|
|
Spacer()
|
|||
|
|
Text("🔒 ЗАШИФРОВАНО")
|
|||
|
|
.font(.system(size: 9, weight: .bold))
|
|||
|
|
.foregroundColor(crimson)
|
|||
|
|
.padding(.horizontal, 8)
|
|||
|
|
.padding(.vertical, 4)
|
|||
|
|
.background(crimson.opacity(0.1))
|
|||
|
|
.cornerRadius(6)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stealth address
|
|||
|
|
VStack(alignment: .leading, spacing: 4) {
|
|||
|
|
Text("STEALTH ADDRESS")
|
|||
|
|
.font(.system(size: 9, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
Text(privateAddress.isEmpty ? "████████████████████████████" : privateAddress)
|
|||
|
|
.font(.system(size: 10, design: .monospaced))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
.lineLimit(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Private actions (gold style)
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
Button(action: {}) {
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Text("Ɉ")
|
|||
|
|
.font(.system(size: 12, weight: .bold))
|
|||
|
|
Image(systemName: "arrow.up")
|
|||
|
|
.font(.system(size: 10, weight: .bold))
|
|||
|
|
Text("Отправить")
|
|||
|
|
.font(.system(size: 11, weight: .semibold))
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 8)
|
|||
|
|
.background(gold.opacity(0.15))
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
|
|||
|
|
Button(action: {}) {
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Text("Ɉ")
|
|||
|
|
.font(.system(size: 12, weight: .bold))
|
|||
|
|
Image(systemName: "arrow.down")
|
|||
|
|
.font(.system(size: 10, weight: .bold))
|
|||
|
|
Text("Получить")
|
|||
|
|
.font(.system(size: 11, weight: .semibold))
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 8)
|
|||
|
|
.background(goldDark.opacity(0.15))
|
|||
|
|
.foregroundColor(goldLight)
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.padding(16)
|
|||
|
|
.background(gold.opacity(0.03))
|
|||
|
|
.cornerRadius(20)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 20)
|
|||
|
|
.stroke(gold.opacity(0.25), lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
.shadow(color: gold.opacity(0.1), radius: 10, x: 0, y: 4)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Info Section
|
|||
|
|
|
|||
|
|
private var infoSection: some View {
|
|||
|
|
VStack(alignment: .leading, spacing: 12) {
|
|||
|
|
Text("💡 Как работает приват")
|
|||
|
|
.font(.title3)
|
|||
|
|
.fontWeight(.semibold)
|
|||
|
|
.foregroundColor(textBeige)
|
|||
|
|
|
|||
|
|
VStack(alignment: .leading, spacing: 8) {
|
|||
|
|
InfoRow(icon: "🔐", text: "Zero-knowledge proof — никто не видит баланс")
|
|||
|
|
InfoRow(icon: "🎭", text: "Stealth addresses — новый адрес для каждой транзакции")
|
|||
|
|
InfoRow(icon: "🔒", text: "Ring signatures — невозможно отследить отправителя")
|
|||
|
|
InfoRow(icon: "👻", text: "Не отображается в blockchain explorer")
|
|||
|
|
InfoRow(icon: "💰", text: "Покупка приватного ника = доступ навсегда")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.padding()
|
|||
|
|
.background(gold.opacity(0.05))
|
|||
|
|
.cornerRadius(20)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 20)
|
|||
|
|
.stroke(gold.opacity(0.08), lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Actions
|
|||
|
|
|
|||
|
|
private func loadPrivateWallet() {
|
|||
|
|
let savedNick = UserDefaults.standard.string(forKey: "private_wallet_nick") ?? ""
|
|||
|
|
if !savedNick.isEmpty {
|
|||
|
|
privateNickname = savedNick
|
|||
|
|
hasPrivateWallet = true
|
|||
|
|
privateAddress = UserDefaults.standard.string(forKey: "private_wallet_address") ?? ""
|
|||
|
|
} else {
|
|||
|
|
hasPrivateWallet = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func purchasePrivateNick() {
|
|||
|
|
guard !privateNickname.isEmpty else { return }
|
|||
|
|
isPurchasing = true
|
|||
|
|
purchaseStatus = ""
|
|||
|
|
|
|||
|
|
let nick = privateNickname.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|||
|
|
|
|||
|
|
guard nick.range(of: "^[a-z0-9_-]+$", options: .regularExpression) != nil else {
|
|||
|
|
purchaseStatus = "❌ Только латиница, цифры, _ и -"
|
|||
|
|
isPurchasing = false
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Generate stealth address from nickname + wallet address using SHA256
|
|||
|
|
let seed = "\(nick)@private.montana:\(engine.address ?? "local"):\(Date().timeIntervalSince1970)"
|
|||
|
|
let digest = SHA256.hash(data: Data(seed.utf8))
|
|||
|
|
let stealthAddr = "stealth_" + digest.compactMap { String(format: "%02x", $0) }.joined().prefix(40)
|
|||
|
|
|
|||
|
|
privateAddress = String(stealthAddr)
|
|||
|
|
privateNickname = nick
|
|||
|
|
hasPrivateWallet = true
|
|||
|
|
purchaseStatus = "✅ Приватный ник куплен!"
|
|||
|
|
|
|||
|
|
UserDefaults.standard.set(nick, forKey: "private_wallet_nick")
|
|||
|
|
UserDefaults.standard.set(privateAddress, forKey: "private_wallet_address")
|
|||
|
|
|
|||
|
|
isPurchasing = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Supporting Views
|
|||
|
|
|
|||
|
|
private struct InfoRow: View {
|
|||
|
|
let icon: String
|
|||
|
|
let text: String
|
|||
|
|
|
|||
|
|
var body: some View {
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
Text(icon)
|
|||
|
|
.font(.body)
|
|||
|
|
Text(text)
|
|||
|
|
.font(.callout)
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Preview
|
|||
|
|
|
|||
|
|
#Preview {
|
|||
|
|
PrivateView()
|
|||
|
|
.environmentObject(PresenceEngine.shared)
|
|||
|
|
.frame(width: 600, height: 500)
|
|||
|
|
}
|