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:
parent
21a1ebd3f4
commit
bd456cec80
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue