Merge pull request #734 from zcash/556_using_transactions_db_views

[#556] Using DB views for transactions instead of raw SQL
This commit is contained in:
Michal Fousek 2023-01-23 12:59:49 +01:00 committed by GitHub
commit 43e00ecb62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 974 additions and 1194 deletions

View File

@ -67,13 +67,15 @@ opt_in_rules:
- toggle_bool
# - trailing_closure # weird in SwiftUI
- unneeded_parentheses_in_closure_argument
- unused_import
- vertical_whitespace_closing_braces
- vertical_whitespace_opening_braces
- weak_delegate
- yoda_condition
- todos
analyzer_rules:
- unused_import
custom_rules:
array_constructor:
name: "Array/Dictionary initializer"

View File

@ -1,24 +1,49 @@
# Unreleased
- [#556] Change data structures which represent transactions.
These data types are gone: `Transaction`, `TransactionEntity`, `ConfirmedTransaction`,
`ConfirmedTransactionEntity`. And these data types were added: `ZcashTransaction.Overview`,
`ZcashTransaction.Received`, `ZcashTransaction.Sent`.
New data structures are very similar to the old ones. Although there many breaking changes.
The APIs of the `SDKSynchronizer` remain unchanged in their behavior. They return different
data types. **When adopting this change, you should check which data types are used by methods
of the `SDKSynchronizer` in your code and change them accordingly.**
New transaction structures no longer have a `memo` property. This responds to the fact that
Zcash transactions can have either none or multiple memos. To get memos for the transaction
the `SDKSynchronizer` has now new methods to fetch those:
- `func getMemos(for transaction: ZcashTransaction.Overview) throws -> [Memo]`,
- `func getMemos(for receivedTransaction: ZcashTransaction.Received) throws -> [Memo]`
- `func getMemos(for sentTransaction: ZcashTransaction.Sent) throws -> [Memo]`
- [#671] Make CompactBlockProcessor Internal.
The CompactBlockProcessor is no longer a public class/API. Any direct access will
end up as a compiler error. Recommended way how to handle things is via `SDKSynchronizer`
from now on. The Demo app has been updated accordingly as well.
- [#657] Change how blocks are downloaded and scanned.
In previous versions, the SDK first downloaded all the blocks and then it
scanned all the blocks. This approach requires a lot of disk space. The SDK now
behaves differently. It downloads a batch of blocks (100 by default), scans those, and
removes those blocks from the disk. And repeats this until all the blocks are processed.
`SyncStatus` was changed. `.downloading`, `.validating`, and `.scanning` symbols
were removed. And the `.scanning` symbol was added. The removed phases of the sync
process are now reported as one phase.
Notifications were also changed similarly. These notifications were
removed: `SDKSynchronizerDownloading`, `SDKSyncronizerValidating`, and `SDKSyncronizerScanning`.
And the `SDKSynchronizerSyncing` notification was added. The added notification replaces
the removed notifications.
- [#677] Add support for wallet wipe into SDK. Add new method `Synchronizer.wipe()`.
- [#663] Foundations for the benchmarking/performance testing in the SDK.
This change presents 2 building blocks for the future automated tests, consisting
This change presents 2 building blocks for the future automated tests, consisting
of a new SDKMetrics interface to control flow of the data in the SDK and
new performance (unit) test measuring synchronization of 100 mainnet blocks.

View File

@ -32,36 +32,43 @@ class TransactionsDataSource: NSObject {
self.synchronizer = synchronizer
}
func load() {
func load() throws {
switch status {
case .pending:
transactions = synchronizer.pendingTransactions.map {
TransactionDetailModel(pendingTransaction: $0)
transactions = try synchronizer.pendingTransactions.map { pendingTransaction in
let defaultFee: Zatoshi = kZcashNetwork.constants.defaultFee(for: pendingTransaction.minedHeight)
let transaction = pendingTransaction.makeTransactionEntity(defaultFee: defaultFee)
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(pendingTransaction: pendingTransaction, memos: memos)
}
case .cleared:
transactions = synchronizer.clearedTransactions.map {
TransactionDetailModel(confirmedTransaction: $0)
transactions = try synchronizer.clearedTransactions.map { transaction in
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(transaction: transaction, memos: memos)
}
case .received:
transactions = synchronizer.receivedTransactions.map {
TransactionDetailModel(confirmedTransaction: $0)
transactions = try synchronizer.receivedTransactions.map { transaction in
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(receivedTransaction: transaction, memos: memos)
}
case .sent:
transactions = synchronizer.sentTransactions.map {
TransactionDetailModel(confirmedTransaction: $0)
transactions = try synchronizer.sentTransactions.map { transaction in
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(sendTransaction: transaction, memos: memos)
}
case .all:
transactions = (
synchronizer.pendingTransactions.map { $0.transactionEntity } +
synchronizer.clearedTransactions.map { $0.transactionEntity }
)
.map { TransactionDetailModel(transaction: $0) }
transactions = try synchronizer.pendingTransactions.map { pendingTransaction in
let defaultFee: Zatoshi = kZcashNetwork.constants.defaultFee(for: pendingTransaction.minedHeight)
let transaction = pendingTransaction.makeTransactionEntity(defaultFee: defaultFee)
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(pendingTransaction: pendingTransaction, memos: memos)
}
transactions += try synchronizer.clearedTransactions.map { transaction in
let memos = try synchronizer.getMemos(for: transaction)
return TransactionDetailModel(transaction: transaction, memos: memos)
}
}
}
func transactionString(_ transcation: TransactionEntity) -> String {
transcation.transactionId.toHexStringTxId()
}
}
// MARK: - UITableViewDataSource

View File

@ -13,7 +13,7 @@ class TransactionsTableViewController: UITableViewController {
var datasource: TransactionsDataSource? {
didSet {
self.tableView.dataSource = datasource
datasource?.load()
try? datasource?.load()
if viewIfLoaded != nil {
self.tableView.reloadData()
}
@ -26,7 +26,7 @@ class TransactionsTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = datasource
datasource?.load()
try? datasource?.load()
self.tableView.reloadData()
}
}

View File

@ -16,7 +16,7 @@ class PaginatedTransactionsViewController: UIViewController {
// swiftlint:disable:next implicitly_unwrapped_optional
var paginatedRepository: PaginatedTransactionRepository!
var transactions: [TransactionEntity] = []
var transactions: [ZcashTransaction.Overview] = []
override func viewDidLoad() {
super.viewDidLoad()
@ -72,7 +72,17 @@ class PaginatedTransactionsViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? TransactionDetailViewController, let row = selectedRow {
destination.model = TransactionDetailModel(transaction: transactions[row])
let transaction = transactions[row]
let memos: [Memo]
do {
memos = try AppDelegate.shared.sharedSynchronizer.getMemos(for: transaction)
} catch {
loggerProxy.warn("Can't load memos \(error)")
memos = []
}
destination.model = TransactionDetailModel(transaction: transaction, memos: memos)
selectedRow = nil
}
}
@ -87,8 +97,8 @@ extension PaginatedTransactionsViewController: PaginatedTableViewDataSource {
let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellIdentifier, for: indexPath)
let transaction = transactions[indexPath.row]
cell.detailTextLabel?.text = transaction.transactionId.toHexStringTxId()
cell.textLabel?.text = transaction.created ?? "No date"
cell.detailTextLabel?.text = transaction.rawID.toHexStringTxId()
cell.textLabel?.text = transaction.blockTime?.description
return cell
}

View File

@ -18,31 +18,41 @@ final class TransactionDetailModel {
var memo: String?
init() {}
init(confirmedTransaction: ConfirmedTransactionEntity) {
self.id = confirmedTransaction.rawTransactionId?.toHexStringTxId()
self.minedHeight = confirmedTransaction.minedHeight.description
self.expiryHeight = confirmedTransaction.expiryHeight?.description
self.created = Date(timeIntervalSince1970: confirmedTransaction.blockTimeInSeconds).description
self.zatoshi = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: confirmedTransaction.value.amount))
if let memoData = confirmedTransaction.memo, let memoString = String(bytes: memoData, encoding: .utf8) {
self.memo = memoString
}
init(sendTransaction transaction: ZcashTransaction.Sent, memos: [Memo]) {
self.id = transaction.rawID?.toHexStringTxId()
self.minedHeight = transaction.minedHeight.description
self.expiryHeight = transaction.expiryHeight?.description
self.created = Date(timeIntervalSince1970: transaction.blockTime).description
self.zatoshi = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: transaction.value.amount))
self.memo = memos.first?.toString()
}
init(pendingTransaction: PendingTransactionEntity) {
init(receivedTransaction transaction: ZcashTransaction.Received, memos: [Memo]) {
self.id = transaction.rawID?.toHexStringTxId()
self.minedHeight = transaction.minedHeight.description
self.expiryHeight = transaction.expiryHeight?.description
self.created = Date(timeIntervalSince1970: transaction.blockTime).description
self.zatoshi = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: transaction.value.amount))
self.memo = memos.first?.toString()
}
init(pendingTransaction: PendingTransactionEntity, memos: [Memo]) {
self.id = pendingTransaction.rawTransactionId?.toHexStringTxId()
self.minedHeight = pendingTransaction.minedHeight.description
self.expiryHeight = pendingTransaction.expiryHeight.description
self.created = Date(timeIntervalSince1970: pendingTransaction.createTime).description
self.zatoshi = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: pendingTransaction.value.amount))
self.memo = memos.first?.toString()
}
init(transaction: TransactionEntity) {
self.id = transaction.transactionId.toHexStringTxId()
self.minedHeight = transaction.minedHeight?.description ?? "no height"
self.expiryHeight = transaction.expiryHeight?.description ?? "no height"
self.created = transaction.created ?? "no date"
init(transaction: ZcashTransaction.Overview, memos: [Memo]) {
self.id = transaction.rawID.toHexStringTxId()
self.minedHeight = transaction.minedHeight?.description
self.expiryHeight = transaction.expiryHeight?.description
self.created = transaction.blockTime?.description
self.zatoshi = "not available in this entity"
self.memo = memos.first?.toString()
}
}

View File

@ -63,7 +63,7 @@ public protocol CompactBlockDownloading {
Gets the transaction for the Id given
- Parameter txId: Data representing the transaction Id
*/
func fetchTransaction(txId: Data) async throws -> TransactionEntity
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched
func fetchUnspentTransactionOutputs(tAddress: String, startHeight: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>
@ -155,7 +155,7 @@ extension CompactBlockDownloader: CompactBlockDownloading {
try self.storage.latestHeight()
}
func fetchTransaction(txId: Data) async throws -> TransactionEntity {
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched {
try await lightwalletService.fetchTransaction(txId: txId)
}
}

View File

@ -15,45 +15,39 @@ extension CompactBlockProcessor {
case txIdNotFound(txId: Data)
}
private func enhance(transaction: TransactionEntity) async throws -> ConfirmedTransactionEntity {
LoggerProxy.debug("Zoom.... Enhance... Tx: \(transaction.transactionId.toHexStringTxId())")
private func enhance(transaction: ZcashTransaction.Overview) async throws -> ZcashTransaction.Overview {
LoggerProxy.debug("Zoom.... Enhance... Tx: \(transaction.rawID.toHexStringTxId())")
let transaction = try await downloader.fetchTransaction(txId: transaction.transactionId)
let fetchedTransaction = try await downloader.fetchTransaction(txId: transaction.rawID)
let transactionID = transaction.transactionId.toHexStringTxId()
let transactionID = fetchedTransaction.rawID.toHexStringTxId()
let block = String(describing: transaction.minedHeight)
LoggerProxy.debug("Decrypting and storing transaction id: \(transactionID) block: \(block)")
guard let rawBytes = transaction.raw?.bytes else {
let error = EnhancementError.noRawData(
message: "Critical Error: transaction id: \(transaction.transactionId.toHexStringTxId()) has no data"
)
LoggerProxy.error("\(error)")
throw error
}
guard let minedHeight = transaction.minedHeight else {
let error = EnhancementError.noRawData(
message: "Critical Error - Attempt to decrypt and store an unmined transaction. Id: \(transaction.transactionId.toHexStringTxId())"
)
LoggerProxy.error("\(error)")
throw error
}
guard rustBackend.decryptAndStoreTransaction(
let decryptionResult = rustBackend.decryptAndStoreTransaction(
dbData: config.dataDb,
txBytes: rawBytes,
minedHeight: Int32(minedHeight),
txBytes: fetchedTransaction.raw.bytes,
minedHeight: Int32(fetchedTransaction.minedHeight),
networkType: config.network.networkType
) else {
)
guard decryptionResult else {
throw EnhancementError.decryptError(
error: rustBackend.lastError() ?? .genericError(message: "`decryptAndStoreTransaction` failed. No message available")
)
}
guard let confirmedTx = try transactionRepository.findConfirmedTransactionBy(rawId: transaction.transactionId) else {
throw EnhancementError.txIdNotFound(txId: transaction.transactionId)
let confirmedTx: ZcashTransaction.Overview
do {
confirmedTx = try transactionRepository.find(rawID: fetchedTransaction.rawID)
} catch {
if let err = error as? TransactionRepositoryError, case .notFound = err {
throw EnhancementError.txIdNotFound(txId: fetchedTransaction.rawID)
} else {
throw error
}
}
return confirmedTx
}
@ -70,7 +64,9 @@ extension CompactBlockProcessor {
// fetch transactions
do {
let startTime = Date()
guard let transactions = try transactionRepository.findTransactions(in: blockRange, limit: Int.max), !transactions.isEmpty else {
let transactions = try transactionRepository.find(in: blockRange, limit: Int.max, kind: .all)
guard !transactions.isEmpty else {
await internalSyncProgress.set(range.upperBound, .latestEnhancedHeight)
LoggerProxy.debug("no transactions detected on range: \(blockRange.printRange)")
return
@ -95,10 +91,13 @@ extension CompactBlockProcessor {
)
)
)
await internalSyncProgress.set(confirmedTx.minedHeight, .latestEnhancedHeight)
if let minedHeight = confirmedTx.minedHeight {
await internalSyncProgress.set(minedHeight, .latestEnhancedHeight)
}
} catch {
retries += 1
LoggerProxy.error("could not enhance txId \(transaction.transactionId.toHexStringTxId()) - Error: \(error)")
LoggerProxy.error("could not enhance txId \(transaction.rawID.toHexStringTxId()) - Error: \(error)")
if retries > maxRetries {
throw error
}
@ -121,8 +120,8 @@ extension CompactBlockProcessor {
LoggerProxy.error("error enhancing transactions! \(error)")
throw error
}
if let foundTxs = try? transactionRepository.findConfirmedTransactions(in: blockRange, offset: 0, limit: Int.max) {
if let foundTxs = try? transactionRepository.find(in: blockRange, limit: Int.max, kind: .all) {
notifyTransactions(foundTxs, in: blockRange)
}

View File

@ -85,7 +85,7 @@ public enum CompactBlockProgress {
}
public var blockDate: Date? {
if case .enhance(let enhancementProgress) = self, let time = enhancementProgress.lastFoundTransaction?.blockTimeInSeconds {
if case .enhance(let enhancementProgress) = self, let time = enhancementProgress.lastFoundTransaction?.blockTime {
return Date(timeIntervalSince1970: time)
}
@ -109,14 +109,14 @@ protocol EnhancementStreamDelegate: AnyObject {
public protocol EnhancementProgress {
var totalTransactions: Int { get }
var enhancedTransactions: Int { get }
var lastFoundTransaction: ConfirmedTransactionEntity? { get }
var lastFoundTransaction: ZcashTransaction.Overview? { get }
var range: CompactBlockRange { get }
}
public struct EnhancementStreamProgress: EnhancementProgress {
public var totalTransactions: Int
public var enhancedTransactions: Int
public var lastFoundTransaction: ConfirmedTransactionEntity?
public var lastFoundTransaction: ZcashTransaction.Overview?
public var range: CompactBlockRange
public var progress: Float {
@ -744,7 +744,7 @@ actor CompactBlockProcessor {
)
}
func notifyTransactions(_ txs: [ConfirmedTransactionEntity], in range: BlockRange) {
func notifyTransactions(_ txs: [ZcashTransaction.Overview], in range: BlockRange) {
NotificationSender.default.post(
name: .blockProcessorFoundTransactions,
object: self,

View File

@ -32,24 +32,13 @@ class PagedTransactionDAO: PaginatedTransactionRepository {
self.kind = kind
}
func getAll(offset: Int, limit: Int, kind: TransactionKind) throws -> [ConfirmedTransactionEntity]? {
switch kind {
case .all:
return try transactionRepository.findAll(offset: offset, limit: limit)
case .received:
return try transactionRepository.findAll(offset: offset, limit: limit)
case .sent:
return try transactionRepository.findAllSentTransactions(offset: offset, limit: limit)
}
}
func page(_ number: Int) throws -> [TransactionEntity]? {
func page(_ number: Int) throws -> [ZcashTransaction.Overview]? {
let offset = number * pageSize
guard offset < itemCount else { return nil }
return try getAll(offset: offset, limit: pageSize, kind: kind)?.map({ $0.transactionEntity })
return try transactionRepository.find(offset: offset, limit: pageSize, kind: kind)
}
func page(_ number: Int, result: @escaping (Result<[TransactionEntity]?, Error>) -> Void) {
func page(_ number: Int, result: @escaping (Result<[ZcashTransaction.Overview]?, Error>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {

View File

@ -1,176 +0,0 @@
//
// TransactionBuilder.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/18/19.
//
import Foundation
import SQLite
enum TransactionBuilder {
enum ConfirmedColumns: Int {
case id
case minedHeight
case transactionIndex
case rawTransactionId
case expiryHeight
case raw
case toAddress
case value
case memo
case noteId
case blockTimeInSeconds
case fee
}
enum ReceivedColumns: Int {
case id
case minedHeight
case transactionIndex
case rawTransactionId
case raw
case value
case memo
case noteId
case blockTimeInSeconds
case fee
}
enum TransactionEntityColumns: Int {
case id
case minedHeight
case txIndex
case txid
case expiryHeight
case raw
case fee
}
static func createTransactionEntity(txId: Data, rawTransaction: RawTransaction) -> TransactionEntity {
Transaction(
id: nil,
transactionId: txId,
created: nil,
transactionIndex: nil,
expiryHeight: nil,
minedHeight: Int(exactly: rawTransaction.height),
raw: rawTransaction.data,
fee: nil
)
}
static func createTransactionEntity(from bindings: [Binding?]) -> TransactionEntity? {
guard let txId = bindings[TransactionEntityColumns.txid.rawValue] as? Blob else {
return nil
}
var rawData: Data?
if let raw = bindings[TransactionEntityColumns.raw.rawValue] as? Blob {
rawData = Data(blob: raw)
}
return Transaction(
id: bindings[TransactionEntityColumns.id.rawValue] as? Int,
transactionId: Data(blob: txId),
created: nil,
transactionIndex: bindings[TransactionEntityColumns.txIndex.rawValue] as? Int,
expiryHeight: bindings[TransactionEntityColumns.expiryHeight.rawValue] as? Int,
minedHeight: bindings[TransactionEntityColumns.minedHeight.rawValue] as? Int,
raw: rawData,
fee: (bindings[TransactionEntityColumns.fee.rawValue] as? Int?)?.flatMap({ Zatoshi(Int64($0)) })
)
}
static func createConfirmedTransaction(from bindings: [Binding?]) -> ConfirmedTransaction? {
guard
let id = bindings[ConfirmedColumns.id.rawValue] as? Int64,
let noteId = bindings[ConfirmedColumns.noteId.rawValue] as? Int64,
let transactionIndex = bindings[ConfirmedColumns.transactionIndex.rawValue] as? Int64,
let value = bindings[ConfirmedColumns.value.rawValue] as? Int64
else { return nil }
let minedHeight = bindings[ConfirmedColumns.minedHeight.rawValue] as? Int64 ?? -1
let blockTimeInSeconds = bindings[ConfirmedColumns.blockTimeInSeconds.rawValue] as? Int64 ?? 0
// Optional values
let toAddress = bindings[ConfirmedColumns.toAddress.rawValue] as? String
var expiryHeight: BlockHeight?
if let expiry = bindings[ConfirmedColumns.expiryHeight.rawValue] as? Int64 {
expiryHeight = BlockHeight(expiry)
}
var raw: Data?
if let rawBlob = bindings[ConfirmedColumns.raw.rawValue] as? Blob {
raw = Data(blob: rawBlob)
}
var memo: Data?
if let memoBlob = bindings[ConfirmedColumns.memo.rawValue] as? Blob {
memo = Data(blob: memoBlob)
}
var transactionId: Data?
if let txIdBlob = bindings[ConfirmedColumns.rawTransactionId.rawValue] as? Blob {
transactionId = Data(blob: txIdBlob)
}
return ConfirmedTransaction(
toAddress: toAddress,
expiryHeight: expiryHeight,
minedHeight: Int(minedHeight),
noteId: Int(noteId),
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
transactionIndex: Int(transactionIndex),
raw: raw,
id: Int(id),
value: Zatoshi(value),
memo: memo,
rawTransactionId: transactionId,
fee: (bindings[ConfirmedColumns.fee.rawValue] as? Int).map({ Zatoshi(Int64($0)) })
)
}
static func createReceivedTransaction(from bindings: [Binding?]) -> ConfirmedTransaction? {
guard let id = bindings[ReceivedColumns.id.rawValue] as? Int64,
let minedHeight = bindings[ReceivedColumns.minedHeight.rawValue] as? Int64,
let noteId = bindings[ReceivedColumns.noteId.rawValue] as? Int64,
let blockTimeInSeconds = bindings[ReceivedColumns.blockTimeInSeconds.rawValue] as? Int64,
let transactionIndex = bindings[ReceivedColumns.transactionIndex.rawValue] as? Int64,
let value = bindings[ReceivedColumns.value.rawValue] as? Int64
else { return nil }
// Optional values
var memo: Data?
if let memoBlob = bindings[ReceivedColumns.memo.rawValue] as? Blob {
memo = Data(blob: memoBlob)
}
var transactionId: Data?
if let txIdBlob = bindings[ReceivedColumns.rawTransactionId.rawValue] as? Blob {
transactionId = Data(blob: txIdBlob)
}
var rawData: Data?
if let rawBlob = bindings[ReceivedColumns.raw.rawValue] as? Blob {
rawData = Data(blob: rawBlob)
}
return ConfirmedTransaction(
toAddress: nil,
expiryHeight: nil,
minedHeight: Int(minedHeight),
noteId: Int(noteId),
blockTimeInSeconds: TimeInterval(integerLiteral: blockTimeInSeconds),
transactionIndex: Int(transactionIndex),
raw: rawData,
id: Int(id),
value: Zatoshi(value),
memo: memo,
rawTransactionId: transactionId,
fee: (bindings[ReceivedColumns.fee.rawValue] as? Int).map({ Zatoshi(Int64($0)) })
)
}
}

View File

@ -8,87 +8,21 @@
import Foundation
import SQLite
struct Transaction: TransactionEntity {
enum CodingKeys: String, CodingKey {
case id = "id_tx"
case transactionId = "txid"
case created
case transactionIndex = "tx_index"
case expiryHeight = "expiry_height"
case minedHeight = "block"
case raw
case fee
}
var id: Int?
var transactionId: Data
var created: String?
var transactionIndex: Int?
var expiryHeight: BlockHeight?
var minedHeight: BlockHeight?
var raw: Data?
var fee: Zatoshi?
}
extension Transaction: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(Int.self, forKey: .id)
self.transactionId = try container.decode(Data.self, forKey: .transactionId)
self.created = try container.decodeIfPresent(String.self, forKey: .created)
self.transactionIndex = try container.decodeIfPresent(Int.self, forKey: .transactionIndex)
self.expiryHeight = try container.decodeIfPresent(BlockHeight.self, forKey: .expiryHeight)
self.minedHeight = try container.decodeIfPresent(BlockHeight.self, forKey: .minedHeight)
self.raw = try container.decodeIfPresent(Data.self, forKey: .raw)
if let fee = try container.decodeIfPresent(Int64.self, forKey: .fee) {
self.fee = Zatoshi(fee)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.id, forKey: .id)
try container.encode(self.transactionId, forKey: .transactionId)
try container.encodeIfPresent(self.created, forKey: .created)
try container.encodeIfPresent(self.transactionIndex, forKey: .transactionIndex)
try container.encodeIfPresent(self.expiryHeight, forKey: .expiryHeight)
try container.encodeIfPresent(self.minedHeight, forKey: .minedHeight)
try container.encodeIfPresent(self.raw, forKey: .raw)
try container.encodeIfPresent(self.fee?.amount, forKey: .fee)
}
}
struct ConfirmedTransaction: ConfirmedTransactionEntity {
var toAddress: String?
var expiryHeight: BlockHeight?
var minedHeight: Int
var noteId: Int
var blockTimeInSeconds: TimeInterval
var transactionIndex: Int
var raw: Data?
var id: Int?
var value: Zatoshi
var memo: Data?
var rawTransactionId: Data?
var fee: Zatoshi?
}
class TransactionSQLDAO: TransactionRepository {
enum TableStructure {
static var id = Expression<Int>(Transaction.CodingKeys.id.rawValue)
static var transactionId = Expression<Blob>(Transaction.CodingKeys.transactionId.rawValue)
static var created = Expression<String?>(Transaction.CodingKeys.created.rawValue)
static var txIndex = Expression<Int?>(Transaction.CodingKeys.transactionIndex.rawValue)
static var expiryHeight = Expression<Int?>(Transaction.CodingKeys.expiryHeight.rawValue)
static var minedHeight = Expression<Int?>(Transaction.CodingKeys.minedHeight.rawValue)
static var raw = Expression<Blob?>(Transaction.CodingKeys.raw.rawValue)
static var fee = Expression<Zatoshi?>(Transaction.CodingKeys.fee.rawValue)
enum NotesTableStructure {
static let transactionID = Expression<Int>("tx")
static let memo = Expression<Blob>("memo")
}
var dbProvider: ConnectionProvider
var transactions = Table("transactions")
private var blockDao: BlockSQLDAO
private let transactionsView = View("v_transactions")
private let receivedTransactionsView = View("v_tx_received")
private let sentTransactionsView = View("v_tx_sent")
private let receivedNotesTable = Table("received_notes")
private let sentNotesTable = Table("sent_notes")
init(dbProvider: ConnectionProvider) {
self.dbProvider = dbProvider
@ -110,312 +44,138 @@ class TransactionSQLDAO: TransactionRepository {
func isInitialized() throws -> Bool {
true
}
func findEncodedTransactionBy(txId: Int) -> EncodedTransaction? {
// try dbProvider
return nil
}
func countAll() throws -> Int {
try dbProvider.connection().scalar(transactions.count)
}
func countUnmined() throws -> Int {
try dbProvider.connection().scalar(transactions.filter(TableStructure.minedHeight == nil).count)
try dbProvider.connection().scalar(transactions.filter(ZcashTransaction.Overview.Column.minedHeight == nil).count)
}
func findBy(id: Int) throws -> TransactionEntity? {
let query = transactions.filter(TableStructure.id == id).limit(1)
let sequence = try dbProvider.connection().prepare(query)
let entity: Transaction? = try sequence.map({ try $0.decode() }).first
func find(id: Int) throws -> ZcashTransaction.Overview {
let query = transactionsView
.filter(ZcashTransaction.Overview.Column.id == id)
.limit(1)
return try execute(query) { try ZcashTransaction.Overview(row: $0) }
}
func find(rawID: Data) throws -> ZcashTransaction.Overview {
let query = transactionsView
.filter(ZcashTransaction.Overview.Column.rawID == Blob(bytes: rawID.bytes))
.limit(1)
return try execute(query) { try ZcashTransaction.Overview(row: $0) }
}
func find(offset: Int, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview] {
let query = transactionsView
.order((ZcashTransaction.Overview.Column.minedHeight ?? BlockHeight.max).desc, ZcashTransaction.Overview.Column.id.desc)
.filterQueryFor(kind: kind)
.limit(limit, offset: offset)
return try execute(query) { try ZcashTransaction.Overview(row: $0) }
}
func find(in range: BlockRange, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview] {
let query = transactionsView
.order((ZcashTransaction.Overview.Column.minedHeight ?? BlockHeight.max).desc, ZcashTransaction.Overview.Column.id.desc)
.filter(
ZcashTransaction.Overview.Column.minedHeight >= BlockHeight(range.start.height) &&
ZcashTransaction.Overview.Column.minedHeight <= BlockHeight(range.end.height)
)
.filterQueryFor(kind: kind)
.limit(limit)
return try execute(query) { try ZcashTransaction.Overview(row: $0) }
}
func find(from transaction: ZcashTransaction.Overview, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview] {
guard
let transactionIndex = transaction.index,
let transactionBlockTime = transaction.blockTime
else { throw TransactionRepositoryError.transactionMissingRequiredFields }
let query = transactionsView
.order((ZcashTransaction.Overview.Column.minedHeight ?? BlockHeight.max).desc, ZcashTransaction.Overview.Column.id.desc)
.filter(Int64(transactionBlockTime) > ZcashTransaction.Overview.Column.blockTime && transactionIndex > ZcashTransaction.Overview.Column.index)
.filterQueryFor(kind: kind)
.limit(limit)
return try execute(query) { try ZcashTransaction.Overview(row: $0) }
}
func findReceived(offset: Int, limit: Int) throws -> [ZcashTransaction.Received] {
let query = receivedTransactionsView
.order((ZcashTransaction.Overview.Column.minedHeight ?? BlockHeight.max).desc, ZcashTransaction.Overview.Column.id.desc)
.limit(limit, offset: offset)
return try execute(query) { try ZcashTransaction.Received(row: $0) }
}
func findSent(offset: Int, limit: Int) throws -> [ZcashTransaction.Sent] {
let query = sentTransactionsView
.order((ZcashTransaction.Overview.Column.minedHeight ?? BlockHeight.max).desc, ZcashTransaction.Overview.Column.id.desc)
.limit(limit, offset: offset)
return try execute(query) { try ZcashTransaction.Sent(row: $0) }
}
func findMemos(for transaction: ZcashTransaction.Overview) throws -> [Memo] {
return try findMemos(for: transaction.id, table: receivedNotesTable)
}
func findMemos(for receivedTransaction: ZcashTransaction.Received) throws -> [Memo] {
return try findMemos(for: receivedTransaction.id, table: receivedNotesTable)
}
func findMemos(for sentTransaction: ZcashTransaction.Sent) throws -> [Memo] {
return try findMemos(for: sentTransaction.id, table: sentNotesTable)
}
private func findMemos(for transactionID: Int, table: Table) throws -> [Memo] {
let query = table
.filter(NotesTableStructure.transactionID == transactionID)
let memos = try dbProvider.connection().prepare(query).compactMap { row in
do {
let rawMemo = try row.get(NotesTableStructure.memo)
return try Memo(bytes: rawMemo.bytes)
} catch {
return nil
}
}
return memos
}
private func execute<Entity>(_ query: View, createEntity: (Row) throws -> Entity) throws -> Entity {
let entities: [Entity] = try execute(query, createEntity: createEntity)
guard let entity = entities.first else { throw TransactionRepositoryError.notFound }
return entity
}
func findBy(rawId: Data) throws -> TransactionEntity? {
let query = transactions.filter(TableStructure.transactionId == Blob(bytes: rawId.bytes)).limit(1)
let entity: Transaction? = try dbProvider.connection().prepare(query).map({ try $0.decode() }).first
return entity
private func execute<Entity>(_ query: View, createEntity: (Row) throws -> Entity) throws -> [Entity] {
let entities = try dbProvider
.connection()
.prepare(query)
.map(createEntity)
return entities
}
}
// MARK: - Queries
extension TransactionSQLDAO {
func findAllSentTransactions(offset: Int = 0, limit: Int = Int.max) throws -> [ConfirmedTransactionEntity]? {
try dbProvider.connection()
.run(
"""
SELECT
transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.to_address AS toAddress,
sent_notes.value AS value,
sent_notes.memo AS memo,
sent_notes.id_note AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
INNER JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE transactions.raw IS NOT NULL
AND minedheight > 0
ORDER BY block IS NOT NULL, height DESC, time DESC, txid DESC
LIMIT \(limit) OFFSET \(offset)
"""
)
.map { bindings -> ConfirmedTransactionEntity in
guard let transaction = TransactionBuilder.createConfirmedTransaction(from: bindings) else {
throw TransactionRepositoryError.malformedTransaction
}
return transaction
}
}
func findAllReceivedTransactions(offset: Int = 0, limit: Int = Int.max) throws -> [ConfirmedTransactionEntity]? {
try dbProvider.connection()
.run(
"""
SELECT
transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.raw AS raw,
received_notes.value AS value,
received_notes.memo AS memo,
received_notes.id_note AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE received_notes.is_change != 1
ORDER BY minedheight DESC, blocktimeinseconds DESC, id DESC
LIMIT \(limit) OFFSET \(offset)
"""
)
.map { bindings -> ConfirmedTransactionEntity in
guard let transaction = TransactionBuilder.createReceivedTransaction(from: bindings) else {
throw TransactionRepositoryError.malformedTransaction
}
return transaction
}
}
func findAll(offset: Int = 0, limit: Int = Int.max) throws -> [ConfirmedTransactionEntity]? {
try dbProvider.connection()
.run(
"""
SELECT
transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
end AS value,
CASE
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
ELSE received_notes.memo
end AS memo,
CASE
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
ELSE received_notes.id_note
end AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (sent_notes.to_address IS NULL AND received_notes.is_change != 1)
OR sent_notes.to_address IS NOT NULL
ORDER BY ( minedheight IS NOT NULL ),
minedheight DESC,
blocktimeinseconds DESC,
id DESC
LIMIT \(limit) OFFSET \(offset)
"""
)
.compactMap { TransactionBuilder.createConfirmedTransaction(from: $0) }
}
func findAll(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
guard let fromTransaction = transaction else {
return try findAll(offset: 0, limit: limit)
private extension View {
func filterQueryFor(kind: TransactionKind) -> View {
switch kind {
case .all:
return self
case .sent:
return filter(ZcashTransaction.Overview.Column.value < 0)
case .received:
return filter(ZcashTransaction.Overview.Column.value >= 0)
}
return try dbProvider.connection()
.run(
"""
SELECT transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
end AS value,
CASE
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
ELSE received_notes.memo
end AS memo,
CASE
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
ELSE received_notes.id_note
end AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (\(fromTransaction.blockTimeInSeconds), \(fromTransaction.transactionIndex)) > (blocktimeinseconds, transactionIndex) AND
((sent_notes.to_address IS NULL AND received_notes.is_change != 1)
OR sent_notes.to_address IS NOT NULL)
ORDER BY ( minedheight IS NOT NULL ),
minedheight DESC,
blocktimeinseconds DESC,
id DESC
LIMIT \(limit)
"""
)
.compactMap { TransactionBuilder.createConfirmedTransaction(from: $0) }
}
func findTransactions(in range: BlockRange, limit: Int = Int.max) throws -> [TransactionEntity]? {
try dbProvider.connection()
.run(
"""
SELECT transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
transactions.fee AS fee
FROM transactions
WHERE \(range.start.height) <= minedheight
AND minedheight <= \(range.end.height)
ORDER BY ( minedheight IS NOT NULL ),
minedheight ASC,
id DESC
LIMIT \(limit)
"""
)
.compactMap { TransactionBuilder.createTransactionEntity(from: $0) }
}
func findConfirmedTransactions(in range: BlockRange, offset: Int = 0, limit: Int = Int.max) throws -> [ConfirmedTransactionEntity]? {
try dbProvider.connection()
.run(
"""
SELECT transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
end AS value,
CASE
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
ELSE received_notes.memo
end AS memo,
CASE
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
ELSE received_notes.id_note
end AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (\(range.start.height) <= minedheight
AND minedheight <= \(range.end.height)) AND
(sent_notes.to_address IS NULL AND received_notes.is_change != 1)
OR sent_notes.to_address IS NOT NULL
ORDER BY ( minedheight IS NOT NULL ),
minedheight DESC,
blocktimeinseconds DESC,
id DESC
LIMIT \(limit) OFFSET \(offset)
"""
)
.compactMap { TransactionBuilder.createConfirmedTransaction(from: $0) }
}
func findConfirmedTransactionBy(rawId: Data) throws -> ConfirmedTransactionEntity? {
try dbProvider.connection()
.run(
"""
SELECT transactions.id_tx AS id,
transactions.block AS minedHeight,
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
end AS value,
CASE
WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo
ELSE received_notes.memo
end AS memo,
CASE
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
ELSE received_notes.id_note
end AS noteId,
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
LEFT JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE minedheight >= 0
AND rawTransactionId == \(Blob(bytes: rawId.bytes)) AND
(sent_notes.to_address IS NULL AND received_notes.is_change != 1)
OR sent_notes.to_address IS NOT NULL
LIMIT 1
"""
)
.compactMap { TransactionBuilder.createConfirmedTransaction(from: $0) }
.first
}
}

View File

@ -7,7 +7,7 @@
import Foundation
struct EncodedTransaction: SignedTransactionEntity {
struct EncodedTransaction {
var transactionId: Data
var raw: Data?
}

View File

@ -15,7 +15,26 @@ public enum PendingTransactionRecipient: Equatable {
/**
Represents a sent transaction that has not been confirmed yet on the blockchain
*/
public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTransaction, RawIdentifiable {
public protocol PendingTransactionEntity: RawIdentifiable {
/**
internal id for this transaction
*/
var id: Int? { get set }
/**
value in zatoshi
*/
var value: Zatoshi { get set }
/**
data containing the memo if any
*/
var memo: Data? { get set }
var fee: Zatoshi? { get set }
var raw: Data? { get set }
/**
recipient address
*/
@ -177,37 +196,22 @@ public extension PendingTransactionEntity {
}
public extension PendingTransactionEntity {
/**
TransactionEntity representation of this PendingTransactionEntity transaction
*/
var transactionEntity: TransactionEntity {
Transaction(
id: self.id ?? -1,
transactionId: self.rawTransactionId ?? Data(),
created: Date(timeIntervalSince1970: self.createTime).description,
transactionIndex: -1,
expiryHeight: self.expiryHeight,
minedHeight: self.minedHeight,
raw: self.raw,
fee: self.fee
)
}
}
public extension ConfirmedTransactionEntity {
/**
TransactionEntity representation of this ConfirmedTransactionEntity transaction
*/
var transactionEntity: TransactionEntity {
Transaction(
id: self.id ?? -1,
transactionId: self.rawTransactionId ?? Data(),
created: Date(timeIntervalSince1970: self.blockTimeInSeconds).description,
transactionIndex: self.transactionIndex,
expiryHeight: self.expiryHeight,
minedHeight: self.minedHeight,
raw: self.raw,
fee: self.fee
func makeTransactionEntity(defaultFee: Zatoshi) -> ZcashTransaction.Overview {
return ZcashTransaction.Overview(
blockTime: createTime,
expiryHeight: expiryHeight,
fee: fee,
id: id ?? -1,
index: nil,
isWalletInternal: false,
hasChange: false,
memoCount: 0,
minedHeight: minedHeight,
raw: raw,
rawID: rawTransactionId ?? Data(),
receivedNoteCount: 0,
sentNoteCount: 0,
value: value
)
}
}

View File

@ -6,102 +6,207 @@
//
import Foundation
/**
convenience representation of all transaction types
*/
public protocol TransactionEntity {
/**
Internal transaction id
*/
var id: Int? { get set }
import SQLite
/**
Blockchain transaction id
*/
var transactionId: Data { get set }
public enum ZcashTransaction {
public struct Overview {
public let blockTime: TimeInterval?
public let expiryHeight: BlockHeight?
public let fee: Zatoshi?
public let id: Int
public let index: Int?
public let isWalletInternal: Bool
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
}
/**
String representing the date of creation
format is yyyy-MM-dd'T'HH:MM:ss.SSSSSSSSSZ
- Example: 2019-12-04T17:49:10.636624000Z
*/
var created: String? { get set }
var transactionIndex: Int? { get set }
var expiryHeight: BlockHeight? { get set }
var minedHeight: BlockHeight? { get set }
var raw: Data? { get set }
public struct Received {
public let blockTime: TimeInterval
public let expiryHeight: BlockHeight?
public let fromAccount: Int
public let id: Int
public let index: Int
public let memoCount: Int
public let minedHeight: BlockHeight
public let noteCount: Int
public let raw: Data?
public let rawID: Data?
public let value: Zatoshi
}
public struct Sent {
public let blockTime: TimeInterval
public let expiryHeight: BlockHeight?
public let fromAccount: Int
public let id: Int
public let index: Int
public let memoCount: Int
public let minedHeight: BlockHeight
public let noteCount: Int
public let raw: Data?
public let rawID: Data?
public let value: Zatoshi
}
public struct Fetched {
public let rawID: Data
public let minedHeight: BlockHeight
public let raw: Data
}
}
/**
Hashable extension default implementation
*/
public extension TransactionEntity {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(transactionId)
hasher.combine(expiryHeight)
hasher.combine(minedHeight)
hasher.combine(raw)
extension ZcashTransaction.Overview {
enum Column {
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>("net_value")
static let fee = Expression<Int64?>("fee_paid")
static let isWalletInternal = Expression<Bool>("is_wallet_internal")
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 func == (lhs: Self, rhs: Self) -> Bool {
guard
lhs.id == rhs.id,
lhs.transactionId == rhs.transactionId,
lhs.created == rhs.created,
lhs.expiryHeight == rhs.expiryHeight,
lhs.minedHeight == rhs.minedHeight,
((lhs.raw != nil && rhs.raw != nil) || (lhs.raw == nil && rhs.raw == nil))
else { return false }
if let lhsRaw = lhs.raw, let rhsRaw = rhs.raw {
return lhsRaw == rhsRaw
init(row: Row) throws {
self.expiryHeight = try row.get(Column.expiryHeight)
self.id = try row.get(Column.id)
self.index = try row.get(Column.index)
self.isWalletInternal = try row.get(Column.isWalletInternal)
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))
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
}
return true
}
func anchor(network: ZcashNetwork) -> BlockHeight? {
if let minedHeight = self.minedHeight, minedHeight != -1 {
guard let minedHeight = self.minedHeight else { return nil }
if minedHeight != -1 {
return max(minedHeight - ZcashSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
}
if let expiryHeight = self.expiryHeight, expiryHeight != -1 {
guard let expiryHeight = self.expiryHeight else { return nil }
if expiryHeight != -1 {
return max(expiryHeight - ZcashSDK.expiryOffset - ZcashSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
}
return nil
}
}
/**
Abstract representation of all transaction types
*/
public protocol AbstractTransaction {
/**
internal id for this transaction
*/
var id: Int? { get set }
extension ZcashTransaction.Received {
enum Column {
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 fromAccount = Expression<Int>("received_by_account")
static let value = Expression<Int64>("received_total")
static let fee = Expression<Int64>("fee_paid")
static let noteCount = Expression<Int>("received_note_count")
static let memoCount = Expression<Int>("memo_count")
static let blockTime = Expression<Int64>("block_time")
}
/**
value in zatoshi
*/
var value: Zatoshi { get set }
init(row: Row) throws {
self.blockTime = TimeInterval(try row.get(Column.blockTime))
self.expiryHeight = try row.get(Column.expiryHeight)
self.fromAccount = try row.get(Column.fromAccount)
self.id = try row.get(Column.id)
self.index = try row.get(Column.index)
self.memoCount = try row.get(Column.memoCount)
self.minedHeight = try row.get(Column.minedHeight)
self.noteCount = try row.get(Column.noteCount)
self.value = Zatoshi(try row.get(Column.value))
/**
data containing the memo if any
*/
var memo: Data? { get set }
if let raw = try row.get(Column.raw) {
self.raw = Data(blob: raw)
} else {
self.raw = nil
}
var fee: Zatoshi? { get set }
if let rawID = try row.get(Column.rawID) {
self.rawID = Data(blob: rawID)
} else {
self.rawID = nil
}
}
}
/**
Capabilities of a signed transaction
*/
public protocol SignedTransactionEntity {
var raw: Data? { get set }
extension ZcashTransaction.Sent {
enum Column {
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 fromAccount = Expression<Int>("sent_from_account")
static let value = Expression<Int64>("sent_total")
static let fee = Expression<Int64>("fee_paid")
static let noteCount = Expression<Int>("sent_note_count")
static let memoCount = Expression<Int>("memo_count")
static let blockTime = Expression<Int64>("block_time")
}
init(row: Row) throws {
self.blockTime = TimeInterval(try row.get(Column.blockTime))
self.expiryHeight = try row.get(Column.expiryHeight)
self.fromAccount = try row.get(Column.fromAccount)
self.id = try row.get(Column.id)
self.index = try row.get(Column.index)
self.memoCount = try row.get(Column.memoCount)
self.minedHeight = try row.get(Column.minedHeight)
self.noteCount = try row.get(Column.noteCount)
self.value = Zatoshi(try row.get(Column.value))
if let raw = try row.get(Column.raw) {
self.raw = Data(blob: raw)
} else {
self.raw = nil
}
if let rawID = try row.get(Column.rawID) {
self.rawID = Data(blob: rawID)
} else {
self.rawID = nil
}
}
}
/**
@ -110,60 +215,3 @@ Capabilities of an entity that can be uniquely identified by a raw transaction i
public protocol RawIdentifiable {
var rawTransactionId: Data? { get set }
}
/**
Attributes that a Mined transaction must have
*/
public protocol MinedTransactionEntity: AbstractTransaction, RawIdentifiable {
/**
height on which this transaction was mined at. Convention is that -1 is returned when it has not been mined yet
*/
var minedHeight: Int { get set }
/**
internal note id that is involved on this transaction
*/
var noteId: Int { get set }
/**
block time in in reference since 1970
*/
var blockTimeInSeconds: TimeInterval { get set }
/**
internal index for this transaction
*/
var transactionIndex: Int { get set }
}
public protocol ConfirmedTransactionEntity: MinedTransactionEntity, SignedTransactionEntity {
/**
recipient address if available
*/
var toAddress: String? { get set }
/**
expiration height for this transaction
*/
var expiryHeight: BlockHeight? { get set }
}
public extension ConfirmedTransactionEntity {
var isOutbound: Bool {
self.toAddress != nil
}
var isInbound: Bool {
self.toAddress == nil
}
var blockTimeInMilliseconds: Double {
self.blockTimeInSeconds * 1000
}
}
public extension AbstractTransaction {
var intValue: Int {
Int(self.value.amount)
}
}

View File

@ -30,6 +30,18 @@ public enum Memo: Equatable {
public init(string: String) throws {
self = .text(try MemoText(String(string.utf8)))
}
/// Represent memo as String. Only `.text` memo can be represented as String.
/// - Returns: Valid String if it can be created from memo; otherwise `nil`.
public func toString() -> String? {
switch self {
case .empty, .future, .arbitrary:
return nil
case .text(let text):
return text.string
}
}
}
public extension Memo {

View File

@ -26,10 +26,10 @@ public protocol PaginatedTransactionRepository {
/**
Returns the page number if exists. Blocking
*/
func page(_ number: Int) throws -> [TransactionEntity]?
func page(_ number: Int) throws -> [ZcashTransaction.Overview]?
/**
Returns the page number if exists. Non-blocking
*/
func page(_ number: Int, result: @escaping (Result<[TransactionEntity]?, Error>) -> Void)
func page(_ number: Int, result: @escaping (Result<[ZcashTransaction.Overview]?, Error>) -> Void)
}

View File

@ -9,6 +9,8 @@ import Foundation
enum TransactionRepositoryError: Error {
case malformedTransaction
case notFound
case transactionMissingRequiredFields
}
protocol TransactionRepository {
@ -16,16 +18,16 @@ protocol TransactionRepository {
func countAll() throws -> Int
func countUnmined() throws -> Int
func blockForHeight(_ height: BlockHeight) throws -> Block?
func findBy(id: Int) throws -> TransactionEntity?
func findBy(rawId: Data) throws -> TransactionEntity?
func findAllSentTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]?
func findAllReceivedTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]?
func findAll(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]?
func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]?
func lastScannedHeight() throws -> BlockHeight
func isInitialized() throws -> Bool
func findEncodedTransactionBy(txId: Int) -> EncodedTransaction?
func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]?
func findConfirmedTransactions(in range: BlockRange, offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]?
func findConfirmedTransactionBy(rawId: Data) throws -> ConfirmedTransactionEntity?
func find(id: Int) throws -> ZcashTransaction.Overview
func find(rawID: Data) throws -> ZcashTransaction.Overview
func find(offset: Int, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview]
func find(in range: BlockRange, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview]
func find(from: ZcashTransaction.Overview, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview]
func findReceived(offset: Int, limit: Int) throws -> [ZcashTransaction.Received]
func findSent(offset: Int, limit: Int) throws -> [ZcashTransaction.Sent]
func findMemos(for transaction: ZcashTransaction.Overview) throws -> [Memo]
func findMemos(for receivedTransaction: ZcashTransaction.Received) throws -> [Memo]
func findMemos(for sentTransaction: ZcashTransaction.Sent) throws -> [Memo]
}

View File

@ -248,12 +248,12 @@ extension LightWalletGRPCService: LightWalletService {
}
}
public func fetchTransaction(txId: Data) async throws -> TransactionEntity {
public func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched {
var txFilter = TxFilter()
txFilter.hash = txId
let rawTx = try await compactTxStreamerAsync.getTransaction(txFilter)
return TransactionBuilder.createTransactionEntity(txId: txId, rawTransaction: rawTx)
return ZcashTransaction.Fetched(rawID: txId, minedHeight: BlockHeight(rawTx.height), raw: rawTx.data)
}
public func fetchUTXOs(

View File

@ -125,7 +125,7 @@ public protocol LightWalletService {
/// - Parameter txId: data representing the transaction ID
/// - Throws: LightWalletServiceError
/// - Returns: LightWalletServiceResponse
func fetchTransaction(txId: Data) async throws -> TransactionEntity
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched
func fetchUTXOs(for tAddress: String, height: BlockHeight) -> AsyncThrowingStream<UnspentTransactionOutputEntity, Error>

View File

@ -132,24 +132,33 @@ public protocol Synchronizer {
var pendingTransactions: [PendingTransactionEntity] { get }
/// all the transactions that are on the blockchain
var clearedTransactions: [ConfirmedTransactionEntity] { get }
var clearedTransactions: [ZcashTransaction.Overview] { get }
/// All transactions that are related to sending funds
var sentTransactions: [ConfirmedTransactionEntity] { get }
var sentTransactions: [ZcashTransaction.Sent] { get }
/// all transactions related to receiving funds
var receivedTransactions: [ConfirmedTransactionEntity] { get }
var receivedTransactions: [ZcashTransaction.Received] { get }
/// A repository serving transactions in a paginated manner
/// - Parameter kind: Transaction Kind expected from this PaginatedTransactionRepository
func paginatedTransactions(of kind: TransactionKind) -> PaginatedTransactionRepository
/// Returns a list of confirmed transactions that precede the given transaction with a limit count.
/// Get all memos for `transaction`.
func getMemos(for transaction: ZcashTransaction.Overview) throws -> [Memo]
/// Get all memos for `receivedTransaction`.
func getMemos(for receivedTransaction: ZcashTransaction.Received) throws -> [Memo]
/// Get all memos for `sentTransaction`.
func getMemos(for sentTransaction: ZcashTransaction.Sent) throws -> [Memo]
/// Returns a list of confirmed transactions that preceed the given transaction with a limit count.
/// - Parameters:
/// - from: the confirmed transaction from which the query should start from or nil to retrieve from the most recent transaction
/// - limit: the maximum amount of items this should return if available
/// - Returns: an array with the given Transactions or nil
func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]?
func allConfirmedTransactions(from transaction: ZcashTransaction.Overview, limit: Int) throws -> [ZcashTransaction.Overview]
/// Returns the latest block height from the provided Lightwallet endpoint
func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void)
@ -253,7 +262,7 @@ public enum TransactionKind {
public enum RewindPolicy {
case birthday
case height(blockheight: BlockHeight)
case transaction(_ transaction: TransactionEntity)
case transaction(_ transaction: ZcashTransaction.Overview)
case quick
}

View File

@ -331,7 +331,7 @@ public class SDKSynchronizer: Synchronizer {
@objc func transactionsFound(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ConfirmedTransactionEntity]
let foundTransactions = userInfo[CompactBlockProcessorNotificationKey.foundTransactions] as? [ZcashTransaction.Overview]
else {
return
}
@ -533,30 +533,42 @@ public class SDKSynchronizer: Synchronizer {
transactionManager.cancel(pendingTransaction: transaction)
}
public func allReceivedTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAllReceivedTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
public func allReceivedTransactions() throws -> [ZcashTransaction.Received] {
try transactionRepository.findReceived(offset: 0, limit: Int.max)
}
public func allPendingTransactions() throws -> [PendingTransactionEntity] {
try transactionManager.allPendingTransactions() ?? [PendingTransactionEntity]()
}
public func allClearedTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAll(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
public func allClearedTransactions() throws -> [ZcashTransaction.Overview] {
return try transactionRepository.find(offset: 0, limit: Int.max, kind: .all)
}
public func allSentTransactions() throws -> [ConfirmedTransactionEntity] {
try transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max) ?? [ConfirmedTransactionEntity]()
public func allSentTransactions() throws -> [ZcashTransaction.Sent] {
return try transactionRepository.findSent(offset: 0, limit: Int.max)
}
public func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
try transactionRepository.findAll(from: transaction, limit: limit)
public func allConfirmedTransactions(from transaction: ZcashTransaction.Overview, limit: Int) throws -> [ZcashTransaction.Overview] {
return try transactionRepository.find(from: transaction, limit: limit, kind: .all)
}
public func paginatedTransactions(of kind: TransactionKind = .all) -> PaginatedTransactionRepository {
PagedTransactionRepositoryBuilder.build(initializer: initializer, kind: .all)
}
public func getMemos(for transaction: ZcashTransaction.Overview) throws -> [Memo] {
return try transactionRepository.findMemos(for: transaction)
}
public func getMemos(for receivedTransaction: ZcashTransaction.Received) throws -> [Memo] {
return try transactionRepository.findMemos(for: receivedTransaction)
}
public func getMemos(for sentTransaction: ZcashTransaction.Sent) throws -> [Memo] {
return try transactionRepository.findMemos(for: sentTransaction)
}
public func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void) {
Task {
do {
@ -751,10 +763,10 @@ public class SDKSynchronizer: Synchronizer {
try transactionManager.allPendingTransactions()?
.filter { $0.isSubmitSuccess && !$0.isMined }
.forEach { pendingTx in
guard let rawId = pendingTx.rawTransactionId else { return }
let transaction = try transactionRepository.findBy(rawId: rawId)
guard let rawID = pendingTx.rawTransactionId else { return }
let transaction = try transactionRepository.find(rawID: rawID)
guard let minedHeight = transaction.minedHeight else { return }
guard let minedHeight = transaction?.minedHeight else { return }
let minedTx = try transactionManager.applyMinedHeight(pendingTransaction: pendingTx, minedHeight: minedHeight)
notifyMinedTransaction(minedTx)
@ -848,16 +860,16 @@ extension SDKSynchronizer {
(try? self.allPendingTransactions()) ?? [PendingTransactionEntity]()
}
public var clearedTransactions: [ConfirmedTransactionEntity] {
(try? self.allClearedTransactions()) ?? [ConfirmedTransactionEntity]()
public var clearedTransactions: [ZcashTransaction.Overview] {
(try? self.allClearedTransactions()) ?? []
}
public var sentTransactions: [ConfirmedTransactionEntity] {
(try? self.allSentTransactions()) ?? [ConfirmedTransactionEntity]()
public var sentTransactions: [ZcashTransaction.Sent] {
(try? self.allSentTransactions()) ?? []
}
public var receivedTransactions: [ConfirmedTransactionEntity] {
(try? self.allReceivedTransactions()) ?? [ConfirmedTransactionEntity]()
public var receivedTransactions: [ZcashTransaction.Received] {
(try? self.allReceivedTransactions()) ?? []
}
}
@ -896,6 +908,6 @@ extension ConnectionState {
private struct NullEnhancementProgress: EnhancementProgress {
var totalTransactions: Int { 0 }
var enhancedTransactions: Int { 0 }
var lastFoundTransaction: ConfirmedTransactionEntity? { nil }
var lastFoundTransaction: ZcashTransaction.Overview? { nil }
var range: CompactBlockRange { 0 ... 0 }
}

View File

@ -70,17 +70,16 @@ class PersistentTransactionManager: OutboundTransactionManager {
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
let encodedTransaction = try await self.encoder.createShieldingTransaction(
let transaction = try await self.encoder.createShieldingTransaction(
spendingKey: spendingKey,
memoBytes: try pendingTransaction.memo?.intoMemoBytes(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
pending.raw = transaction.raw
pending.rawTransactionId = transaction.rawID
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
@ -110,26 +109,26 @@ class PersistentTransactionManager: OutboundTransactionManager {
switch pendingTransaction.recipient {
case .address(let addr):
toAddress = addr.stringEncoded
case .internalAccount: break
case .internalAccount:
break
}
guard let toAddress else {
throw TransactionManagerError.cannotEncodeInternalTx(pendingTransaction)
}
let encodedTransaction = try await self.encoder.createTransaction(
let transaction = try await self.encoder.createTransaction(
spendingKey: spendingKey,
zatoshi: pendingTransaction.value,
to: toAddress,
memoBytes: try pendingTransaction.memo?.intoMemoBytes(),
from: pendingTransaction.accountIndex
)
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
var pending = pendingTransaction
pending.encodeAttempts += 1
pending.raw = encodedTransaction.raw
pending.rawTransactionId = encodedTransaction.transactionId
pending.raw = transaction.raw
pending.rawTransactionId = transaction.rawID
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()

View File

@ -35,7 +35,7 @@ protocol TransactionEncoder {
to address: String,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> EncodedTransaction
) async throws -> ZcashTransaction.Overview
/**
Creates a transaction that will attempt to shield transparent funds that are present on the cacheDB .throwing an exception whenever things are missing. When the provided wallet implementation doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using double-bangs for things).
@ -51,11 +51,5 @@ protocol TransactionEncoder {
spendingKey: UnifiedSpendingKey,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> EncodedTransaction
/// Fetch the Transaction Entity from the encoded representation
/// - Parameter encodedTransaction: The encoded transaction to expand
/// - Returns: a TransactionEntity based on the given Encoded Transaction
/// - Throws: a TransactionEncoderError
func expandEncodedTransaction(_ encodedTransaction: EncodedTransaction) throws -> TransactionEntity
) async throws -> ZcashTransaction.Overview
}

View File

@ -53,7 +53,7 @@ class WalletTransactionEncoder: TransactionEncoder {
to address: String,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> EncodedTransaction {
) async throws -> ZcashTransaction.Overview {
let txId = try createSpend(
spendingKey: spendingKey,
zatoshi: zatoshi,
@ -61,16 +61,10 @@ class WalletTransactionEncoder: TransactionEncoder {
memoBytes: memoBytes,
from: accountIndex
)
do {
let transactionEntity = try repository.findBy(id: txId)
guard let transaction = transactionEntity else {
throw TransactionEncoderError.notFound(transactionId: txId)
}
LoggerProxy.debug("sentTransaction id: \(txId)")
return EncodedTransaction(transactionId: transaction.transactionId, raw: transaction.raw)
LoggerProxy.debug("transaction id: \(txId)")
return try repository.find(id: txId)
} catch {
throw TransactionEncoderError.notFound(transactionId: txId)
}
@ -109,7 +103,7 @@ class WalletTransactionEncoder: TransactionEncoder {
spendingKey: UnifiedSpendingKey,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> EncodedTransaction {
) async throws -> ZcashTransaction.Overview {
let txId = try createShieldingSpend(
spendingKey: spendingKey,
memo: memoBytes,
@ -117,18 +111,13 @@ class WalletTransactionEncoder: TransactionEncoder {
)
do {
let transactionEntity = try repository.findBy(id: txId)
guard let transaction = transactionEntity else {
throw TransactionEncoderError.notFound(transactionId: txId)
}
LoggerProxy.debug("sentTransaction id: \(txId)")
return EncodedTransaction(transactionId: transaction.transactionId, raw: transaction.raw)
LoggerProxy.debug("transaction id: \(txId)")
return try repository.find(id: txId)
} catch {
throw TransactionEncoderError.notFound(transactionId: txId)
}
}
func createShieldingSpend(
spendingKey: UnifiedSpendingKey,
memo: MemoBytes?,
@ -162,17 +151,4 @@ class WalletTransactionEncoder: TransactionEncoder {
// TODO: [#713] change this to something that makes sense, https://github.com/zcash/ZcashLightClientKit/issues/713
return readableSpend && readableOutput
}
/**
Fetch the Transaction Entity from the encoded representation
- Parameter encodedTransaction: The encoded transaction to expand
- Returns: a TransactionEntity based on the given Encoded Transaction
- Throws: a TransactionEncoderError
*/
func expandEncodedTransaction(_ encodedTransaction: EncodedTransaction) throws -> TransactionEntity {
guard let transaction = try? repository.findBy(rawId: encodedTransaction.transactionId) else {
throw TransactionEncoderError.couldNotExpand(txId: encodedTransaction.transactionId)
}
return transaction
}
}

View File

@ -172,10 +172,7 @@ class AdvancedReOrgTests: XCTestCase {
/*
4. get that transaction hex encoded data
*/
guard let receivedTxData = receivedTx.raw else {
XCTFail("received tx has no raw data!")
return
}
let receivedTxData = receivedTx.raw ?? Data()
let receivedRawTx = RawTransaction.with { rawTx in
rawTx.height = UInt64(receivedTxHeight)
@ -605,7 +602,7 @@ class AdvancedReOrgTests: XCTestCase {
var initialBalance = Zatoshi(-1)
var initialVerifiedBalance = Zatoshi(-1)
var incomingTx: ConfirmedTransactionEntity?
var incomingTx: ZcashTransaction.Received!
try coordinator.sync(completion: { _ in
firstSyncExpectation.fulfill()
}, error: self.handleError)
@ -619,15 +616,7 @@ class AdvancedReOrgTests: XCTestCase {
initialVerifiedBalance = coordinator.synchronizer.initializer.getVerifiedBalance()
incomingTx = coordinator.synchronizer.receivedTransactions.first(where: { $0.minedHeight == incomingTxHeight })
guard let transaction = incomingTx else {
XCTFail("no tx found")
return
}
guard let txRawData = transaction.raw else {
XCTFail("transaction has no raw data")
return
}
let txRawData = incomingTx.raw ?? Data()
let rawTransaction = RawTransaction.with({ rawTx in
rawTx.data = txRawData
@ -958,14 +947,16 @@ class AdvancedReOrgTests: XCTestCase {
15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection
*/
XCTAssertEqual(coordinator.synchronizer.pendingTransactions.count, 0)
let sentTransactions = coordinator.synchronizer.sentTransactions
.first(
where: { transaction in
return transaction.rawID == newlyPendingTx.rawTransactionId
}
)
XCTAssertNotNil(
coordinator.synchronizer.sentTransactions
.first(
where: { transaction in
guard let txId = transaction.rawTransactionId else { return false }
return txId == newlyPendingTx.rawTransactionId
}
),
sentTransactions,
"Sent Tx is not on sent transactions"
)

View File

@ -124,7 +124,7 @@ class BalanceTests: XCTestCase {
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
let foundTx = txs.first(where: { $0.rawID == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
@ -282,7 +282,7 @@ class BalanceTests: XCTestCase {
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
let foundTx = txs.first(where: { $0.rawID == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
@ -439,7 +439,7 @@ class BalanceTests: XCTestCase {
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
let foundTx = txs.first(where: { $0.rawID == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
@ -948,88 +948,86 @@ class BalanceTests: XCTestCase {
*/
try await withCheckedThrowingContinuation { continuation in
do {
try coordinator.sync(completion: { synchronizer in
let confirmedTx: ConfirmedTransactionEntity!
do {
confirmedTx = try synchronizer.allClearedTransactions().first(where: { confirmed -> Bool in
confirmed.transactionEntity.transactionId == pendingTx?.transactionEntity.transactionId
})
} catch {
XCTFail("Error retrieving cleared transactions")
return
}
try coordinator.sync(
completion: { synchronizer in
let confirmedTx: ZcashTransaction.Overview!
do {
confirmedTx = try synchronizer.allClearedTransactions().first(where: { confirmed -> Bool in
confirmed.rawID == pendingTx?.rawTransactionId
})
} catch {
XCTFail("Error retrieving cleared transactions")
return
}
/*
Theres a sent transaction matching the amount sent to the given zAddr
*/
XCTAssertEqual(confirmedTx.value, self.sendAmount)
XCTAssertEqual(confirmedTx.toAddress, self.testRecipientAddress)
let confirmedMemo = try confirmedTx.memo?.intoMemoBytes()?.intoMemo()
XCTAssertEqual(confirmedMemo, memo)
/*
Theres a sent transaction matching the amount sent to the given zAddr
*/
XCTAssertEqual(confirmedTx.value, self.sendAmount)
// TODO [#683]: Add API to SDK to fetch memos.
// let confirmedMemo = try confirmedTx.memo?.intoMemoBytes()?.intoMemo()
// XCTAssertEqual(confirmedMemo, memo)
guard let transactionId = confirmedTx.rawTransactionId else {
XCTFail("no raw transaction id")
return
}
/*
Find out what note was used
*/
let sentNotesRepo = SentNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
/*
Find out what note was used
*/
let sentNotesRepo = SentNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
)
guard let sentNote = try? sentNotesRepo.sentNote(byRawTransactionId: transactionId) else {
XCTFail("Could not find sent note with transaction Id \(transactionId)")
return
}
guard let sentNote = try? sentNotesRepo.sentNote(byRawTransactionId: confirmedTx.rawID) else {
XCTFail("Could not finde sent note with transaction Id \(confirmedTx.rawID)")
return
}
let receivedNotesRepo = ReceivedNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
let receivedNotesRepo = ReceivedNotesSQLDAO(
dbProvider: SimpleConnectionProvider(
path: self.coordinator.synchronizer.initializer.dataDbURL.absoluteString,
readonly: true
)
)
)
/*
get change note
*/
guard let receivedNote = try? receivedNotesRepo.receivedNote(byRawTransactionId: transactionId) else {
XCTFail("Could not find received not with change for transaction Id \(transactionId)")
return
}
/*
get change note
*/
guard let receivedNote = try? receivedNotesRepo.receivedNote(byRawTransactionId: confirmedTx.rawID) else {
XCTFail("Could not find received not with change for transaction Id \(confirmedTx.rawID)")
return
}
/*
Theres a change note of value (previous note value - sent amount)
*/
XCTAssertEqual(
previousVerifiedBalance - self.sendAmount - self.network.constants.defaultFee(for: self.defaultLatestHeight),
Zatoshi(Int64(receivedNote.value))
)
/*
Theres a change note of value (previous note value - sent amount)
*/
XCTAssertEqual(
previousVerifiedBalance - self.sendAmount - self.network.constants.defaultFee(for: self.defaultLatestHeight),
Zatoshi(Int64(receivedNote.value))
)
/*
Balance meets verified Balance and total balance criteria
*/
self.verifiedBalanceValidation(
previousBalance: previousVerifiedBalance,
spentNoteValue: Zatoshi(Int64(sentNote.value)),
changeValue: Zatoshi(Int64(receivedNote.value)),
sentAmount: self.sendAmount,
currentVerifiedBalance: synchronizer.initializer.getVerifiedBalance()
)
/*
Balance meets verified Balance and total balance criteria
*/
self.verifiedBalanceValidation(
previousBalance: previousVerifiedBalance,
spentNoteValue: Zatoshi(Int64(sentNote.value)),
changeValue: Zatoshi(Int64(receivedNote.value)),
sentAmount: self.sendAmount,
currentVerifiedBalance: synchronizer.initializer.getVerifiedBalance()
)
self.totalBalanceValidation(
totalBalance: synchronizer.initializer.getBalance(),
previousTotalbalance: previousTotalBalance,
sentAmount: self.sendAmount
)
self.totalBalanceValidation(
totalBalance: synchronizer.initializer.getBalance(),
previousTotalbalance: previousTotalBalance,
sentAmount: self.sendAmount
)
syncToMinedheightExpectation.fulfill()
continuation.resume()
}, error: self.handleError)
syncToMinedheightExpectation.fulfill()
continuation.resume()
},
error: self.handleError
)
} catch {
continuation.resume(throwing: error)
}
@ -1192,7 +1190,7 @@ class BalanceTests: XCTestCase {
}
class SDKSynchonizerListener {
var transactionsFound: (([ConfirmedTransactionEntity]) -> Void)?
var transactionsFound: (([ZcashTransaction.Overview]) -> Void)?
var synchronizerMinedTransaction: ((PendingTransactionEntity) -> Void)?
func subscribeToSynchronizer(_ synchronizer: SDKSynchronizer) {
@ -1206,7 +1204,7 @@ class SDKSynchonizerListener {
@objc func txFound(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let txs = notification.userInfo?[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ConfirmedTransactionEntity] else {
guard let txs = notification.userInfo?[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ZcashTransaction.Overview] else {
XCTFail("expected [ConfirmedTransactionEntity] array")
return
}

View File

@ -235,12 +235,12 @@ class RewindRescanTests: XCTestCase {
return
}
try await coordinator.synchronizer.rewind(.transaction(transaction.transactionEntity))
try await coordinator.synchronizer.rewind(.transaction(transaction))
// assert that after the new height is
XCTAssertEqual(
try coordinator.synchronizer.initializer.transactionRepository.lastScannedHeight(),
transaction.transactionEntity.anchor(network: network)
transaction.anchor(network: network)
)
let secondScanExpectation = XCTestExpectation(description: "rescan")
@ -334,7 +334,7 @@ class RewindRescanTests: XCTestCase {
let sentTxHeight = latestHeight + 1
notificationHandler.transactionsFound = { txs in
let foundTx = txs.first(where: { $0.rawTransactionId == pendingTx.rawTransactionId })
let foundTx = txs.first(where: { $0.rawID == pendingTx.rawTransactionId })
XCTAssertNotNil(foundTx)
XCTAssertEqual(foundTx?.minedHeight, sentTxHeight)
@ -401,7 +401,7 @@ class RewindRescanTests: XCTestCase {
XCTFail("should have found sent transaction but didn't")
return
}
XCTAssertEqual(transaction.rawTransactionId, pendingTx.rawTransactionId, "should have mined sent transaction but didn't")
XCTAssertEqual(transaction.rawID, pendingTx.rawTransactionId, "should have mined sent transaction but didn't")
}
notificationHandler.synchronizerMinedTransaction = { transaction in

View File

@ -329,7 +329,7 @@ class ShieldFundsTests: XCTestCase {
// verify that there's a confirmed transaction that's the shielding transaction
let clearedTransaction = coordinator.synchronizer.clearedTransactions.first(
where: { $0.rawTransactionId == shieldingPendingTx?.rawTransactionId }
where: { $0.rawID == shieldingPendingTx?.rawTransactionId }
)
XCTAssertNotNil(clearedTransaction)

View File

@ -31,7 +31,7 @@ class SychronizerDarksideTests: XCTestCase {
var expectedReorgHeight: BlockHeight = 665188
var expectedRewindHeight: BlockHeight = 665188
var reorgExpectation = XCTestExpectation(description: "reorg")
var foundTransactions: [ConfirmedTransactionEntity] = []
var foundTransactions: [ZcashTransaction.Overview] = []
override func setUpWithError() throws {
try super.setUpWithError()
@ -218,7 +218,7 @@ class SychronizerDarksideTests: XCTestCase {
@objc func handleFoundTransactions(_ notification: Notification) {
guard
let userInfo = notification.userInfo,
let transactions = userInfo[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ConfirmedTransactionEntity]
let transactions = userInfo[SDKSynchronizer.NotificationKeys.foundTransactions] as? [ZcashTransaction.Overview]
else {
return
}

View File

@ -57,6 +57,28 @@ class MemoTests: XCTestCase {
XCTAssertNil(Self.canonicalEmptyMemo().asZcashTransactionMemo())
}
/* Test conversion to string */
func testEmptyMemoToString() {
let memo: Memo = .empty
XCTAssertNil(memo.toString())
}
func testTextMemoToString() throws {
let memo: Memo = .text(try MemoText(Self.validMemoDataExpectedString))
XCTAssertEqual(memo.toString(), Self.validMemoDataExpectedString)
}
func testFutureMemoToString() throws {
let memo: Memo = .future(try MemoBytes(bytes: Self.validMemoDataExpectedString.data(using: .utf8)!.bytes))
XCTAssertNil(memo.toString())
}
func testArbitraryMemoToString() {
let memo: Memo = .arbitrary(Self.validMemoDataExpectedString.data(using: .utf8)!.bytes)
XCTAssertNil(memo.toString())
}
/**
*******
* mocked memos

View File

@ -37,79 +37,185 @@ class TransactionRepositoryTests: XCTestCase {
XCTAssertNotNil(count)
XCTAssertEqual(count, 0)
}
func testBlockForHeight() {
var block: Block!
XCTAssertNoThrow(try { block = try self.transactionRepository.blockForHeight(663150) }())
XCTAssertEqual(block.height, 663150)
}
func testLastScannedHeight() {
var height: BlockHeight!
XCTAssertNoThrow(try { height = try self.transactionRepository.lastScannedHeight() }())
XCTAssertEqual(height, 665000)
}
func testFindInRange() {
var transactions: [ZcashTransaction.Overview]!
XCTAssertNoThrow(
try {
transactions = try self.transactionRepository.find(in: BlockRange(startHeight: 663218, endHeight: 663974), limit: 3, kind: .received)
}()
)
XCTAssertEqual(transactions.count, 3)
XCTAssertEqual(transactions[0].minedHeight, 663974)
XCTAssertEqual(transactions[0].isSentTransaction, false)
XCTAssertEqual(transactions[1].minedHeight, 663953)
XCTAssertEqual(transactions[1].isSentTransaction, false)
XCTAssertEqual(transactions[2].minedHeight, 663229)
XCTAssertEqual(transactions[2].isSentTransaction, false)
}
func testFindById() {
var transaction: TransactionEntity?
XCTAssertNoThrow(try { transaction = try self.transactionRepository.findBy(id: 10) }())
guard let transaction = transaction else {
XCTFail("transaction is nil")
return
}
var transaction: ZcashTransaction.Overview!
XCTAssertNoThrow(try { transaction = try self.transactionRepository.find(id: 10) }())
XCTAssertEqual(transaction.id, 10)
XCTAssertEqual(transaction.minedHeight, 663942)
XCTAssertEqual(transaction.transactionIndex, 5)
XCTAssertEqual(transaction.index, 5)
}
func testFindByTxId() {
var transaction: TransactionEntity?
var transaction: ZcashTransaction.Overview!
let id = Data(fromHexEncodedString: "01af48bcc4e9667849a073b8b5c539a0fc19de71aac775377929dc6567a36eff")!
XCTAssertNoThrow(
try { transaction = try self.transactionRepository.findBy(rawId: id) }()
)
XCTAssertNoThrow(try { transaction = try self.transactionRepository.find(rawID: id) }())
guard let transaction = transaction else {
XCTFail("transaction is nil")
return
}
XCTAssertEqual(transaction.id, 8)
XCTAssertEqual(transaction.minedHeight, 663922)
XCTAssertEqual(transaction.transactionIndex, 1)
XCTAssertEqual(transaction.index, 1)
}
func testFindAllSentTransactions() {
var transactions: [ConfirmedTransactionEntity]?
XCTAssertNoThrow(try { transactions = try self.transactionRepository.findAllSentTransactions(offset: 0, limit: Int.max) }())
guard let txs = transactions else {
XCTFail("find all sent transactions returned no transactions")
return
}
XCTAssertEqual(txs.count, 13)
var transactions: [ZcashTransaction.Overview] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.find(offset: 0, limit: Int.max, kind: .sent) }())
XCTAssertEqual(transactions.count, 13)
transactions.forEach { XCTAssertEqual($0.isSentTransaction, true) }
}
func testFindAllReceivedTransactions() {
var transactions: [ConfirmedTransactionEntity]?
XCTAssertNoThrow(try { transactions = try self.transactionRepository.findAllReceivedTransactions(offset: 0, limit: Int.max) }())
guard let txs = transactions else {
XCTFail("find all received transactions returned no transactions")
return
}
XCTAssertEqual(txs.count, 7)
var transactions: [ZcashTransaction.Overview] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.find(offset: 0, limit: Int.max, kind: .received) }())
XCTAssertEqual(transactions.count, 8)
transactions.forEach { XCTAssertEqual($0.isSentTransaction, false) }
}
func testFindAllTransactions() {
var transactions: [ConfirmedTransactionEntity]?
XCTAssertNoThrow(try { transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max) }())
guard let txs = transactions else {
XCTFail("find all transactions returned no transactions")
return
}
XCTAssertEqual(txs.count, 20)
var transactions: [ZcashTransaction.Overview] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.find(offset: 0, limit: Int.max, kind: .all) }())
XCTAssertEqual(transactions.count, 21)
}
func testFindReceivedOffsetLimit() {
var transactions: [ZcashTransaction.Received] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.findReceived(offset: 3, limit: 3) }())
XCTAssertEqual(transactions.count, 3)
XCTAssertEqual(transactions[0].minedHeight, 664022)
XCTAssertEqual(transactions[1].minedHeight, 664012)
XCTAssertEqual(transactions[2].minedHeight, 664003)
}
func testFindSentOffsetLimit() {
var transactions: [ZcashTransaction.Sent] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.findSent(offset: 3, limit: 3) }())
XCTAssertEqual(transactions.count, 3)
XCTAssertEqual(transactions[0].minedHeight, 664022)
XCTAssertEqual(transactions[1].minedHeight, 664012)
XCTAssertEqual(transactions[2].minedHeight, 663956)
}
func testFindMemoForTransaction() {
let transaction = ZcashTransaction.Overview(
blockTime: nil,
expiryHeight: nil,
fee: nil,
id: 9,
index: nil,
isWalletInternal: false,
hasChange: false,
memoCount: 0,
minedHeight: nil,
raw: nil,
rawID: Data(),
receivedNoteCount: 0,
sentNoteCount: 0,
value: Zatoshi.zero
)
var memos: [Memo]!
XCTAssertNoThrow(try { memos = try self.transactionRepository.findMemos(for: transaction) }())
XCTAssertEqual(memos.count, 1)
XCTAssertEqual(memos[0].toString(), "Some funds")
}
func testFindMemoForReceivedTransaction() {
let transaction = ZcashTransaction.Received(
blockTime: 1,
expiryHeight: nil,
fromAccount: 0,
id: 9,
index: 0,
memoCount: 0,
minedHeight: 0,
noteCount: 0,
raw: nil,
rawID: nil,
value: Zatoshi.zero
)
var memos: [Memo]!
XCTAssertNoThrow(try { memos = try self.transactionRepository.findMemos(for: transaction) }())
XCTAssertEqual(memos.count, 1)
XCTAssertEqual(memos[0].toString(), "Some funds")
}
func testFindMemoForSentTransaction() {
let transaction = ZcashTransaction.Sent(
blockTime: 1,
expiryHeight: nil,
fromAccount: 0,
id: 9,
index: 0,
memoCount: 0,
minedHeight: 0,
noteCount: 0,
raw: nil,
rawID: nil,
value: Zatoshi.zero
)
var memos: [Memo]!
XCTAssertNoThrow(try { memos = try self.transactionRepository.findMemos(for: transaction) }())
XCTAssertEqual(memos.count, 1)
XCTAssertEqual(memos[0].toString(), "Some funds")
}
func testFindTransactionWithNULLMinedHeight() {
var transactions: [ZcashTransaction.Overview] = []
XCTAssertNoThrow(try { transactions = try self.transactionRepository.find(offset: 0, limit: 3, kind: .all) }())
XCTAssertEqual(transactions.count, 3)
XCTAssertEqual(transactions[0].id, 21)
XCTAssertEqual(transactions[0].minedHeight, nil)
XCTAssertEqual(transactions[1].id, 20)
XCTAssertEqual(transactions[1].minedHeight, 664037)
XCTAssertEqual(transactions[2].id, 19)
XCTAssertEqual(transactions[2].minedHeight, 664022)
}
func testFindAllPerformance() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
do {
_ = try self.transactionRepository.findAll(offset: 0, limit: Int.max)
_ = try self.transactionRepository.find(offset: 0, limit: Int.max, kind: .all)
} catch {
XCTFail("find all failed")
}
@ -117,62 +223,27 @@ class TransactionRepositoryTests: XCTestCase {
}
func testFindAllFrom() throws {
guard
let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max),
let allFromNil = try self.transactionRepository.findAll(from: nil, limit: Int.max)
else {
return XCTFail("find all failed")
}
XCTAssertEqual(transactions.count, allFromNil.count)
for transaction in transactions {
guard allFromNil.first(where: { $0.rawTransactionId == transaction.rawTransactionId }) != nil else {
XCTFail("not equal")
return
}
}
}
func testFindAllFromSlice() throws {
let limit = 4
let start = 7
guard
let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max),
let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit)
else {
return XCTFail("find all failed")
}
XCTAssertEqual(limit, allFromNil.count)
let slice = transactions[start + 1 ... start + limit]
XCTAssertEqual(slice.count, allFromNil.count)
for transaction in slice {
guard allFromNil.first(where: { $0.rawTransactionId == transaction.rawTransactionId }) != nil else {
XCTFail("not equal")
return
}
}
}
func testFindAllFromLastSlice() throws {
let limit = 5
let start = 15
guard
let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max),
let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit)
else {
return XCTFail("find all failed")
}
var transaction: ZcashTransaction.Overview!
XCTAssertNoThrow(try { transaction = try self.transactionRepository.find(id: 16) }())
let slice = transactions[start + 1 ..< transactions.count]
XCTAssertEqual(slice.count, allFromNil.count)
for transaction in slice {
guard allFromNil.first(where: { $0.rawTransactionId == transaction.rawTransactionId }) != nil else {
XCTFail("not equal")
var transactionsFrom: [ZcashTransaction.Overview] = []
XCTAssertNoThrow(try { transactionsFrom = try self.transactionRepository.find(from: transaction, limit: Int.max, kind: .all) }())
XCTAssertEqual(transactionsFrom.count, 8)
transactionsFrom.forEach { preceededTransaction in
guard let preceededTransactionIndex = preceededTransaction.index, let transactionIndex = transaction.index else {
XCTFail("Transactions are missing indexes.")
return
}
guard let preceededTransactionBlockTime = preceededTransaction.blockTime, let transactionBlockTime = transaction.blockTime else {
XCTFail("Transactions are missing block time.")
return
}
XCTAssertLessThan(preceededTransactionIndex, transactionIndex)
XCTAssertLessThan(preceededTransactionBlockTime, transactionBlockTime)
}
}
}

View File

@ -171,7 +171,7 @@ class DarksideWalletService: LightWalletService {
try await service.submit(spendTransaction: spendTransaction)
}
func fetchTransaction(txId: Data) async throws -> TransactionEntity {
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched {
try await service.fetchTransaction(txId: txId)
}
}

View File

@ -71,7 +71,7 @@ class MockLightWalletService: LightWalletService {
LightWalletServiceMockResponse(errorCode: 0, errorMessage: "", unknownFields: UnknownStorage())
}
func fetchTransaction(txId: Data) async throws -> TransactionEntity {
Transaction(id: 1, transactionId: Data(), created: "Today", transactionIndex: 1, expiryHeight: -1, minedHeight: -1, raw: nil)
func fetchTransaction(txId: Data) async throws -> ZcashTransaction.Fetched {
return ZcashTransaction.Fetched(rawID: Data(), minedHeight: -1, raw: Data())
}
}

View File

@ -8,6 +8,10 @@
import Foundation
@testable import ZcashLightClientKit
enum MockTransactionRepositoryError: Error {
case notImplemented
}
class MockTransactionRepository {
enum Kind {
case sent
@ -18,12 +22,13 @@ class MockTransactionRepository {
var receivedCount: Int
var sentCount: Int
var scannedHeight: BlockHeight
var transactions: [ConfirmedTransactionEntity] = []
var reference: [Kind] = []
var sentTransactions: [ConfirmedTransaction] = []
var receivedTransactions: [ConfirmedTransaction] = []
var network: ZcashNetwork
var transactions: [ZcashTransaction.Overview] = []
var receivedTransactions: [ZcashTransaction.Received] = []
var sentTransactions: [ZcashTransaction.Sent] = []
var allCount: Int {
receivedCount + sentCount
}
@ -42,15 +47,6 @@ class MockTransactionRepository {
self.network = network
}
func generate() {
var txArray: [ConfirmedTransactionEntity] = []
reference = referenceArray()
for index in 0 ..< reference.count {
txArray.append(mockTx(index: index, kind: reference[index]))
}
transactions = txArray
}
func referenceArray() -> [Kind] {
var template: [Kind] = []
@ -64,47 +60,6 @@ class MockTransactionRepository {
return template.shuffled()
}
func mockTx(index: Int, kind: Kind) -> ConfirmedTransactionEntity {
switch kind {
case .received:
return mockReceived(index)
case .sent:
return mockSent(index)
}
}
func mockSent(_ index: Int) -> ConfirmedTransactionEntity {
ConfirmedTransaction(
toAddress: "some_address",
expiryHeight: BlockHeight.max,
minedHeight: randomBlockHeight(),
noteId: index,
blockTimeInSeconds: randomTimeInterval(),
transactionIndex: index,
raw: Data(),
id: index,
value: Zatoshi(Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi)),
memo: nil,
rawTransactionId: Data()
)
}
func mockReceived(_ index: Int) -> ConfirmedTransactionEntity {
ConfirmedTransaction(
toAddress: nil,
expiryHeight: BlockHeight.max,
minedHeight: randomBlockHeight(),
noteId: index,
blockTimeInSeconds: randomTimeInterval(),
transactionIndex: index,
raw: Data(),
id: index,
value: Zatoshi(Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi)),
memo: nil,
rawTransactionId: Data()
)
}
func randomBlockHeight() -> BlockHeight {
BlockHeight.random(in: network.constants.saplingActivationHeight ... 1_000_000)
}
@ -112,12 +67,6 @@ class MockTransactionRepository {
func randomTimeInterval() -> TimeInterval {
Double.random(in: Date().timeIntervalSince1970 - 1000000.0 ... Date().timeIntervalSince1970)
}
func slice(txs: [ConfirmedTransactionEntity], offset: Int, limit: Int) -> [ConfirmedTransactionEntity] {
guard offset < txs.count else { return [] }
return Array(txs[offset ..< min(offset + limit, txs.count - offset)])
}
}
extension MockTransactionRepository.Kind: Equatable {}
@ -138,39 +87,12 @@ extension MockTransactionRepository: TransactionRepository {
nil
}
func findBy(id: Int) throws -> TransactionEntity? {
transactions.first(where: { $0.id == id })?.transactionEntity
func findBy(id: Int) throws -> ZcashTransaction.Overview? {
transactions.first(where: { $0.id == id })
}
func findBy(rawId: Data) throws -> TransactionEntity? {
transactions.first(where: { $0.rawTransactionId == rawId })?.transactionEntity
}
func findAllSentTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
guard let indices = reference.indices(where: { $0 == .sent }) else { return nil }
let sentTxs = indices.map { idx -> ConfirmedTransactionEntity in
transactions[idx]
}
return slice(txs: sentTxs, offset: offset, limit: limit)
}
func findAllReceivedTransactions(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
guard let indices = reference.indices(where: { $0 == .received }) else { return nil }
let receivedTxs = indices.map { idx -> ConfirmedTransactionEntity in
transactions[idx]
}
return slice(txs: receivedTxs, offset: offset, limit: limit)
}
func findAll(offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
transactions
}
func findAll(from: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]? {
nil
func findBy(rawId: Data) throws -> ZcashTransaction.Overview? {
transactions.first(where: { $0.rawID == rawId })
}
func lastScannedHeight() throws -> BlockHeight {
@ -181,20 +103,114 @@ extension MockTransactionRepository: TransactionRepository {
true
}
func findEncodedTransactionBy(txId: Int) -> EncodedTransaction? {
nil
func generate() {
var txArray: [ZcashTransaction.Overview] = []
reference = referenceArray()
for index in 0 ..< reference.count {
txArray.append(mockTx(index: index, kind: reference[index]))
}
transactions = txArray
}
func findTransactions(in range: BlockRange, limit: Int) throws -> [TransactionEntity]? {
nil
func mockTx(index: Int, kind: Kind) -> ZcashTransaction.Overview {
switch kind {
case .received:
return mockReceived(index)
case .sent:
return mockSent(index)
}
}
func findConfirmedTransactionBy(rawId: Data) throws -> ConfirmedTransactionEntity? {
nil
func mockSent(_ index: Int) -> ZcashTransaction.Overview {
return ZcashTransaction.Overview(
blockTime: randomTimeInterval(),
expiryHeight: BlockHeight.max,
fee: Zatoshi(2),
id: index,
index: index,
isWalletInternal: true,
hasChange: true,
memoCount: 0,
minedHeight: randomBlockHeight(),
raw: Data(),
rawID: Data(),
receivedNoteCount: 0,
sentNoteCount: 1,
value: Zatoshi(-Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi))
)
}
func findConfirmedTransactions(in range: BlockRange, offset: Int, limit: Int) throws -> [ConfirmedTransactionEntity]? {
nil
func mockReceived(_ index: Int) -> ZcashTransaction.Overview {
return ZcashTransaction.Overview(
blockTime: randomTimeInterval(),
expiryHeight: BlockHeight.max,
fee: Zatoshi(2),
id: index,
index: index,
isWalletInternal: true,
hasChange: true,
memoCount: 0,
minedHeight: randomBlockHeight(),
raw: Data(),
rawID: Data(),
receivedNoteCount: 1,
sentNoteCount: 0,
value: Zatoshi(Int64.random(in: 1 ... Zatoshi.Constants.oneZecInZatoshi))
)
}
func slice(txs: [ZcashTransaction.Overview], offset: Int, limit: Int) -> [ZcashTransaction.Overview] {
guard offset < txs.count else { return [] }
return Array(txs[offset ..< min(offset + limit, txs.count - offset)])
}
func find(id: Int) throws -> ZcashTransaction.Overview {
guard let transaction = transactions.first(where: { $0.id == id }) else {
throw TransactionRepositoryError.notFound
}
return transaction
}
func find(rawID: Data) throws -> ZcashTransaction.Overview {
guard let transaction = transactions.first(where: { $0.rawID == rawID }) else {
throw TransactionRepositoryError.notFound
}
return transaction
}
func find(offset: Int, limit: Int, kind: TransactionKind) throws -> [ZcashLightClientKit.ZcashTransaction.Overview] {
throw MockTransactionRepositoryError.notImplemented
}
func find(in range: BlockRange, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview] {
throw MockTransactionRepositoryError.notImplemented
}
func find(from: ZcashTransaction.Overview, limit: Int, kind: TransactionKind) throws -> [ZcashTransaction.Overview] {
throw MockTransactionRepositoryError.notImplemented
}
func findReceived(offset: Int, limit: Int) throws -> [ZcashTransaction.Received] {
throw MockTransactionRepositoryError.notImplemented
}
func findSent(offset: Int, limit: Int) throws -> [ZcashTransaction.Sent] {
throw MockTransactionRepositoryError.notImplemented
}
func findMemos(for transaction: ZcashLightClientKit.ZcashTransaction.Overview) throws -> [ZcashLightClientKit.Memo] {
throw MockTransactionRepositoryError.notImplemented
}
func findMemos(for receivedTransaction: ZcashLightClientKit.ZcashTransaction.Received) throws -> [ZcashLightClientKit.Memo] {
throw MockTransactionRepositoryError.notImplemented
}
func findMemos(for sentTransaction: ZcashLightClientKit.ZcashTransaction.Sent) throws -> [ZcashLightClientKit.Memo] {
throw MockTransactionRepositoryError.notImplemented
}
}