Add Fee field to Transaction, ConfirmedTransaction, ReceivedTransactions and Pen

dingTransactions.
Update Notes DAOs with new fields.

Make Zatoshi Codable

add tests for Recipient.forEncodedAddress
This commit is contained in:
Francisco Gindre 2022-10-19 17:28:53 -03:00
parent 849083ffd9
commit 789cf01188
12 changed files with 130 additions and 63 deletions

View File

@ -82,6 +82,7 @@ private extension MigrationManager {
createdTable.column(PendingTransactionSQLDAO.TableColumns.value)
createdTable.column(PendingTransactionSQLDAO.TableColumns.raw)
createdTable.column(PendingTransactionSQLDAO.TableColumns.memo)
createdTable.column(PendingTransactionSQLDAO.TableColumns.fee)
}
try pendingDb.connection().transaction(.immediate) {
@ -112,7 +113,8 @@ private extension MigrationManager {
txid BLOB,
value INTEGER NOT NULL,
raw BLOB,
memo BLOB
memo BLOB,
fee INTEGER
);
INSERT INTO pending_transactions
@ -132,7 +134,8 @@ private extension MigrationManager {
txid,
value,
raw,
memo
memo,
NULL
FROM pending_transactions_old;
DROP TABLE pending_transactions_old

View File

@ -21,6 +21,7 @@ struct ReceivedNote: ReceivedNoteEntity, Codable {
case value
case memo
case spent
case tx
}
var id: Int
var diversifier: Data
@ -33,6 +34,7 @@ struct ReceivedNote: ReceivedNoteEntity, Codable {
var value: Int
var memo: Data?
var spent: Int?
var tx: Int
}
class ReceivedNotesSQLDAO: ReceivedNoteRepository {
@ -77,8 +79,9 @@ struct SentNote: SentNoteEntity, Codable {
case transactionId = "tx"
case outputPool = "output_pool"
case outputIndex = "output_index"
case account = "from_account"
case address = "to_address"
case fromAccount = "from_account"
case toAddress = "to_address"
case toAccount = "to_account"
case value
case memo
}
@ -87,8 +90,9 @@ struct SentNote: SentNoteEntity, Codable {
var transactionId: Int
var outputPool: Int
var outputIndex: Int
var account: Int
var address: String
var fromAccount: Int
var toAddress: String
var toAccount: Int
var value: Int
var memo: Data?
}
@ -126,19 +130,5 @@ class SentNotesSQLDAO: SentNotesRepository {
try row.decode()
}
.first
// try dbProvider.connection().run("""
// SELECT sent_notes.id_note as id,
// sent_notes.tx as transactionId,
// sent_notes.output_index as outputIndex,
// sent_notes.account as account,
// sent_notes.to_address as address,
// sent_notes.value as value,
// sent_notes.memo as memo
// FROM sent_note JOIN transactions
// WHERE sent_note.tx = transactions.id_tx AND
// transactions.txid = \(Blob(bytes: byRawTransactionId.bytes))
// """).map({ row -> SentNoteEntity in
// try row.decode()
// }).first
}
}

View File

@ -26,6 +26,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
case value
case memo
case rawTransactionId = "txid"
case fee
}
var recipient: PendingTransactionRecipient
@ -43,7 +44,8 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
var value: Zatoshi
var memo: Data?
var rawTransactionId: Data?
var fee: Zatoshi?
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
PendingTransaction(
recipient: entity.recipient,
@ -60,10 +62,11 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
id: entity.id,
value: entity.value,
memo: entity.memo == nil ? Data(MemoBytes.empty().bytes) : entity.memo,
rawTransactionId: entity.raw
rawTransactionId: entity.raw,
fee: entity.fee
)
}
init(
recipient: PendingTransactionRecipient,
accountIndex: Int,
@ -79,7 +82,8 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
id: Int?,
value: Zatoshi,
memo: Data?,
rawTransactionId: Data?
rawTransactionId: Data?,
fee: Zatoshi?
) {
self.recipient = recipient
self.accountIndex = accountIndex
@ -96,23 +100,27 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
self.value = value
self.memo = memo
self.rawTransactionId = rawTransactionId
self.fee = fee
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let toAddress: String? = try container.decodeIfPresent(String.self, forKey: .toAddress)
let toInternalAccount: Int? = try container.decodeIfPresent(Int.self, forKey: .toInternalAccount)
switch (toAddress, toInternalAccount) {
case let (.some(address), nil):
self.recipient = .address(Recipient.forEncodedAddress(encoded: address)!.0)
guard let recipient = Recipient.forEncodedAddress(encoded: address) else {
throw StorageError.malformedEntity(fields: ["toAddress"])
}
self.recipient = .address(recipient.0)
case let (nil, .some(accountId)):
self.recipient = .internalAccount(UInt32(accountId))
default:
throw StorageError.malformedEntity(fields: ["toAddress", "toInternalAccount"])
}
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)
@ -124,16 +132,17 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
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)
self.fee = try container.decodeIfPresent(Zatoshi.self, forKey: .fee)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var toAddress: String?
var accountId: Int?
switch (self.recipient) {
@ -142,8 +151,9 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
case .internalAccount(let acct):
accountId = Int(acct)
}
try container.encode(toAddress, forKey: .toAddress)
try container.encode(accountId, forKey: .toInternalAccount)
try container.encodeIfPresent(accountId, forKey: .toInternalAccount)
try container.encode(self.accountIndex, forKey: .accountIndex)
try container.encode(self.minedHeight, forKey: .minedHeight)
try container.encode(self.expiryHeight, forKey: .expiryHeight)
@ -158,8 +168,9 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
try container.encode(self.value.amount, forKey: .value)
try container.encodeIfPresent(self.memo, forKey: .memo)
try container.encodeIfPresent(self.rawTransactionId, forKey: .rawTransactionId)
try container.encodeIfPresent(self.fee, forKey: .fee)
}
func isSameTransactionId<T>(other: T) -> Bool where T: RawIdentifiable {
self.rawTransactionId == other.rawTransactionId
}
@ -182,7 +193,8 @@ extension PendingTransaction {
id: nil,
value: value,
memo: Data(memo.bytes),
rawTransactionId: nil
rawTransactionId: nil,
fee: nil
)
}
}
@ -205,12 +217,13 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
static var value = Expression<Zatoshi>("value")
static var memo = Expression<Blob?>("memo")
static var rawTransactionId = Expression<Blob?>("txid")
static var fee = Expression<Zatoshi?>("fee")
}
static let table = Table("pending_transactions")
var dbProvider: ConnectionProvider
init(dbProvider: ConnectionProvider) {
self.dbProvider = dbProvider
}
@ -226,7 +239,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let id = pendingTx.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
let updatedRows = try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).update(pendingTx))
if updatedRows == 0 {
LoggerProxy.error("attempted to update pending transactions but no rows were updated")
@ -237,7 +250,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let id = transaction.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
do {
try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).delete())
} catch {
@ -251,7 +264,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let txId = pendingTx.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
try dbProvider.connection().run(Self.table.filter(TableColumns.id == txId).update(pendingTx))
}
@ -259,10 +272,10 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let row = try dbProvider.connection().pluck(Self.table.filter(TableColumns.id == id).limit(1)) else {
return nil
}
do {
let pendingTx: PendingTransaction = try row.decode()
return pendingTx
} catch {
throw StorageError.operationFailed
@ -273,7 +286,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(Self.table).map { row in
try row.decode()
}
return allTxs
}

View File

@ -21,6 +21,7 @@ enum TransactionBuilder {
case memo
case noteId
case blockTimeInSeconds
case fee
}
enum ReceivedColumns: Int {
@ -33,6 +34,7 @@ enum TransactionBuilder {
case memo
case noteId
case blockTimeInSeconds
case fee
}
enum TransactionEntityColumns: Int {
@ -42,6 +44,7 @@ enum TransactionBuilder {
case txid
case expiryHeight
case raw
case fee
}
static func createTransactionEntity(txId: Data, rawTransaction: RawTransaction) -> TransactionEntity {
@ -52,7 +55,8 @@ enum TransactionBuilder {
transactionIndex: nil,
expiryHeight: nil,
minedHeight: Int(exactly: rawTransaction.height),
raw: rawTransaction.data
raw: rawTransaction.data,
fee: nil
)
}
@ -73,7 +77,8 @@ enum TransactionBuilder {
transactionIndex: bindings[TransactionEntityColumns.txIndex.rawValue] as? Int,
expiryHeight: bindings[TransactionEntityColumns.expiryHeight.rawValue] as? Int,
minedHeight: bindings[TransactionEntityColumns.minedHeight.rawValue] as? Int,
raw: rawData
raw: rawData,
fee: (bindings[TransactionEntityColumns.fee.rawValue] as? Int?)?.flatMap({ Zatoshi(Int64($0)) })
)
}
@ -110,7 +115,7 @@ enum TransactionBuilder {
if let txIdBlob = bindings[ConfirmedColumns.rawTransactionId.rawValue] as? Blob {
transactionId = Data(blob: txIdBlob)
}
return ConfirmedTransaction(
toAddress: toAddress,
expiryHeight: expiryHeight,
@ -122,7 +127,8 @@ enum TransactionBuilder {
id: Int(id),
value: Zatoshi(value),
memo: memo,
rawTransactionId: transactionId
rawTransactionId: transactionId,
fee: (bindings[ConfirmedColumns.fee.rawValue] as? Int).map({ Zatoshi(Int64($0)) })
)
}
@ -163,7 +169,8 @@ enum TransactionBuilder {
id: Int(id),
value: Zatoshi(value),
memo: memo,
rawTransactionId: transactionId
rawTransactionId: transactionId,
fee: (bindings[ReceivedColumns.fee.rawValue] as? Int).map({ Zatoshi(Int64($0)) })
)
}
}

View File

@ -17,6 +17,7 @@ struct Transaction: TransactionEntity, Decodable {
case expiryHeight = "expiry_height"
case minedHeight = "block"
case raw
case fee
}
var id: Int?
@ -26,6 +27,7 @@ struct Transaction: TransactionEntity, Decodable {
var expiryHeight: BlockHeight?
var minedHeight: BlockHeight?
var raw: Data?
var fee: Zatoshi?
}
struct ConfirmedTransaction: ConfirmedTransactionEntity {
@ -40,6 +42,7 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
var value: Zatoshi
var memo: Data?
var rawTransactionId: Data?
var fee: Zatoshi?
}
class TransactionSQLDAO: TransactionRepository {
@ -51,6 +54,7 @@ class TransactionSQLDAO: TransactionRepository {
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
@ -119,7 +123,8 @@ extension TransactionSQLDAO {
sent_notes.value AS value,
sent_notes.memo AS memo,
sent_notes.id_note AS noteId,
blocks.time AS blockTimeInSeconds
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
INNER JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
@ -153,7 +158,8 @@ extension TransactionSQLDAO {
received_notes.value AS value,
received_notes.memo AS memo,
received_notes.id_note AS noteId,
blocks.time AS blockTimeInSeconds
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
@ -197,7 +203,8 @@ extension TransactionSQLDAO {
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
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
@ -244,7 +251,8 @@ extension TransactionSQLDAO {
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
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
@ -274,7 +282,8 @@ extension TransactionSQLDAO {
transactions.tx_index AS transactionIndex,
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw
transactions.raw AS raw,
transactions.fee AS fee
FROM transactions
WHERE \(range.start.height) <= minedheight
AND minedheight <= \(range.end.height)
@ -310,7 +319,8 @@ extension TransactionSQLDAO {
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
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx
@ -355,7 +365,8 @@ extension TransactionSQLDAO {
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
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
LEFT JOIN received_notes
ON transactions.id_tx = received_notes.tx

View File

@ -188,7 +188,8 @@ public extension PendingTransactionEntity {
transactionIndex: -1,
expiryHeight: self.expiryHeight,
minedHeight: self.minedHeight,
raw: self.raw
raw: self.raw,
fee: self.fee
)
}
}
@ -205,7 +206,8 @@ public extension ConfirmedTransactionEntity {
transactionIndex: self.transactionIndex,
expiryHeight: self.expiryHeight,
minedHeight: self.minedHeight,
raw: self.raw
raw: self.raw,
fee: self.fee
)
}
}

View File

@ -11,8 +11,8 @@ protocol SentNoteEntity {
var id: Int { get set }
var transactionId: Int { get set }
var outputIndex: Int { get set }
var account: Int { get set }
var address: String { get set }
var fromAccount: Int { get set }
var toAddress: String { get set }
var value: Int { get set }
var memo: Data? { get set }
}
@ -22,8 +22,8 @@ extension SentNoteEntity {
guard lhs.id == rhs.id,
lhs.transactionId == rhs.transactionId,
lhs.outputIndex == rhs.outputIndex,
lhs.account == rhs.account,
lhs.address == rhs.address,
lhs.fromAccount == rhs.fromAccount,
lhs.toAddress == rhs.toAddress,
lhs.value == rhs.value,
lhs.memo == rhs.memo else { return false }
return true
@ -33,8 +33,8 @@ extension SentNoteEntity {
hasher.combine(id)
hasher.combine(transactionId)
hasher.combine(outputIndex)
hasher.combine(account)
hasher.combine(address)
hasher.combine(fromAccount)
hasher.combine(toAddress)
hasher.combine(value)
if let memo = memo {
hasher.combine(memo)

View File

@ -93,6 +93,8 @@ public protocol AbstractTransaction {
data containing the memo if any
*/
var memo: Data? { get set }
var fee: Zatoshi? { get set }
}
/**

View File

@ -280,7 +280,7 @@ public struct WalletBalance: Equatable {
public var verified: Zatoshi
public var total: Zatoshi
public init(verified: Zatoshi, total: Zatoshi) {
public init(verified: Zatoshi, total: Zatoshi) {
self.verified = verified
self.total = total
}

View File

@ -89,3 +89,20 @@ public extension NSDecimalNumber {
self.roundedZec.stringValue
}
}
extension Zatoshi: Codable {
enum CodingKeys: String, CodingKey {
case amount
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.amount = try values.decode(Int64.self, forKey: .amount)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.amount, forKey: .amount)
}
}

View File

@ -188,4 +188,14 @@ class ZcashRustBackendTests: XCTestCase {
)
)
}
func testGetMetadataFromAddress() throws {
let recipientAddress = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a"
let metadata = ZcashRustBackend.getAddressMetadata(recipientAddress)
XCTAssertEqual(metadata?.networkType, .mainnet)
XCTAssertEqual(metadata?.addressType, .sapling)
}
}

View File

@ -31,4 +31,16 @@ final class RecipientTests: XCTestCase {
XCTAssertEqual(try Recipient(transparentString, network: .mainnet), .transparent(expectedTransparentAddress))
}
func testRecipentFromEncoding() throws {
let address = "zs17mg40levjezevuhdp5pqrd52zere7r7vrjgdwn5sj4xsqtm20euwahv9anxmwr3y3kmwuz8k55a"
let recipient = Recipient.forEncodedAddress(
encoded: address
)
XCTAssertEqual(recipient?.0, .sapling(SaplingAddress(validatedEncoding: address)))
XCTAssertEqual(recipient?.1, .mainnet)
}
}