// // ZcashRustBackend.swift // ZcashLightClientKit // // Created by Jack Grigg on 5/8/19. // Copyright © 2019 Electric Coin Company. All rights reserved. // // swiftlint:disable type_body_length import Foundation import libzcashlc let globalDBLock = NSLock() actor ZcashRustBackend: ZcashRustBackendWelding { let minimumConfirmations: UInt32 = 10 let useZIP317Fees = true let dbData: (String, UInt) let fsBlockDbRoot: (String, UInt) let spendParamsPath: (String, UInt) let outputParamsPath: (String, UInt) let keyDeriving: ZcashKeyDerivationBackendWelding nonisolated let networkType: NetworkType static var tracingEnabled = false /// Creates instance of `ZcashRustBackend`. /// - Parameters: /// - dbData: `URL` pointing to file where data database will be. /// - fsBlockDbRoot: `URL` pointing to the filesystem root directory where the fsBlock cache is. /// this directory is expected to contain a `/blocks` sub-directory with the blocks stored in the convened filename /// format `{height}-{hash}-block`. This directory has must be granted both write and read permissions. /// - spendParamsPath: `URL` pointing to spend parameters file. /// - outputParamsPath: `URL` pointing to output parameters file. /// - networkType: Network type to use. /// - enableTracing: this sets up whether the tracing system will dump logs onto the OSLogger system or not. /// **Important note:** this will enable the tracing **for all instances** of ZcashRustBackend, not only for this one. init(dbData: URL, fsBlockDbRoot: URL, spendParamsPath: URL, outputParamsPath: URL, networkType: NetworkType, enableTracing: Bool = false) { self.dbData = dbData.osStr() self.fsBlockDbRoot = fsBlockDbRoot.osPathStr() self.spendParamsPath = spendParamsPath.osPathStr() self.outputParamsPath = outputParamsPath.osPathStr() self.networkType = networkType self.keyDeriving = ZcashKeyDerivationBackend(networkType: networkType) if enableTracing && !Self.tracingEnabled { Self.tracingEnabled = true Self.enableTracing() } } func listAccounts() async throws -> [Int32] { globalDBLock.lock() let accountsPtr = zcashlc_list_accounts( dbData.0, dbData.1, networkType.networkId ) globalDBLock.unlock() guard let accountsPtr else { throw ZcashError.rustListAccounts(lastErrorMessage(fallback: "`listAccounts` failed with unknown error")) } defer { zcashlc_free_accounts(accountsPtr) } var accounts: [Int32] = [] for i in (0 ..< Int(accountsPtr.pointee.len)) { let account = accountsPtr.pointee.ptr.advanced(by: i).pointee accounts.append(Int32(account.account_index)) } return accounts } func createAccount(seed: [UInt8], treeState: TreeState, recoverUntil: UInt32?) async throws -> UnifiedSpendingKey { var rUntil: Int64 = -1 if let recoverUntil { rUntil = Int64(recoverUntil) } let treeStateBytes = try treeState.serializedData(partial: false).bytes globalDBLock.lock() let ffiBinaryKeyPtr = zcashlc_create_account( dbData.0, dbData.1, seed, UInt(seed.count), treeStateBytes, UInt(treeStateBytes.count), rUntil, networkType.networkId ) globalDBLock.unlock() guard let ffiBinaryKeyPtr else { throw ZcashError.rustCreateAccount(lastErrorMessage(fallback: "`createAccount` failed with unknown error")) } defer { zcashlc_free_binary_key(ffiBinaryKeyPtr) } return ffiBinaryKeyPtr.pointee.unsafeToUnifiedSpendingKey(network: networkType) } func isSeedRelevantToAnyDerivedAccount(seed: [UInt8]) async throws -> Bool { globalDBLock.lock() let result = zcashlc_is_seed_relevant_to_any_derived_account( dbData.0, dbData.1, seed, UInt(seed.count), networkType.networkId ) globalDBLock.unlock() // -1 is the error sentinel. guard result >= 0 else { throw ZcashError.rustIsSeedRelevantToAnyDerivedAccount(lastErrorMessage(fallback: "`isSeedRelevantToAnyDerivedAccount` failed with unknown error")) } // 0 is false, 1 is true. return result != 0 } func proposeTransfer( account: Int32, to address: String, value: Int64, memo: MemoBytes? ) async throws -> FfiProposal { globalDBLock.lock() let proposal = zcashlc_propose_transfer( dbData.0, dbData.1, account, [CChar](address.utf8CString), value, memo?.bytes, networkType.networkId, minimumConfirmations, useZIP317Fees ) globalDBLock.unlock() guard let proposal else { throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`proposeTransfer` failed with unknown error")) } defer { zcashlc_free_boxed_slice(proposal) } return try FfiProposal(contiguousBytes: Data( bytes: proposal.pointee.ptr, count: Int(proposal.pointee.len) )) } func proposeTransferFromURI( _ uri: String, account: Int32 ) async throws -> FfiProposal { globalDBLock.lock() let proposal = zcashlc_propose_transfer_from_uri( dbData.0, dbData.1, account, [CChar](uri.utf8CString), networkType.networkId, minimumConfirmations, useZIP317Fees ) globalDBLock.unlock() guard let proposal else { throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`proposeTransfer` failed with unknown error")) } defer { zcashlc_free_boxed_slice(proposal) } return try FfiProposal(contiguousBytes: Data( bytes: proposal.pointee.ptr, count: Int(proposal.pointee.len) )) } func decryptAndStoreTransaction(txBytes: [UInt8], minedHeight: Int32) async throws { globalDBLock.lock() let result = zcashlc_decrypt_and_store_transaction( dbData.0, dbData.1, txBytes, UInt(txBytes.count), UInt32(minedHeight), networkType.networkId ) globalDBLock.unlock() guard result != 0 else { throw ZcashError.rustDecryptAndStoreTransaction(lastErrorMessage(fallback: "`decryptAndStoreTransaction` failed with unknown error")) } } func getCurrentAddress(account: Int32) async throws -> UnifiedAddress { globalDBLock.lock() let addressCStr = zcashlc_get_current_address( dbData.0, dbData.1, account, networkType.networkId ) globalDBLock.unlock() guard let addressCStr else { throw ZcashError.rustGetCurrentAddress(lastErrorMessage(fallback: "`getCurrentAddress` failed with unknown error")) } defer { zcashlc_string_free(addressCStr) } guard let address = String(validatingUTF8: addressCStr) else { throw ZcashError.rustGetCurrentAddressInvalidAddress } return UnifiedAddress(validatedEncoding: address, networkType: networkType) } func getNearestRewindHeight(height: Int32) async throws -> Int32 { globalDBLock.lock() let result = zcashlc_get_nearest_rewind_height( dbData.0, dbData.1, height, networkType.networkId ) globalDBLock.unlock() guard result > 0 else { throw ZcashError.rustGetNearestRewindHeight(lastErrorMessage(fallback: "`getNearestRewindHeight` failed with unknown error")) } return result } func getNextAvailableAddress(account: Int32) async throws -> UnifiedAddress { globalDBLock.lock() let addressCStr = zcashlc_get_next_available_address( dbData.0, dbData.1, account, networkType.networkId ) globalDBLock.unlock() guard let addressCStr else { throw ZcashError.rustGetNextAvailableAddress(lastErrorMessage(fallback: "`getNextAvailableAddress` failed with unknown error")) } defer { zcashlc_string_free(addressCStr) } guard let address = String(validatingUTF8: addressCStr) else { throw ZcashError.rustGetNextAvailableAddressInvalidAddress } return UnifiedAddress(validatedEncoding: address, networkType: networkType) } func getMemo(txId: Data, outputIndex: UInt16) async throws -> Memo? { guard txId.count == 32 else { throw ZcashError.rustGetMemoInvalidTxIdLength } var contiguousMemoBytes = ContiguousArray(MemoBytes.empty().bytes) var success = false globalDBLock.lock() contiguousMemoBytes.withUnsafeMutableBufferPointer { memoBytePtr in success = zcashlc_get_memo(dbData.0, dbData.1, txId.bytes, outputIndex, memoBytePtr.baseAddress, networkType.networkId) } globalDBLock.unlock() guard success else { return nil } return (try? MemoBytes(contiguousBytes: contiguousMemoBytes)).flatMap { try? $0.intoMemo() } } func getTransparentBalance(account: Int32) async throws -> Int64 { guard account >= 0 else { throw ZcashError.rustGetTransparentBalanceNegativeAccount(Int(account)) } globalDBLock.lock() let balance = zcashlc_get_total_transparent_balance_for_account( dbData.0, dbData.1, networkType.networkId, account ) globalDBLock.unlock() guard balance >= 0 else { throw ZcashError.rustGetTransparentBalance( Int(account), lastErrorMessage(fallback: "Error getting Total Transparent balance from account \(account)") ) } return balance } func getVerifiedTransparentBalance(account: Int32) async throws -> Int64 { guard account >= 0 else { throw ZcashError.rustGetVerifiedTransparentBalanceNegativeAccount(Int(account)) } globalDBLock.lock() let balance = zcashlc_get_verified_transparent_balance_for_account( dbData.0, dbData.1, networkType.networkId, account, minimumConfirmations ) globalDBLock.unlock() guard balance >= 0 else { throw ZcashError.rustGetVerifiedTransparentBalance( Int(account), lastErrorMessage(fallback: "Error getting verified transparent balance from account \(account)") ) } return balance } func initDataDb(seed: [UInt8]?) async throws -> DbInitResult { globalDBLock.lock() let initResult = zcashlc_init_data_database(dbData.0, dbData.1, seed, UInt(seed?.count ?? 0), networkType.networkId) globalDBLock.unlock() switch initResult { case 0: // ok return DbInitResult.success case 1: return DbInitResult.seedRequired case 2: return DbInitResult.seedNotRelevant default: throw ZcashError.rustInitDataDb(lastErrorMessage(fallback: "`initDataDb` failed with unknown error")) } } func initBlockMetadataDb() async throws { globalDBLock.lock() let result = zcashlc_init_block_metadata_db(fsBlockDbRoot.0, fsBlockDbRoot.1) globalDBLock.unlock() guard result else { throw ZcashError.rustInitBlockMetadataDb(lastErrorMessage(fallback: "`initBlockMetadataDb` failed with unknown error")) } } func writeBlocksMetadata(blocks: [ZcashCompactBlock]) async throws { var ffiBlockMetaVec: [FFIBlockMeta] = [] for block in blocks { let meta = block.meta let hashPtr = UnsafeMutablePointer.allocate(capacity: meta.hash.count) let contiguousHashBytes = ContiguousArray(meta.hash.bytes) let result: Void? = contiguousHashBytes.withContiguousStorageIfAvailable { hashBytesPtr in // swiftlint:disable:next force_unwrapping hashPtr.initialize(from: hashBytesPtr.baseAddress!, count: hashBytesPtr.count) } guard result != nil else { defer { hashPtr.deallocate() ffiBlockMetaVec.deallocateElements() } throw ZcashError.rustWriteBlocksMetadataAllocationProblem } ffiBlockMetaVec.append( FFIBlockMeta( height: UInt32(block.height), block_hash_ptr: hashPtr, block_hash_ptr_len: UInt(contiguousHashBytes.count), block_time: meta.time, sapling_outputs_count: meta.saplingOutputs, orchard_actions_count: meta.orchardOutputs ) ) } var contiguousFFIBlocks = ContiguousArray(ffiBlockMetaVec) let len = UInt(contiguousFFIBlocks.count) let fsBlocks = UnsafeMutablePointer.allocate(capacity: 1) defer { ffiBlockMetaVec.deallocateElements() } try contiguousFFIBlocks.withContiguousMutableStorageIfAvailable { ptr in var meta = FFIBlocksMeta() meta.ptr = ptr.baseAddress meta.len = len fsBlocks.initialize(to: meta) globalDBLock.lock() let res = zcashlc_write_block_metadata(fsBlockDbRoot.0, fsBlockDbRoot.1, fsBlocks) globalDBLock.unlock() guard res else { throw ZcashError.rustWriteBlocksMetadata(lastErrorMessage(fallback: "`writeBlocksMetadata` failed with unknown error")) } } } func latestCachedBlockHeight() async throws -> BlockHeight { globalDBLock.lock() let height = zcashlc_latest_cached_block_height(fsBlockDbRoot.0, fsBlockDbRoot.1) globalDBLock.unlock() if height >= 0 { return BlockHeight(height) } else if height == -1 { return BlockHeight.empty() } else { throw ZcashError.rustLatestCachedBlockHeight(lastErrorMessage(fallback: "`latestCachedBlockHeight` failed with unknown error")) } } func listTransparentReceivers(account: Int32) async throws -> [TransparentAddress] { globalDBLock.lock() let encodedKeysPtr = zcashlc_list_transparent_receivers( dbData.0, dbData.1, account, networkType.networkId ) globalDBLock.unlock() guard let encodedKeysPtr else { throw ZcashError.rustListTransparentReceivers(lastErrorMessage(fallback: "`listTransparentReceivers` failed with unknown error")) } defer { zcashlc_free_keys(encodedKeysPtr) } var addresses: [TransparentAddress] = [] for i in (0 ..< Int(encodedKeysPtr.pointee.len)) { let key = encodedKeysPtr.pointee.ptr.advanced(by: i).pointee guard let taddrStr = String(validatingUTF8: key.encoding) else { throw ZcashError.rustListTransparentReceiversInvalidAddress } addresses.append( TransparentAddress(validatedEncoding: taddrStr) ) } return addresses } func putUnspentTransparentOutput( txid: [UInt8], index: Int, script: [UInt8], value: Int64, height: BlockHeight ) async throws { globalDBLock.lock() let result = zcashlc_put_utxo( dbData.0, dbData.1, txid, UInt(txid.count), Int32(index), script, UInt(script.count), value, Int32(height), networkType.networkId ) globalDBLock.unlock() guard result else { throw ZcashError.rustPutUnspentTransparentOutput(lastErrorMessage(fallback: "`putUnspentTransparentOutput` failed with unknown error")) } } func rewindToHeight(height: Int32) async throws { globalDBLock.lock() let result = zcashlc_rewind_to_height(dbData.0, dbData.1, height, networkType.networkId) globalDBLock.unlock() guard result else { throw ZcashError.rustRewindToHeight(height, lastErrorMessage(fallback: "`rewindToHeight` failed with unknown error")) } } func rewindCacheToHeight(height: Int32) async throws { globalDBLock.lock() let result = zcashlc_rewind_fs_block_cache_to_height(fsBlockDbRoot.0, fsBlockDbRoot.1, height) globalDBLock.unlock() guard result else { throw ZcashError.rustRewindCacheToHeight(lastErrorMessage(fallback: "`rewindCacheToHeight` failed with unknown error")) } } func putSaplingSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws { var ffiSubtreeRootsVec: [FfiSubtreeRoot] = [] for root in roots { let hashPtr = UnsafeMutablePointer.allocate(capacity: root.rootHash.count) let contiguousHashBytes = ContiguousArray(root.rootHash.bytes) let result: Void? = contiguousHashBytes.withContiguousStorageIfAvailable { hashBytesPtr in // swiftlint:disable:next force_unwrapping hashPtr.initialize(from: hashBytesPtr.baseAddress!, count: hashBytesPtr.count) } guard result != nil else { defer { hashPtr.deallocate() ffiSubtreeRootsVec.deallocateElements() } throw ZcashError.rustPutSaplingSubtreeRootsAllocationProblem } ffiSubtreeRootsVec.append( FfiSubtreeRoot( root_hash_ptr: hashPtr, root_hash_ptr_len: UInt(contiguousHashBytes.count), completing_block_height: UInt32(root.completingBlockHeight) ) ) } var contiguousFfiRoots = ContiguousArray(ffiSubtreeRootsVec) let len = UInt(contiguousFfiRoots.count) let rootsPtr = UnsafeMutablePointer.allocate(capacity: 1) defer { ffiSubtreeRootsVec.deallocateElements() rootsPtr.deallocate() } try contiguousFfiRoots.withContiguousMutableStorageIfAvailable { ptr in var roots = FfiSubtreeRoots() roots.ptr = ptr.baseAddress roots.len = len rootsPtr.initialize(to: roots) globalDBLock.lock() let res = zcashlc_put_sapling_subtree_roots(dbData.0, dbData.1, startIndex, rootsPtr, networkType.networkId) globalDBLock.unlock() guard res else { throw ZcashError.rustPutSaplingSubtreeRoots(lastErrorMessage(fallback: "`putSaplingSubtreeRoots` failed with unknown error")) } } } func putOrchardSubtreeRoots(startIndex: UInt64, roots: [SubtreeRoot]) async throws { var ffiSubtreeRootsVec: [FfiSubtreeRoot] = [] for root in roots { let hashPtr = UnsafeMutablePointer.allocate(capacity: root.rootHash.count) let contiguousHashBytes = ContiguousArray(root.rootHash.bytes) let result: Void? = contiguousHashBytes.withContiguousStorageIfAvailable { hashBytesPtr in // swiftlint:disable:next force_unwrapping hashPtr.initialize(from: hashBytesPtr.baseAddress!, count: hashBytesPtr.count) } guard result != nil else { defer { hashPtr.deallocate() ffiSubtreeRootsVec.deallocateElements() } throw ZcashError.rustPutOrchardSubtreeRootsAllocationProblem } ffiSubtreeRootsVec.append( FfiSubtreeRoot( root_hash_ptr: hashPtr, root_hash_ptr_len: UInt(contiguousHashBytes.count), completing_block_height: UInt32(root.completingBlockHeight) ) ) } var contiguousFfiRoots = ContiguousArray(ffiSubtreeRootsVec) let len = UInt(contiguousFfiRoots.count) let rootsPtr = UnsafeMutablePointer.allocate(capacity: 1) defer { ffiSubtreeRootsVec.deallocateElements() rootsPtr.deallocate() } try contiguousFfiRoots.withContiguousMutableStorageIfAvailable { ptr in var roots = FfiSubtreeRoots() roots.ptr = ptr.baseAddress roots.len = len rootsPtr.initialize(to: roots) globalDBLock.lock() let res = zcashlc_put_orchard_subtree_roots(dbData.0, dbData.1, startIndex, rootsPtr, networkType.networkId) globalDBLock.unlock() guard res else { throw ZcashError.rustPutOrchardSubtreeRoots(lastErrorMessage(fallback: "`putOrchardSubtreeRoots` failed with unknown error")) } } } func updateChainTip(height: Int32) async throws { globalDBLock.lock() let result = zcashlc_update_chain_tip(dbData.0, dbData.1, height, networkType.networkId) globalDBLock.unlock() guard result else { throw ZcashError.rustUpdateChainTip(lastErrorMessage(fallback: "`updateChainTip` failed with unknown error")) } } func fullyScannedHeight() async throws -> BlockHeight? { globalDBLock.lock() let height = zcashlc_fully_scanned_height(dbData.0, dbData.1, networkType.networkId) globalDBLock.unlock() if height >= 0 { return BlockHeight(height) } else if height == -1 { return nil } else { throw ZcashError.rustFullyScannedHeight(lastErrorMessage(fallback: "`fullyScannedHeight` failed with unknown error")) } } func maxScannedHeight() async throws -> BlockHeight? { globalDBLock.lock() let height = zcashlc_max_scanned_height(dbData.0, dbData.1, networkType.networkId) globalDBLock.unlock() if height >= 0 { return BlockHeight(height) } else if height == -1 { return nil } else { throw ZcashError.rustMaxScannedHeight(lastErrorMessage(fallback: "`maxScannedHeight` failed with unknown error")) } } func getWalletSummary() async throws -> WalletSummary? { globalDBLock.lock() let summaryPtr = zcashlc_get_wallet_summary(dbData.0, dbData.1, networkType.networkId, minimumConfirmations) globalDBLock.unlock() guard let summaryPtr else { throw ZcashError.rustGetWalletSummary(lastErrorMessage(fallback: "`getWalletSummary` failed with unknown error")) } defer { zcashlc_free_wallet_summary(summaryPtr) } if summaryPtr.pointee.fully_scanned_height < 0 { return nil } var accountBalances: [UInt32: AccountBalance] = [:] for i in (0 ..< Int(summaryPtr.pointee.account_balances_len)) { let accountBalance = summaryPtr.pointee.account_balances.advanced(by: i).pointee accountBalances[accountBalance.account_id] = accountBalance.toAccountBalance() } return WalletSummary( accountBalances: accountBalances, chainTipHeight: BlockHeight(summaryPtr.pointee.chain_tip_height), fullyScannedHeight: BlockHeight(summaryPtr.pointee.fully_scanned_height), scanProgress: summaryPtr.pointee.scan_progress?.pointee.toScanProgress(), nextSaplingSubtreeIndex: UInt32(summaryPtr.pointee.next_sapling_subtree_index), nextOrchardSubtreeIndex: UInt32(summaryPtr.pointee.next_orchard_subtree_index) ) } func suggestScanRanges() async throws -> [ScanRange] { globalDBLock.lock() let scanRangesPtr = zcashlc_suggest_scan_ranges(dbData.0, dbData.1, networkType.networkId) globalDBLock.unlock() guard let scanRangesPtr else { throw ZcashError.rustSuggestScanRanges(lastErrorMessage(fallback: "`suggestScanRanges` failed with unknown error")) } defer { zcashlc_free_scan_ranges(scanRangesPtr) } var scanRanges: [ScanRange] = [] for i in (0 ..< Int(scanRangesPtr.pointee.len)) { let scanRange = scanRangesPtr.pointee.ptr.advanced(by: i).pointee scanRanges.append( ScanRange( range: Range(uncheckedBounds: ( BlockHeight(scanRange.start), BlockHeight(scanRange.end) )), priority: ScanRange.Priority(scanRange.priority) ) ) } return scanRanges } 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, fromStateBytes, UInt(fromStateBytes.count), limit, networkType.networkId) globalDBLock.unlock() guard let summaryPtr else { throw ZcashError.rustScanBlocks(lastErrorMessage(fallback: "`scanBlocks` failed with unknown error")) } defer { zcashlc_free_scan_summary(summaryPtr) } return ScanSummary( scannedRange: Range(uncheckedBounds: ( BlockHeight(summaryPtr.pointee.scanned_start), BlockHeight(summaryPtr.pointee.scanned_end) )), spentSaplingNoteCount: summaryPtr.pointee.spent_sapling_note_count, receivedSaplingNoteCount: summaryPtr.pointee.received_sapling_note_count ) } func proposeShielding( account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String? ) async throws -> FfiProposal? { globalDBLock.lock() let proposal = zcashlc_propose_shielding( dbData.0, dbData.1, account, memo?.bytes, UInt64(shieldingThreshold.amount), transparentReceiver.map { [CChar]($0.utf8CString) }, networkType.networkId, minimumConfirmations, useZIP317Fees ) globalDBLock.unlock() guard let proposal else { throw ZcashError.rustShieldFunds(lastErrorMessage(fallback: "`proposeShielding` failed with unknown error")) } defer { zcashlc_free_boxed_slice(proposal) } return try FfiProposal(contiguousBytes: Data( bytes: proposal.pointee.ptr, count: Int(proposal.pointee.len) )) } func createProposedTransactions( proposal: FfiProposal, usk: UnifiedSpendingKey ) async throws -> [Data] { let proposalBytes = try proposal.serializedData(partial: false).bytes globalDBLock.lock() let txIdsPtr = proposalBytes.withUnsafeBufferPointer { proposalPtr in usk.bytes.withUnsafeBufferPointer { uskPtr in zcashlc_create_proposed_transactions( dbData.0, dbData.1, proposalPtr.baseAddress, UInt(proposalBytes.count), uskPtr.baseAddress, UInt(usk.bytes.count), spendParamsPath.0, spendParamsPath.1, outputParamsPath.0, outputParamsPath.1, networkType.networkId ) } } globalDBLock.unlock() guard let txIdsPtr else { throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error")) } defer { zcashlc_free_txids(txIdsPtr) } var txIds: [Data] = [] for i in (0 ..< Int(txIdsPtr.pointee.len)) { let txId = FfiTxId(tuple: txIdsPtr.pointee.ptr.advanced(by: i).pointee) txIds.append(Data(txId.array)) } return txIds } nonisolated func consensusBranchIdFor(height: Int32) throws -> Int32 { let branchId = zcashlc_branch_id_for_height(height, networkType.networkId) guard branchId != -1 else { throw ZcashError.rustNoConsensusBranchId(height) } return branchId } } private extension ZcashRustBackend { static func enableTracing() { zcashlc_init_on_load(false) } } private extension ZcashRustBackend { nonisolated func lastErrorMessage(fallback: String) -> String { let errorLen = zcashlc_last_error_length() defer { zcashlc_clear_last_error() } if errorLen > 0 { let error = UnsafeMutablePointer.allocate(capacity: Int(errorLen)) defer { error.deallocate() } zcashlc_error_message_utf8(error, errorLen) if let errorMessage = String(validatingUTF8: error) { return errorMessage } else { return fallback } } else { return fallback } } } private extension URL { func osStr() -> (String, UInt) { let path = self.absoluteString return (path, UInt(path.lengthOfBytes(using: .utf8))) } /// use when the rust ffi needs to make filesystem operations func osPathStr() -> (String, UInt) { let path = self.path return (path, UInt(path.lengthOfBytes(using: .utf8))) } } extension String { /** Checks whether this string contains null bytes before it's real ending */ func containsCStringNullBytesBeforeStringEnding() -> Bool { self.utf8CString.firstIndex(of: 0) != (self.utf8CString.count - 1) } func isDbNotEmptyErrorMessage() -> Bool { return contains("is not empty") } } extension FFIBinaryKey { /// converts an [`FFIBinaryKey`] into a [`UnifiedSpendingKey`] /// - Note: This does not check that the converted value actually holds a valid USK func unsafeToUnifiedSpendingKey(network: NetworkType) -> UnifiedSpendingKey { .init( network: network, bytes: self.encoding.toByteArray( length: Int(self.encoding_len) ), account: self.account_id ) } } extension UnsafeMutablePointer where Pointee == UInt8 { /// copies the bytes pointed on func toByteArray(length: Int) -> [UInt8] { var bytes: [UInt8] = [] for index in 0 ..< length { bytes.append(self.advanced(by: index).pointee) } return bytes } } extension Array where Element == FFIBlockMeta { func deallocateElements() { self.forEach { element in element.block_hash_ptr.deallocate() } } } extension Array where Element == FfiSubtreeRoot { func deallocateElements() { self.forEach { element in element.root_hash_ptr.deallocate() } } } extension FfiBalance { /// Converts an [`FfiBalance`] into a [`PoolBalance`]. func toPoolBalance() -> PoolBalance { .init( spendableValue: Zatoshi(self.spendable_value), changePendingConfirmation: Zatoshi(self.change_pending_confirmation), valuePendingSpendability: Zatoshi(self.value_pending_spendability) ) } } extension FfiAccountBalance { /// Converts an [`FfiAccountBalance`] into a [`AccountBalance`]. func toAccountBalance() -> AccountBalance { .init( saplingBalance: self.sapling_balance.toPoolBalance(), orchardBalance: self.orchard_balance.toPoolBalance(), unshielded: Zatoshi(self.unshielded) ) } } extension FfiScanProgress { /// Converts an [`FfiScanProgress`] into a [`ScanProgress`]. func toScanProgress() -> ScanProgress { .init( numerator: self.numerator, denominator: self.denominator ) } } // 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] { withUnsafeBytes(of: self.tuple) { buf in [UInt8](buf) } } }