Skip to content

Commit

Permalink
Merge branch 'main' into ffi-0.13.0
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Feb 27, 2025
2 parents 07b33f1 + 72c5e84 commit 8f9cb31
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
permissions:
contents: read

runs-on: macos-14
runs-on: macos-15

steps:
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `SDKSynchronizer.redactPCZTForSigner`: Decrease the size of a PCZT for sending to a signer.
- `SDKSynchronizer.PCZTRequiresSaplingProofs`: Check whether the Sapling parameters are required for a given PCZT.

## Updated
- Methods returning an array of `ZcashTransaction.Overview` try to evaluate transaction's missing blockTime. This typically applies to an expired transaction.

# 2.2.8 - 2025-01-10

## Added
Expand Down
65 changes: 65 additions & 0 deletions Sources/ZcashLightClientKit/DAO/BlockDao.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// BlockDao.swift
// ZcashLightClientKit
//
// Created by Lukas Korba on 2025-01-25.
//

import Foundation
import SQLite

protocol BlockDao {
func block(at height: BlockHeight) throws -> Block?
}

struct Block: Codable {
enum CodingKeys: String, CodingKey {
case height
case time
}

enum TableStructure {
static let height = SQLite.Expression<Int>(Block.CodingKeys.height.rawValue)
static let time = SQLite.Expression<Int>(Block.CodingKeys.time.rawValue)
}

let height: BlockHeight
let time: Int

static let table = Table("blocks")
}

class BlockSQLDAO: BlockDao {
let dbProvider: ConnectionProvider
let table: Table
let height = SQLite.Expression<Int>("height")

init(dbProvider: ConnectionProvider) {
self.dbProvider = dbProvider
self.table = Table("Blocks")
}

/// - Throws:
/// - `blockDAOCantDecode` if block data loaded from DB can't be decoded to `Block` object.
/// - `blockDAOBlock` if sqlite query to load block metadata failed.
func block(at height: BlockHeight) throws -> Block? {
do {
return try dbProvider
.connection()
.prepare(Block.table.filter(Block.TableStructure.height == height).limit(1))
.map {
do {
return try $0.decode()
} catch {
throw ZcashError.blockDAOCantDecode(error)
}
}
.first
} catch {
if let error = error as? ZcashError {
throw error
} else {
throw ZcashError.blockDAOBlock(error)
}
}
}
}
54 changes: 53 additions & 1 deletion Sources/ZcashLightClientKit/DAO/TransactionDao.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ class TransactionSQLDAO: TransactionRepository {
static let memo = SQLite.Expression<Blob>("memo")
}

enum UserMetadata {
static let txid = SQLite.Expression<Blob>("txid")
static let memoCount = SQLite.Expression<Int>("memo_count")
static let memo = SQLite.Expression<String>("memo")
}

let dbProvider: ConnectionProvider

private let blockDao: BlockSQLDAO
private let transactionsView = View("v_transactions")
private let txOutputsView = View("v_tx_outputs")
private let traceClosure: ((String) -> Void)?

init(dbProvider: ConnectionProvider, traceClosure: ((String) -> Void)? = nil) {
self.dbProvider = dbProvider
self.blockDao = BlockSQLDAO(dbProvider: dbProvider)
self.traceClosure = traceClosure
}

Expand All @@ -38,7 +46,49 @@ class TransactionSQLDAO: TransactionRepository {
func isInitialized() async throws -> Bool {
true
}

func resolveMissingBlockTimes(for transactions: [ZcashTransaction.Overview]) async throws -> [ZcashTransaction.Overview] {
var transactionsCopy = transactions

for i in 0..<transactions.count {
let transaction = transactions[i]

guard transaction.blockTime == nil else {
continue
}

if let expiryHeight = transaction.expiryHeight {
if let block = try await blockForHeight(expiryHeight) {
transactionsCopy[i].blockTime = TimeInterval(block.time)
}
}
}

return transactionsCopy
}

@DBActor
func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data] {
let query = transactionsView
.join(txOutputsView, on: transactionsView[UserMetadata.txid] == txOutputsView[UserMetadata.txid])
.filter(transactionsView[UserMetadata.memoCount] > 0)
.filter(txOutputsView[UserMetadata.memo].like("%\(searchTerm)%"))

var txids: [Data] = []
for row in try connection().prepare(query) {
let txidBlob = try row.get(txOutputsView[UserMetadata.txid])
let txid = Data(blob: txidBlob)
txids.append(txid)
}

return txids
}

@DBActor
func blockForHeight(_ height: BlockHeight) async throws -> Block? {
try blockDao.block(at: height)
}

@DBActor
func countAll() async throws -> Int {
do {
Expand Down Expand Up @@ -71,7 +121,9 @@ class TransactionSQLDAO: TransactionRepository {
.filterQueryFor(kind: kind)
.limit(limit, offset: offset)

return try await execute(query) { try ZcashTransaction.Overview(row: $0) }
let transactions: [ZcashTransaction.Overview] = try await execute(query) { try ZcashTransaction.Overview(row: $0) }

return try await resolveMissingBlockTimes(for: transactions)
}

func find(in range: CompactBlockRange, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview] {
Expand Down
14 changes: 9 additions & 5 deletions Sources/ZcashLightClientKit/Entity/TransactionEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import SQLite

public enum ZcashTransaction {
public struct Overview {
public struct Overview: Equatable, Identifiable {
/// Represents the transaction state based on current height of the chain,
/// mined height and expiry height of a transaction.
public enum State {
Expand Down Expand Up @@ -43,9 +43,11 @@ public enum ZcashTransaction {
}
}
}

public var id: Data { rawID }

public let accountUUID: AccountUUID
public let blockTime: TimeInterval?
public var blockTime: TimeInterval?
public let expiryHeight: BlockHeight?
public let fee: Zatoshi?
public let index: Int?
Expand All @@ -62,8 +64,8 @@ public enum ZcashTransaction {
public let isExpiredUmined: Bool?
}

public struct Output {
public enum Pool {
public struct Output: Equatable, Identifiable {
public enum Pool: Equatable {
case transaparent
case sapling
case orchard
Expand All @@ -82,6 +84,8 @@ public enum ZcashTransaction {
}
}

public var id: Data { rawID }

public let rawID: Data
public let pool: Pool
public let index: Int
Expand All @@ -93,7 +97,7 @@ public enum ZcashTransaction {
}

/// Used when fetching blocks from the lightwalletd
struct Fetched {
struct Fetched: Equatable {
public let rawID: Data
public let minedHeight: UInt32?
public let raw: Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ protocol TransactionRepository {
func countAll() async throws -> Int
func countUnmined() async throws -> Int
func isInitialized() async throws -> Bool
func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data]
func find(rawID: Data) async throws -> ZcashTransaction.Overview
func find(offset: Int, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview]
func find(in range: CompactBlockRange, limit: Int, kind: TransactionKind) async throws -> [ZcashTransaction.Overview]
Expand Down
4 changes: 3 additions & 1 deletion Sources/ZcashLightClientKit/Synchronizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public enum SynchronizerEvent {
case minedTransaction(ZcashTransaction.Overview)

// Sent when the synchronizer finds a mined transaction
case foundTransactions(_ transactions: [ZcashTransaction.Overview], _ inRange: CompactBlockRange)
case foundTransactions(_ transactions: [ZcashTransaction.Overview], _ inRange: CompactBlockRange?)
// Sent when the synchronizer fetched utxos from lightwalletd attempted to store them.
case storedUTXOs(_ inserted: [UnspentTransactionOutputEntity], _ skipped: [UnspentTransactionOutputEntity])
// Connection state to LightwalletEndpoint changed.
Expand Down Expand Up @@ -355,6 +355,8 @@ public protocol Synchronizer: AnyObject {
keySource: String?
) async throws -> AccountUUID

func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data]

/// Rescans the known blocks with the current keys.
///
/// `rewind(policy:)` can be called anytime. If the sync process is in progress then it is stopped first. In this case, it make some significant
Expand Down
11 changes: 11 additions & 0 deletions Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ public class SDKSynchronizer: Synchronizer {
}

private func foundTransactions(transactions: [ZcashTransaction.Overview], in range: CompactBlockRange) {
guard !transactions.isEmpty else { return }

streamsUpdateQueue.async { [weak self] in
self?.eventSubject.send(.foundTransactions(transactions, range))
}
Expand Down Expand Up @@ -379,6 +381,11 @@ public class SDKSynchronizer: Synchronizer {
var iterator = transactions.makeIterator()
var submitFailed = false

// let clients know the transaction repository changed
if !transactions.isEmpty {
eventSubject.send(.foundTransactions(transactions, nil))
}

return AsyncThrowingStream() {
guard let transaction = iterator.next() else { return nil }

Expand Down Expand Up @@ -447,6 +454,10 @@ public class SDKSynchronizer: Synchronizer {
return submitTransactions(transactions)
}

public func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data] {
try await transactionRepository.fetchTxidsWithMemoContaining(searchTerm: searchTerm)
}

public func allReceivedTransactions() async throws -> [ZcashTransaction.Overview] {
try await transactionRepository.findReceived(offset: 0, limit: Int.max)
}
Expand Down
4 changes: 4 additions & 0 deletions Tests/TestUtils/MockTransactionRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ extension MockTransactionRepository.Kind: Equatable {}

// MARK: - TransactionRepository
extension MockTransactionRepository: TransactionRepository {
func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data] {
[]
}

func findForResubmission(upTo: ZcashLightClientKit.BlockHeight) async throws -> [ZcashLightClientKit.ZcashTransaction.Overview] {
[]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,30 @@ class SynchronizerMock: Synchronizer {
}
}

// MARK: - fetchTxidsWithMemoContaining

var fetchTxidsWithMemoContainingSearchTermThrowableError: Error?
var fetchTxidsWithMemoContainingSearchTermCallsCount = 0
var fetchTxidsWithMemoContainingSearchTermCalled: Bool {
return fetchTxidsWithMemoContainingSearchTermCallsCount > 0
}
var fetchTxidsWithMemoContainingSearchTermReceivedSearchTerm: String?
var fetchTxidsWithMemoContainingSearchTermReturnValue: [Data]!
var fetchTxidsWithMemoContainingSearchTermClosure: ((String) async throws -> [Data])?

func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data] {
if let error = fetchTxidsWithMemoContainingSearchTermThrowableError {
throw error
}
fetchTxidsWithMemoContainingSearchTermCallsCount += 1
fetchTxidsWithMemoContainingSearchTermReceivedSearchTerm = searchTerm
if let closure = fetchTxidsWithMemoContainingSearchTermClosure {
return try await closure(searchTerm)
} else {
return fetchTxidsWithMemoContainingSearchTermReturnValue
}
}

// MARK: - rewind

var rewindCallsCount = 0
Expand Down Expand Up @@ -2135,6 +2159,30 @@ class TransactionRepositoryMock: TransactionRepository {
}
}

// MARK: - fetchTxidsWithMemoContaining

var fetchTxidsWithMemoContainingSearchTermThrowableError: Error?
var fetchTxidsWithMemoContainingSearchTermCallsCount = 0
var fetchTxidsWithMemoContainingSearchTermCalled: Bool {
return fetchTxidsWithMemoContainingSearchTermCallsCount > 0
}
var fetchTxidsWithMemoContainingSearchTermReceivedSearchTerm: String?
var fetchTxidsWithMemoContainingSearchTermReturnValue: [Data]!
var fetchTxidsWithMemoContainingSearchTermClosure: ((String) async throws -> [Data])?

func fetchTxidsWithMemoContaining(searchTerm: String) async throws -> [Data] {
if let error = fetchTxidsWithMemoContainingSearchTermThrowableError {
throw error
}
fetchTxidsWithMemoContainingSearchTermCallsCount += 1
fetchTxidsWithMemoContainingSearchTermReceivedSearchTerm = searchTerm
if let closure = fetchTxidsWithMemoContainingSearchTermClosure {
return try await closure(searchTerm)
} else {
return fetchTxidsWithMemoContainingSearchTermReturnValue
}
}

// MARK: - find

var findRawIDThrowableError: Error?
Expand Down

0 comments on commit 8f9cb31

Please sign in to comment.