Merge pull request #567 from nuttycom/pending_transaction_entity_recipient

Add internal recipient information to `PendingTransactionEntity`
This commit is contained in:
Francisco Gindre 2022-10-21 18:27:52 -03:00 committed by GitHub
commit 37b060852c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 690 additions and 265 deletions

View File

@ -86,8 +86,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"branch" : "bin/librustzcash_0_7",
"revision" : "e8fbb84c1bec44af9dbef7e27c85f25e8f51a5af"
"revision" : "0059f090e655667f9ee5ed3306bd87ca78c7711a"
}
}
],

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0"),
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", branch: "bin/librustzcash_0_7")
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "0059f090e655667f9ee5ed3306bd87ca78c7711a")
],
targets: [
.target(
@ -44,7 +44,9 @@ let package = Package(
resources: [
.copy("Resources/test_data.db"),
.copy("Resources/cache.db"),
.copy("Resources/ZcashSdk_Data.db"),
.copy("Resources/darkside_caches.db"),
.copy("Resources/darkside_data.db"),
.copy("Resources/darkside_pending.db")
]
),
.testTarget(

View File

@ -9,17 +9,19 @@ import Foundation
import SQLite
class MigrationManager {
enum CacheDbMigration: Int32 {
case none = 0
}
enum PendingDbMigration: Int32 {
enum CacheDbMigration: Int32, CaseIterable {
case none = 0
}
static let latestCacheDbMigrationVersion: Int32 = CacheDbMigration.none.rawValue
static let latestPendingDbMigrationVersion: Int32 = PendingDbMigration.none.rawValue
enum PendingDbMigration: Int32, CaseIterable {
case none = 0
case v1 = 1
case v2 = 2
}
static let nextCacheDbMigration: CacheDbMigration = CacheDbMigration.none
static let nextPendingDbMigration: PendingDbMigration = PendingDbMigration.v2
var cacheDb: ConnectionProvider
var pendingDb: ConnectionProvider
var network: NetworkType
@ -34,7 +36,7 @@ class MigrationManager {
self.network = networkType
}
func performMigration(ufvks: [UnifiedFullViewingKey]) throws {
func performMigration() throws {
try migrateCacheDb()
try migratePendingDb()
}
@ -42,34 +44,129 @@ class MigrationManager {
private extension MigrationManager {
func migratePendingDb() throws {
// getUserVersion returns a default value of zero for an unmigrated database.
let currentPendingDbVersion = try pendingDb.connection().getUserVersion()
LoggerProxy.debug(
"Attempting to perform migration for pending Db - currentVersion: \(currentPendingDbVersion)." +
"Latest version is: \(Self.latestPendingDbMigrationVersion)"
"Latest version is: \(Self.nextPendingDbMigration.rawValue - 1)"
)
if currentPendingDbVersion < Self.latestPendingDbMigrationVersion {
// perform no migration just adjust the version number
try self.cacheDb.connection().setUserVersion(PendingDbMigration.none.rawValue)
} else {
LoggerProxy.debug("PendingDb Db - no migration needed")
for v in (currentPendingDbVersion..<Self.nextPendingDbMigration.rawValue) {
switch PendingDbMigration(rawValue: v) {
case .some(.none):
try migratePendingDbV1()
case .some(.v1):
try migratePendingDbV2()
case .some(.v2):
// we have no migrations to run after v2; this case should ordinarily be
// unreachable due to the bound on the loop.
break
case nil:
throw StorageError.migrationFailedWithMessage(message: "Invalid migration version: \(v).")
}
}
}
func migratePendingDbV1() throws {
let statement = PendingTransactionSQLDAO.table.create(ifNotExists: true) { createdTable in
createdTable.column(PendingTransactionSQLDAO.TableColumns.id, primaryKey: .autoincrement)
createdTable.column(PendingTransactionSQLDAO.TableColumns.toAddress)
createdTable.column(PendingTransactionSQLDAO.TableColumns.accountIndex)
createdTable.column(PendingTransactionSQLDAO.TableColumns.minedHeight)
createdTable.column(PendingTransactionSQLDAO.TableColumns.expiryHeight)
createdTable.column(PendingTransactionSQLDAO.TableColumns.cancelled)
createdTable.column(PendingTransactionSQLDAO.TableColumns.encodeAttempts, defaultValue: 0)
createdTable.column(PendingTransactionSQLDAO.TableColumns.errorMessage)
createdTable.column(PendingTransactionSQLDAO.TableColumns.errorCode)
createdTable.column(PendingTransactionSQLDAO.TableColumns.submitAttempts, defaultValue: 0)
createdTable.column(PendingTransactionSQLDAO.TableColumns.createTime)
createdTable.column(PendingTransactionSQLDAO.TableColumns.rawTransactionId)
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) {
try pendingDb.connection().execute(statement);
try pendingDb.connection().setUserVersion(PendingDbMigration.v1.rawValue);
}
}
func migratePendingDbV2() throws {
try pendingDb.connection().transaction(.immediate) {
let statement =
"""
ALTER TABLE pending_transactions RENAME TO pending_transactions_old;
CREATE TABLE pending_transactions(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
to_address TEXT,
to_internal INTEGER,
account_index INTEGER NOT NULL,
mined_height INTEGER,
expiry_height INTEGER,
cancelled INTEGER,
encode_attempts INTEGER DEFAULT (0),
error_message TEXT,
error_code INTEGER,
submit_attempts INTEGER DEFAULT (0),
create_time REAL,
txid BLOB,
value INTEGER NOT NULL,
raw BLOB,
memo BLOB,
fee INTEGER
);
INSERT INTO pending_transactions
SELECT
id,
to_address,
NULL,
account_index,
mined_height,
expiry_height,
cancelled,
encode_attempts,
error_message,
error_code,
submit_attempts,
create_time,
txid,
value,
raw,
memo,
NULL
FROM pending_transactions_old;
DROP TABLE pending_transactions_old
"""
try pendingDb.connection().execute(statement);
try pendingDb.connection().setUserVersion(PendingDbMigration.v2.rawValue);
}
}
func migrateCacheDb() throws {
// getUserVersion returns a default value of zero for an unmigrated database.
let currentCacheDbVersion = try cacheDb.connection().getUserVersion()
LoggerProxy.debug(
"Attempting to perform migration for cache Db - currentVersion: \(currentCacheDbVersion)." +
"Latest version is: \(Self.latestCacheDbMigrationVersion)"
"Latest version is: \(Self.nextCacheDbMigration.rawValue)"
)
if currentCacheDbVersion < Self.latestCacheDbMigrationVersion {
// perform no migration just adjust the version number
try self.cacheDb.connection().setUserVersion(CacheDbMigration.none.rawValue)
} else {
LoggerProxy.debug("Cache Db - no migration needed")
for v in (currentCacheDbVersion..<Self.nextCacheDbMigration.rawValue) {
switch CacheDbMigration(rawValue: v) {
case .some(.none):
// we have no migrations to run; this case should ordinarily be
// unreachable due to the bound on the loop.
break
case nil:
throw StorageError.migrationFailedWithMessage(message: "Invalid migration version: \(v).")
}
}
}
}
@ -81,8 +178,8 @@ extension Connection {
}
return Int32(version)
}
func setUserVersion(_ version: Int32) throws {
try run("PRAGMA user_version = \(version)")
try execute("PRAGMA user_version = \(version)")
}
}

View File

@ -1059,14 +1059,6 @@ extension CompactBlockProcessor {
for try await utxo in stream {
utxos.append(utxo)
}
guard try rustBackend.clearUtxos(
dbData: dataDb,
address: tAddress,
sinceHeight: startHeight - 1,
networkType: self.config.network.networkType
) >= 0 else {
throw CompactBlockProcessorError.generalError(message: "attempted to clear utxos but -1 was returned")
}
return storeUTXOs(utxos, in: dataDb)
} catch {
throw mapError(error)

View File

@ -30,21 +30,6 @@ extension CompactBlockProcessor {
}
.flatMap({ $0 })
do {
for tAddress in tAddresses {
guard try rustBackend.clearUtxos(
dbData: config.dataDb,
address: tAddress,
sinceHeight: config.walletBirthday - 1,
networkType: config.network.networkType
) >= 0 else {
throw rustBackend.lastError() ?? .genericError(message: "clearUtxos failed. no error message available")
}
}
} catch {
throw FetchUTXOError.clearingFailed(error)
}
var utxos: [UnspentTransactionOutputEntity] = []
let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses.map { $0.stringEncoded }, startHeight: config.walletBirthday)
for try await transaction in stream {

View File

@ -31,6 +31,14 @@ extension NetworkType {
default: return nil
}
}
static func forNetworkId(_ id: UInt32) -> NetworkType? {
switch id {
case 1: return .mainnet
case 0: return .testnet
default: return nil
}
}
}
public enum ZcashNetworkBuilder {

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
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.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

@ -11,6 +11,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case toAddress = "to_address"
case toInternalAccount = "to_internal"
case accountIndex = "account_index"
case minedHeight = "mined_height"
case expiryHeight = "expiry_height"
@ -25,9 +26,10 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
case value
case memo
case rawTransactionId = "txid"
case fee
}
var toAddress: String
var recipient: PendingTransactionRecipient
var accountIndex: Int
var minedHeight: BlockHeight
var expiryHeight: BlockHeight
@ -42,10 +44,11 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
var value: Zatoshi
var memo: Data?
var rawTransactionId: Data?
var fee: Zatoshi?
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
PendingTransaction(
toAddress: entity.toAddress,
recipient: entity.recipient,
accountIndex: entity.accountIndex,
minedHeight: entity.minedHeight,
expiryHeight: entity.expiryHeight,
@ -59,12 +62,13 @@ 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(
toAddress: String,
recipient: PendingTransactionRecipient,
accountIndex: Int,
minedHeight: BlockHeight,
expiryHeight: BlockHeight,
@ -78,9 +82,10 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
id: Int?,
value: Zatoshi,
memo: Data?,
rawTransactionId: Data?
rawTransactionId: Data?,
fee: Zatoshi?
) {
self.toAddress = toAddress
self.recipient = recipient
self.accountIndex = accountIndex
self.minedHeight = minedHeight
self.expiryHeight = expiryHeight
@ -95,12 +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)
self.toAddress = try container.decode(String.self, forKey: .toAddress)
switch (toAddress, toInternalAccount) {
case let (.some(address), nil):
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)
@ -112,17 +132,30 @@ 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)
if let feeValue = try container.decodeIfPresent(Int64.self, forKey: .fee) {
self.fee = Zatoshi(feeValue)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var toAddress: String?
var accountId: Int?
switch (self.recipient) {
case .address(let recipient):
toAddress = recipient.stringEncoded
case .internalAccount(let acct):
accountId = Int(acct)
}
try container.encode(self.toAddress, forKey: .toAddress)
try container.encodeIfPresent(toAddress, forKey: .toAddress)
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)
@ -137,17 +170,18 @@ 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?.amount, forKey: .fee)
}
func isSameTransactionId<T>(other: T) -> Bool where T: RawIdentifiable {
self.rawTransactionId == other.rawTransactionId
}
}
extension PendingTransaction {
init(value: Zatoshi, toAddress: String, memo: MemoBytes, account index: Int) {
init(value: Zatoshi, recipient: PendingTransactionRecipient, memo: MemoBytes, account index: Int) {
self = PendingTransaction(
toAddress: toAddress,
recipient: recipient,
accountIndex: index,
minedHeight: -1,
expiryHeight: -1,
@ -161,14 +195,16 @@ extension PendingTransaction {
id: nil,
value: value,
memo: Data(memo.bytes),
rawTransactionId: nil
rawTransactionId: nil,
fee: nil
)
}
}
class PendingTransactionSQLDAO: PendingTransactionRepository {
enum TableColumns {
static var toAddress = Expression<String>("to_address")
static var toAddress = Expression<String?>("to_address")
static var toInternalAccount = Expression<Int?>("to_internal")
static var accountIndex = Expression<Int>("account_index")
static var minedHeight = Expression<Int?>("mined_height")
static var expiryHeight = Expression<Int?>("expiry_height")
@ -183,42 +219,21 @@ 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")
}
let table = Table("pending_transactions")
static let table = Table("pending_transactions")
var dbProvider: ConnectionProvider
init(dbProvider: ConnectionProvider) {
self.dbProvider = dbProvider
}
func createrTableIfNeeded() throws {
let statement = table.create(ifNotExists: true) { createdTable in
createdTable.column(TableColumns.id, primaryKey: .autoincrement)
createdTable.column(TableColumns.toAddress)
createdTable.column(TableColumns.accountIndex)
createdTable.column(TableColumns.minedHeight)
createdTable.column(TableColumns.expiryHeight)
createdTable.column(TableColumns.cancelled)
createdTable.column(TableColumns.encodeAttempts, defaultValue: 0)
createdTable.column(TableColumns.errorMessage)
createdTable.column(TableColumns.errorCode)
createdTable.column(TableColumns.submitAttempts, defaultValue: 0)
createdTable.column(TableColumns.createTime)
createdTable.column(TableColumns.rawTransactionId)
createdTable.column(TableColumns.value)
createdTable.column(TableColumns.raw)
createdTable.column(TableColumns.memo)
}
try dbProvider.connection().run(statement)
}
func create(_ transaction: PendingTransactionEntity) throws -> Int {
let pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
return try Int(dbProvider.connection().run(table.insert(pendingTx)))
return try Int(dbProvider.connection().run(Self.table.insert(pendingTx)))
}
func update(_ transaction: PendingTransactionEntity) throws {
@ -226,8 +241,8 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let id = pendingTx.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
let updatedRows = try dbProvider.connection().run(table.filter(TableColumns.id == id).update(pendingTx))
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,9 +252,9 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let id = transaction.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
do {
try dbProvider.connection().run(table.filter(TableColumns.id == id).delete())
try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).delete())
} catch {
throw StorageError.updateFailed
}
@ -251,18 +266,18 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
guard let txId = pendingTx.id else {
throw StorageError.malformedEntity(fields: ["id"])
}
try dbProvider.connection().run(table.filter(TableColumns.id == txId).update(pendingTx))
try dbProvider.connection().run(Self.table.filter(TableColumns.id == txId).update(pendingTx))
}
func find(by id: Int) throws -> PendingTransactionEntity? {
guard let row = try dbProvider.connection().pluck(table.filter(TableColumns.id == id).limit(1)) else {
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
@ -270,15 +285,15 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
}
func getAll() throws -> [PendingTransactionEntity] {
let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(table).map { row in
let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(Self.table).map { row in
try row.decode()
}
return allTxs
}
func applyMinedHeight(_ height: BlockHeight, id: Int) throws {
let transaction = table.filter(TableColumns.id == id)
let transaction = Self.table.filter(TableColumns.id == id)
let updatedRows = try dbProvider.connection()
.run(transaction.update([TableColumns.minedHeight <- height]))

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

@ -8,7 +8,7 @@
import Foundation
import SQLite
struct Transaction: TransactionEntity, Decodable {
struct Transaction: TransactionEntity {
enum CodingKeys: String, CodingKey {
case id = "id_tx"
case transactionId = "txid"
@ -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,36 @@ struct Transaction: TransactionEntity, Decodable {
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 {
@ -40,6 +71,7 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
var value: Zatoshi
var memo: Data?
var rawTransactionId: Data?
var fee: Zatoshi?
}
class TransactionSQLDAO: TransactionRepository {
@ -51,6 +83,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
@ -115,11 +148,12 @@ extension TransactionSQLDAO {
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.address AS toAddress,
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
blocks.time AS blockTimeInSeconds,
transactions.fee AS fee
FROM transactions
INNER JOIN sent_notes
ON transactions.id_tx = sent_notes.tx
@ -153,7 +187,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
@ -184,7 +219,7 @@ extension TransactionSQLDAO {
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.address AS toAddress,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
@ -197,7 +232,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
@ -205,8 +241,8 @@ extension TransactionSQLDAO {
ON transactions.id_tx = sent_notes.tx
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (sent_notes.address IS NULL AND received_notes.is_change != 1)
OR sent_notes.address IS NOT NULL
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,
@ -231,7 +267,7 @@ extension TransactionSQLDAO {
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.address AS toAddress,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
@ -244,7 +280,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
@ -253,8 +290,8 @@ extension TransactionSQLDAO {
LEFT JOIN blocks
ON transactions.block = blocks.height
WHERE (\(fromTransaction.blockTimeInSeconds), \(fromTransaction.transactionIndex)) > (blocktimeinseconds, transactionIndex) AND
(sent_notes.address IS NULL AND received_notes.is_change != 1)
OR sent_notes.address IS NOT NULL
((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,
@ -274,7 +311,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)
@ -297,7 +335,7 @@ extension TransactionSQLDAO {
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.address AS toAddress,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
@ -310,7 +348,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
@ -320,8 +359,8 @@ extension TransactionSQLDAO {
ON transactions.block = blocks.height
WHERE (\(range.start.height) <= minedheight
AND minedheight <= \(range.end.height)) AND
(sent_notes.address IS NULL AND received_notes.is_change != 1)
OR sent_notes.address IS NOT NULL
(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,
@ -342,7 +381,7 @@ extension TransactionSQLDAO {
transactions.txid AS rawTransactionId,
transactions.expiry_height AS expiryHeight,
transactions.raw AS raw,
sent_notes.address AS toAddress,
sent_notes.to_address AS toAddress,
CASE
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
ELSE received_notes.value
@ -355,7 +394,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
@ -365,8 +405,8 @@ extension TransactionSQLDAO {
ON transactions.block = blocks.height
WHERE minedheight >= 0
AND rawTransactionId == \(Blob(bytes: rawId.bytes)) AND
(sent_notes.address IS NULL AND received_notes.is_change != 1)
OR sent_notes.address IS NOT NULL
(sent_notes.to_address IS NULL AND received_notes.is_change != 1)
OR sent_notes.to_address IS NOT NULL
LIMIT 1
"""
)

View File

@ -6,6 +6,12 @@
//
import Foundation
public enum PendingTransactionRecipient: Equatable {
case address(Recipient)
case internalAccount(UInt32)
}
/**
Represents a sent transaction that has not been confirmed yet on the blockchain
*/
@ -13,12 +19,12 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
/**
recipient address
*/
var toAddress: String { get set }
var recipient: PendingTransactionRecipient { get }
/**
index of the account from which the funds were sent
*/
var accountIndex: Int { get set }
var accountIndex: Int { get }
/**
height which the block was mined at.
@ -34,7 +40,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
/**
value is 1 if the transaction was cancelled
*/
var cancelled: Int { get set }
var cancelled: Int { get }
/**
how many times this transaction encoding was attempted
@ -61,7 +67,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
- Note: represented in timeIntervalySince1970
*/
var createTime: TimeInterval { get set }
var createTime: TimeInterval { get }
/**
Checks whether this transaction is the same as the given transaction
@ -182,7 +188,8 @@ public extension PendingTransactionEntity {
transactionIndex: -1,
expiryHeight: self.expiryHeight,
minedHeight: self.minedHeight,
raw: self.raw
raw: self.raw,
fee: self.fee
)
}
}
@ -199,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

@ -245,6 +245,14 @@ public class Initializer {
throw rustBackend.lastError() ?? InitializerError.accountInitFailed
}
let migrationManager = MigrationManager(
cacheDbConnection: SimpleConnectionProvider(path: cacheDbURL.path),
pendingDbConnection: SimpleConnectionProvider(path: pendingDbURL.path),
networkType: self.network.networkType
)
try migrationManager.performMigration()
return .success
}

View File

@ -1,6 +1,6 @@
//
// WalletTypes.swift
//
//
//
// Created by Francisco Gindre on 4/6/21.
//
@ -94,6 +94,34 @@ public struct SaplingExtendedFullViewingKey: Equatable, StringEncoded, Undescrib
}
}
public enum AddressType: Equatable {
case p2pkh
case p2sh
case sapling
case unified
var id: UInt32 {
switch self {
case .p2pkh: return 0
case .p2sh: return 1
case .sapling: return 2
case .unified: return 3
}
}
}
extension AddressType {
static func forId(_ id: UInt32) -> AddressType? {
switch id {
case 0: return .p2pkh
case 1: return .p2sh
case 2: return .sapling
case 3: return .unified
default: return nil
}
}
}
/// A Transparent Address that can be encoded as a String
///
/// Transactions sent to this address are totally visible in the public
@ -125,7 +153,7 @@ public struct TransparentAddress: Equatable, StringEncoded, Comparable {
/// Represents a Sapling receiver address. Comonly called zAddress.
/// This address corresponds to the Zcash Sapling shielded pool.
/// Although this it is fully functional, we encourage developers to
/// choose `UnifiedAddress` before Sapling or Transparent ones.
/// choose `UnifiedAddress` before Sapling or Transparent ones.
public struct SaplingAddress: Equatable, StringEncoded {
var encoding: String
@ -238,13 +266,25 @@ public enum Recipient: Equatable, StringEncoded {
throw KeyEncodingError.invalidEncoding
}
}
static func forEncodedAddress(encoded: String) -> (Recipient, NetworkType)? {
return DerivationTool.getAddressMetadata(encoded).map { m in
switch m.addressType {
case .p2pkh: return (.transparent(TransparentAddress(validatedEncoding: encoded)),
m.networkType)
case .p2sh: return (.transparent(TransparentAddress(validatedEncoding: encoded)), m.networkType)
case .sapling: return (.sapling(SaplingAddress(validatedEncoding: encoded)), m.networkType)
case .unified: return (.unified(UnifiedAddress(validatedEncoding: encoded)), m.networkType)
}
}
}
}
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

@ -10,30 +10,9 @@ import Foundation
import libzcashlc
class ZcashRustBackend: ZcashRustBackendWelding {
static let minimumConfirmations: UInt32 = 10
static func clearUtxos(
dbData: URL,
address: TransparentAddress,
sinceHeight: BlockHeight,
networkType: NetworkType
) throws -> Int32 {
let dbData = dbData.osStr()
let result = zcashlc_clear_utxos(
dbData.0,
dbData.1,
[CChar](address.stringEncoded.utf8CString),
Int32(sinceHeight),
networkType.networkId
)
guard result >= 0 else {
throw lastError() ?? .genericError(message: "No error message available")
}
return result
}
static func createAccount(dbData: URL, seed: [UInt8], networkType: NetworkType) throws -> UnifiedSpendingKey {
let dbData = dbData.osStr()
@ -69,7 +48,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
zcashlc_create_to_address(
dbData.0,
dbData.1,
Int32(usk.account),
uskPtr.baseAddress,
UInt(usk.bytes.count),
[CChar](address.utf8CString),
@ -339,6 +317,26 @@ class ZcashRustBackend: ZcashRustBackendWelding {
return RustWeldingError.genericError(message: message)
}
static func getAddressMetadata(_ address: String) -> AddressMetadata? {
var networkId: UInt32 = 0
var addrId: UInt32 = 0
guard zcashlc_get_address_metadata(
[CChar](address.utf8CString),
&networkId,
&addrId
) else {
return nil
}
guard let network = NetworkType.forNetworkId(networkId),
let addrType = AddressType.forId(addrId)
else {
return nil
}
return AddressMetadata(network: network, addrType: addrType)
}
static func getTransparentReceiver(for uAddr: UnifiedAddress) throws -> TransparentAddress? {
guard let transparentCStr = zcashlc_get_transparent_receiver_for_unified_address(
[CChar](uAddr.encoding.utf8CString)
@ -597,7 +595,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
zcashlc_shield_funds(
dbData.0,
dbData.1,
Int32(usk.account),
uskBuffer.baseAddress,
UInt(usk.bytes.count),
memo.bytes,

View File

@ -31,23 +31,6 @@ public enum DbInitResult {
}
protocol ZcashRustBackendWelding {
/// clears the cached utxos for the given address from the specified height on for the
/// provided addresses. This will clear all UTXOs for the address from the database.
/// if there are unspent funds, the balance will be zero after clearing up UTXOs,
/// needing to put them back again to restore the balance (if they weren't spent)
/// - Parameters:
/// - dbData: location of the data db file
/// - address: the address of the UTXO
/// - sinceheight: clear the UXTOs from that address on
/// - networkType: network type of this key
/// - Returns: the amount of UTXOs cleared or -1 on error
static func clearUtxos(
dbData: URL,
address: TransparentAddress,
sinceHeight: BlockHeight,
networkType: NetworkType
) throws -> Int32
/// Adds the next available account-level spend authority, given the current set of [ZIP 316]
/// account identifiers known, to the wallet database.
///
@ -278,6 +261,10 @@ protocol ZcashRustBackendWelding {
networkType: NetworkType
) throws -> DbInitResult
/// Returns the network and address type for the given Zcash address string,
/// if the string represents a valid Zcash address.
static func getAddressMetadata(_ address: String) -> AddressMetadata?
/// Validates the if the given string is a valid Sapling Address
/// - Parameter address: UTF-8 encoded String to validate
/// - Parameter networkType: network type of this key

View File

@ -468,11 +468,11 @@ public class SDKSynchronizer: Synchronizer {
} catch {
throw SynchronizerError.parameterMissing(underlyingError: error)
}
return try await createToAddress(
spendingKey: spendingKey,
zatoshi: zatoshi,
toAddress: toAddress.stringEncoded,
recipient: toAddress,
memo: memo
)
}
@ -492,13 +492,7 @@ public class SDKSynchronizer: Synchronizer {
throw ShieldFundsError.insuficientTransparentFunds
}
// FIXME: Define who's the recipient of a shielding transaction #521
// https://github.com/zcash/ZcashLightClientKit/issues/521
guard let uAddr = self.getUnifiedAddress(accountIndex: accountIndex) else {
throw ShieldFundsError.shieldingFailed(underlyingError: KeyEncodingError.invalidEncoding)
}
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, toAddress: uAddr.stringEncoded, memo: try memo.asMemoBytes(), from: accountIndex)
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, recipient: .internalAccount(spendingKey.account), memo: try memo.asMemoBytes(), from: accountIndex)
// TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
let transaction = try await transactionManager.encodeShieldingTransaction(
@ -515,13 +509,13 @@ public class SDKSynchronizer: Synchronizer {
func createToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
toAddress: String,
recipient: Recipient,
memo: Memo
) async throws -> PendingTransactionEntity {
do {
let spend = try transactionManager.initSpend(
zatoshi: zatoshi,
toAddress: toAddress,
recipient: .address(recipient),
memo: memo.asMemoBytes(),
from: Int(spendingKey.account)
)

View File

@ -69,6 +69,10 @@ public class DerivationTool: KeyDeriving {
try rustwelding.getTransparentReceiver(for: unifiedAddress)
}
public static func getAddressMetadata(_ addr: String) -> AddressMetadata? {
rustwelding.getAddressMetadata(addr)
}
/// Given a spending key, return the associated viewing key.
/// - Parameter spendingKey: the `UnifiedSpendingKey` from which to derive the `UnifiedFullViewingKey` from.
/// - Returns: the viewing key that corresponds to the spending key.
@ -106,6 +110,16 @@ public class DerivationTool: KeyDeriving {
}
}
public struct AddressMetadata {
var networkType: NetworkType
var addressType: AddressType
public init(network: NetworkType, addrType: AddressType) {
self.networkType = network
self.addressType = addrType
}
}
extension DerivationTool: KeyValidation {
public func isValidUnifiedFullViewingKey(_ ufvk: String) -> Bool {
DerivationTool.rustwelding.isValidUnifiedFullViewingKey(ufvk, networkType: networkType)

View File

@ -8,7 +8,7 @@
import Foundation
enum TransactionManagerError: Error {
case couldNotCreateSpend(toAddress: String, account: Int, zatoshi: Zatoshi)
case couldNotCreateSpend(recipient: PendingTransactionRecipient, account: Int, zatoshi: Zatoshi)
case encodingFailed(PendingTransactionEntity)
case updateFailed(PendingTransactionEntity)
case notPending(PendingTransactionEntity)
@ -16,6 +16,7 @@ enum TransactionManagerError: Error {
case internalInconsistency(PendingTransactionEntity)
case submitFailed(PendingTransactionEntity, errorCode: Int)
case shieldingEncodingFailed(PendingTransactionEntity, reason: String)
case cannotEncodeInternalTx(PendingTransactionEntity)
}
class PersistentTransactionManager: OutboundTransactionManager {
@ -40,7 +41,7 @@ class PersistentTransactionManager: OutboundTransactionManager {
func initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: PendingTransactionRecipient,
memo: MemoBytes,
from accountIndex: Int
) throws -> PendingTransactionEntity {
@ -48,14 +49,14 @@ class PersistentTransactionManager: OutboundTransactionManager {
by: try repository.create(
PendingTransaction(
value: zatoshi,
toAddress: toAddress,
recipient: recipient,
memo: memo,
account: accountIndex
)
)
) else {
throw TransactionManagerError.couldNotCreateSpend(
toAddress: toAddress,
recipient: recipient,
account: accountIndex,
zatoshi: zatoshi
)
@ -102,10 +103,18 @@ class PersistentTransactionManager: OutboundTransactionManager {
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
var toAddress: String?
switch (pendingTransaction.recipient) {
case .address(let addr):
toAddress = addr.stringEncoded
case .internalAccount(_):
throw TransactionManagerError.cannotEncodeInternalTx(pendingTransaction)
}
let encodedTransaction = try await self.encoder.createTransaction(
spendingKey: spendingKey,
zatoshi: pendingTransaction.value,
to: pendingTransaction.toAddress,
to: toAddress!,
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
from: pendingTransaction.accountIndex
)
@ -260,9 +269,7 @@ enum OutboundTransactionManagerBuilder {
enum PendingTransactionRepositoryBuilder {
static func build(initializer: Initializer) throws -> PendingTransactionRepository {
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
try dao.createrTableIfNeeded()
return dao
PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
}
}

View File

@ -16,7 +16,7 @@ transactions through to completion.
protocol OutboundTransactionManager {
func initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: PendingTransactionRecipient,
memo: MemoBytes,
from accountIndex: Int
) throws -> PendingTransactionEntity

View File

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

View File

@ -16,8 +16,8 @@ class NotesRepositoryTests: XCTestCase {
override func setUp() {
super.setUp()
sentNotesRepository = TestDbBuilder.sentNotesRepository()
receivedNotesRepository = TestDbBuilder.receivedNotesRepository()
sentNotesRepository = try! TestDbBuilder.sentNotesRepository()
receivedNotesRepository = try! TestDbBuilder.receivedNotesRepository()
}
override func tearDown() {
@ -29,12 +29,12 @@ class NotesRepositoryTests: XCTestCase {
func testSentCount() {
var count: Int?
XCTAssertNoThrow(try { count = try sentNotesRepository.count() }())
XCTAssertEqual(count, 0)
XCTAssertEqual(count, 13)
}
func testReceivedCount() {
var count: Int?
XCTAssertNoThrow(try { count = try receivedNotesRepository.count() }())
XCTAssertEqual(count, 27)
XCTAssertEqual(count, 22)
}
}

View File

@ -12,16 +12,17 @@ import XCTest
// swiftlint:disable force_try force_unwrapping implicitly_unwrapped_optional
class PendingTransactionRepositoryTests: XCTestCase {
let dbUrl = try! TestDbBuilder.pendingTransactionsDbURL()
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let recipient = SaplingAddress(validatedEncoding: "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6")
var pendingRepository: PendingTransactionRepository!
override func setUp() {
super.setUp()
cleanUpDb()
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString))
try! dao.createrTableIfNeeded()
pendingRepository = dao
let pendingDbProvider = SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString)
let migrations = try! MigrationManager(cacheDbConnection: InMemoryDbProvider(), pendingDbConnection: pendingDbProvider, networkType: .testnet)
try! migrations.performMigration()
pendingRepository = PendingTransactionSQLDAO(dbProvider: pendingDbProvider)
}
override func tearDown() {
@ -51,7 +52,7 @@ class PendingTransactionRepositoryTests: XCTestCase {
XCTAssertEqual(transaction.accountIndex, expected.accountIndex)
XCTAssertEqual(transaction.value, expected.value)
XCTAssertEqual(transaction.toAddress, expected.toAddress)
XCTAssertEqual(transaction.recipient, expected.recipient)
}
func testFindById() {
@ -124,8 +125,6 @@ class PendingTransactionRepositoryTests: XCTestCase {
}
func testUpdate() {
let newAccountIndex = 1
let newValue = Zatoshi(123_456)
let transaction = createAndStoreMockedTransaction()
guard let id = transaction.id else {
@ -141,9 +140,12 @@ class PendingTransactionRepositoryTests: XCTestCase {
XCTFail("failed to store tx")
return
}
let oldEncodeAttempts = stored!.encodeAttempts
let oldSubmitAttempts = stored!.submitAttempts
stored!.accountIndex = newAccountIndex
stored!.value = newValue
stored!.encodeAttempts += 1
stored!.submitAttempts += 5
XCTAssertNoThrow(try pendingRepository.update(stored!))
@ -152,9 +154,9 @@ class PendingTransactionRepositoryTests: XCTestCase {
return
}
XCTAssertEqual(updatedTransaction.value, newValue)
XCTAssertEqual(updatedTransaction.accountIndex, newAccountIndex)
XCTAssertEqual(updatedTransaction.toAddress, stored!.toAddress)
XCTAssertEqual(updatedTransaction.encodeAttempts, oldEncodeAttempts + 1)
XCTAssertEqual(updatedTransaction.submitAttempts, oldSubmitAttempts + 5)
XCTAssertEqual(updatedTransaction.recipient, stored!.recipient)
}
func createAndStoreMockedTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
@ -174,6 +176,6 @@ class PendingTransactionRepositoryTests: XCTestCase {
}
private func mockTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
PendingTransaction(value: value, toAddress: recipientAddress, memo: .empty(), account: 0)
PendingTransaction(value: value, recipient: .address(.sapling(recipient)), memo: .empty(), account: 0)
}
}

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)
}
}

View File

@ -15,7 +15,8 @@ class TransactionRepositoryTests: XCTestCase {
override func setUp() {
super.setUp()
transactionRepository = TestDbBuilder.transactionRepository()
transactionRepository = try! TestDbBuilder.transactionRepository()
}
override func tearDown() {
@ -27,7 +28,7 @@ class TransactionRepositoryTests: XCTestCase {
var count: Int?
XCTAssertNoThrow(try { count = try self.transactionRepository.countAll() }())
XCTAssertNotNil(count)
XCTAssertEqual(count, 27)
XCTAssertEqual(count, 21)
}
func testCountUnmined() {
@ -46,14 +47,14 @@ class TransactionRepositoryTests: XCTestCase {
}
XCTAssertEqual(transaction.id, 10)
XCTAssertEqual(transaction.minedHeight, 652812)
XCTAssertEqual(transaction.minedHeight, 663942)
XCTAssertEqual(transaction.transactionIndex, 5)
}
func testFindByTxId() {
var transaction: TransactionEntity?
let id = Data(fromHexEncodedString: "0BAFC5B83F5B39A5270144ECD98DBC65115055927EDDA8FF20F081FFF13E4780")!
let id = Data(fromHexEncodedString: "01af48bcc4e9667849a073b8b5c539a0fc19de71aac775377929dc6567a36eff")!
XCTAssertNoThrow(
try { transaction = try self.transactionRepository.findBy(rawId: id) }()
@ -64,9 +65,9 @@ class TransactionRepositoryTests: XCTestCase {
return
}
XCTAssertEqual(transaction.id, 10)
XCTAssertEqual(transaction.minedHeight, 652812)
XCTAssertEqual(transaction.transactionIndex, 5)
XCTAssertEqual(transaction.id, 8)
XCTAssertEqual(transaction.minedHeight, 663922)
XCTAssertEqual(transaction.transactionIndex, 1)
}
func testFindAllSentTransactions() {
@ -77,7 +78,7 @@ class TransactionRepositoryTests: XCTestCase {
return
}
XCTAssertEqual(txs.count, 0)
XCTAssertEqual(txs.count, 13)
}
func testFindAllReceivedTransactions() {
@ -88,7 +89,7 @@ class TransactionRepositoryTests: XCTestCase {
return
}
XCTAssertEqual(txs.count, 27)
XCTAssertEqual(txs.count, 7)
}
func testFindAllTransactions() {
@ -99,7 +100,7 @@ class TransactionRepositoryTests: XCTestCase {
return
}
XCTAssertEqual(txs.count, 27)
XCTAssertEqual(txs.count, 20)
}
func testFindAllPerformance() {
@ -156,15 +157,15 @@ class TransactionRepositoryTests: XCTestCase {
}
func testFindAllFromLastSlice() throws {
let limit = 10
let start = 20
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")
}
let slice = transactions[start + 1 ..< transactions.count]
XCTAssertEqual(slice.count, allFromNil.count)
for transaction in slice {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -55,6 +55,10 @@ extension LightWalletServiceMockResponse {
}
class MockRustBackend: ZcashRustBackendWelding {
static func getAddressMetadata(_ address: String) -> ZcashLightClientKit.AddressMetadata? {
nil
}
static func clearUtxos(dbData: URL, address: ZcashLightClientKit.TransparentAddress, sinceHeight: ZcashLightClientKit.BlockHeight, networkType: ZcashLightClientKit.NetworkType) throws -> Int32 {
0
}

View File

@ -70,29 +70,40 @@ class TestDbBuilder {
}
static func prePopulatedMainnetDataDbURL() -> URL? {
Bundle.module.url(forResource: "ZcashSdk_Data", withExtension: "db")
Bundle.module.url(forResource: "darkside_data", withExtension: "db")
}
static func prepopulatedDataDbProvider() -> ConnectionProvider? {
static func prepopulatedDataDbProvider() throws -> ConnectionProvider? {
guard let url = prePopulatedMainnetDataDbURL() else { return nil }
let provider = SimpleConnectionProvider(path: url.absoluteString, readonly: true)
return provider
let initResult = try ZcashRustBackend.initDataDb(
dbData: url,
seed: TestSeed().seed(),
networkType: .mainnet
)
switch (initResult) {
case .success: return provider
case .seedRequired:
throw StorageError.migrationFailedWithMessage(message: "Seed value required to initialize the wallet database")
}
}
static func transactionRepository() -> TransactionRepository? {
guard let provider = prepopulatedDataDbProvider() else { return nil }
static func transactionRepository() throws -> TransactionRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return TransactionSQLDAO(dbProvider: provider)
}
static func sentNotesRepository() -> SentNotesRepository? {
guard let provider = prepopulatedDataDbProvider() else { return nil }
static func sentNotesRepository() throws -> SentNotesRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return SentNotesSQLDAO(dbProvider: provider)
}
static func receivedNotesRepository() -> ReceivedNoteRepository? {
guard let provider = prepopulatedDataDbProvider() else { return nil }
static func receivedNotesRepository() throws -> ReceivedNoteRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return ReceivedNotesSQLDAO(dbProvider: provider)
}

View File

@ -0,0 +1,176 @@
//
// TestDbBuilder.swift
// ZcashLightClientKitTests
//
// Created by Francisco Gindre on 10/14/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
import SQLite
@testable import ZcashLightClientKit
struct TestDbHandle {
var originalDb: URL
var readWriteDb: URL
init(originalDb: URL) {
self.originalDb = originalDb
// avoid files clashing because crashing tests failed to remove previous ones by incrementally changing the filename
self.readWriteDb = FileManager.default.temporaryDirectory
.appendingPathComponent(
self.originalDb.lastPathComponent.appending("_\(Date().timeIntervalSince1970)")
)
}
func setUp() throws {
try FileManager.default.copyItem(at: originalDb, to: readWriteDb)
}
func dispose() {
try? FileManager.default.removeItem(at: readWriteDb)
}
func connectionProvider(readwrite: Bool = true) -> ConnectionProvider {
SimpleConnectionProvider(path: self.readWriteDb.absoluteString, readonly: !readwrite)
}
}
// This requires reference semantics, an enum cannot be used
// swiftlint:disable:next convenience_type
class TestDbBuilder {
enum TestBuilderError: Error {
case generalError
}
static func inMemoryCompactBlockStorage() throws -> CompactBlockStorage {
let compactBlockDao = CompactBlockStorage(connectionProvider: try InMemoryDbProvider())
try compactBlockDao.createTable()
return compactBlockDao
}
static func diskCompactBlockStorage(at url: URL) throws -> CompactBlockStorage {
let compactBlockDao = CompactBlockStorage(connectionProvider: SimpleConnectionProvider(path: url.absoluteString))
try compactBlockDao.createTable()
return compactBlockDao
}
static func pendingTransactionsDbURL() throws -> URL {
try __documentsDirectory().appendingPathComponent("pending.db")
}
static func prePopulatedCacheDbURL() -> URL? {
Bundle.module.url(forResource: "cache", withExtension: "db")
}
static func prePopulatedDataDbURL() -> URL? {
Bundle.module.url(forResource: "test_data", withExtension: "db")
}
<<<<<<< Updated upstream
static func prePopulatedMainnetDataDbURL() -> URL? {
Bundle.module.url(forResource: "ZcashSdk_Data", withExtension: "db")
}
static func prepopulatedDataDbProvider() throws -> ConnectionProvider? {
guard let url = prePopulatedMainnetDataDbURL() else { return nil }
let provider = SimpleConnectionProvider(path: url.absoluteString, readonly: true)
let initResult = try ZcashRustBackend.initDataDb(dbData: url, seed: nil, networkType: NetworkType.testnet)
=======
static func prePopulatedMainnetDataDbURL() -> URL? {
Bundle.module.url(forResource: "darkside_data", withExtension: "db")
}
static func prepopulatedDataDbProvider() throws -> ConnectionProvider? {
guard let url = prePopulatedMainnetDataDbURL() else { return nil }
let provider = SimpleConnectionProvider(path: url.absoluteString, readonly: true)
let initResult = try ZcashRustBackend.initDataDb(
dbData: url,
seed: TestSeed().seed(),
networkType: .mainnet
)
>>>>>>> Stashed changes
switch (initResult) {
case .success: return provider
case .seedRequired:
throw StorageError.migrationFailedWithMessage(message: "Seed value required to initialize the wallet database")
}
}
static func transactionRepository() throws -> TransactionRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return TransactionSQLDAO(dbProvider: provider)
}
static func sentNotesRepository() throws -> SentNotesRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return SentNotesSQLDAO(dbProvider: provider)
}
static func receivedNotesRepository() throws -> ReceivedNoteRepository? {
guard let provider = try prepopulatedDataDbProvider() else { return nil }
return ReceivedNotesSQLDAO(dbProvider: provider)
}
static func seed(db: CompactBlockRepository, with blockRange: CompactBlockRange) async throws {
guard let blocks = StubBlockCreator.createBlockRange(blockRange) else {
throw TestBuilderError.generalError
}
try await db.write(blocks: blocks)
}
}
struct InMemoryDbProvider: ConnectionProvider {
var readonly: Bool
var conn: Connection
init(readonly: Bool = false) throws {
self.readonly = readonly
self.conn = try Connection(.inMemory, readonly: readonly)
}
func connection() throws -> Connection {
self.conn
}
}
enum StubBlockCreator {
static func createRandomDataBlock(with height: BlockHeight) -> ZcashCompactBlock? {
guard let data = randomData(ofLength: 100) else {
LoggerProxy.debug("error creating stub block")
return nil
}
return ZcashCompactBlock(height: height, data: data)
}
static func createBlockRange(_ range: CompactBlockRange) -> [ZcashCompactBlock]? {
var blocks: [ZcashCompactBlock] = []
for height in range {
guard let block = createRandomDataBlock(with: height) else {
return nil
}
blocks.append(block)
}
return blocks
}
static func randomData(ofLength length: Int) -> Data? {
var bytes = [UInt8](repeating: 0, count: length)
let status = SecRandomCopyBytes(kSecRandomDefault, length, &bytes)
if status == errSecSuccess {
return Data(bytes: &bytes, count: bytes.count)
}
LoggerProxy.debug("Status \(status)")
return nil
}
}