2019-11-26 14:32:20 -08:00
|
|
|
//
|
|
|
|
// PendingTransactionDao.swift
|
|
|
|
// ZcashLightClientKit
|
|
|
|
//
|
|
|
|
// Created by Francisco Gindre on 11/19/19.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
2019-12-03 07:19:44 -08:00
|
|
|
import SQLite
|
2022-05-31 05:27:24 -07:00
|
|
|
struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
enum CodingKeys: String, CodingKey {
|
|
|
|
case toAddress = "to_address"
|
2022-09-30 06:45:51 -07:00
|
|
|
case toInternalAccount = "to_internal"
|
2019-12-03 07:19:44 -08:00
|
|
|
case accountIndex = "account_index"
|
|
|
|
case minedHeight = "mined_height"
|
|
|
|
case expiryHeight = "expiry_height"
|
|
|
|
case cancelled
|
|
|
|
case encodeAttempts = "encode_attempts"
|
|
|
|
case submitAttempts = "submit_attempts"
|
|
|
|
case errorMessage = "error_message"
|
|
|
|
case errorCode = "error_code"
|
|
|
|
case createTime = "create_time"
|
|
|
|
case raw
|
|
|
|
case id
|
|
|
|
case value
|
|
|
|
case memo
|
|
|
|
case rawTransactionId = "txid"
|
|
|
|
}
|
|
|
|
|
2022-09-30 06:45:51 -07:00
|
|
|
var recipient: PendingTransactionRecipient
|
2019-12-03 07:19:44 -08:00
|
|
|
var accountIndex: Int
|
|
|
|
var minedHeight: BlockHeight
|
|
|
|
var expiryHeight: BlockHeight
|
|
|
|
var cancelled: Int
|
|
|
|
var encodeAttempts: Int
|
|
|
|
var submitAttempts: Int
|
|
|
|
var errorMessage: String?
|
|
|
|
var errorCode: Int?
|
|
|
|
var createTime: TimeInterval
|
|
|
|
var raw: Data?
|
|
|
|
var id: Int?
|
2022-06-22 12:45:37 -07:00
|
|
|
var value: Zatoshi
|
2019-12-03 07:19:44 -08:00
|
|
|
var memo: Data?
|
|
|
|
var rawTransactionId: Data?
|
2021-09-17 06:49:58 -07:00
|
|
|
|
|
|
|
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
|
|
|
|
PendingTransaction(
|
2022-09-30 06:45:51 -07:00
|
|
|
recipient: entity.recipient,
|
2021-09-17 06:49:58 -07:00
|
|
|
accountIndex: entity.accountIndex,
|
|
|
|
minedHeight: entity.minedHeight,
|
|
|
|
expiryHeight: entity.expiryHeight,
|
|
|
|
cancelled: entity.cancelled,
|
|
|
|
encodeAttempts: entity.encodeAttempts,
|
|
|
|
submitAttempts: entity.submitAttempts,
|
|
|
|
errorMessage: entity.errorMessage,
|
|
|
|
errorCode: entity.errorCode,
|
|
|
|
createTime: entity.createTime,
|
|
|
|
raw: entity.raw,
|
|
|
|
id: entity.id,
|
|
|
|
value: entity.value,
|
2022-05-31 05:27:24 -07:00
|
|
|
memo: entity.memo == nil ? Data(MemoBytes.empty().bytes) : entity.memo,
|
2021-09-17 06:49:58 -07:00
|
|
|
rawTransactionId: entity.raw
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-27 08:51:13 -07:00
|
|
|
init(
|
2022-09-30 06:45:51 -07:00
|
|
|
recipient: PendingTransactionRecipient,
|
2022-06-27 08:51:13 -07:00
|
|
|
accountIndex: Int,
|
|
|
|
minedHeight: BlockHeight,
|
|
|
|
expiryHeight: BlockHeight,
|
|
|
|
cancelled: Int,
|
|
|
|
encodeAttempts: Int,
|
|
|
|
submitAttempts: Int,
|
|
|
|
errorMessage: String?,
|
|
|
|
errorCode: Int?,
|
|
|
|
createTime: TimeInterval,
|
|
|
|
raw: Data?,
|
|
|
|
id: Int?,
|
|
|
|
value: Zatoshi,
|
|
|
|
memo: Data?,
|
|
|
|
rawTransactionId: Data?
|
|
|
|
) {
|
2022-09-30 06:45:51 -07:00
|
|
|
self.recipient = recipient
|
2022-06-27 08:51:13 -07:00
|
|
|
self.accountIndex = accountIndex
|
|
|
|
self.minedHeight = minedHeight
|
|
|
|
self.expiryHeight = expiryHeight
|
|
|
|
self.cancelled = cancelled
|
|
|
|
self.encodeAttempts = encodeAttempts
|
|
|
|
self.submitAttempts = submitAttempts
|
|
|
|
self.errorMessage = errorMessage
|
|
|
|
self.errorCode = errorCode
|
|
|
|
self.createTime = createTime
|
|
|
|
self.raw = raw
|
|
|
|
self.id = id
|
|
|
|
self.value = value
|
|
|
|
self.memo = memo
|
|
|
|
self.rawTransactionId = rawTransactionId
|
|
|
|
}
|
|
|
|
|
|
|
|
init(from decoder: Decoder) throws {
|
|
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
|
2022-09-30 06:45:51 -07:00
|
|
|
let toAddress: String? = try container.decodeIfPresent(String.self, forKey: .toAddress)
|
2022-10-19 13:13:18 -07:00
|
|
|
let toInternalAccount: Int? = try container.decodeIfPresent(Int.self, forKey: .toInternalAccount)
|
2022-09-30 06:45:51 -07:00
|
|
|
|
|
|
|
switch (toAddress, toInternalAccount) {
|
|
|
|
case let (.some(address), nil):
|
|
|
|
self.recipient = .address(Recipient.forEncodedAddress(encoded: address)!.0)
|
|
|
|
case let (nil, .some(accountId)):
|
2022-10-19 13:13:18 -07:00
|
|
|
self.recipient = .internalAccount(UInt32(accountId))
|
2022-09-30 06:45:51 -07:00
|
|
|
default:
|
|
|
|
throw StorageError.malformedEntity(fields: ["toAddress", "toInternalAccount"])
|
|
|
|
}
|
|
|
|
|
2022-06-27 08:51:13 -07:00
|
|
|
self.accountIndex = try container.decode(Int.self, forKey: .accountIndex)
|
|
|
|
self.minedHeight = try container.decode(BlockHeight.self, forKey: .minedHeight)
|
|
|
|
self.expiryHeight = try container.decode(BlockHeight.self, forKey: .expiryHeight)
|
|
|
|
self.cancelled = try container.decode(Int.self, forKey: .cancelled)
|
|
|
|
self.encodeAttempts = try container.decode(Int.self, forKey: .encodeAttempts)
|
|
|
|
self.submitAttempts = try container.decode(Int.self, forKey: .submitAttempts)
|
|
|
|
self.errorMessage = try container.decodeIfPresent(String.self, forKey: .errorMessage)
|
|
|
|
self.errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
|
|
|
|
self.createTime = try container.decode(TimeInterval.self, forKey: .createTime)
|
|
|
|
self.raw = try container.decodeIfPresent(Data.self, forKey: .raw)
|
|
|
|
self.id = try container.decodeIfPresent(Int.self, forKey: .id)
|
|
|
|
|
|
|
|
let zatoshiValue = try container.decode(Int64.self, forKey: .value)
|
|
|
|
self.value = Zatoshi(zatoshiValue)
|
|
|
|
self.memo = try container.decodeIfPresent(Data.self, forKey: .memo)
|
|
|
|
self.rawTransactionId = try container.decodeIfPresent(Data.self, forKey: .rawTransactionId)
|
|
|
|
}
|
|
|
|
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
|
2022-09-30 06:45:51 -07:00
|
|
|
var toAddress: String?
|
2022-10-19 13:13:18 -07:00
|
|
|
var accountId: Int?
|
2022-09-30 06:45:51 -07:00
|
|
|
switch (self.recipient) {
|
|
|
|
case .address(let recipient):
|
|
|
|
toAddress = recipient.stringEncoded
|
|
|
|
case .internalAccount(let acct):
|
2022-10-19 13:13:18 -07:00
|
|
|
accountId = Int(acct)
|
2022-09-30 06:45:51 -07:00
|
|
|
}
|
|
|
|
try container.encode(toAddress, forKey: .toAddress)
|
|
|
|
try container.encode(accountId, forKey: .toInternalAccount)
|
2022-06-27 08:51:13 -07:00
|
|
|
try container.encode(self.accountIndex, forKey: .accountIndex)
|
|
|
|
try container.encode(self.minedHeight, forKey: .minedHeight)
|
|
|
|
try container.encode(self.expiryHeight, forKey: .expiryHeight)
|
|
|
|
try container.encode(self.cancelled, forKey: .cancelled)
|
|
|
|
try container.encode(self.encodeAttempts, forKey: .encodeAttempts)
|
|
|
|
try container.encode(self.submitAttempts, forKey: .submitAttempts)
|
|
|
|
try container.encodeIfPresent(self.errorMessage, forKey: .errorMessage)
|
|
|
|
try container.encodeIfPresent(self.errorCode, forKey: .errorCode)
|
|
|
|
try container.encode(self.createTime, forKey: .createTime)
|
|
|
|
try container.encodeIfPresent(self.raw, forKey: .raw)
|
|
|
|
try container.encodeIfPresent(self.id, forKey: .id)
|
|
|
|
try container.encode(self.value.amount, forKey: .value)
|
|
|
|
try container.encodeIfPresent(self.memo, forKey: .memo)
|
|
|
|
try container.encodeIfPresent(self.rawTransactionId, forKey: .rawTransactionId)
|
|
|
|
}
|
|
|
|
|
2019-12-06 04:38:47 -08:00
|
|
|
func isSameTransactionId<T>(other: T) -> Bool where T: RawIdentifiable {
|
2019-12-03 07:19:44 -08:00
|
|
|
self.rawTransactionId == other.rawTransactionId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension PendingTransaction {
|
2022-09-30 06:45:51 -07:00
|
|
|
init(value: Zatoshi, recipient: PendingTransactionRecipient, memo: MemoBytes, account index: Int) {
|
2021-09-17 06:49:58 -07:00
|
|
|
self = PendingTransaction(
|
2022-09-30 06:45:51 -07:00
|
|
|
recipient: recipient,
|
2021-09-17 06:49:58 -07:00
|
|
|
accountIndex: index,
|
|
|
|
minedHeight: -1,
|
|
|
|
expiryHeight: -1,
|
|
|
|
cancelled: 0,
|
|
|
|
encodeAttempts: 0,
|
|
|
|
submitAttempts: 0,
|
|
|
|
errorMessage: nil,
|
|
|
|
errorCode: nil,
|
|
|
|
createTime: Date().timeIntervalSince1970,
|
|
|
|
raw: nil,
|
|
|
|
id: nil,
|
2022-06-22 12:45:37 -07:00
|
|
|
value: value,
|
2022-05-31 05:27:24 -07:00
|
|
|
memo: Data(memo.bytes),
|
2021-09-17 06:49:58 -07:00
|
|
|
rawTransactionId: nil
|
|
|
|
)
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-26 14:32:20 -08:00
|
|
|
class PendingTransactionSQLDAO: PendingTransactionRepository {
|
2021-09-17 06:49:58 -07:00
|
|
|
enum TableColumns {
|
2022-10-18 10:44:36 -07:00
|
|
|
static var toAddress = Expression<String?>("to_address")
|
|
|
|
static var toInternalAccount = Expression<Int?>("to_internal")
|
2019-12-03 07:19:44 -08:00
|
|
|
static var accountIndex = Expression<Int>("account_index")
|
|
|
|
static var minedHeight = Expression<Int?>("mined_height")
|
|
|
|
static var expiryHeight = Expression<Int?>("expiry_height")
|
|
|
|
static var cancelled = Expression<Int?>("cancelled")
|
|
|
|
static var encodeAttempts = Expression<Int?>("encode_attempts")
|
|
|
|
static var errorMessage = Expression<String?>("error_message")
|
|
|
|
static var submitAttempts = Expression<Int?>("submit_attempts")
|
|
|
|
static var errorCode = Expression<Int?>("error_code")
|
|
|
|
static var createTime = Expression<TimeInterval?>("create_time")
|
|
|
|
static var raw = Expression<Blob?>("raw")
|
|
|
|
static var id = Expression<Int>("id")
|
2022-06-22 12:45:37 -07:00
|
|
|
static var value = Expression<Zatoshi>("value")
|
2019-12-03 07:19:44 -08:00
|
|
|
static var memo = Expression<Blob?>("memo")
|
|
|
|
static var rawTransactionId = Expression<Blob?>("txid")
|
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-10-18 10:44:36 -07:00
|
|
|
static let table = Table("pending_transactions")
|
2019-11-26 14:32:20 -08:00
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
var dbProvider: ConnectionProvider
|
|
|
|
|
2019-11-26 14:32:20 -08:00
|
|
|
init(dbProvider: ConnectionProvider) {
|
|
|
|
self.dbProvider = dbProvider
|
|
|
|
}
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
func create(_ transaction: PendingTransactionEntity) throws -> Int {
|
2021-09-15 05:21:29 -07:00
|
|
|
let pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
2019-11-26 14:32:20 -08:00
|
|
|
|
2022-10-18 10:44:36 -07:00
|
|
|
return try Int(dbProvider.connection().run(Self.table.insert(pendingTx)))
|
2019-12-03 07:19:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
func update(_ transaction: PendingTransactionEntity) throws {
|
2021-09-15 05:21:29 -07:00
|
|
|
let pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
|
|
|
guard let id = pendingTx.id else {
|
2019-12-03 07:19:44 -08:00
|
|
|
throw StorageError.malformedEntity(fields: ["id"])
|
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-10-18 10:44:36 -07:00
|
|
|
let updatedRows = try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).update(pendingTx))
|
2020-07-22 12:32:07 -07:00
|
|
|
if updatedRows == 0 {
|
|
|
|
LoggerProxy.error("attempted to update pending transactions but no rows were updated")
|
|
|
|
}
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
func delete(_ transaction: PendingTransactionEntity) throws {
|
2019-12-03 07:19:44 -08:00
|
|
|
guard let id = transaction.id else {
|
2021-09-17 06:49:58 -07:00
|
|
|
throw StorageError.malformedEntity(fields: ["id"])
|
|
|
|
}
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
do {
|
2022-10-18 10:44:36 -07:00
|
|
|
try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).delete())
|
2019-12-03 07:19:44 -08:00
|
|
|
} catch {
|
|
|
|
throw StorageError.updateFailed
|
|
|
|
}
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
func cancel(_ transaction: PendingTransactionEntity) throws {
|
2021-09-15 05:21:29 -07:00
|
|
|
var pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
|
|
|
pendingTx.cancelled = 1
|
|
|
|
guard let txId = pendingTx.id else {
|
2019-12-03 07:19:44 -08:00
|
|
|
throw StorageError.malformedEntity(fields: ["id"])
|
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2022-10-18 10:44:36 -07:00
|
|
|
try dbProvider.connection().run(Self.table.filter(TableColumns.id == txId).update(pendingTx))
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
func find(by id: Int) throws -> PendingTransactionEntity? {
|
2022-10-18 10:44:36 -07:00
|
|
|
guard let row = try dbProvider.connection().pluck(Self.table.filter(TableColumns.id == id).limit(1)) else {
|
2019-12-03 07:19:44 -08:00
|
|
|
return nil
|
|
|
|
}
|
2021-09-17 06:49:58 -07:00
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
do {
|
2021-09-15 05:21:29 -07:00
|
|
|
let pendingTx: PendingTransaction = try row.decode()
|
2022-06-22 12:45:37 -07:00
|
|
|
|
2021-09-15 05:21:29 -07:00
|
|
|
return pendingTx
|
2019-12-03 07:19:44 -08:00
|
|
|
} catch {
|
|
|
|
throw StorageError.operationFailed
|
|
|
|
}
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
func getAll() throws -> [PendingTransactionEntity] {
|
2022-10-18 10:44:36 -07:00
|
|
|
let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(Self.table).map { row in
|
2019-12-03 07:19:44 -08:00
|
|
|
try row.decode()
|
2021-09-17 06:49:58 -07:00
|
|
|
}
|
|
|
|
|
2019-12-03 07:19:44 -08:00
|
|
|
return allTxs
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|
|
|
|
|
2020-07-22 12:32:07 -07:00
|
|
|
func applyMinedHeight(_ height: BlockHeight, id: Int) throws {
|
2022-10-18 10:44:36 -07:00
|
|
|
let transaction = Self.table.filter(TableColumns.id == id)
|
2020-07-22 12:32:07 -07:00
|
|
|
|
2021-09-17 06:49:58 -07:00
|
|
|
let updatedRows = try dbProvider.connection()
|
|
|
|
.run(transaction.update([TableColumns.minedHeight <- height]))
|
2020-07-22 12:32:07 -07:00
|
|
|
|
2021-09-17 06:49:58 -07:00
|
|
|
if updatedRows == 0 {
|
2020-07-22 12:32:07 -07:00
|
|
|
LoggerProxy.error("attempted to update a row but none was updated")
|
|
|
|
}
|
|
|
|
}
|
2019-11-26 14:32:20 -08:00
|
|
|
}
|