From 98a955dc72fc6a962f9b8719ef6e173e601cba64 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 19 Dec 2023 05:00:47 +0000 Subject: [PATCH] Migrate to in-progress version of FFI backend 0.5.0 Includes: - Exposed `WalletSummary`. - Exposed transaction proposals. --- .../xcshareddata/swiftpm/Package.resolved | 3 +- Package.resolved | 3 +- Package.swift | 3 +- .../Block/Actions/ScanAction.swift | 2 +- .../Block/CompactBlockProcessor.swift | 9 +- .../SaplingParametersHandler.swift | 10 +- .../Error/ZcashError.swift | 26 +- .../Error/ZcashErrorCode.swift | 8 +- .../Metrics/SDKMetrics.swift | 9 +- ...ScanProgress.swift => WalletSummary.swift} | 26 +- .../Service/GRPC/ProtoBuf/proposal.pb.swift | 548 ++++++++++++++++++ .../GRPC/ProtoBuf/proto/proposal.proto | 91 +++ .../Rust/ZcashRustBackend.swift | 223 ++++--- .../Rust/ZcashRustBackendWelding.swift | 63 +- .../Synchronizer/SDKSynchronizer.swift | 10 +- .../WalletTransactionEncoder.swift | 24 +- .../AutoMockable.generated.swift | 263 ++++----- Tests/TestUtils/Sourcery/generateMocks.sh | 2 +- Tests/TestUtils/Stubs.swift | 14 +- 19 files changed, 992 insertions(+), 345 deletions(-) rename Sources/ZcashLightClientKit/Model/{ScanProgress.swift => WalletSummary.swift} (54%) create mode 100644 Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift create mode 100644 Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto 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 e67e0b6f..242be58c 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "514dcd7e6fbfa252bf36d9f00d6b98f465a70704", - "version" : "0.4.1" + "revision" : "e1f4107d1f1b212081c44555000c19db351a55ff" } } ], diff --git a/Package.resolved b/Package.resolved index 9962853c..90e7239d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -104,8 +104,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "9bc5877ef6302e877922f79ebead52e50bce94fd", - "version" : "0.4.0" + "revision" : "e1f4107d1f1b212081c44555000c19db351a55ff" } } ], diff --git a/Package.swift b/Package.swift index 27f4c78f..4acbf004 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ 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"), - .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", from: "0.4.1") + .package(url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "e1f4107d1f1b212081c44555000c19db351a55ff") ], targets: [ .target( @@ -28,6 +28,7 @@ let package = Package( ], exclude: [ "Modules/Service/GRPC/ProtoBuf/proto/compact_formats.proto", + "Modules/Service/GRPC/ProtoBuf/proto/proposal.proto", "Modules/Service/GRPC/ProtoBuf/proto/service.proto", "Error/Sourcery/" ], diff --git a/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift index a70afbc9..b5381c2e 100644 --- a/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift +++ b/Sources/ZcashLightClientKit/Block/Actions/ScanAction.swift @@ -60,7 +60,7 @@ extension ScanAction: Action { await self?.latestBlocksDataProvider.updateScannedData() // report scan progress only if it's available - if let scanProgress = try? await self?.rustBackend.getScanProgress() { + if let scanProgress = try? await self?.rustBackend.getWalletSummary()?.scanProgress { let progress = try scanProgress.progress() self?.logger.debug("progress: \(progress)") await didUpdate(.syncProgress(progress)) diff --git a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift index e0cfc5f8..aed80206 100644 --- a/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift +++ b/Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift @@ -651,13 +651,8 @@ extension CompactBlockProcessor { await send(event: .finished(lastScannedHeight)) await context.update(state: .finished) - let verifiedBalance = Zatoshi((try? await rustBackend.getVerifiedBalance(account: 0)) ?? 0) - let totalBalance = Zatoshi((try? await rustBackend.getBalance(account: 0)) ?? 0) - let shieldedBalance = WalletBalance( - verified: verifiedBalance, - total: totalBalance - ) - await metrics.logCBPOverviewReport(logger, shieldedBalance: shieldedBalance) + let walletSummary = try? await rustBackend.getWalletSummary() + await metrics.logCBPOverviewReport(logger, walletSummary: walletSummary) // If new blocks were mined during previous sync run the sync process again if newerBlocksWereMinedDuringSync { diff --git a/Sources/ZcashLightClientKit/Block/SaplingParameters/SaplingParametersHandler.swift b/Sources/ZcashLightClientKit/Block/SaplingParameters/SaplingParametersHandler.swift index 09746b6d..4538a225 100644 --- a/Sources/ZcashLightClientKit/Block/SaplingParameters/SaplingParametersHandler.swift +++ b/Sources/ZcashLightClientKit/Block/SaplingParameters/SaplingParametersHandler.swift @@ -1,6 +1,6 @@ // -// HandleSaplingParametersIfNeeded.swift -// +// SaplingParametersHandler.swift +// // // Created by Lukáš Korba on 23.11.2022. // @@ -28,11 +28,13 @@ extension SaplingParametersHandlerImpl: SaplingParametersHandler { try Task.checkCancellation() do { - let totalShieldedBalance = try await rustBackend.getBalance(account: Int32(0)) + let totalSaplingBalance = + try await rustBackend.getWalletSummary()?.accountBalances[0]?.saplingBalance.total().amount + ?? 0 let totalTransparentBalance = try await rustBackend.getTransparentBalance(account: Int32(0)) // Download Sapling parameters only if sapling funds are detected. - guard totalShieldedBalance > 0 || totalTransparentBalance > 0 else { return } + guard totalSaplingBalance > 0 || totalTransparentBalance > 0 else { return } } catch { // if sapling balance can't be detected of we fail to obtain the balance // for some reason we shall not proceed to download the parameters and diff --git a/Sources/ZcashLightClientKit/Error/ZcashError.swift b/Sources/ZcashLightClientKit/Error/ZcashError.swift index 1bb52cc6..f2fce14c 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashError.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashError.swift @@ -121,11 +121,6 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0003 case rustDecryptAndStoreTransaction(_ rustError: String) - /// Error from rust layer when calling ZcashRustBackend.getBalance - /// - `account` is account passed to ZcashRustBackend.getBalance. - /// - `rustError` contains error generated by the rust layer. - /// ZRUST0004 - case rustGetBalance(_ account: Int, _ rustError: String) /// Error from rust layer when calling ZcashRustBackend.getCurrentAddress /// - `rustError` contains error generated by the rust layer. /// ZRUST0005 @@ -153,11 +148,6 @@ public enum ZcashError: Equatable, Error { /// - `rustError` contains error generated by the rust layer. /// ZRUST0011 case rustGetTransparentBalance(_ account: Int, _ rustError: String) - /// Error from rust layer when calling ZcashRustBackend.getVerifiedBalance - /// - `account` is account passed to ZcashRustBackend.getVerifiedBalance. - /// - `rustError` contains error generated by the rust layer. - /// ZRUST0012 - case rustGetVerifiedBalance(_ account: Int, _ rustError: String) /// account parameter is lower than 0 when calling ZcashRustBackend.getVerifiedTransparentBalance /// - `account` is account passed to ZcashRustBackend.getVerifiedTransparentBalance. /// ZRUST0013 @@ -297,10 +287,6 @@ public enum ZcashError: Equatable, Error { /// Invalid transaction ID length when calling ZcashRustBackend.getMemo. txId must be 32 bytes. /// ZRUST0050 case rustGetMemoInvalidTxIdLength - /// Error from rust layer when calling ZcashRustBackend.getScanProgress - /// - `rustError` contains error generated by the rust layer. - /// ZRUST0051 - case rustGetScanProgress(_ rustError: String) /// Error from rust layer when calling ZcashRustBackend.fullyScannedHeight /// - `rustError` contains error generated by the rust layer. /// ZRUST0052 @@ -317,6 +303,10 @@ public enum ZcashError: Equatable, Error { /// - `progress` value reported /// ZRUST0055 case rustScanProgressOutOfRange(_ progress: String) + /// Error from rust layer when calling ZcashRustBackend.getWalletSummary + /// - `rustError` contains error generated by the rust layer. + /// ZRUST0056 + case rustGetWalletSummary(_ rustError: String) /// SQLite query failed when fetching all accounts from the database. /// - `sqliteError` is error produced by SQLite library. /// ZADAO0001 @@ -614,7 +604,6 @@ public enum ZcashError: Equatable, Error { case .rustCreateAccount: return "Error from rust layer when calling ZcashRustBackend.createAccount" case .rustCreateToAddress: return "Error from rust layer when calling ZcashRustBackend.createToAddress" case .rustDecryptAndStoreTransaction: return "Error from rust layer when calling ZcashRustBackend.decryptAndStoreTransaction" - case .rustGetBalance: return "Error from rust layer when calling ZcashRustBackend.getBalance" case .rustGetCurrentAddress: return "Error from rust layer when calling ZcashRustBackend.getCurrentAddress" case .rustGetCurrentAddressInvalidAddress: return "Unified address generated by rust layer is invalid when calling ZcashRustBackend.getCurrentAddress" case .rustGetNearestRewindHeight: return "Error from rust layer when calling ZcashRustBackend.getNearestRewindHeight" @@ -622,7 +611,6 @@ public enum ZcashError: Equatable, Error { case .rustGetNextAvailableAddressInvalidAddress: return "Unified address generated by rust layer is invalid when calling ZcashRustBackend.getNextAvailableAddress" case .rustGetTransparentBalanceNegativeAccount: return "account parameter is lower than 0 when calling ZcashRustBackend.getTransparentBalance" case .rustGetTransparentBalance: return "Error from rust layer when calling ZcashRustBackend.getTransparentBalance" - case .rustGetVerifiedBalance: return "Error from rust layer when calling ZcashRustBackend.getVerifiedBalance" case .rustGetVerifiedTransparentBalanceNegativeAccount: return "account parameter is lower than 0 when calling ZcashRustBackend.getVerifiedTransparentBalance" case .rustGetVerifiedTransparentBalance: return "Error from rust layer when calling ZcashRustBackend.getVerifiedTransparentBalance" case .rustInitDataDb: return "Error from rust layer when calling ZcashRustBackend.initDataDb" @@ -660,11 +648,11 @@ public enum ZcashError: Equatable, Error { case .rustUpdateChainTip: return "Error from rust layer when calling ZcashRustBackend.updateChainTip" case .rustSuggestScanRanges: return "Error from rust layer when calling ZcashRustBackend.suggestScanRanges" case .rustGetMemoInvalidTxIdLength: return "Invalid transaction ID length when calling ZcashRustBackend.getMemo. txId must be 32 bytes." - case .rustGetScanProgress: return "Error from rust layer when calling ZcashRustBackend.getScanProgress" case .rustFullyScannedHeight: return "Error from rust layer when calling ZcashRustBackend.fullyScannedHeight" case .rustMaxScannedHeight: return "Error from rust layer when calling ZcashRustBackend.maxScannedHeight" case .rustLatestCachedBlockHeight: return "Error from rust layer when calling ZcashRustBackend.latestCachedBlockHeight" case .rustScanProgressOutOfRange: return "Rust layer's call ZcashRustBackend.getScanProgress returned values that after computation are outside of allowed range 0-100%." + case .rustGetWalletSummary: return "Error from rust layer when calling ZcashRustBackend.getWalletSummary" 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." @@ -785,7 +773,6 @@ public enum ZcashError: Equatable, Error { case .rustCreateAccount: return .rustCreateAccount case .rustCreateToAddress: return .rustCreateToAddress case .rustDecryptAndStoreTransaction: return .rustDecryptAndStoreTransaction - case .rustGetBalance: return .rustGetBalance case .rustGetCurrentAddress: return .rustGetCurrentAddress case .rustGetCurrentAddressInvalidAddress: return .rustGetCurrentAddressInvalidAddress case .rustGetNearestRewindHeight: return .rustGetNearestRewindHeight @@ -793,7 +780,6 @@ public enum ZcashError: Equatable, Error { case .rustGetNextAvailableAddressInvalidAddress: return .rustGetNextAvailableAddressInvalidAddress case .rustGetTransparentBalanceNegativeAccount: return .rustGetTransparentBalanceNegativeAccount case .rustGetTransparentBalance: return .rustGetTransparentBalance - case .rustGetVerifiedBalance: return .rustGetVerifiedBalance case .rustGetVerifiedTransparentBalanceNegativeAccount: return .rustGetVerifiedTransparentBalanceNegativeAccount case .rustGetVerifiedTransparentBalance: return .rustGetVerifiedTransparentBalance case .rustInitDataDb: return .rustInitDataDb @@ -831,11 +817,11 @@ public enum ZcashError: Equatable, Error { case .rustUpdateChainTip: return .rustUpdateChainTip case .rustSuggestScanRanges: return .rustSuggestScanRanges case .rustGetMemoInvalidTxIdLength: return .rustGetMemoInvalidTxIdLength - case .rustGetScanProgress: return .rustGetScanProgress case .rustFullyScannedHeight: return .rustFullyScannedHeight case .rustMaxScannedHeight: return .rustMaxScannedHeight case .rustLatestCachedBlockHeight: return .rustLatestCachedBlockHeight case .rustScanProgressOutOfRange: return .rustScanProgressOutOfRange + case .rustGetWalletSummary: return .rustGetWalletSummary 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 a093770b..1047244a 100644 --- a/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift +++ b/Sources/ZcashLightClientKit/Error/ZcashErrorCode.swift @@ -71,8 +71,6 @@ public enum ZcashErrorCode: String { case rustCreateToAddress = "ZRUST0002" /// Error from rust layer when calling ZcashRustBackend.decryptAndStoreTransaction case rustDecryptAndStoreTransaction = "ZRUST0003" - /// Error from rust layer when calling ZcashRustBackend.getBalance - case rustGetBalance = "ZRUST0004" /// Error from rust layer when calling ZcashRustBackend.getCurrentAddress case rustGetCurrentAddress = "ZRUST0005" /// Unified address generated by rust layer is invalid when calling ZcashRustBackend.getCurrentAddress @@ -87,8 +85,6 @@ public enum ZcashErrorCode: String { case rustGetTransparentBalanceNegativeAccount = "ZRUST0010" /// Error from rust layer when calling ZcashRustBackend.getTransparentBalance case rustGetTransparentBalance = "ZRUST0011" - /// Error from rust layer when calling ZcashRustBackend.getVerifiedBalance - case rustGetVerifiedBalance = "ZRUST0012" /// account parameter is lower than 0 when calling ZcashRustBackend.getVerifiedTransparentBalance case rustGetVerifiedTransparentBalanceNegativeAccount = "ZRUST0013" /// Error from rust layer when calling ZcashRustBackend.getVerifiedTransparentBalance @@ -163,8 +159,6 @@ public enum ZcashErrorCode: String { case rustSuggestScanRanges = "ZRUST0049" /// Invalid transaction ID length when calling ZcashRustBackend.getMemo. txId must be 32 bytes. case rustGetMemoInvalidTxIdLength = "ZRUST0050" - /// Error from rust layer when calling ZcashRustBackend.getScanProgress - case rustGetScanProgress = "ZRUST0051" /// Error from rust layer when calling ZcashRustBackend.fullyScannedHeight case rustFullyScannedHeight = "ZRUST0052" /// Error from rust layer when calling ZcashRustBackend.maxScannedHeight @@ -173,6 +167,8 @@ public enum ZcashErrorCode: String { case rustLatestCachedBlockHeight = "ZRUST0054" /// Rust layer's call ZcashRustBackend.getScanProgress returned values that after computation are outside of allowed range 0-100%. case rustScanProgressOutOfRange = "ZRUST0055" + /// Error from rust layer when calling ZcashRustBackend.getWalletSummary + case rustGetWalletSummary = "ZRUST0056" /// 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/Metrics/SDKMetrics.swift b/Sources/ZcashLightClientKit/Metrics/SDKMetrics.swift index 1b64be21..28176331 100644 --- a/Sources/ZcashLightClientKit/Metrics/SDKMetrics.swift +++ b/Sources/ZcashLightClientKit/Metrics/SDKMetrics.swift @@ -12,7 +12,7 @@ protocol SDKMetrics { func actionStart(_ action: CBPState) func actionDetail(_ detail: String, `for` action: CBPState) func actionStop() - func logCBPOverviewReport(_ logger: Logger, shieldedBalance: WalletBalance) async + func logCBPOverviewReport(_ logger: Logger, walletSummary: WalletSummary?) async } final class SDKMetricsImpl: SDKMetrics { @@ -102,15 +102,16 @@ final class SDKMetricsImpl: SDKMetrics { } // swiftlint:disable string_concatenation - func logCBPOverviewReport(_ logger: Logger, shieldedBalance: WalletBalance) async { + func logCBPOverviewReport(_ logger: Logger, walletSummary: WalletSummary?) async { actionStop() + let accountBalance = walletSummary?.accountBalances[0] logger.sync( """ SYNC (\(syncs)) REPORT finished in: \(Date().timeIntervalSince1970 - cbpStartTime) - verified balance: \(shieldedBalance.verified.amount) - total balance: \(shieldedBalance.total.amount) + verified balance: \(accountBalance?.saplingBalance.spendableValue.amount ?? 0) + total balance: \(accountBalance?.saplingBalance.total().amount ?? 0) """ ) diff --git a/Sources/ZcashLightClientKit/Model/ScanProgress.swift b/Sources/ZcashLightClientKit/Model/WalletSummary.swift similarity index 54% rename from Sources/ZcashLightClientKit/Model/ScanProgress.swift rename to Sources/ZcashLightClientKit/Model/WalletSummary.swift index 5a897207..baefd5a6 100644 --- a/Sources/ZcashLightClientKit/Model/ScanProgress.swift +++ b/Sources/ZcashLightClientKit/Model/WalletSummary.swift @@ -1,12 +1,27 @@ // -// ScanProgress.swift -// +// WalletSummary.swift +// // // Created by Jack Grigg on 06/09/2023. // import Foundation +struct PoolBalance: Equatable { + let spendableValue: Zatoshi + let changePendingConfirmation: Zatoshi + let valuePendingSpendability: Zatoshi + + func total() -> Zatoshi { + self.spendableValue + self.changePendingConfirmation + self.valuePendingSpendability + } +} + +struct AccountBalance: Equatable { + let saplingBalance: PoolBalance + let unshielded: Zatoshi +} + struct ScanProgress: Equatable { let numerator: UInt64 let denominator: UInt64 @@ -27,3 +42,10 @@ struct ScanProgress: Equatable { return value } } + +struct WalletSummary: Equatable { + let accountBalances: [UInt32: AccountBalance] + let chainTipHeight: BlockHeight + let fullyScannedHeight: BlockHeight + let scanProgress: ScanProgress? +} diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift new file mode 100644 index 00000000..3a7e3701 --- /dev/null +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proposal.pb.swift @@ -0,0 +1,548 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: proto/proposal.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright (c) 2023 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum FfiValuePool: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Protobuf requires that enums have a zero discriminant as the default + /// value. However, we need to require that a known value pool is selected, + /// and we do not want to fall back to any default, so sending the + /// PoolNotSpecified value will be treated as an error. + case poolNotSpecified // = 0 + + /// The transparent value pool (P2SH is not distinguished from P2PKH) + case transparent // = 1 + + /// The Sapling value pool + case sapling // = 2 + + /// The Orchard value pool + case orchard // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .poolNotSpecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .poolNotSpecified + case 1: self = .transparent + case 2: self = .sapling + case 3: self = .orchard + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .poolNotSpecified: return 0 + case .transparent: return 1 + case .sapling: return 2 + case .orchard: return 3 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension FfiValuePool: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [FfiValuePool] = [ + .poolNotSpecified, + .transparent, + .sapling, + .orchard, + ] +} + +#endif // swift(>=4.2) + +/// The fee rule used in constructing a Proposal +enum FfiFeeRule: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Protobuf requires that enums have a zero discriminant as the default + /// value. However, we need to require that a known fee rule is selected, + /// and we do not want to fall back to any default, so sending the + /// FeeRuleNotSpecified value will be treated as an error. + case notSpecified // = 0 + + /// 10000 ZAT + case preZip313 // = 1 + + /// 1000 ZAT + case zip313 // = 2 + + /// MAX(10000, 5000 * logical_actions) ZAT + case zip317 // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .notSpecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .notSpecified + case 1: self = .preZip313 + case 2: self = .zip313 + case 3: self = .zip317 + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .notSpecified: return 0 + case .preZip313: return 1 + case .zip313: return 2 + case .zip317: return 3 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension FfiFeeRule: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [FfiFeeRule] = [ + .notSpecified, + .preZip313, + .zip313, + .zip317, + ] +} + +#endif // swift(>=4.2) + +/// A data structure that describes the inputs to be consumed and outputs to +/// be produced in a proposed transaction. +struct FfiProposal { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var protoVersion: UInt32 = 0 + + /// ZIP 321 serialized transaction request + var transactionRequest: String = String() + + /// The anchor height to be used in creating the transaction, if any. + /// Setting the anchor height to zero will disallow the use of any shielded + /// inputs. + var anchorHeight: UInt32 = 0 + + /// The inputs to be used in creating the transaction. + var inputs: [FfiProposedInput] = [] + + /// The total value, fee value, and change outputs of the proposed + /// transaction + var balance: FfiTransactionBalance { + get {return _balance ?? FfiTransactionBalance()} + set {_balance = newValue} + } + /// Returns true if `balance` has been explicitly set. + var hasBalance: Bool {return self._balance != nil} + /// Clears the value of `balance`. Subsequent reads from it will return its default value. + mutating func clearBalance() {self._balance = nil} + + /// The fee rule used in constructing this proposal + var feeRule: FfiFeeRule = .notSpecified + + /// The target height for which the proposal was constructed + /// + /// The chain must contain at least this many blocks in order for the proposal to + /// be executed. + var minTargetHeight: UInt32 = 0 + + /// A flag indicating whether the proposal is for a shielding transaction, + /// used for determining which OVK to select for wallet-internal outputs. + var isShielding: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _balance: FfiTransactionBalance? = nil +} + +/// The unique identifier and value for each proposed input. +struct FfiProposedInput { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var txid: Data = Data() + + var valuePool: FfiValuePool = .poolNotSpecified + + var index: UInt32 = 0 + + var value: UInt64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The proposed change outputs and fee value. +struct FfiTransactionBalance { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var proposedChange: [FfiChangeValue] = [] + + var feeRequired: UInt64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A proposed change output. If the transparent value pool is selected, +/// the `memo` field must be null. +struct FfiChangeValue { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: UInt64 = 0 + + var valuePool: FfiValuePool = .poolNotSpecified + + var memo: FfiMemoBytes { + get {return _memo ?? FfiMemoBytes()} + set {_memo = newValue} + } + /// Returns true if `memo` has been explicitly set. + var hasMemo: Bool {return self._memo != nil} + /// Clears the value of `memo`. Subsequent reads from it will return its default value. + mutating func clearMemo() {self._memo = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _memo: FfiMemoBytes? = nil +} + +/// An object wrapper for memo bytes, to facilitate representing the +/// `change_memo == None` case. +struct FfiMemoBytes { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var value: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension FfiValuePool: @unchecked Sendable {} +extension FfiFeeRule: @unchecked Sendable {} +extension FfiProposal: @unchecked Sendable {} +extension FfiProposedInput: @unchecked Sendable {} +extension FfiTransactionBalance: @unchecked Sendable {} +extension FfiChangeValue: @unchecked Sendable {} +extension FfiMemoBytes: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "cash.z.wallet.sdk.ffi" + +extension FfiValuePool: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "PoolNotSpecified"), + 1: .same(proto: "Transparent"), + 2: .same(proto: "Sapling"), + 3: .same(proto: "Orchard"), + ] +} + +extension FfiFeeRule: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "FeeRuleNotSpecified"), + 1: .same(proto: "PreZip313"), + 2: .same(proto: "Zip313"), + 3: .same(proto: "Zip317"), + ] +} + +extension FfiProposal: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Proposal" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "protoVersion"), + 2: .same(proto: "transactionRequest"), + 3: .same(proto: "anchorHeight"), + 4: .same(proto: "inputs"), + 5: .same(proto: "balance"), + 6: .same(proto: "feeRule"), + 7: .same(proto: "minTargetHeight"), + 8: .same(proto: "isShielding"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.protoVersion) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.transactionRequest) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.anchorHeight) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.inputs) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._balance) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.feeRule) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.minTargetHeight) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.isShielding) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.protoVersion != 0 { + try visitor.visitSingularUInt32Field(value: self.protoVersion, fieldNumber: 1) + } + if !self.transactionRequest.isEmpty { + try visitor.visitSingularStringField(value: self.transactionRequest, fieldNumber: 2) + } + if self.anchorHeight != 0 { + try visitor.visitSingularUInt32Field(value: self.anchorHeight, fieldNumber: 3) + } + if !self.inputs.isEmpty { + try visitor.visitRepeatedMessageField(value: self.inputs, fieldNumber: 4) + } + try { if let v = self._balance { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + if self.feeRule != .notSpecified { + try visitor.visitSingularEnumField(value: self.feeRule, fieldNumber: 6) + } + if self.minTargetHeight != 0 { + try visitor.visitSingularUInt32Field(value: self.minTargetHeight, fieldNumber: 7) + } + if self.isShielding != false { + try visitor.visitSingularBoolField(value: self.isShielding, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FfiProposal, rhs: FfiProposal) -> Bool { + if lhs.protoVersion != rhs.protoVersion {return false} + if lhs.transactionRequest != rhs.transactionRequest {return false} + if lhs.anchorHeight != rhs.anchorHeight {return false} + if lhs.inputs != rhs.inputs {return false} + if lhs._balance != rhs._balance {return false} + if lhs.feeRule != rhs.feeRule {return false} + if lhs.minTargetHeight != rhs.minTargetHeight {return false} + if lhs.isShielding != rhs.isShielding {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension FfiProposedInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ProposedInput" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "txid"), + 2: .same(proto: "valuePool"), + 3: .same(proto: "index"), + 4: .same(proto: "value"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.txid) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.valuePool) }() + case 3: try { try decoder.decodeSingularUInt32Field(value: &self.index) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.txid.isEmpty { + try visitor.visitSingularBytesField(value: self.txid, fieldNumber: 1) + } + if self.valuePool != .poolNotSpecified { + try visitor.visitSingularEnumField(value: self.valuePool, fieldNumber: 2) + } + if self.index != 0 { + try visitor.visitSingularUInt32Field(value: self.index, fieldNumber: 3) + } + if self.value != 0 { + try visitor.visitSingularUInt64Field(value: self.value, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FfiProposedInput, rhs: FfiProposedInput) -> Bool { + if lhs.txid != rhs.txid {return false} + if lhs.valuePool != rhs.valuePool {return false} + if lhs.index != rhs.index {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension FfiTransactionBalance: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TransactionBalance" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "proposedChange"), + 2: .same(proto: "feeRequired"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.proposedChange) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.feeRequired) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.proposedChange.isEmpty { + try visitor.visitRepeatedMessageField(value: self.proposedChange, fieldNumber: 1) + } + if self.feeRequired != 0 { + try visitor.visitSingularUInt64Field(value: self.feeRequired, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FfiTransactionBalance, rhs: FfiTransactionBalance) -> Bool { + if lhs.proposedChange != rhs.proposedChange {return false} + if lhs.feeRequired != rhs.feeRequired {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension FfiChangeValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ChangeValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + 2: .same(proto: "valuePool"), + 3: .same(proto: "memo"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.value) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.valuePool) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._memo) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.value != 0 { + try visitor.visitSingularUInt64Field(value: self.value, fieldNumber: 1) + } + if self.valuePool != .poolNotSpecified { + try visitor.visitSingularEnumField(value: self.valuePool, fieldNumber: 2) + } + try { if let v = self._memo { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FfiChangeValue, rhs: FfiChangeValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.valuePool != rhs.valuePool {return false} + if lhs._memo != rhs._memo {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension FfiMemoBytes: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MemoBytes" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.value.isEmpty { + try visitor.visitSingularBytesField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: FfiMemoBytes, rhs: FfiMemoBytes) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto new file mode 100644 index 00000000..3ebee76f --- /dev/null +++ b/Sources/ZcashLightClientKit/Modules/Service/GRPC/ProtoBuf/proto/proposal.proto @@ -0,0 +1,91 @@ +// Copyright (c) 2023 The Zcash developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php . + +syntax = "proto3"; +package cash.z.wallet.sdk.ffi; +option swift_prefix = "Ffi"; + +// A data structure that describes the inputs to be consumed and outputs to +// be produced in a proposed transaction. +message Proposal { + uint32 protoVersion = 1; + // ZIP 321 serialized transaction request + string transactionRequest = 2; + // The anchor height to be used in creating the transaction, if any. + // Setting the anchor height to zero will disallow the use of any shielded + // inputs. + uint32 anchorHeight = 3; + // The inputs to be used in creating the transaction. + repeated ProposedInput inputs = 4; + // The total value, fee value, and change outputs of the proposed + // transaction + TransactionBalance balance = 5; + // The fee rule used in constructing this proposal + FeeRule feeRule = 6; + // The target height for which the proposal was constructed + // + // The chain must contain at least this many blocks in order for the proposal to + // be executed. + uint32 minTargetHeight = 7; + // A flag indicating whether the proposal is for a shielding transaction, + // used for determining which OVK to select for wallet-internal outputs. + bool isShielding = 8; +} + +enum ValuePool { + // Protobuf requires that enums have a zero discriminant as the default + // value. However, we need to require that a known value pool is selected, + // and we do not want to fall back to any default, so sending the + // PoolNotSpecified value will be treated as an error. + PoolNotSpecified = 0; + // The transparent value pool (P2SH is not distinguished from P2PKH) + Transparent = 1; + // The Sapling value pool + Sapling = 2; + // The Orchard value pool + Orchard = 3; +} + +// The unique identifier and value for each proposed input. +message ProposedInput { + bytes txid = 1; + ValuePool valuePool = 2; + uint32 index = 3; + uint64 value = 4; +} + +// The fee rule used in constructing a Proposal +enum FeeRule { + // Protobuf requires that enums have a zero discriminant as the default + // value. However, we need to require that a known fee rule is selected, + // and we do not want to fall back to any default, so sending the + // FeeRuleNotSpecified value will be treated as an error. + FeeRuleNotSpecified = 0; + // 10000 ZAT + PreZip313 = 1; + // 1000 ZAT + Zip313 = 2; + // MAX(10000, 5000 * logical_actions) ZAT + Zip317 = 3; +} + +// The proposed change outputs and fee value. +message TransactionBalance { + repeated ChangeValue proposedChange = 1; + uint64 feeRequired = 2; +} + +// A proposed change output. If the transparent value pool is selected, +// the `memo` field must be null. +message ChangeValue { + uint64 value = 1; + ValuePool valuePool = 2; + MemoBytes memo = 3; +} + +// An object wrapper for memo bytes, to facilitate representing the +// `change_memo == None` case. +message MemoBytes { + bytes value = 1; +} diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 6c5083c1..9b50445b 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -80,45 +80,36 @@ actor ZcashRustBackend: ZcashRustBackendWelding { return ffiBinaryKeyPtr.pointee.unsafeToUnifiedSpendingKey(network: networkType) } - func createToAddress( - usk: UnifiedSpendingKey, + func proposeTransfer( + account: Int32, to address: String, value: Int64, memo: MemoBytes? - ) async throws -> Data { - var contiguousTxIdBytes = ContiguousArray([UInt8](repeating: 0x0, count: 32)) - + ) async throws -> FfiProposal { globalDBLock.lock() - let success = contiguousTxIdBytes.withUnsafeMutableBufferPointer { txIdBytePtr in - usk.bytes.withUnsafeBufferPointer { uskPtr in - zcashlc_create_to_address( - dbData.0, - dbData.1, - uskPtr.baseAddress, - UInt(usk.bytes.count), - [CChar](address.utf8CString), - value, - memo?.bytes, - spendParamsPath.0, - spendParamsPath.1, - outputParamsPath.0, - outputParamsPath.1, - networkType.networkId, - minimumConfirmations, - useZIP317Fees, - txIdBytePtr.baseAddress - ) - } - } + let proposal = zcashlc_propose_transfer( + dbData.0, + dbData.1, + account, + [CChar](address.utf8CString), + value, + memo?.bytes, + networkType.networkId, + minimumConfirmations, + useZIP317Fees + ) globalDBLock.unlock() - guard success else { - throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error")) + guard let proposal else { + throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`proposeTransfer` failed with unknown error")) } - return contiguousTxIdBytes.withUnsafeBufferPointer { txIdBytePtr in - Data(txIdBytePtr) - } + 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 { @@ -138,18 +129,6 @@ actor ZcashRustBackend: ZcashRustBackendWelding { } } - func getBalance(account: Int32) async throws -> Int64 { - globalDBLock.lock() - let balance = zcashlc_get_balance(dbData.0, dbData.1, account, networkType.networkId) - globalDBLock.unlock() - - guard balance >= 0 else { - throw ZcashError.rustGetBalance(Int(account), lastErrorMessage(fallback: "Error getting total balance from account \(account)")) - } - - return balance - } - func getCurrentAddress(account: Int32) async throws -> UnifiedAddress { globalDBLock.lock() let addressCStr = zcashlc_get_current_address( @@ -256,27 +235,6 @@ actor ZcashRustBackend: ZcashRustBackendWelding { return balance } - func getVerifiedBalance(account: Int32) async throws -> Int64 { - globalDBLock.lock() - let balance = zcashlc_get_verified_balance( - dbData.0, - dbData.1, - account, - networkType.networkId, - minimumConfirmations - ) - globalDBLock.unlock() - - guard balance >= 0 else { - throw ZcashError.rustGetVerifiedBalance( - Int(account), - lastErrorMessage(fallback: "Error getting verified balance from account \(account)") - ) - } - - return balance - } - func getVerifiedTransparentBalance(account: Int32) async throws -> Int64 { guard account >= 0 else { throw ZcashError.rustGetVerifiedTransparentBalanceNegativeAccount(Int(account)) @@ -576,21 +534,34 @@ actor ZcashRustBackend: ZcashRustBackendWelding { } } - func getScanProgress() async throws -> ScanProgress? { + func getWalletSummary() async throws -> WalletSummary? { globalDBLock.lock() - let result = zcashlc_get_scan_progress(dbData.0, dbData.1, networkType.networkId) + let summaryPtr = zcashlc_get_wallet_summary(dbData.0, dbData.1, networkType.networkId, minimumConfirmations) globalDBLock.unlock() - if result.denominator == 0 { - switch result.numerator { - case 0: - return nil - default: - throw ZcashError.rustGetScanProgress(lastErrorMessage(fallback: "`getScanProgress` failed with unknown error")) - } - } else { - return ScanProgress(numerator: result.numerator, denominator: result.denominator) + 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() + ) } func suggestScanRanges() async throws -> [ScanRange] { @@ -633,38 +604,69 @@ actor ZcashRustBackend: ZcashRustBackendWelding { } } - func shieldFunds( - usk: UnifiedSpendingKey, + func proposeShielding( + account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi + ) async throws -> FfiProposal { + globalDBLock.lock() + let proposal = zcashlc_propose_shielding( + dbData.0, + dbData.1, + account, + memo?.bytes, + UInt64(shieldingThreshold.amount), + 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 createProposedTransaction( + proposal: FfiProposal, + usk: UnifiedSpendingKey ) async throws -> Data { var contiguousTxIdBytes = ContiguousArray([UInt8](repeating: 0x0, count: 32)) + let proposalBytes = try proposal.serializedData(partial: false).bytes + globalDBLock.lock() let success = contiguousTxIdBytes.withUnsafeMutableBufferPointer { txIdBytePtr in - usk.bytes.withUnsafeBufferPointer { uskBuffer in - zcashlc_shield_funds( - dbData.0, - dbData.1, - uskBuffer.baseAddress, - UInt(usk.bytes.count), - memo?.bytes, - UInt64(shieldingThreshold.amount), - spendParamsPath.0, - spendParamsPath.1, - outputParamsPath.0, - outputParamsPath.1, - networkType.networkId, - minimumConfirmations, - useZIP317Fees, - txIdBytePtr.baseAddress - ) + proposalBytes.withUnsafeBufferPointer { proposalPtr in + usk.bytes.withUnsafeBufferPointer { uskPtr in + zcashlc_create_proposed_transaction( + 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, + txIdBytePtr.baseAddress + ) + } } } globalDBLock.unlock() guard success else { - throw ZcashError.rustShieldFunds(lastErrorMessage(fallback: "`shieldFunds` failed with unknown error")) + throw ZcashError.rustCreateToAddress(lastErrorMessage(fallback: "`createToAddress` failed with unknown error")) } return contiguousTxIdBytes.withUnsafeBufferPointer { txIdBytePtr in @@ -778,3 +780,34 @@ extension Array where Element == FfiSubtreeRoot { } } } + +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(), + unshielded: Zatoshi(self.unshielded) + ) + } +} + +extension FfiScanProgress { + /// Converts an [`FfiScanProgress`] into a [`ScanProgress`]. + func toScanProgress() -> ScanProgress { + .init( + numerator: self.numerator, + denominator: self.denominator + ) + } +} diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index a8ed2350..884fa9e7 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -41,30 +41,12 @@ protocol ZcashRustBackendWelding { /// - Throws: `rustCreateAccount`. func createAccount(seed: [UInt8], treeState: TreeState, recoverUntil: UInt32?) async throws -> UnifiedSpendingKey - /// Creates a transaction to the given address from the given account - /// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent. - /// - Parameter to: recipient address - /// - Parameter value: transaction amount in Zatoshi - /// - Parameter memo: the `MemoBytes` for this transaction. pass `nil` when sending to transparent receivers - /// - Throws: `rustCreateToAddress`. - func createToAddress( - usk: UnifiedSpendingKey, - to address: String, - value: Int64, - memo: MemoBytes? - ) async throws -> Data - /// 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. /// - Throws: `rustDecryptAndStoreTransaction`. func decryptAndStoreTransaction(txBytes: [UInt8], minedHeight: Int32) async throws - /// Get the (unverified) balance from the given account. - /// - parameter account: index of the given account - /// - Throws: `rustGetBalance`. - func getBalance(account: Int32) async throws -> Int64 - /// Returns the most-recently-generated unified payment address for the specified account. /// - parameter account: index of the given account /// - Throws: @@ -120,11 +102,6 @@ protocol ZcashRustBackendWelding { /// - `rustListTransparentReceiversInvalidAddress` if transarent received generated by rust is invalid. func listTransparentReceivers(account: Int32) async throws -> [TransparentAddress] - /// Get the verified balance from the given account - /// - parameter account: index of the given account - /// - Throws: `rustGetVerifiedBalance` when rust layer throws error. - func getVerifiedBalance(account: Int32) async throws -> Int64 - /// Get the verified cached transparent balance for the given account /// - parameter account: account index to query the balance for. /// - Throws: @@ -167,8 +144,8 @@ protocol ZcashRustBackendWelding { /// height due to the fact that out-of-order scanning can leave gaps. func maxScannedHeight() async throws -> BlockHeight? - /// Returns the scan progress derived from the current wallet state. - func getScanProgress() async throws -> ScanProgress? + /// Returns the account balances and sync status of the wallet. + func getWalletSummary() async throws -> WalletSummary? /// Returns a list of suggested scan ranges based upon the current wallet state. /// @@ -215,14 +192,42 @@ protocol ZcashRustBackendWelding { height: BlockHeight ) async throws - /// Creates a transaction to shield all found UTXOs in data db for the account the provided `UnifiedSpendingKey` has spend authority for. - /// - Parameter usk: `UnifiedSpendingKey` that spend transparent funds and where the funds will be shielded to. + /// Select transaction inputs, compute fees, and construct a proposal for a transaction + /// that can then be authorized and made ready for submission to the network with + /// `createProposedTransaction`. + /// + /// - parameter account: index of the given account + /// - Parameter to: recipient address + /// - Parameter value: transaction amount in Zatoshi + /// - Parameter memo: the `MemoBytes` for this transaction. pass `nil` when sending to transparent receivers + /// - Throws: `rustCreateToAddress`. + func proposeTransfer( + account: Int32, + to address: String, + value: Int64, + memo: MemoBytes? + ) async throws -> FfiProposal + + /// Constructs a transaction proposal to shield all found UTXOs in data db for the given account, + /// that can then be authorized and made ready for submission to the network with + /// `createProposedTransaction`. + /// + /// - parameter account: index of the given account /// - Parameter memo: the `Memo` for this transaction /// - Throws: `rustShieldFunds` if rust layer returns error. - func shieldFunds( - usk: UnifiedSpendingKey, + func proposeShielding( + account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi + ) async throws -> FfiProposal + + /// Creates a transaction from the given proposal. + /// - Parameter proposal: the transaction proposal. + /// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent. + /// - Throws: `rustCreateToAddress`. + func createProposedTransaction( + proposal: FfiProposal, + usk: UnifiedSpendingKey ) async throws -> Data /// Gets the consensus branch id for the given height diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index dd18b8c0..0bfb2c5f 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -414,15 +414,13 @@ public class SDKSynchronizer: Synchronizer { } public func getShieldedBalance(accountIndex: Int = 0) async throws -> Zatoshi { - let balance = try await initializer.rustBackend.getBalance(account: Int32(accountIndex)) - - return Zatoshi(balance) + try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)]? + .saplingBalance.total() ?? Zatoshi.zero } public func getShieldedVerifiedBalance(accountIndex: Int = 0) async throws -> Zatoshi { - let balance = try await initializer.rustBackend.getVerifiedBalance(account: Int32(accountIndex)) - - return Zatoshi(balance) + try await initializer.rustBackend.getWalletSummary()?.accountBalances[UInt32(accountIndex)]? + .saplingBalance.spendableValue ?? Zatoshi.zero } public func getUnifiedAddress(accountIndex: Int) async throws -> UnifiedAddress { diff --git a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index 80a2edfa..f24927bd 100644 --- a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -85,13 +85,19 @@ class WalletTransactionEncoder: TransactionEncoder { throw ZcashError.walletTransEncoderCreateTransactionMissingSaplingParams } - let txId = try await rustBackend.createToAddress( - usk: spendingKey, + // TODO: Expose the proposal in a way that enables querying its fee. + let proposal = try await rustBackend.proposeTransfer( + account: Int32(spendingKey.account), to: address, value: zatoshi.amount, memo: memoBytes ) + let txId = try await rustBackend.createProposedTransaction( + proposal: proposal, + usk: spendingKey + ) + return txId } @@ -121,13 +127,19 @@ class WalletTransactionEncoder: TransactionEncoder { guard ensureParams(spend: self.spendParamsURL, output: self.outputParamsURL) else { throw ZcashError.walletTransEncoderShieldFundsMissingSaplingParams } - - let txId = try await rustBackend.shieldFunds( - usk: spendingKey, + + // TODO: Expose the proposal in a way that enables querying its fee. + let proposal = try await rustBackend.proposeShielding( + account: Int32(spendingKey.account), memo: memo, shieldingThreshold: shieldingThreshold ) - + + let txId = try await rustBackend.createProposedTransaction( + proposal: proposal, + usk: spendingKey + ) + return txId } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index 14deef6f..aea07cbd 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.0.3 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import Combine @testable import ZcashLightClientKit @@ -1191,17 +1191,17 @@ class SDKMetricsMock: SDKMetrics { // MARK: - logCBPOverviewReport - var logCBPOverviewReportShieldedBalanceCallsCount = 0 - var logCBPOverviewReportShieldedBalanceCalled: Bool { - return logCBPOverviewReportShieldedBalanceCallsCount > 0 + var logCBPOverviewReportWalletSummaryCallsCount = 0 + var logCBPOverviewReportWalletSummaryCalled: Bool { + return logCBPOverviewReportWalletSummaryCallsCount > 0 } - var logCBPOverviewReportShieldedBalanceReceivedArguments: (logger: Logger, shieldedBalance: WalletBalance)? - var logCBPOverviewReportShieldedBalanceClosure: ((Logger, WalletBalance) async -> Void)? + var logCBPOverviewReportWalletSummaryReceivedArguments: (logger: Logger, walletSummary: WalletSummary?)? + var logCBPOverviewReportWalletSummaryClosure: ((Logger, WalletSummary?) async -> Void)? - func logCBPOverviewReport(_ logger: Logger, shieldedBalance: WalletBalance) async { - logCBPOverviewReportShieldedBalanceCallsCount += 1 - logCBPOverviewReportShieldedBalanceReceivedArguments = (logger: logger, shieldedBalance: shieldedBalance) - await logCBPOverviewReportShieldedBalanceClosure!(logger, shieldedBalance) + func logCBPOverviewReport(_ logger: Logger, walletSummary: WalletSummary?) async { + logCBPOverviewReportWalletSummaryCallsCount += 1 + logCBPOverviewReportWalletSummaryReceivedArguments = (logger: logger, walletSummary: walletSummary) + await logCBPOverviewReportWalletSummaryClosure!(logger, walletSummary) } } @@ -2179,39 +2179,6 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } - // MARK: - createToAddress - - var createToAddressUskToValueMemoThrowableError: Error? - func setCreateToAddressUskToValueMemoThrowableError(_ param: Error?) async { - createToAddressUskToValueMemoThrowableError = param - } - var createToAddressUskToValueMemoCallsCount = 0 - var createToAddressUskToValueMemoCalled: Bool { - return createToAddressUskToValueMemoCallsCount > 0 - } - var createToAddressUskToValueMemoReceivedArguments: (usk: UnifiedSpendingKey, address: String, value: Int64, memo: MemoBytes?)? - var createToAddressUskToValueMemoReturnValue: Data! - func setCreateToAddressUskToValueMemoReturnValue(_ param: Data) async { - createToAddressUskToValueMemoReturnValue = param - } - var createToAddressUskToValueMemoClosure: ((UnifiedSpendingKey, String, Int64, MemoBytes?) async throws -> Data)? - func setCreateToAddressUskToValueMemoClosure(_ param: ((UnifiedSpendingKey, String, Int64, MemoBytes?) async throws -> Data)?) async { - createToAddressUskToValueMemoClosure = param - } - - func createToAddress(usk: UnifiedSpendingKey, to address: String, value: Int64, memo: MemoBytes?) async throws -> Data { - if let error = createToAddressUskToValueMemoThrowableError { - throw error - } - createToAddressUskToValueMemoCallsCount += 1 - createToAddressUskToValueMemoReceivedArguments = (usk: usk, address: address, value: value, memo: memo) - if let closure = createToAddressUskToValueMemoClosure { - return try await closure(usk, address, value, memo) - } else { - return createToAddressUskToValueMemoReturnValue - } - } - // MARK: - decryptAndStoreTransaction var decryptAndStoreTransactionTxBytesMinedHeightThrowableError: Error? @@ -2237,39 +2204,6 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { try await decryptAndStoreTransactionTxBytesMinedHeightClosure!(txBytes, minedHeight) } - // MARK: - getBalance - - var getBalanceAccountThrowableError: Error? - func setGetBalanceAccountThrowableError(_ param: Error?) async { - getBalanceAccountThrowableError = param - } - var getBalanceAccountCallsCount = 0 - var getBalanceAccountCalled: Bool { - return getBalanceAccountCallsCount > 0 - } - var getBalanceAccountReceivedAccount: Int32? - var getBalanceAccountReturnValue: Int64! - func setGetBalanceAccountReturnValue(_ param: Int64) async { - getBalanceAccountReturnValue = param - } - var getBalanceAccountClosure: ((Int32) async throws -> Int64)? - func setGetBalanceAccountClosure(_ param: ((Int32) async throws -> Int64)?) async { - getBalanceAccountClosure = param - } - - func getBalance(account: Int32) async throws -> Int64 { - if let error = getBalanceAccountThrowableError { - throw error - } - getBalanceAccountCallsCount += 1 - getBalanceAccountReceivedAccount = account - if let closure = getBalanceAccountClosure { - return try await closure(account) - } else { - return getBalanceAccountReturnValue - } - } - // MARK: - getCurrentAddress var getCurrentAddressAccountThrowableError: Error? @@ -2501,39 +2435,6 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } - // MARK: - getVerifiedBalance - - var getVerifiedBalanceAccountThrowableError: Error? - func setGetVerifiedBalanceAccountThrowableError(_ param: Error?) async { - getVerifiedBalanceAccountThrowableError = param - } - var getVerifiedBalanceAccountCallsCount = 0 - var getVerifiedBalanceAccountCalled: Bool { - return getVerifiedBalanceAccountCallsCount > 0 - } - var getVerifiedBalanceAccountReceivedAccount: Int32? - var getVerifiedBalanceAccountReturnValue: Int64! - func setGetVerifiedBalanceAccountReturnValue(_ param: Int64) async { - getVerifiedBalanceAccountReturnValue = param - } - var getVerifiedBalanceAccountClosure: ((Int32) async throws -> Int64)? - func setGetVerifiedBalanceAccountClosure(_ param: ((Int32) async throws -> Int64)?) async { - getVerifiedBalanceAccountClosure = param - } - - func getVerifiedBalance(account: Int32) async throws -> Int64 { - if let error = getVerifiedBalanceAccountThrowableError { - throw error - } - getVerifiedBalanceAccountCallsCount += 1 - getVerifiedBalanceAccountReceivedAccount = account - if let closure = getVerifiedBalanceAccountClosure { - return try await closure(account) - } else { - return getVerifiedBalanceAccountReturnValue - } - } - // MARK: - getVerifiedTransparentBalance var getVerifiedTransparentBalanceAccountThrowableError: Error? @@ -2729,34 +2630,34 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { } } - // MARK: - getScanProgress + // MARK: - getWalletSummary - var getScanProgressThrowableError: Error? - func setGetScanProgressThrowableError(_ param: Error?) async { - getScanProgressThrowableError = param + var getWalletSummaryThrowableError: Error? + func setGetWalletSummaryThrowableError(_ param: Error?) async { + getWalletSummaryThrowableError = param } - var getScanProgressCallsCount = 0 - var getScanProgressCalled: Bool { - return getScanProgressCallsCount > 0 + var getWalletSummaryCallsCount = 0 + var getWalletSummaryCalled: Bool { + return getWalletSummaryCallsCount > 0 } - var getScanProgressReturnValue: ScanProgress? - func setGetScanProgressReturnValue(_ param: ScanProgress?) async { - getScanProgressReturnValue = param + var getWalletSummaryReturnValue: WalletSummary? + func setGetWalletSummaryReturnValue(_ param: WalletSummary?) async { + getWalletSummaryReturnValue = param } - var getScanProgressClosure: (() async throws -> ScanProgress?)? - func setGetScanProgressClosure(_ param: (() async throws -> ScanProgress?)?) async { - getScanProgressClosure = param + var getWalletSummaryClosure: (() async throws -> WalletSummary?)? + func setGetWalletSummaryClosure(_ param: (() async throws -> WalletSummary?)?) async { + getWalletSummaryClosure = param } - func getScanProgress() async throws -> ScanProgress? { - if let error = getScanProgressThrowableError { + func getWalletSummary() async throws -> WalletSummary? { + if let error = getWalletSummaryThrowableError { throw error } - getScanProgressCallsCount += 1 - if let closure = getScanProgressClosure { + getWalletSummaryCallsCount += 1 + if let closure = getWalletSummaryClosure { return try await closure() } else { - return getScanProgressReturnValue + return getWalletSummaryReturnValue } } @@ -2841,36 +2742,102 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { try await putUnspentTransparentOutputTxidIndexScriptValueHeightClosure!(txid, index, script, value, height) } - // MARK: - shieldFunds + // MARK: - proposeTransfer - var shieldFundsUskMemoShieldingThresholdThrowableError: Error? - func setShieldFundsUskMemoShieldingThresholdThrowableError(_ param: Error?) async { - shieldFundsUskMemoShieldingThresholdThrowableError = param + var proposeTransferAccountToValueMemoThrowableError: Error? + func setProposeTransferAccountToValueMemoThrowableError(_ param: Error?) async { + proposeTransferAccountToValueMemoThrowableError = param } - var shieldFundsUskMemoShieldingThresholdCallsCount = 0 - var shieldFundsUskMemoShieldingThresholdCalled: Bool { - return shieldFundsUskMemoShieldingThresholdCallsCount > 0 + var proposeTransferAccountToValueMemoCallsCount = 0 + var proposeTransferAccountToValueMemoCalled: Bool { + return proposeTransferAccountToValueMemoCallsCount > 0 } - var shieldFundsUskMemoShieldingThresholdReceivedArguments: (usk: UnifiedSpendingKey, memo: MemoBytes?, shieldingThreshold: Zatoshi)? - var shieldFundsUskMemoShieldingThresholdReturnValue: Data! - func setShieldFundsUskMemoShieldingThresholdReturnValue(_ param: Data) async { - shieldFundsUskMemoShieldingThresholdReturnValue = param + var proposeTransferAccountToValueMemoReceivedArguments: (account: Int32, address: String, value: Int64, memo: MemoBytes?)? + var proposeTransferAccountToValueMemoReturnValue: FfiProposal! + func setProposeTransferAccountToValueMemoReturnValue(_ param: FfiProposal) async { + proposeTransferAccountToValueMemoReturnValue = param } - var shieldFundsUskMemoShieldingThresholdClosure: ((UnifiedSpendingKey, MemoBytes?, Zatoshi) async throws -> Data)? - func setShieldFundsUskMemoShieldingThresholdClosure(_ param: ((UnifiedSpendingKey, MemoBytes?, Zatoshi) async throws -> Data)?) async { - shieldFundsUskMemoShieldingThresholdClosure = param + var proposeTransferAccountToValueMemoClosure: ((Int32, String, Int64, MemoBytes?) async throws -> FfiProposal)? + func setProposeTransferAccountToValueMemoClosure(_ param: ((Int32, String, Int64, MemoBytes?) async throws -> FfiProposal)?) async { + proposeTransferAccountToValueMemoClosure = param } - func shieldFunds(usk: UnifiedSpendingKey, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> Data { - if let error = shieldFundsUskMemoShieldingThresholdThrowableError { + func proposeTransfer(account: Int32, to address: String, value: Int64, memo: MemoBytes?) async throws -> FfiProposal { + if let error = proposeTransferAccountToValueMemoThrowableError { throw error } - shieldFundsUskMemoShieldingThresholdCallsCount += 1 - shieldFundsUskMemoShieldingThresholdReceivedArguments = (usk: usk, memo: memo, shieldingThreshold: shieldingThreshold) - if let closure = shieldFundsUskMemoShieldingThresholdClosure { - return try await closure(usk, memo, shieldingThreshold) + proposeTransferAccountToValueMemoCallsCount += 1 + proposeTransferAccountToValueMemoReceivedArguments = (account: account, address: address, value: value, memo: memo) + if let closure = proposeTransferAccountToValueMemoClosure { + return try await closure(account, address, value, memo) } else { - return shieldFundsUskMemoShieldingThresholdReturnValue + return proposeTransferAccountToValueMemoReturnValue + } + } + + // MARK: - proposeShielding + + var proposeShieldingAccountMemoShieldingThresholdThrowableError: Error? + func setProposeShieldingAccountMemoShieldingThresholdThrowableError(_ param: Error?) async { + proposeShieldingAccountMemoShieldingThresholdThrowableError = param + } + var proposeShieldingAccountMemoShieldingThresholdCallsCount = 0 + var proposeShieldingAccountMemoShieldingThresholdCalled: Bool { + return proposeShieldingAccountMemoShieldingThresholdCallsCount > 0 + } + var proposeShieldingAccountMemoShieldingThresholdReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi)? + var proposeShieldingAccountMemoShieldingThresholdReturnValue: FfiProposal! + func setProposeShieldingAccountMemoShieldingThresholdReturnValue(_ param: FfiProposal) async { + proposeShieldingAccountMemoShieldingThresholdReturnValue = param + } + var proposeShieldingAccountMemoShieldingThresholdClosure: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)? + func setProposeShieldingAccountMemoShieldingThresholdClosure(_ param: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?) async { + proposeShieldingAccountMemoShieldingThresholdClosure = param + } + + func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> FfiProposal { + if let error = proposeShieldingAccountMemoShieldingThresholdThrowableError { + throw error + } + proposeShieldingAccountMemoShieldingThresholdCallsCount += 1 + proposeShieldingAccountMemoShieldingThresholdReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold) + if let closure = proposeShieldingAccountMemoShieldingThresholdClosure { + return try await closure(account, memo, shieldingThreshold) + } else { + return proposeShieldingAccountMemoShieldingThresholdReturnValue + } + } + + // MARK: - createProposedTransaction + + var createProposedTransactionProposalUskThrowableError: Error? + func setCreateProposedTransactionProposalUskThrowableError(_ param: Error?) async { + createProposedTransactionProposalUskThrowableError = param + } + var createProposedTransactionProposalUskCallsCount = 0 + var createProposedTransactionProposalUskCalled: Bool { + return createProposedTransactionProposalUskCallsCount > 0 + } + var createProposedTransactionProposalUskReceivedArguments: (proposal: FfiProposal, usk: UnifiedSpendingKey)? + var createProposedTransactionProposalUskReturnValue: Data! + func setCreateProposedTransactionProposalUskReturnValue(_ param: Data) async { + createProposedTransactionProposalUskReturnValue = param + } + var createProposedTransactionProposalUskClosure: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)? + func setCreateProposedTransactionProposalUskClosure(_ param: ((FfiProposal, UnifiedSpendingKey) async throws -> Data)?) async { + createProposedTransactionProposalUskClosure = param + } + + func createProposedTransaction(proposal: FfiProposal, usk: UnifiedSpendingKey) async throws -> Data { + if let error = createProposedTransactionProposalUskThrowableError { + throw error + } + createProposedTransactionProposalUskCallsCount += 1 + createProposedTransactionProposalUskReceivedArguments = (proposal: proposal, usk: usk) + if let closure = createProposedTransactionProposalUskClosure { + return try await closure(proposal, usk) + } else { + return createProposedTransactionProposalUskReturnValue } } diff --git a/Tests/TestUtils/Sourcery/generateMocks.sh b/Tests/TestUtils/Sourcery/generateMocks.sh index 503f7af7..0ba65de2 100755 --- a/Tests/TestUtils/Sourcery/generateMocks.sh +++ b/Tests/TestUtils/Sourcery/generateMocks.sh @@ -3,7 +3,7 @@ scriptDir=${0:a:h} cd "${scriptDir}" -sourcery_version=2.0.3 +sourcery_version=2.1.2 if which sourcery >/dev/null; then if [[ $(sourcery --version) != $sourcery_version ]]; then diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index 93cc36cf..c0d2b1d3 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -75,7 +75,6 @@ class RustBackendMockHelper { await rustBackendMock.setInitBlockMetadataDbClosure() { } await rustBackendMock.setWriteBlocksMetadataBlocksClosure() { _ in } await rustBackendMock.setGetTransparentBalanceAccountReturnValue(0) - await rustBackendMock.setGetVerifiedBalanceAccountReturnValue(0) await rustBackendMock.setListTransparentReceiversAccountReturnValue([]) await rustBackendMock.setGetCurrentAddressAccountThrowableError(ZcashError.rustGetCurrentAddress("mocked error")) await rustBackendMock.setGetNextAvailableAddressAccountThrowableError(ZcashError.rustGetNextAvailableAddress("mocked error")) @@ -84,22 +83,15 @@ class RustBackendMockHelper { await rustBackendMock.setInitDataDbSeedReturnValue(.seedRequired) await rustBackendMock.setGetNearestRewindHeightHeightReturnValue(-1) await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in } - await rustBackendMock.setCreateToAddressUskToValueMemoThrowableError(ZcashError.rustCreateToAddress("mocked error")) - await rustBackendMock.setShieldFundsUskMemoShieldingThresholdThrowableError(ZcashError.rustShieldFunds("mocked error")) + await rustBackendMock.setProposeTransferAccountToValueMemoThrowableError(ZcashError.rustCreateToAddress("mocked error")) + await rustBackendMock.setProposeShieldingAccountMemoShieldingThresholdThrowableError(ZcashError.rustShieldFunds("mocked error")) + await rustBackendMock.setCreateProposedTransactionProposalUskThrowableError(ZcashError.rustCreateToAddress("mocked error")) await rustBackendMock.setDecryptAndStoreTransactionTxBytesMinedHeightThrowableError(ZcashError.rustDecryptAndStoreTransaction("mock fail")) await rustBackendMock.setInitDataDbSeedClosure() { seed in return try await rustBackend.initDataDb(seed: seed) } - await rustBackendMock.setGetBalanceAccountClosure() { account in - return try await rustBackend.getBalance(account: account) - } - - await rustBackendMock.setGetVerifiedBalanceAccountClosure() { account in - return try await rustBackend.getVerifiedBalance(account: account) - } - await rustBackendMock.setRewindToHeightHeightClosure() { height in try await rustBackend.rewindToHeight(height: height) }