451 lines
15 KiB
Swift
451 lines
15 KiB
Swift
//
|
|
// TransactionState.swift
|
|
// Zashi
|
|
//
|
|
// Created by Lukáš Korba on 26.04.2022.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import ZcashLightClientKit
|
|
import Generated
|
|
|
|
/// Representation of the transaction on the SDK side, used as a bridge to the TCA wallet side.
|
|
public struct TransactionState: Equatable, Identifiable {
|
|
public enum Status: Equatable {
|
|
case failed
|
|
case paid
|
|
case received
|
|
case receiving
|
|
case sending
|
|
case shielding
|
|
case shielded
|
|
}
|
|
|
|
public var errorMessage: String?
|
|
public var expiryHeight: BlockHeight?
|
|
public var memoCount: Int
|
|
public var minedHeight: BlockHeight?
|
|
public var shielded = true
|
|
public var zAddress: String?
|
|
public var isSentTransaction: Bool
|
|
public var isShieldingTransaction: Bool
|
|
public var isTransparentRecipient: Bool
|
|
|
|
public var fee: Zatoshi?
|
|
public var id: String
|
|
public var status: Status
|
|
public var timestamp: TimeInterval?
|
|
public var zecAmount: Zatoshi
|
|
public var isMarkedAsRead = false
|
|
public var isInAddressBook = false
|
|
public var hasTransparentOutputs = false
|
|
public var totalSpent: Zatoshi?
|
|
public var totalReceived: Zatoshi?
|
|
|
|
public var rawID: Data? = nil
|
|
|
|
// UI Colors
|
|
public func balanceColor(_ colorScheme: ColorScheme) -> Color {
|
|
status == .failed
|
|
? Design.Utility.ErrorRed._600.color(colorScheme)
|
|
: (isSpending || isShieldingTransaction)
|
|
? Design.Utility.ErrorRed._600.color(colorScheme)
|
|
: Asset.Colors.primary.color
|
|
}
|
|
|
|
public func titleColor(_ colorScheme: ColorScheme) -> Color {
|
|
status == .failed
|
|
? Design.Text.error.color(colorScheme)
|
|
: !isSentTransaction
|
|
? Design.Utility.SuccessGreen._700.color(colorScheme)
|
|
: Design.Text.primary.color(colorScheme)
|
|
}
|
|
|
|
public func iconColor(_ colorScheme: ColorScheme) -> Color {
|
|
status == .failed
|
|
? Design.Utility.WarningYellow._500.color(colorScheme)
|
|
: isPending
|
|
? Design.Utility.HyperBlue._500.color(colorScheme)
|
|
: Design.Text.tertiary.color(colorScheme)
|
|
}
|
|
|
|
public func iconGradientStartColor(_ colorScheme: ColorScheme) -> Color {
|
|
status == .failed
|
|
? Design.Utility.WarningYellow._50.color(colorScheme)
|
|
: isPending
|
|
? Design.Utility.HyperBlue._50.color(colorScheme)
|
|
: Design.Surfaces.bgSecondary.color(colorScheme)
|
|
}
|
|
|
|
public func iconGradientEndColor(_ colorScheme: ColorScheme) -> Color {
|
|
status == .failed
|
|
? Design.Utility.WarningYellow._100.color(colorScheme)
|
|
: isPending
|
|
? Design.Utility.HyperBlue._100.color(colorScheme)
|
|
: Design.Surfaces.bgSecondary.color(colorScheme)
|
|
}
|
|
|
|
// UI Texts
|
|
public var address: String {
|
|
zAddress ?? ""
|
|
}
|
|
|
|
public var title: String {
|
|
switch status {
|
|
case .failed:
|
|
// TODO: failed shileded is not covered!
|
|
return isSentTransaction
|
|
? L10n.Transaction.failedSend
|
|
: L10n.Transaction.failedReceive
|
|
case .paid:
|
|
return L10n.Transaction.sent
|
|
case .received:
|
|
return L10n.Transaction.received
|
|
case .receiving:
|
|
return L10n.Transaction.receiving
|
|
case .sending:
|
|
return L10n.Transaction.sending
|
|
case .shielding:
|
|
return L10n.Transaction.shieldingFunds
|
|
case .shielded:
|
|
return L10n.Transaction.shieldedFunds
|
|
}
|
|
}
|
|
|
|
public var dateString: String? {
|
|
guard let timestamp else { return nil }
|
|
|
|
return Date(timeIntervalSince1970: timestamp).asHumanReadable()
|
|
}
|
|
|
|
public var listDateString: String? {
|
|
guard let timestamp else { return nil }
|
|
|
|
let formatter = DateFormatter()
|
|
let date = Date(timeIntervalSince1970: timestamp)
|
|
formatter.dateFormat = "MMM d '\(L10n.Filter.at)' h:mm a"
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
public var listDateYearString: String? {
|
|
guard let timestamp else { return nil }
|
|
|
|
let formatter = DateFormatter()
|
|
let date = Date(timeIntervalSince1970: timestamp)
|
|
formatter.dateFormat = "MMM d, YYYY '\(L10n.Filter.at)' h:mm a"
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
public var daysAgo: String {
|
|
guard let timestamp else { return "" }
|
|
|
|
let transactionDate = Date(timeIntervalSince1970: timestamp)
|
|
|
|
let calendar = Calendar.current
|
|
let startOfToday = calendar.startOfDay(for: Date())
|
|
let startOfGivenDate = calendar.startOfDay(for: transactionDate)
|
|
let components = calendar.dateComponents([.day], from: startOfGivenDate, to: startOfToday)
|
|
|
|
if let daysAgo = components.day {
|
|
if daysAgo == 0 {
|
|
return L10n.Filter.today
|
|
} else if daysAgo == 1 {
|
|
return L10n.Filter.yesterday
|
|
} else if daysAgo < 31 {
|
|
return L10n.Filter.daysAgo(daysAgo)
|
|
} else {
|
|
return listDateString ?? ""
|
|
}
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// Helper flags
|
|
public var isPending: Bool {
|
|
switch status {
|
|
case .failed:
|
|
return false
|
|
case .paid:
|
|
return false
|
|
case .received:
|
|
return false
|
|
case .receiving:
|
|
return true
|
|
case .sending:
|
|
return true
|
|
case .shielded:
|
|
return false
|
|
case .shielding:
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// The purpose of this flag is to help understand if the transaction affected the wallet and a user paid a fee
|
|
public var isSpending: Bool {
|
|
switch status {
|
|
case .paid, .sending:
|
|
return true
|
|
case .received, .receiving:
|
|
return false
|
|
case .shielded, .shielding:
|
|
return false
|
|
case .failed:
|
|
return isSentTransaction
|
|
}
|
|
}
|
|
|
|
// Values
|
|
public var totalAmount: Zatoshi {
|
|
Zatoshi(zecAmount.amount + (fee?.amount ?? 0))
|
|
}
|
|
|
|
public var netValue: String {
|
|
isShieldingTransaction
|
|
? Zatoshi(totalSpent?.amount ?? 0).atLeastThreeDecimalsZashiFormatted()
|
|
: zecAmount.atLeastThreeDecimalsZashiFormatted()
|
|
}
|
|
|
|
public var amountWithoutFee: Zatoshi {
|
|
Zatoshi(zecAmount.amount - (fee?.amount ?? 0))
|
|
}
|
|
|
|
public init(
|
|
errorMessage: String? = nil,
|
|
expiryHeight: BlockHeight? = nil,
|
|
memoCount: Int = 0,
|
|
minedHeight: BlockHeight? = nil,
|
|
shielded: Bool = true,
|
|
zAddress: String? = nil,
|
|
fee: Zatoshi?,
|
|
id: String,
|
|
status: Status,
|
|
timestamp: TimeInterval? = nil,
|
|
zecAmount: Zatoshi,
|
|
isSentTransaction: Bool = false,
|
|
isShieldingTransaction: Bool = false,
|
|
isTransparentRecipient: Bool = false,
|
|
isMarkedAsRead: Bool = false
|
|
) {
|
|
self.errorMessage = errorMessage
|
|
self.expiryHeight = expiryHeight
|
|
self.memoCount = memoCount
|
|
self.minedHeight = minedHeight
|
|
self.shielded = shielded
|
|
self.zAddress = zAddress
|
|
self.fee = fee
|
|
self.id = id
|
|
self.status = status
|
|
self.timestamp = timestamp
|
|
self.zecAmount = zecAmount
|
|
self.isSentTransaction = isSentTransaction
|
|
self.isShieldingTransaction = isShieldingTransaction
|
|
self.isTransparentRecipient = isTransparentRecipient
|
|
self.isMarkedAsRead = isMarkedAsRead
|
|
}
|
|
|
|
public func confirmationsWith(_ latestMinedHeight: BlockHeight?) -> BlockHeight {
|
|
guard let minedHeight, let latestMinedHeight, minedHeight > 0, latestMinedHeight > 0 else {
|
|
return 0
|
|
}
|
|
|
|
return latestMinedHeight - minedHeight
|
|
}
|
|
|
|
public func transactionListHeight(_ mempoolHeight: BlockHeight) -> BlockHeight {
|
|
var tlHeight = mempoolHeight
|
|
|
|
if let minedHeight = minedHeight {
|
|
tlHeight = minedHeight
|
|
} else if let expiredHeight = expiryHeight, expiredHeight > 0 {
|
|
tlHeight = expiredHeight
|
|
}
|
|
|
|
return tlHeight
|
|
}
|
|
}
|
|
|
|
extension TransactionState {
|
|
public init(
|
|
transaction: ZcashTransaction.Overview,
|
|
memos: [Memo]? = nil,
|
|
hasTransparentOutputs: Bool = false,
|
|
latestBlockHeight: BlockHeight?
|
|
) {
|
|
expiryHeight = transaction.expiryHeight
|
|
minedHeight = transaction.minedHeight
|
|
fee = transaction.fee
|
|
id = transaction.rawID.toHexStringTxId()
|
|
timestamp = transaction.blockTime
|
|
isSentTransaction = transaction.isSentTransaction
|
|
isShieldingTransaction = transaction.isShielding
|
|
zecAmount = isSentTransaction ? Zatoshi(-transaction.value.amount) : transaction.value
|
|
isTransparentRecipient = false
|
|
self.hasTransparentOutputs = hasTransparentOutputs
|
|
memoCount = transaction.memoCount
|
|
totalSpent = transaction.totalSpent
|
|
totalReceived = transaction.totalReceived
|
|
|
|
// TODO: [#1313] SDK improvements so a client doesn't need to determing if the transaction isPending
|
|
// https://github.com/zcash/ZcashLightClientKit/issues/1313
|
|
// The only reason why `latestBlockHeight` is provided here is to determine pending
|
|
// state of the transaction. SDK knows the latestBlockHeight so ideally ZcashTransaction.Overview
|
|
// already knows and provides isPending as a bool value.
|
|
// Once SDK's #1313 is done, adopt the SDK and remove latestBlockHeight here.
|
|
var isPending = false
|
|
var isExpired = false
|
|
|
|
if let latestBlockHeight {
|
|
isPending = transaction.isPending(currentHeight: latestBlockHeight)
|
|
if let expiryHeight = transaction.expiryHeight, expiryHeight <= latestBlockHeight && minedHeight == nil {
|
|
isExpired = true
|
|
}
|
|
}
|
|
|
|
// failed check
|
|
if isExpired {
|
|
status = .failed
|
|
} else if isShieldingTransaction {
|
|
status = isPending ? .shielding : .shielded
|
|
} else {
|
|
switch (isSentTransaction, isPending) {
|
|
case (true, true): status = .sending
|
|
case (true, false): status = .paid
|
|
case (false, true): status = .receiving
|
|
case (false, false): status = .received
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Placeholders
|
|
|
|
extension TransactionState {
|
|
public static func placeholder(
|
|
amount: Zatoshi = .zero,
|
|
fee: Zatoshi = .zero,
|
|
shielded: Bool = true,
|
|
status: Status = .received,
|
|
timestamp: TimeInterval = 0.0,
|
|
uuid: String = UUID().debugDescription
|
|
) -> TransactionState {
|
|
.init(
|
|
expiryHeight: -1,
|
|
minedHeight: -1,
|
|
shielded: shielded,
|
|
zAddress: nil,
|
|
fee: fee,
|
|
id: uuid,
|
|
status: status,
|
|
timestamp: timestamp,
|
|
zecAmount: status == .received ? amount : Zatoshi(-amount.amount)
|
|
)
|
|
}
|
|
|
|
public static let mockedSent = TransactionState(
|
|
minedHeight: BlockHeight(1),
|
|
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .paid,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_000_000)
|
|
)
|
|
|
|
public static let mockedReceived = TransactionState(
|
|
minedHeight: BlockHeight(1),
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .received,
|
|
timestamp: 1699292621,
|
|
zecAmount: Zatoshi(25_000_000)
|
|
)
|
|
|
|
public static let mockedFailed = TransactionState(
|
|
minedHeight: nil,
|
|
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .failed,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_108_700),
|
|
isSentTransaction: true
|
|
)
|
|
|
|
public static let mockedFailedReceive = TransactionState(
|
|
minedHeight: nil,
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .failed,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_001_000),
|
|
isSentTransaction: false
|
|
)
|
|
|
|
public static let mockedSending = TransactionState(
|
|
minedHeight: nil,
|
|
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .sending,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_000_000),
|
|
isSentTransaction: true
|
|
)
|
|
|
|
public static let mockedReceiving = TransactionState(
|
|
minedHeight: nil,
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .receiving,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_000_000),
|
|
isSentTransaction: false
|
|
)
|
|
|
|
public static let mockedShielded = TransactionState(
|
|
minedHeight: BlockHeight(1),
|
|
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .shielded,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_000_000),
|
|
isShieldingTransaction: true
|
|
)
|
|
|
|
public static let mockedShieldedExpanded = TransactionState(
|
|
minedHeight: BlockHeight(1),
|
|
zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3",
|
|
fee: Zatoshi(10_000),
|
|
id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja",
|
|
status: .shielded,
|
|
timestamp: 1699290621,
|
|
zecAmount: Zatoshi(25_000_000),
|
|
isShieldingTransaction: true
|
|
)
|
|
}
|
|
|
|
public struct TransactionStateMockHelper {
|
|
public var date: TimeInterval
|
|
public var amount: Zatoshi
|
|
public var shielded = true
|
|
public var status: TransactionState.Status = .received
|
|
public var uuid = ""
|
|
|
|
public init(
|
|
date: TimeInterval,
|
|
amount: Zatoshi,
|
|
shielded: Bool = true,
|
|
status: TransactionState.Status = .received,
|
|
uuid: String = ""
|
|
) {
|
|
self.date = date
|
|
self.amount = amount
|
|
self.shielded = shielded
|
|
self.status = status
|
|
self.uuid = uuid
|
|
}
|
|
}
|