Migrate wallet addresses storage from UserDefaults to a single JSON file #3760

pull/3777/head
Krypto Pank 3 years ago
parent d205e474b3
commit 0cf51ddbbd
  1. 12
      AlphaWallet.xcodeproj/project.pbxproj
  2. 7
      AlphaWallet/AppCoordinator.swift
  3. 4
      AlphaWallet/AppDelegate.swift
  4. 2
      AlphaWallet/Core/CoinTicker/CoinTickersFetcherCache.swift
  5. 78
      AlphaWallet/Core/Coordinators/Alerts/StorageType.swift
  6. 1
      AlphaWallet/Core/Features.swift
  7. 2
      AlphaWallet/Core/Types/AppTracker.swift
  8. 24
      AlphaWallet/Core/Types/RealmConfiguration.swift
  9. 2
      AlphaWallet/InCoordinator.swift
  10. 90
      AlphaWallet/KeyManagement/DefaultsWalletAddressesStore.swift
  11. 115
      AlphaWallet/KeyManagement/EtherKeystore.swift
  12. 159
      AlphaWallet/KeyManagement/JsonWalletAddressesStore.swift
  13. 35
      AlphaWallet/KeyManagement/WalletAddressesStoreType.swift
  14. 28
      AlphaWallet/Settings/Types/Config.swift
  15. 6
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinatorForActivities.swift
  16. 2
      AlphaWallet/Transactions/Types/TransactionsTracker.swift
  17. 2
      AlphaWallet/Wallet/ViewControllers/ImportWalletViewController.swift
  18. 2
      AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift

@ -822,6 +822,9 @@
87584B4125EF911D0070063B /* ShowSeedPhraseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4025EF911D0070063B /* ShowSeedPhraseCoordinator.swift */; };
87584B4425EFAFEC0070063B /* BuyTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4325EFAFEC0070063B /* BuyTokenService.swift */; };
87584B4725EFB0950070063B /* Ramp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87584B4625EFB0950070063B /* Ramp.swift */; };
8759C9E7279ADEFC00FE361F /* WalletAddressesStoreType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8759C9E6279ADEFC00FE361F /* WalletAddressesStoreType.swift */; };
8759C9E9279BD7B100FE361F /* DefaultsWalletAddressesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8759C9E8279BD7B100FE361F /* DefaultsWalletAddressesStore.swift */; };
8759C9EB279BD7C300FE361F /* JsonWalletAddressesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8759C9EA279BD7C300FE361F /* JsonWalletAddressesStore.swift */; };
8759C9ED279E9D6100FE361F /* TextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8759C9EC279E9D6100FE361F /* TextFieldView.swift */; };
875B3C34250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */; };
87620024266E00F70059B05A /* known_contract.json in Resources */ = {isa = PBXBuildFile; fileRef = 87620023266E00F60059B05A /* known_contract.json */; };
@ -1895,6 +1898,9 @@
87584B4025EF911D0070063B /* ShowSeedPhraseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowSeedPhraseCoordinator.swift; sourceTree = "<group>"; };
87584B4325EFAFEC0070063B /* BuyTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyTokenService.swift; sourceTree = "<group>"; };
87584B4625EFB0950070063B /* Ramp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ramp.swift; sourceTree = "<group>"; };
8759C9E6279ADEFC00FE361F /* WalletAddressesStoreType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAddressesStoreType.swift; sourceTree = "<group>"; };
8759C9E8279BD7B100FE361F /* DefaultsWalletAddressesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsWalletAddressesStore.swift; sourceTree = "<group>"; };
8759C9EA279BD7C300FE361F /* JsonWalletAddressesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonWalletAddressesStore.swift; sourceTree = "<group>"; };
8759C9EC279E9D6100FE361F /* TextFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldView.swift; sourceTree = "<group>"; };
875B3C33250A75FA0085BD08 /* QRCodeResolutionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeResolutionCoordinator.swift; sourceTree = "<group>"; };
87620023266E00F60059B05A /* known_contract.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = known_contract.json; sourceTree = "<group>"; };
@ -3683,6 +3689,9 @@
5E7C7DD409C330DA4033F504 /* Keystore.swift */,
5E7C771FE9E98CDEBB012C28 /* EthereumSigner.swift */,
5E7C7778166E01A0D483C58D /* SecureEnclave.swift */,
8759C9E6279ADEFC00FE361F /* WalletAddressesStoreType.swift */,
8759C9E8279BD7B100FE361F /* DefaultsWalletAddressesStore.swift */,
8759C9EA279BD7C300FE361F /* JsonWalletAddressesStore.swift */,
);
path = KeyManagement;
sourceTree = "<group>";
@ -5667,6 +5676,7 @@
29F1C85820036926003780D8 /* AppTracker.swift in Sources */,
293E62711FA2F63500CB0A66 /* InitialWalletCreationCoordinator.swift in Sources */,
874527D9270B3A45008DB272 /* xDaiBridge.swift in Sources */,
8759C9E9279BD7B100FE361F /* DefaultsWalletAddressesStore.swift in Sources */,
291D73C61F7F500D00A8AB56 /* TransactionItemState.swift in Sources */,
29BE3FD21F707DC300F6BFC2 /* TransactionDataCoordinator.swift in Sources */,
29F1C85120032688003780D8 /* Address.swift in Sources */,
@ -5800,6 +5810,7 @@
879F184A26E73D14000602F2 /* TokenInstanceAttributeView.swift in Sources */,
2912CCF91F6A830700C6CBE3 /* AppDelegate.swift in Sources */,
29A0E1851F706B8C00BAAAED /* String.swift in Sources */,
8759C9EB279BD7C300FE361F /* JsonWalletAddressesStore.swift in Sources */,
296AF9A71F736EC70058AF78 /* RPCServers.swift in Sources */,
87DCCB51266F655D003E8EA0 /* WalletSummaryViewModel.swift in Sources */,
296AF9A91F737F6F0058AF78 /* SendRawTransactionRequest.swift in Sources */,
@ -6335,6 +6346,7 @@
5E7C7F53A21D1D0FE576DD8F /* ElevateWalletSecurityViewController.swift in Sources */,
5E7C743467F5D428AF4E4F0F /* ElevateWalletSecurityViewModel.swift in Sources */,
5E7C7751F57FE1B8A563A79B /* WalletSecurityLevelIndicator.swift in Sources */,
8759C9E7279ADEFC00FE361F /* WalletAddressesStoreType.swift in Sources */,
5E7C70CF44075CF73F03AD4B /* TokensViewControllerTableViewHeader.swift in Sources */,
5E7C7233A9A5F9D1A89FF569 /* TokensViewControllerTableViewSectionHeader.swift in Sources */,
87C65F532660DD2E00919819 /* WalletBalanceFetcher.swift in Sources */,

@ -1,7 +1,7 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import UIKit
import UIKit
import PromiseKit
class AppCoordinator: NSObject, Coordinator {
@ -105,6 +105,7 @@ class AppCoordinator: NSObject, Coordinator {
func start() {
if isRunningTests() {
try! RealmConfiguration.removeWalletsFolderForTests()
JsonWalletAddressesStore.removeWalletsFolderForTests()
startImpl()
} else {
DispatchQueue.main.async {
@ -154,7 +155,7 @@ class AppCoordinator: NSObject, Coordinator {
walletBalanceCoordinator.start()
oneInchSwapService.fetchSupportedTokens()
rampBuyService.fetchSupportedTokens()
if keystore.hasWallets {
showTransactions(for: keystore.currentWallet, animated: false)
} else {
@ -495,7 +496,7 @@ extension AppCoordinator: UniversalLinkCoordinatorDelegate {
}
extension AppCoordinator: AccountsCoordinatorDelegate {
private func disconnectWalletConnectSessionsSelectively(for reason: RestartReason, walletConnectCoordinator: WalletConnectCoordinator) {
switch reason {
case .changeLocalization:

@ -123,7 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
let tokenInfo = String(format: "%02.2hhx", arguments: [deviceToken[i]])
token.append(tokenInfo)
}
UserDefaults.standard.set(token, forKey: "deviceTokenForSNS")
UserDefaults.standardOrForTests.set(token, forKey: "deviceTokenForSNS")
/// Create a platform endpoint. In this case, the endpoint is a
/// device endpoint ARN
cognitoRegistration()
@ -140,7 +140,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
if task.error == nil {
let createEndpointResponse = task.result! as AWSSNSCreateEndpointResponse
if let endpointArnForSNS = createEndpointResponse.endpointArn {
UserDefaults.standard.set(endpointArnForSNS, forKey: "endpointArnForSNS")
UserDefaults.standardOrForTests.set(endpointArnForSNS, forKey: "endpointArnForSNS")
//every user should subscribe to the security topic
self.subscribeToTopicSNS(token: token, topicEndpoint: self.SNSSecurityTopicEndpoint)
// if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {

@ -25,7 +25,7 @@ class CoinTickersFetcherFileCache: NSObject, CoinTickersFetcherCacheType {
private (set) lazy var tickersJsonPath: URL = documentDirectory.appendingPathComponent("tickers.json")
private (set) lazy var historyJsonPath: URL = documentDirectory.appendingPathComponent("history.json")
private let defaults: UserDefaults = .standard
private let defaults: UserDefaults = .standardOrForTests
private enum Keys {
static let lastFetchedDateKey = "lastFetchedDateKey"

@ -8,6 +8,7 @@
import UIKit
protocol StorageType {
@discardableResult func dataExists(forKey key: String) -> Bool
@discardableResult func data(forKey: String) -> Data?
@discardableResult func setData(_ data: Data, forKey: String) -> Bool
@discardableResult func deleteEntry(forKey: String) -> Bool
@ -28,45 +29,83 @@ extension StorageType {
return result
}
func load<T: Codable>(forKey key: String) -> T? {
guard let data = data(forKey: key) else {
return nil
}
guard let result = try? JSONDecoder().decode(T.self, from: data) else {
return nil
}
return result
}
}
struct FileStorage: StorageType {
private var documentsDirectory: URL {
var fileExtension: String = "data"
private let serialQueue: DispatchQueue = DispatchQueue(label: "org.alphawallet.swift.file")
var directoryUrl: URL = {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}()
func dataExists(forKey key: String) -> Bool {
let url = fileURL(with: key, fileExtension: fileExtension)
return FileManager.default.fileExists(atPath: url.path)
}
func data(forKey key: String) -> Data? {
let url = fileURL(with: key)
let data = try? Data(contentsOf: url)
let url = fileURL(with: key, fileExtension: fileExtension)
var data: Data?
dispatchPrecondition(condition: .notOnQueue(serialQueue))
serialQueue.sync {
data = try? Data(contentsOf: url)
}
return data
}
func setData(_ data: Data, forKey key: String) -> Bool {
var url = fileURL(with: key)
do {
try data.write(to: url, options: .atomicWrite)
try url.addSkipBackupAttributeToItemAtURL()
return true
} catch {
return false
var url = fileURL(with: key, fileExtension: fileExtension)
var result: Bool = false
dispatchPrecondition(condition: .notOnQueue(serialQueue))
serialQueue.sync {
do {
try data.write(to: url, options: .atomicWrite)
try url.addSkipBackupAttributeToItemAtURL()
result = true
} catch {
result = false
}
}
return result
}
func deleteEntry(forKey key: String) -> Bool {
let url = fileURL(with: key)
do {
try FileManager.default.removeItem(at: url)
return true
} catch {
return false
let url = fileURL(with: key, fileExtension: fileExtension)
var result: Bool = false
dispatchPrecondition(condition: .notOnQueue(serialQueue))
serialQueue.sync {
do {
try FileManager.default.removeItem(at: url)
result = true
} catch {
result = false
}
}
return result
}
private func fileURL(with key: String, fileExtension: String = "data") -> URL {
return documentsDirectory.appendingPathComponent("\(key).\(fileExtension)", isDirectory: false)
return directoryUrl.appendingPathComponent("\(key).\(fileExtension)", isDirectory: false)
}
}
@ -78,4 +117,3 @@ extension URL {
try self.setResourceValues(resourceValues)
}
}

@ -28,4 +28,5 @@ enum Features {
static let isExportJsonKeystoreEnabled = true
static let is24SeedWordPhraseAllowed = true
static let isAnalyticsUIEnabled = true
static let isJsonFileBasedStorageForWalletAddressesEnabled = true
}

@ -39,7 +39,7 @@ class AppTracker {
}
init(
defaults: UserDefaults = .standard
defaults: UserDefaults = .standardOrForTests
) {
self.defaults = defaults
}

@ -4,7 +4,7 @@ import Foundation
import RealmSwift
struct RealmConfiguration {
private static let walletsFolderForTests = "testSuiteWallets"
private static let walletsFolderForTests = "testSuiteWalletsForRealm"
static func configuration(for account: Wallet, server: RPCServer) -> Realm.Configuration {
var config = realmConfiguration()
config.fileURL = defaultRealmFolderUrl.appendingPathComponent("\(account.address.eip55String.lowercased())-\(server.chainID).realm")
@ -59,3 +59,25 @@ struct RealmConfiguration {
}
}
extension FileManager {
func removeAllItems(directory: URL) {
do {
let urls = try contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
for url in urls {
try removeItem(at: url)
}
} catch {
//no-op
}
}
@discardableResult func createSubDirectoryIfNotExists(name: String, directory root: URL) throws -> URL {
let directory = root.appendingPathComponent(name)
guard !fileExists(atPath: directory.absoluteString) else { return directory }
try createDirectory(atPath: directory.path, withIntermediateDirectories: true, attributes: nil)
return directory
}
}

@ -88,7 +88,7 @@ class InCoordinator: NSObject, Coordinator {
}()
private lazy var whatsNewExperimentCoordinator: WhatsNewExperimentCoordinator = {
let coordinator = WhatsNewExperimentCoordinator(navigationController: navigationController, userDefaults: UserDefaults.standard, analyticsCoordinator: analyticsCoordinator)
let coordinator = WhatsNewExperimentCoordinator(navigationController: navigationController, userDefaults: UserDefaults.standardOrForTests, analyticsCoordinator: analyticsCoordinator)
coordinator.delegate = self
return coordinator
}()

@ -0,0 +1,90 @@
//
// DefaultsWalletAddressesStore.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 22.01.2022.
//
import Foundation
struct DefaultsWalletAddressesStore: WalletAddressesStoreType {
private struct Keys {
static let watchAddresses = "watchAddresses"
static let ethereumAddressesWithPrivateKeys = "ethereumAddressesWithPrivateKeys"
static let ethereumAddressesWithSeed = "ethereumAddressesWithSeed"
static let ethereumAddressesProtectedByUserPresence = "ethereumAddressesProtectedByUserPresence"
}
let userDefaults: UserDefaults
var hasWallets: Bool {
return !wallets.isEmpty
}
var hasMigratedFromKeystoreFiles: Bool {
return userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) != nil
}
init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
}
var wallets: [Wallet] {
let watchAddresses = self.watchAddresses.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .watch($0)) }
let addressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
let addressesWithSeed = ethereumAddressesWithSeed.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
return addressesWithSeed + addressesWithPrivateKeys + watchAddresses
}
var watchAddresses: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.watchAddresses) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
userDefaults.set(data, forKey: Keys.watchAddresses)
}
}
var ethereumAddressesWithPrivateKeys: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
userDefaults.set(data, forKey: Keys.ethereumAddressesWithPrivateKeys)
}
}
var ethereumAddressesWithSeed: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithSeed) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
userDefaults.set(data, forKey: Keys.ethereumAddressesWithSeed)
}
}
var ethereumAddressesProtectedByUserPresence: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesProtectedByUserPresence) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
userDefaults.set(data, forKey: Keys.ethereumAddressesProtectedByUserPresence)
}
}
}

@ -12,6 +12,19 @@ enum EtherKeystoreError: LocalizedError {
case protectionDisabled
}
extension UserDefaults {
//NOTE: its quite important to use single instance of user defaults, otherwise the data will be written in different suites
private static let testSuiteDefaults = UserDefaults(suiteName: NSUUID().uuidString)!
static var standardOrForTests: UserDefaults {
if isRunningTests() {
return testSuiteDefaults
} else {
return .standard
}
}
}
// swiftlint:disable type_body_length
///We use ECDSA keys (created and stored in the Secure Enclave), achieving symmetric encryption based on Diffie-Hellman to encrypt the HD wallet seed (actually entropy) and raw private keys and store the ciphertext in the keychain.
///
@ -23,10 +36,6 @@ enum EtherKeystoreError: LocalizedError {
open class EtherKeystore: NSObject, Keystore {
private struct Keys {
static let recentlyUsedAddress: String = "recentlyUsedAddress"
static let watchAddresses = "watchAddresses"
static let ethereumAddressesWithPrivateKeys = "ethereumAddressesWithPrivateKeys"
static let ethereumAddressesWithSeed = "ethereumAddressesWithSeed"
static let ethereumAddressesProtectedByUserPresence = "ethereumAddressesProtectedByUserPresence"
static let ethereumRawPrivateKeyUserPresenceNotRequiredPrefix = "ethereumRawPrivateKeyUserPresenceNotRequired-"
static let ethereumSeedUserPresenceNotRequiredPrefix = "ethereumSeedUserPresenceNotRequired-"
static let ethereumRawPrivateKeyUserPresenceRequiredPrefix = "ethereumRawPrivateKeyUserPresenceRequired-"
@ -51,59 +60,7 @@ open class EtherKeystore: NSObject, Keystore {
private let keychain: KeychainSwift
private let defaultKeychainAccessUserPresenceRequired: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly(userPresenceRequired: true)
private let defaultKeychainAccessUserPresenceNotRequired: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly(userPresenceRequired: false)
private let userDefaults: UserDefaults
private var watchAddresses: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.watchAddresses) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.watchAddresses)
}
}
private var ethereumAddressesWithPrivateKeys: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesWithPrivateKeys)
}
}
private var ethereumAddressesWithSeed: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithSeed) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesWithSeed)
}
}
private var ethereumAddressesProtectedByUserPresence: [String] {
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesProtectedByUserPresence) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesProtectedByUserPresence)
}
}
private var walletAddressesStore: WalletAddressesStoreType
private var analyticsCoordinator: AnalyticsCoordinator
@ -127,16 +84,13 @@ open class EtherKeystore: NSObject, Keystore {
}
var wallets: [Wallet] {
let watchAddresses = self.watchAddresses.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .watch($0)) }
let addressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
let addressesWithSeed = ethereumAddressesWithSeed.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
return addressesWithSeed + addressesWithPrivateKeys + watchAddresses
walletAddressesStore.wallets
}
var subscribableWallets: Subscribable<Set<Wallet>> = .init(nil)
var hasMigratedFromKeystoreFiles: Bool {
return userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) != nil
return walletAddressesStore.hasMigratedFromKeystoreFiles
}
var recentlyUsedWallet: Wallet? {
@ -171,20 +125,19 @@ open class EtherKeystore: NSObject, Keystore {
(try! EtherKeystore(analyticsCoordinator: NoOpAnalyticsService())).currentWallet
}
init(keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix), userDefaults: UserDefaults = .standard, analyticsCoordinator: AnalyticsCoordinator) throws {
init(keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix), userDefaults: UserDefaults = .standardOrForTests, analyticsCoordinator: AnalyticsCoordinator) throws {
if !UIApplication.shared.isProtectedDataAvailable {
throw EtherKeystoreError.protectionDisabled
}
self.keychain = keychain
self.keychain.synchronizable = false
self.analyticsCoordinator = analyticsCoordinator
self.userDefaults = userDefaults
self.walletAddressesStore = EtherKeystore.migratedWalletAddressesStore(userDefaults: userDefaults)
super.init()
subscribableWallets.value = Set<Wallet>(wallets)
}
// Async
func createAccount(completion: @escaping (Result<AlphaWallet.Address, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else {
@ -266,7 +219,7 @@ open class EtherKeystore: NSObject, Keystore {
guard !isAddressAlreadyInWalletsList(address: address) else {
return .failure(.duplicateAccount)
}
watchAddresses = [watchAddresses, [address.eip55String]].flatMap {
walletAddressesStore.watchAddresses = [walletAddressesStore.watchAddresses, [address.eip55String]].flatMap {
$0
}
@ -289,22 +242,22 @@ open class EtherKeystore: NSObject, Keystore {
}
private func addToListOfEthereumAddressesWithPrivateKeys(_ address: AlphaWallet.Address) {
let updatedOwnedAddresses = Array(Set(ethereumAddressesWithPrivateKeys + [address.eip55String]))
ethereumAddressesWithPrivateKeys = updatedOwnedAddresses
let updatedOwnedAddresses = Array(Set(walletAddressesStore.ethereumAddressesWithPrivateKeys + [address.eip55String]))
walletAddressesStore.ethereumAddressesWithPrivateKeys = updatedOwnedAddresses
notifyWalletUpdated()
}
private func addToListOfEthereumAddressesWithSeed(_ address: AlphaWallet.Address) {
let updated = Array(Set(ethereumAddressesWithSeed + [address.eip55String]))
ethereumAddressesWithSeed = updated
let updated = Array(Set(walletAddressesStore.ethereumAddressesWithSeed + [address.eip55String]))
walletAddressesStore.ethereumAddressesWithSeed = updated
notifyWalletUpdated()
}
private func addToListOfEthereumAddressesProtectedByUserPresence(_ address: AlphaWallet.Address) {
let updated = Array(Set(ethereumAddressesProtectedByUserPresence + [address.eip55String]))
ethereumAddressesProtectedByUserPresence = updated
let updated = Array(Set(walletAddressesStore.ethereumAddressesProtectedByUserPresence + [address.eip55String]))
walletAddressesStore.ethereumAddressesProtectedByUserPresence = updated
notifyWalletUpdated()
}
@ -476,22 +429,22 @@ open class EtherKeystore: NSObject, Keystore {
}
private func removeAccountFromBookkeeping(_ account: AlphaWallet.Address) {
ethereumAddressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.filter { $0 != account.eip55String }
ethereumAddressesWithSeed = ethereumAddressesWithSeed.filter { $0 != account.eip55String }
ethereumAddressesProtectedByUserPresence = ethereumAddressesProtectedByUserPresence.filter { $0 != account.eip55String }
watchAddresses = watchAddresses.filter { $0 != account.eip55String }
walletAddressesStore.ethereumAddressesWithPrivateKeys = walletAddressesStore.ethereumAddressesWithPrivateKeys.filter { $0 != account.eip55String }
walletAddressesStore.ethereumAddressesWithSeed = walletAddressesStore.ethereumAddressesWithSeed.filter { $0 != account.eip55String }
walletAddressesStore.ethereumAddressesProtectedByUserPresence = walletAddressesStore.ethereumAddressesProtectedByUserPresence.filter { $0 != account.eip55String }
walletAddressesStore.watchAddresses = walletAddressesStore.watchAddresses.filter { $0 != account.eip55String }
notifyWalletUpdated()
}
func isHdWallet(account: AlphaWallet.Address) -> Bool {
return ethereumAddressesWithSeed.contains(account.eip55String)
return walletAddressesStore.ethereumAddressesWithSeed.contains(account.eip55String)
}
func isHdWallet(wallet: Wallet) -> Bool {
switch wallet.type {
case .real(let account):
return ethereumAddressesWithSeed.contains(account.eip55String)
return walletAddressesStore.ethereumAddressesWithSeed.contains(account.eip55String)
case .watch:
return false
}
@ -500,7 +453,7 @@ open class EtherKeystore: NSObject, Keystore {
func isKeystore(wallet: Wallet) -> Bool {
switch wallet.type {
case .real(let account):
return ethereumAddressesWithPrivateKeys.contains(account.eip55String)
return walletAddressesStore.ethereumAddressesWithPrivateKeys.contains(account.eip55String)
case .watch:
return false
}
@ -511,12 +464,12 @@ open class EtherKeystore: NSObject, Keystore {
case .real:
return false
case .watch(let address):
return watchAddresses.contains(address.eip55String)
return walletAddressesStore.watchAddresses.contains(address.eip55String)
}
}
func isProtectedByUserPresence(account: AlphaWallet.Address) -> Bool {
return ethereumAddressesProtectedByUserPresence.contains(account.eip55String)
return walletAddressesStore.ethereumAddressesProtectedByUserPresence.contains(account.eip55String)
}
func signPersonalMessage(_ message: Data, for account: AlphaWallet.Address) -> Result<Data, KeystoreError> {

@ -0,0 +1,159 @@
//
// JsonWalletAddressesStore.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 22.01.2022.
//
import Foundation
struct JsonWalletAddressesStore: WalletAddressesStoreType {
private static let walletsFolderForTests = "testSuiteWalletsForWalletAddresses"
static func createStorage() -> StorageType {
let directoryUrl: URL = {
if isRunningTests() {
let cacheDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let directory = try! FileManager.default.createSubDirectoryIfNotExists(name: walletsFolderForTests, directory: cacheDirectoryUrl)
return directory
} else {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}()
return FileStorage(fileExtension: "json", directoryUrl: directoryUrl)
}
static func removeWalletsFolderForTests() {
guard isRunningTests() else { return }
let cacheDirectoryUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let directory = cacheDirectoryUrl.appendingPathComponent(walletsFolderForTests)
//NOTE: we want to clear all elready created wallets in cache directory while performing tests
FileManager.default.removeAllItems(directory: directory)
}
private struct Keys {
static let walletAddresses = "walletAddresses"
}
private var storage: StorageType
private var walletAddresses: WalletAddresses
init(storage: StorageType = JsonWalletAddressesStore.createStorage()) {
self.storage = storage
if let value: WalletAddresses = storage.load(forKey: Keys.walletAddresses) {
walletAddresses = value
} else {
walletAddresses = WalletAddresses()
}
}
var hasAnyStoredData: Bool {
return storage.dataExists(forKey: Keys.walletAddresses)
}
var hasWallets: Bool {
return !wallets.isEmpty
}
var hasMigratedFromKeystoreFiles: Bool {
walletAddresses.ethereumAddressesWithPrivateKeys != nil
}
var wallets: [Wallet] {
let watchAddresses = self.watchAddresses.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .watch($0)) }
let addressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
let addressesWithSeed = ethereumAddressesWithSeed.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real($0)) }
return addressesWithSeed + addressesWithPrivateKeys + watchAddresses
}
var watchAddresses: [String] {
get {
walletAddresses.watchAddresses ?? []
}
set {
walletAddresses.watchAddresses = newValue
saveWalletCollectionToFile()
}
}
var ethereumAddressesWithPrivateKeys: [String] {
get {
walletAddresses.ethereumAddressesWithPrivateKeys ?? []
}
set {
walletAddresses.ethereumAddressesWithPrivateKeys = newValue
saveWalletCollectionToFile()
}
}
var ethereumAddressesWithSeed: [String] {
get {
walletAddresses.ethereumAddressesWithSeed ?? []
}
set {
walletAddresses.ethereumAddressesWithSeed = newValue
saveWalletCollectionToFile()
}
}
var ethereumAddressesProtectedByUserPresence: [String] {
get {
walletAddresses.ethereumAddressesProtectedByUserPresence ?? []
}
set {
walletAddresses.ethereumAddressesProtectedByUserPresence = newValue
saveWalletCollectionToFile()
}
}
private func saveWalletCollectionToFile() {
guard let data = try? JSONEncoder().encode(walletAddresses) else {
return
}
storage.setData(data, forKey: Keys.walletAddresses)
}
}
extension EtherKeystore {
private static let rawJsonWalletStore = JsonWalletAddressesStore.createStorage()
static func migratedWalletAddressesStore(userDefaults: UserDefaults) -> WalletAddressesStoreType {
if Features.isJsonFileBasedStorageForWalletAddressesEnabled {
//NOTE: its quite important to remove test wallets right before fetching, otherwise tests will fails, especially Keystore related
JsonWalletAddressesStore.removeWalletsFolderForTests()
let jsonWalletAddressesStore = JsonWalletAddressesStore(storage: rawJsonWalletStore)
if !jsonWalletAddressesStore.hasAnyStoredData {
let userDefaultsWalletAddressesStore = DefaultsWalletAddressesStore(userDefaults: userDefaults)
if jsonWalletAddressesStore.hasWallets && !userDefaultsWalletAddressesStore.hasWallets {
return jsonWalletAddressesStore
} else {
return userDefaultsWalletAddressesStore.migrate(to: jsonWalletAddressesStore)
}
} else {
return jsonWalletAddressesStore
}
} else {
return DefaultsWalletAddressesStore(userDefaults: userDefaults)
}
}
}
private struct WalletAddresses: Codable {
var watchAddresses: [String]?
var ethereumAddressesWithPrivateKeys: [String]?
var ethereumAddressesWithSeed: [String]?
var ethereumAddressesProtectedByUserPresence: [String]?
init() {
watchAddresses = []
ethereumAddressesWithPrivateKeys = []
ethereumAddressesWithSeed = []
ethereumAddressesProtectedByUserPresence = []
}
}

@ -0,0 +1,35 @@
//
// WalletStore.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 21.01.2022.
//
import Foundation
protocol WalletAddressesStoreMigrationType {
func migrate(to store: WalletAddressesStoreType) -> WalletAddressesStoreType
}
protocol WalletAddressesStoreType: WalletAddressesStoreMigrationType {
var watchAddresses: [String] { get set }
var ethereumAddressesWithPrivateKeys: [String] { get set }
var ethereumAddressesWithSeed: [String] { get set }
var ethereumAddressesProtectedByUserPresence: [String] { get set }
var hasWallets: Bool { get }
var wallets: [Wallet] { get }
var hasMigratedFromKeystoreFiles: Bool { get }
}
extension WalletAddressesStoreType {
func migrate(to store: WalletAddressesStoreType) -> WalletAddressesStoreType {
var store = store
store.watchAddresses = watchAddresses
store.ethereumAddressesWithPrivateKeys = ethereumAddressesWithPrivateKeys
store.ethereumAddressesWithSeed = ethereumAddressesWithSeed
store.ethereumAddressesProtectedByUserPresence = ethereumAddressesProtectedByUserPresence
return store
}
}

@ -15,7 +15,7 @@ struct Config {
//TODO `currency` was originally a instance-side property, but was refactored out. Maybe better if it it's moved elsewhere
static func getCurrency() -> Currency {
let defaults = UserDefaults.standard
let defaults = UserDefaults.standardOrForTests
//If it is saved currency
if let currency = defaults.string(forKey: Keys.currencyID) {
@ -33,13 +33,13 @@ struct Config {
}
static func setCurrency(_ currency: Currency) {
let defaults = UserDefaults.standard
let defaults = UserDefaults.standardOrForTests
defaults.set(currency.rawValue, forKey: Keys.currencyID)
}
//TODO `locale` was originally a instance-side property, but was refactored out. Maybe better if it it's moved elsewhere
static func getLocale() -> String? {
let defaults = UserDefaults.standard
let defaults = UserDefaults.standardOrForTests
return defaults.string(forKey: Keys.locale)
}
@ -61,7 +61,7 @@ struct Config {
}
static func setLocale(_ locale: String?) {
let defaults = UserDefaults.standard
let defaults = UserDefaults.standardOrForTests
let preferenceKeyForOverridingInAppLanguage = "AppleLanguages"
if let locale = locale {
defaults.set(locale, forKey: Keys.locale)
@ -75,12 +75,12 @@ struct Config {
}
//TODO Only Dapp browser uses this. Shall we move it?
static func setChainId(_ chainId: Int, defaults: UserDefaults = UserDefaults.standard) {
static func setChainId(_ chainId: Int, defaults: UserDefaults = UserDefaults.standardOrForTests) {
defaults.set(chainId, forKey: Keys.chainID)
}
//TODO Only Dapp browser uses this
static func getChainId(defaults: UserDefaults = UserDefaults.standard) -> Int {
static func getChainId(defaults: UserDefaults = UserDefaults.standardOrForTests) -> Int {
let id = defaults.integer(forKey: Keys.chainID)
guard id > 0 else { return RPCServer.main.chainID }
return id
@ -108,40 +108,40 @@ struct Config {
defaults.set(dictionary, forKey: generateLastFetchedErc20InteractionBlockNumberKey(wallet))
}
static func getLastFetchedErc20InteractionBlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) -> Int? {
static func getLastFetchedErc20InteractionBlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) -> Int? {
guard let dictionary = defaults.value(forKey: generateLastFetchedErc20InteractionBlockNumberKey(wallet)) as? [String: NSNumber] else { return nil }
return dictionary["\(server.chainID)"]?.intValue
}
static func setLastFetchedErc721InteractionBlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) {
static func setLastFetchedErc721InteractionBlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) {
var dictionary: [String: NSNumber] = (defaults.value(forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet)) as? [String: NSNumber]) ?? .init()
dictionary["\(server.chainID)"] = NSNumber(value: blockNumber)
defaults.set(dictionary, forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet))
}
static func getLastFetchedErc721InteractionBlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) -> Int? {
static func getLastFetchedErc721InteractionBlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) -> Int? {
guard let dictionary = defaults.value(forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet)) as? [String: NSNumber] else { return nil }
return dictionary["\(server.chainID)"]?.intValue
}
static func setLastFetchedAutoDetectedTransactedTokenErc20BlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) {
static func setLastFetchedAutoDetectedTransactedTokenErc20BlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) {
var dictionary: [String: NSNumber] = (defaults.value(forKey: generateLastFetchedAutoDetectedTransactedTokenErc20BlockNumberKey(wallet)) as? [String: NSNumber]) ?? .init()
dictionary["\(server.chainID)"] = NSNumber(value: blockNumber)
defaults.set(dictionary, forKey: generateLastFetchedAutoDetectedTransactedTokenErc20BlockNumberKey(wallet))
}
static func getLastFetchedAutoDetectedTransactedTokenErc20BlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) -> Int? {
static func getLastFetchedAutoDetectedTransactedTokenErc20BlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) -> Int? {
guard let dictionary = defaults.value(forKey: generateLastFetchedAutoDetectedTransactedTokenErc20BlockNumberKey(wallet)) as? [String: NSNumber] else { return nil }
return dictionary["\(server.chainID)"]?.intValue
}
static func setLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) {
static func setLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) {
var dictionary: [String: NSNumber] = (defaults.value(forKey: generateLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumberKey(wallet)) as? [String: NSNumber]) ?? .init()
dictionary["\(server.chainID)"] = NSNumber(value: blockNumber)
defaults.set(dictionary, forKey: generateLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumberKey(wallet))
}
static func getLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) -> Int? {
static func getLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standardOrForTests) -> Int? {
guard let dictionary = defaults.value(forKey: generateLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumberKey(wallet)) as? [String: NSNumber] else { return nil }
return dictionary["\(server.chainID)"]?.intValue
}
@ -259,7 +259,7 @@ struct Config {
var subscribableEnabledServers: Subscribable<[RPCServer]>
init(defaults: UserDefaults = UserDefaults.standard) {
init(defaults: UserDefaults = UserDefaults.standardOrForTests) {
self.defaults = defaults
subscribableEnabledServers = .init(nil)
}

@ -77,9 +77,9 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
})
}
typealias EnabledTokenAddreses = [(contract: AlphaWallet.Address, tokenType: TokenType, server: RPCServer)]
private func tokensForEnabledRPCServers() -> Promise<EnabledTokenAddreses> {
tokenCollection.tokenObjects.map { tokenObjects -> EnabledTokenAddreses in
typealias EnabledTokenAddresses = [(contract: AlphaWallet.Address, tokenType: TokenType, server: RPCServer)]
private func tokensForEnabledRPCServers() -> Promise<EnabledTokenAddresses> {
tokenCollection.tokenObjects.map { tokenObjects -> EnabledTokenAddresses in
tokenObjects.compactMap { (contract: $0.contractAddress, tokenType: $0.type, server: $0.server) }
}
}

@ -27,7 +27,7 @@ class TransactionsTracker {
init(
sessionID: String,
defaults: UserDefaults = .standard
defaults: UserDefaults = .standardOrForTests
) {
self.sessionID = sessionID
self.defaults = defaults

@ -272,7 +272,7 @@ class ImportWalletViewController: UIViewController {
navigationItem.rightBarButtonItem = UIBarButtonItem.qrCodeBarButton(self, selector: #selector(openReader))
if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") {
if UserDefaults.standardOrForTests.bool(forKey: "FASTLANE_SNAPSHOT") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.demo()

@ -94,7 +94,7 @@ class SendViewControllerTests: XCTestCase {
XCTAssertEqual(vc.amountTextField.value, "")
vc.allFundsSelected()
XCTAssertEqual(vc.amountTextField.value, "0,002")
XCTAssertNotNil(vc.shortValueForAllFunds)
XCTAssertTrue((vc.shortValueForAllFunds ?? "").nonEmpty)

Loading…
Cancel
Save