// // WrappedSDKSynchronizer.swift // secant-testnet // // Created by Lukáš Korba on 13.04.2022. // import Foundation import ZcashLightClientKit import Combine import ComposableArchitecture enum WrappedSDKSynchronizerState: Equatable { case unknown case transactionsUpdated case started case progressUpdated case statusWillUpdate case synced case stopped case disconnected case syncing case downloading case validating case scanning case enhancing case fetching case minedTransaction case foundTransactions case failed case connectionStateChanged } struct Balance: WalletBalance, Equatable { var verified: Int64 var total: Int64 } protocol WrappedSDKSynchronizer { var synchronizer: SDKSynchronizer? { get } var stateChanged: CurrentValueSubject { get } var notificationCenter: WrappedNotificationCenter { get } func prepareWith(initializer: Initializer) throws func start(retry: Bool) throws func stop() func status() -> String func getShieldedBalance() -> Effect func getAllClearedTransactions() -> Effect<[TransactionState], Never> func getAllPendingTransactions() -> Effect<[TransactionState], Never> func getAllTransactions() -> Effect<[TransactionState], Never> func getTransparentAddress(account: Int) -> TransparentAddress? func getShieldedAddress(account: Int) -> SaplingShieldedAddress? func sendTransaction( with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?, from account: Int ) -> Effect, Never> } extension WrappedSDKSynchronizer { func start() throws { try start(retry: false) } func getTransparentAddress() -> TransparentAddress? { getTransparentAddress(account: 0) } func getShieldedAddress() -> SaplingShieldedAddress? { getShieldedAddress(account: 0) } } class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { private var cancellables: [AnyCancellable] = [] private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var notificationCenter: WrappedNotificationCenter init(notificationCenter: WrappedNotificationCenter = .live) { self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } deinit { synchronizer?.stop() } func prepareWith(initializer: Initializer) throws { synchronizer = try SDKSynchronizer(initializer: initializer) notificationCenter.publisherFor(.synchronizerStarted)? .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.synchronizerStarted() } .store(in: &cancellables) notificationCenter.publisherFor(.synchronizerSynced)? .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.synchronizerSynced() } .store(in: &cancellables) notificationCenter.publisherFor(.synchronizerProgressUpdated)? .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.synchronizerProgressUpdated() } .store(in: &cancellables) notificationCenter.publisherFor(.synchronizerStopped)? .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.synchronizerStopped() } .store(in: &cancellables) try synchronizer?.prepare() } func start(retry: Bool) throws { try synchronizer?.start(retry: retry) } func stop() { synchronizer?.stop() } func synchronizerStarted() { stateChanged.send(.started) } func synchronizerSynced() { stateChanged.send(.synced) } func synchronizerProgressUpdated() { stateChanged.send(.progressUpdated) } func synchronizerStopped() { stateChanged.send(.stopped) } func status() -> String { guard let synchronizer = synchronizer else { return "" } return SDKSynchronizer.textFor(state: synchronizer.status) } func getShieldedBalance() -> Effect { if let shieldedVerifiedBalance = synchronizer?.getShieldedVerifiedBalance(), let shieldedTotalBalance = synchronizer?.getShieldedBalance(accountIndex: 0) { return Effect(value: Balance(verified: shieldedVerifiedBalance, total: shieldedTotalBalance)) } return .none } func getAllClearedTransactions() -> Effect<[TransactionState], Never> { if let clearedTransactions = try? synchronizer?.allClearedTransactions() { return Effect(value: clearedTransactions.map { TransactionState.init(confirmedTransaction: $0, sent: ($0.toAddress != nil)) }) } return .none } func getAllPendingTransactions() -> Effect<[TransactionState], Never> { if let pendingTransactions = try? synchronizer?.allPendingTransactions(), let syncedBlockHeight = synchronizer?.latestScannedHeight { return Effect(value: pendingTransactions.map { // TODO: - can we initialize it with latestBlockHeight: = nil? TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight) }) } return .none } func getAllTransactions() -> Effect<[TransactionState], Never> { if let pendingTransactions = try? synchronizer?.allPendingTransactions(), let clearedTransactions = try? synchronizer?.allClearedTransactions(), let syncedBlockHeight = synchronizer?.latestScannedHeight { let clearedTxs = clearedTransactions.map { TransactionState.init(confirmedTransaction: $0, sent: ($0.toAddress != nil)) } let pendingTxs = pendingTransactions.map { TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight) } let txs = clearedTxs.filter { cleared in pendingTxs.first { pending in pending.id == cleared.id } == nil } return .merge( Effect(value: txs), Effect(value: pendingTxs) ) .flatMap(Publishers.Sequence.init(sequence:)) .collect() .eraseToEffect() } return .none } func getTransparentAddress(account: Int) -> TransparentAddress? { synchronizer?.getTransparentAddress(accountIndex: account) } func getShieldedAddress(account: Int) -> SaplingShieldedAddress? { synchronizer?.getShieldedAddress(accountIndex: account) } func sendTransaction( with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?, from account: Int ) -> Effect, Never> { Deferred { Future { [weak self] promise in self?.synchronizer?.sendToAddress( spendingKey: spendingKey, zatoshi: zatoshi, toAddress: recipientAddress, memo: memo, from: account) { result in switch result { case .failure(let error as NSError): promise(.failure(error)) case .success(let pendingTx): promise(.success(TransactionState(pendingTransaction: pendingTx))) } } } } .mapError { $0 as NSError } .catchToEffect() } } class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { private var cancellables: [AnyCancellable] = [] private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var notificationCenter: WrappedNotificationCenter init(notificationCenter: WrappedNotificationCenter = .mock) { self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } deinit { synchronizer?.stop() } func prepareWith(initializer: Initializer) throws { synchronizer = try SDKSynchronizer(initializer: initializer) NotificationCenter.default.publisher(for: .synchronizerSynced) .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] _ in self?.synchronizerSynced() }) .store(in: &cancellables) try synchronizer?.prepare() } func start(retry: Bool) throws { try synchronizer?.start(retry: retry) } func stop() { synchronizer?.stop() } func synchronizerSynced() { stateChanged.send(.synced) } func status() -> String { guard let synchronizer = synchronizer else { return "" } return SDKSynchronizer.textFor(state: synchronizer.status) } func getShieldedBalance() -> Effect { return Effect(value: Balance(verified: 12345000, total: 12345000)) } func getAllClearedTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: 1, status: .paid(success: false)), TransactionStateMockHelper(date: 1651039101, amount: 2), TransactionStateMockHelper(date: 1651039000, amount: 3, status: .paid(success: true)), TransactionStateMockHelper(date: 1651039505, amount: 4), TransactionStateMockHelper(date: 1651039404, amount: 5) ] return Effect( value: mocked.map { TransactionState.placeholder( date: Date.init(timeIntervalSince1970: $0.date), amount: $0.amount, shielded: $0.shielded, status: $0.status, subtitle: $0.subtitle ) } ) } func getAllPendingTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039606, amount: 6, status: .paid(success: false), subtitle: "pending"), TransactionStateMockHelper(date: 1651039303, amount: 7, subtitle: "pending"), TransactionStateMockHelper(date: 1651039707, amount: 8, status: .paid(success: true), subtitle: "pending"), TransactionStateMockHelper(date: 1651039808, amount: 9, subtitle: "pending") ] return Effect( value: mocked.map { TransactionState.placeholder( date: Date.init(timeIntervalSince1970: $0.date), amount: $0.amount, shielded: $0.shielded, status: $0.status, subtitle: $0.subtitle ) } ) } func getAllTransactions() -> Effect<[TransactionState], Never> { return .merge( getAllClearedTransactions(), getAllPendingTransactions() ) .flatMap(Publishers.Sequence.init(sequence:)) .collect() .eraseToEffect() } func getTransparentAddress(account: Int) -> TransparentAddress? { nil } func getShieldedAddress(account: Int) -> SaplingShieldedAddress? { nil } func sendTransaction( with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?, from account: Int ) -> Effect, Never> { let transactionState = TransactionState( expirationHeight: 40, memo: "test", minedHeight: 50, shielded: true, zAddress: "tteafadlamnelkqe", date: Date.init(timeIntervalSince1970: 1234567), id: "id", status: .paid(success: true), subtitle: "sub", zecAmount: 10 ) return Effect(value: Result.success(transactionState)) } } class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var notificationCenter: WrappedNotificationCenter init(notificationCenter: WrappedNotificationCenter = .mock) { self.notificationCenter = notificationCenter self.stateChanged = CurrentValueSubject(.unknown) } func prepareWith(initializer: Initializer) throws { } func start(retry: Bool) throws { } func stop() { } func synchronizerSynced() { } func status() -> String { "" } func getShieldedBalance() -> Effect { return .none } func getAllClearedTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: 1, status: .paid(success: false), uuid: "aa11"), TransactionStateMockHelper(date: 1651039101, amount: 2, uuid: "bb22"), TransactionStateMockHelper(date: 1651039000, amount: 3, status: .paid(success: true), uuid: "cc33"), TransactionStateMockHelper(date: 1651039505, amount: 4, uuid: "dd44"), TransactionStateMockHelper(date: 1651039404, amount: 5, uuid: "ee55") ] return Effect( value: mocked.map { TransactionState.placeholder( date: Date.init(timeIntervalSince1970: $0.date), amount: $0.amount, shielded: $0.shielded, status: $0.status, subtitle: $0.subtitle, uuid: $0.uuid ) } ) } func getAllPendingTransactions() -> Effect<[TransactionState], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039606, amount: 6, status: .paid(success: false), subtitle: "pending", uuid: "ff66"), TransactionStateMockHelper(date: 1651039303, amount: 7, subtitle: "pending", uuid: "gg77"), TransactionStateMockHelper(date: 1651039707, amount: 8, status: .paid(success: true), subtitle: "pending", uuid: "hh88"), TransactionStateMockHelper(date: 1651039808, amount: 9, subtitle: "pending", uuid: "ii99") ] return Effect( value: mocked.map { TransactionState.placeholder( date: Date.init(timeIntervalSince1970: $0.date), amount: $0.amount, shielded: $0.shielded, status: $0.status, subtitle: $0.subtitle, uuid: $0.uuid ) } ) } func getAllTransactions() -> Effect<[TransactionState], Never> { return .merge( getAllClearedTransactions(), getAllPendingTransactions() ) .flatMap(Publishers.Sequence.init(sequence:)) .collect() .eraseToEffect() } func getTransparentAddress(account: Int) -> TransparentAddress? { nil } func getShieldedAddress(account: Int) -> SaplingShieldedAddress? { nil } func sendTransaction( with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?, from account: Int ) -> Effect, Never> { return Effect(value: Result.failure(SynchronizerError.criticalError as NSError)) } }