From 84ac6252feb88b9dbf9865651df3bc478225f5a1 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 28 Feb 2024 16:23:12 +0000 Subject: [PATCH] Adjust `Synchronizer.proposeShielding` API - Returns `null` when there are no funds to shield or the shielding threshold is not met. - Throws an exception if there are funds to shield in more than one transparent receiver within the account. - Has an optional parameter for specifying which transparent receiver to shield funds from. This commit only alters the API to support the above; the functional changes require modifying the FFI and Rust backend, which will happen in a separate commit. --- .../Rust/ZcashRustBackend.swift | 9 ++- .../Rust/ZcashRustBackendWelding.swift | 12 +++- .../ZcashLightClientKit/Synchronizer.swift | 12 +++- .../Synchronizer/ClosureSDKSynchronizer.swift | 10 ++- .../Synchronizer/CombineSDKSynchronizer.swift | 12 +++- .../Synchronizer/SDKSynchronizer.swift | 17 +++-- .../Transaction/TransactionEncoder.swift | 12 +++- .../WalletTransactionEncoder.swift | 12 ++-- .../AutoMockable.generated.swift | 68 +++++++++---------- Tests/TestUtils/Stubs.swift | 2 +- 10 files changed, 108 insertions(+), 58 deletions(-) diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 100c0944..99d128a6 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -619,8 +619,13 @@ actor ZcashRustBackend: ZcashRustBackendWelding { func proposeShielding( account: Int32, memo: MemoBytes?, - shieldingThreshold: Zatoshi - ) async throws -> FfiProposal { + shieldingThreshold: Zatoshi, + transparentReceiver: String? + ) async throws -> FfiProposal? { + if transparentReceiver != nil { + throw ZcashError.rustScanBlocks("TODO: Implement transparentReceiver support in FFI") + } + globalDBLock.lock() let proposal = zcashlc_propose_shielding( dbData.0, diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index 4d30e496..fd6cc03f 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -212,14 +212,22 @@ protocol ZcashRustBackendWelding { /// that can then be authorized and made ready for submission to the network with /// `createProposedTransaction`. /// + /// Returns the proposal, or `nil` if the transparent balance that would be shielded + /// is zero or below `shieldingThreshold`. + /// /// - parameter account: index of the given account /// - Parameter memo: the `Memo` for this transaction + /// - Parameter transparentReceiver: a specific transparent receiver within the account + /// that should be the source of transparent funds. Default is `nil` which + /// will select whichever of the account's transparent receivers has funds + /// to shield. /// - Throws: `rustShieldFunds` if rust layer returns error. func proposeShielding( account: Int32, memo: MemoBytes?, - shieldingThreshold: Zatoshi - ) async throws -> FfiProposal + shieldingThreshold: Zatoshi, + transparentReceiver: String? + ) async throws -> FfiProposal? /// Creates a transaction from the given proposal. /// - Parameter proposal: the transaction proposal. diff --git a/Sources/ZcashLightClientKit/Synchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer.swift index 639fce98..e94013de 100644 --- a/Sources/ZcashLightClientKit/Synchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer.swift @@ -176,14 +176,22 @@ public protocol Synchronizer: AnyObject { /// - Parameter accountIndex: the account for which to shield funds. /// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created. /// - Parameter memo: an optional memo to include as part of the proposal's transactions. + /// - Parameter transparentReceiver: a specific transparent receiver within the account + /// that should be the source of transparent funds. Default is `nil` which + /// will select whichever of the account's transparent receivers has funds + /// to shield. + /// + /// Returns the proposal, or `nil` if the transparent balance that would be shielded + /// is zero or below `shieldingThreshold`. /// /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws /// `SynchronizerErrors.notPrepared`. func proposeShielding( accountIndex: Int, shieldingThreshold: Zatoshi, - memo: Memo - ) async throws -> Proposal + memo: Memo, + transparentReceiver: TransparentAddress? + ) async throws -> Proposal? /// Creates the transactions in the given proposal. /// diff --git a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift index 086b47e1..1f2d31ae 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/ClosureSDKSynchronizer.swift @@ -86,10 +86,16 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer { accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, - completion: @escaping (Result) -> Void + transparentReceiver: TransparentAddress? = nil, + completion: @escaping (Result) -> Void ) { AsyncToClosureGateway.executeThrowingAction(completion) { - try await self.synchronizer.proposeShielding(accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo) + try await self.synchronizer.proposeShielding( + accountIndex: accountIndex, + shieldingThreshold: shieldingThreshold, + memo: memo, + transparentReceiver: transparentReceiver + ) } } diff --git a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift index e30625a0..aa38be92 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/CombineSDKSynchronizer.swift @@ -83,10 +83,16 @@ extension CombineSDKSynchronizer: CombineSynchronizer { public func proposeShielding( accountIndex: Int, shieldingThreshold: Zatoshi, - memo: Memo - ) -> SinglePublisher { + memo: Memo, + transparentReceiver: TransparentAddress? = nil + ) -> SinglePublisher { AsyncToCombineGateway.executeThrowingAction() { - try await self.synchronizer.proposeShielding(accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo) + try await self.synchronizer.proposeShielding( + accountIndex: accountIndex, + shieldingThreshold: shieldingThreshold, + memo: memo, + transparentReceiver: transparentReceiver + ) } } diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index 9918d171..a162a872 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -282,13 +282,19 @@ public class SDKSynchronizer: Synchronizer { return proposal } - public func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo) async throws -> Proposal { + public func proposeShielding( + accountIndex: Int, + shieldingThreshold: Zatoshi, + memo: Memo, + transparentReceiver: TransparentAddress? = nil + ) async throws -> Proposal? { try throwIfUnprepared() let proposal = try await transactionEncoder.proposeShielding( accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, - memoBytes: memo.asMemoBytes() + memoBytes: memo.asMemoBytes(), + transparentReceiver: transparentReceiver?.stringEncoded ) return proposal @@ -384,11 +390,12 @@ public class SDKSynchronizer: Synchronizer { throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds } - let proposal = try await transactionEncoder.proposeShielding( + guard let proposal = try await transactionEncoder.proposeShielding( accountIndex: Int(spendingKey.account), shieldingThreshold: shieldingThreshold, - memoBytes: memo.asMemoBytes() - ) + memoBytes: memo.asMemoBytes(), + transparentReceiver: nil + ) else { throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds } let transactions = try await transactionEncoder.createProposedTransactions( proposal: proposal, diff --git a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift index 163b7450..6acfef78 100644 --- a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift @@ -40,14 +40,22 @@ protocol TransactionEncoder { /// - Parameter accountIndex: the account for which to shield funds. /// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created. /// - Parameter memoBytes: an optional memo to include as part of the proposal's transactions. + /// - Parameter transparentReceiver: a specific transparent receiver within the account + /// that should be the source of transparent funds. Default is `nil` which + /// will select whichever of the account's transparent receivers has funds + /// to shield. + /// + /// Returns the proposal, or `nil` if the transparent balance that would be shielded + /// is zero or below `shieldingThreshold`. /// /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws /// `SynchronizerErrors.notPrepared`. func proposeShielding( accountIndex: Int, shieldingThreshold: Zatoshi, - memoBytes: MemoBytes? - ) async throws -> Proposal + memoBytes: MemoBytes?, + transparentReceiver: String? + ) async throws -> Proposal? /// Creates the transactions in the given proposal. /// diff --git a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index fb4c2c1b..5e9ed9bd 100644 --- a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -74,13 +74,15 @@ class WalletTransactionEncoder: TransactionEncoder { func proposeShielding( accountIndex: Int, shieldingThreshold: Zatoshi, - memoBytes: MemoBytes? - ) async throws -> Proposal { - let proposal = try await rustBackend.proposeShielding( + memoBytes: MemoBytes?, + transparentReceiver: String? = nil + ) async throws -> Proposal? { + guard let proposal = try await rustBackend.proposeShielding( account: Int32(accountIndex), memo: memoBytes, - shieldingThreshold: shieldingThreshold - ) + shieldingThreshold: shieldingThreshold, + transparentReceiver: transparentReceiver + ) else { return nil } return Proposal(inner: proposal) } diff --git a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift index cc2049da..ebb863bc 100644 --- a/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift +++ b/Tests/TestUtils/Sourcery/GeneratedMocks/AutoMockable.generated.swift @@ -1423,25 +1423,25 @@ class SynchronizerMock: Synchronizer { // MARK: - proposeShielding - var proposeShieldingAccountIndexShieldingThresholdMemoThrowableError: Error? - var proposeShieldingAccountIndexShieldingThresholdMemoCallsCount = 0 - var proposeShieldingAccountIndexShieldingThresholdMemoCalled: Bool { - return proposeShieldingAccountIndexShieldingThresholdMemoCallsCount > 0 + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError: Error? + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount = 0 + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCalled: Bool { + return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount > 0 } - var proposeShieldingAccountIndexShieldingThresholdMemoReceivedArguments: (accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo)? - var proposeShieldingAccountIndexShieldingThresholdMemoReturnValue: Proposal! - var proposeShieldingAccountIndexShieldingThresholdMemoClosure: ((Int, Zatoshi, Memo) async throws -> Proposal)? + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments: (accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?)? + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue: Proposal? + var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure: ((Int, Zatoshi, Memo, TransparentAddress?) async throws -> Proposal?)? - func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo) async throws -> Proposal { - if let error = proposeShieldingAccountIndexShieldingThresholdMemoThrowableError { + func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?) async throws -> Proposal? { + if let error = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError { throw error } - proposeShieldingAccountIndexShieldingThresholdMemoCallsCount += 1 - proposeShieldingAccountIndexShieldingThresholdMemoReceivedArguments = (accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo) - if let closure = proposeShieldingAccountIndexShieldingThresholdMemoClosure { - return try await closure(accountIndex, shieldingThreshold, memo) + proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount += 1 + proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments = (accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo, transparentReceiver: transparentReceiver) + if let closure = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure { + return try await closure(accountIndex, shieldingThreshold, memo, transparentReceiver) } else { - return proposeShieldingAccountIndexShieldingThresholdMemoReturnValue + return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue } } @@ -2828,34 +2828,34 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding { // MARK: - proposeShielding - var proposeShieldingAccountMemoShieldingThresholdThrowableError: Error? - func setProposeShieldingAccountMemoShieldingThresholdThrowableError(_ param: Error?) async { - proposeShieldingAccountMemoShieldingThresholdThrowableError = param + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error? + func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(_ param: Error?) async { + proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError = param } - var proposeShieldingAccountMemoShieldingThresholdCallsCount = 0 - var proposeShieldingAccountMemoShieldingThresholdCalled: Bool { - return proposeShieldingAccountMemoShieldingThresholdCallsCount > 0 + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount = 0 + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCalled: Bool { + return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount > 0 } - var proposeShieldingAccountMemoShieldingThresholdReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi)? - var proposeShieldingAccountMemoShieldingThresholdReturnValue: FfiProposal! - func setProposeShieldingAccountMemoShieldingThresholdReturnValue(_ param: FfiProposal) async { - proposeShieldingAccountMemoShieldingThresholdReturnValue = param + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?)? + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue: FfiProposal? + func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue(_ param: FfiProposal?) async { + proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue = param } - var proposeShieldingAccountMemoShieldingThresholdClosure: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)? - func setProposeShieldingAccountMemoShieldingThresholdClosure(_ param: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?) async { - proposeShieldingAccountMemoShieldingThresholdClosure = param + var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)? + func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure(_ param: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?) async { + proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure = param } - func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> FfiProposal { - if let error = proposeShieldingAccountMemoShieldingThresholdThrowableError { + func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?) async throws -> FfiProposal? { + if let error = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError { throw error } - proposeShieldingAccountMemoShieldingThresholdCallsCount += 1 - proposeShieldingAccountMemoShieldingThresholdReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold) - if let closure = proposeShieldingAccountMemoShieldingThresholdClosure { - return try await closure(account, memo, shieldingThreshold) + proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount += 1 + proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold, transparentReceiver: transparentReceiver) + if let closure = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure { + return try await closure(account, memo, shieldingThreshold, transparentReceiver) } else { - return proposeShieldingAccountMemoShieldingThresholdReturnValue + return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue } } diff --git a/Tests/TestUtils/Stubs.swift b/Tests/TestUtils/Stubs.swift index 660aac76..dbc62987 100644 --- a/Tests/TestUtils/Stubs.swift +++ b/Tests/TestUtils/Stubs.swift @@ -84,7 +84,7 @@ class RustBackendMockHelper { await rustBackendMock.setGetNearestRewindHeightHeightReturnValue(-1) await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in } await rustBackendMock.setProposeTransferAccountToValueMemoThrowableError(ZcashError.rustCreateToAddress("mocked error")) - await rustBackendMock.setProposeShieldingAccountMemoShieldingThresholdThrowableError(ZcashError.rustShieldFunds("mocked error")) + await rustBackendMock.setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(ZcashError.rustShieldFunds("mocked error")) await rustBackendMock.setCreateProposedTransactionProposalUskThrowableError(ZcashError.rustCreateToAddress("mocked error")) await rustBackendMock.setDecryptAndStoreTransactionTxBytesMinedHeightThrowableError(ZcashError.rustDecryptAndStoreTransaction("mock fail"))