Shared states for transactions and memos

- The logic around transactions have been fully refactored. The data are held by shared state only as a one source of truth
- The same idea has been done for on demand loaded memos
- The TransactionState is no longer required to be updated or copied because it doesn't carry any data and it's only an enriched version of ZcashTransaction.Overview for Zashi purposes
This commit is contained in:
Lukas Korba 2025-01-31 12:53:40 +01:00
parent 21a1ebd3f4
commit bd456cec80
21 changed files with 400 additions and 586 deletions

View File

@ -43,18 +43,19 @@ public struct SDKSynchronizerClient {
public let rewind: (RewindPolicy) -> AnyPublisher<Void, Error>
public var getAllTransactions: (AccountUUID?) async throws -> [TransactionState]
public var transactionStatesFromZcashTransactions: (AccountUUID?, [ZcashTransaction.Overview]) async throws -> [TransactionState]
public var getMemos: (Data) async throws -> [Memo]
public let getUnifiedAddress: (_ account: AccountUUID) async throws -> UnifiedAddress?
public let getTransparentAddress: (_ account: AccountUUID) async throws -> TransparentAddress?
public let getSaplingAddress: (_ account: AccountUUID) async throws -> SaplingAddress?
public let getAccountsBalances: () async throws -> [AccountUUID: AccountBalance]
public var wipe: () -> AnyPublisher<Void, Error>?
public var switchToEndpoint: (LightWalletEndpoint) async throws -> Void
// Proposals
public var proposeTransfer: (AccountUUID, Recipient, Zatoshi, Memo?) async throws -> Proposal
public var createProposedTransactions: (Proposal, UnifiedSpendingKey) async throws -> CreateProposedTransactionsResult

View File

@ -83,55 +83,20 @@ extension SDKSynchronizerClient: DependencyKey {
},
rewind: { synchronizer.rewind($0) },
getAllTransactions: { accountUUID in
guard let accountUUID else {
return []
}
let clearedTransactions = try await synchronizer.allTransactions()
let clearedTransactions = try await synchronizer.allTransactions().compactMap { rawTransaction in
rawTransaction.accountUUID == accountUUID ? rawTransaction : nil
}
var clearedTxs: [TransactionState] = []
let latestBlockHeight = try await SDKSynchronizerClient.latestBlockHeight(synchronizer: synchronizer)
for clearedTransaction in clearedTransactions {
var hasTransparentOutputs = false
let outputs = await synchronizer.getTransactionOutputs(for: clearedTransaction)
for output in outputs {
if case .transaparent = output.pool {
hasTransparentOutputs = true
break
}
}
var transaction = TransactionState.init(
transaction: clearedTransaction,
memos: nil,
hasTransparentOutputs: hasTransparentOutputs,
latestBlockHeight: latestBlockHeight
)
let recipients = await synchronizer.getRecipients(for: clearedTransaction)
let addresses = recipients.compactMap {
if case let .address(address) = $0 {
return address
} else {
return nil
}
}
transaction.rawID = clearedTransaction.rawID
transaction.zAddress = addresses.first?.stringEncoded
if let someAddress = addresses.first,
case .transparent = someAddress {
transaction.isTransparentRecipient = true
}
clearedTxs.append(transaction)
}
return clearedTxs
return try await SDKSynchronizerClient.transactionStatesFromZcashTransactions(
accountUUID: accountUUID,
zcashTransactions: clearedTransactions,
synchronizer: synchronizer
)
},
transactionStatesFromZcashTransactions: { accountUUID, zcashTransactions in
try await SDKSynchronizerClient.transactionStatesFromZcashTransactions(
accountUUID: accountUUID,
zcashTransactions: zcashTransactions,
synchronizer: synchronizer
)
},
getMemos: { try await synchronizer.getMemos(for: $0) },
getUnifiedAddress: { try await synchronizer.getUnifiedAddress(accountUUID: $0) },
@ -325,3 +290,61 @@ private extension SDKSynchronizerClient {
return latestBlockHeight
}
}
extension SDKSynchronizerClient {
static func transactionStatesFromZcashTransactions(
accountUUID: AccountUUID?,
zcashTransactions: [ZcashTransaction.Overview],
synchronizer: SDKSynchronizer
) async throws -> [TransactionState] {
guard let accountUUID else {
return []
}
let clearedTransactions = zcashTransactions.compactMap { rawTransaction in
rawTransaction.accountUUID == accountUUID ? rawTransaction : nil
}
var clearedTxs: [TransactionState] = []
let latestBlockHeight = try await SDKSynchronizerClient.latestBlockHeight(synchronizer: synchronizer)
for clearedTransaction in clearedTransactions {
var hasTransparentOutputs = false
let outputs = await synchronizer.getTransactionOutputs(for: clearedTransaction)
for output in outputs {
if case .transaparent = output.pool {
hasTransparentOutputs = true
break
}
}
var transaction = TransactionState.init(
transaction: clearedTransaction,
memos: nil,
hasTransparentOutputs: hasTransparentOutputs,
latestBlockHeight: latestBlockHeight
)
let recipients = await synchronizer.getRecipients(for: clearedTransaction)
let addresses = recipients.compactMap {
if case let .address(address) = $0 {
return address
} else {
return nil
}
}
transaction.rawID = clearedTransaction.rawID
transaction.zAddress = addresses.first?.stringEncoded
if let someAddress = addresses.first,
case .transparent = someAddress {
transaction.isTransparentRecipient = true
}
clearedTxs.append(transaction)
}
return clearedTxs
}
}

View File

@ -27,6 +27,7 @@ extension SDKSynchronizerClient: TestDependencyKey {
importAccount: unimplemented("\(Self.self).importAccount", placeholder: nil),
rewind: unimplemented("\(Self.self).rewind", placeholder: Fail(error: "Error").eraseToAnyPublisher()),
getAllTransactions: unimplemented("\(Self.self).getAllTransactions", placeholder: []),
transactionStatesFromZcashTransactions: unimplemented("\(Self.self).transactionStatesFromZcashTransactions", placeholder: []),
getMemos: unimplemented("\(Self.self).getMemos", placeholder: []),
getUnifiedAddress: unimplemented("\(Self.self).getUnifiedAddress", placeholder: nil),
getTransparentAddress: unimplemented("\(Self.self).getTransparentAddress", placeholder: nil),
@ -63,6 +64,7 @@ extension SDKSynchronizerClient {
importAccount: { _, _, _, _, _, _ in nil },
rewind: { _ in Empty<Void, Error>().eraseToAnyPublisher() },
getAllTransactions: { _ in [] },
transactionStatesFromZcashTransactions: { _, _ in [] },
getMemos: { _ in [] },
getUnifiedAddress: { _ in nil },
getTransparentAddress: { _ in nil },
@ -152,6 +154,7 @@ extension SDKSynchronizerClient {
return clearedTransactions
},
transactionStatesFromZcashTransactions: @escaping (AccountUUID?, [ZcashTransaction.Overview]) async throws -> [TransactionState] = { _, _ in [] },
getMemos: @escaping (_ rawID: Data) -> [Memo] = { _ in [] },
getUnifiedAddress: @escaping (_ account: AccountUUID) -> UnifiedAddress? = { _ in
// swiftlint:disable force_try
@ -203,6 +206,7 @@ extension SDKSynchronizerClient {
importAccount: importAccount,
rewind: rewind,
getAllTransactions: getAllTransactions,
transactionStatesFromZcashTransactions: transactionStatesFromZcashTransactions,
getMemos: getMemos,
getUnifiedAddress: getUnifiedAddress,
getTransparentAddress: getTransparentAddress,

View File

@ -51,7 +51,7 @@ public struct HomeView: View {
}
VStack(spacing: 0) {
if store.transactionListState.transactionList.isEmpty && !store.transactionListState.isInvalidated {
if store.transactionListState.transactions.isEmpty && !store.transactionListState.isInvalidated {
noTransactionsView()
} else {
transactionsView()
@ -99,7 +99,7 @@ public struct HomeView: View {
Spacer()
if store.transactionListState.transactionList.count > TransactionList.Constants.homePageTransactionsCount {
if store.transactionListState.transactions.count > TransactionList.Constants.homePageTransactionsCount {
Button {
store.send(.seeAllTransactionsTapped)
} label: {
@ -189,7 +189,7 @@ struct HomeView_Previews: PreviewProvider {
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing"
),
transactionListState: .placeholder,
transactionListState: .initial,
walletBalancesState: .initial,
walletConfig: .initial
)

View File

@ -0,0 +1,47 @@
//
// RootAddressBook.swift
// modules
//
// Created by Lukáš Korba on 31.01.2025.
//
import Combine
import ComposableArchitecture
import Foundation
import ZcashLightClientKit
import Generated
import Models
extension Root {
public func addressBookReduce() -> Reduce<Root.State, Root.Action> {
Reduce { state, action in
switch action {
case .loadContacts:
if let account = state.zashiWalletAccount {
do {
let result = try addressBook.allLocalContacts(account.account)
let abContacts = result.contacts
if result.remoteStoreResult == .failure {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
return .send(.contactsLoaded(abContacts))
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
return .none
}
}
return .none
case .contactsLoaded(let abContacts):
state.$addressBookContacts.withLock { $0 = abContacts }
return .none
case .resetZashiSucceeded:
state.$addressBookContacts.withLock { $0 = .empty }
return .none
default: return .none
}
}
}
}

View File

@ -167,7 +167,7 @@ extension Root {
case .tabs, .initialization, .onboarding, .updateStateAfterConfigUpdate, .alert, .phraseDisplay, .synchronizerStateChanged,
.welcome, .binding, .resetZashiSDKFailed, .resetZashiSDKSucceeded, .resetZashiKeychainFailed, .resetZashiKeychainRequest, .resetZashiFinishProcessing, .resetZashiKeychainFailedWithCorruptedData, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog,
.notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated, .batteryStateChanged, .cancelAllRunningEffects, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .osStatusError:
.notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated, .batteryStateChanged, .cancelAllRunningEffects, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .osStatusError, .observeTransactions, .foundTransactions, .minedTransaction, .fetchTransactionsForTheSelectedAccount, .fetchedTransactions, .noChangeInTransactions, .loadContacts, .contactsLoaded:
return .none
}
}

View File

@ -373,7 +373,8 @@ extension Root {
.map(Root.Action.batteryStateChanged)
}
.cancellable(id: CancelBatteryStateId, cancelInFlight: true),
.send(.batteryStateChanged(nil))
.send(.batteryStateChanged(nil)),
.send(.observeTransactions)
)
case .initialization(.loadedWalletAccounts(let walletAccounts)):
@ -387,46 +388,44 @@ extension Root {
}
}
}
return .none
return .send(.loadContacts)
case .initialization(.checkBackupPhraseValidation):
let storedWallet: StoredWallet
do {
let storedWallet: StoredWallet
do {
storedWallet = try walletStorage.exportWallet()
} catch {
return .send(.destination(.updateDestination(.osStatusError)))
}
var landingDestination = Root.DestinationState.Destination.tabs
if !storedWallet.hasUserPassedPhraseBackupTest {
let phraseWords = mnemonic.asWords(storedWallet.seedPhrase.value())
let recoveryPhrase = RecoveryPhrase(words: phraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseDisplayState.birthday = storedWallet.birthday
if let value = storedWallet.birthday?.value() {
let latestBlock = numberFormatter.string(NSDecimalNumber(value: value))
state.phraseDisplayState.birthdayValue = "\(String(describing: latestBlock ?? ""))"
}
landingDestination = .phraseDisplay
}
state.appInitializationState = .initialized
let isAtDeeplinkWarningScreen = state.destinationState.destination == .deeplinkWarning
return .run { [landingDestination] send in
if landingDestination == .tabs {
await send(.tabs(.home(.transactionList(.onAppear))))
}
try await mainQueue.sleep(for: .seconds(0.5))
if !isAtDeeplinkWarningScreen {
await send(.destination(.updateDestination(landingDestination)))
}
}
.cancellable(id: CancelId, cancelInFlight: true)
storedWallet = try walletStorage.exportWallet()
} catch {
return .send(.destination(.updateDestination(.osStatusError)))
}
var landingDestination = Root.DestinationState.Destination.tabs
if !storedWallet.hasUserPassedPhraseBackupTest {
let phraseWords = mnemonic.asWords(storedWallet.seedPhrase.value())
let recoveryPhrase = RecoveryPhrase(words: phraseWords.map { $0.redacted })
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseDisplayState.birthday = storedWallet.birthday
if let value = storedWallet.birthday?.value() {
let latestBlock = numberFormatter.string(NSDecimalNumber(value: value))
state.phraseDisplayState.birthdayValue = "\(String(describing: latestBlock ?? ""))"
}
landingDestination = .phraseDisplay
}
state.appInitializationState = .initialized
let isAtDeeplinkWarningScreen = state.destinationState.destination == .deeplinkWarning
return .run { [landingDestination] send in
if landingDestination == .tabs {
await send(.tabs(.home(.transactionList(.onAppear))))
}
try await mainQueue.sleep(for: .seconds(0.5))
if !isAtDeeplinkWarningScreen {
await send(.destination(.updateDestination(landingDestination)))
}
}
.cancellable(id: CancelId, cancelInFlight: true)
case .initialization(.resetZashiRequest):
state.alert = AlertState.wipeRequest()
return .none
@ -458,6 +457,8 @@ extension Root {
state.$selectedWalletAccount.withLock { $0 = nil }
state.$walletAccounts.withLock { $0 = [] }
state.$zashiWalletAccount.withLock { $0 = nil }
state.$transactionMemos.withLock { $0 = [:] }
return .send(.resetZashiKeychainRequest)
case .resetZashiKeychainRequest:
@ -636,7 +637,7 @@ extension Root {
case .tabs, .destination, .onboarding, .phraseDisplay, .notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated,
.welcome, .binding, .debug, .exportLogs, .alert, .splashFinished, .splashRemovalRequested,
.confirmationDialog, .batteryStateChanged, .cancelAllRunningEffects, .flexaOnTransactionRequest, .flexaTransactionFailed, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .deeplinkWarning, .osStatusError:
.confirmationDialog, .batteryStateChanged, .cancelAllRunningEffects, .flexaOnTransactionRequest, .flexaTransactionFailed, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted, .deeplinkWarning, .osStatusError, .observeTransactions, .foundTransactions, .minedTransaction, .fetchTransactionsForTheSelectedAccount, .fetchedTransactions, .noChangeInTransactions, .loadContacts, .contactsLoaded:
return .none
}
}

View File

@ -32,6 +32,7 @@ import LocalAuthenticationHandler
import DeeplinkWarning
import URIParser
import OSStatusError
import AddressBookClient
@Reducer
public struct Root {
@ -50,8 +51,12 @@ public struct Root {
@ObservableState
public struct State: Equatable {
public var CancelEventId = UUID()
public var CancelStateId = UUID()
public var addressBookBinding: Bool = false
public var addressBookContactBinding: Bool = false
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
public var addressBookState: AddressBook.State
@Presents public var alert: AlertState<Action>?
public var appInitializationState: InitializationState = .uninitialized
@ -77,6 +82,8 @@ public struct Root {
public var serverSetupViewBinding: Bool = false
public var splashAppeared = false
public var tabsState: Tabs.State
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
@Shared(.inMemory(.transactionMemos)) public var transactionMemos: [String: [String]] = [:]
@Shared(.inMemory(.walletAccounts)) public var walletAccounts: [WalletAccount] = []
public var walletConfig: WalletConfig
@Shared(.inMemory(.walletStatus)) public var walletStatus: WalletStatus = .none
@ -162,8 +169,21 @@ public struct Root {
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(Welcome.Action)
// Transactions
case observeTransactions
case foundTransactions([ZcashTransaction.Overview])
case minedTransaction(ZcashTransaction.Overview)
case fetchTransactionsForTheSelectedAccount
case fetchedTransactions([TransactionState])
case noChangeInTransactions
// Address Book
case loadContacts
case contactsLoaded(AddressBookContacts)
}
@Dependency(\.addressBook) var addressBook
@Dependency(\.autolockHandler) var autolockHandler
@Dependency(\.crashReporter) var crashReporter
@Dependency(\.databaseFiles) var databaseFiles
@ -235,6 +255,10 @@ public struct Root {
destinationReduce()
debugReduce()
transactionsReduce()
addressBookReduce()
}
public var body: some Reducer<State, Action> {

View File

@ -0,0 +1,95 @@
//
// RootTransactions.swift
// modules
//
// Created by Lukáš Korba on 29.01.2025.
//
import Combine
import ComposableArchitecture
import Foundation
import ZcashLightClientKit
import Generated
import Models
extension Root {
public func transactionsReduce() -> Reduce<Root.State, Root.Action> {
Reduce { state, action in
switch action {
case .observeTransactions:
return .merge(
.publisher {
sdkSynchronizer.eventStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.compactMap {
if case SynchronizerEvent.foundTransactions(let transactions, let range) = $0 {
return Root.Action.foundTransactions(transactions)
} else if case SynchronizerEvent.minedTransaction(let transaction) = $0 {
return Root.Action.minedTransaction(transaction)
}
return nil
}
}
.cancellable(id: state.CancelEventId, cancelInFlight: true),
.publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map {
if $0.syncStatus == .upToDate {
return Root.Action.fetchTransactionsForTheSelectedAccount
}
return Root.Action.noChangeInTransactions
}
}
.cancellable(id: state.CancelStateId, cancelInFlight: true),
.send(.fetchTransactionsForTheSelectedAccount)
)
case .noChangeInTransactions:
return .none
case .foundTransactions(let transactions):
// v1
return .send(.fetchTransactionsForTheSelectedAccount)
case .minedTransaction(let transaction):
// v1
return .send(.fetchTransactionsForTheSelectedAccount)
case .fetchTransactionsForTheSelectedAccount:
guard let accountUUID = state.selectedWalletAccount?.id else {
return .none
}
return .run { send in
if let transactions = try? await sdkSynchronizer.getAllTransactions(accountUUID) {
await send(.fetchedTransactions(transactions))
}
}
case .fetchedTransactions(let transactions):
let mempoolHeight = sdkSynchronizer.latestState().latestBlockHeight + 1
let sortedTransactions = transactions
.sorted(by: { lhs, rhs in
lhs.transactionListHeight(mempoolHeight) > rhs.transactionListHeight(mempoolHeight)
})
state.$transactions.withLock {
$0 = IdentifiedArrayOf<TransactionState>(uniqueElements: sortedTransactions)
}
return .none
// MARK: - External Signals
case .tabs(.walletAccountTapped):
return .send(.fetchTransactionsForTheSelectedAccount)
case .resetZashiSucceeded:
state.$transactions.withLock { $0 = [] }
return .none
default: return .none
}
}
}
}

View File

@ -169,7 +169,6 @@ public struct SendConfirmation {
case binding(BindingAction<SendConfirmation.State>)
case closeTapped
case confirmWithKeystoneTapped
case fetchedABContacts(AddressBookContacts)
case getSignatureTapped
case goBackPressed
case goBackPressedFromRequestZec
@ -192,7 +191,6 @@ public struct SendConfirmation {
case shareFinished
case showHideButtonTapped
case transactionDetails(TransactionDetails.Action)
case transactionResultReady
case updateDestination(State.Destination?)
case updateFailedData(Int, String, String)
case updateResult(State.Result?)
@ -258,24 +256,6 @@ public struct SendConfirmation {
state.isTransparentAddress = derivationTool.isTransparentAddress(state.address, zcashSDKEnvironment.network.networkType)
state.canSendMail = MFMailComposeViewController.canSendMail()
state.alias = nil
if let account = state.zashiWalletAccount {
do {
let result = try addressBook.allLocalContacts(account.account)
let abContacts = result.contacts
if result.remoteStoreResult == .failure {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
return .send(.fetchedABContacts(abContacts))
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
return .none
}
}
return .none
case .fetchedABContacts(let abContacts):
state.$addressBookContacts.withLock { $0 = abContacts }
state.alias = nil
for contact in state.addressBookContacts.contacts {
if contact.id == state.address {
state.alias = contact.name
@ -283,7 +263,7 @@ public struct SendConfirmation {
}
}
return .none
case .alert(.presented(let action)):
return .send(action)
@ -350,8 +330,6 @@ public struct SendConfirmation {
let result = try await sdkSynchronizer.createProposedTransactions(proposal, spendingKey)
await send(.transactionResultReady)
switch result {
case .grpcFailure(let txIds):
await send(.updateTxIdToExpand(txIds.last))
@ -611,7 +589,6 @@ public struct SendConfirmation {
let result = try await sdkSynchronizer.createTransactionFromPCZT(pcztWithProofs, pcztWithSigs)
await send(.resetPCZTs)
await send(.transactionResultReady)
switch result {
case .grpcFailure(let txIds):
@ -659,10 +636,7 @@ public struct SendConfirmation {
state.pcztToShare = nil
state.proposal = nil
return .none
case .transactionResultReady:
return .none
case .addressBook:
return .none

View File

@ -204,7 +204,6 @@ public struct SendFlow {
case currencyUpdated(RedactableString)
case dismissAddressBookHint
case exchangeRateSetupChanged
case fetchedABContacts(AddressBookContacts)
case memo(MessageEditor.Action)
case onAppear
case onDisapear
@ -252,25 +251,8 @@ public struct SendFlow {
guard let account = state.zashiWalletAccount else {
return .send(.exchangeRateSetupChanged)
}
do {
let result = try addressBook.allLocalContacts(account.account)
let abContacts = result.contacts
if result.remoteStoreResult == .failure {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
return .merge(
.send(.exchangeRateSetupChanged),
.send(.fetchedABContacts(abContacts))
)
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
return .send(.exchangeRateSetupChanged)
}
return .send(.exchangeRateSetupChanged)
case .fetchedABContacts(let abContacts):
state.$addressBookContacts.withLock { $0 = abContacts }
return .none
case .onDisapear:
return .cancel(id: state.cancelId)

View File

@ -134,6 +134,8 @@ public struct Tabs {
public var stackDestinationTransactionsHPBindingsAlive = 0
public var textToSelect = ""
public var transactionDetailsState: TransactionDetails.State = .initial
@Shared(.inMemory(.transactionMemos)) public var transactionMemos: [String: [String]] = [:]
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
public var transactionsManagerState: TransactionsManager.State = .initial
@Shared(.inMemory(.walletAccounts)) public var walletAccounts: [WalletAccount] = []
public var zecKeyboardState: ZecKeyboard.State
@ -307,8 +309,7 @@ public struct Tabs {
return .concatenate(
.send(.home(.walletBalances(.updateBalances))),
.send(.send(.walletBalances(.updateBalances))),
.send(.balanceBreakdown(.walletBalances(.updateBalances))),
.send(.home(.transactionList(.foundTransactions)))
.send(.balanceBreakdown(.walletBalances(.updateBalances)))
)
case .addKeystoneHWWallet(.continueTapped):
@ -474,13 +475,10 @@ public struct Tabs {
state.selectedTab = .send
return .none
case .sendConfirmation(.updateTxIdToExpand):
return .send(.home(.transactionList(.synchronizerStateChanged(.upToDate))))
case .sendConfirmation(.viewTransactionTapped):
if let txid = state.sendConfirmationState.txIdToExpand {
if let index = state.homeState.transactionListState.transactionList.index(id: txid) {
state.sendConfirmationState.transactionDetailsState.transaction = state.homeState.transactionListState.transactionList[index]
if let index = state.transactions.index(id: txid) {
state.sendConfirmationState.transactionDetailsState.transaction = state.transactions[index]
state.sendConfirmationState.transactionDetailsState.isCloseButtonRequired = true
return .send(.sendConfirmation(.updateStackDestinationTransactions(.details)))
}
@ -562,9 +560,6 @@ public struct Tabs {
.send(.sendConfirmation(.updateStackDestination(nil)))
)
case.sendConfirmation(.transactionResultReady):
return .send(.home(.transactionList(.foundTransactions)))
case .sendConfirmation:
return .none
@ -661,39 +656,19 @@ public struct Tabs {
return .none
case .home(.transactionList(.transactionTapped(let txId))):
if let index = state.homeState.transactionListState.transactionList.index(id: txId) {
let transaction = state.homeState.transactionListState.transactionList[index]
// update of the unread state
if !transaction.isSpending
&& !transaction.isMarkedAsRead
&& transaction.isUnread {
do {
try readTransactionsStorage.markIdAsRead(transaction.id.redacted)
state.homeState.transactionListState.transactionList[index].isMarkedAsRead = true
} catch { }
}
state.transactionDetailsState.transaction = transaction
if let index = state.transactions.index(id: txId) {
state.transactionDetailsState.transaction = state.transactions[index]
return .send(.updateStackDestinationTransactionsHP(.details))
}
return .none
case .transactionsManager(.transactionTapped(let txId)):
if let index = state.transactionsManagerState.transactionList.index(id: txId) {
let transaction = state.transactionsManagerState.transactionList[index]
// update of the unread state
if !transaction.isSpending
&& !transaction.isMarkedAsRead
&& transaction.isUnread {
do {
try readTransactionsStorage.markIdAsRead(transaction.id.redacted)
state.transactionsManagerState.transactionList[index].isMarkedAsRead = true
} catch { }
}
state.transactionDetailsState.transaction = transaction
if let index = state.transactions.index(id: txId) {
state.transactionDetailsState.transaction = state.transactions[index]
return .send(.updateStackDestinationTransactions(.details))
}
return .none
case .transactionDetails(.saveAddressTapped):
state.addressBookState.address = state.transactionDetailsState.transaction.address
state.addressBookState.originalAddress = state.addressBookState.address
@ -710,7 +685,7 @@ public struct Tabs {
state.stackDestinationTransactions = nil
state.stackDestinationTransactionsHP = nil
state.sendState.address = state.transactionDetailsState.transaction.address.redacted
state.sendState.memoState.text = state.transactionDetailsState.transaction.textMemos?.first ?? ""
state.sendState.memoState.text = state.transactionMemos[state.transactionDetailsState.transaction.id]?.first ?? ""
let zecAmount = state.transactionDetailsState.transaction.zecAmount.decimalString().redacted
return .run { send in
await send(.send(.zecAmountUpdated(zecAmount)))

View File

@ -24,6 +24,8 @@ import UserMetadataProvider
public struct TransactionDetails {
@ObservableState
public struct State: Equatable {
public var CancelId = UUID()
enum Constants {
static let messageExpandThreshold: Int = 130
static let annotationMaxLength: Int = 90
@ -39,15 +41,18 @@ public struct TransactionDetails {
public var areMessagesResolved = false
public var alias: String?
public var areDetailsExpanded = false
public var isEditMode = false
public var isBookmarked = false
public var isCloseButtonRequired = false
public var isEditMode = false
public var isMined = false
@Shared(.appStorage(.sensitiveContent)) var isSensitiveContentHidden = false
public var messageStates: [MessageState] = []
public var annotation = ""
public var annotationOrigin = ""
@Shared(.inMemory(.toast)) public var toast: Toast.Edge? = nil
public var transaction: TransactionState
@Shared(.inMemory(.transactionMemos)) public var transactionMemos: [String: [String]] = [:]
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
public var annotationRequest = false
@Shared(.inMemory(.zashiWalletAccount)) public var zashiWalletAccount: WalletAccount? = nil
@ -56,11 +61,7 @@ public struct TransactionDetails {
}
public var memos: [String] {
if let memos = transaction.memos {
return memos.compactMap { $0.toString() }
}
return []
transactionMemos[transaction.id] ?? []
}
public init(
@ -77,17 +78,19 @@ public struct TransactionDetails {
case bookmarkTapped
case closeDetailTapped
case deleteNoteTapped
case fetchedABContacts(AddressBookContacts)
case memosLoaded([Memo])
case messageTapped(Int)
case noteButtonTapped
case onAppear
case onDisappear
case observeTransactionChange
case resolveMemos
case saveAddressTapped
case saveNoteTapped
case sendAgainTapped
case sentToRowTapped
case transactionIdTapped
case transactionsUpdated
}
@Dependency(\.addressBook) var addressBook
@ -106,28 +109,52 @@ public struct TransactionDetails {
state.areDetailsExpanded = false
state.messageStates = []
state.alias = nil
for contact in state.addressBookContacts.contacts {
if contact.id == state.transaction.address {
state.alias = contact.name
break
}
}
state.areMessagesResolved = false
state.isMined = state.transaction.minedHeight != nil
state.isBookmarked = userMetadataProvider.isBookmarked(state.transaction.id)
state.annotation = userMetadataProvider.annotationFor(state.transaction.id) ?? ""
state.annotationOrigin = state.annotation
if let account = state.zashiWalletAccount {
do {
let result = try addressBook.allLocalContacts(account.account)
let abContacts = result.contacts
if result.remoteStoreResult == .failure {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
return .merge(
.send(.resolveMemos),
.send(.fetchedABContacts(abContacts))
)
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
return .send(.resolveMemos)
state.areMessagesResolved = !state.memos.isEmpty
if state.memos.isEmpty {
return .merge(
.send(.resolveMemos),
.send(.observeTransactionChange)
)
}
return .send(.observeTransactionChange)
case .onDisappear:
return .cancel(id: state.CancelId)
case .observeTransactionChange:
if !state.isMined {
return .publisher {
state.$transactions.publisher
.map { _ in
TransactionDetails.Action.transactionsUpdated
}
}
.cancellable(id: state.CancelId, cancelInFlight: true)
}
return .none
case .transactionsUpdated:
if let index = state.transactions.index(id: state.transaction.id) {
let transaction = state.transactions[index]
if !state.isMined && transaction.minedHeight != nil {
state.transaction = transaction
state.isMined = true
return .cancel(id: state.CancelId)
}
}
return .send(.resolveMemos)
return .none
case .binding(\.annotation):
if state.annotation.count > TransactionDetails.State.Constants.annotationMaxLength {
state.annotation = String(state.annotation.prefix(TransactionDetails.State.Constants.annotationMaxLength))
@ -164,23 +191,14 @@ public struct TransactionDetails {
state.areMessagesResolved = true
return .none
case .fetchedABContacts(let abContacts):
state.$addressBookContacts.withLock { $0 = abContacts }
state.alias = nil
for contact in state.addressBookContacts.contacts {
if contact.id == state.transaction.address {
state.alias = contact.name
break
}
}
return .none
case .memosLoaded(let memos):
state.transaction.memos = memos
state.areMessagesResolved = true
state.$transactionMemos.withLock {
$0[state.transaction.id] = memos.compactMap { $0.toString() }
}
state.messageStates = state.memos.map {
$0.count < State.Constants.messageExpandThreshold ? .short : .longCollapsed
}
state.areMessagesResolved = true
return .none
case .noteButtonTapped:

View File

@ -144,9 +144,8 @@ public struct TransactionDetailsView: View {
bookmarkButton()
}
)
.onAppear {
store.send(.onAppear)
}
.onAppear { store.send(.onAppear) }
.onDisappear { store.send(.onDisappear) }
.sheet(isPresented: $store.annotationRequest) {
annotationContent(store.isEditMode)
}

View File

@ -1,169 +1,49 @@
import ComposableArchitecture
import SwiftUI
import ZcashLightClientKit
import Utils
import Models
import Generated
import Pasteboard
import SDKSynchronizer
import ReadTransactionsStorage
import ZcashSDKEnvironment
import AddressBookClient
import UIComponents
import TransactionDetails
import AddressBook
@Reducer
public struct TransactionList {
public enum Constants {
public static let homePageTransactionsCount = 5
}
private let CancelStateId = UUID()
private let CancelEventId = UUID()
@ObservableState
public struct State: Equatable {
public var CancelId = UUID()
public var isInvalidated = true
public var latestTransactionId = ""
public var latestTransactionList: [TransactionState] = []
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
public var transactionList: IdentifiedArrayOf<TransactionState>
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
public var transactionListHomePage: IdentifiedArrayOf<TransactionState> = []
@Shared(.inMemory(.zashiWalletAccount)) public var zashiWalletAccount: WalletAccount? = nil
public init(
latestTransactionList: [TransactionState] = [],
transactionList: IdentifiedArrayOf<TransactionState>
) {
self.latestTransactionList = latestTransactionList
self.transactionList = transactionList
self.transactionListHomePage = IdentifiedArrayOf<TransactionState>(uniqueElements: transactionList.prefix(Constants.homePageTransactionsCount))
}
public init() { }
}
public enum Action: Equatable {
case foundTransactions
case onAppear
case onDisappear
case synchronizerStateChanged(SyncStatus)
case transactionsUpdated
case transactionTapped(String)
case updateTransactionList([TransactionState])
}
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.readTransactionsStorage) var readTransactionsStorage
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
public init() {}
// swiftlint:disable:next cyclomatic_complexity
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
let selectedAccount = state.selectedWalletAccount
return .merge(
.publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map { TransactionList.Action.synchronizerStateChanged($0.syncStatus) }
}
.cancellable(id: CancelStateId, cancelInFlight: true),
.publisher {
sdkSynchronizer.eventStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.compactMap {
if case SynchronizerEvent.foundTransactions = $0 {
return TransactionList.Action.foundTransactions
}
return nil
}
}
.cancellable(id: CancelEventId, cancelInFlight: true),
.run { send in
guard selectedAccount != nil else { return }
if let transactions = try? await sdkSynchronizer.getAllTransactions(selectedAccount?.id) {
await send(.updateTransactionList(transactions))
return .publisher {
state.$transactions.publisher
.map { _ in
TransactionList.Action.transactionsUpdated
}
}
)
case .onDisappear:
return .concatenate(
.cancel(id: CancelStateId),
.cancel(id: CancelEventId)
)
case .synchronizerStateChanged(.upToDate):
guard let accountUUID = state.selectedWalletAccount?.id else {
return .none
}
return .run { send in
if let transactions = try? await sdkSynchronizer.getAllTransactions(accountUUID) {
await send(.updateTransactionList(transactions))
}
}
case .synchronizerStateChanged:
return .none
case .foundTransactions:
guard let accountUUID = state.selectedWalletAccount?.id else {
return .none
}
return .run { send in
if let transactions = try? await sdkSynchronizer.getAllTransactions(accountUUID) {
await send(.updateTransactionList(transactions))
}
}
case .updateTransactionList(let transactionList):
.cancellable(id: state.CancelId, cancelInFlight: true)
case .transactionsUpdated:
state.isInvalidated = false
// update the list only if there is anything new
guard state.latestTransactionList != transactionList else {
return .none
}
state.latestTransactionList = transactionList
var readIds: [RedactableString: Bool] = [:]
if let ids = try? readTransactionsStorage.readIds() {
readIds = ids
}
let timestamp: TimeInterval = (try? readTransactionsStorage.availabilityTimestamp()) ?? 0
let mempoolHeight = sdkSynchronizer.latestState().latestBlockHeight + 1
let sortedTransactionList = transactionList
.sorted(by: { lhs, rhs in
lhs.transactionListHeight(mempoolHeight) > rhs.transactionListHeight(mempoolHeight)
}).map { transaction in
var copiedTransaction = transaction
// update the expanded states
if let index = state.transactionList.index(id: transaction.id) {
copiedTransaction.rawID = state.transactionList[index].rawID
copiedTransaction.memos = state.transactionList[index].memos
}
// update the read/unread state
if !transaction.isSpending {
if let tsTimestamp = copiedTransaction.timestamp, tsTimestamp > timestamp {
copiedTransaction.isMarkedAsRead = readIds[copiedTransaction.id.redacted] ?? false
} else {
copiedTransaction.isMarkedAsRead = true
}
}
return copiedTransaction
}
state.transactionList = IdentifiedArrayOf(uniqueElements: sortedTransactionList)
state.transactionListHomePage = IdentifiedArrayOf(uniqueElements: sortedTransactionList.prefix(Constants.homePageTransactionsCount))
state.transactionListHomePage = IdentifiedArrayOf(uniqueElements: state.transactions.prefix(Constants.homePageTransactionsCount))
state.latestTransactionId = state.transactionListHomePage.last?.id ?? ""
return .none
case .transactionTapped:

View File

@ -9,7 +9,7 @@ import AddressBook
public struct TransactionListView: View {
let store: StoreOf<TransactionList>
let tokenName: String
public init(store: StoreOf<TransactionList>, tokenName: String) {
self.store = store
self.tokenName = tokenName
@ -48,11 +48,10 @@ public struct TransactionListView: View {
.listRowSeparator(.hidden)
}
}
.disabled(store.transactionList.isEmpty)
.disabled(store.transactions.isEmpty)
.applyScreenBackground()
.listStyle(.plain)
.onAppear { store.send(.onAppear) }
.onDisappear(perform: { store.send(.onDisappear) })
}
}
}
@ -61,7 +60,7 @@ public struct TransactionListView: View {
#Preview {
NavigationView {
TransactionListView(store: .placeholder, tokenName: "ZEC")
TransactionListView(store: .initial, tokenName: "ZEC")
.preferredColorScheme(.light)
}
}
@ -69,47 +68,17 @@ public struct TransactionListView: View {
// MARK: Placeholders
extension TransactionList.State {
public static var placeholder: Self {
.init(transactionList: .mocked)
}
public static var initial: Self {
.init(transactionList: [])
.init()
}
}
extension StoreOf<TransactionList> {
public static var placeholder: Store<TransactionList.State, TransactionList.Action> {
public static var initial: Store<TransactionList.State, TransactionList.Action> {
Store(
initialState: .placeholder
initialState: .initial
) {
TransactionList()
.dependency(\.zcashSDKEnvironment, .testnet)
}
}
}
extension IdentifiedArrayOf where Element == TransactionState {
public static var placeholder: IdentifiedArrayOf<TransactionState> {
.init(
uniqueElements: (0..<30).map {
TransactionState(
fee: Zatoshi(10),
id: String($0),
status: .paid,
timestamp: 1234567,
zecAmount: Zatoshi(25)
)
}
)
}
public static var mocked: IdentifiedArrayOf<TransactionState> {
.init(
uniqueElements: [
TransactionState.mockedSent,
TransactionState.mockedReceived
]
)
}
}

View File

@ -42,20 +42,18 @@ public struct TransactionsManager {
@ObservableState
public struct State: Equatable {
public var CancelStateId = UUID()
public var CancelEventId = UUID()
public var CancelId = UUID()
public var activeFilters: [Filter] = []
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
public var filteredTransactionsList: IdentifiedArrayOf<TransactionState> = []
public var filtersRequest = false
public var isInvalidated = true
public var latestTransactionList: [TransactionState] = []
public var searchedTransactionsList: IdentifiedArrayOf<TransactionState> = []
public var searchTerm = ""
public var selectedFilters: [Filter] = []
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
public var transactionList: IdentifiedArrayOf<TransactionState>
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
public var transactionSections: [Section] = []
@Shared(.inMemory(.zashiWalletAccount)) public var zashiWalletAccount: WalletAccount? = nil
@ -67,11 +65,7 @@ public struct TransactionsManager {
public var isSentFilterActive: Bool { selectedFilters.contains(.sent) }
public var isUnreadFilterActive: Bool { selectedFilters.contains(.unread) }
public init(
transactionList: IdentifiedArrayOf<TransactionState>
) {
self.transactionList = transactionList
}
public init() { }
}
public enum Action: BindableAction, Equatable {
@ -80,14 +74,11 @@ public struct TransactionsManager {
case binding(BindingAction<TransactionsManager.State>)
case eraseSearchTermTapped
case filterTapped
case foundTransactions
case onAppear
case onDisappear
case resetFiltersTapped
case synchronizerStateChanged(SyncStatus)
case toggleFilter(Filter)
case transactionsUpdated
case transactionTapped(String)
case updateTransactionList([TransactionState])
case updateTransactionPeriods
case updateTransactionsAccordingToFilters
case updateTransactionsAccordingToSearchTerm
@ -96,7 +87,6 @@ public struct TransactionsManager {
@Dependency(\.addressBook) var addressBook
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.numberFormatter) var numberFormatter
@Dependency(\.readTransactionsStorage) var readTransactionsStorage
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.userMetadataProvider) var userMetadataProvider
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@ -109,51 +99,14 @@ public struct TransactionsManager {
Reduce { state, action in
switch action {
case .onAppear:
let selectedAccount = state.selectedWalletAccount
if let abAccount = state.zashiWalletAccount {
do {
let result = try addressBook.allLocalContacts(abAccount.account)
let abContacts = result.contacts
if result.remoteStoreResult == .failure {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
return .publisher {
state.$transactions.publisher
.map { _ in
TransactionsManager.Action.transactionsUpdated
}
state.$addressBookContacts.withLock { $0 = abContacts }
} catch {
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
}
}
return .merge(
.publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map { TransactionsManager.Action.synchronizerStateChanged($0.syncStatus) }
}
.cancellable(id: state.CancelStateId, cancelInFlight: true),
.publisher {
sdkSynchronizer.eventStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.compactMap {
if case SynchronizerEvent.foundTransactions = $0 {
return TransactionsManager.Action.foundTransactions
}
return nil
}
}
.cancellable(id: state.CancelEventId, cancelInFlight: true),
.run { send in
guard selectedAccount != nil else { return }
if let transactions = try? await sdkSynchronizer.getAllTransactions(selectedAccount?.id) {
await send(.updateTransactionList(transactions))
}
}
)
case .onDisappear:
return .concatenate(
.cancel(id: state.CancelStateId),
.cancel(id: state.CancelEventId)
)
.cancellable(id: state.CancelId, cancelInFlight: true)
case .binding(\.searchTerm):
return .send(.updateTransactionsAccordingToSearchTerm)
@ -186,93 +139,20 @@ public struct TransactionsManager {
state.selectedFilters.append(filter)
}
return .none
case .synchronizerStateChanged(.upToDate):
guard let accountUUID = state.selectedWalletAccount?.id else {
return .none
}
return .run { send in
if let transactions = try? await sdkSynchronizer.getAllTransactions(accountUUID) {
await send(.updateTransactionList(transactions))
}
}
case .synchronizerStateChanged:
return .none
case .transactionTapped:
return .none
case .foundTransactions:
guard let accountUUID = state.selectedWalletAccount?.id else {
return .none
}
return .run { send in
if let transactions = try? await sdkSynchronizer.getAllTransactions(accountUUID) {
await send(.updateTransactionList(transactions))
}
}
case .updateTransactionList(let transactionList):
case .transactionsUpdated:
state.isInvalidated = false
// update the list only if there is anything new
guard state.latestTransactionList != transactionList else {
return .none
}
state.latestTransactionList = transactionList
var readIds: [RedactableString: Bool] = [:]
if let ids = try? readTransactionsStorage.readIds() {
readIds = ids
}
let timestamp: TimeInterval = (try? readTransactionsStorage.availabilityTimestamp()) ?? 0
let mempoolHeight = sdkSynchronizer.latestState().latestBlockHeight + 1
let sortedTransactionList = transactionList
.sorted(by: { lhs, rhs in
lhs.transactionListHeight(mempoolHeight) > rhs.transactionListHeight(mempoolHeight)
}).map { transaction in
var copiedTransaction = transaction
// update the expanded states
if let index = state.transactionList.index(id: transaction.id) {
copiedTransaction.rawID = state.transactionList[index].rawID
copiedTransaction.memos = state.transactionList[index].memos
}
// update the read/unread state
if !transaction.isSpending {
if let tsTimestamp = copiedTransaction.timestamp, tsTimestamp > timestamp {
copiedTransaction.isMarkedAsRead = readIds[copiedTransaction.id.redacted] ?? false
} else {
copiedTransaction.isMarkedAsRead = true
}
}
// in address book
copiedTransaction.isInAddressBook = false
for contact in state.addressBookContacts.contacts {
if contact.id == transaction.address {
copiedTransaction.isInAddressBook = true
break
}
}
return copiedTransaction
}
state.transactionList = IdentifiedArrayOf(uniqueElements: sortedTransactionList)
return .send(.updateTransactionsAccordingToSearchTerm)
case .updateTransactionsAccordingToSearchTerm:
if !state.searchTerm.isEmpty && state.searchTerm.count >= 2 {
state.searchedTransactionsList.removeAll()
// synchronous search
state.transactionList.forEach { transaction in
state.transactions.forEach { transaction in
if checkSearchTerm(state.searchTerm, transaction: transaction, addressBookContacts: state.addressBookContacts) {
state.searchedTransactionsList.append(transaction)
}
@ -291,13 +171,13 @@ public struct TransactionsManager {
}
}
} else {
state.searchedTransactionsList = state.transactionList
state.searchedTransactionsList = state.transactions
}
return .send(.updateTransactionsAccordingToFilters)
case .asynchronousMemoSearchResult(let txids):
let results = state.transactionList.filter { txids.contains($0.id) }
let results = state.transactions.filter { txids.contains($0.id) }
state.searchedTransactionsList.append(contentsOf: results)
return .send(.updateTransactionsAccordingToFilters)
@ -420,7 +300,7 @@ extension TransactionsManager {
}
// Regex amounts
var input = transaction.totalAmount.decimalString()
var input = transaction.zecAmount.decimalString()
if transaction.isSpending {
input = "-\(input)"

View File

@ -142,12 +142,11 @@ public struct TransactionsManagerView: View {
}
}
}
.disabled(store.transactionList.isEmpty)
.disabled(store.transactions.isEmpty)
.walletStatusPanel()
.applyScreenBackground()
.listStyle(.plain)
.onAppear { store.send(.onAppear) }
.onDisappear(perform: { store.send(.onDisappear) })
.navigationBarItems(trailing: hideBalancesButton())
.sheet(isPresented: $store.filtersRequest) {
filtersContent()
@ -229,5 +228,5 @@ extension TransactionsManager {
// MARK: - Placeholders
extension TransactionsManager.State {
public static let initial = TransactionsManager.State(transactionList: [])
public static let initial = TransactionsManager.State()
}

View File

@ -19,4 +19,6 @@ public extension String {
static let walletAccounts = "sharedStateKey_walletAccounts"
static let selectedWalletAccount = "sharedStateKey_selectedWalletAccount"
static let zashiWalletAccount = "sharedStateKey_zashiWalletAccount"
static let transactions = "sharedStateKey_transactions"
static let transactionMemos = "sharedStateKey_transactionMemos"
}

View File

@ -25,7 +25,6 @@ public struct TransactionState: Equatable, Identifiable {
public var errorMessage: String?
public var expiryHeight: BlockHeight?
public var memoCount: Int
public var memos: [Memo]?
public var minedHeight: BlockHeight?
public var shielded = true
public var zAddress: String?
@ -85,36 +84,6 @@ public struct TransactionState: Equatable, Identifiable {
: Design.Surfaces.bgSecondary.color(colorScheme)
}
public var isUnread: Bool {
// in case memos haven't been loaded yet
// non-nil rawID represents unloaded memos state
if rawID != nil && memoCount > 0 {
return !isMarkedAsRead
}
// there must be memos
guard let memos else { return false }
// it must be a textual one
var textMemoExists = false
for memo in memos {
if case .text = memo {
if let memoText = memo.toString(), !memoText.isEmpty {
textMemoExists = true
break
}
}
}
if !textMemoExists {
return false
}
// and it must be externally marked as read
return !isMarkedAsRead
}
// UI Texts
public var address: String {
zAddress ?? ""
@ -230,29 +199,10 @@ public struct TransactionState: Equatable, Identifiable {
Zatoshi(zecAmount.amount + (fee?.amount ?? 0))
}
public var textMemos: [String]? {
guard let memos else { return nil }
var res: [String] = []
for memo in memos {
if case .text = memo {
guard let memoText = memo.toString(), !memoText.isEmpty else {
continue
}
res.append(memoText)
}
}
return res.isEmpty ? nil : res
}
public init(
errorMessage: String? = nil,
expiryHeight: BlockHeight? = nil,
memoCount: Int = 0,
memos: [Memo]? = nil,
minedHeight: BlockHeight? = nil,
shielded: Bool = true,
zAddress: String? = nil,
@ -269,7 +219,6 @@ public struct TransactionState: Equatable, Identifiable {
self.errorMessage = errorMessage
self.expiryHeight = expiryHeight
self.memoCount = memoCount
self.memos = memos
self.minedHeight = minedHeight
self.shielded = shielded
self.zAddress = zAddress
@ -323,7 +272,6 @@ extension TransactionState {
isTransparentRecipient = false
self.hasTransparentOutputs = hasTransparentOutputs
memoCount = transaction.memoCount
self.memos = memos
// TODO: [#1313] SDK improvements so a client doesn't need to determing if the transaction isPending
// https://github.com/zcash/ZcashLightClientKit/issues/1313
@ -362,7 +310,6 @@ extension TransactionState {
) -> TransactionState {
.init(
expiryHeight: -1,
memos: nil,
minedHeight: -1,
shielded: shielded,
zAddress: nil,
@ -375,7 +322,6 @@ extension TransactionState {
}
public static let mockedSent = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: BlockHeight(1),
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
fee: Zatoshi(10_000),
@ -386,7 +332,6 @@ extension TransactionState {
)
public static let mockedReceived = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: BlockHeight(1),
fee: Zatoshi(10_000),
id: "t1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
@ -396,7 +341,6 @@ extension TransactionState {
)
public static let mockedFailed = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: nil,
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
fee: Zatoshi(10_000),
@ -408,7 +352,6 @@ extension TransactionState {
)
public static let mockedFailedReceive = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: nil,
fee: Zatoshi(10_000),
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
@ -419,7 +362,6 @@ extension TransactionState {
)
public static let mockedSending = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: nil,
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
fee: Zatoshi(10_000),
@ -431,7 +373,6 @@ extension TransactionState {
)
public static let mockedReceiving = TransactionState(
memos: [try! Memo(string: "Hi, pay me and I'll pay you")],
minedHeight: nil,
fee: Zatoshi(10_000),
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",

View File

@ -118,7 +118,7 @@ public struct TransactionRowView: View {
Text(L10n.General.hideBalancesMost)
} else {
Text(transaction.isSpending ? "- " : "")
+ Text(transaction.totalAmount.decimalString())
+ Text(transaction.zecAmount.decimalString())
+ Text(" \(tokenName)")
}
}