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",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
|
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "bin/librustzcash_0_7",
|
"revision" : "0059f090e655667f9ee5ed3306bd87ca78c7711a"
|
||||||
"revision" : "e8fbb84c1bec44af9dbef7e27c85f25e8f51a5af"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -16,7 +16,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"),
|
.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(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: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
|
@ -44,7 +44,9 @@ let package = Package(
|
||||||
resources: [
|
resources: [
|
||||||
.copy("Resources/test_data.db"),
|
.copy("Resources/test_data.db"),
|
||||||
.copy("Resources/cache.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(
|
.testTarget(
|
||||||
|
|
|
@ -9,17 +9,19 @@ import Foundation
|
||||||
import SQLite
|
import SQLite
|
||||||
|
|
||||||
class MigrationManager {
|
class MigrationManager {
|
||||||
enum CacheDbMigration: Int32 {
|
enum CacheDbMigration: Int32, CaseIterable {
|
||||||
case none = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PendingDbMigration: Int32 {
|
|
||||||
case none = 0
|
case none = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
static let latestCacheDbMigrationVersion: Int32 = CacheDbMigration.none.rawValue
|
enum PendingDbMigration: Int32, CaseIterable {
|
||||||
static let latestPendingDbMigrationVersion: Int32 = PendingDbMigration.none.rawValue
|
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 cacheDb: ConnectionProvider
|
||||||
var pendingDb: ConnectionProvider
|
var pendingDb: ConnectionProvider
|
||||||
var network: NetworkType
|
var network: NetworkType
|
||||||
|
@ -34,7 +36,7 @@ class MigrationManager {
|
||||||
self.network = networkType
|
self.network = networkType
|
||||||
}
|
}
|
||||||
|
|
||||||
func performMigration(ufvks: [UnifiedFullViewingKey]) throws {
|
func performMigration() throws {
|
||||||
try migrateCacheDb()
|
try migrateCacheDb()
|
||||||
try migratePendingDb()
|
try migratePendingDb()
|
||||||
}
|
}
|
||||||
|
@ -42,34 +44,129 @@ class MigrationManager {
|
||||||
|
|
||||||
private extension MigrationManager {
|
private extension MigrationManager {
|
||||||
func migratePendingDb() throws {
|
func migratePendingDb() throws {
|
||||||
|
// getUserVersion returns a default value of zero for an unmigrated database.
|
||||||
let currentPendingDbVersion = try pendingDb.connection().getUserVersion()
|
let currentPendingDbVersion = try pendingDb.connection().getUserVersion()
|
||||||
|
|
||||||
LoggerProxy.debug(
|
LoggerProxy.debug(
|
||||||
"Attempting to perform migration for pending Db - currentVersion: \(currentPendingDbVersion)." +
|
"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 {
|
for v in (currentPendingDbVersion..<Self.nextPendingDbMigration.rawValue) {
|
||||||
// perform no migration just adjust the version number
|
switch PendingDbMigration(rawValue: v) {
|
||||||
try self.cacheDb.connection().setUserVersion(PendingDbMigration.none.rawValue)
|
case .some(.none):
|
||||||
} else {
|
try migratePendingDbV1()
|
||||||
LoggerProxy.debug("PendingDb Db - no migration needed")
|
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 {
|
func migrateCacheDb() throws {
|
||||||
|
// getUserVersion returns a default value of zero for an unmigrated database.
|
||||||
let currentCacheDbVersion = try cacheDb.connection().getUserVersion()
|
let currentCacheDbVersion = try cacheDb.connection().getUserVersion()
|
||||||
|
|
||||||
LoggerProxy.debug(
|
LoggerProxy.debug(
|
||||||
"Attempting to perform migration for cache Db - currentVersion: \(currentCacheDbVersion)." +
|
"Attempting to perform migration for cache Db - currentVersion: \(currentCacheDbVersion)." +
|
||||||
"Latest version is: \(Self.latestCacheDbMigrationVersion)"
|
"Latest version is: \(Self.nextCacheDbMigration.rawValue)"
|
||||||
)
|
)
|
||||||
|
|
||||||
if currentCacheDbVersion < Self.latestCacheDbMigrationVersion {
|
for v in (currentCacheDbVersion..<Self.nextCacheDbMigration.rawValue) {
|
||||||
// perform no migration just adjust the version number
|
switch CacheDbMigration(rawValue: v) {
|
||||||
try self.cacheDb.connection().setUserVersion(CacheDbMigration.none.rawValue)
|
case .some(.none):
|
||||||
} else {
|
// we have no migrations to run; this case should ordinarily be
|
||||||
LoggerProxy.debug("Cache Db - no migration needed")
|
// 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)
|
return Int32(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setUserVersion(_ version: Int32) throws {
|
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 {
|
for try await utxo in stream {
|
||||||
utxos.append(utxo)
|
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)
|
return storeUTXOs(utxos, in: dataDb)
|
||||||
} catch {
|
} catch {
|
||||||
throw mapError(error)
|
throw mapError(error)
|
||||||
|
|
|
@ -30,21 +30,6 @@ extension CompactBlockProcessor {
|
||||||
}
|
}
|
||||||
.flatMap({ $0 })
|
.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] = []
|
var utxos: [UnspentTransactionOutputEntity] = []
|
||||||
let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses.map { $0.stringEncoded }, startHeight: config.walletBirthday)
|
let stream: AsyncThrowingStream<UnspentTransactionOutputEntity, Error> = downloader.fetchUnspentTransactionOutputs(tAddresses: tAddresses.map { $0.stringEncoded }, startHeight: config.walletBirthday)
|
||||||
for try await transaction in stream {
|
for try await transaction in stream {
|
||||||
|
|
|
@ -31,6 +31,14 @@ extension NetworkType {
|
||||||
default: return nil
|
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 {
|
public enum ZcashNetworkBuilder {
|
||||||
|
|
|
@ -21,6 +21,7 @@ struct ReceivedNote: ReceivedNoteEntity, Codable {
|
||||||
case value
|
case value
|
||||||
case memo
|
case memo
|
||||||
case spent
|
case spent
|
||||||
|
case tx
|
||||||
}
|
}
|
||||||
var id: Int
|
var id: Int
|
||||||
var diversifier: Data
|
var diversifier: Data
|
||||||
|
@ -33,6 +34,7 @@ struct ReceivedNote: ReceivedNoteEntity, Codable {
|
||||||
var value: Int
|
var value: Int
|
||||||
var memo: Data?
|
var memo: Data?
|
||||||
var spent: Int?
|
var spent: Int?
|
||||||
|
var tx: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReceivedNotesSQLDAO: ReceivedNoteRepository {
|
class ReceivedNotesSQLDAO: ReceivedNoteRepository {
|
||||||
|
@ -77,8 +79,9 @@ struct SentNote: SentNoteEntity, Codable {
|
||||||
case transactionId = "tx"
|
case transactionId = "tx"
|
||||||
case outputPool = "output_pool"
|
case outputPool = "output_pool"
|
||||||
case outputIndex = "output_index"
|
case outputIndex = "output_index"
|
||||||
case account = "from_account"
|
case fromAccount = "from_account"
|
||||||
case address
|
case toAddress = "to_address"
|
||||||
|
case toAccount = "to_account"
|
||||||
case value
|
case value
|
||||||
case memo
|
case memo
|
||||||
}
|
}
|
||||||
|
@ -87,8 +90,9 @@ struct SentNote: SentNoteEntity, Codable {
|
||||||
var transactionId: Int
|
var transactionId: Int
|
||||||
var outputPool: Int
|
var outputPool: Int
|
||||||
var outputIndex: Int
|
var outputIndex: Int
|
||||||
var account: Int
|
var fromAccount: Int
|
||||||
var address: String
|
var toAddress: String
|
||||||
|
var toAccount: Int
|
||||||
var value: Int
|
var value: Int
|
||||||
var memo: Data?
|
var memo: Data?
|
||||||
}
|
}
|
||||||
|
@ -126,19 +130,5 @@ class SentNotesSQLDAO: SentNotesRepository {
|
||||||
try row.decode()
|
try row.decode()
|
||||||
}
|
}
|
||||||
.first
|
.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 {
|
enum CodingKeys: String, CodingKey {
|
||||||
case toAddress = "to_address"
|
case toAddress = "to_address"
|
||||||
|
case toInternalAccount = "to_internal"
|
||||||
case accountIndex = "account_index"
|
case accountIndex = "account_index"
|
||||||
case minedHeight = "mined_height"
|
case minedHeight = "mined_height"
|
||||||
case expiryHeight = "expiry_height"
|
case expiryHeight = "expiry_height"
|
||||||
|
@ -25,9 +26,10 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
case value
|
case value
|
||||||
case memo
|
case memo
|
||||||
case rawTransactionId = "txid"
|
case rawTransactionId = "txid"
|
||||||
|
case fee
|
||||||
}
|
}
|
||||||
|
|
||||||
var toAddress: String
|
var recipient: PendingTransactionRecipient
|
||||||
var accountIndex: Int
|
var accountIndex: Int
|
||||||
var minedHeight: BlockHeight
|
var minedHeight: BlockHeight
|
||||||
var expiryHeight: BlockHeight
|
var expiryHeight: BlockHeight
|
||||||
|
@ -42,10 +44,11 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
var value: Zatoshi
|
var value: Zatoshi
|
||||||
var memo: Data?
|
var memo: Data?
|
||||||
var rawTransactionId: Data?
|
var rawTransactionId: Data?
|
||||||
|
var fee: Zatoshi?
|
||||||
|
|
||||||
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
|
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
|
||||||
PendingTransaction(
|
PendingTransaction(
|
||||||
toAddress: entity.toAddress,
|
recipient: entity.recipient,
|
||||||
accountIndex: entity.accountIndex,
|
accountIndex: entity.accountIndex,
|
||||||
minedHeight: entity.minedHeight,
|
minedHeight: entity.minedHeight,
|
||||||
expiryHeight: entity.expiryHeight,
|
expiryHeight: entity.expiryHeight,
|
||||||
|
@ -59,12 +62,13 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
value: entity.value,
|
value: entity.value,
|
||||||
memo: entity.memo == nil ? Data(MemoBytes.empty().bytes) : entity.memo,
|
memo: entity.memo == nil ? Data(MemoBytes.empty().bytes) : entity.memo,
|
||||||
rawTransactionId: entity.raw
|
rawTransactionId: entity.raw,
|
||||||
|
fee: entity.fee
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
toAddress: String,
|
recipient: PendingTransactionRecipient,
|
||||||
accountIndex: Int,
|
accountIndex: Int,
|
||||||
minedHeight: BlockHeight,
|
minedHeight: BlockHeight,
|
||||||
expiryHeight: BlockHeight,
|
expiryHeight: BlockHeight,
|
||||||
|
@ -78,9 +82,10 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
id: Int?,
|
id: Int?,
|
||||||
value: Zatoshi,
|
value: Zatoshi,
|
||||||
memo: Data?,
|
memo: Data?,
|
||||||
rawTransactionId: Data?
|
rawTransactionId: Data?,
|
||||||
|
fee: Zatoshi?
|
||||||
) {
|
) {
|
||||||
self.toAddress = toAddress
|
self.recipient = recipient
|
||||||
self.accountIndex = accountIndex
|
self.accountIndex = accountIndex
|
||||||
self.minedHeight = minedHeight
|
self.minedHeight = minedHeight
|
||||||
self.expiryHeight = expiryHeight
|
self.expiryHeight = expiryHeight
|
||||||
|
@ -95,12 +100,27 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
|
||||||
self.value = value
|
self.value = value
|
||||||
self.memo = memo
|
self.memo = memo
|
||||||
self.rawTransactionId = rawTransactionId
|
self.rawTransactionId = rawTransactionId
|
||||||
|
self.fee = fee
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
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.accountIndex = try container.decode(Int.self, forKey: .accountIndex)
|
||||||
self.minedHeight = try container.decode(BlockHeight.self, forKey: .minedHeight)
|
self.minedHeight = try container.decode(BlockHeight.self, forKey: .minedHeight)
|
||||||
self.expiryHeight = try container.decode(BlockHeight.self, forKey: .expiryHeight)
|
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.createTime = try container.decode(TimeInterval.self, forKey: .createTime)
|
||||||
self.raw = try container.decodeIfPresent(Data.self, forKey: .raw)
|
self.raw = try container.decodeIfPresent(Data.self, forKey: .raw)
|
||||||
self.id = try container.decodeIfPresent(Int.self, forKey: .id)
|
self.id = try container.decodeIfPresent(Int.self, forKey: .id)
|
||||||
|
|
||||||
let zatoshiValue = try container.decode(Int64.self, forKey: .value)
|
let zatoshiValue = try container.decode(Int64.self, forKey: .value)
|
||||||
self.value = Zatoshi(zatoshiValue)
|
self.value = Zatoshi(zatoshiValue)
|
||||||
self.memo = try container.decodeIfPresent(Data.self, forKey: .memo)
|
self.memo = try container.decodeIfPresent(Data.self, forKey: .memo)
|
||||||
self.rawTransactionId = try container.decodeIfPresent(Data.self, forKey: .rawTransactionId)
|
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 {
|
func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
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.accountIndex, forKey: .accountIndex)
|
||||||
try container.encode(self.minedHeight, forKey: .minedHeight)
|
try container.encode(self.minedHeight, forKey: .minedHeight)
|
||||||
try container.encode(self.expiryHeight, forKey: .expiryHeight)
|
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.encode(self.value.amount, forKey: .value)
|
||||||
try container.encodeIfPresent(self.memo, forKey: .memo)
|
try container.encodeIfPresent(self.memo, forKey: .memo)
|
||||||
try container.encodeIfPresent(self.rawTransactionId, forKey: .rawTransactionId)
|
try container.encodeIfPresent(self.rawTransactionId, forKey: .rawTransactionId)
|
||||||
|
try container.encodeIfPresent(self.fee?.amount, forKey: .fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSameTransactionId<T>(other: T) -> Bool where T: RawIdentifiable {
|
func isSameTransactionId<T>(other: T) -> Bool where T: RawIdentifiable {
|
||||||
self.rawTransactionId == other.rawTransactionId
|
self.rawTransactionId == other.rawTransactionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PendingTransaction {
|
extension PendingTransaction {
|
||||||
init(value: Zatoshi, toAddress: String, memo: MemoBytes, account index: Int) {
|
init(value: Zatoshi, recipient: PendingTransactionRecipient, memo: MemoBytes, account index: Int) {
|
||||||
self = PendingTransaction(
|
self = PendingTransaction(
|
||||||
toAddress: toAddress,
|
recipient: recipient,
|
||||||
accountIndex: index,
|
accountIndex: index,
|
||||||
minedHeight: -1,
|
minedHeight: -1,
|
||||||
expiryHeight: -1,
|
expiryHeight: -1,
|
||||||
|
@ -161,14 +195,16 @@ extension PendingTransaction {
|
||||||
id: nil,
|
id: nil,
|
||||||
value: value,
|
value: value,
|
||||||
memo: Data(memo.bytes),
|
memo: Data(memo.bytes),
|
||||||
rawTransactionId: nil
|
rawTransactionId: nil,
|
||||||
|
fee: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PendingTransactionSQLDAO: PendingTransactionRepository {
|
class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
enum TableColumns {
|
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 accountIndex = Expression<Int>("account_index")
|
||||||
static var minedHeight = Expression<Int?>("mined_height")
|
static var minedHeight = Expression<Int?>("mined_height")
|
||||||
static var expiryHeight = Expression<Int?>("expiry_height")
|
static var expiryHeight = Expression<Int?>("expiry_height")
|
||||||
|
@ -183,42 +219,21 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
static var value = Expression<Zatoshi>("value")
|
static var value = Expression<Zatoshi>("value")
|
||||||
static var memo = Expression<Blob?>("memo")
|
static var memo = Expression<Blob?>("memo")
|
||||||
static var rawTransactionId = Expression<Blob?>("txid")
|
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
|
var dbProvider: ConnectionProvider
|
||||||
|
|
||||||
init(dbProvider: ConnectionProvider) {
|
init(dbProvider: ConnectionProvider) {
|
||||||
self.dbProvider = dbProvider
|
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 {
|
func create(_ transaction: PendingTransactionEntity) throws -> Int {
|
||||||
let pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction)
|
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 {
|
func update(_ transaction: PendingTransactionEntity) throws {
|
||||||
|
@ -226,8 +241,8 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
guard let id = pendingTx.id else {
|
guard let id = pendingTx.id else {
|
||||||
throw StorageError.malformedEntity(fields: ["id"])
|
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 {
|
if updatedRows == 0 {
|
||||||
LoggerProxy.error("attempted to update pending transactions but no rows were updated")
|
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 {
|
guard let id = transaction.id else {
|
||||||
throw StorageError.malformedEntity(fields: ["id"])
|
throw StorageError.malformedEntity(fields: ["id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try dbProvider.connection().run(table.filter(TableColumns.id == id).delete())
|
try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).delete())
|
||||||
} catch {
|
} catch {
|
||||||
throw StorageError.updateFailed
|
throw StorageError.updateFailed
|
||||||
}
|
}
|
||||||
|
@ -251,18 +266,18 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
guard let txId = pendingTx.id else {
|
guard let txId = pendingTx.id else {
|
||||||
throw StorageError.malformedEntity(fields: ["id"])
|
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? {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let pendingTx: PendingTransaction = try row.decode()
|
let pendingTx: PendingTransaction = try row.decode()
|
||||||
|
|
||||||
return pendingTx
|
return pendingTx
|
||||||
} catch {
|
} catch {
|
||||||
throw StorageError.operationFailed
|
throw StorageError.operationFailed
|
||||||
|
@ -270,15 +285,15 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAll() throws -> [PendingTransactionEntity] {
|
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()
|
try row.decode()
|
||||||
}
|
}
|
||||||
|
|
||||||
return allTxs
|
return allTxs
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMinedHeight(_ height: BlockHeight, id: Int) throws {
|
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()
|
let updatedRows = try dbProvider.connection()
|
||||||
.run(transaction.update([TableColumns.minedHeight <- height]))
|
.run(transaction.update([TableColumns.minedHeight <- height]))
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum TransactionBuilder {
|
||||||
case memo
|
case memo
|
||||||
case noteId
|
case noteId
|
||||||
case blockTimeInSeconds
|
case blockTimeInSeconds
|
||||||
|
case fee
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReceivedColumns: Int {
|
enum ReceivedColumns: Int {
|
||||||
|
@ -33,6 +34,7 @@ enum TransactionBuilder {
|
||||||
case memo
|
case memo
|
||||||
case noteId
|
case noteId
|
||||||
case blockTimeInSeconds
|
case blockTimeInSeconds
|
||||||
|
case fee
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TransactionEntityColumns: Int {
|
enum TransactionEntityColumns: Int {
|
||||||
|
@ -42,6 +44,7 @@ enum TransactionBuilder {
|
||||||
case txid
|
case txid
|
||||||
case expiryHeight
|
case expiryHeight
|
||||||
case raw
|
case raw
|
||||||
|
case fee
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createTransactionEntity(txId: Data, rawTransaction: RawTransaction) -> TransactionEntity {
|
static func createTransactionEntity(txId: Data, rawTransaction: RawTransaction) -> TransactionEntity {
|
||||||
|
@ -52,7 +55,8 @@ enum TransactionBuilder {
|
||||||
transactionIndex: nil,
|
transactionIndex: nil,
|
||||||
expiryHeight: nil,
|
expiryHeight: nil,
|
||||||
minedHeight: Int(exactly: rawTransaction.height),
|
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,
|
transactionIndex: bindings[TransactionEntityColumns.txIndex.rawValue] as? Int,
|
||||||
expiryHeight: bindings[TransactionEntityColumns.expiryHeight.rawValue] as? Int,
|
expiryHeight: bindings[TransactionEntityColumns.expiryHeight.rawValue] as? Int,
|
||||||
minedHeight: bindings[TransactionEntityColumns.minedHeight.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 {
|
if let txIdBlob = bindings[ConfirmedColumns.rawTransactionId.rawValue] as? Blob {
|
||||||
transactionId = Data(blob: txIdBlob)
|
transactionId = Data(blob: txIdBlob)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfirmedTransaction(
|
return ConfirmedTransaction(
|
||||||
toAddress: toAddress,
|
toAddress: toAddress,
|
||||||
expiryHeight: expiryHeight,
|
expiryHeight: expiryHeight,
|
||||||
|
@ -122,7 +127,8 @@ enum TransactionBuilder {
|
||||||
id: Int(id),
|
id: Int(id),
|
||||||
value: Zatoshi(value),
|
value: Zatoshi(value),
|
||||||
memo: memo,
|
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),
|
id: Int(id),
|
||||||
value: Zatoshi(value),
|
value: Zatoshi(value),
|
||||||
memo: memo,
|
memo: memo,
|
||||||
rawTransactionId: transactionId
|
rawTransactionId: transactionId,
|
||||||
|
fee: (bindings[ReceivedColumns.fee.rawValue] as? Int).map({ Zatoshi(Int64($0)) })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SQLite
|
import SQLite
|
||||||
|
|
||||||
struct Transaction: TransactionEntity, Decodable {
|
struct Transaction: TransactionEntity {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id = "id_tx"
|
case id = "id_tx"
|
||||||
case transactionId = "txid"
|
case transactionId = "txid"
|
||||||
|
@ -17,6 +17,7 @@ struct Transaction: TransactionEntity, Decodable {
|
||||||
case expiryHeight = "expiry_height"
|
case expiryHeight = "expiry_height"
|
||||||
case minedHeight = "block"
|
case minedHeight = "block"
|
||||||
case raw
|
case raw
|
||||||
|
case fee
|
||||||
}
|
}
|
||||||
|
|
||||||
var id: Int?
|
var id: Int?
|
||||||
|
@ -26,6 +27,36 @@ struct Transaction: TransactionEntity, Decodable {
|
||||||
var expiryHeight: BlockHeight?
|
var expiryHeight: BlockHeight?
|
||||||
var minedHeight: BlockHeight?
|
var minedHeight: BlockHeight?
|
||||||
var raw: Data?
|
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 {
|
struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
||||||
|
@ -40,6 +71,7 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
|
||||||
var value: Zatoshi
|
var value: Zatoshi
|
||||||
var memo: Data?
|
var memo: Data?
|
||||||
var rawTransactionId: Data?
|
var rawTransactionId: Data?
|
||||||
|
var fee: Zatoshi?
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionSQLDAO: TransactionRepository {
|
class TransactionSQLDAO: TransactionRepository {
|
||||||
|
@ -51,6 +83,7 @@ class TransactionSQLDAO: TransactionRepository {
|
||||||
static var expiryHeight = Expression<Int?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
static var expiryHeight = Expression<Int?>(Transaction.CodingKeys.expiryHeight.rawValue)
|
||||||
static var minedHeight = Expression<Int?>(Transaction.CodingKeys.minedHeight.rawValue)
|
static var minedHeight = Expression<Int?>(Transaction.CodingKeys.minedHeight.rawValue)
|
||||||
static var raw = Expression<Blob?>(Transaction.CodingKeys.raw.rawValue)
|
static var raw = Expression<Blob?>(Transaction.CodingKeys.raw.rawValue)
|
||||||
|
static var fee = Expression<Zatoshi?>(Transaction.CodingKeys.fee.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dbProvider: ConnectionProvider
|
var dbProvider: ConnectionProvider
|
||||||
|
@ -115,11 +148,12 @@ extension TransactionSQLDAO {
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw,
|
transactions.raw AS raw,
|
||||||
sent_notes.address AS toAddress,
|
sent_notes.to_address AS toAddress,
|
||||||
sent_notes.value AS value,
|
sent_notes.value AS value,
|
||||||
sent_notes.memo AS memo,
|
sent_notes.memo AS memo,
|
||||||
sent_notes.id_note AS noteId,
|
sent_notes.id_note AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
INNER JOIN sent_notes
|
INNER JOIN sent_notes
|
||||||
ON transactions.id_tx = sent_notes.tx
|
ON transactions.id_tx = sent_notes.tx
|
||||||
|
@ -153,7 +187,8 @@ extension TransactionSQLDAO {
|
||||||
received_notes.value AS value,
|
received_notes.value AS value,
|
||||||
received_notes.memo AS memo,
|
received_notes.memo AS memo,
|
||||||
received_notes.id_note AS noteId,
|
received_notes.id_note AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
|
|
||||||
FROM transactions
|
FROM transactions
|
||||||
LEFT JOIN received_notes
|
LEFT JOIN received_notes
|
||||||
|
@ -184,7 +219,7 @@ extension TransactionSQLDAO {
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw,
|
transactions.raw AS raw,
|
||||||
sent_notes.address AS toAddress,
|
sent_notes.to_address AS toAddress,
|
||||||
CASE
|
CASE
|
||||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||||
ELSE received_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
|
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||||
ELSE received_notes.id_note
|
ELSE received_notes.id_note
|
||||||
end AS noteId,
|
end AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
LEFT JOIN received_notes
|
LEFT JOIN received_notes
|
||||||
ON transactions.id_tx = received_notes.tx
|
ON transactions.id_tx = received_notes.tx
|
||||||
|
@ -205,8 +241,8 @@ extension TransactionSQLDAO {
|
||||||
ON transactions.id_tx = sent_notes.tx
|
ON transactions.id_tx = sent_notes.tx
|
||||||
LEFT JOIN blocks
|
LEFT JOIN blocks
|
||||||
ON transactions.block = blocks.height
|
ON transactions.block = blocks.height
|
||||||
WHERE (sent_notes.address IS NULL AND received_notes.is_change != 1)
|
WHERE (sent_notes.to_address IS NULL AND received_notes.is_change != 1)
|
||||||
OR sent_notes.address IS NOT NULL
|
OR sent_notes.to_address IS NOT NULL
|
||||||
ORDER BY ( minedheight IS NOT NULL ),
|
ORDER BY ( minedheight IS NOT NULL ),
|
||||||
minedheight DESC,
|
minedheight DESC,
|
||||||
blocktimeinseconds DESC,
|
blocktimeinseconds DESC,
|
||||||
|
@ -231,7 +267,7 @@ extension TransactionSQLDAO {
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw,
|
transactions.raw AS raw,
|
||||||
sent_notes.address AS toAddress,
|
sent_notes.to_address AS toAddress,
|
||||||
CASE
|
CASE
|
||||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||||
ELSE received_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
|
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||||
ELSE received_notes.id_note
|
ELSE received_notes.id_note
|
||||||
end AS noteId,
|
end AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
LEFT JOIN received_notes
|
LEFT JOIN received_notes
|
||||||
ON transactions.id_tx = received_notes.tx
|
ON transactions.id_tx = received_notes.tx
|
||||||
|
@ -253,8 +290,8 @@ extension TransactionSQLDAO {
|
||||||
LEFT JOIN blocks
|
LEFT JOIN blocks
|
||||||
ON transactions.block = blocks.height
|
ON transactions.block = blocks.height
|
||||||
WHERE (\(fromTransaction.blockTimeInSeconds), \(fromTransaction.transactionIndex)) > (blocktimeinseconds, transactionIndex) AND
|
WHERE (\(fromTransaction.blockTimeInSeconds), \(fromTransaction.transactionIndex)) > (blocktimeinseconds, transactionIndex) AND
|
||||||
(sent_notes.address IS NULL AND received_notes.is_change != 1)
|
((sent_notes.to_address IS NULL AND received_notes.is_change != 1)
|
||||||
OR sent_notes.address IS NOT NULL
|
OR sent_notes.to_address IS NOT NULL)
|
||||||
ORDER BY ( minedheight IS NOT NULL ),
|
ORDER BY ( minedheight IS NOT NULL ),
|
||||||
minedheight DESC,
|
minedheight DESC,
|
||||||
blocktimeinseconds DESC,
|
blocktimeinseconds DESC,
|
||||||
|
@ -274,7 +311,8 @@ extension TransactionSQLDAO {
|
||||||
transactions.tx_index AS transactionIndex,
|
transactions.tx_index AS transactionIndex,
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw
|
transactions.raw AS raw,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
WHERE \(range.start.height) <= minedheight
|
WHERE \(range.start.height) <= minedheight
|
||||||
AND minedheight <= \(range.end.height)
|
AND minedheight <= \(range.end.height)
|
||||||
|
@ -297,7 +335,7 @@ extension TransactionSQLDAO {
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw,
|
transactions.raw AS raw,
|
||||||
sent_notes.address AS toAddress,
|
sent_notes.to_address AS toAddress,
|
||||||
CASE
|
CASE
|
||||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||||
ELSE received_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
|
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||||
ELSE received_notes.id_note
|
ELSE received_notes.id_note
|
||||||
end AS noteId,
|
end AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
LEFT JOIN received_notes
|
LEFT JOIN received_notes
|
||||||
ON transactions.id_tx = received_notes.tx
|
ON transactions.id_tx = received_notes.tx
|
||||||
|
@ -320,8 +359,8 @@ extension TransactionSQLDAO {
|
||||||
ON transactions.block = blocks.height
|
ON transactions.block = blocks.height
|
||||||
WHERE (\(range.start.height) <= minedheight
|
WHERE (\(range.start.height) <= minedheight
|
||||||
AND minedheight <= \(range.end.height)) AND
|
AND minedheight <= \(range.end.height)) AND
|
||||||
(sent_notes.address IS NULL AND received_notes.is_change != 1)
|
(sent_notes.to_address IS NULL AND received_notes.is_change != 1)
|
||||||
OR sent_notes.address IS NOT NULL
|
OR sent_notes.to_address IS NOT NULL
|
||||||
ORDER BY ( minedheight IS NOT NULL ),
|
ORDER BY ( minedheight IS NOT NULL ),
|
||||||
minedheight DESC,
|
minedheight DESC,
|
||||||
blocktimeinseconds DESC,
|
blocktimeinseconds DESC,
|
||||||
|
@ -342,7 +381,7 @@ extension TransactionSQLDAO {
|
||||||
transactions.txid AS rawTransactionId,
|
transactions.txid AS rawTransactionId,
|
||||||
transactions.expiry_height AS expiryHeight,
|
transactions.expiry_height AS expiryHeight,
|
||||||
transactions.raw AS raw,
|
transactions.raw AS raw,
|
||||||
sent_notes.address AS toAddress,
|
sent_notes.to_address AS toAddress,
|
||||||
CASE
|
CASE
|
||||||
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
WHEN sent_notes.value IS NOT NULL THEN sent_notes.value
|
||||||
ELSE received_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
|
WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note
|
||||||
ELSE received_notes.id_note
|
ELSE received_notes.id_note
|
||||||
end AS noteId,
|
end AS noteId,
|
||||||
blocks.time AS blockTimeInSeconds
|
blocks.time AS blockTimeInSeconds,
|
||||||
|
transactions.fee AS fee
|
||||||
FROM transactions
|
FROM transactions
|
||||||
LEFT JOIN received_notes
|
LEFT JOIN received_notes
|
||||||
ON transactions.id_tx = received_notes.tx
|
ON transactions.id_tx = received_notes.tx
|
||||||
|
@ -365,8 +405,8 @@ extension TransactionSQLDAO {
|
||||||
ON transactions.block = blocks.height
|
ON transactions.block = blocks.height
|
||||||
WHERE minedheight >= 0
|
WHERE minedheight >= 0
|
||||||
AND rawTransactionId == \(Blob(bytes: rawId.bytes)) AND
|
AND rawTransactionId == \(Blob(bytes: rawId.bytes)) AND
|
||||||
(sent_notes.address IS NULL AND received_notes.is_change != 1)
|
(sent_notes.to_address IS NULL AND received_notes.is_change != 1)
|
||||||
OR sent_notes.address IS NOT NULL
|
OR sent_notes.to_address IS NOT NULL
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
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
|
Represents a sent transaction that has not been confirmed yet on the blockchain
|
||||||
*/
|
*/
|
||||||
|
@ -13,12 +19,12 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
|
||||||
/**
|
/**
|
||||||
recipient address
|
recipient address
|
||||||
*/
|
*/
|
||||||
var toAddress: String { get set }
|
var recipient: PendingTransactionRecipient { get }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
index of the account from which the funds were sent
|
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.
|
height which the block was mined at.
|
||||||
|
@ -34,7 +40,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
|
||||||
/**
|
/**
|
||||||
value is 1 if the transaction was cancelled
|
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
|
how many times this transaction encoding was attempted
|
||||||
|
@ -61,7 +67,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
|
||||||
|
|
||||||
- Note: represented in timeIntervalySince1970
|
- Note: represented in timeIntervalySince1970
|
||||||
*/
|
*/
|
||||||
var createTime: TimeInterval { get set }
|
var createTime: TimeInterval { get }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks whether this transaction is the same as the given transaction
|
Checks whether this transaction is the same as the given transaction
|
||||||
|
@ -182,7 +188,8 @@ public extension PendingTransactionEntity {
|
||||||
transactionIndex: -1,
|
transactionIndex: -1,
|
||||||
expiryHeight: self.expiryHeight,
|
expiryHeight: self.expiryHeight,
|
||||||
minedHeight: self.minedHeight,
|
minedHeight: self.minedHeight,
|
||||||
raw: self.raw
|
raw: self.raw,
|
||||||
|
fee: self.fee
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,7 +206,8 @@ public extension ConfirmedTransactionEntity {
|
||||||
transactionIndex: self.transactionIndex,
|
transactionIndex: self.transactionIndex,
|
||||||
expiryHeight: self.expiryHeight,
|
expiryHeight: self.expiryHeight,
|
||||||
minedHeight: self.minedHeight,
|
minedHeight: self.minedHeight,
|
||||||
raw: self.raw
|
raw: self.raw,
|
||||||
|
fee: self.fee
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ protocol SentNoteEntity {
|
||||||
var id: Int { get set }
|
var id: Int { get set }
|
||||||
var transactionId: Int { get set }
|
var transactionId: Int { get set }
|
||||||
var outputIndex: Int { get set }
|
var outputIndex: Int { get set }
|
||||||
var account: Int { get set }
|
var fromAccount: Int { get set }
|
||||||
var address: String { get set }
|
var toAddress: String { get set }
|
||||||
var value: Int { get set }
|
var value: Int { get set }
|
||||||
var memo: Data? { get set }
|
var memo: Data? { get set }
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ extension SentNoteEntity {
|
||||||
guard lhs.id == rhs.id,
|
guard lhs.id == rhs.id,
|
||||||
lhs.transactionId == rhs.transactionId,
|
lhs.transactionId == rhs.transactionId,
|
||||||
lhs.outputIndex == rhs.outputIndex,
|
lhs.outputIndex == rhs.outputIndex,
|
||||||
lhs.account == rhs.account,
|
lhs.fromAccount == rhs.fromAccount,
|
||||||
lhs.address == rhs.address,
|
lhs.toAddress == rhs.toAddress,
|
||||||
lhs.value == rhs.value,
|
lhs.value == rhs.value,
|
||||||
lhs.memo == rhs.memo else { return false }
|
lhs.memo == rhs.memo else { return false }
|
||||||
return true
|
return true
|
||||||
|
@ -33,8 +33,8 @@ extension SentNoteEntity {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
hasher.combine(transactionId)
|
hasher.combine(transactionId)
|
||||||
hasher.combine(outputIndex)
|
hasher.combine(outputIndex)
|
||||||
hasher.combine(account)
|
hasher.combine(fromAccount)
|
||||||
hasher.combine(address)
|
hasher.combine(toAddress)
|
||||||
hasher.combine(value)
|
hasher.combine(value)
|
||||||
if let memo = memo {
|
if let memo = memo {
|
||||||
hasher.combine(memo)
|
hasher.combine(memo)
|
||||||
|
|
|
@ -93,6 +93,8 @@ public protocol AbstractTransaction {
|
||||||
data containing the memo if any
|
data containing the memo if any
|
||||||
*/
|
*/
|
||||||
var memo: Data? { get set }
|
var memo: Data? { get set }
|
||||||
|
|
||||||
|
var fee: Zatoshi? { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -245,6 +245,14 @@ public class Initializer {
|
||||||
throw rustBackend.lastError() ?? InitializerError.accountInitFailed
|
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
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
// WalletTypes.swift
|
// WalletTypes.swift
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Created by Francisco Gindre on 4/6/21.
|
// 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
|
/// A Transparent Address that can be encoded as a String
|
||||||
///
|
///
|
||||||
/// Transactions sent to this address are totally visible in the public
|
/// 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.
|
/// Represents a Sapling receiver address. Comonly called zAddress.
|
||||||
/// This address corresponds to the Zcash Sapling shielded pool.
|
/// This address corresponds to the Zcash Sapling shielded pool.
|
||||||
/// Although this it is fully functional, we encourage developers to
|
/// 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 {
|
public struct SaplingAddress: Equatable, StringEncoded {
|
||||||
var encoding: String
|
var encoding: String
|
||||||
|
|
||||||
|
@ -238,13 +266,25 @@ public enum Recipient: Equatable, StringEncoded {
|
||||||
throw KeyEncodingError.invalidEncoding
|
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 struct WalletBalance: Equatable {
|
||||||
public var verified: Zatoshi
|
public var verified: Zatoshi
|
||||||
public var total: Zatoshi
|
public var total: Zatoshi
|
||||||
|
|
||||||
public init(verified: Zatoshi, total: Zatoshi) {
|
public init(verified: Zatoshi, total: Zatoshi) {
|
||||||
self.verified = verified
|
self.verified = verified
|
||||||
self.total = total
|
self.total = total
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,3 +89,20 @@ public extension NSDecimalNumber {
|
||||||
self.roundedZec.stringValue
|
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
|
import libzcashlc
|
||||||
|
|
||||||
class ZcashRustBackend: ZcashRustBackendWelding {
|
class ZcashRustBackend: ZcashRustBackendWelding {
|
||||||
|
|
||||||
static let minimumConfirmations: UInt32 = 10
|
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 {
|
static func createAccount(dbData: URL, seed: [UInt8], networkType: NetworkType) throws -> UnifiedSpendingKey {
|
||||||
let dbData = dbData.osStr()
|
let dbData = dbData.osStr()
|
||||||
|
|
||||||
|
@ -69,7 +48,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
||||||
zcashlc_create_to_address(
|
zcashlc_create_to_address(
|
||||||
dbData.0,
|
dbData.0,
|
||||||
dbData.1,
|
dbData.1,
|
||||||
Int32(usk.account),
|
|
||||||
uskPtr.baseAddress,
|
uskPtr.baseAddress,
|
||||||
UInt(usk.bytes.count),
|
UInt(usk.bytes.count),
|
||||||
[CChar](address.utf8CString),
|
[CChar](address.utf8CString),
|
||||||
|
@ -339,6 +317,26 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
||||||
return RustWeldingError.genericError(message: message)
|
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? {
|
static func getTransparentReceiver(for uAddr: UnifiedAddress) throws -> TransparentAddress? {
|
||||||
guard let transparentCStr = zcashlc_get_transparent_receiver_for_unified_address(
|
guard let transparentCStr = zcashlc_get_transparent_receiver_for_unified_address(
|
||||||
[CChar](uAddr.encoding.utf8CString)
|
[CChar](uAddr.encoding.utf8CString)
|
||||||
|
@ -597,7 +595,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
||||||
zcashlc_shield_funds(
|
zcashlc_shield_funds(
|
||||||
dbData.0,
|
dbData.0,
|
||||||
dbData.1,
|
dbData.1,
|
||||||
Int32(usk.account),
|
|
||||||
uskBuffer.baseAddress,
|
uskBuffer.baseAddress,
|
||||||
UInt(usk.bytes.count),
|
UInt(usk.bytes.count),
|
||||||
memo.bytes,
|
memo.bytes,
|
||||||
|
|
|
@ -31,23 +31,6 @@ public enum DbInitResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol ZcashRustBackendWelding {
|
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]
|
/// Adds the next available account-level spend authority, given the current set of [ZIP 316]
|
||||||
/// account identifiers known, to the wallet database.
|
/// account identifiers known, to the wallet database.
|
||||||
///
|
///
|
||||||
|
@ -278,6 +261,10 @@ protocol ZcashRustBackendWelding {
|
||||||
networkType: NetworkType
|
networkType: NetworkType
|
||||||
) throws -> DbInitResult
|
) 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
|
/// Validates the if the given string is a valid Sapling Address
|
||||||
/// - Parameter address: UTF-8 encoded String to validate
|
/// - Parameter address: UTF-8 encoded String to validate
|
||||||
/// - Parameter networkType: network type of this key
|
/// - Parameter networkType: network type of this key
|
||||||
|
|
|
@ -468,11 +468,11 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
} catch {
|
} catch {
|
||||||
throw SynchronizerError.parameterMissing(underlyingError: error)
|
throw SynchronizerError.parameterMissing(underlyingError: error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return try await createToAddress(
|
return try await createToAddress(
|
||||||
spendingKey: spendingKey,
|
spendingKey: spendingKey,
|
||||||
zatoshi: zatoshi,
|
zatoshi: zatoshi,
|
||||||
toAddress: toAddress.stringEncoded,
|
recipient: toAddress,
|
||||||
memo: memo
|
memo: memo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -492,13 +492,7 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
throw ShieldFundsError.insuficientTransparentFunds
|
throw ShieldFundsError.insuficientTransparentFunds
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Define who's the recipient of a shielding transaction #521
|
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, recipient: .internalAccount(spendingKey.account), memo: try memo.asMemoBytes(), from: accountIndex)
|
||||||
// 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)
|
|
||||||
|
|
||||||
// TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
|
// 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(
|
let transaction = try await transactionManager.encodeShieldingTransaction(
|
||||||
|
@ -515,13 +509,13 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
func createToAddress(
|
func createToAddress(
|
||||||
spendingKey: UnifiedSpendingKey,
|
spendingKey: UnifiedSpendingKey,
|
||||||
zatoshi: Zatoshi,
|
zatoshi: Zatoshi,
|
||||||
toAddress: String,
|
recipient: Recipient,
|
||||||
memo: Memo
|
memo: Memo
|
||||||
) async throws -> PendingTransactionEntity {
|
) async throws -> PendingTransactionEntity {
|
||||||
do {
|
do {
|
||||||
let spend = try transactionManager.initSpend(
|
let spend = try transactionManager.initSpend(
|
||||||
zatoshi: zatoshi,
|
zatoshi: zatoshi,
|
||||||
toAddress: toAddress,
|
recipient: .address(recipient),
|
||||||
memo: memo.asMemoBytes(),
|
memo: memo.asMemoBytes(),
|
||||||
from: Int(spendingKey.account)
|
from: Int(spendingKey.account)
|
||||||
)
|
)
|
||||||
|
|
|
@ -69,6 +69,10 @@ public class DerivationTool: KeyDeriving {
|
||||||
try rustwelding.getTransparentReceiver(for: unifiedAddress)
|
try rustwelding.getTransparentReceiver(for: unifiedAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func getAddressMetadata(_ addr: String) -> AddressMetadata? {
|
||||||
|
rustwelding.getAddressMetadata(addr)
|
||||||
|
}
|
||||||
|
|
||||||
/// Given a spending key, return the associated viewing key.
|
/// Given a spending key, return the associated viewing key.
|
||||||
/// - Parameter spendingKey: the `UnifiedSpendingKey` from which to derive the `UnifiedFullViewingKey` from.
|
/// - Parameter spendingKey: the `UnifiedSpendingKey` from which to derive the `UnifiedFullViewingKey` from.
|
||||||
/// - Returns: the viewing key that corresponds to the spending key.
|
/// - 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 {
|
extension DerivationTool: KeyValidation {
|
||||||
public func isValidUnifiedFullViewingKey(_ ufvk: String) -> Bool {
|
public func isValidUnifiedFullViewingKey(_ ufvk: String) -> Bool {
|
||||||
DerivationTool.rustwelding.isValidUnifiedFullViewingKey(ufvk, networkType: networkType)
|
DerivationTool.rustwelding.isValidUnifiedFullViewingKey(ufvk, networkType: networkType)
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum TransactionManagerError: Error {
|
enum TransactionManagerError: Error {
|
||||||
case couldNotCreateSpend(toAddress: String, account: Int, zatoshi: Zatoshi)
|
case couldNotCreateSpend(recipient: PendingTransactionRecipient, account: Int, zatoshi: Zatoshi)
|
||||||
case encodingFailed(PendingTransactionEntity)
|
case encodingFailed(PendingTransactionEntity)
|
||||||
case updateFailed(PendingTransactionEntity)
|
case updateFailed(PendingTransactionEntity)
|
||||||
case notPending(PendingTransactionEntity)
|
case notPending(PendingTransactionEntity)
|
||||||
|
@ -16,6 +16,7 @@ enum TransactionManagerError: Error {
|
||||||
case internalInconsistency(PendingTransactionEntity)
|
case internalInconsistency(PendingTransactionEntity)
|
||||||
case submitFailed(PendingTransactionEntity, errorCode: Int)
|
case submitFailed(PendingTransactionEntity, errorCode: Int)
|
||||||
case shieldingEncodingFailed(PendingTransactionEntity, reason: String)
|
case shieldingEncodingFailed(PendingTransactionEntity, reason: String)
|
||||||
|
case cannotEncodeInternalTx(PendingTransactionEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
class PersistentTransactionManager: OutboundTransactionManager {
|
class PersistentTransactionManager: OutboundTransactionManager {
|
||||||
|
@ -40,7 +41,7 @@ class PersistentTransactionManager: OutboundTransactionManager {
|
||||||
|
|
||||||
func initSpend(
|
func initSpend(
|
||||||
zatoshi: Zatoshi,
|
zatoshi: Zatoshi,
|
||||||
toAddress: String,
|
recipient: PendingTransactionRecipient,
|
||||||
memo: MemoBytes,
|
memo: MemoBytes,
|
||||||
from accountIndex: Int
|
from accountIndex: Int
|
||||||
) throws -> PendingTransactionEntity {
|
) throws -> PendingTransactionEntity {
|
||||||
|
@ -48,14 +49,14 @@ class PersistentTransactionManager: OutboundTransactionManager {
|
||||||
by: try repository.create(
|
by: try repository.create(
|
||||||
PendingTransaction(
|
PendingTransaction(
|
||||||
value: zatoshi,
|
value: zatoshi,
|
||||||
toAddress: toAddress,
|
recipient: recipient,
|
||||||
memo: memo,
|
memo: memo,
|
||||||
account: accountIndex
|
account: accountIndex
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) else {
|
) else {
|
||||||
throw TransactionManagerError.couldNotCreateSpend(
|
throw TransactionManagerError.couldNotCreateSpend(
|
||||||
toAddress: toAddress,
|
recipient: recipient,
|
||||||
account: accountIndex,
|
account: accountIndex,
|
||||||
zatoshi: zatoshi
|
zatoshi: zatoshi
|
||||||
)
|
)
|
||||||
|
@ -102,10 +103,18 @@ class PersistentTransactionManager: OutboundTransactionManager {
|
||||||
pendingTransaction: PendingTransactionEntity
|
pendingTransaction: PendingTransactionEntity
|
||||||
) async throws -> PendingTransactionEntity {
|
) async throws -> PendingTransactionEntity {
|
||||||
do {
|
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(
|
let encodedTransaction = try await self.encoder.createTransaction(
|
||||||
spendingKey: spendingKey,
|
spendingKey: spendingKey,
|
||||||
zatoshi: pendingTransaction.value,
|
zatoshi: pendingTransaction.value,
|
||||||
to: pendingTransaction.toAddress,
|
to: toAddress!,
|
||||||
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
|
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
|
||||||
from: pendingTransaction.accountIndex
|
from: pendingTransaction.accountIndex
|
||||||
)
|
)
|
||||||
|
@ -260,9 +269,7 @@ enum OutboundTransactionManagerBuilder {
|
||||||
|
|
||||||
enum PendingTransactionRepositoryBuilder {
|
enum PendingTransactionRepositoryBuilder {
|
||||||
static func build(initializer: Initializer) throws -> PendingTransactionRepository {
|
static func build(initializer: Initializer) throws -> PendingTransactionRepository {
|
||||||
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
|
PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false))
|
||||||
try dao.createrTableIfNeeded()
|
|
||||||
return dao
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ transactions through to completion.
|
||||||
protocol OutboundTransactionManager {
|
protocol OutboundTransactionManager {
|
||||||
func initSpend(
|
func initSpend(
|
||||||
zatoshi: Zatoshi,
|
zatoshi: Zatoshi,
|
||||||
toAddress: String,
|
recipient: PendingTransactionRecipient,
|
||||||
memo: MemoBytes,
|
memo: MemoBytes,
|
||||||
from accountIndex: Int
|
from accountIndex: Int
|
||||||
) throws -> PendingTransactionEntity
|
) throws -> PendingTransactionEntity
|
||||||
|
|
|
@ -209,4 +209,14 @@ class ZcashRustBackendTests: XCTestCase {
|
||||||
actualReceivers.sorted()
|
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() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
sentNotesRepository = TestDbBuilder.sentNotesRepository()
|
sentNotesRepository = try! TestDbBuilder.sentNotesRepository()
|
||||||
receivedNotesRepository = TestDbBuilder.receivedNotesRepository()
|
receivedNotesRepository = try! TestDbBuilder.receivedNotesRepository()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
|
@ -29,12 +29,12 @@ class NotesRepositoryTests: XCTestCase {
|
||||||
func testSentCount() {
|
func testSentCount() {
|
||||||
var count: Int?
|
var count: Int?
|
||||||
XCTAssertNoThrow(try { count = try sentNotesRepository.count() }())
|
XCTAssertNoThrow(try { count = try sentNotesRepository.count() }())
|
||||||
XCTAssertEqual(count, 0)
|
XCTAssertEqual(count, 13)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReceivedCount() {
|
func testReceivedCount() {
|
||||||
var count: Int?
|
var count: Int?
|
||||||
XCTAssertNoThrow(try { count = try receivedNotesRepository.count() }())
|
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
|
// swiftlint:disable force_try force_unwrapping implicitly_unwrapped_optional
|
||||||
class PendingTransactionRepositoryTests: XCTestCase {
|
class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
let dbUrl = try! TestDbBuilder.pendingTransactionsDbURL()
|
let dbUrl = try! TestDbBuilder.pendingTransactionsDbURL()
|
||||||
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
|
let recipient = SaplingAddress(validatedEncoding: "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6")
|
||||||
|
|
||||||
var pendingRepository: PendingTransactionRepository!
|
var pendingRepository: PendingTransactionRepository!
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
cleanUpDb()
|
cleanUpDb()
|
||||||
let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString))
|
let pendingDbProvider = SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString)
|
||||||
try! dao.createrTableIfNeeded()
|
let migrations = try! MigrationManager(cacheDbConnection: InMemoryDbProvider(), pendingDbConnection: pendingDbProvider, networkType: .testnet)
|
||||||
pendingRepository = dao
|
try! migrations.performMigration()
|
||||||
|
pendingRepository = PendingTransactionSQLDAO(dbProvider: pendingDbProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
|
@ -51,7 +52,7 @@ class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
|
|
||||||
XCTAssertEqual(transaction.accountIndex, expected.accountIndex)
|
XCTAssertEqual(transaction.accountIndex, expected.accountIndex)
|
||||||
XCTAssertEqual(transaction.value, expected.value)
|
XCTAssertEqual(transaction.value, expected.value)
|
||||||
XCTAssertEqual(transaction.toAddress, expected.toAddress)
|
XCTAssertEqual(transaction.recipient, expected.recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindById() {
|
func testFindById() {
|
||||||
|
@ -124,8 +125,6 @@ class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUpdate() {
|
func testUpdate() {
|
||||||
let newAccountIndex = 1
|
|
||||||
let newValue = Zatoshi(123_456)
|
|
||||||
let transaction = createAndStoreMockedTransaction()
|
let transaction = createAndStoreMockedTransaction()
|
||||||
|
|
||||||
guard let id = transaction.id else {
|
guard let id = transaction.id else {
|
||||||
|
@ -141,9 +140,12 @@ class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
XCTFail("failed to store tx")
|
XCTFail("failed to store tx")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let oldEncodeAttempts = stored!.encodeAttempts
|
||||||
|
let oldSubmitAttempts = stored!.submitAttempts
|
||||||
|
|
||||||
stored!.accountIndex = newAccountIndex
|
stored!.encodeAttempts += 1
|
||||||
stored!.value = newValue
|
stored!.submitAttempts += 5
|
||||||
|
|
||||||
XCTAssertNoThrow(try pendingRepository.update(stored!))
|
XCTAssertNoThrow(try pendingRepository.update(stored!))
|
||||||
|
|
||||||
|
@ -152,9 +154,9 @@ class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(updatedTransaction.value, newValue)
|
XCTAssertEqual(updatedTransaction.encodeAttempts, oldEncodeAttempts + 1)
|
||||||
XCTAssertEqual(updatedTransaction.accountIndex, newAccountIndex)
|
XCTAssertEqual(updatedTransaction.submitAttempts, oldSubmitAttempts + 5)
|
||||||
XCTAssertEqual(updatedTransaction.toAddress, stored!.toAddress)
|
XCTAssertEqual(updatedTransaction.recipient, stored!.recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAndStoreMockedTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
|
func createAndStoreMockedTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
|
||||||
|
@ -174,6 +176,6 @@ class PendingTransactionRepositoryTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mockTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
|
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))
|
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() {
|
override func setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
transactionRepository = TestDbBuilder.transactionRepository()
|
|
||||||
|
transactionRepository = try! TestDbBuilder.transactionRepository()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
|
@ -27,7 +28,7 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
var count: Int?
|
var count: Int?
|
||||||
XCTAssertNoThrow(try { count = try self.transactionRepository.countAll() }())
|
XCTAssertNoThrow(try { count = try self.transactionRepository.countAll() }())
|
||||||
XCTAssertNotNil(count)
|
XCTAssertNotNil(count)
|
||||||
XCTAssertEqual(count, 27)
|
XCTAssertEqual(count, 21)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCountUnmined() {
|
func testCountUnmined() {
|
||||||
|
@ -46,14 +47,14 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(transaction.id, 10)
|
XCTAssertEqual(transaction.id, 10)
|
||||||
XCTAssertEqual(transaction.minedHeight, 652812)
|
XCTAssertEqual(transaction.minedHeight, 663942)
|
||||||
XCTAssertEqual(transaction.transactionIndex, 5)
|
XCTAssertEqual(transaction.transactionIndex, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindByTxId() {
|
func testFindByTxId() {
|
||||||
var transaction: TransactionEntity?
|
var transaction: TransactionEntity?
|
||||||
|
|
||||||
let id = Data(fromHexEncodedString: "0BAFC5B83F5B39A5270144ECD98DBC65115055927EDDA8FF20F081FFF13E4780")!
|
let id = Data(fromHexEncodedString: "01af48bcc4e9667849a073b8b5c539a0fc19de71aac775377929dc6567a36eff")!
|
||||||
|
|
||||||
XCTAssertNoThrow(
|
XCTAssertNoThrow(
|
||||||
try { transaction = try self.transactionRepository.findBy(rawId: id) }()
|
try { transaction = try self.transactionRepository.findBy(rawId: id) }()
|
||||||
|
@ -64,9 +65,9 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(transaction.id, 10)
|
XCTAssertEqual(transaction.id, 8)
|
||||||
XCTAssertEqual(transaction.minedHeight, 652812)
|
XCTAssertEqual(transaction.minedHeight, 663922)
|
||||||
XCTAssertEqual(transaction.transactionIndex, 5)
|
XCTAssertEqual(transaction.transactionIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindAllSentTransactions() {
|
func testFindAllSentTransactions() {
|
||||||
|
@ -77,7 +78,7 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(txs.count, 0)
|
XCTAssertEqual(txs.count, 13)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindAllReceivedTransactions() {
|
func testFindAllReceivedTransactions() {
|
||||||
|
@ -88,7 +89,7 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(txs.count, 27)
|
XCTAssertEqual(txs.count, 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindAllTransactions() {
|
func testFindAllTransactions() {
|
||||||
|
@ -99,7 +100,7 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
XCTAssertEqual(txs.count, 27)
|
XCTAssertEqual(txs.count, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindAllPerformance() {
|
func testFindAllPerformance() {
|
||||||
|
@ -156,15 +157,15 @@ class TransactionRepositoryTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testFindAllFromLastSlice() throws {
|
func testFindAllFromLastSlice() throws {
|
||||||
let limit = 10
|
let limit = 5
|
||||||
let start = 20
|
let start = 15
|
||||||
guard
|
guard
|
||||||
let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max),
|
let transactions = try self.transactionRepository.findAll(offset: 0, limit: Int.max),
|
||||||
let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit)
|
let allFromNil = try self.transactionRepository.findAll(from: transactions[start], limit: limit)
|
||||||
else {
|
else {
|
||||||
return XCTFail("find all failed")
|
return XCTFail("find all failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
let slice = transactions[start + 1 ..< transactions.count]
|
let slice = transactions[start + 1 ..< transactions.count]
|
||||||
XCTAssertEqual(slice.count, allFromNil.count)
|
XCTAssertEqual(slice.count, allFromNil.count)
|
||||||
for transaction in slice {
|
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 {
|
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 {
|
static func clearUtxos(dbData: URL, address: ZcashLightClientKit.TransparentAddress, sinceHeight: ZcashLightClientKit.BlockHeight, networkType: ZcashLightClientKit.NetworkType) throws -> Int32 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,29 +70,40 @@ class TestDbBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func prePopulatedMainnetDataDbURL() -> URL? {
|
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 }
|
guard let url = prePopulatedMainnetDataDbURL() else { return nil }
|
||||||
|
|
||||||
let provider = SimpleConnectionProvider(path: url.absoluteString, readonly: true)
|
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? {
|
static func transactionRepository() throws -> TransactionRepository? {
|
||||||
guard let provider = prepopulatedDataDbProvider() else { return nil }
|
guard let provider = try prepopulatedDataDbProvider() else { return nil }
|
||||||
|
|
||||||
return TransactionSQLDAO(dbProvider: provider)
|
return TransactionSQLDAO(dbProvider: provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func sentNotesRepository() -> SentNotesRepository? {
|
static func sentNotesRepository() throws -> SentNotesRepository? {
|
||||||
guard let provider = prepopulatedDataDbProvider() else { return nil }
|
guard let provider = try prepopulatedDataDbProvider() else { return nil }
|
||||||
return SentNotesSQLDAO(dbProvider: provider)
|
return SentNotesSQLDAO(dbProvider: provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func receivedNotesRepository() -> ReceivedNoteRepository? {
|
static func receivedNotesRepository() throws -> ReceivedNoteRepository? {
|
||||||
guard let provider = prepopulatedDataDbProvider() else { return nil }
|
guard let provider = try prepopulatedDataDbProvider() else { return nil }
|
||||||
return ReceivedNotesSQLDAO(dbProvider: provider)
|
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