diff --git a/CHANGELOG.md b/CHANGELOG.md index d20ec467..af815009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Change Log ### Added - `cash.z.ecc.android.sdk`: + - `Synchronizer.getCurrentAddress` + - `Synchronizer.getLegacySaplingAddress` + - `Synchronizer.getLegacyTransparentAddress` - `Synchronizer.isValidUnifiedAddr` - `cash.z.ecc.android.sdk.model`: - `FirstClassByteArray` @@ -37,6 +40,10 @@ Change Log all transparent secret keys within an account) instead of a transparent secret key. ### Removed +- `cash.z.ecc.android.sdk`: + - `Synchronizer.getAddress` (use `Synchronizer.getCurrentAddress` instead). + - `Synchronizer.getShieldedAddress` (use `Synchronizer.getLegacySaplingAddress` instead). + - `Synchronizer.getTransparentAddress` (use `Synchronizer.getLegacyTransparentAddress` instead). - `cash.z.ecc.android.sdk.type.UnifiedViewingKey` - This type had a bug where the `extpub` field actually was storing a plain transparent public key, and not the extended public key as intended. This made it incompatible diff --git a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt index e2a8d451..eaca74e9 100644 --- a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt +++ b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt @@ -76,7 +76,7 @@ class SampleCodeTest { // /////////////////////////////////////////////////// // Get Address @Test fun getAddress() = runBlocking { - val address = synchronizer.getAddress() + val address = synchronizer.getCurrentAddress() assertFalse(address.isBlank()) log("Address: $address") } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 04792d1e..f4df9d0b 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -5,11 +5,15 @@ import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed +import cash.z.ecc.android.sdk.Initializer +import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext import cash.z.ecc.android.sdk.demoapp.util.fromResources +import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import kotlinx.coroutines.launch @@ -21,6 +25,7 @@ import kotlinx.coroutines.runBlocking */ class GetAddressFragment : BaseDemoFragment() { + private lateinit var synchronizer: Synchronizer private lateinit var viewingKey: UnifiedFullViewingKey private lateinit var seed: ByteArray @@ -36,28 +41,46 @@ class GetAddressFragment : BaseDemoFragment() { // have the seed stored seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() - // the derivation tool can be used for generating keys and addresses + // converting seed into viewingKey viewingKey = runBlocking { DerivationTool.deriveUnifiedFullViewingKeys( seed, ZcashNetwork.fromResources(requireApplicationContext()) ).first() } - } - private fun displayAddress() { - // a full fledged app would just get the address from the synchronizer - viewLifecycleOwner.lifecycleScope.launchWhenStarted { - val uaddress = DerivationTool.deriveUnifiedAddress( - seed, - ZcashNetwork.fromResources(requireApplicationContext()) - ) - binding.textInfo.text = "address:\n$uaddress" + // using the ViewingKey to initialize + runBlocking { + Initializer.new(requireApplicationContext(), null) { + val network = ZcashNetwork.fromResources(requireApplicationContext()) + it.newWallet( + viewingKey, + network = network, + lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network) + ) + } + }.let { initializer -> + synchronizer = Synchronizer.newBlocking(initializer) } } - // TODO [#677]: Show an example with the synchronizer - // TODO [#677]: https://github.com/zcash/zcash-android-wallet-sdk/issues/677 + private fun displayAddress() { + viewLifecycleOwner.lifecycleScope.launchWhenStarted { + val uaddress = synchronizer.getCurrentAddress() + val sapling = synchronizer.getLegacySaplingAddress() + val transparent = synchronizer.getLegacyTransparentAddress() + binding.textInfo.text = """ + Unified Address: + $uaddress + + Legacy Sapling: + $sapling + + Legacy transparent: + $transparent + """.trimIndent() + } + } // // Android Lifecycle overrides diff --git a/sdk-lib/Cargo.lock b/sdk-lib/Cargo.lock index 72c99df2..dae06a16 100644 --- a/sdk-lib/Cargo.lock +++ b/sdk-lib/Cargo.lock @@ -2028,6 +2028,7 @@ dependencies = [ "schemer", "secp256k1", "secrecy", + "zcash_address", "zcash_client_backend", "zcash_client_sqlite", "zcash_primitives", diff --git a/sdk-lib/Cargo.toml b/sdk-lib/Cargo.toml index fb17148d..523ac2cf 100644 --- a/sdk-lib/Cargo.toml +++ b/sdk-lib/Cargo.toml @@ -21,6 +21,7 @@ log-panics = "2.0.0" schemer = "0.2" secp256k1 = "0.21" secrecy = "0.8" +zcash_address = "0.1" zcash_client_backend = { version = "0.5", features = ["transparent-inputs", "unstable"] } zcash_client_sqlite = { version = "0.3", features = ["transparent-inputs", "unstable"] } zcash_primitives = "0.7" @@ -30,6 +31,7 @@ zcash_proofs = "0.7" [patch.crates-io] group = { git = "https://github.com/zkcrypto/group.git", rev = "a7f3ceb2373e9fe536996f7b4d55c797f3e667f0" } orchard = { git = 'https://github.com/zcash/orchard.git', rev='f206b3f5d4e31bba75d03d9d03d5fa25825a9384' } +zcash_address = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } zcash_note_encryption = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffadf5a0120a74d70d281974d079ccd58c600' } @@ -38,6 +40,7 @@ zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffad ## Uncomment this to test librustzcash changes locally #[patch.crates-io] +#zcash_address = { path = '../../clones/librustzcash/components/zcash_address' } #zcash_client_backend = { path = '../../clones/librustzcash/zcash_client_backend' } #zcash_client_sqlite = { path = '../../clones/librustzcash/zcash_client_sqlite' } #zcash_primitives = { path = '../../clones/librustzcash/zcash_primitives' } @@ -45,6 +48,7 @@ zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='774ffad ## Uncomment this to test someone else's librustzcash changes in a branch #[patch.crates-io] +#zcash_address = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } #zcash_client_backend = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } #zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } #zcash_primitives = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index e23124fc..93b5ce05 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -357,7 +357,7 @@ class SdkSynchronizer internal constructor( suspend fun refreshUtxos() { twig("refreshing utxos", -1) - refreshUtxos(getTransparentAddress()) + refreshUtxos(getLegacyTransparentAddress()) } /** @@ -379,7 +379,7 @@ class SdkSynchronizer internal constructor( suspend fun refreshTransparentBalance() { twig("refreshing transparent balance") - _transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress()) + _transparentBalances.value = processor.getUtxoCacheBalance(getLegacyTransparentAddress()) } suspend fun isValidAddress(address: String): Boolean { @@ -637,20 +637,22 @@ class SdkSynchronizer internal constructor( override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId) - // TODO(str4d): Rename this to getCurrentAddress (and remove/add in changelog). /** * Returns the current Unified Address for this account. */ - override suspend fun getAddress(accountId: Int): String = getShieldedAddress(accountId) + override suspend fun getCurrentAddress(accountId: Int): String = + processor.getCurrentAddress(accountId) - override suspend fun getShieldedAddress(accountId: Int): String = - processor.getShieldedAddress(accountId) + /** + * Returns the legacy Sapling address corresponding to the current Unified Address for this account. + */ + override suspend fun getLegacySaplingAddress(accountId: Int): String = + processor.getLegacySaplingAddress(accountId) - // TODO(str4d): Change this to do the right thing. /** * Returns the legacy transparent address corresponding to the current Unified Address for this account. */ - override suspend fun getTransparentAddress(accountId: Int): String = + override suspend fun getLegacyTransparentAddress(accountId: Int): String = processor.getTransparentAddress(accountId) override fun sendToAddress( @@ -692,7 +694,7 @@ class SdkSynchronizer internal constructor( val tAddr = DerivationTool.deriveTransparentAddressFromAccountPrivateKey(transparentAccountPrivateKey, network) val tBalance = processor.getUtxoCacheBalance(tAddr) - val zAddr = getAddress(0) + val zAddr = getCurrentAddress(0) // Emit the placeholder transaction, then switch to monitoring the database txManager.initSpend(tBalance.available, zAddr, memo, 0).let { placeHolderTx -> diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index d3f8b07b..4935e68e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -5,7 +5,6 @@ import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.model.BlockHeight -import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -188,35 +187,34 @@ interface Synchronizer { // suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey /** - * Gets the shielded address for the given account. This is syntactic sugar for - * [getShieldedAddress] because we use z-addrs by default. + * Gets the current unified address for the given account. * * @param accountId the optional accountId whose address is of interest. By default, the first * account is used. * - * @return the shielded address for the given account. + * @return the current unified address for the given account. */ - suspend fun getAddress(accountId: Int = 0) = getShieldedAddress(accountId) + suspend fun getCurrentAddress(accountId: Int = 0): String /** - * Gets the shielded address for the given account. + * Gets the legacy Sapling address corresponding to the current unified address for the given account. * * @param accountId the optional accountId whose address is of interest. By default, the first * account is used. * - * @return the shielded address for the given account. + * @return a legacy Sapling address for the given account. */ - suspend fun getShieldedAddress(accountId: Int = 0): String + suspend fun getLegacySaplingAddress(accountId: Int = 0): String /** - * Gets the transparent address for the given account. + * Gets the legacy transparent address corresponding to the current unified address for the given account. * * @param accountId the optional accountId whose address is of interest. By default, the first * account is used. * - * @return the address for the given account. + * @return a legacy transparent address for the given account. */ - suspend fun getTransparentAddress(accountId: Int = 0): String + suspend fun getLegacyTransparentAddress(accountId: Int = 0): String /** * Sends zatoshi. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index e143c967..427d26a2 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt @@ -1023,17 +1023,34 @@ class CompactBlockProcessor internal constructor( rustBackend.createAccount(seed) /** - * Get address corresponding to the given account for this wallet. + * Get the current unified address for the given wallet account. * - * @return the address of this wallet. + * @return the current unified address of this account. */ - suspend fun getShieldedAddress(accountId: Int = 0) = - repository.getAccount(accountId)?.rawShieldedAddress - ?: throw InitializerException.MissingAddressException("shielded") + suspend fun getCurrentAddress(accountId: Int = 0) = + rustBackend.getCurrentAddress(accountId) + /** + * Get the legacy Sapling address corresponding to the current unified address for the given wallet account. + * + * @return a Sapling address. + */ + suspend fun getLegacySaplingAddress(accountId: Int = 0) = + rustBackend.getSaplingReceiver( + rustBackend.getCurrentAddress(accountId) + ) + ?: throw InitializerException.MissingAddressException("legacy Sapling") + + /** + * Get the legacy transparent address corresponding to the current unified address for the given wallet account. + * + * @return a transparent address. + */ suspend fun getTransparentAddress(accountId: Int = 0) = - repository.getAccount(accountId)?.rawTransparentAddress - ?: throw InitializerException.MissingAddressException("transparent") + rustBackend.getTransparentReceiver( + rustBackend.getCurrentAddress(accountId) + ) + ?: throw InitializerException.MissingAddressException("legacy transparent") /** * Calculates the latest balance info. Defaults to the first account. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt index 2ba4ed94..2500ec8d 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt @@ -12,10 +12,5 @@ data class Account( val account: Int? = 0, @ColumnInfo(name = "ufvk") - val unifiedFullViewingKey: String? = "", - - val address: String? = "", - - @ColumnInfo(name = "transparent_address") - val transparentAddress: String? = "" + val unifiedFullViewingKey: String? = "" ) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt index 4937bab5..8d705a33 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt @@ -231,17 +231,6 @@ interface SentDao { interface AccountDao { @Query("SELECT COUNT(account) FROM accounts") suspend fun count(): Int - - @Query( - """ - SELECT account AS accountId, - transparent_address AS rawTransparentAddress, - address AS rawShieldedAddress - FROM accounts - WHERE account = :id - """ - ) - suspend fun findAccountById(id: Int): UnifiedAddressAccount? } /** diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt index 4e3abf46..75e3f5f7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt @@ -94,8 +94,6 @@ internal class PagedTransactionRepository private constructor( override suspend fun count() = transactions.count() - override suspend fun getAccount(accountId: Int) = accounts.findAccountById(accountId) - override suspend fun getAccountCount() = accounts.count() /** diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt index 2a69a817..a25fe814 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt @@ -85,8 +85,6 @@ interface TransactionRepository { suspend fun count(): Int - suspend fun getAccount(accountId: Int): UnifiedAddressAccount? - suspend fun getAccountCount(): Int // diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index d195a0d0..d75ffdce 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt @@ -109,12 +109,9 @@ internal class RustBackend private constructor( ) } - override suspend fun getTransparentAddress(account: Int, index: Int): String { - throw NotImplementedError( - "TODO: implement this at the zcash_client_sqlite level. But for now, use " + - "DerivationTool, instead to derive addresses from seeds" - ) - } + override fun getTransparentReceiver(ua: String) = getTransparentReceiverForUnifiedAddress(ua) + + override fun getSaplingReceiver(ua: String) = getSaplingReceiverForUnifiedAddress(ua) override suspend fun getBalance(account: Int): Zatoshi { val longValue = withContext(SdkDispatchers.DATABASE_IO) { @@ -426,6 +423,12 @@ internal class RustBackend private constructor( networkId: Int ): String + @JvmStatic + private external fun getTransparentReceiverForUnifiedAddress(ua: String): String? + + @JvmStatic + private external fun getSaplingReceiverForUnifiedAddress(ua: String): String? + @JvmStatic private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index 7bcc13d7..ea35744f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt @@ -53,7 +53,9 @@ internal interface RustBackendWelding { suspend fun getCurrentAddress(account: Int = 0): String - suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String + fun getTransparentReceiver(ua: String): String? + + fun getSaplingReceiver(ua: String): String? suspend fun getBalance(account: Int = 0): Zatoshi diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index 61f74823..c9c7c6a8 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/sdk-lib/src/main/rust/lib.rs @@ -21,9 +21,10 @@ use log::Level; use schemer::MigratorError; use secp256k1::PublicKey; use secrecy::SecretVec; +use zcash_address::{ToAddress, ZcashAddress}; use zcash_client_backend::keys::UnifiedSpendingKey; use zcash_client_backend::{ - address::RecipientAddress, + address::{RecipientAddress, UnifiedAddress}, data_api::{ chain::{scan_cached_blocks, validate_chain}, error::Error, @@ -490,6 +491,91 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getCurrentA unwrap_exc_or(&env, res, ptr::null_mut()) } +struct UnifiedAddressParser(UnifiedAddress); + +impl zcash_address::TryFromRawAddress for UnifiedAddressParser { + type Error = failure::Error; + + fn try_from_raw_unified( + data: zcash_address::unified::Address, + ) -> Result> { + data.try_into() + .map(UnifiedAddressParser) + .map_err(|e| format_err!("Invalid Unified Address: {}", e).into()) + } +} + +/// Returns the transparent receiver within the given Unified Address, if any. +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getTransparentReceiverForUnifiedAddress( + env: JNIEnv<'_>, + _: JClass<'_>, + ua: JString<'_>, +) -> jstring { + let res = panic::catch_unwind(|| { + let ua_str = utils::java_string_to_rust(&env, ua); + + let (network, ua) = match ZcashAddress::try_from_encoded(&ua_str) { + Ok(addr) => addr + .convert::<(_, UnifiedAddressParser)>() + .map_err(|e| format_err!("Not a Unified Address: {}", e)), + Err(e) => return Err(format_err!("Invalid Zcash address: {}", e)), + }?; + + if let Some(taddr) = ua.0.transparent() { + let taddr = match taddr { + TransparentAddress::PublicKey(data) => { + ZcashAddress::from_transparent_p2pkh(network, *data) + } + TransparentAddress::Script(data) => { + ZcashAddress::from_transparent_p2sh(network, *data) + } + }; + + let output = env + .new_string(taddr.encode()) + .expect("Couldn't create Java string!"); + Ok(output.into_inner()) + } else { + Err(format_err!( + "Unified Address doesn't contain a transparent receiver" + )) + } + }); + unwrap_exc_or(&env, res, ptr::null_mut()) +} + +/// Returns the Sapling receiver within the given Unified Address, if any. +#[no_mangle] +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getSaplingReceiverForUnifiedAddress( + env: JNIEnv<'_>, + _: JClass<'_>, + ua: JString<'_>, +) -> jstring { + let res = panic::catch_unwind(|| { + let ua_str = utils::java_string_to_rust(&env, ua); + + let (network, ua) = match ZcashAddress::try_from_encoded(&ua_str) { + Ok(addr) => addr + .convert::<(_, UnifiedAddressParser)>() + .map_err(|e| format_err!("Not a Unified Address: {}", e)), + Err(e) => return Err(format_err!("Invalid Zcash address: {}", e)), + }?; + + if let Some(addr) = ua.0.sapling() { + let output = env + .new_string(ZcashAddress::from_sapling(network, addr.to_bytes()).encode()) + .expect("Couldn't create Java string!"); + Ok(output.into_inner()) + } else { + Err(format_err!( + "Unified Address doesn't contain a Sapling receiver" + )) + } + }); + unwrap_exc_or(&env, res, ptr::null_mut()) +} + #[no_mangle] pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_isValidShieldedAddress( env: JNIEnv<'_>,