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.
This commit is contained in:
Jack Grigg 2024-02-28 16:23:12 +00:00
parent 7508fce6b6
commit fffbd857fc
10 changed files with 108 additions and 58 deletions

View File

@ -619,8 +619,13 @@ actor ZcashRustBackend: ZcashRustBackendWelding {
func proposeShielding( func proposeShielding(
account: Int32, account: Int32,
memo: MemoBytes?, memo: MemoBytes?,
shieldingThreshold: Zatoshi shieldingThreshold: Zatoshi,
) async throws -> FfiProposal { transparentReceiver: String?
) async throws -> FfiProposal? {
if transparentReceiver != nil {
throw ZcashError.rustScanBlocks("TODO: Implement transparentReceiver support in FFI")
}
globalDBLock.lock() globalDBLock.lock()
let proposal = zcashlc_propose_shielding( let proposal = zcashlc_propose_shielding(
dbData.0, dbData.0,

View File

@ -212,14 +212,22 @@ protocol ZcashRustBackendWelding {
/// that can then be authorized and made ready for submission to the network with /// that can then be authorized and made ready for submission to the network with
/// `createProposedTransaction`. /// `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 account: index of the given account
/// - Parameter memo: the `Memo` for this transaction /// - 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. /// - Throws: `rustShieldFunds` if rust layer returns error.
func proposeShielding( func proposeShielding(
account: Int32, account: Int32,
memo: MemoBytes?, memo: MemoBytes?,
shieldingThreshold: Zatoshi shieldingThreshold: Zatoshi,
) async throws -> FfiProposal transparentReceiver: String?
) async throws -> FfiProposal?
/// Creates a transaction from the given proposal. /// Creates a transaction from the given proposal.
/// - Parameter proposal: the transaction proposal. /// - Parameter proposal: the transaction proposal.

View File

@ -176,14 +176,22 @@ public protocol Synchronizer: AnyObject {
/// - Parameter accountIndex: the account for which to shield funds. /// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created. /// - 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 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 /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`. /// `SynchronizerErrors.notPrepared`.
func proposeShielding( func proposeShielding(
accountIndex: Int, accountIndex: Int,
shieldingThreshold: Zatoshi, shieldingThreshold: Zatoshi,
memo: Memo memo: Memo,
) async throws -> Proposal transparentReceiver: TransparentAddress?
) async throws -> Proposal?
/// Creates the transactions in the given proposal. /// Creates the transactions in the given proposal.
/// ///

View File

@ -86,10 +86,16 @@ extension ClosureSDKSynchronizer: ClosureSynchronizer {
accountIndex: Int, accountIndex: Int,
shieldingThreshold: Zatoshi, shieldingThreshold: Zatoshi,
memo: Memo, memo: Memo,
completion: @escaping (Result<Proposal, Error>) -> Void transparentReceiver: TransparentAddress? = nil,
completion: @escaping (Result<Proposal?, Error>) -> Void
) { ) {
AsyncToClosureGateway.executeThrowingAction(completion) { 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
)
} }
} }

View File

@ -83,10 +83,16 @@ extension CombineSDKSynchronizer: CombineSynchronizer {
public func proposeShielding( public func proposeShielding(
accountIndex: Int, accountIndex: Int,
shieldingThreshold: Zatoshi, shieldingThreshold: Zatoshi,
memo: Memo memo: Memo,
) -> SinglePublisher<Proposal, Error> { transparentReceiver: TransparentAddress? = nil
) -> SinglePublisher<Proposal?, Error> {
AsyncToCombineGateway.executeThrowingAction() { 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
)
} }
} }

View File

@ -282,13 +282,19 @@ public class SDKSynchronizer: Synchronizer {
return proposal 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() try throwIfUnprepared()
let proposal = try await transactionEncoder.proposeShielding( let proposal = try await transactionEncoder.proposeShielding(
accountIndex: accountIndex, accountIndex: accountIndex,
shieldingThreshold: shieldingThreshold, shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes() memoBytes: memo.asMemoBytes(),
transparentReceiver: transparentReceiver?.stringEncoded
) )
return proposal return proposal
@ -384,11 +390,12 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds
} }
let proposal = try await transactionEncoder.proposeShielding( guard let proposal = try await transactionEncoder.proposeShielding(
accountIndex: Int(spendingKey.account), accountIndex: Int(spendingKey.account),
shieldingThreshold: shieldingThreshold, shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes() memoBytes: memo.asMemoBytes(),
) transparentReceiver: nil
) else { throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds }
let transactions = try await transactionEncoder.createProposedTransactions( let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal, proposal: proposal,

View File

@ -40,14 +40,22 @@ protocol TransactionEncoder {
/// - Parameter accountIndex: the account for which to shield funds. /// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter shieldingThreshold: the minimum transparent balance required before a proposal will be created. /// - 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 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 /// If `prepare()` hasn't already been called since creation of the synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`. /// `SynchronizerErrors.notPrepared`.
func proposeShielding( func proposeShielding(
accountIndex: Int, accountIndex: Int,
shieldingThreshold: Zatoshi, shieldingThreshold: Zatoshi,
memoBytes: MemoBytes? memoBytes: MemoBytes?,
) async throws -> Proposal transparentReceiver: String?
) async throws -> Proposal?
/// Creates the transactions in the given proposal. /// Creates the transactions in the given proposal.
/// ///

View File

@ -74,13 +74,15 @@ class WalletTransactionEncoder: TransactionEncoder {
func proposeShielding( func proposeShielding(
accountIndex: Int, accountIndex: Int,
shieldingThreshold: Zatoshi, shieldingThreshold: Zatoshi,
memoBytes: MemoBytes? memoBytes: MemoBytes?,
) async throws -> Proposal { transparentReceiver: String? = nil
let proposal = try await rustBackend.proposeShielding( ) async throws -> Proposal? {
guard let proposal = try await rustBackend.proposeShielding(
account: Int32(accountIndex), account: Int32(accountIndex),
memo: memoBytes, memo: memoBytes,
shieldingThreshold: shieldingThreshold shieldingThreshold: shieldingThreshold,
) transparentReceiver: transparentReceiver
) else { return nil }
return Proposal(inner: proposal) return Proposal(inner: proposal)
} }

View File

@ -1423,25 +1423,25 @@ class SynchronizerMock: Synchronizer {
// MARK: - proposeShielding // MARK: - proposeShielding
var proposeShieldingAccountIndexShieldingThresholdMemoThrowableError: Error? var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError: Error?
var proposeShieldingAccountIndexShieldingThresholdMemoCallsCount = 0 var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount = 0
var proposeShieldingAccountIndexShieldingThresholdMemoCalled: Bool { var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCalled: Bool {
return proposeShieldingAccountIndexShieldingThresholdMemoCallsCount > 0 return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount > 0
} }
var proposeShieldingAccountIndexShieldingThresholdMemoReceivedArguments: (accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo)? var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments: (accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?)?
var proposeShieldingAccountIndexShieldingThresholdMemoReturnValue: Proposal! var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue: Proposal?
var proposeShieldingAccountIndexShieldingThresholdMemoClosure: ((Int, Zatoshi, Memo) async throws -> Proposal)? var proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure: ((Int, Zatoshi, Memo, TransparentAddress?) async throws -> Proposal?)?
func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo) async throws -> Proposal { func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo, transparentReceiver: TransparentAddress?) async throws -> Proposal? {
if let error = proposeShieldingAccountIndexShieldingThresholdMemoThrowableError { if let error = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverThrowableError {
throw error throw error
} }
proposeShieldingAccountIndexShieldingThresholdMemoCallsCount += 1 proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverCallsCount += 1
proposeShieldingAccountIndexShieldingThresholdMemoReceivedArguments = (accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo) proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReceivedArguments = (accountIndex: accountIndex, shieldingThreshold: shieldingThreshold, memo: memo, transparentReceiver: transparentReceiver)
if let closure = proposeShieldingAccountIndexShieldingThresholdMemoClosure { if let closure = proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverClosure {
return try await closure(accountIndex, shieldingThreshold, memo) return try await closure(accountIndex, shieldingThreshold, memo, transparentReceiver)
} else { } else {
return proposeShieldingAccountIndexShieldingThresholdMemoReturnValue return proposeShieldingAccountIndexShieldingThresholdMemoTransparentReceiverReturnValue
} }
} }
@ -2828,34 +2828,34 @@ actor ZcashRustBackendWeldingMock: ZcashRustBackendWelding {
// MARK: - proposeShielding // MARK: - proposeShielding
var proposeShieldingAccountMemoShieldingThresholdThrowableError: Error? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError: Error?
func setProposeShieldingAccountMemoShieldingThresholdThrowableError(_ param: Error?) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError(_ param: Error?) async {
proposeShieldingAccountMemoShieldingThresholdThrowableError = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError = param
} }
var proposeShieldingAccountMemoShieldingThresholdCallsCount = 0 var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount = 0
var proposeShieldingAccountMemoShieldingThresholdCalled: Bool { var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCalled: Bool {
return proposeShieldingAccountMemoShieldingThresholdCallsCount > 0 return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount > 0
} }
var proposeShieldingAccountMemoShieldingThresholdReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi)? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments: (account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?)?
var proposeShieldingAccountMemoShieldingThresholdReturnValue: FfiProposal! var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue: FfiProposal?
func setProposeShieldingAccountMemoShieldingThresholdReturnValue(_ param: FfiProposal) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue(_ param: FfiProposal?) async {
proposeShieldingAccountMemoShieldingThresholdReturnValue = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue = param
} }
var proposeShieldingAccountMemoShieldingThresholdClosure: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)? var proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?
func setProposeShieldingAccountMemoShieldingThresholdClosure(_ param: ((Int32, MemoBytes?, Zatoshi) async throws -> FfiProposal)?) async { func setProposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure(_ param: ((Int32, MemoBytes?, Zatoshi, String?) async throws -> FfiProposal?)?) async {
proposeShieldingAccountMemoShieldingThresholdClosure = param proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure = param
} }
func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi) async throws -> FfiProposal { func proposeShielding(account: Int32, memo: MemoBytes?, shieldingThreshold: Zatoshi, transparentReceiver: String?) async throws -> FfiProposal? {
if let error = proposeShieldingAccountMemoShieldingThresholdThrowableError { if let error = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverThrowableError {
throw error throw error
} }
proposeShieldingAccountMemoShieldingThresholdCallsCount += 1 proposeShieldingAccountMemoShieldingThresholdTransparentReceiverCallsCount += 1
proposeShieldingAccountMemoShieldingThresholdReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold) proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReceivedArguments = (account: account, memo: memo, shieldingThreshold: shieldingThreshold, transparentReceiver: transparentReceiver)
if let closure = proposeShieldingAccountMemoShieldingThresholdClosure { if let closure = proposeShieldingAccountMemoShieldingThresholdTransparentReceiverClosure {
return try await closure(account, memo, shieldingThreshold) return try await closure(account, memo, shieldingThreshold, transparentReceiver)
} else { } else {
return proposeShieldingAccountMemoShieldingThresholdReturnValue return proposeShieldingAccountMemoShieldingThresholdTransparentReceiverReturnValue
} }
} }

View File

@ -84,7 +84,7 @@ class RustBackendMockHelper {
await rustBackendMock.setGetNearestRewindHeightHeightReturnValue(-1) await rustBackendMock.setGetNearestRewindHeightHeightReturnValue(-1)
await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in } await rustBackendMock.setPutUnspentTransparentOutputTxidIndexScriptValueHeightClosure() { _, _, _, _, _ in }
await rustBackendMock.setProposeTransferAccountToValueMemoThrowableError(ZcashError.rustCreateToAddress("mocked error")) 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.setCreateProposedTransactionProposalUskThrowableError(ZcashError.rustCreateToAddress("mocked error"))
await rustBackendMock.setDecryptAndStoreTransactionTxBytesMinedHeightThrowableError(ZcashError.rustDecryptAndStoreTransaction("mock fail")) await rustBackendMock.setDecryptAndStoreTransactionTxBytesMinedHeightThrowableError(ZcashError.rustDecryptAndStoreTransaction("mock fail"))