import Foundation import Security /// Small Keychain wrapper for wallet secrets. Secrets never touch UserDefaults. enum Keychain { private static let service = "com.koins.app" static func set(_ value: String, for key: String) { guard let data = value.data(using: .utf8) else { return } remove(key) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, ] SecItemAdd(query as CFDictionary, nil) } static func get(_ key: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, ] var item: CFTypeRef? guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let data = item as? Data, let value = String(data: data, encoding: .utf8) else { return nil } return value } static func remove(_ key: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, ] SecItemDelete(query as CFDictionary) } } enum WalletStore { private static let defaults = UserDefaults.standard private static func walletKey(_ chain: Chain) -> String { "koins.wallet.\(chain.rawValue)" } private static func secretKey(_ chain: Chain) -> String { "koins.secret.\(chain.rawValue)" } static func wallet(_ chain: Chain) -> WalletRecord? { guard let data = defaults.data(forKey: walletKey(chain)) else { return nil } return try? JSONDecoder().decode(WalletRecord.self, from: data) } static func saveWallet(_ wallet: WalletRecord, chain: Chain) { guard let data = try? JSONEncoder().encode(wallet) else { return } defaults.set(data, forKey: walletKey(chain)) } static func removeWallet(_ chain: Chain) { defaults.removeObject(forKey: walletKey(chain)) defaults.removeObject(forKey: cacheKey(chain)) Keychain.remove(secretKey(chain)) } // MARK: - Cached state (balance, transactions, accounts) private static func cacheKey(_ chain: Chain) -> String { "koins.cache.\(chain.rawValue)" } static func cachedState(_ chain: Chain) -> ChainCache? { guard let data = defaults.data(forKey: cacheKey(chain)) else { return nil } return try? JSONDecoder().decode(ChainCache.self, from: data) } static func saveCachedState(_ state: ChainCache, chain: Chain) { guard let data = try? JSONEncoder().encode(state) else { return } defaults.set(data, forKey: cacheKey(chain)) } // MARK: - Swap orders (kyc.rip) private static let swapOrdersKey = "koins.swapOrders" static func swapOrders() -> [SwapOrder] { guard let data = defaults.data(forKey: swapOrdersKey), let orders = try? JSONDecoder().decode([SwapOrder].self, from: data) else { return [] } return orders } static func saveSwapOrder(_ order: SwapOrder) { var orders = swapOrders() orders.removeAll { $0.id == order.id } orders.insert(order, at: 0) guard let data = try? JSONEncoder().encode(orders) else { return } defaults.set(data, forKey: swapOrdersKey) } static func removeSwapOrder(id: String) { var orders = swapOrders() orders.removeAll { $0.id == id } guard let data = try? JSONEncoder().encode(orders) else { return } defaults.set(data, forKey: swapOrdersKey) } static func secret(_ chain: Chain) -> String? { Keychain.get(secretKey(chain)) } static func saveSecret(_ secret: String, chain: Chain) { Keychain.set(secret, for: secretKey(chain)) } }