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:
commit
43e00ecb62
|
@ -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"
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)) })
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct EncodedTransaction: SignedTransactionEntity {
|
||||
struct EncodedTransaction {
|
||||
var transactionId: Data
|
||||
var raw: Data?
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/*
|
||||
There’s 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)
|
||||
/*
|
||||
There’s 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
|
||||
}
|
||||
|
||||
/*
|
||||
There’s 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))
|
||||
)
|
||||
/*
|
||||
There’s 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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue