[#293] first draft of history of transactions (#293)

pending + cleared + sorted transactions

cleanup

any scheduler + tests skeleton

transaction history tests

syncedBlockHeight from the synchronizer
This commit is contained in:
Lukas Korba 2022-04-28 12:22:31 +02:00 committed by GitHub
parent ccb9301fb2
commit 3e8169551b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 636 additions and 76 deletions

View File

@ -101,6 +101,8 @@
9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; };
9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; };
9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; };
9E5BF63C2818305D00BA3F17 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; };
9E5BF63F2819542C00BA3F17 /* TransactionHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63E2819542C00BA3F17 /* TransactionHistoryTests.swift */; };
9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; };
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; };
@ -264,6 +266,8 @@
9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = "<group>"; };
9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = "<group>"; };
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionState.swift; sourceTree = "<group>"; };
9E5BF63E2819542C00BA3F17 /* TransactionHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryTests.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
@ -460,8 +464,9 @@
0D4E7A1926B364180058B01E /* secantTests */ = {
isa = PBXGroup;
children = (
9EAFEB802805791400199FC9 /* AppReducer */,
9EF8135927ECC25E0075AF48 /* Util */,
9E5BF63D281953F900BA3F17 /* TransactionHistoryTests */,
9EAFEB802805791400199FC9 /* AppReducerTests */,
9EF8135927ECC25E0075AF48 /* UtilTests */,
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
6654C7422715A48E00901167 /* OnboardingTests */,
@ -779,12 +784,20 @@
path = Drawer;
sourceTree = "<group>";
};
9EAFEB802805791400199FC9 /* AppReducer */ = {
9E5BF63D281953F900BA3F17 /* TransactionHistoryTests */ = {
isa = PBXGroup;
children = (
9E5BF63E2819542C00BA3F17 /* TransactionHistoryTests.swift */,
);
path = TransactionHistoryTests;
sourceTree = "<group>";
};
9EAFEB802805791400199FC9 /* AppReducerTests */ = {
isa = PBXGroup;
children = (
9EAFEB812805793200199FC9 /* AppReducerTests.swift */,
);
path = AppReducer;
path = AppReducerTests;
sourceTree = "<group>";
};
9EAFEB8B2808174900199FC9 /* Sandbox */ = {
@ -812,7 +825,7 @@
path = Preamble;
sourceTree = "<group>";
};
9EF8135927ECC25E0075AF48 /* Util */ = {
9EF8135927ECC25E0075AF48 /* UtilTests */ = {
isa = PBXGroup;
children = (
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */,
@ -820,7 +833,7 @@
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */,
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
);
path = Util;
path = UtilTests;
sourceTree = "<group>";
};
F93874EC273C4DE200F0E875 /* Home */ = {
@ -844,6 +857,7 @@
isa = PBXGroup;
children = (
F96B41E3273B501F0021B49A /* TransactionHistoryStore.swift */,
9E5BF63B2818305D00BA3F17 /* TransactionState.swift */,
F96B41E4273B501F0021B49A /* Views */,
);
path = TransactionHistory;
@ -1273,6 +1287,7 @@
9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */,
F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */,
F96B41EB273B50520021B49A /* Strings.swift in Sources */,
9E5BF63C2818305D00BA3F17 /* TransactionState.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
@ -1300,6 +1315,7 @@
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */,
9E5BF63F2819542C00BA3F17 /* TransactionHistoryTests.swift in Sources */,
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */,

View File

@ -52,7 +52,7 @@ struct AppEnvironment {
extension AppEnvironment {
static let live = AppEnvironment(
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer(),
wrappedSDKSynchronizer: MockWrappedSDKSynchronizer(),
databaseFiles: .live(),
mnemonicSeedPhraseProvider: .live,
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
@ -291,6 +291,7 @@ extension AppReducer {
action: /AppAction.home,
environment: { environment in
HomeEnvironment(
scheduler: environment.scheduler,
wrappedSDKSynchronizer: environment.wrappedSDKSynchronizer
)
}

View File

@ -34,9 +34,11 @@ enum HomeAction: Equatable {
case updateBalance(Balance)
case updateDrawer(DrawerOverlay)
case updateRoute(HomeState.Route?)
case updateTransactions([TransactionState])
}
struct HomeEnvironment {
let scheduler: AnySchedulerOf<DispatchQueue>
let wrappedSDKSynchronizer: WrappedSDKSynchronizer
}
@ -47,7 +49,15 @@ private struct ListenerId: Hashable {}
typealias HomeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
extension HomeReducer {
static let `default` = HomeReducer { state, action, environment in
static let `default` = HomeReducer.combine(
[
homeReducer,
historyReducer
]
)
.debug()
private static let homeReducer = HomeReducer { state, action, environment in
switch action {
case .onAppear:
return environment.wrappedSDKSynchronizer.stateChanged
@ -59,11 +69,18 @@ extension HomeReducer {
return Effect.cancel(id: ListenerId())
case .synchronizerStateChanged(.synced):
return environment.wrappedSDKSynchronizer.getShieldedBalance()
.receive(on: DispatchQueue.main)
return .merge(
environment.wrappedSDKSynchronizer.getAllClearedTransactions()
.receive(on: environment.scheduler)
.map(HomeAction.updateTransactions)
.eraseToEffect(),
environment.wrappedSDKSynchronizer.getShieldedBalance()
.receive(on: environment.scheduler)
.map({ Balance(verified: $0.verified, total: $0.total) })
.map(HomeAction.updateBalance)
.eraseToEffect()
)
case .synchronizerStateChanged(let synchronizerState):
return .none
@ -81,11 +98,8 @@ extension HomeReducer {
state.transactionHistoryState.isScrollable = drawerOverlay == .full ? true : false
return .none
case .transactionHistory(let historyAction):
return TransactionHistoryReducer
.default
.run(&state.transactionHistoryState, historyAction, ())
.map(HomeAction.transactionHistory)
case .updateTransactions(let transactions):
return .none
case .updateRoute(let route):
state.route = route
@ -102,8 +116,28 @@ extension HomeReducer {
case .scan(let action):
return .none
case .transactionHistory(.updateRoute(.all)):
return state.drawerOverlay != .full ? Effect(value: .updateDrawer(.full)) : .none
case .transactionHistory(.updateRoute(.latest)):
return state.drawerOverlay != .partial ? Effect(value: .updateDrawer(.partial)) : .none
case .transactionHistory(let historyAction):
return .none
}
}
private static let historyReducer: HomeReducer = TransactionHistoryReducer.default.pullback(
state: \HomeState.transactionHistoryState,
action: /HomeAction.transactionHistory,
environment: { environment in
TransactionHistoryEnvironment(
scheduler: environment.scheduler,
wrappedSDKSynchronizer: environment.wrappedSDKSynchronizer
)
}
)
}
// MARK: - HomeViewStore
@ -180,7 +214,7 @@ extension HomeState {
sendState: .placeholder,
scanState: .placeholder,
totalBalance: 0.0,
transactionHistoryState: .placeHolder,
transactionHistoryState: .emptyPlaceHolder,
verifiedBalance: 0.0
)
}

View File

@ -161,6 +161,7 @@ extension HomeStore {
initialState: .placeholder,
reducer: .default.debug(),
environment: HomeEnvironment(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer()
)
)

View File

@ -27,7 +27,7 @@ enum SandboxAction: Equatable {
typealias SandboxReducer = Reducer<SandboxState, SandboxAction, Void>
extension SandboxReducer {
static let `default` = SandboxReducer { state, action, _ in
static let `default` = SandboxReducer { state, action, environment in
switch action {
case let .updateRoute(route):
state.route = route
@ -35,7 +35,14 @@ extension SandboxReducer {
case let .transactionHistory(transactionHistoryAction):
return TransactionHistoryReducer
.default
.run(&state.transactionHistoryState, transactionHistoryAction, ())
.run(
&state.transactionHistoryState,
transactionHistoryAction,
TransactionHistoryEnvironment(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer()
)
)
.map(SandboxAction.transactionHistory)
case let .profile(profileAction):
return ProfileReducer
@ -83,10 +90,10 @@ extension SandboxViewStore {
let isAlreadySelected = (self.selectedTranactionID != nil)
let transcation = self.transactionHistoryState.transactions[5]
let newRoute = isAlreadySelected ? nil : TransactionHistoryState.Route.showTransaction(transcation)
send(.transactionHistory(.setRoute(newRoute)))
send(.transactionHistory(.updateRoute(newRoute)))
}
var selectedTranactionID: Int? {
var selectedTranactionID: String? {
self.transactionHistoryState
.route
.flatMap(/TransactionHistoryState.Route.showTransaction)

View File

@ -1,6 +1,26 @@
import SwiftUI
import ComposableArchitecture
struct Transaction: Identifiable, Equatable, Hashable {
var id: Int
var amount: UInt
var memo: String
var toAddress: String
var fromAddress: String
}
extension Transaction {
static var placeholder: Self {
.init(
id: 2,
amount: 123,
memo: "defaultMemo",
toAddress: "ToAddress",
fromAddress: "FromAddress"
)
}
}
struct SendView: View {
enum Route: Equatable {
case showApprove

View File

@ -1,37 +1,79 @@
import ComposableArchitecture
import SwiftUI
struct Transaction: Identifiable, Equatable, Hashable {
var id: Int
var amount: UInt
var memo: String
var toAddress: String
var fromAddress: String
extension Date {
func asHumanReadable() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter.string(from: self)
}
}
struct TransactionHistoryState: Equatable {
enum Route: Equatable {
case showTransaction(Transaction)
case latest
case all
case showTransaction(TransactionState)
}
var route: Route?
var isScrollable = false
var transactions: IdentifiedArrayOf<Transaction>
var transactions: IdentifiedArrayOf<TransactionState>
}
enum TransactionHistoryAction: Equatable {
case setRoute(TransactionHistoryState.Route?)
case onAppear
case onDisappear
case updateRoute(TransactionHistoryState.Route?)
case synchronizerStateChanged(WrappedSDKSynchronizerState)
case updateTransactions([TransactionState])
}
struct TransactionHistoryEnvironment {
let scheduler: AnySchedulerOf<DispatchQueue>
let wrappedSDKSynchronizer: WrappedSDKSynchronizer
}
// MARK: - TransactionHistoryReducer
typealias TransactionHistoryReducer = Reducer<TransactionHistoryState, TransactionHistoryAction, Void>
private struct ListenerId: Hashable {}
typealias TransactionHistoryReducer = Reducer<TransactionHistoryState, TransactionHistoryAction, TransactionHistoryEnvironment>
extension TransactionHistoryReducer {
static let `default` = TransactionHistoryReducer { state, action, _ in
static let `default` = TransactionHistoryReducer { state, action, environment in
switch action {
case let .setRoute(route):
case .onAppear:
return environment.wrappedSDKSynchronizer.stateChanged
.map(TransactionHistoryAction.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: ListenerId(), cancelInFlight: true)
case .onDisappear:
return Effect.cancel(id: ListenerId())
case .synchronizerStateChanged(.synced):
return environment.wrappedSDKSynchronizer.getAllTransactions()
.receive(on: environment.scheduler)
.map(TransactionHistoryAction.updateTransactions)
.eraseToEffect()
case .synchronizerStateChanged(let synchronizerState):
return .none
case .updateTransactions(let transactions):
let sortedTransactions = transactions
.sorted(by: { lhs, rhs in
lhs.date > rhs.date
})
state.transactions = IdentifiedArrayOf(uniqueElements: sortedTransactions)
return .none
case let .updateRoute(route):
state.route = route
return .none
}
@ -49,11 +91,11 @@ typealias TransactionHistoryViewStore = ViewStore<TransactionHistoryState, Trans
extension TransactionHistoryViewStore {
private typealias Route = TransactionHistoryState.Route
func bindingForSelectingTransaction(_ transaction: Transaction) -> Binding<Bool> {
func bindingForSelectingTransaction(_ transaction: TransactionState) -> Binding<Bool> {
self.binding(
get: { $0.route.map(/TransactionHistoryState.Route.showTransaction) == transaction },
send: { isActive in
TransactionHistoryAction.setRoute( isActive ? TransactionHistoryState.Route.showTransaction(transaction) : nil)
TransactionHistoryAction.updateRoute( isActive ? TransactionHistoryState.Route.showTransaction(transaction) : nil)
}
)
}
@ -61,14 +103,14 @@ extension TransactionHistoryViewStore {
// MARK: PlaceHolders
extension Transaction {
extension TransactionState {
static var placeholder: Self {
.init(
id: 2,
amount: 123,
memo: "defaultMemo",
toAddress: "ToAddress",
fromAddress: "FromAddress"
date: Date.init(timeIntervalSince1970: 1234567),
id: "2",
status: .paid(success: true),
subtitle: "",
zecAmount: 25
)
}
}
@ -77,6 +119,10 @@ extension TransactionHistoryState {
static var placeHolder: Self {
.init(transactions: .placeholder)
}
static var emptyPlaceHolder: Self {
.init(transactions: [])
}
}
extension TransactionHistoryStore {
@ -84,33 +130,39 @@ extension TransactionHistoryStore {
return Store(
initialState: .placeHolder,
reducer: .default,
environment: ()
environment: TransactionHistoryEnvironment(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer()
)
)
}
static var demoWithSelectedTransaction: Store<TransactionHistoryState, TransactionHistoryAction> {
let transactions = IdentifiedArrayOf<Transaction>.placeholder
let transactions = IdentifiedArrayOf<TransactionState>.placeholder
return Store(
initialState: TransactionHistoryState(
route: .showTransaction(transactions[3]),
transactions: transactions
),
reducer: .default.debug(),
environment: ()
environment: TransactionHistoryEnvironment(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer()
)
)
}
}
extension IdentifiedArrayOf where Element == Transaction {
static var placeholder: IdentifiedArrayOf<Transaction> {
extension IdentifiedArrayOf where Element == TransactionState {
static var placeholder: IdentifiedArrayOf<TransactionState> {
return .init(
uniqueElements: (0..<30).map {
Transaction(
id: $0,
amount: 25,
memo: "defaultMemo",
toAddress: "ToAddress",
fromAddress: "FromAddress"
TransactionState(
date: Date.init(timeIntervalSince1970: 1234567),
id: String($0),
status: .paid(success: true),
subtitle: "",
zecAmount: 25
)
}
)

View File

@ -0,0 +1,94 @@
//
// TransactionState.swift
// secant-testnet
//
// Created by Lukáš Korba on 26.04.2022.
//
import Foundation
import ZcashLightClientKit
struct TransactionState: Equatable, Identifiable {
enum Status: Equatable {
case paid(success: Bool)
case received
}
var expirationHeight = -1
var memo: String?
var minedHeight = -1
var shielded = true
var zAddress: String?
var date: Date
var id: String
var status: Status
var subtitle: String
var zecAmount: Int64
}
extension TransactionState {
init(confirmedTransaction: ConfirmedTransactionEntity, sent: Bool = false) {
date = Date(timeIntervalSince1970: confirmedTransaction.blockTimeInSeconds)
id = confirmedTransaction.transactionEntity.transactionId.toHexStringTxId()
shielded = true
status = sent ? .paid(success: confirmedTransaction.minedHeight > 0) : .received
subtitle = "sent"
zAddress = confirmedTransaction.toAddress
zecAmount = (sent ? -Int64(confirmedTransaction.value) : Int64(confirmedTransaction.value))
if let memo = confirmedTransaction.memo {
self.memo = memo.asZcashTransactionMemo()
}
minedHeight = confirmedTransaction.minedHeight
}
init(pendingTransaction: PendingTransactionEntity, latestBlockHeight: BlockHeight? = nil) {
date = Date(timeIntervalSince1970: pendingTransaction.createTime)
id = pendingTransaction.rawTransactionId?.toHexStringTxId() ?? String(pendingTransaction.createTime)
shielded = true
status = .paid(success: pendingTransaction.isSubmitSuccess)
expirationHeight = pendingTransaction.expiryHeight
subtitle = "pending"
zAddress = pendingTransaction.toAddress
zecAmount = -Int64(pendingTransaction.value)
if let memo = pendingTransaction.memo {
self.memo = memo.asZcashTransactionMemo()
}
minedHeight = pendingTransaction.minedHeight
}
}
// MARK: - Placeholders
extension TransactionState {
static func placeholder(
date: Date,
amount: Int64,
shielded: Bool = true,
status: Status = .received,
subtitle: String = "",
uuid: String = UUID().debugDescription
) -> TransactionState {
.init(
expirationHeight: -1,
memo: nil,
minedHeight: -1,
shielded: shielded,
zAddress: nil,
date: date,
id: uuid,
status: status,
subtitle: subtitle,
zecAmount: status == .received ? amount : -amount
)
}
}
struct TransactionStateMockHelper {
var date: TimeInterval
var amount: Int64
var shielded = true
var status: TransactionState.Status = .received
var subtitle = "cleared"
var uuid = ""
}

View File

@ -1,13 +1,14 @@
import SwiftUI
struct TransactionDetailView: View {
var transaction: Transaction
var transaction: TransactionState
var body: some View {
Text(String(dumping: transaction))
.padding()
.navigationTitle("Transaction: \(transaction.id)")
}
}
struct TransactionDetail_Previews: PreviewProvider {
static var previews: some View {
NavigationView {

View File

@ -9,6 +9,9 @@ struct TransactionHistoryView: View {
UITableViewCell.appearance().backgroundColor = .clear
return WithViewStore(store) { viewStore in
Group {
header(with: viewStore)
if viewStore.isScrollable {
List {
transactionsList(with: viewStore)
@ -16,9 +19,12 @@ struct TransactionHistoryView: View {
.listStyle(.sidebar)
} else {
transactionsList(with: viewStore)
.padding(.leading, 32)
.padding(.horizontal, 32)
}
}
.onAppear(perform: { viewStore.send(.onAppear) })
.onDisappear(perform: { viewStore.send(.onDisappear) })
}
}
}
@ -26,19 +32,61 @@ extension TransactionHistoryView {
func transactionsList(with viewStore: TransactionHistoryViewStore) -> some View {
ForEach(viewStore.transactions) { transaction in
WithStateBinding(binding: viewStore.bindingForSelectingTransaction(transaction)) { active in
VStack {
HStack {
Text("Show Transaction \(transaction.id)")
Text(transaction.date.asHumanReadable())
.font(.system(size: 12))
.fontWeight(.thin)
Spacer()
Text(transaction.subtitle)
.font(.system(size: 12))
.fontWeight(.thin)
.foregroundColor(transaction.subtitle == "pending" ? .red : .green)
}
HStack {
Text(transaction.status == .received ? "Recevied" : "Sent")
Spacer()
Text(transaction.status == .received ? "+" : "")
+ Text("\(String(format: "%.7f", transaction.zecAmount.asHumanReadableZecBalance())) ZEC")
}
}
.navigationLink(
isActive: active,
destination: { TransactionDetailView(transaction: transaction) }
)
.foregroundColor(Asset.Colors.Text.body.color)
.listRowBackground(Color.clear)
Spacer()
}
}
}
func header(with viewStore: TransactionHistoryViewStore) -> some View {
HStack(spacing: 0) {
VStack {
Button("Latest") {
viewStore.send(.updateRoute(.latest))
}
Rectangle()
.frame(height: 1.5)
.foregroundColor(Asset.Colors.TextField.Underline.purple.color)
}
VStack {
Button("All") {
viewStore.send(.updateRoute(.all))
}
Rectangle()
.frame(height: 1.5)
.foregroundColor(Asset.Colors.TextField.Underline.gray.color)
}
}
}
}
@ -46,6 +94,7 @@ struct TransactionView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TransactionHistoryView(store: .placeholder)
.preferredColorScheme(.dark)
}
}
}

View File

@ -47,6 +47,9 @@ protocol WrappedSDKSynchronizer {
func synchronizerSynced()
func getShieldedBalance() -> Effect<Balance, Never>
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?
@ -113,6 +116,38 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer {
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> {
return .merge(
getAllClearedTransactions(),
getAllPendingTransactions()
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
}
func getTransparentAddress(account: Int) -> TransparentAddress? {
synchronizer?.getTransparentAddress(accountIndex: account)
}
@ -123,6 +158,108 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer {
}
class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
private var cancellables: [AnyCancellable] = []
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never>
init() {
self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.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 getShieldedBalance() -> Effect<Balance, Never> {
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 * 100000000,
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 * 100000000,
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 }
}
class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never>
@ -142,6 +279,63 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
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 * 100000000,
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 * 100000000,
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 }

View File

@ -13,7 +13,7 @@ class AppReducerTests: XCTestCase {
static let testScheduler = DispatchQueue.test
let testEnvironment = AppEnvironment(
wrappedSDKSynchronizer: MockWrappedSDKSynchronizer(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer(),
databaseFiles: .throwing,
mnemonicSeedPhraseProvider: .mock,
scheduler: testScheduler.eraseToAnyScheduler(),
@ -24,7 +24,7 @@ class AppReducerTests: XCTestCase {
func testWalletInitializationState_Uninitialized() throws {
let uninitializedEnvironment = AppEnvironment(
wrappedSDKSynchronizer: MockWrappedSDKSynchronizer(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer(),
databaseFiles: .throwing,
mnemonicSeedPhraseProvider: .mock,
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
@ -46,7 +46,7 @@ class AppReducerTests: XCTestCase {
)
let keysMissingEnvironment = AppEnvironment(
wrappedSDKSynchronizer: MockWrappedSDKSynchronizer(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer(),
databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)),
mnemonicSeedPhraseProvider: .mock,
scheduler: Self.testScheduler.eraseToAnyScheduler(),
@ -68,7 +68,7 @@ class AppReducerTests: XCTestCase {
)
let keysMissingEnvironment = AppEnvironment(
wrappedSDKSynchronizer: MockWrappedSDKSynchronizer(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer(),
databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)),
mnemonicSeedPhraseProvider: .mock,
scheduler: Self.testScheduler.eraseToAnyScheduler(),

View File

@ -0,0 +1,91 @@
//
// TransactionHistoryTests.swift
// secantTests
//
// Created by Lukáš Korba on 27.04.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
class TransactionHistoryTests: XCTestCase {
static let testScheduler = DispatchQueue.test
let testEnvironment = TransactionHistoryEnvironment(
scheduler: testScheduler.eraseToAnyScheduler(),
wrappedSDKSynchronizer: TestWrappedSDKSynchronizer()
)
func testSynchronizerSubscription() throws {
let store = TestStore(
initialState: TransactionHistoryState(
route: .latest,
isScrollable: true,
transactions: []
),
reducer: TransactionHistoryReducer.default,
environment: testEnvironment
)
store.send(.onAppear)
store.receive(.synchronizerStateChanged(.unknown))
// ending the subscription
store.send(.onDisappear)
}
func testSynchronizerStateChanged2Synced() throws {
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"),
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")
]
let transactions = mocked.map {
TransactionState.placeholder(
date: Date.init(timeIntervalSince1970: $0.date),
amount: $0.amount * 100000000,
shielded: $0.shielded,
status: $0.status,
subtitle: $0.subtitle,
uuid: $0.uuid
)
}
let identifiedTransactions = IdentifiedArrayOf(uniqueElements: transactions)
let store = TestStore(
initialState: TransactionHistoryState(
route: .latest,
isScrollable: true,
transactions: identifiedTransactions
),
reducer: TransactionHistoryReducer.default,
environment: testEnvironment
)
store.send(.synchronizerStateChanged(.synced))
Self.testScheduler.advance(by: 0.01)
store.receive(.updateTransactions(transactions)) { state in
let receivedTransactions = IdentifiedArrayOf(
uniqueElements:
transactions
.sorted(by: { lhs, rhs in
lhs.date > rhs.date
})
)
state.transactions = receivedTransactions
}
}
}