ZcashLightClientKit/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift

236 lines
8.2 KiB
Swift

//
// TransactionEntity.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/14/19.
//
import Foundation
import SQLite
public enum ZcashTransaction {
public struct Overview {
/// Represents the transaction state based on current height of the chain,
/// mined height and expiry height of a transaction.
public enum State {
/// transaction has a `minedHeight` that's greater or equal than
/// `ZcashSDK.defaultStaleTolerance` confirmations.
case confirmed
/// transaction has no `minedHeight` but current known height is less than
/// `expiryHeight`.
case pending
/// transaction has no
case expired
init(currentHeight: BlockHeight,
minedHeight: BlockHeight?,
expiredUnmined: Bool
) {
guard !expiredUnmined else {
self = .expired
return
}
if let minedHeight, (currentHeight - minedHeight) >= ZcashSDK.defaultStaleTolerance {
self = .confirmed
} else if let minedHeight, (currentHeight - minedHeight) < ZcashSDK.defaultStaleTolerance {
self = .pending
} else if minedHeight == nil {
self = .pending
} else {
self = .expired
}
}
}
public let accountId: Int
public let blockTime: TimeInterval?
public let expiryHeight: BlockHeight?
public let fee: Zatoshi?
public let id: Int
public let index: Int?
public var isSentTransaction: Bool { value < Zatoshi(0) }
public let hasChange: Bool
public let memoCount: Int
public let minedHeight: BlockHeight?
public let raw: Data?
public let rawID: Data
public let receivedNoteCount: Int
public let sentNoteCount: Int
public let value: Zatoshi
public let isExpiredUmined: Bool
}
public struct Output {
public enum Pool {
case transaparent
case sapling
case other(Int)
init(rawValue: Int) {
switch rawValue {
case 0:
self = .transaparent
case 2:
self = .sapling
default:
self = .other(rawValue)
}
}
}
public let idTx: Int
public let pool: Pool
public let index: Int
public let fromAccount: Int?
public let recipient: TransactionRecipient
public let value: Zatoshi
public let isChange: Bool
public let memo: Memo?
}
/// Used when fetching blocks from the lightwalletd
struct Fetched {
public let rawID: Data
public let minedHeight: BlockHeight
public let raw: Data
}
}
extension ZcashTransaction.Output {
enum Column {
static let idTx = Expression<Int>("id_tx")
static let pool = Expression<Int>("output_pool")
static let index = Expression<Int>("output_index")
static let toAccount = Expression<Int?>("to_account")
static let fromAccount = Expression<Int?>("from_account")
static let toAddress = Expression<String?>("to_address")
static let value = Expression<Int64>("value")
static let isChange = Expression<Bool>("is_change")
static let memo = Expression<Blob?>("memo")
}
init(row: Row) throws {
do {
idTx = try row.get(Column.idTx)
pool = .init(rawValue: try row.get(Column.pool))
index = try row.get(Column.index)
fromAccount = try row.get(Column.fromAccount)
value = Zatoshi(try row.get(Column.value))
isChange = try row.get(Column.isChange)
if
let outputRecipient = try row.get(Column.toAddress),
let metadata = DerivationTool.getAddressMetadata(outputRecipient)
{
recipient = TransactionRecipient.address(try Recipient(outputRecipient, network: metadata.networkType))
} else if let toAccount = try row.get(Column.toAccount) {
recipient = .internalAccount(UInt32(toAccount))
} else {
throw ZcashError.zcashTransactionOutputInconsistentRecipient
}
if let memoData = try row.get(Column.memo) {
memo = try Memo(bytes: memoData.bytes)
} else {
memo = nil
}
} catch {
throw ZcashError.zcashTransactionOutputInit(error)
}
}
}
extension ZcashTransaction.Overview {
enum Column {
static let accountId = Expression<Int>("account_id")
static let id = Expression<Int>("id_tx")
static let minedHeight = Expression<BlockHeight?>("mined_height")
static let index = Expression<Int?>("tx_index")
static let rawID = Expression<Blob>("txid")
static let expiryHeight = Expression<BlockHeight?>("expiry_height")
static let raw = Expression<Blob?>("raw")
static let value = Expression<Int64>("account_balance_delta")
static let fee = Expression<Int64?>("fee_paid")
static let hasChange = Expression<Bool>("has_change")
static let sentNoteCount = Expression<Int>("sent_note_count")
static let receivedNoteCount = Expression<Int>("received_note_count")
static let memoCount = Expression<Int>("memo_count")
static let blockTime = Expression<Int64?>("block_time")
static let expiredUnmined = Expression<Bool>("expired_unmined")
}
init(row: Row) throws {
do {
self.accountId = try row.get(Column.accountId)
self.expiryHeight = try row.get(Column.expiryHeight)
self.id = try row.get(Column.id)
self.index = try row.get(Column.index)
self.hasChange = try row.get(Column.hasChange)
self.memoCount = try row.get(Column.memoCount)
self.minedHeight = try row.get(Column.minedHeight)
self.rawID = Data(blob: try row.get(Column.rawID))
self.receivedNoteCount = try row.get(Column.receivedNoteCount)
self.sentNoteCount = try row.get(Column.sentNoteCount)
self.value = Zatoshi(try row.get(Column.value))
self.isExpiredUmined = try row.get(Column.expiredUnmined)
if let blockTime = try row.get(Column.blockTime) {
self.blockTime = TimeInterval(blockTime)
} else {
self.blockTime = nil
}
if let fee = try row.get(Column.fee) {
self.fee = Zatoshi(fee)
} else {
self.fee = nil
}
if let raw = try row.get(Column.raw) {
self.raw = Data(blob: raw)
} else {
self.raw = nil
}
} catch {
throw ZcashError.zcashTransactionOverviewInit(error)
}
}
func anchor(network: ZcashNetwork) -> BlockHeight? {
guard let minedHeight = self.minedHeight else { return nil }
if minedHeight != -1 {
return max(minedHeight - ZcashSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
}
guard let expiryHeight = self.expiryHeight else { return nil }
if expiryHeight != -1 {
return max(expiryHeight - ZcashSDK.expiryOffset - ZcashSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
}
return nil
}
}
/// extension to handle pending states
public extension ZcashTransaction.Overview {
func getState(for currentHeight: BlockHeight) -> State {
State(
currentHeight: currentHeight,
minedHeight: minedHeight,
expiredUnmined: self.isExpiredUmined
)
}
func isPending(currentHeight: BlockHeight) -> Bool {
getState(for: currentHeight) == .pending
}
}
/**
Capabilities of an entity that can be uniquely identified by a raw transaction id
*/
public protocol RawIdentifiable {
var rawTransactionId: Data? { get set }
}