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:
Jack Grigg 2024-03-15 14:23:25 +00:00
parent 6207cc999b
commit dd9942b6ab
5 changed files with 5 additions and 198 deletions

View File

@ -18,6 +18,10 @@ Possible errors:
- `ZcashError.rustProposeTransferFromURI`
- Other errors that `sentToAddress` can throw
## Removed
- `SDKSynchronizer.latestUTXOs`
# 2.0.11 - 2024-03-08
## Changed

View File

@ -8,25 +8,12 @@
import Foundation
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
var prevoutTxId: Data
var prevoutIndex: Int
let script: Data
let valueZat: Int
let height: Int
let spentInTx: Int?
}
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))
}
}

View File

@ -223,14 +223,12 @@ extension LightWalletGRPCService: LightWalletService {
do {
guard let reply = try await iterator.next() else { return nil }
return UTXO(
id: nil,
address: reply.address,
prevoutTxId: reply.txid,
prevoutIndex: Int(reply.index),
script: reply.script,
valueZat: Int(reply.valueZat),
height: Int(reply.height),
spentInTx: nil
height: Int(reply.height)
)
} catch {
let serviceError = error.mapToServiceError()

View File

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

View File

@ -40,7 +40,6 @@ public class SDKSynchronizer: Synchronizer {
public let network: ZcashNetwork
private let transactionEncoder: TransactionEncoder
private let transactionRepository: TransactionRepository
private let utxoRepository: UnspentTransactionOutputRepository
private let syncSessionIDGenerator: SyncSessionIDGenerator
private let syncSession: SyncSession
@ -55,7 +54,6 @@ public class SDKSynchronizer: Synchronizer {
initializer: initializer,
transactionEncoder: WalletTransactionEncoder(initializer: initializer),
transactionRepository: initializer.transactionRepository,
utxoRepository: UTXORepositoryBuilder.build(initializer: initializer),
blockProcessor: CompactBlockProcessor(
initializer: initializer,
walletBirthdayProvider: { initializer.walletBirthday }
@ -69,7 +67,6 @@ public class SDKSynchronizer: Synchronizer {
initializer: Initializer,
transactionEncoder: TransactionEncoder,
transactionRepository: TransactionRepository,
utxoRepository: UnspentTransactionOutputRepository,
blockProcessor: CompactBlockProcessor,
syncSessionTicker: SessionTicker
) {
@ -78,7 +75,6 @@ public class SDKSynchronizer: Synchronizer {
self.initializer = initializer
self.transactionEncoder = transactionEncoder
self.transactionRepository = transactionRepository
self.utxoRepository = utxoRepository
self.blockProcessor = blockProcessor
self.network = initializer.network
self.metrics = initializer.container.resolve(SDKMetrics.self)
@ -137,8 +133,6 @@ public class SDKSynchronizer: Synchronizer {
throw error
}
try await utxoRepository.initialise()
if case .seedRequired = try await self.initializer.initialize(with: seed, walletBirthday: walletBirthday, for: walletMode) {
return .seedRequired
}
@ -502,25 +496,6 @@ public class SDKSynchronizer: Synchronizer {
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 {
try throwIfUnprepared()
return try await blockProcessor.refreshUTXOs(tAddress: address, startHeight: height)