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.

Part of Electric-Coin-Company/zcash-android-wallet-sdk#680.
This commit is contained in:
Jack Grigg 2024-02-28 01:44:12 +00:00
parent e36bbdec7a
commit abffb3f9ee
17 changed files with 167 additions and 63 deletions

View File

@ -31,8 +31,9 @@ interface Backend {
suspend fun proposeShielding(
account: Int,
shieldingThreshold: Long,
memo: ByteArray? = byteArrayOf()
): ProposalUnsafe
memo: ByteArray? = byteArrayOf(),
transparentReceiver: String? = null
): ProposalUnsafe?
suspend fun createProposedTransaction(
proposal: ProposalUnsafe,

View File

@ -308,19 +308,23 @@ class RustBackend private constructor(
override suspend fun proposeShielding(
account: Int,
shieldingThreshold: Long,
memo: ByteArray?
): ProposalUnsafe {
memo: ByteArray?,
transparentReceiver: String?
): ProposalUnsafe? {
return withContext(SdkDispatchers.DATABASE_IO) {
ProposalUnsafe.parse(
proposeShielding(
dataDbFile.absolutePath,
account,
shieldingThreshold,
memo ?: ByteArray(0),
networkId = networkId,
useZip317Fees = IS_USE_ZIP_317_FEES
proposeShielding(
dataDbFile.absolutePath,
account,
shieldingThreshold,
memo ?: ByteArray(0),
transparentReceiver,
networkId = networkId,
useZip317Fees = IS_USE_ZIP_317_FEES
)?.let {
ProposalUnsafe.parse(
it
)
)
}
}
}
@ -588,9 +592,10 @@ class RustBackend private constructor(
account: Int,
shieldingThreshold: Long,
memo: ByteArray,
transparentReceiver: String?,
networkId: Int,
useZip317Fees: Boolean
): ByteArray
): ByteArray?
@JvmStatic
@Suppress("LongParameterList")

View File

@ -1482,6 +1482,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
account: jint,
shielding_threshold: jlong,
memo: JByteArray<'local>,
transparent_receiver: JString<'local>,
network_id: jint,
use_zip317_fees: jboolean,
) -> jbyteArray {
@ -1494,10 +1495,36 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
.map_err(|()| format_err!("Invalid shielding threshold, out of range"))?;
let memo_bytes = env.convert_byte_array(memo).unwrap();
let transparent_receiver =
match utils::java_nullable_string_to_rust(env, &transparent_receiver) {
None => Ok(None),
Some(addr) => match Address::decode(&network, &addr) {
None => Err(format_err!("Transparent receiver is for the wrong network")),
Some(addr) => match addr {
Address::Sapling(_) | Address::Unified(_) => Err(format_err!(
"Transparent receiver is not a transparent address"
)),
Address::Transparent(addr) => {
if db_data
.get_transparent_receivers(account)?
.contains_key(&addr)
{
Ok(Some(addr))
} else {
Err(format_err!(
"Transparent receiver does not belong to account {}",
u32::from(account),
))
}
}
},
},
}?;
let min_confirmations = 0;
let min_confirmations_for_heights = NonZeroU32::new(1).unwrap();
let from_addrs: Vec<TransparentAddress> = db_data
let account_receivers = db_data
.get_target_and_anchor_heights(min_confirmations_for_heights)
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
@ -1515,9 +1542,23 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
e
)
})
})?
.into_keys()
.collect();
})?;
let from_addrs = if let Some((addr, _)) = transparent_receiver.map_or_else(||
if account_receivers.len() > 1 {
Err(format_err!(
"Account has more than one transparent receiver with funds to shield; this is not yet supported by the SDK. Provide a specific transparent receiver to shield funds from."
))
} else {
Ok(account_receivers.iter().next().map(|(a, v)| (*a, *v)))
},
|addr| Ok(account_receivers.get(&addr).map(|value| (addr, *value)))
)?.filter(|(_, value)| *value >= shielding_threshold.into()) {
[addr]
} else {
// There are no transparent funds to shield; don't create a proposal.
return Ok(ptr::null_mut());
};
let memo = Memo::from_bytes(&memo_bytes).unwrap();

View File

@ -38,6 +38,10 @@ pub(crate) fn java_string_to_rust(env: &mut JNIEnv, jstring: &JString) -> String
.into()
}
pub(crate) fn java_nullable_string_to_rust(env: &mut JNIEnv, jstring: &JString) -> Option<String> {
(!jstring.is_null()).then(|| java_string_to_rust(env, jstring))
}
pub(crate) fn rust_bytes_to_java<'a>(
env: &JNIEnv<'a>,
data: &[u8],

View File

@ -129,10 +129,12 @@ class TestWallet(
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
if (walletBalance.value > 0L) {
synchronizer.createProposedTransactions(
synchronizer.proposeShielding(shieldedSpendingKey.account, Zatoshi(100000)),
shieldedSpendingKey
)
synchronizer.proposeShielding(shieldedSpendingKey.account, Zatoshi(100000))?.let {
synchronizer.createProposedTransactions(
it,
shieldedSpendingKey
)
}
}
}

View File

@ -68,10 +68,12 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
Account.DEFAULT
)
sharedViewModel.synchronizerFlow.value?.let { synchronizer ->
synchronizer.createProposedTransactions(
synchronizer.proposeShielding(usk.account, Zatoshi(100000)),
usk
)
synchronizer.proposeShielding(usk.account, Zatoshi(100000))?.let { it1 ->
synchronizer.createProposedTransactions(
it1,
usk
)
}
}
}
}

View File

@ -230,12 +230,14 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
viewModelScope.launch {
val spendingKey = spendingKey.filterNotNull().first()
kotlin.runCatching {
synchronizer.createProposedTransactions(
synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000)),
spendingKey
)
synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let {
synchronizer.createProposedTransactions(
it,
spendingKey
)
}
}
.onSuccess { mutableSendState.value = SendState.Sent(it.toList()) }
.onSuccess { it?.let { mutableSendState.value = SendState.Sent(it.toList()) } }
.onFailure { mutableSendState.value = SendState.Error(it) }
}
} else {

View File

@ -132,10 +132,12 @@ class TestWallet(
Twig.debug { "FOUND utxo balance of total: $walletBalance" }
if (walletBalance.value > 0L) {
synchronizer.createProposedTransactions(
synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000)),
spendingKey
)
synchronizer.proposeShielding(spendingKey.account, Zatoshi(100000))?.let {
synchronizer.createProposedTransactions(
it,
spendingKey
)
}
}
}

View File

@ -89,8 +89,9 @@ internal class FakeRustBackend(
override suspend fun proposeShielding(
account: Int,
shieldingThreshold: Long,
memo: ByteArray?
): ProposalUnsafe {
memo: ByteArray?,
transparentReceiver: String?
): ProposalUnsafe? {
TODO("Not yet implemented")
}

View File

@ -570,8 +570,9 @@ class SdkSynchronizer private constructor(
override suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: String
): Proposal = txManager.proposeShielding(account, shieldingThreshold, memo)
memo: String,
transparentReceiver: String?
): Proposal? = txManager.proposeShielding(account, shieldingThreshold, memo, transparentReceiver)
@Throws(TransactionEncoderException::class)
override suspend fun createProposedTransactions(
@ -636,7 +637,7 @@ class SdkSynchronizer private constructor(
message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.",
replaceWith =
ReplaceWith(
"createProposedTransactions(proposeShielding(usk.account, shieldingThreshold, memo), usk)"
"proposeShielding(usk.account, shieldingThreshold, memo)?.let { createProposedTransactions(it, usk) }"
)
)
@Throws(TransactionEncoderException::class, TransactionSubmitException::class)

View File

@ -193,12 +193,23 @@ interface Synchronizer {
* @param shieldingThreshold the minimum transparent balance required before a
* proposal will be created.
* @param memo the optional memo to include as part of the proposal's transactions.
* @param transparentReceiver a specific transparent receiver within the account that
* should be the source of transparent funds. Default is
* null which will select whichever of the account's
* transparent receivers has funds to shield.
*
* @return the proposal, or null if the transparent balance that would be shielded is
* zero or below `shieldingThreshold`.
*
* @throws Exception if `transparentReceiver` is null and there are transparent funds
* in more than one of the account's transparent receivers.
*/
suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
): Proposal
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX,
transparentReceiver: String? = null
): Proposal?
/**
* Creates the transactions in the given proposal.
@ -247,7 +258,7 @@ interface Synchronizer {
message = "Upcoming SDK 2.1 will create multiple transactions at once for some recipients.",
replaceWith =
ReplaceWith(
"createProposedTransactions(proposeShielding(usk.account, shieldingThreshold, memo), usk)"
"proposeShielding(usk.account, shieldingThreshold, memo)?.let { createProposedTransactions(it, usk) }"
)
)
suspend fun shieldFunds(

View File

@ -35,8 +35,9 @@ internal interface TypesafeBackend {
suspend fun proposeShielding(
account: Account,
shieldingThreshold: Long,
memo: ByteArray? = byteArrayOf()
): Proposal
memo: ByteArray? = byteArrayOf(),
transparentReceiver: String? = null
): Proposal?
suspend fun createProposedTransaction(
proposal: Proposal,

View File

@ -54,15 +54,19 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
override suspend fun proposeShielding(
account: Account,
shieldingThreshold: Long,
memo: ByteArray?
): Proposal =
Proposal.fromUnsafe(
backend.proposeShielding(
account.value,
shieldingThreshold,
memo
memo: ByteArray?,
transparentReceiver: String?
): Proposal? =
backend.proposeShielding(
account.value,
shieldingThreshold,
memo,
transparentReceiver
)?.let {
Proposal.fromUnsafe(
it
)
)
}
override suspend fun createProposedTransaction(
proposal: Proposal,

View File

@ -59,12 +59,23 @@ internal interface OutboundTransactionManager {
* @param shieldingThreshold the minimum transparent balance required before a
* proposal will be created.
* @param memo the optional memo to include as part of the proposal's transactions.
* @param transparentReceiver a specific transparent receiver within the account that
* should be the source of transparent funds. Default is
* null which will select whichever of the account's
* transparent receivers has funds to shield.
*
* @return the proposal, or null if the transparent balance that would be shielded is
* zero or below `shieldingThreshold`.
*
* @throws Exception if `transparentReceiver` is null and there are transparent funds
* in more than one of the account's transparent receivers.
*/
suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: String
): Proposal
memo: String,
transparentReceiver: String?
): Proposal?
/**
* Creates the transactions in the given proposal.

View File

@ -53,8 +53,9 @@ internal class OutboundTransactionManagerImpl(
override suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: String
): Proposal = encoder.proposeShielding(account, shieldingThreshold, memo.toByteArray())
memo: String,
transparentReceiver: String?
): Proposal? = encoder.proposeShielding(account, shieldingThreshold, memo.toByteArray(), transparentReceiver)
override suspend fun createProposedTransactions(
proposal: Proposal,

View File

@ -64,12 +64,23 @@ internal interface TransactionEncoder {
* @param shieldingThreshold the minimum transparent balance required before a
* proposal will be created.
* @param memo the optional memo to include as part of the proposal's transactions.
* @param transparentReceiver a specific transparent receiver within the account that
* should be the source of transparent funds. Default is
* null which will select whichever of the account's
* transparent receivers has funds to shield.
*
* @return the proposal, or null if the transparent balance that would be shielded is
* zero or below `shieldingThreshold`.
*
* @throws Exception if `transparentReceiver` is null and there are transparent funds
* in more than one of the account's transparent receivers.
*/
suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: ByteArray? = byteArrayOf()
): Proposal
memo: ByteArray? = byteArrayOf(),
transparentReceiver: String? = null
): Proposal?
/**
* Creates the transactions in the given proposal.

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.exception.SdkException
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.masked
import cash.z.ecc.android.sdk.internal.SaplingParamTool
@ -97,11 +98,12 @@ internal class TransactionEncoderImpl(
override suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: ByteArray?
): Proposal {
memo: ByteArray?,
transparentReceiver: String?
): Proposal? {
@Suppress("TooGenericExceptionCaught")
return try {
backend.proposeShielding(account, shieldingThreshold.value, memo)
backend.proposeShielding(account, shieldingThreshold.value, memo, transparentReceiver)
} catch (t: Throwable) {
// TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)
// then consider custom error that says no UTXOs existed to shield
@ -239,7 +241,9 @@ internal class TransactionEncoderImpl(
return try {
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
Twig.debug { "params exist! attempting to shield..." }
val proposal = backend.proposeShielding(usk.account, 100000, memo)
val proposal =
backend.proposeShielding(usk.account, 100000, memo)
?: throw SdkException("Insufficient balance (have 0, need 100000 including fee)", null)
backend.createProposedTransaction(proposal, usk)
} catch (t: Throwable) {
// TODO [#680]: if this error matches: Insufficient balance (have 0, need 1000 including fee)