From a5a0ef0ac10c99fec426fd1766525412ba5e8ed7 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 15 Mar 2024 16:01:39 +0000 Subject: [PATCH] Migrate to latest in-progress revision of Rust crates - New backend method `ZcashRustBackend.isSeedRelevantToWallet` - `ZcashRustBackend.scanBlocks` now takes a `fromState` argument. Co-authored-by: Lukas Korba --- .../xcshareddata/swiftpm/Package.resolved | 2 +- Package.resolved | 2 +- Package.swift | 4 +- .../Block/Scan/BlockScanner.swift | 5 +- .../Entity/TransactionEntity.swift | 4 +- .../Error/ZcashError.swift | 6 ++ .../Error/ZcashErrorCode.swift | 2 + .../Error/ZcashErrorCodeDefinition.swift | 4 + .../Service/GRPC/LightWalletGRPCService.swift | 4 + .../Modules/Service/LightWalletService.swift | 2 + .../Rust/ZcashRustBackend.swift | 36 ++++++- .../Rust/ZcashRustBackendWelding.swift | 8 +- .../Synchronizer/Dependencies.swift | 2 + Tests/TestUtils/DarkSideWalletService.swift | 4 + Tests/TestUtils/FakeService.swift | 4 + .../AutoMockable.generated.swift | 97 +++++++++++++++---- Tests/TestUtils/Stubs.swift | 4 +- 17 files changed, 158 insertions(+), 32 deletions(-) diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 587a1805..6c8ed3d6 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -176,7 +176,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "4eccfb5ea825f5f0ebb3cab964d08ef21103feb4" + "revision" : "d1d038d653806f23fc291931823d95f2dc411486" } } ], diff --git a/Package.resolved b/Package.resolved index 176c726a..a77515f2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,7 +122,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "4eccfb5ea825f5f0ebb3cab964d08ef21103feb4" + "revision" : "d1d038d653806f23fc291931823d95f2dc411486" } } ], diff --git a/Package.swift b/Package.swift index 3aa8d8cf..9891f59b 100644 --- a/Package.swift +++ b/Package.swift @@ -16,8 +16,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.19.1"), .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.14.1"), - // Compiled from revision `c2ac47300d062b76134c515589301b202277e3fa`. - .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "4eccfb5ea825f5f0ebb3cab964d08ef21103feb4") + // Compiled from revision `70cce1272c26ed52fbe7bfa334be781373b64bfd`. + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "d1d038d653806f23fc291931823d95f2dc411486") ], targets: [ .target( diff --git a/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift b/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift index cf203f6b..48d38d4d 100644 --- a/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift +++ b/Sources/ZcashLightClientKit/Block/Scan/BlockScanner.swift @@ -23,6 +23,7 @@ protocol BlockScanner { struct BlockScannerImpl { let config: BlockScannerConfig let rustBackend: ZcashRustBackendWelding + let service: LightWalletService let transactionRepository: TransactionRepository let metrics: SDKMetrics let logger: Logger @@ -56,7 +57,9 @@ extension BlockScannerImpl: BlockScanner { let scanSummary: ScanSummary let scanStartTime = Date() do { - scanSummary = try await self.rustBackend.scanBlocks(fromHeight: Int32(startHeight), limit: batchSize) + let fromState = try await service.getTreeState(BlockID(height: startHeight - 1)) + + scanSummary = try await self.rustBackend.scanBlocks(fromHeight: Int32(startHeight), fromState: fromState, limit: batchSize) } catch { logger.debug("block scanning failed with error: \(String(describing: error))") throw error diff --git a/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift b/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift index f52f816c..245cada3 100644 --- a/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift +++ b/Sources/ZcashLightClientKit/Entity/TransactionEntity.swift @@ -101,8 +101,8 @@ extension ZcashTransaction.Output { static let rawID = Expression("txid") static let pool = Expression("output_pool") static let index = Expression("output_index") - static let toAccount = Expression("to_account") - static let fromAccount = Expression("from_account") + static let toAccount = Expression("to_account_id") + static let fromAccount = Expression("from_account_id") static let toAddress = Expression("to_address") static let value = Expression("value") static let isChange = Expression("is_change") diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index 2f7fb20e..50f01e03 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -329,6 +329,10 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0058 case rustListAccounts(_ rustError: String) + /// Error from rust layer when calling ZcashRustBackend.isSeedRelevantToWallet + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0059 + case rustIsSeedRelevantToWallet(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -686,6 +690,7 @@ public enum ZcashError: Equatable, Error { case .rustGetWalletSummary: return "Error from rust layer when calling ZcashRustBackend.getWalletSummary" case .rustProposeTransferFromURI: return "Error from rust layer when calling ZcashRustBackend." case .rustListAccounts: return "Error from rust layer when calling ZcashRustBackend." + case .rustIsSeedRelevantToWallet: return "Error from rust layer when calling ZcashRustBackend.isSeedRelevantToWallet" case .accountDAOGetAll: return "SQLite query failed when fetching all accounts from the database." case .accountDAOGetAllCantDecode: return "Fetched accounts from SQLite but can't decode them." case .accountDAOFindBy: return "SQLite query failed when seaching for accounts in the database." @@ -862,6 +867,7 @@ public enum ZcashError: Equatable, Error { case .rustGetWalletSummary: return .rustGetWalletSummary case .rustProposeTransferFromURI: return .rustProposeTransferFromURI case .rustListAccounts: return .rustListAccounts + case .rustIsSeedRelevantToWallet: return .rustIsSeedRelevantToWallet case .accountDAOGetAll: return .accountDAOGetAll case .accountDAOGetAllCantDecode: return .accountDAOGetAllCantDecode case .accountDAOFindBy: return .accountDAOFindBy diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift index 3eea20a3..2eb3ad15 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -179,6 +179,8 @@ public enum ZcashErrorCode: String { case rustProposeTransferFromURI = "ZRUST0057" /// Error from rust layer when calling ZcashRustBackend. case rustListAccounts = "ZRUST0058" + /// Error from rust layer when calling ZcashRustBackend.isSeedRelevantToWallet + case rustIsSeedRelevantToWallet = "ZRUST0059" /// SQLite query failed when fetching all accounts from the database. case accountDAOGetAll = "ZADAO0001" /// Fetched accounts from SQLite but can't decode them. diff --git a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift index f574dc1f..a72d9062 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCodeDefinition.swift @@ -356,6 +356,10 @@ enum ZcashErrorDefinition { /// - `rustError` contains error generated by the rust layer. // sourcery: code="ZRUST0058" case rustListAccounts(_ rustError: String) + /// Error from rust layer when calling ZcashRustBackend.isSeedRelevantToWallet + /// - `rustError` contains error generated by the rust layer. + // sourcery: code="ZRUST0059" + case rustIsSeedRelevantToWallet(_ rustError: String) // MARK: - Account DAO diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift b/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift index c089f531..02d2b534 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/LightWalletGRPCService.swift @@ -276,6 +276,10 @@ extension LightWalletGRPCService: LightWalletService { } } + func getTreeState(_ id: BlockID) async throws -> TreeState { + try await compactTxStreamer.getTreeState(id) + } + func closeConnection() { _ = channel.close() } diff --git a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift index b6f3cf60..9680588f 100644 --- a/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift +++ b/Sources/ZcashLightClientKit/Modules/Service/LightWalletService.swift @@ -196,4 +196,6 @@ protocol LightWalletService: AnyObject { /// - Parameters: /// - request: Request to send to GetSubtreeRoots. func getSubtreeRoots(_ request: GetSubtreeRootsArg) -> AsyncThrowingStream + + func getTreeState(_ id: BlockID) async throws -> TreeState } diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 3e0b9e15..2791187a 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -105,6 +105,26 @@ actor ZcashRustBackend: ZcashRustBackendWelding { return ffiBinaryKeyPtr.pointee.unsafeToUnifiedSpendingKey(network: networkType) } + func isSeedRelevantToWallet(seed: [UInt8]) async throws -> Bool { + globalDBLock.lock() + let result = zcashlc_is_seed_relevant_to_wallet( + dbData.0, + dbData.1, + seed, + UInt(seed.count), + networkType.networkId + ) + globalDBLock.unlock() + + // -1 is the error sentinel. + guard result >= 0 else { + throw ZcashError.rustIsSeedRelevantToWallet(lastErrorMessage(fallback: "`isSeedRelevantToWallet` failed with unknown error")) + } + + // 0 is false, 1 is true. + return result != 0 + } + func proposeTransfer( account: Int32, to address: String, @@ -648,9 +668,20 @@ actor ZcashRustBackend: ZcashRustBackendWelding { return scanRanges } - func scanBlocks(fromHeight: Int32, limit: UInt32 = 0) async throws -> ScanSummary { + func scanBlocks(fromHeight: Int32, fromState: TreeState, limit: UInt32 = 0) async throws -> ScanSummary { + let fromStateBytes = try fromState.serializedData(partial: false).bytes + globalDBLock.lock() - let summaryPtr = zcashlc_scan_blocks(fsBlockDbRoot.0, fsBlockDbRoot.1, dbData.0, dbData.1, fromHeight, limit, networkType.networkId) + let summaryPtr = zcashlc_scan_blocks( + fsBlockDbRoot.0, + fsBlockDbRoot.1, + dbData.0, + dbData.1, + fromHeight, + fromStateBytes, + UInt(fromStateBytes.count), + limit, + networkType.networkId) globalDBLock.unlock() guard let summaryPtr else { @@ -881,6 +912,7 @@ extension FfiScanProgress { } } +// swiftlint:disable large_tuple line_length struct FfiTxId { var tuple: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) var array: [UInt8] { diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 807a2114..c4409ff2 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -44,6 +44,11 @@ protocol ZcashRustBackendWelding { /// - Throws: `rustCreateAccount`. func createAccount(seed: [UInt8], treeState: TreeState, recoverUntil: UInt32?) async throws -> UnifiedSpendingKey + /// Checks whether the given seed is relevant to any of the accounts in the wallet. + /// + /// - parameter seed: byte array of the seed + func isSeedRelevantToWallet(seed: [UInt8]) async throws -> Bool + /// Scans a transaction for any information that can be decrypted by the accounts in the wallet, and saves it to the wallet. /// - parameter tx: the transaction to decrypt /// - parameter minedHeight: height on which this transaction was mined. this is used to fetch the consensus branch ID. @@ -176,9 +181,10 @@ protocol ZcashRustBackendWelding { /// cache, an error will be signalled. /// /// - parameter fromHeight: scan starting from the given height. + /// - parameter fromState: The TreeState Protobuf object for the height prior to `fromHeight` /// - parameter limit: scan up to limit blocks. /// - Throws: `rustScanBlocks` if rust layer returns error. - func scanBlocks(fromHeight: Int32, limit: UInt32) async throws -> ScanSummary + func scanBlocks(fromHeight: Int32, fromState: TreeState, limit: UInt32) async throws -> ScanSummary /// Upserts a UTXO into the data db database /// - parameter txid: the txid bytes for the UTXO diff --git a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift index 8405a332..dd32a14f 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/Dependencies.swift @@ -119,6 +119,7 @@ enum Dependencies { } container.register(type: BlockScanner.self, isSingleton: true) { di in + let service = di.resolve(LightWalletService.self) let rustBackend = di.resolve(ZcashRustBackendWelding.self) let transactionRepository = di.resolve(TransactionRepository.self) let metrics = di.resolve(SDKMetrics.self) @@ -132,6 +133,7 @@ enum Dependencies { return BlockScannerImpl( config: blockScannerConfig, rustBackend: rustBackend, + service: service, transactionRepository: transactionRepository, metrics: metrics, logger: logger diff --git a/Tests/TestUtils/DarkSideWalletService.swift b/Tests/TestUtils/DarkSideWalletService.swift index 60e3f90e..2b1fbb56 100644 --- a/Tests/TestUtils/DarkSideWalletService.swift +++ b/Tests/TestUtils/DarkSideWalletService.swift @@ -190,6 +190,10 @@ class DarksideWalletService: LightWalletService { func getSubtreeRoots(_ request: ZcashLightClientKit.GetSubtreeRootsArg) -> AsyncThrowingStream { service.getSubtreeRoots(request) } + + func getTreeState(_ id: BlockID) async throws -> TreeState { + try await service.getTreeState(id) + } } enum DarksideWalletDConstants: NetworkConstants { diff --git a/Tests/TestUtils/FakeService.swift b/Tests/TestUtils/FakeService.swift index 07e2d74b..3358f519 100644 --- a/Tests/TestUtils/FakeService.swift +++ b/Tests/TestUtils/FakeService.swift @@ -82,4 +82,8 @@ class MockLightWalletService: LightWalletService { func getSubtreeRoots(_ request: ZcashLightClientKit.GetSubtreeRootsArg) -> AsyncThrowingStream { service.getSubtreeRoots(request) } + + func getTreeState(_ id: BlockID) async throws -> TreeState { + try await service.getTreeState(id) + } } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 12d12d34..1cba8589 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -963,6 +963,30 @@ class LightWalletServiceMock: LightWalletService { } } + // MARK: - getTreeState + + var getTreeStateThrowableError: Error? + var getTreeStateCallsCount = 0 + var getTreeStateCalled: Bool { + return getTreeStateCallsCount > 0 + } + var getTreeStateReceivedId: BlockID? + var getTreeStateReturnValue: TreeState! + var getTreeStateClosure: ((BlockID) async throws -> TreeState)? + + func getTreeState(_ id: BlockID) async throws -> TreeState { + if let error = getTreeStateThrowableError { + throw error + } + getTreeStateCallsCount += 1 + getTreeStateReceivedId = id + if let closure = getTreeStateClosure { + return try await closure(id) + } else { + return getTreeStateReturnValue + } + } + } class LightWalletdInfoMock: LightWalletdInfo { @@ -2277,6 +2301,39 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } + // MARK: - isSeedRelevantToWallet + + var isSeedRelevantToWalletSeedThrowableError: Error? + func setIsSeedRelevantToWalletSeedThrowableError(_ param: Error?) async { + isSeedRelevantToWalletSeedThrowableError = param + } + var isSeedRelevantToWalletSeedCallsCount = 0 + var isSeedRelevantToWalletSeedCalled: Bool { + return isSeedRelevantToWalletSeedCallsCount > 0 + } + var isSeedRelevantToWalletSeedReceivedSeed: [UInt8]? + var isSeedRelevantToWalletSeedReturnValue: Bool! + func setIsSeedRelevantToWalletSeedReturnValue(_ param: Bool) async { + isSeedRelevantToWalletSeedReturnValue = param + } + var isSeedRelevantToWalletSeedClosure: (([UInt8]) async throws -> Bool)? + func setIsSeedRelevantToWalletSeedClosure(_ param: (([UInt8]) async throws -> Bool)?) async { + isSeedRelevantToWalletSeedClosure = param + } + + func isSeedRelevantToWallet(seed: [UInt8]) async throws -> Bool { + if let error = isSeedRelevantToWalletSeedThrowableError { + throw error + } + isSeedRelevantToWalletSeedCallsCount += 1 + isSeedRelevantToWalletSeedReceivedSeed = seed + if let closure = isSeedRelevantToWalletSeedClosure { + return try await closure(seed) + } else { + return isSeedRelevantToWalletSeedReturnValue + } + } + // MARK: - decryptAndStoreTransaction var decryptAndStoreTransactionTxBytesMinedHeightThrowableError: Error? @@ -2792,34 +2849,34 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { // MARK: - scanBlocks - var scanBlocksFromHeightLimitThrowableError: Error? - func setScanBlocksFromHeightLimitThrowableError(_ param: Error?) async { - scanBlocksFromHeightLimitThrowableError = param + var scanBlocksFromHeightFromStateLimitThrowableError: Error? + func setScanBlocksFromHeightFromStateLimitThrowableError(_ param: Error?) async { + scanBlocksFromHeightFromStateLimitThrowableError = param } - var scanBlocksFromHeightLimitCallsCount = 0 - var scanBlocksFromHeightLimitCalled: Bool { - return scanBlocksFromHeightLimitCallsCount > 0 + var scanBlocksFromHeightFromStateLimitCallsCount = 0 + var scanBlocksFromHeightFromStateLimitCalled: Bool { + return scanBlocksFromHeightFromStateLimitCallsCount > 0 } - var scanBlocksFromHeightLimitReceivedArguments: (fromHeight: Int32, limit: UInt32)? - var scanBlocksFromHeightLimitReturnValue: ScanSummary! - func setScanBlocksFromHeightLimitReturnValue(_ param: ScanSummary) async { - scanBlocksFromHeightLimitReturnValue = param + var scanBlocksFromHeightFromStateLimitReceivedArguments: (fromHeight: Int32, fromState: TreeState, limit: UInt32)? + var scanBlocksFromHeightFromStateLimitReturnValue: ScanSummary! + func setScanBlocksFromHeightFromStateLimitReturnValue(_ param: ScanSummary) async { + scanBlocksFromHeightFromStateLimitReturnValue = param } - var scanBlocksFromHeightLimitClosure: ((Int32, UInt32) async throws -> ScanSummary)? - func setScanBlocksFromHeightLimitClosure(_ param: ((Int32, UInt32) async throws -> ScanSummary)?) async { - scanBlocksFromHeightLimitClosure = param + var scanBlocksFromHeightFromStateLimitClosure: ((Int32, TreeState, UInt32) async throws -> ScanSummary)? + func setScanBlocksFromHeightFromStateLimitClosure(_ param: ((Int32, TreeState, UInt32) async throws -> ScanSummary)?) async { + scanBlocksFromHeightFromStateLimitClosure = param } - func scanBlocks(fromHeight: Int32, limit: UInt32) async throws -> ScanSummary { - if let error = scanBlocksFromHeightLimitThrowableError { + func scanBlocks(fromHeight: Int32, fromState: TreeState, limit: UInt32) async throws -> ScanSummary { + if let error = scanBlocksFromHeightFromStateLimitThrowableError { throw error } - scanBlocksFromHeightLimitCallsCount += 1 - scanBlocksFromHeightLimitReceivedArguments = (fromHeight: fromHeight, limit: limit) - if let closure = scanBlocksFromHeightLimitClosure { - return try await closure(fromHeight, limit) + scanBlocksFromHeightFromStateLimitCallsCount += 1 + scanBlocksFromHeightFromStateLimitReceivedArguments = (fromHeight: fromHeight, fromState: fromState, limit: limit) + if let closure = scanBlocksFromHeightFromStateLimitClosure { + return try await closure(fromHeight, fromState, limit) } else { - return scanBlocksFromHeightLimitReturnValue + return scanBlocksFromHeightFromStateLimitReturnValue } } diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index f5f33521..b552b079 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -102,8 +102,8 @@ class RustBackendMockHelper { try await rustBackend.suggestScanRanges() } - await rustBackendMock.setScanBlocksFromHeightLimitClosure() { fromHeight, limit in - try await rustBackend.scanBlocks(fromHeight: fromHeight, limit: limit) + await rustBackendMock.setScanBlocksFromHeightFromStateLimitClosure { fromHeight, fromState, limit in + try await rustBackend.scanBlocks(fromHeight: fromHeight, fromState: fromState, limit: limit) } }