434 lines
19 KiB
Swift
434 lines
19 KiB
Swift
//
|
|
// TransactionDao.swift
|
|
// ZcashLightClientKit
|
|
//
|
|
// Created by Francisco Gindre on 11/15/19.
|
|
//
|
|
|
|
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)
|
|
}
|
|
|
|
var dbProvider: ConnectionProvider
|
|
var transactions = Table("transactions")
|
|
private var blockDao: BlockSQLDAO
|
|
|
|
init(dbProvider: ConnectionProvider) {
|
|
self.dbProvider = dbProvider
|
|
self.blockDao = BlockSQLDAO(dbProvider: dbProvider)
|
|
}
|
|
|
|
func blockForHeight(_ height: BlockHeight) throws -> Block? {
|
|
try blockDao.block(at: height)
|
|
}
|
|
|
|
func lastScannedHeight() throws -> BlockHeight {
|
|
try blockDao.latestBlockHeight()
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
extension Data {
|
|
init(blob: SQLite.Blob) {
|
|
let bytes = blob.bytes
|
|
self = Data(bytes: bytes, count: bytes.count)
|
|
}
|
|
|
|
var bytes: [UInt8] {
|
|
return [UInt8](self)
|
|
}
|
|
}
|
|
|
|
extension Array where Element == UInt8 {
|
|
var data: Data {
|
|
return Data(self)
|
|
}
|
|
}
|