diff --git a/Package.resolved b/Package.resolved index c8a2f042..11a56554 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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" } } ], diff --git a/Package.swift b/Package.swift index 889f434a..1f23d8f1 100644 --- a/Package.swift +++ b/Package.swift @@ -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( diff --git a/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift b/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift index 5af74216..963cb547 100644 --- a/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift +++ b/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift @@ -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..= 0 else { - throw CompactBlockProcessorError.generalError(message: "attempted to clear utxos but -1 was returned") - } return storeUTXOs(utxos, in: dataDb) } catch { throw mapError(error) diff --git a/Sources/ZcashLightClientKit/Block/Processor/FetchUnspentTxOutputs.swift b/Sources/ZcashLightClientKit/Block/Processor/FetchUnspentTxOutputs.swift index 04484829..108c40a6 100644 --- a/Sources/ZcashLightClientKit/Block/Processor/FetchUnspentTxOutputs.swift +++ b/Sources/ZcashLightClientKit/Block/Processor/FetchUnspentTxOutputs.swift @@ -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 = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses.map { $0.stringEncoded }, startHeight: config.walletBirthday) for try await transaction in stream { diff --git a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift index 970f95c7..0e28c4a3 100644 --- a/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift +++ b/Sources/ZcashLightClientKit/Constants/ZcashSDK.swift @@ -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 { diff --git a/Sources/ZcashLightClientKit/DAO/NotesDao.swift b/Sources/ZcashLightClientKit/DAO/NotesDao.swift index 38470adb..b6147b48 100644 --- a/Sources/ZcashLightClientKit/DAO/NotesDao.swift +++ b/Sources/ZcashLightClientKit/DAO/NotesDao.swift @@ -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 } } diff --git a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift index adf6f8fd..ccc42c8c 100644 --- a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift +++ b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift @@ -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(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("to_address") + static var toAddress = Expression("to_address") + static var toInternalAccount = Expression("to_internal") static var accountIndex = Expression("account_index") static var minedHeight = Expression("mined_height") static var expiryHeight = Expression("expiry_height") @@ -183,42 +219,21 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { static var value = Expression("value") static var memo = Expression("memo") static var rawTransactionId = Expression("txid") + static var fee = Expression("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])) diff --git a/Sources/ZcashLightClientKit/DAO/TransactionBuilder.swift b/Sources/ZcashLightClientKit/DAO/TransactionBuilder.swift index 0cf3f48a..de33db38 100644 --- a/Sources/ZcashLightClientKit/DAO/TransactionBuilder.swift +++ b/Sources/ZcashLightClientKit/DAO/TransactionBuilder.swift @@ -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)) }) ) } } diff --git a/Sources/ZcashLightClientKit/DAO/TransactionDao.swift b/Sources/ZcashLightClientKit/DAO/TransactionDao.swift index 57c66f9c..c4295ac9 100644 --- a/Sources/ZcashLightClientKit/DAO/TransactionDao.swift +++ b/Sources/ZcashLightClientKit/DAO/TransactionDao.swift @@ -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(Transaction.CodingKeys.expiryHeight.rawValue) static var minedHeight = Expression(Transaction.CodingKeys.minedHeight.rawValue) static var raw = Expression(Transaction.CodingKeys.raw.rawValue) + static var fee = Expression(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 """ ) diff --git a/Sources/ZcashLightClientKit/Entity/PendingTransactionEntity.swift b/Sources/ZcashLightClientKit/Entity/PendingTransactionEntity.swift index 572f36f4..90d5799b 100644 --- a/Sources/ZcashLightClientKit/Entity/PendingTransactionEntity.swift +++ b/Sources/ZcashLightClientKit/Entity/PendingTransactionEntity.swift @@ -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 ) } } diff --git a/Sources/ZcashLightClientKit/Entity/SentNoteEntity.swift b/Sources/ZcashLightClientKit/Entity/SentNoteEntity.swift index 2962ccb8..15fb20aa 100644 --- a/Sources/ZcashLightClientKit/Entity/SentNoteEntity.swift +++ b/Sources/ZcashLightClientKit/Entity/SentNoteEntity.swift @@ -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) diff --git a/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift b/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift index cdeaebe7..bfecff33 100644 --- a/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift +++ b/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift @@ -93,6 +93,8 @@ public protocol AbstractTransaction { data containing the memo if any */ var memo: Data? { get set } + + var fee: Zatoshi? { get set } } /** diff --git a/Sources/ZcashLightClientKit/Initializer.swift b/Sources/ZcashLightClientKit/Initializer.swift index b596da8b..085ba55e 100644 --- a/Sources/ZcashLightClientKit/Initializer.swift +++ b/Sources/ZcashLightClientKit/Initializer.swift @@ -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 } diff --git a/Sources/ZcashLightClientKit/Model/WalletTypes.swift b/Sources/ZcashLightClientKit/Model/WalletTypes.swift index 1a6e1527..51e94dfb 100644 --- a/Sources/ZcashLightClientKit/Model/WalletTypes.swift +++ b/Sources/ZcashLightClientKit/Model/WalletTypes.swift @@ -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 } diff --git a/Sources/ZcashLightClientKit/Model/Zatoshi.swift b/Sources/ZcashLightClientKit/Model/Zatoshi.swift index 00e26efb..031f2413 100644 --- a/Sources/ZcashLightClientKit/Model/Zatoshi.swift +++ b/Sources/ZcashLightClientKit/Model/Zatoshi.swift @@ -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) + } +} diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index e9cb2992..1ada55ca 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -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, diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 60370398..644aa28d 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -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 diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index b0bec08e..60311785 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -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) ) diff --git a/Sources/ZcashLightClientKit/Tool/DerivationTool.swift b/Sources/ZcashLightClientKit/Tool/DerivationTool.swift index cb0b3ae4..5c1e002c 100644 --- a/Sources/ZcashLightClientKit/Tool/DerivationTool.swift +++ b/Sources/ZcashLightClientKit/Tool/DerivationTool.swift @@ -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) diff --git a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift index ea4d9f2b..5343e19c 100644 --- a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift +++ b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift @@ -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)) } } diff --git a/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift b/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift index 67b8c48f..57a9395e 100644 --- a/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift +++ b/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift @@ -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 diff --git a/Tests/NetworkTests/ZcashRustBackendTests.swift b/Tests/NetworkTests/ZcashRustBackendTests.swift index 1ad3ccae..147b7dfa 100644 --- a/Tests/NetworkTests/ZcashRustBackendTests.swift +++ b/Tests/NetworkTests/ZcashRustBackendTests.swift @@ -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) + } } diff --git a/Tests/OfflineTests/NotesRepositoryTests.swift b/Tests/OfflineTests/NotesRepositoryTests.swift index 98408b9d..301b680d 100644 --- a/Tests/OfflineTests/NotesRepositoryTests.swift +++ b/Tests/OfflineTests/NotesRepositoryTests.swift @@ -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) } } diff --git a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift index bcab55b9..bf2e34d8 100644 --- a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift +++ b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift @@ -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) } } diff --git a/Tests/OfflineTests/RecipientTests.swift b/Tests/OfflineTests/RecipientTests.swift index 40ba4b1e..7e5dd986 100644 --- a/Tests/OfflineTests/RecipientTests.swift +++ b/Tests/OfflineTests/RecipientTests.swift @@ -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) + + } } diff --git a/Tests/OfflineTests/TransactionRepositoryTests.swift b/Tests/OfflineTests/TransactionRepositoryTests.swift index 9374d7c8..964cf043 100644 --- a/Tests/OfflineTests/TransactionRepositoryTests.swift +++ b/Tests/OfflineTests/TransactionRepositoryTests.swift @@ -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 { diff --git a/Tests/TestUtils/Resources/ZcashSdk_Data.db b/Tests/TestUtils/Resources/ZcashSdk_Data.db deleted file mode 100644 index 55171c1d..00000000 Binary files a/Tests/TestUtils/Resources/ZcashSdk_Data.db and /dev/null differ diff --git a/Tests/TestUtils/Resources/darkside_caches.db b/Tests/TestUtils/Resources/darkside_caches.db new file mode 100644 index 00000000..e6d9c841 Binary files /dev/null and b/Tests/TestUtils/Resources/darkside_caches.db differ diff --git a/Tests/TestUtils/Resources/darkside_data.db b/Tests/TestUtils/Resources/darkside_data.db new file mode 100644 index 00000000..230c984b Binary files /dev/null and b/Tests/TestUtils/Resources/darkside_data.db differ diff --git a/Tests/TestUtils/Resources/darkside_pending.db b/Tests/TestUtils/Resources/darkside_pending.db new file mode 100644 index 00000000..759b2cbf Binary files /dev/null and b/Tests/TestUtils/Resources/darkside_pending.db differ diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index b7b4db0f..38174680 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -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 } diff --git a/Tests/TestUtils/TestDbBuilder.swift b/Tests/TestUtils/TestDbBuilder.swift index 666f5f66..6f1375bb 100644 --- a/Tests/TestUtils/TestDbBuilder.swift +++ b/Tests/TestUtils/TestDbBuilder.swift @@ -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) } diff --git a/Tests/TestUtils/TestDbBuilder.swift.orig b/Tests/TestUtils/TestDbBuilder.swift.orig new file mode 100644 index 00000000..724d2ef3 --- /dev/null +++ b/Tests/TestUtils/TestDbBuilder.swift.orig @@ -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 + } +}