Skip to content

Commit

Permalink
Add backup wallets to icloud in settings
Browse files Browse the repository at this point in the history
  • Loading branch information
ant013 committed May 29, 2023
1 parent 3af15a3 commit 301deba
Show file tree
Hide file tree
Showing 26 changed files with 658 additions and 103 deletions.
6 changes: 6 additions & 0 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1848,6 +1848,7 @@
ABC9A36297D869E49C152CAB /* SwapRevokeConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1360FE305343B1049CF /* SwapRevokeConfirmationViewController.swift */; };
ABC9A395A96C1F7C30F21940 /* WalletConnectV2PendingRequestsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD3F677671FB57CCD886 /* WalletConnectV2PendingRequestsService.swift */; };
ABC9A3B155B3F6E7E0F2CB07 /* HudHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A381CB4C09FF7CB62A94 /* HudHelper.swift */; };
ABC9A3BC9A18F74818EF5C17 /* MetadataMonitorNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2491ADC4E5E089CD42 /* MetadataMonitorNew.swift */; };
ABC9A3C3FC55AB33A9896382 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB69D8053840476C26FA /* UIViewController.swift */; };
ABC9A3CC73251E7F83A94181 /* UniswapV3TradeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF418357FF7AFC64B3F /* UniswapV3TradeService.swift */; };
ABC9A3D46AA7356763213BA6 /* FeePriceScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF8E8DE67732371A00E0 /* FeePriceScale.swift */; };
Expand Down Expand Up @@ -2085,6 +2086,7 @@
ABC9AEF0A2ECD0E627AF065B /* ECashAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A4C563432A34A634B82A /* ECashAdapter.swift */; };
ABC9AEF62C857F322FFA87E4 /* ContactBookAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC8CCF3B57FDFC817356 /* ContactBookAddressService.swift */; };
ABC9AF1729BA19223BB39E06 /* SendEip721ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7C3BC5FC664BBF14C4F /* SendEip721ViewModel.swift */; };
ABC9AF371FBB4BEA654A78B6 /* MetadataMonitorNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2491ADC4E5E089CD42 /* MetadataMonitorNew.swift */; };
ABC9AF4D82ACDFBFBDC2D23C /* MarketCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A37521CD6E2CC5BA4E68 /* MarketCardView.swift */; };
ABC9AF5B0B1D5FE002288AE1 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB0A37663BC3F17C7A81 /* FileStorage.swift */; };
ABC9AF64794A2AFC6106DCF3 /* WalletConnectV1ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7809A4A33BEBCFA3194 /* WalletConnectV1ListViewModel.swift */; };
Expand Down Expand Up @@ -3385,6 +3387,7 @@
ABC9A99184EE1D5D052C52E9 /* ContactBookSyncSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookSyncSettingsViewController.swift; sourceTree = "<group>"; };
ABC9A9C09ECB9B0CCBAD8C21 /* SendEip1155ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip1155ViewController.swift; sourceTree = "<group>"; };
ABC9A9F6635146BEBFB432D1 /* ChartCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartCell.swift; sourceTree = "<group>"; };
ABC9AA2491ADC4E5E089CD42 /* MetadataMonitorNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataMonitorNew.swift; sourceTree = "<group>"; };
ABC9AA2DC16F947607B1794E /* WalletConnectV1ListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV1ListView.swift; sourceTree = "<group>"; };
ABC9AA31438063F7AB7BDDC8 /* WalletConnectV2RequestMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2RequestMapper.swift; sourceTree = "<group>"; };
ABC9AA3B8927F9F138ABCFB8 /* WalletConnectV2AppShowService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2AppShowService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4154,6 +4157,7 @@
ABC9AB8F4032F87355FD3693 /* MetadataMonitor.swift */,
11B35ABF8159065957CD3EF8 /* SubscriptionManager.swift */,
11B35EE072CE5471B0DFF841 /* TestNetManager.swift */,
ABC9AA2491ADC4E5E089CD42 /* MetadataMonitorNew.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -8148,6 +8152,7 @@
ABC9A8AE39B8925B28B97F77 /* WalletBackupConverter.swift in Sources */,
ABC9A321C32D5F8BDC9CC224 /* WalletBackupManager.swift in Sources */,
ABC9A99724D817AF0E6C5EC3 /* FileStorage.swift in Sources */,
ABC9A3BC9A18F74818EF5C17 /* MetadataMonitorNew.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -9287,6 +9292,7 @@
ABC9A7EACB2FA65355C2BA4E /* WalletBackupConverter.swift in Sources */,
ABC9ADFB9D0CD84EDF0F873D /* WalletBackupManager.swift in Sources */,
ABC9AF5B0B1D5FE002288AE1 /* FileStorage.swift in Sources */,
ABC9AF371FBB4BEA654A78B6 /* MetadataMonitorNew.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Edit@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Edit@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion UnstoppableWallet/UnstoppableWallet/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class App {

let appManager: AppManager
let contactManager: ContactBookManager?
let cloudAccountBackupManager: CloudAccountBackupManager

init() throws {
appConfigProvider = AppConfigProvider()
Expand Down Expand Up @@ -154,7 +155,9 @@ class App {
accountManager = AccountManager(storage: accountCachedStorage)
accountRestoreWarningManager = AccountRestoreWarningManager(accountManager: accountManager, localStorage: StorageKit.LocalStorage.default)
accountFactory = AccountFactory(accountManager: accountManager)
backupManager = BackupManager(accountManager: accountManager)

cloudAccountBackupManager = CloudAccountBackupManager(ubiquityContainerIdentifier: CloudAccountBackupManager.iCloudSharedContainer, logger: Logger(minLogLevel: .error))
backupManager = BackupManager(accountManager: accountManager, cloudBackupManager: cloudAccountBackupManager)

kitCleaner = KitCleaner(accountManager: accountManager)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ class WalletBackup: Codable {
let id: String
let type: AccountType.Abstract
let version: Int
let timestamp: TimeInterval?

enum CodingKeys: String, CodingKey {
case crypto
case id
case type
case version
case timestamp
}

init(crypto: WalletBackupCrypto, id: String, type: AccountType.Abstract, version: Int) {
init(crypto: WalletBackupCrypto, id: String, type: AccountType.Abstract, version: Int, timestamp: TimeInterval) {
self.crypto = crypto
self.id = id
self.type = type
self.version = version
self.timestamp = timestamp.rounded()
}

required init(from decoder: Decoder) throws {
Expand All @@ -26,6 +29,7 @@ class WalletBackup: Codable {
id = try container.decode(String.self, forKey: .id)
type = try container.decode(AccountType.Abstract.self, forKey: .type)
version = try container.decode(Int.self, forKey: .version)
timestamp = try? container.decode(TimeInterval.self, forKey: .timestamp)
}

func encode(to encoder: Encoder) throws {
Expand All @@ -34,6 +38,7 @@ class WalletBackup: Codable {
try container.encode(id, forKey: .id)
try container.encode(type, forKey: .type)
try container.encode(version, forKey: .version)
try container.encode(timestamp, forKey: .timestamp)
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
import RxSwift
import RxRelay
import Combine
import HsExtensions

class BackupManager {
private let accountManager: AccountManager
private let cloudBackupManager: CloudAccountBackupManager

init(accountManager: AccountManager) {
private let disposeBag = DisposeBag()
private var cancellables = Set<AnyCancellable>()

private let allBackedUpRelay = PublishRelay<Bool>()

@PostPublished var items: [String: WalletBackup]

init(accountManager: AccountManager, cloudBackupManager: CloudAccountBackupManager) {
self.accountManager = accountManager
self.cloudBackupManager = cloudBackupManager

items = cloudBackupManager.items

subscribe(disposeBag, accountManager.accountsObservable) { [weak self] _ in self?.updateAllBackedUp() }
cloudBackupManager.$items
.sink { [weak self] _ in
self?.updateAllBackedUp()
}
.store(in: &cancellables)

}

private func updateAllBackedUp() {
print("Check UpdateAll BackedUp: \(allBackedUp))")
allBackedUpRelay.accept(allBackedUp)
}

}

extension BackupManager {

var allBackedUp: Bool {
accountManager.accounts.allSatisfy { $0.backedUp }
accountManager.accounts.allSatisfy { $0.backedUp || cloudBackupManager.backedUp(uniqueId: $0.type.uniqueId()) }
}

var allBackedUpObservable: Observable<Bool> {
accountManager.accountsObservable.map { $0.allSatisfy { $0.backedUp } }
allBackedUpRelay.asObservable()
}

func setAccountBackedUp(id: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import Foundation
import Combine
import HsToolKit
import HsExtensions

class CloudAccountBackupManager {
static private let batchingInterval: TimeInterval = 1
static private let fileExtension = ".json"
static let iCloudSharedContainer = "iCloud.io.horizontalsystems.bank-wallet.shared.dev"

private let ubiquityContainerIdentifier: String
private let fileStorage: FileStorage
private let logger: Logger?

private var metadataMonitor: MetadataMonitorNew?
private var publishers = [AnyCancellable]()

var iCloudUrl: URL? {
FileManager
Expand All @@ -12,22 +22,111 @@ class CloudAccountBackupManager {
.appendingPathComponent("Documents")
}

@PostPublished private(set) var items = [String: WalletBackup]()
@PostPublished private(set) var state = State.loading

init(ubiquityContainerIdentifier: String, logger: Logger?) {
self.ubiquityContainerIdentifier = ubiquityContainerIdentifier

fileStorage = FileStorage(logger: logger)
self.logger = logger

initializeMetadataMonitor()

reload()
}

private func initializeMetadataMonitor() {
// create monitor and handle its events
guard let url = iCloudUrl else {
logger?.log(level: .debug, message: "CloudAccountManager.initializeMetadataMonitor, url not available.")
state = .error(BackupError.urlNotAvailable)
logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)")
return
}

let metadataMonitor = MetadataMonitorNew(url: url, batchingInterval: Self.batchingInterval, logger: logger)
self.metadataMonitor = metadataMonitor
logger?.debug("=C-MANAGER> Turn ON monitor")

metadataMonitor.needUpdatePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.reload()
}.store(in: &publishers)
}

private func reload() {
state = .loading
logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)")

guard let url = iCloudUrl else {
logger?.log(level: .debug, message: "CloudAccountManager.forceDownloadContainerFiles, url not available.")
state = .error(BackupError.urlNotAvailable)
logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)")
return
}

Task {
do {
forceDownloadContainerFiles(url: url)
let items = try await downloadItems(url: url)

state = .success
logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)")
self.items = items
} catch {
state = .error(error)
logger?.log(level: .debug, message: "CloudAccountManager.state = \(state)")
}
}
}

private func forceDownloadContainerFiles(url: URL) {
// try to download new files from cloud to local cloud storage
// ignore any errors
do {
let files = try fileStorage.fileList(url: url)
files.forEach { file in
do {
try fileStorage.prepareUbiquitousItem(url: url, filename: file)
} catch {
logger?.log(level: .debug, message: "CloudAccountManager.forceDownloadContainerFiles, can't prepareUbiquitousItem \(existFilenames)")
}
}
} catch {
logger?.log(level: .debug, message: "CloudAccountManager.forceDownloadContainerFiles, error: \(error)")
}
}

private func downloadItems(url: URL) async throws -> [String: WalletBackup] {
let files = try fileStorage.fileList(url: url).filter { s in s.contains(Self.fileExtension) }
var items = [String: WalletBackup]()

for file in files {
do {
let data = try await fileStorage.read(directoryUrl: url, filename: file)
let backup = try JSONDecoder().decode(WalletBackup.self, from: data)
items[file] = backup
} catch {
logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't read \(file). Because: \(error)")
}
}

logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, read \(items.count) files")
return items
}

}

extension CloudAccountBackupManager {

func backedUp(accountId: Data) async throws -> Bool {
false
func backedUp(uniqueId: Data) -> Bool {
items.contains { _, backup in backup.id == uniqueId.hs.hex }
}

var existFilenames: [String] {
[]
items.map { ($0.key as NSString).deletingPathExtension }
}

}
Expand All @@ -45,29 +144,55 @@ extension CloudAccountBackupManager {
throw BackupError.urlNotAvailable
}

print("icloudUrl: \(iCloudUrl.path)")

do {
let name = name + Self.fileExtension
let encoded = try WalletBackupConverter.encode(accountType: accountType, passphrase: passphrase)
// let json = encoded.hs.to(type: String.self)

try await fileStorage.write(directoryUrl: iCloudUrl, filename: name, data: encoded)
logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, save \(name)")
} catch {
print("ERROR: \(error)")
logger?.log(level: .debug, message: "CloudAccountManager.downloadItems, can't save \(name). Because: \(error)")
throw error
}

}

func delete(uid: Data) async throws -> AccountType {
fatalError("delete(uid:) has not been implemented")
func delete(uniqueId: Data) async throws {
guard let iCloudUrl else {
throw BackupError.urlNotAvailable
}

guard let item = items.first(where: { name, backup in backup.id == uniqueId.hs.hex }) else {
throw BackupError.itemNotFound
}

let fileUrl = iCloudUrl.appendingPathComponent(item.key)
do {
try await fileStorage.deleteFile(url: fileUrl)

// system will automatically updates items but after 1-2 seconds. So we need force update
items[item.key] = nil

logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) successful")
} catch {
logger?.log(level: .debug, message: "CloudAccountManager.delete \(item.key) unsuccessful because: \(error)")
throw error
}
}

}

extension CloudAccountBackupManager {

enum BackupError: Error {
case urlNotAvailable
}
enum BackupError: Error {
case urlNotAvailable
case itemNotFound
}

enum State {
case loading
case success
case error(Error)
}

}
Loading

0 comments on commit 301deba

Please sign in to comment.