diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 7ae657a..92a6190 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -2617,7 +2617,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit/"; requirement = { kind = upToNextMajorVersion; - minimumVersion = "0.17.0-beta"; + minimumVersion = "0.18.0-beta"; }; }; 6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ef5f7e1..844281b 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "4d543d811ee644fa4cc4bfa0be996b4dd6ba0f54", - "version" : "0.13.3" + "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", + "version" : "0.14.1" } }, { @@ -203,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi.git", "state" : { - "revision" : "fad9802b907822d5a1877584c91f3786824521b7", - "version" : "0.1.0" + "revision" : "b6013b8b313523b2c72ce62dbdc17f6ffad3a500", + "version" : "0.1.1" } }, { @@ -212,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash/ZcashLightClientKit", "state" : { - "revision" : "d9b85b40ad36ac5183f44b6db9805e44171ee988", - "version" : "0.17.0-beta" + "revision" : "731c7bbf4514a90b879c5c15968cdb41ac728e3a", + "version" : "0.18.0-beta" } } ], diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift index bd7691f..4c2efa1 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift @@ -59,6 +59,8 @@ protocol SDKSynchronizerClient { func getShieldedBalance() -> WalletBalance? func getTransparentBalance() -> WalletBalance? + func getAllSentTransactions() -> Effect<[WalletEvent], Never> + func getAllReceivedTransactions() -> Effect<[WalletEvent], Never> func getAllClearedTransactions() -> Effect<[WalletEvent], Never> func getAllPendingTransactions() -> Effect<[WalletEvent], Never> func getAllTransactions() -> Effect<[WalletEvent], Never> diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift index fd1d667..e986526 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift @@ -113,10 +113,35 @@ class LiveSDKSynchronizerClient: SDKSynchronizerClient { latestScannedSynchronizerState?.transparentBalance } + func getAllSentTransactions() -> Effect<[WalletEvent], Never> { + if let transactions = try? synchronizer?.allSentTransactions() { + return Effect(value: transactions.map { + let memos = try? synchronizer?.getMemos(for: $0) + let transaction = TransactionState.init(transaction: $0, memos: memos) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + }) + } + + return .none + } + + func getAllReceivedTransactions() -> Effect<[WalletEvent], Never> { + if let transactions = try? synchronizer?.allReceivedTransactions() { + return Effect(value: transactions.map { + let memos = try? synchronizer?.getMemos(for: $0) + let transaction = TransactionState.init(transaction: $0, memos: memos) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + }) + } + + return .none + } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { - if let clearedTransactions = try? synchronizer?.allClearedTransactions() { - return Effect(value: clearedTransactions.map { - let transaction = TransactionState.init(confirmedTransaction: $0, sent: ($0.toAddress != nil)) + if let transactions = try? synchronizer?.allClearedTransactions() { + return Effect(value: transactions.map { + let memos = try? synchronizer?.getMemos(for: $0) + let transaction = TransactionState.init(transaction: $0, memos: memos) return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) }) } @@ -125,9 +150,9 @@ class LiveSDKSynchronizerClient: SDKSynchronizerClient { } func getAllPendingTransactions() -> Effect<[WalletEvent], Never> { - if let pendingTransactions = try? synchronizer?.allPendingTransactions(), + if let transactions = try? synchronizer?.allPendingTransactions(), let syncedBlockHeight = synchronizer?.latestScannedHeight { - return Effect(value: pendingTransactions.map { + return Effect(value: transactions.map { let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight) return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp) }) @@ -141,21 +166,21 @@ class LiveSDKSynchronizerClient: SDKSynchronizerClient { let clearedTransactions = try? synchronizer?.allClearedTransactions(), let syncedBlockHeight = synchronizer?.latestScannedHeight { let clearedTxs: [WalletEvent] = clearedTransactions.map { - let transaction = TransactionState.init(confirmedTransaction: $0, sent: ($0.toAddress != nil)) + let transaction = TransactionState.init(transaction: $0) return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) } let pendingTxs: [WalletEvent] = pendingTransactions.map { let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: syncedBlockHeight) return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp) } - - let txs = clearedTxs.filter { cleared in + let cTxs = clearedTxs.filter { transaction in pendingTxs.first { pending in - pending.id == cleared.id + pending.id == transaction.id } == nil } + return .merge( - Effect(value: txs), + Effect(value: cTxs), Effect(value: pendingTxs) ) .flatMap(Publishers.Sequence.init(sequence:)) diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift index 597b4f1..3131adb 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerMocks.swift @@ -47,6 +47,56 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) } + func getAllSentTransactions() -> Effect<[WalletEvent], Never> { + let mocked: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") + ] + + return Effect( + value: + mocked.map { + let transaction = TransactionState.placeholder( + amount: $0.amount, + fee: Zatoshi(10), + shielded: $0.shielded, + status: $0.status, + timestamp: $0.date, + uuid: $0.uuid + ) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0) + } + ) + } + + func getAllReceivedTransactions() -> Effect<[WalletEvent], Never> { + let mocked: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") + ] + + return Effect( + value: + mocked.map { + let transaction = TransactionState.placeholder( + amount: $0.amount, + fee: Zatoshi(10), + shielded: $0.shielded, + status: $0.status, + timestamp: $0.date, + uuid: $0.uuid + ) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0) + } + ) + } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), @@ -67,7 +117,7 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient { timestamp: $0.date, uuid: $0.uuid ) - return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0) } ) } @@ -132,9 +182,12 @@ class MockSDKSynchronizerClient: SDKSynchronizerClient { to recipientAddress: Recipient, memo: Memo? ) -> Effect, Never> { + var memos: [Memo]? = [] + if let memo { memos?.append(memo) } + let transactionState = TransactionState( - expirationHeight: 40, - memo: memo, + expiryHeight: 40, + memos: memos, minedHeight: 50, shielded: true, zAddress: "tteafadlamnelkqe", diff --git a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift index 540e827..9290c13 100644 --- a/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift +++ b/secant/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift @@ -42,6 +42,10 @@ class NoopSDKSynchronizer: SDKSynchronizerClient { func getTransparentBalance() -> WalletBalance? { nil } + func getAllSentTransactions() -> Effect<[WalletEvent], Never> { Effect(value: []) } + + func getAllReceivedTransactions() -> Effect<[WalletEvent], Never> { Effect(value: []) } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { Effect(value: []) } func getAllPendingTransactions() -> Effect<[WalletEvent], Never> { Effect(value: []) } @@ -69,7 +73,6 @@ class NoopSDKSynchronizer: SDKSynchronizerClient { } class TestSDKSynchronizerClient: SDKSynchronizerClient { - private(set) var blockProcessor: CompactBlockProcessor? private(set) var notificationCenter: NotificationCenterClient private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject @@ -97,6 +100,56 @@ class TestSDKSynchronizerClient: SDKSynchronizerClient { func getTransparentBalance() -> WalletBalance? { nil } + func getAllSentTransactions() -> Effect<[WalletEvent], Never> { + let mocked: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") + ] + + return Effect( + value: + mocked.map { + let transaction = TransactionState.placeholder( + amount: $0.amount, + fee: Zatoshi(10), + shielded: $0.shielded, + status: $0.status, + timestamp: $0.date, + uuid: $0.uuid + ) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + } + ) + } + + func getAllReceivedTransactions() -> Effect<[WalletEvent], Never> { + let mocked: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") + ] + + return Effect( + value: + mocked.map { + let transaction = TransactionState.placeholder( + amount: $0.amount, + fee: Zatoshi(10), + shielded: $0.shielded, + status: $0.status, + timestamp: $0.date, + uuid: $0.uuid + ) + return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + } + ) + } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index af84389..2e22f86 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -40,8 +40,8 @@ struct HomeReducer: ReducerProtocol { Zatoshi.from(decimal: shieldedBalance.total.decimalValue.decimalValue * zecPrice) } - var isDownloading: Bool { - if case .downloading = synchronizerStatusSnapshot.syncStatus { + var isSyncing: Bool { + if case .syncing = synchronizerStatusSnapshot.syncStatus { return true } return false @@ -120,7 +120,7 @@ struct HomeReducer: ReducerProtocol { case .synchronizerStateChanged(.synced): return .merge( - sdkSynchronizer.getAllClearedTransactions() + sdkSynchronizer.getAllTransactions() .receive(on: mainQueue) .map(HomeReducer.Action.updateWalletEvents) .eraseToEffect(), diff --git a/secant/Features/Home/HomeView.swift b/secant/Features/Home/HomeView.swift index 550212f..ea5192d 100644 --- a/secant/Features/Home/HomeView.swift +++ b/secant/Features/Home/HomeView.swift @@ -118,8 +118,8 @@ extension HomeView { VStack { ZStack { CircularProgress( - outerCircleProgress: viewStore.isDownloading ? 0 : viewStore.synchronizerStatusSnapshot.progress, - innerCircleProgress: viewStore.isDownloading ? viewStore.synchronizerStatusSnapshot.progress : 1, + outerCircleProgress: viewStore.isSyncing ? 0 : viewStore.synchronizerStatusSnapshot.progress, + innerCircleProgress: 1, maxSegments: viewStore.requiredTransactionConfirmations, innerCircleHidden: viewStore.isUpToDate ) diff --git a/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift b/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift index eb71b42..8ba909b 100644 --- a/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift +++ b/secant/Features/WalletEventsFlow/Views/TransactionDetailView.swift @@ -25,26 +25,26 @@ struct TransactionDetailView: View { plainText("fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) address(mark: .inactive, viewStore: viewStore) - if let text = transaction.memo?.toString() { memo(text, viewStore, mark: .highlight) } + if let text = transaction.memos?.first?.toString() { memo(text, viewStore, mark: .highlight) } confirmed(mark: .success, viewStore: viewStore) case .pending: plainText("You are sending \(transaction.zecAmount.decimalString()) ZEC") plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) - if let text = transaction.memo?.toString() { memo(text, viewStore, mark: .inactive) } + if let text = transaction.memos?.first?.toString() { memo(text, viewStore, mark: .inactive) } confirming(mark: .highlight, viewStore: viewStore) case .received: plainText("You received \(transaction.zecAmount.decimalString()) ZEC") plainText("fee \(transaction.fee.decimalString()) ZEC") plainText("total amount \(transaction.totalAmount.decimalString()) ZEC") address(mark: .inactive, viewStore: viewStore) - if let text = transaction.memo?.toString() { memo(text, viewStore, mark: .highlight) } + if let text = transaction.memos?.first?.toString() { memo(text, viewStore, mark: .highlight) } confirmed(mark: .success, viewStore: viewStore) case .failed: plainText("You DID NOT send \(transaction.zecAmount.decimalString()) ZEC", mark: .fail) plainText("Includes network fee \(transaction.fee.decimalString()) ZEC", mark: .inactive) plainText("total amount \(transaction.totalAmount.decimalString()) ZEC", mark: .inactive) - if let text = transaction.memo?.toString() { memo(text, viewStore, mark: .inactive) } + if let text = transaction.memos?.first?.toString() { memo(text, viewStore, mark: .inactive) } if let errorMessage = transaction.errorMessage { plainTwoColumnText(left: "Failed", right: errorMessage, mark: .fail) } @@ -68,11 +68,11 @@ extension TransactionDetailView { Text("PENDING") Spacer() case .failed: - Text("\(transaction.date.asHumanReadable())") + Text("\(transaction.date?.asHumanReadable() ?? "date not available")") Spacer() Text("FAILED") default: - Text("\(transaction.date.asHumanReadable())") + Text("\(transaction.date?.asHumanReadable() ?? "date not available")") Spacer() Text("HEIGHT \(heightText)") } @@ -192,7 +192,8 @@ extension TransactionDetailView { } var heightText: String { - transaction.minedHeight > 0 ? String(transaction.minedHeight) : "unconfirmed" + guard let minedHeight = transaction.minedHeight else { return "unconfirmed" } + return minedHeight > 0 ? String(minedHeight) : "unconfirmed" } } @@ -253,12 +254,7 @@ struct TransactionDetail_Previews: PreviewProvider { transaction: TransactionState( errorMessage: "possible roll back", - memo: try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """), + memos: [Memo.placeholder], minedHeight: 1_875_256, zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", fee: Zatoshi(1_000_000), @@ -273,3 +269,13 @@ struct TransactionDetail_Previews: PreviewProvider { } } } + +private extension Memo { + // swiftlint:disable:next force_try + static let placeholder = try! Memo(string: + """ + Testing some long memo so I can see many lines of text \ + instead of just one. This can take some time and I'm \ + bored to write all this stuff. + """) +} diff --git a/secant/Features/WalletEventsFlow/Views/TransactionRowView.swift b/secant/Features/WalletEventsFlow/Views/TransactionRowView.swift index 985b3e5..2f91cd2 100644 --- a/secant/Features/WalletEventsFlow/Views/TransactionRowView.swift +++ b/secant/Features/WalletEventsFlow/Views/TransactionRowView.swift @@ -62,14 +62,14 @@ extension TransactionRowView { var operationTitle: String { switch transaction.status { case .paid(success: _): - return "You sent to" + return "You sent" case .received: return "Unknown paid you" case .failed: // TODO: [#392] final text to be provided (https://github.com/zcash/secant-ios-wallet/issues/392) return "Transaction failed" case .pending: - return "You are sending to" + return "You are sending" } } diff --git a/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift b/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift index bad93dc..8b51366 100644 --- a/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift +++ b/secant/Features/WalletEventsFlow/WalletEventsFlowStore.swift @@ -60,7 +60,7 @@ struct WalletEventsFlowReducer: ReducerProtocol { if let latestMinedHeight = sdkSynchronizer.synchronizer?.latestScannedHeight { state.latestMinedHeight = latestMinedHeight } - return sdkSynchronizer.getAllTransactions() + return sdkSynchronizer.getAllClearedTransactions() .receive(on: mainQueue) .map(WalletEventsFlowReducer.Action.updateWalletEvents) .eraseToEffect() @@ -71,7 +71,10 @@ struct WalletEventsFlowReducer: ReducerProtocol { case .updateWalletEvents(let walletEvents): let sortedWalletEvents = walletEvents .sorted(by: { lhs, rhs in - lhs.timestamp > rhs.timestamp + guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { + return false + } + return lhsTimestamp > rhsTimestamp }) state.walletEvents = IdentifiedArrayOf(uniqueElements: sortedWalletEvents) return .none diff --git a/secant/Models/SyncStatusSnapshot.swift b/secant/Models/SyncStatusSnapshot.swift index 19a1cf2..3f76245 100644 --- a/secant/Models/SyncStatusSnapshot.swift +++ b/secant/Models/SyncStatusSnapshot.swift @@ -21,18 +21,12 @@ struct SyncStatusSnapshot: Equatable { static func snapshotFor(state: SyncStatus) -> SyncStatusSnapshot { switch state { - case .downloading(let progress): - return SyncStatusSnapshot(state, "downloading - \(String(format: "%d%%", Int(progress.progress * 100.0)))", progress.progress) - case .enhancing(let enhanceProgress): return SyncStatusSnapshot(state, "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)") case .fetching: return SyncStatusSnapshot(state, "fetching UTXOs") - case .scanning(let progress): - return SyncStatusSnapshot(state, "scanning - \(String(format: "%d%%", Int(progress.progress * 100.0)))", progress.progress) - case .disconnected: return SyncStatusSnapshot(state, "disconnected 💔") @@ -45,11 +39,11 @@ struct SyncStatusSnapshot: Equatable { case .unprepared: return SyncStatusSnapshot(state, "Unprepared 😅") - case .validating: - return SyncStatusSnapshot(state, "Validating") - case .error(let err): return SyncStatusSnapshot(state, "Error: \(err.localizedDescription)") + + case .syncing(let progress): + return SyncStatusSnapshot(state, "Syncing \(progress)") } } } diff --git a/secant/Models/TransactionState.swift b/secant/Models/TransactionState.swift index f2e6092..e40899d 100644 --- a/secant/Models/TransactionState.swift +++ b/secant/Models/TransactionState.swift @@ -18,27 +18,38 @@ struct TransactionState: Equatable, Identifiable { } var errorMessage: String? - var expirationHeight = -1 - var memo: Memo? - var minedHeight = -1 + var expiryHeight: BlockHeight? + var memos: [Memo]? + var minedHeight: BlockHeight? var shielded = true var zAddress: String? var fee: Zatoshi var id: String var status: Status - var timestamp: TimeInterval + var timestamp: TimeInterval? var zecAmount: Zatoshi - var address: String { zAddress ?? "" } - var date: Date { Date(timeIntervalSince1970: timestamp) } - var totalAmount: Zatoshi { Zatoshi(zecAmount.amount + fee.amount) } + var address: String { + zAddress ?? "" + } + + var date: Date? { + guard let timestamp else { return nil } + + return Date(timeIntervalSince1970: timestamp) + } + + var totalAmount: Zatoshi { + Zatoshi(zecAmount.amount + fee.amount) + } + var viewOnlineURL: URL? { URL(string: "https://zcashblockexplorer.com/transactions/\(id)") } - + func confirmationsWith(_ latestMinedHeight: BlockHeight?) -> BlockHeight { - guard let latestMinedHeight = latestMinedHeight, minedHeight > 0, latestMinedHeight > 0 else { + guard let minedHeight, let latestMinedHeight, minedHeight > 0, latestMinedHeight > 0 else { return 0 } @@ -47,20 +58,39 @@ struct TransactionState: Equatable, Identifiable { } extension TransactionState { - init(confirmedTransaction: ConfirmedTransactionEntity, sent: Bool = false) { - timestamp = confirmedTransaction.blockTimeInSeconds - id = confirmedTransaction.transactionEntity.transactionId.toHexStringTxId() - shielded = true - status = sent ? .paid(success: confirmedTransaction.minedHeight > 0) : .received - zAddress = confirmedTransaction.toAddress - zecAmount = sent ? Zatoshi(-confirmedTransaction.value.amount) : confirmedTransaction.value - fee = Zatoshi(10) - if let memoData = confirmedTransaction.memo { - self.memo = try? Memo(bytes: Array(memoData)) - } - minedHeight = confirmedTransaction.minedHeight + init(transaction: ZcashTransaction.Overview, memos: [Memo]? = nil) { + expiryHeight = transaction.expiryHeight + minedHeight = transaction.minedHeight + fee = transaction.fee ?? Zatoshi(0) + id = transaction.rawID.toHexStringTxId() + status = transaction.isSentTransaction ? .paid(success: minedHeight ?? 0 > 0) : .received + timestamp = transaction.blockTime + zecAmount = transaction.isSentTransaction ? Zatoshi(-transaction.value.amount) : transaction.value + self.memos = memos } - + + init(transaction: ZcashTransaction.Sent, memos: [Memo]? = nil) { + expiryHeight = transaction.expiryHeight + minedHeight = transaction.minedHeight + fee = .zero + id = transaction.rawID?.toHexStringTxId() ?? "" + status = .paid(success: minedHeight ?? 0 > 0) + timestamp = transaction.blockTime + zecAmount = transaction.value + self.memos = memos + } + + init(transaction: ZcashTransaction.Received, memos: [Memo]? = nil) { + expiryHeight = transaction.expiryHeight + minedHeight = transaction.minedHeight + fee = .zero + id = transaction.rawID?.toHexStringTxId() ?? "" + status = .received + timestamp = transaction.blockTime + zecAmount = transaction.value + self.memos = memos + } + init(pendingTransaction: PendingTransactionEntity, latestBlockHeight: BlockHeight? = nil) { timestamp = pendingTransaction.createTime id = pendingTransaction.rawTransactionId?.toHexStringTxId() ?? String(pendingTransaction.createTime) @@ -69,12 +99,9 @@ extension TransactionState { pendingTransaction.minedHeight > 0 ? .paid(success: pendingTransaction.isSubmitSuccess) : .pending - expirationHeight = pendingTransaction.expiryHeight + expiryHeight = pendingTransaction.expiryHeight zecAmount = pendingTransaction.value fee = Zatoshi(10) - if let memoData = pendingTransaction.memo { - self.memo = try? Memo(bytes: Array(memoData)) - } minedHeight = pendingTransaction.minedHeight errorMessage = pendingTransaction.errorMessage @@ -99,8 +126,8 @@ extension TransactionState { uuid: String = UUID().debugDescription ) -> TransactionState { .init( - expirationHeight: -1, - memo: nil, + expiryHeight: -1, + memos: nil, minedHeight: -1, shielded: shielded, zAddress: nil, diff --git a/secant/Models/WalletEvent.swift b/secant/Models/WalletEvent.swift index ad930ab..cf18bf0 100644 --- a/secant/Models/WalletEvent.swift +++ b/secant/Models/WalletEvent.swift @@ -24,7 +24,7 @@ struct WalletEvent: Equatable, Identifiable { let id: String let state: WalletEventState - var timestamp: TimeInterval + var timestamp: TimeInterval? } // MARK: - Rows diff --git a/secantTests/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift index 3f2faab..26db167 100644 --- a/secantTests/HomeTests/HomeTests.swift +++ b/secantTests/HomeTests/HomeTests.swift @@ -50,18 +50,27 @@ class HomeTests: XCTestCase { TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), - TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55"), + TransactionStateMockHelper( + date: 1651039606, + amount: Zatoshi(6), + status: .paid(success: false), + uuid: "ff66" + ), + TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"), + TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99") ] let walletEvents: [WalletEvent] = transactionsHelper.map { let transaction = TransactionState.placeholder( amount: $0.amount, fee: Zatoshi(10), shielded: $0.shielded, - status: $0.status, + status: $0.amount.amount > 5 ? .pending : $0.status, timestamp: $0.date, uuid: $0.uuid ) - return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp) + return WalletEvent(id: transaction.id, state: $0.amount.amount > 5 ? .pending(transaction) : .send(transaction), timestamp: transaction.timestamp) } store.receive(.updateWalletEvents(walletEvents)) diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 86c9749..40a7819 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -62,10 +62,14 @@ class SendTests: XCTestCase { } await testScheduler.advance(by: 0.01) - + guard let memo = try? Memo(string: "test") else { + XCTFail("testSendSucceeded: memo is expected to be successfuly initialized.") + return + } + let transactionState = TransactionState( - expirationHeight: 40, - memo: try? Memo(string: "test"), + expiryHeight: 40, + memos: [memo], minedHeight: 50, shielded: true, zAddress: "tteafadlamnelkqe", @@ -132,8 +136,8 @@ class SendTests: XCTestCase { await testScheduler.advance(by: 0.01) let transactionState = TransactionState( - expirationHeight: 40, - memo: nil, + expiryHeight: 40, + memos: [], minedHeight: 50, shielded: true, zAddress: "tteafadlamnelkqe", diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift index cb63518..40121c0 100644 --- a/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift @@ -21,7 +21,7 @@ class HomeCircularProgressSnapshotTests: XCTestCase { progressHeight: BlockHeight(55) ) - return SyncStatusSnapshot.snapshotFor(state: .downloading(blockProgress)) + return SyncStatusSnapshot.snapshotFor(state: .syncing(blockProgress)) } } @@ -57,7 +57,7 @@ class HomeCircularProgressSnapshotTests: XCTestCase { progressHeight: BlockHeight(72) ) - return SyncStatusSnapshot.snapshotFor(state: .scanning(blockProgress)) + return SyncStatusSnapshot.snapshotFor(state: .syncing(blockProgress)) } } diff --git a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift index d3b6cca..2753dcc 100644 --- a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift +++ b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift @@ -63,13 +63,19 @@ class WalletEventsSnapshotTests: XCTestCase { } func testWalletEventDetailSnapshot_sent() throws { + let memo = try? Memo(string: + """ + Testing some long memo so I can see many lines of text \ + instead of just one. This can take some time and I'm \ + bored to write all this stuff. + """) + guard let memo else { + XCTFail("testWalletEventDetailSnapshot_sent: memo is expected to be successfuly initialized") + return + } + let transaction = TransactionState( - memo: try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """), + memos: [memo], minedHeight: 1_875_256, zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", fee: Zatoshi(1_000_000), @@ -110,13 +116,19 @@ class WalletEventsSnapshotTests: XCTestCase { } func testWalletEventDetailSnapshot_received() throws { + let memo = try? Memo(string: + """ + Testing some long memo so I can see many lines of text \ + instead of just one. This can take some time and I'm \ + bored to write all this stuff. + """) + guard let memo else { + XCTFail("testWalletEventDetailSnapshot_received: memo is expected to be successfuly initialized") + return + } + let transaction = TransactionState( - memo: try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """), + memos: [memo], minedHeight: 1_875_256, zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", fee: Zatoshi(1_000_000), @@ -157,13 +169,19 @@ class WalletEventsSnapshotTests: XCTestCase { } func testWalletEventDetailSnapshot_pending() throws { + let memo = try? Memo(string: + """ + Testing some long memo so I can see many lines of text \ + instead of just one. This can take some time and I'm \ + bored to write all this stuff. + """) + guard let memo else { + XCTFail("testWalletEventDetailSnapshot_pending: memo is expected to be successfuly initialized") + return + } + let transaction = TransactionState( - memo: try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """), + memos: [memo], minedHeight: 1_875_256, zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", fee: Zatoshi(1_000_000), @@ -209,14 +227,20 @@ class WalletEventsSnapshotTests: XCTestCase { } func testWalletEventDetailSnapshot_failed() throws { + let memo = try? Memo(string: + """ + Testing some long memo so I can see many lines of text \ + instead of just one. This can take some time and I'm \ + bored to write all this stuff. + """) + guard let memo else { + XCTFail("testWalletEventDetailSnapshot_failed: memo is expected to be successfuly initialized") + return + } + let transaction = TransactionState( errorMessage: "possible roll back", - memo: try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """), + memos: [memo], minedHeight: 1_875_256, zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", fee: Zatoshi(1_000_000), diff --git a/secantTests/WalletEventsTests/WalletEventsTests.swift b/secantTests/WalletEventsTests/WalletEventsTests.swift index 6b77531..70ed634 100644 --- a/secantTests/WalletEventsTests/WalletEventsTests.swift +++ b/secantTests/WalletEventsTests/WalletEventsTests.swift @@ -39,16 +39,7 @@ class WalletEventsTests: XCTestCase { TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), - TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55"), - TransactionStateMockHelper( - date: 1651039606, - amount: Zatoshi(6), - status: .paid(success: false), - uuid: "ff66" - ), - TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"), - TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"), - TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99") + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") ] let walletEvents: [WalletEvent] = mocked.map { @@ -56,7 +47,7 @@ class WalletEventsTests: XCTestCase { amount: $0.amount, fee: Zatoshi(10), shielded: $0.shielded, - status: $0.amount.amount > 5 ? .pending : $0.status, + status: $0.status, timestamp: $0.date, uuid: $0.uuid ) @@ -90,7 +81,10 @@ class WalletEventsTests: XCTestCase { uniqueElements: walletEvents .sorted(by: { lhs, rhs in - lhs.timestamp > rhs.timestamp + guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { + return false + } + return lhsTimestamp > rhsTimestamp }) )