pending + cleared + sorted transactions cleanup any scheduler + tests skeleton transaction history tests syncedBlockHeight from the synchronizer
This commit is contained in:
parent
ccb9301fb2
commit
3e8169551b
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -161,6 +161,7 @@ extension HomeStore {
|
|||
initialState: .placeholder,
|
||||
reducer: .default.debug(),
|
||||
environment: HomeEnvironment(
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
wrappedSDKSynchronizer: LiveWrappedSDKSynchronizer()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 = ""
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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(),
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue