diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc5e382..5436269f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,41 @@ Change Log ========== -Upcoming +## Unreleased + +### Added +- `cash.z.ecc.android.sdk.type.UnifiedFullViewingKey`, representing a Unified Full Viewing + Key as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-full-incoming-viewing-keys). + - TODO: Actually encode per ZIP 316. +- `cash.z.ecc.android.sdk.tool`: + - `DerivationTool.deriveUnifiedAddress` + - `DerivationTool.deriveUnifiedFullViewingKeys` + - `DerivationTool.validateUnifiedFullViewingKey` + - Still unimplemented. + +### Changed +- The following methods now take or return `UnifiedFullViewingKey` instead of + `UnifiedViewingKey`: + - `cash.z.ecc.android.sdk`: + - `Initializer.Config.addViewingKey` + - `Initializer.Config.importWallet` + - `Initializer.Config.newWallet` + - `Initializer.Config.setViewingKeys` + +### Removed +- `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 + with ZIP 316. +- `cash.z.ecc.android.sdk.tool`: + - `DerivationTool.deriveShieldedAddress` + - TODO: Do we still need to be able to derive Sapling shielded addresses for legacy + support? Currently removed because `UnifiedFullViewingKey` doesn't expose the + Sapling FVK on the Kotlin side (unlike the previous `UnifiedViewingKey`). + - `DerivationTool.deriveUnifiedViewingKeys` + - `DerivationTool.validateUnifiedViewingKey` + +1.9.0-beta01 ------------------------------------ - Split `ZcashNetwork` into `ZcashNetwork` and `LightWalletEndpoint` to decouple network and server configuration diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index f7835308..ab0dc4d2 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt @@ -74,8 +74,8 @@ class TestWallet( val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val available get() = synchronizer.saplingBalances.value?.available - val shieldedAddress = - runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } + val unifiedAddress = + runBlocking { DerivationTool.deriveUnifiedAddress(seed, network = network) } val transparentAddress = runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) } val birthdayHeight get() = synchronizer.latestBirthdayHeight 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 df754ca8..1d03c162 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 @@ -11,7 +11,7 @@ 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.ZcashNetwork import cash.z.ecc.android.sdk.tool.DerivationTool -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -21,7 +21,7 @@ import kotlinx.coroutines.runBlocking */ class GetAddressFragment : BaseDemoFragment() { - private lateinit var viewingKey: UnifiedViewingKey + private lateinit var viewingKey: UnifiedFullViewingKey private lateinit var seed: ByteArray /** @@ -37,15 +37,22 @@ class GetAddressFragment : BaseDemoFragment() { seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() // the derivation tool can be used for generating keys and addresses - viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } + 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 zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())) - val taddress = DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())) - binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress" + val uaddress = DerivationTool.deriveUnifiedAddress( + seed, + ZcashNetwork.fromResources(requireApplicationContext()) + ) + binding.textInfo.text = "address:\n$uaddress" } } @@ -72,11 +79,11 @@ class GetAddressFragment : BaseDemoFragment() { override fun onActionButtonClicked() { viewLifecycleOwner.lifecycleScope.launch { copyToClipboard( - DerivationTool.deriveShieldedAddress( - viewingKey.extfvk, + DerivationTool.deriveUnifiedAddress( + viewingKey.encoding, ZcashNetwork.fromResources(requireApplicationContext()) ), - "Shielded address copied to clipboard!" + "Unified address copied to clipboard!" ) } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index c2879267..1c372785 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt @@ -47,7 +47,12 @@ class GetBalanceFragment : BaseDemoFragment() { val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() // converting seed into viewingKey - val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } + val viewingKey = runBlocking { + DerivationTool.deriveUnifiedFullViewingKeys( + seed, + ZcashNetwork.fromResources(requireApplicationContext()) + ).first() + } // using the ViewingKey to initialize runBlocking { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 6e07a3c0..e89779e3 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -78,11 +78,11 @@ class GetPrivateKeyFragment : BaseDemoFragment() { override fun onActionButtonClicked() { lifecycleScope.launch { copyToClipboard( - DerivationTool.deriveUnifiedViewingKeys( + DerivationTool.deriveUnifiedFullViewingKeys( seed, ZcashNetwork.fromResources(requireApplicationContext()) - ).first().extpub, - "ViewingKey copied to clipboard!" + ).first().encoding, + "UnifiedFullViewingKey copied to clipboard!" ) } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index f8e8a5ed..d05dec17 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -65,7 +65,7 @@ class ListTransactionsFragment : BaseDemoFragment mnemonics.toSeed(seedPhrase.toCharArray()) }.map { seed -> - DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.Mainnet) + DerivationTool.deriveUnifiedAddress(seed, ZcashNetwork.Mainnet) }.collect { address -> println("xrxrx2\t$address") assertTrue(address.startsWith("zs1")) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index e296dfce..c5919b46 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt @@ -74,8 +74,8 @@ class TestWallet( val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService) val available get() = synchronizer.saplingBalances.value?.available - val shieldedAddress = - runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) } + val unifiedAddress = + runBlocking { DerivationTool.deriveUnifiedAddress(seed, network = network) } val transparentAddress = runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) } val birthdayHeight get() = synchronizer.latestBirthdayHeight diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt index b3b4882d..5e76adfc 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt @@ -14,7 +14,7 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.tool.CheckpointTool import cash.z.ecc.android.sdk.tool.DerivationTool -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File @@ -29,7 +29,7 @@ class Initializer private constructor( val network: ZcashNetwork, val alias: String, val lightWalletEndpoint: LightWalletEndpoint, - val viewingKeys: List, + val viewingKeys: List, val overwriteVks: Boolean, internal val checkpoint: Checkpoint ) { @@ -37,7 +37,7 @@ class Initializer private constructor( suspend fun erase() = erase(context, network, alias) class Config private constructor( - val viewingKeys: MutableList = mutableListOf(), + val viewingKeys: MutableList = mutableListOf(), var alias: String = ZcashSdk.DEFAULT_ALIAS ) { var birthdayHeight: BlockHeight? = null @@ -119,13 +119,13 @@ class Initializer private constructor( * probably has serious bugs. */ fun setViewingKeys( - vararg unifiedViewingKeys: UnifiedViewingKey, + vararg unifiedFullViewingKeys: UnifiedFullViewingKey, overwrite: Boolean = false ): Config = apply { overwriteVks = overwrite viewingKeys.apply { clear() - addAll(unifiedViewingKeys) + addAll(unifiedFullViewingKeys) } } @@ -138,7 +138,7 @@ class Initializer private constructor( * is not currently well supported. Consider it an alpha-preview feature that might work but * probably has serious bugs. */ - fun addViewingKey(unifiedFullViewingKey: UnifiedViewingKey): Config = apply { + fun addViewingKey(unifiedFullViewingKey: UnifiedFullViewingKey): Config = apply { viewingKeys.add(unifiedFullViewingKey) } @@ -175,7 +175,7 @@ class Initializer private constructor( alias: String = ZcashSdk.DEFAULT_ALIAS ): Config = importWallet( - DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0], + DerivationTool.deriveUnifiedFullViewingKeys(seed, network = network)[0], birthday, network, lightWalletEndpoint, @@ -185,8 +185,9 @@ class Initializer private constructor( /** * Default function for importing a wallet. */ + @Suppress("LongParameterList") fun importWallet( - viewingKey: UnifiedViewingKey, + viewingKey: UnifiedFullViewingKey, birthday: BlockHeight?, network: ZcashNetwork, lightWalletEndpoint: LightWalletEndpoint, @@ -207,7 +208,7 @@ class Initializer private constructor( lightWalletEndpoint: LightWalletEndpoint, alias: String = ZcashSdk.DEFAULT_ALIAS ): Config = newWallet( - DerivationTool.deriveUnifiedViewingKeys(seed, network)[0], + DerivationTool.deriveUnifiedFullViewingKeys(seed, network)[0], network, lightWalletEndpoint, alias @@ -217,7 +218,7 @@ class Initializer private constructor( * Default function for creating a new wallet. */ fun newWallet( - viewingKey: UnifiedViewingKey, + viewingKey: UnifiedFullViewingKey, network: ZcashNetwork, lightWalletEndpoint: LightWalletEndpoint, alias: String = ZcashSdk.DEFAULT_ALIAS @@ -232,6 +233,7 @@ class Initializer private constructor( * Convenience method for setting thew viewingKeys from a given seed. This is the same as * calling `setViewingKeys` with the keys that match this seed. */ + @Suppress("SpreadOperator") suspend fun setSeed( seed: ByteArray, network: ZcashNetwork, @@ -239,7 +241,7 @@ class Initializer private constructor( ): Config = apply { setViewingKeys( - *DerivationTool.deriveUnifiedViewingKeys( + *DerivationTool.deriveUnifiedFullViewingKeys( seed, network, numberOfAccounts @@ -288,7 +290,7 @@ class Initializer private constructor( " have been set on this Initializer." } viewingKeys.forEach { - DerivationTool.validateUnifiedViewingKey(it) + DerivationTool.validateUnifiedFullViewingKey(it) } } 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 9d845f82..5be15328 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 @@ -17,7 +17,7 @@ import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext @@ -111,13 +111,14 @@ internal class PagedTransactionRepository private constructor( // TODO: convert this into a wallet repository rather than "transaction repository" companion object { - internal suspend fun new( + @Suppress("LongParameterList") + suspend fun new( appContext: Context, zcashNetwork: ZcashNetwork, pageSize: Int = 10, rustBackend: RustBackend, birthday: Checkpoint, - viewingKeys: List, + viewingKeys: List, overwriteVks: Boolean = false ): PagedTransactionRepository { initMissingDatabases(rustBackend, birthday, viewingKeys) @@ -159,7 +160,7 @@ internal class PagedTransactionRepository private constructor( private suspend fun initMissingDatabases( rustBackend: RustBackend, birthday: Checkpoint, - viewingKeys: List + viewingKeys: List ) { maybeCreateDataDb(rustBackend) maybeInitBlocksTable(rustBackend, birthday) @@ -199,7 +200,7 @@ internal class PagedTransactionRepository private constructor( */ private suspend fun maybeInitAccountsTable( rustBackend: RustBackend, - viewingKeys: List + viewingKeys: List ) { // TODO: consider converting these to typed exceptions in the welding layer tryWarn( @@ -214,7 +215,7 @@ internal class PagedTransactionRepository private constructor( private suspend fun applyKeyMigrations( rustBackend: RustBackend, overwriteVks: Boolean, - viewingKeys: List + viewingKeys: List ) { if (overwriteVks) { twig("applying key migrations . . .") 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 1515d579..5ee31899 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 @@ -11,7 +11,7 @@ import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.tool.DerivationTool -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey import kotlinx.coroutines.withContext import java.io.File @@ -50,18 +50,13 @@ internal class RustBackend private constructor( ) } - override suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean { - val extfvks = Array(keys.size) { "" } - val extpubs = Array(keys.size) { "" } - keys.forEachIndexed { i, key -> - extfvks[i] = key.extfvk - extpubs[i] = key.extpub - } + override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey): Boolean { + val ufvks = Array(keys.size) { keys[it].encoding } + return withContext(SdkDispatchers.DATABASE_IO) { initAccountsTableWithKeys( pathDataDb, - extfvks, - extpubs, + ufvks, networkId = network.id ) } @@ -70,8 +65,8 @@ internal class RustBackend private constructor( override suspend fun initAccountsTable( seed: ByteArray, numberOfAccounts: Int - ): Array { - return DerivationTool.deriveUnifiedViewingKeys(seed, network, numberOfAccounts).apply { + ): Array { + return DerivationTool.deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts).apply { initAccountsTable(*this) } } @@ -387,8 +382,7 @@ internal class RustBackend private constructor( @JvmStatic private external fun initAccountsTableWithKeys( dbDataPath: String, - extfvk: Array, - extpub: Array, + ufvks: Array, 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 6b6bffe5..b9951fa2 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 @@ -5,7 +5,7 @@ import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey /** * Contract defining the exposed capabilities of the Rust backend. @@ -34,9 +34,9 @@ internal interface RustBackendWelding { suspend fun decryptAndStoreTransaction(tx: ByteArray) - suspend fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array + suspend fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array - suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean + suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey): Boolean suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean @@ -88,12 +88,12 @@ internal interface RustBackendWelding { // Implemented by `DerivationTool` interface Derivation { - suspend fun deriveShieldedAddress( + suspend fun deriveUnifiedAddress( viewingKey: String, network: ZcashNetwork ): String - suspend fun deriveShieldedAddress( + suspend fun deriveUnifiedAddress( seed: ByteArray, network: ZcashNetwork, accountIndex: Int = 0 @@ -134,10 +134,10 @@ internal interface RustBackendWelding { network: ZcashNetwork ): String - suspend fun deriveUnifiedViewingKeys( + suspend fun deriveUnifiedFullViewingKeys( seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int = 1 - ): Array + ): Array } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt index 21596a70..130a0c17 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt @@ -3,25 +3,25 @@ package cash.z.ecc.android.sdk.tool import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.android.sdk.type.UnifiedViewingKey +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey class DerivationTool { companion object : RustBackendWelding.Derivation { /** - * Given a seed and a number of accounts, return the associated viewing keys. + * Given a seed and a number of accounts, return the associated Unified Full Viewing Keys. * * @param seed the seed from which to derive viewing keys. * @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully * supported so the default value of 1 is recommended. * - * @return the viewing keys that correspond to the seed, formatted as Strings. + * @return the UFVKs derived from the seed, encoded as Strings. */ - override suspend fun deriveUnifiedViewingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array = + override suspend fun deriveUnifiedFullViewingKeys(seed: ByteArray, network: ZcashNetwork, numberOfAccounts: Int): Array = withRustBackendLoaded { - deriveUnifiedViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map { - UnifiedViewingKey(it[0], it[1]) + deriveUnifiedFullViewingKeysFromSeed(seed, numberOfAccounts, networkId = network.id).map { + UnifiedFullViewingKey(it) }.toTypedArray() } @@ -51,7 +51,7 @@ class DerivationTool { } /** - * Given a seed and account index, return the associated address. + * Given a seed and account index, return the associated Unified Address. * * @param seed the seed from which to derive the address. * @param accountIndex the index of the account to use for deriving the address. Multiple @@ -59,21 +59,21 @@ class DerivationTool { * * @return the address that corresponds to the seed and account index. */ - override suspend fun deriveShieldedAddress(seed: ByteArray, network: ZcashNetwork, accountIndex: Int): String = + override suspend fun deriveUnifiedAddress(seed: ByteArray, network: ZcashNetwork, accountIndex: Int): String = withRustBackendLoaded { - deriveShieldedAddressFromSeed(seed, accountIndex, networkId = network.id) + deriveUnifiedAddressFromSeed(seed, accountIndex, networkId = network.id) } /** - * Given a viewing key string, return the associated address. + * Given a Unified Full Viewing Key string, return the associated Unified Address. * * @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to * a specific account so no account index is required. * * @return the address that corresponds to the viewing key. */ - override suspend fun deriveShieldedAddress(viewingKey: String, network: ZcashNetwork): String = withRustBackendLoaded { - deriveShieldedAddressFromViewingKey(viewingKey, networkId = network.id) + override suspend fun deriveUnifiedAddress(viewingKey: String, network: ZcashNetwork): String = withRustBackendLoaded { + deriveUnifiedAddressFromViewingKey(viewingKey, networkId = network.id) } // WIP probably shouldn't be used just yet. Why? @@ -95,7 +95,8 @@ class DerivationTool { deriveTransparentSecretKeyFromSeed(seed, account, index, networkId = network.id) } - fun validateUnifiedViewingKey(viewingKey: UnifiedViewingKey, networkId: Int = ZcashNetwork.Mainnet.id) { + @Suppress("UnusedPrivateMember") + fun validateUnifiedFullViewingKey(viewingKey: UnifiedFullViewingKey, networkId: Int = ZcashNetwork.Mainnet.id) { // TODO } @@ -121,24 +122,24 @@ class DerivationTool { ): Array @JvmStatic - private external fun deriveUnifiedViewingKeysFromSeed( + private external fun deriveUnifiedFullViewingKeysFromSeed( seed: ByteArray, numberOfAccounts: Int, networkId: Int - ): Array> + ): Array @JvmStatic private external fun deriveExtendedFullViewingKey(spendingKey: String, networkId: Int): String @JvmStatic - private external fun deriveShieldedAddressFromSeed( + private external fun deriveUnifiedAddressFromSeed( seed: ByteArray, accountIndex: Int, networkId: Int ): String @JvmStatic - private external fun deriveShieldedAddressFromViewingKey(key: String, networkId: Int): String + private external fun deriveUnifiedAddressFromViewingKey(key: String, networkId: Int): String @JvmStatic private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int, networkId: Int): String diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt index 28623364..198dfed7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt @@ -1,16 +1,16 @@ package cash.z.ecc.android.sdk.type /** - * A grouping of keys that correspond to a single wallet account but do not have spend authority. + * A [ZIP 316] Unified Full Viewing Key, corresponding to a single wallet account. * - * @param extfvk the extended full viewing key which provides the ability to see inbound and - * outbound shielded transactions. It can also be used to derive a z-addr. - * @param extpub the extended public key which provides the ability to see transparent - * transactions. It can also be used to derive a t-addr. + * A `UnifiedFullViewingKey` has the authority to view transactions for an account, but + * does not have spend authority. It can be used to derive a [UnifiedAddress] for the + * account. + * + * @param[encoding] The string encoding of the UFVK. */ -data class UnifiedViewingKey( - val extfvk: String = "", - val extpub: String = "" +data class UnifiedFullViewingKey( + val encoding: String = "" ) data class UnifiedAddressAccount( diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index 21836ac3..5c20c97c 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/sdk-lib/src/main/rust/lib.rs @@ -26,14 +26,12 @@ use zcash_client_backend::{ WalletRead, }, encoding::{ - decode_extended_full_viewing_key, decode_extended_spending_key, - encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address, - AddressCodec, + decode_extended_spending_key, encode_extended_full_viewing_key, + encode_extended_spending_key, encode_payment_address, AddressCodec, }, keys::{ - derive_public_key_from_seed, derive_secret_key_from_seed, - derive_transparent_address_from_public_key, derive_transparent_address_from_secret_key, - spending_key, Wif, + derive_secret_key_from_seed, derive_transparent_address_from_public_key, + derive_transparent_address_from_secret_key, spending_key, Wif, }, wallet::{AccountId, OvkPolicy, WalletTransparentOutput}, }; @@ -127,42 +125,39 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount env: JNIEnv<'_>, _: JClass<'_>, db_data: JString<'_>, - extfvks_arr: jobjectArray, - extpubs_arr: jobjectArray, + ufvks_arr: jobjectArray, network_id: jint, ) -> jboolean { let res = panic::catch_unwind(|| { let network = parse_network(network_id as u32)?; let db_data = wallet_db(&env, network, db_data)?; // TODO: avoid all this unwrapping and also surface errors, better - let count = env.get_array_length(extfvks_arr).unwrap(); - let extfvks = (0..count) - .map(|i| env.get_object_array_element(extfvks_arr, i)) + let count = env.get_array_length(ufvks_arr).unwrap(); + let ufvks = (0..count) + .map(|i| env.get_object_array_element(ufvks_arr, i)) .map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into())) - .map(|vkstr| { - decode_extended_full_viewing_key( - network.hrp_sapling_extended_full_viewing_key(), - &vkstr, - ) - .map_err(|err| format_err!("Invalid bech32: {}", err)) - .and_then(|extfvk| - extfvk.ok_or_else(|| { + .map(|ufvkstr| { + // TODO: replace with `zcash_address::unified::Ufvk` + utils::fake_ufvk_decode(&ufvkstr).ok_or_else(|| { let (network_name, other) = if network == TestNetwork { ("testnet", "mainnet") } else { ("mainnet", "testnet") }; format_err!("Error: Wrong network! Unable to decode viewing key for {}. Check whether this is a key for {}.", network_name, other) - })) + }) }) .collect::, _>>()?; - let taddrs: Vec<_> = (0..count) - .map(|i| env.get_object_array_element(extpubs_arr, i)) - .map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into())) - .map(|extpub_str| PublicKey::from_str(&extpub_str).unwrap()) - .map(|pk| derive_transparent_address_from_public_key(&pk)) - .collect::>(); + let (taddrs, extfvks): (Vec<_>, Vec<_>) = ufvks + .into_iter() + .map(|(extpub, extfvk)| { + ( + derive_transparent_address_from_public_key(&extpub.public_key), + extfvk, + ) + }) + .unzip(); match init_accounts_table(&db_data, &extfvks[..], &taddrs[..]) { Ok(()) => Ok(JNI_TRUE), @@ -210,7 +205,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveE } #[no_mangle] -pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedViewingKeysFromSeed( +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedFullViewingKeysFromSeed( env: JNIEnv<'_>, _: JClass<'_>, seed: jbyteArray, @@ -226,32 +221,25 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveU return Err(format_err!("accounts argument must be greater than zero")); }; - let extfvks: Vec<_> = (0..accounts) + let ufvks: Vec<_> = (0..accounts) .map(|account| { - encode_extended_full_viewing_key( - network.hrp_sapling_extended_full_viewing_key(), - &ExtendedFullViewingKey::from(&spending_key( - &seed, - network.coin_type(), - AccountId(account), - )), - ) + let sapling = ExtendedFullViewingKey::from(&spending_key( + &seed, + network.coin_type(), + AccountId(account), + )); + let p2pkh = + utils::p2pkh_full_viewing_key(&network, &seed, AccountId(account)).unwrap(); + // TODO: Replace with `zcash_address::unified::Ufvk` + utils::fake_ufvk_encode(&p2pkh, &sapling) }) .collect(); - let extpubs: Vec<_> = (0..accounts) - .map(|account| { - let pk = - derive_public_key_from_seed(&network, &seed, AccountId(account), 0).unwrap(); - hex::encode(&pk.serialize()) - }) - .collect(); - - Ok(utils::rust_vec_to_java_2d( + Ok(utils::rust_vec_to_java( &env, - extfvks, - extpubs, - |env, extfvkstr| env.new_string(extfvkstr), + ufvks, + "java/lang/String", + |env, ufvk| env.new_string(ufvk), |env| env.new_string(""), )) }); @@ -259,7 +247,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveU } #[no_mangle] -pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromSeed( +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedAddressFromSeed( env: JNIEnv<'_>, _: JClass<'_>, seed: jbyteArray, @@ -275,11 +263,16 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveS return Err(format_err!("accountIndex argument must be positive")); }; - let address = spending_key(&seed, network.coin_type(), AccountId(account_index)) + let (di, sapling) = spending_key(&seed, network.coin_type(), AccountId(account_index)) .default_address() - .unwrap() - .1; - let address_str = encode_payment_address(network.hrp_sapling_payment_address(), &address); + .unwrap(); + let p2pkh = utils::p2pkh_addr( + utils::p2pkh_full_viewing_key(&network, &seed, AccountId(account_index)).unwrap(), + di, + ) + .unwrap(); + // TODO: replace this with `zcash_address::unified::Address`. + let address_str = utils::fake_ua_encode(&p2pkh, &sapling); let output = env .new_string(address_str) .expect("Couldn't create Java string!"); @@ -289,33 +282,31 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveS } #[no_mangle] -pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveShieldedAddressFromViewingKey( +pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveUnifiedAddressFromViewingKey( env: JNIEnv<'_>, _: JClass<'_>, - extfvk_string: JString<'_>, - network_id: jint, + ufvk_string: JString<'_>, + _network_id: jint, ) -> jstring { let res = panic::catch_unwind(|| { - let network = parse_network(network_id as u32)?; - let extfvk_string = utils::java_string_to_rust(&env, extfvk_string); - let extfvk = match decode_extended_full_viewing_key( - network.hrp_sapling_extended_full_viewing_key(), - &extfvk_string, - ) { - Ok(Some(extfvk)) => extfvk, - Ok(None) => { - return Err(format_err!("Failed to parse viewing key string in order to derive the address. Deriving a viewing key from the string returned no results. Encoding was valid but type was incorrect.")); - } - Err(e) => { + //let network = parse_network(network_id as u32)?; + let ufvk_string = utils::java_string_to_rust(&env, ufvk_string); + let ufvk = match utils::fake_ufvk_decode(&ufvk_string) { + Some(ufvk) => ufvk, + None => { return Err(format_err!( - "Error while deriving viewing key from string input: {}", - e + "Error while deriving viewing key from string input" )); } }; - let address = extfvk.default_address().unwrap().1; - let address_str = encode_payment_address(network.hrp_sapling_payment_address(), &address); + // Derive the default Sapling payment address (like older SDKs used). + let (di, sapling) = ufvk.1.default_address().unwrap(); + // Derive the transparent address corresponding to the default Sapling diversifier + // index (matching ZIP 316). + let p2pkh = utils::p2pkh_addr(ufvk.0, di).unwrap(); + // TODO: replace this with `zcash_address::unified::Address`. + let address_str = utils::fake_ua_encode(&p2pkh, &sapling); let output = env .new_string(address_str) .expect("Couldn't create Java string!"); diff --git a/sdk-lib/src/main/rust/utils.rs b/sdk-lib/src/main/rust/utils.rs index 1d5a30d0..bfe9c3a1 100644 --- a/sdk-lib/src/main/rust/utils.rs +++ b/sdk-lib/src/main/rust/utils.rs @@ -1,3 +1,7 @@ +use hdwallet::{ + traits::{Deserialize, Serialize}, + ExtendedPrivKey, ExtendedPubKey, KeyIndex, +}; use jni::{ descriptors::Desc, errors::Result as JNIResult, @@ -5,7 +9,15 @@ use jni::{ sys::{jobjectArray, jsize}, JNIEnv, }; -use std::ops::Deref; +use zcash_client_backend::{keys::derive_transparent_address_from_public_key, wallet::AccountId}; +use zcash_primitives::{ + consensus, + legacy::TransparentAddress, + sapling::PaymentAddress, + zip32::{DiversifierIndex, ExtendedFullViewingKey}, +}; + +use std::{convert::TryInto, ops::Deref}; pub(crate) mod exception; @@ -41,39 +53,111 @@ where } // 2D array -pub(crate) fn rust_vec_to_java_2d<'a, T, V, F, G>( - env: &JNIEnv<'a>, - data1: Vec, - data2: Vec, - element_map: F, - empty_element: G, -) -> jobjectArray -where - V: Deref>, - F: Fn(&JNIEnv<'a>, T) -> JNIResult, - G: Fn(&JNIEnv<'a>) -> JNIResult, -{ - let jempty = empty_element(env).expect("Couldn't create Java string!"); - let outer = env - .new_object_array( - data1.len() as jsize, - "[Ljava/lang/String;", - *jni::objects::JObject::null(), - ) - .expect("Couldn't create Java array of string arrays!"); +//pub(crate) fn rust_vec_to_java_2d<'a, T, V, F, G>( +// env: &JNIEnv<'a>, +// data1: Vec, +// data2: Vec, +// element_map: F, +// empty_element: G, +//) -> jobjectArray +//where +// V: Deref>, +// F: Fn(&JNIEnv<'a>, T) -> JNIResult, +// G: Fn(&JNIEnv<'a>) -> JNIResult, +//{ +// let jempty = empty_element(env).expect("Couldn't create Java string!"); +// let outer = env +// .new_object_array( +// data1.len() as jsize, +// "[Ljava/lang/String;", +// *jni::objects::JObject::null(), +// ) +// .expect("Couldn't create Java array of string arrays!"); +// +// for (i, (elem1, elem2)) in data1.into_iter().zip(data2.into_iter()).enumerate() { +// let inner = env +// .new_object_array(2 as jsize, "java/lang/String", *jempty) +// .expect("Couldn't create Java array!"); +// let jelem1 = element_map(env, elem1).expect("Couldn't map element to Java!"); +// let jelem2 = element_map(env, elem2).expect("Couldn't map element to Java!"); +// env.set_object_array_element(inner, 0 as jsize, *jelem1) +// .expect("Couldn't set Java array element!"); +// env.set_object_array_element(inner, 1 as jsize, *jelem2) +// .expect("Couldn't set Java array element!"); +// env.set_object_array_element(outer, i as jsize, inner) +// .expect("Couldn't set Java array element!"); +// } +// outer +//} - for (i, (elem1, elem2)) in data1.into_iter().zip(data2.into_iter()).enumerate() { - let inner = env - .new_object_array(2 as jsize, "java/lang/String", *jempty) - .expect("Couldn't create Java array!"); - let jelem1 = element_map(env, elem1).expect("Couldn't map element to Java!"); - let jelem2 = element_map(env, elem2).expect("Couldn't map element to Java!"); - env.set_object_array_element(inner, 0 as jsize, *jelem1) - .expect("Couldn't set Java array element!"); - env.set_object_array_element(inner, 1 as jsize, *jelem2) - .expect("Couldn't set Java array element!"); - env.set_object_array_element(outer, i as jsize, inner) - .expect("Couldn't set Java array element!"); - } - outer +pub(crate) fn p2pkh_full_viewing_key( + params: &P, + seed: &[u8], + account: AccountId, +) -> Result { + let pk = ExtendedPrivKey::with_seed(&seed)?; + let private_key = pk + .derive_private_key(KeyIndex::hardened_from_normalize_index(44)?)? + .derive_private_key(KeyIndex::hardened_from_normalize_index(params.coin_type())?)? + .derive_private_key(KeyIndex::hardened_from_normalize_index(account.0)?)?; + Ok(ExtendedPubKey::from_private_key(&private_key)) } + +pub(crate) fn p2pkh_addr( + fvk: ExtendedPubKey, + index: DiversifierIndex, +) -> Result { + let pubkey = fvk + .derive_public_key(KeyIndex::Normal(0))? + .derive_public_key(KeyIndex::Normal(u32::from_le_bytes( + index.0[..4].try_into().unwrap(), + )))? + .public_key; + Ok(derive_transparent_address_from_public_key(&pubkey)) +} + +/// This is temporary, and will be replaced by `zcash_address::unified::Ufvk`. +pub(crate) fn fake_ufvk_encode(p2pkh: &ExtendedPubKey, sapling: &ExtendedFullViewingKey) -> String { + let mut ufvk = p2pkh.serialize(); + sapling.write(&mut ufvk).unwrap(); + format!("DONOTUSEUFVK{}", hex::encode(&ufvk)) +} + +/// This is temporary, and will be replaced by `zcash_address::unified::Ufvk`. +pub(crate) fn fake_ufvk_decode(encoding: &str) -> Option<(ExtendedPubKey, ExtendedFullViewingKey)> { + encoding + .strip_prefix("DONOTUSEUFVK") + .and_then(|data| hex::decode(data).ok()) + .and_then(|data| { + ExtendedPubKey::deserialize(&data[..65]) + .ok() + .zip(ExtendedFullViewingKey::read(&data[65..]).ok()) + }) +} + +/// This is temporary, and will be replaced by `zcash_address::unified::Address`. +pub(crate) fn fake_ua_encode(p2pkh: &TransparentAddress, sapling: &PaymentAddress) -> String { + format!( + "DONOTUSEUA{}{}", + hex::encode(match p2pkh { + TransparentAddress::PublicKey(data) => data, + TransparentAddress::Script(_) => panic!(), + }), + hex::encode(&sapling.to_bytes()) + ) +} + +// This is temporary, and will be replaced by `zcash_address::unified::Address`. +//pub(crate) fn fake_ua_decode(encoding: &str) -> Option<(TransparentAddress, PaymentAddress)> { +// encoding +// .strip_prefix("DONOTUSEUA") +// .and_then(|data| hex::decode(data).ok()) +// .and_then(|data| { +// PaymentAddress::from_bytes(&data[20..].try_into().unwrap()).map(|pa| { +// ( +// TransparentAddress::PublicKey(data[..20].try_into().unwrap()), +// pa, +// ) +// }) +// }) +//}