Merge pull request #567 from nuttycom/pending_transaction_entity_recipient
Add internal recipient information to `PendingTransactionEntity`
This commit is contained in:
commit
37b060852c
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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)) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -93,6 +93,8 @@ public protocol AbstractTransaction {
|
|||
data containing the memo if any
|
||||
*/
|
||||
var memo: Data? { get set }
|
||||
|
||||
var fee: Zatoshi? { get set }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
Binary file not shown.
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue