Replace `Synchronizer.getAddress()` with `Synchronizer.getCurrentAddress()`

The SDK now exposes UAs primarily, with additional methods for obtaining
corresponding Sapling and transparent addresses for legacy compatibility.

The `Account` DAO is also fixed to use the migrated `accounts` table format.

The demo app now shows the current UA and the legacy Sapling and transparent
addresses.

Closes zcash/zcash-android-wallet-sdk#677.
This commit is contained in:
Carter Jernigan 2022-10-06 13:40:49 -04:00 committed by Carter Jernigan
parent f69cacb9e6
commit 597cc43886
15 changed files with 192 additions and 69 deletions

View File

@ -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

View File

@ -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")
}

View File

@ -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<FragmentGetAddressBinding>() {
private lateinit var synchronizer: Synchronizer
private lateinit var viewingKey: UnifiedFullViewingKey
private lateinit var seed: ByteArray
@ -36,28 +41,46 @@ class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
// 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

1
sdk-lib/Cargo.lock generated
View File

@ -2028,6 +2028,7 @@ dependencies = [
"schemer",
"secp256k1",
"secrecy",
"zcash_address",
"zcash_client_backend",
"zcash_client_sqlite",
"zcash_primitives",

View File

@ -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" }

View File

@ -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 ->

View File

@ -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.

View File

@ -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.

View File

@ -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? = ""
)

View File

@ -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?
}
/**

View File

@ -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()
/**

View File

@ -85,8 +85,6 @@ interface TransactionRepository {
suspend fun count(): Int
suspend fun getAccount(accountId: Int): UnifiedAddressAccount?
suspend fun getAccountCount(): Int
//

View File

@ -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

View File

@ -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

View File

@ -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<Self, zcash_address::ConversionError<Self::Error>> {
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<'_>,