116 lines
3.8 KiB
Swift
116 lines
3.8 KiB
Swift
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))
|
|
}
|
|
} |