Remove `UnspentTransactionOutputRepository`
This removes the last direct access to the `utxos` table; all access now goes through the Rust FFI. `SDKSynchronizer.latestUTXOs` is removed without replacement. It was introduced during the initial addition of shielding support, but: - It is no longer used anywhere inside the SDK (when added, it was used in a few other methods). - It is not exposed in the `Synchronizer` protocol. - It is AFAICT unused in Zashi iOS, Edge, and Unstoppable. - It was functionally replaced by `refreshUTXOs`, which performs best-effort UTXO updates instead of failing on any error. (It also does not clear the `utxo`s table which makes it not equivalent.)
This commit is contained in:
parent
6207cc999b
commit
dd9942b6ab
|
@ -18,6 +18,10 @@ Possible errors:
|
||||||
- `ZcashError.rustProposeTransferFromURI`
|
- `ZcashError.rustProposeTransferFromURI`
|
||||||
- Other errors that `sentToAddress` can throw
|
- Other errors that `sentToAddress` can throw
|
||||||
|
|
||||||
|
## Removed
|
||||||
|
|
||||||
|
- `SDKSynchronizer.latestUTXOs`
|
||||||
|
|
||||||
# 2.0.11 - 2024-03-08
|
# 2.0.11 - 2024-03-08
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
|
@ -8,25 +8,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct UTXO: Decodable, Encodable {
|
struct UTXO: Decodable, Encodable {
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case id = "id_utxo"
|
|
||||||
case address
|
|
||||||
case prevoutTxId = "prevout_txid"
|
|
||||||
case prevoutIndex = "prevout_idx"
|
|
||||||
case script
|
|
||||||
case valueZat = "value_zat"
|
|
||||||
case height
|
|
||||||
case spentInTx = "spent_in_tx"
|
|
||||||
}
|
|
||||||
|
|
||||||
let id: Int?
|
|
||||||
let address: String
|
let address: String
|
||||||
var prevoutTxId: Data
|
var prevoutTxId: Data
|
||||||
var prevoutIndex: Int
|
var prevoutIndex: Int
|
||||||
let script: Data
|
let script: Data
|
||||||
let valueZat: Int
|
let valueZat: Int
|
||||||
let height: Int
|
let height: Int
|
||||||
let spentInTx: Int?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UTXO: UnspentTransactionOutputEntity {
|
extension UTXO: UnspentTransactionOutputEntity {
|
||||||
|
@ -48,145 +35,3 @@ extension UTXO: UnspentTransactionOutputEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UnspentTransactionOutputEntity {
|
|
||||||
/**
|
|
||||||
As UTXO, with id and spentIntTx set to __nil__
|
|
||||||
*/
|
|
||||||
func asUTXO() -> UTXO {
|
|
||||||
UTXO(
|
|
||||||
id: nil,
|
|
||||||
address: address,
|
|
||||||
prevoutTxId: txid,
|
|
||||||
prevoutIndex: index,
|
|
||||||
script: script,
|
|
||||||
valueZat: valueZat,
|
|
||||||
height: height,
|
|
||||||
spentInTx: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import SQLite
|
|
||||||
class UnspentTransactionOutputSQLDAO: UnspentTransactionOutputRepository {
|
|
||||||
enum TableColumns {
|
|
||||||
static let id = Expression<Int>("id_utxo")
|
|
||||||
static let address = Expression<String>("address")
|
|
||||||
static let txid = Expression<Blob>("prevout_txid")
|
|
||||||
static let index = Expression<Int>("prevout_idx")
|
|
||||||
static let script = Expression<Blob>("script")
|
|
||||||
static let valueZat = Expression<Int>("value_zat")
|
|
||||||
static let height = Expression<Int>("height")
|
|
||||||
static let spentInTx = Expression<Int?>("spent_in_tx")
|
|
||||||
}
|
|
||||||
|
|
||||||
let table = Table("utxos")
|
|
||||||
|
|
||||||
let dbProvider: ConnectionProvider
|
|
||||||
|
|
||||||
init(dbProvider: ConnectionProvider) {
|
|
||||||
self.dbProvider = dbProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - Throws: `unspentTransactionOutputDAOCreateTable` if creation table fails.
|
|
||||||
func initialise() async throws {
|
|
||||||
try await createTableIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createTableIfNeeded() async throws {
|
|
||||||
let stringStatement =
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS utxos (
|
|
||||||
id_utxo INTEGER PRIMARY KEY,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
prevout_txid BLOB NOT NULL,
|
|
||||||
prevout_idx INTEGER NOT NULL,
|
|
||||||
script BLOB NOT NULL,
|
|
||||||
value_zat INTEGER NOT NULL,
|
|
||||||
height INTEGER NOT NULL,
|
|
||||||
spent_in_tx INTEGER,
|
|
||||||
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
|
|
||||||
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
do {
|
|
||||||
globalDBLock.lock()
|
|
||||||
defer { globalDBLock.unlock() }
|
|
||||||
|
|
||||||
try dbProvider.connection().run(stringStatement)
|
|
||||||
} catch {
|
|
||||||
throw ZcashError.unspentTransactionOutputDAOCreateTable(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - Throws: `unspentTransactionOutputDAOStore` if sqlite query fails.
|
|
||||||
func store(utxos: [UnspentTransactionOutputEntity]) async throws {
|
|
||||||
do {
|
|
||||||
globalDBLock.lock()
|
|
||||||
defer { globalDBLock.unlock() }
|
|
||||||
|
|
||||||
let db = try dbProvider.connection()
|
|
||||||
try db.transaction {
|
|
||||||
for utxo in utxos.map({ $0 as? UTXO ?? $0.asUTXO() }) {
|
|
||||||
try db.run(table.insert(utxo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
throw ZcashError.unspentTransactionOutputDAOStore(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - Throws: `unspentTransactionOutputDAOClearAll` if sqlite query fails.
|
|
||||||
func clearAll(address: String?) async throws {
|
|
||||||
do {
|
|
||||||
globalDBLock.lock()
|
|
||||||
defer { globalDBLock.unlock() }
|
|
||||||
|
|
||||||
if let tAddr = address {
|
|
||||||
try dbProvider.connection().run(table.filter(TableColumns.address == tAddr).delete())
|
|
||||||
} else {
|
|
||||||
try dbProvider.connection().run(table.delete())
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
throw ZcashError.unspentTransactionOutputDAOClearAll(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// - Throws:
|
|
||||||
/// - `unspentTransactionOutputDAOClearAll` if the data fetched from the DB can't be decoded to `UTXO` object.
|
|
||||||
/// - `unspentTransactionOutputDAOGetAll` if sqlite query fails.
|
|
||||||
func getAll(address: String?) async throws -> [UnspentTransactionOutputEntity] {
|
|
||||||
do {
|
|
||||||
if let tAddress = address {
|
|
||||||
let allTxs: [UTXO] = try dbProvider.connection()
|
|
||||||
.prepare(table.filter(TableColumns.address == tAddress))
|
|
||||||
.map { row in
|
|
||||||
do {
|
|
||||||
return try row.decode()
|
|
||||||
} catch {
|
|
||||||
throw ZcashError.unspentTransactionOutputDAOGetAllCantDecode(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return allTxs
|
|
||||||
} else {
|
|
||||||
let allTxs: [UTXO] = try dbProvider.connection()
|
|
||||||
.prepare(table)
|
|
||||||
.map { row in
|
|
||||||
try row.decode()
|
|
||||||
}
|
|
||||||
return allTxs
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if let error = error as? ZcashError {
|
|
||||||
throw error
|
|
||||||
} else {
|
|
||||||
throw ZcashError.unspentTransactionOutputDAOGetAll(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UTXORepositoryBuilder {
|
|
||||||
static func build(initializer: Initializer) -> UnspentTransactionOutputRepository {
|
|
||||||
return UnspentTransactionOutputSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.dataDbURL.path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -223,14 +223,12 @@ extension LightWalletGRPCService: LightWalletService {
|
||||||
do {
|
do {
|
||||||
guard let reply = try await iterator.next() else { return nil }
|
guard let reply = try await iterator.next() else { return nil }
|
||||||
return UTXO(
|
return UTXO(
|
||||||
id: nil,
|
|
||||||
address: reply.address,
|
address: reply.address,
|
||||||
prevoutTxId: reply.txid,
|
prevoutTxId: reply.txid,
|
||||||
prevoutIndex: Int(reply.index),
|
prevoutIndex: Int(reply.index),
|
||||||
script: reply.script,
|
script: reply.script,
|
||||||
valueZat: Int(reply.valueZat),
|
valueZat: Int(reply.valueZat),
|
||||||
height: Int(reply.height),
|
height: Int(reply.height)
|
||||||
spentInTx: nil
|
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
let serviceError = error.mapToServiceError()
|
let serviceError = error.mapToServiceError()
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
//
|
|
||||||
// UnspentTransactionOutputRepository.swift
|
|
||||||
// ZcashLightClientKit
|
|
||||||
//
|
|
||||||
// Created by Francisco Gindre on 12/11/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
protocol UnspentTransactionOutputRepository {
|
|
||||||
func initialise() async throws
|
|
||||||
func getAll(address: String?) async throws -> [UnspentTransactionOutputEntity]
|
|
||||||
func store(utxos: [UnspentTransactionOutputEntity]) async throws
|
|
||||||
func clearAll(address: String?) async throws
|
|
||||||
}
|
|
|
@ -40,7 +40,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
public let network: ZcashNetwork
|
public let network: ZcashNetwork
|
||||||
private let transactionEncoder: TransactionEncoder
|
private let transactionEncoder: TransactionEncoder
|
||||||
private let transactionRepository: TransactionRepository
|
private let transactionRepository: TransactionRepository
|
||||||
private let utxoRepository: UnspentTransactionOutputRepository
|
|
||||||
|
|
||||||
private let syncSessionIDGenerator: SyncSessionIDGenerator
|
private let syncSessionIDGenerator: SyncSessionIDGenerator
|
||||||
private let syncSession: SyncSession
|
private let syncSession: SyncSession
|
||||||
|
@ -55,7 +54,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
initializer: initializer,
|
initializer: initializer,
|
||||||
transactionEncoder: WalletTransactionEncoder(initializer: initializer),
|
transactionEncoder: WalletTransactionEncoder(initializer: initializer),
|
||||||
transactionRepository: initializer.transactionRepository,
|
transactionRepository: initializer.transactionRepository,
|
||||||
utxoRepository: UTXORepositoryBuilder.build(initializer: initializer),
|
|
||||||
blockProcessor: CompactBlockProcessor(
|
blockProcessor: CompactBlockProcessor(
|
||||||
initializer: initializer,
|
initializer: initializer,
|
||||||
walletBirthdayProvider: { initializer.walletBirthday }
|
walletBirthdayProvider: { initializer.walletBirthday }
|
||||||
|
@ -69,7 +67,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
initializer: Initializer,
|
initializer: Initializer,
|
||||||
transactionEncoder: TransactionEncoder,
|
transactionEncoder: TransactionEncoder,
|
||||||
transactionRepository: TransactionRepository,
|
transactionRepository: TransactionRepository,
|
||||||
utxoRepository: UnspentTransactionOutputRepository,
|
|
||||||
blockProcessor: CompactBlockProcessor,
|
blockProcessor: CompactBlockProcessor,
|
||||||
syncSessionTicker: SessionTicker
|
syncSessionTicker: SessionTicker
|
||||||
) {
|
) {
|
||||||
|
@ -78,7 +75,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
self.initializer = initializer
|
self.initializer = initializer
|
||||||
self.transactionEncoder = transactionEncoder
|
self.transactionEncoder = transactionEncoder
|
||||||
self.transactionRepository = transactionRepository
|
self.transactionRepository = transactionRepository
|
||||||
self.utxoRepository = utxoRepository
|
|
||||||
self.blockProcessor = blockProcessor
|
self.blockProcessor = blockProcessor
|
||||||
self.network = initializer.network
|
self.network = initializer.network
|
||||||
self.metrics = initializer.container.resolve(SDKMetrics.self)
|
self.metrics = initializer.container.resolve(SDKMetrics.self)
|
||||||
|
@ -137,8 +133,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
try await utxoRepository.initialise()
|
|
||||||
|
|
||||||
if case .seedRequired = try await self.initializer.initialize(with: seed, walletBirthday: walletBirthday, for: walletMode) {
|
if case .seedRequired = try await self.initializer.initialize(with: seed, walletBirthday: walletBirthday, for: walletMode) {
|
||||||
return .seedRequired
|
return .seedRequired
|
||||||
}
|
}
|
||||||
|
@ -502,25 +496,6 @@ public class SDKSynchronizer: Synchronizer {
|
||||||
try await blockProcessor.latestHeight()
|
try await blockProcessor.latestHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func latestUTXOs(address: String) async throws -> [UnspentTransactionOutputEntity] {
|
|
||||||
try throwIfUnprepared()
|
|
||||||
|
|
||||||
guard initializer.isValidTransparentAddress(address) else {
|
|
||||||
throw ZcashError.synchronizerLatestUTXOsInvalidTAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = initializer.lightWalletService.fetchUTXOs(for: address, height: network.constants.saplingActivationHeight)
|
|
||||||
|
|
||||||
// swiftlint:disable:next array_constructor
|
|
||||||
var utxos: [UnspentTransactionOutputEntity] = []
|
|
||||||
for try await transactionEntity in stream {
|
|
||||||
utxos.append(transactionEntity)
|
|
||||||
}
|
|
||||||
try await self.utxoRepository.clearAll(address: address)
|
|
||||||
try await self.utxoRepository.store(utxos: utxos)
|
|
||||||
return utxos
|
|
||||||
}
|
|
||||||
|
|
||||||
public func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs {
|
public func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs {
|
||||||
try throwIfUnprepared()
|
try throwIfUnprepared()
|
||||||
return try await blockProcessor.refreshUTXOs(tAddress: address, startHeight: height)
|
return try await blockProcessor.refreshUTXOs(tAddress: address, startHeight: height)
|
||||||
|
|
Loading…
Reference in New Issue