[#642] Adopt SDK 1.10.0 API changes

* Adopt SDK 1.10.0 API changes

* Rename ShieldedSapling -> LegacySapling

* Viewing key re-derived for fix fixture value

* Changes used API for retrieving Transparent address

- Switched API DerivationTool.deriveTransparentAddress() -> Synchronizer.getLegacyTransparentAddress()
- Renamed Transparent -> LegacyTransparent

* Adds new common transaction model class

- Synchronizer.toTransactions() now works only with clearedTransactions and pendingTransactions
-Added CommonTransaction wrapper for all attributes of PendingTransaction and TransactionOverview to cover as much as possible. It's prepared for discussion about which attributes we really need and which we can omit.

* Regeneration of addresses in fixture

- Applies for both Transparent and Sapling addresses
- Fixed related test
- Added a new test for checking Transparent address abbreviation, as it also serves as a little check of the fixture address and can prevent an incorrect address change in the future

* Remove Viewing key usage from entire app

- As we agreed that the app should not access to it now
- Removed from all related places

* Change CommonTransaction to sealed class

* Comments update

* Switch SDK to 1.10.0-beta01-SNAPSHOT version

* Revert back legacy naming on Transparent and Sapling addresses

* Increased timeout limit to satisfy SDK initialization

* Remove unused import
This commit is contained in:
Honza Rychnovsky 2022-11-25 09:30:27 +01:00 committed by GitHub
parent 381af575ef
commit e71c7854a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 126 additions and 201 deletions

View File

@ -145,7 +145,7 @@ ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.4
# TODO [#279]: Revert to stable SDK before app release
# TODO [#279]: https://github.com/zcash/secant-android-wallet/issues/279
ZCASH_SDK_VERSION=1.9.0-beta02
ZCASH_SDK_VERSION=1.10.0-beta01-SNAPSHOT
ZXING_VERSION=3.5.0

View File

@ -1,11 +1,9 @@
package cash.z.ecc.sdk.model
import androidx.test.filters.SmallTest
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
import cash.z.ecc.sdk.fixture.WalletAddressesFixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
@ -16,17 +14,8 @@ class WalletAddressesTest {
fun security() = runTest {
val walletAddresses = WalletAddressesFixture.new()
val actual = WalletAddressesFixture.new().toString()
assertFalse(actual.contains(walletAddresses.shieldedSapling.address))
assertFalse(actual.contains(walletAddresses.sapling.address))
assertFalse(actual.contains(walletAddresses.transparent.address))
assertFalse(actual.contains(walletAddresses.unified.address))
assertFalse(actual.contains(walletAddresses.viewingKey))
}
@Test
@SmallTest
fun new() = runTest {
val expected = WalletAddressesFixture.new()
val actual = WalletAddresses.new(PersistableWalletFixture.new())
assertEquals(expected, actual)
}
}

View File

@ -3,9 +3,10 @@
package cash.z.ecc.sdk
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.sdk.model.ZecSend
fun Synchronizer.send(spendingKey: String, send: ZecSend) = sendToAddress(
suspend fun Synchronizer.send(spendingKey: UnifiedSpendingKey, send: ZecSend) = sendToAddress(
spendingKey,
send.amount,
send.destination.address,

View File

@ -6,13 +6,14 @@ object WalletAddressFixture {
// These fixture values are derived from the secret defined in PersistableWalletFixture
// TODO [#161]: Pending SDK support
// TODO [#161]: https://github.com/zcash/secant-android-wallet/issues/161
const val UNIFIED_ADDRESS_STRING = "Unified GitHub Issue #161"
@Suppress("MaxLineLength")
const val SHIELDED_SAPLING_ADDRESS_STRING = "ztestsapling1475xtm56czrzmleqzzlu4cxvjjfsy2p6rv78q07232cpsx5ee52k0mn5jyndq09mampkgvrxnwg"
const val TRANSPARENT_ADDRESS_STRING = "tmXuTnE11JojToagTqxXUn6KvdxDE3iLKbp"
const val SAPLING_ADDRESS_STRING = "zs1hf72k87gev2qnvg9228vn2xt97adfelju2hm2ap4xwrxkau5dz56mvkeseer3u8283wmy7skt4u"
const val TRANSPARENT_ADDRESS_STRING = "t1QZMTZaU1EwXppCLL5dR6U9y2M4ph3CSPK"
suspend fun unified() = WalletAddress.Unified.new(UNIFIED_ADDRESS_STRING)
suspend fun shieldedSapling() = WalletAddress.ShieldedSapling.new(SHIELDED_SAPLING_ADDRESS_STRING)
suspend fun sapling() = WalletAddress.Sapling.new(SAPLING_ADDRESS_STRING)
suspend fun transparent() = WalletAddress.Transparent.new(TRANSPARENT_ADDRESS_STRING)
}

View File

@ -4,18 +4,14 @@ import cash.z.ecc.sdk.model.WalletAddress
import cash.z.ecc.sdk.model.WalletAddresses
object WalletAddressesFixture {
// These fixture values are derived from the secret defined in PersistableWalletFixture
const val VIEWING_KEY = "03feaa290589a20f795f302ba03847b0a6c9c2b571d75d80bc4ebb02382d0549da"
suspend fun new(
unified: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING,
shieldedSapling: String = WalletAddressFixture.SHIELDED_SAPLING_ADDRESS_STRING,
transparent: String = WalletAddressFixture.TRANSPARENT_ADDRESS_STRING,
viewingKey: String = VIEWING_KEY
sapling: String = WalletAddressFixture.SAPLING_ADDRESS_STRING,
transparent: String = WalletAddressFixture.TRANSPARENT_ADDRESS_STRING
) = WalletAddresses(
WalletAddress.Unified.new(unified),
WalletAddress.ShieldedSapling.new(shieldedSapling),
WalletAddress.Transparent.new(transparent),
viewingKey
WalletAddress.Sapling.new(sapling),
WalletAddress.Transparent.new(transparent)
)
}

View File

@ -13,6 +13,8 @@ data class SeedPhrase(val split: List<String>) {
fun joinToString() = split.joinToString(DEFAULT_DELIMITER)
fun toByteArray() = joinToString().encodeToByteArray()
companion object {
const val SEED_PHRASE_SIZE = 24

View File

@ -11,22 +11,22 @@ sealed class WalletAddress(val address: String) {
}
}
class ShieldedSapling private constructor(address: String) : WalletAddress(address) {
class Sapling private constructor(address: String) : WalletAddress(address) {
companion object {
suspend fun new(address: String): WalletAddress.ShieldedSapling {
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
suspend fun new(address: String): Sapling {
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
// TODO [#342]: refactor SDK to enable direct calls for address verification
return WalletAddress.ShieldedSapling(address)
return Sapling(address)
}
}
}
class Transparent private constructor(address: String) : WalletAddress(address) {
companion object {
suspend fun new(address: String): WalletAddress.Transparent {
// https://github.com/zcash/zcash-android-wallet-sdk/issues/342
suspend fun new(address: String): Transparent {
// TODO [#342]: https://github.com/zcash/zcash-android-wallet-sdk/issues/342
// TODO [#342]: refactor SDK to enable direct calls for address verification
return WalletAddress.Transparent(address)
return Transparent(address)
}
}
}

View File

@ -1,50 +1,32 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.Account
data class WalletAddresses(
val unified: WalletAddress.Unified,
val shieldedSapling: WalletAddress.ShieldedSapling,
val transparent: WalletAddress.Transparent,
val viewingKey: String
val sapling: WalletAddress.Sapling,
val transparent: WalletAddress.Transparent
) {
// Override to prevent leaking details in logs
override fun toString() = "WalletAddresses"
companion object {
suspend fun new(persistableWallet: PersistableWallet): WalletAddresses {
// Dispatcher needed because SecureRandom is loaded, which is slow and performs IO
// https://github.com/zcash/kotlin-bip39/issues/13
val bip39Seed = withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(persistableWallet.seedPhrase.joinToString()).toSeed()
}
suspend fun new(synchronizer: Synchronizer): WalletAddresses {
val saplingAddress = WalletAddress.Sapling.new(
synchronizer.getSaplingAddress(Account.DEFAULT)
)
val viewingKey = DerivationTool.deriveUnifiedViewingKeys(bip39Seed, persistableWallet.network)[0].extpub
val shieldedSaplingAddress = DerivationTool.deriveShieldedAddress(
bip39Seed,
persistableWallet.network
).let {
WalletAddress.ShieldedSapling.new(it)
}
val transparentAddress = DerivationTool.deriveTransparentAddress(
bip39Seed,
persistableWallet.network
).let {
WalletAddress.Transparent.new(it)
}
val transparentAddress = WalletAddress.Transparent.new(
synchronizer.getTransparentAddress(Account.DEFAULT)
)
// TODO [#161]: Pending SDK support, fix providing correct values for the unified
// TODO [#161]: https://github.com/zcash/secant-android-wallet/issues/161
return WalletAddresses(
unified = WalletAddress.Unified.new("Unified GitHub Issue #161"),
shieldedSapling = shieldedSaplingAddress,
transparent = transparentAddress,
viewingKey = viewingKey
sapling = saplingAddress,
transparent = transparentAddress
)
}
}

View File

@ -24,7 +24,6 @@ data class ExtendedColors(
val addressHighlightUnified: Color,
val addressHighlightSapling: Color,
val addressHighlightTransparent: Color,
val addressHighlightViewing: Color,
val dangerous: Color,
val onDangerous: Color,
val reference: Color

View File

@ -50,7 +50,6 @@ internal object Dark {
val addressHighlightUnified = Color(0xFFFFD800)
val addressHighlightSapling = Color(0xFF1BBFF6)
val addressHighlightTransparent = Color(0xFF97999A)
val addressHighlightViewing = Color(0xFF504062)
val dangerous = Color(0xFFEC0008)
val onDangerous = Color(0xFFFFFFFF)
@ -101,7 +100,6 @@ internal object Light {
val addressHighlightUnified = Color(0xFFFFD800)
val addressHighlightSapling = Color(0xFF1BBFF6)
val addressHighlightTransparent = Color(0xFF97999A)
val addressHighlightViewing = Color(0xFF504062)
val dangerous = Color(0xFFEC0008)
val onDangerous = Color(0xFFFFFFFF)
@ -148,7 +146,6 @@ internal val DarkExtendedColorPalette = ExtendedColors(
addressHighlightUnified = Dark.addressHighlightUnified,
addressHighlightSapling = Dark.addressHighlightSapling,
addressHighlightTransparent = Dark.addressHighlightTransparent,
addressHighlightViewing = Dark.addressHighlightViewing,
dangerous = Dark.dangerous,
onDangerous = Dark.onDangerous,
reference = Dark.reference
@ -171,7 +168,6 @@ internal val LightExtendedColorPalette = ExtendedColors(
addressHighlightUnified = Light.addressHighlightUnified,
addressHighlightSapling = Light.addressHighlightSapling,
addressHighlightTransparent = Light.addressHighlightTransparent,
addressHighlightViewing = Light.addressHighlightViewing,
dangerous = Light.dangerous,
onDangerous = Light.onDangerous,
reference = Light.reference
@ -195,7 +191,6 @@ internal val LocalExtendedColors = staticCompositionLocalOf {
addressHighlightUnified = Color.Unspecified,
addressHighlightSapling = Color.Unspecified,
addressHighlightTransparent = Color.Unspecified,
addressHighlightViewing = Color.Unspecified,
dangerous = Color.Unspecified,
onDangerous = Color.Unspecified,
reference = Color.Unspecified

View File

@ -33,29 +33,23 @@ class WalletAddressViewTest : UiTestPrerequisites() {
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_unified)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_sapling)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_transparent)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_viewing_key)).also {
it.assertExists()
}
composeTestRule.onNodeWithText(walletAddresses.unified.address).also {
it.assertExists()
}
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(walletAddresses.transparent.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
it.assertDoesNotExist()
}
}
@Test
@ -80,20 +74,20 @@ class WalletAddressViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun shielded_sapling_expands() = runTest {
fun sapling_expands() = runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_shielded_sapling)).also {
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_sapling)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(walletAddresses.shieldedSapling.address).also {
composeTestRule.onNodeWithText(walletAddresses.sapling.address).also {
it.assertExists()
}
}
@ -118,26 +112,6 @@ class WalletAddressViewTest : UiTestPrerequisites() {
}
}
@Test
@MediumTest
fun viewing_expands() = runTest {
val walletAddresses = WalletAddressesFixture.new()
newTestSetup(walletAddresses)
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithText(getStringResource(R.string.wallet_address_viewing_key)).also {
it.assertExists()
it.performClick()
}
composeTestRule.onNodeWithText(walletAddresses.viewingKey).also {
it.assertExists()
}
}
@Test
@MediumTest
fun back() = runTest {

View File

@ -13,11 +13,25 @@ class WalletAddressExtTest {
@Test
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
fun testAbbreviated() = runTest {
val actual = WalletAddressFixture.shieldedSapling().abbreviated(getAppContext())
fun testAbbreviatedSaplingAddress() = runTest {
val actual = WalletAddressFixture.sapling().abbreviated(getAppContext())
// TODO [#248]: The expected value should probably be reversed if the locale is RTL
val expected = "ztest…rxnwg"
// TODO [#248]: https://github.com/zcash/secant-android-wallet/issues/248
val expected = "zs1hf…skt4u"
assertEquals(expected, actual)
}
@Test
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
fun testAbbreviatedTransparentAddress() = runTest {
val actual = WalletAddressFixture.transparent().abbreviated(getAppContext())
// TODO [#248]: The expected value should probably be reversed if the locale is RTL
// TODO [#248]: https://github.com/zcash/secant-android-wallet/issues/248
val expected = "t1QZM…3CSPK"
assertEquals(expected, actual)
}

View File

@ -1,16 +1,11 @@
package co.electriccoin.zcash.global
import android.content.Context
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.ext.onFirst
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.UnifiedViewingKey
import cash.z.ecc.sdk.model.PersistableWallet
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.spackle.LazyWithArgument
@ -22,7 +17,6 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose
@ -44,7 +38,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.util.UUID
class WalletCoordinator(context: Context) {
@ -81,7 +74,7 @@ class WalletCoordinator(context: Context) {
private sealed class InternalSynchronizerStatus {
object NoWallet : InternalSynchronizerStatus()
class Available(val synchronizer: cash.z.ecc.android.sdk.Synchronizer) : InternalSynchronizerStatus()
class Available(val synchronizer: Synchronizer) : InternalSynchronizerStatus()
class Lockout(val id: UUID) : InternalSynchronizerStatus()
}
@ -93,9 +86,14 @@ class WalletCoordinator(context: Context) {
flowOf(InternalSynchronizerStatus.NoWallet)
} else {
callbackFlow<InternalSynchronizerStatus.Available> {
val initializer = Initializer.new(context, persistableWallet.toConfig())
val synchronizer = synchronizerMutex.withLock {
val synchronizer = Synchronizer.new(initializer)
val synchronizer = Synchronizer.new(
context = context,
zcashNetwork = persistableWallet.network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
birthday = persistableWallet.birthday,
seed = persistableWallet.seedPhrase.toByteArray()
)
synchronizer.start(walletScope)
}
@ -170,11 +168,10 @@ class WalletCoordinator(context: Context) {
.filter { it.id == lockoutId }
.onFirst {
synchronizerMutex.withLock {
val didDelete = Initializer.erase(
applicationContext,
ZcashNetwork.fromResources(applicationContext)
val didDelete = Synchronizer.erase(
appContext = applicationContext,
network = ZcashNetwork.fromResources(applicationContext)
)
Twig.info { "SDK erase result: $didDelete" }
}
}
@ -210,9 +207,9 @@ class WalletCoordinator(context: Context) {
}
synchronizerMutex.withLock {
val didDelete = Initializer.erase(
applicationContext,
ZcashNetwork.fromResources(applicationContext)
val didDelete = Synchronizer.erase(
appContext = applicationContext,
network = ZcashNetwork.fromResources(applicationContext)
)
Twig.info { "SDK erase result: $didDelete" }
}
@ -223,22 +220,3 @@ class WalletCoordinator(context: Context) {
}
}
}
private suspend fun PersistableWallet.deriveViewingKey(): UnifiedViewingKey {
// Dispatcher needed because SecureRandom is loaded, which is slow and performs IO
// https://github.com/zcash/kotlin-bip39/issues/13
val bip39Seed = withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(seedPhrase.joinToString()).toSeed()
}
return DerivationTool.deriveUnifiedViewingKeys(bip39Seed, network)[0]
}
private suspend fun PersistableWallet.toConfig(): Initializer.Config {
val network = network
val vk = deriveViewingKey()
return Initializer.Config {
it.importWallet(vk, birthday, network, LightWalletEndpoint.defaultForNetwork(network))
}
}

View File

@ -123,15 +123,11 @@ private fun WalletDetailAddresses(walletAddresses: WalletAddresses) {
ListHeader(text = stringResource(R.string.wallet_address_header_includes))
}
SaplingAddress(walletAddresses.shieldedSapling.address)
SaplingAddress(walletAddresses.sapling.address)
TransparentAddress(walletAddresses.transparent.address)
}
}
}
Divider(thickness = 8.dp)
ViewingKey(walletAddresses.viewingKey)
}
// Note: The addresses code below has opportunities to be made more DRY.
@ -148,7 +144,7 @@ private fun SaplingAddress(saplingAddress: String) {
SmallIndicator(ZcashTheme.colors.addressHighlightSapling)
ExpandableRow(
title = stringResource(R.string.wallet_address_shielded_sapling),
title = stringResource(R.string.wallet_address_sapling),
content = saplingAddress,
isInitiallyExpanded = false
)
@ -171,27 +167,6 @@ private fun TransparentAddress(transparentAddress: String) {
}
}
@Composable
private fun ViewingKey(viewingKey: String) {
Row(
Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
) {
Image(
painter = ColorPainter(ZcashTheme.colors.addressHighlightViewing),
contentDescription = "",
modifier = Modifier
.width(SMALL_INDICATOR_WIDTH)
)
ExpandableRow(
title = stringResource(R.string.wallet_address_viewing_key),
content = viewingKey,
isInitiallyExpanded = false
)
}
}
@Composable
private fun ExpandableRow(
title: String,

View File

@ -0,0 +1,12 @@
package co.electriccoin.zcash.ui.screen.home.model
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.TransactionOverview
/**
* A common transactions wrapper class to provide unified way to work with a transactions classes from our SDK.
*/
sealed class CommonTransaction {
data class Pending(val data: PendingTransaction) : CommonTransaction()
data class Overview(val data: TransactionOverview) : CommonTransaction()
}

View File

@ -42,7 +42,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.db.entity.Transaction
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
import cash.z.ecc.sdk.model.PercentDecimal
import co.electriccoin.zcash.crash.android.CrashReporter
@ -57,6 +56,7 @@ import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
@ -86,7 +86,7 @@ fun ComposablePreview() {
@Composable
fun Home(
walletSnapshot: WalletSnapshot,
transactionHistory: List<Transaction>,
transactionHistory: List<CommonTransaction>,
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
@ -179,7 +179,7 @@ private fun DebugMenu(resetSdk: () -> Unit, wipeEntireWallet: () -> Unit) {
private fun HomeMainContent(
paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot,
transactionHistory: List<Transaction>,
transactionHistory: List<CommonTransaction>,
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
@ -321,7 +321,7 @@ private fun Status(
@Composable
@Suppress("MagicNumber")
private fun History(transactionHistory: List<Transaction>) {
private fun History(transactionHistory: List<CommonTransaction>) {
if (transactionHistory.isEmpty()) {
return
}

View File

@ -7,13 +7,13 @@ import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.Transaction
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PendingTransaction
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.isMined
import cash.z.ecc.android.sdk.model.isSubmitSuccess
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.sdk.model.FiatCurrency
import cash.z.ecc.sdk.model.PercentDecimal
@ -26,11 +26,11 @@ import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import co.electriccoin.zcash.work.WorkIds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@ -56,6 +57,7 @@ import kotlin.time.ExperimentalTime
// To make this more multiplatform compatible, we need to remove the dependency on Context
// for loading the preferences.
// TODO [#292]: Should be moved to SDK-EXT-UI module.
// TODO [#292]: https://github.com/zcash/secant-android-wallet/issues/292
class WalletViewModel(application: Application) : AndroidViewModel(application) {
private val walletCoordinator = co.electriccoin.zcash.global.WalletCoordinator.getInstance(application)
@ -117,8 +119,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
val bip39Seed = withContext(Dispatchers.IO) {
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
}
DerivationTool.deriveSpendingKeys(bip39Seed, it.network)[0]
DerivationTool.deriveUnifiedSpendingKey(
seed = bip39Seed,
network = it.network,
account = Account.DEFAULT
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
@ -143,7 +148,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
// This is not the right API, because the transaction list could be very long and might need UI filtering
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
val transactionSnapshot: StateFlow<List<Transaction>> = synchronizer
val transactionSnapshot: StateFlow<List<CommonTransaction>> = synchronizer
.flatMapLatest {
if (null == it) {
flowOf(emptyList())
@ -157,11 +162,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
emptyList()
)
@OptIn(FlowPreview::class)
val addresses: StateFlow<WalletAddresses?> = secretState
.filterIsInstance<SecretState.Ready>()
.map { WalletAddresses.new(it.persistableWallet) }
.stateIn(
val addresses: StateFlow<WalletAddresses?> = synchronizer
.filterNotNull()
.map {
WalletAddresses.new(it)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
@ -371,15 +376,14 @@ private fun Synchronizer.toWalletSnapshot() =
private fun Synchronizer.toTransactions() =
combine(
clearedTransactions.distinctUntilChanged(),
pendingTransactions.distinctUntilChanged(),
sentTransactions.distinctUntilChanged(),
receivedTransactions.distinctUntilChanged()
) { cleared, pending, sent, received ->
pendingTransactions.distinctUntilChanged()
) { cleared, pending ->
// TODO [#157]: Sort the transactions to show the most recent
buildList<Transaction> {
addAll(cleared)
addAll(pending)
addAll(sent)
addAll(received)
// TODO [#157]: https://github.com/zcash/secant-android-wallet/issues/157
// Note that the list of transactions will not be sorted.
buildList {
addAll(cleared.map { CommonTransaction.Overview(it) })
addAll(pending.map { CommonTransaction.Pending(it) })
}
}

View File

@ -6,11 +6,13 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import cash.z.ecc.sdk.send
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapSend(
@ -25,6 +27,7 @@ private fun WrapSend(
goBack: () -> Unit
) {
val walletViewModel by activity.viewModels<WalletViewModel>()
val scope = rememberCoroutineScope()
val synchronizer = walletViewModel.synchronizer.collectAsState().value
val spendableBalance = walletViewModel.walletSnapshot.collectAsState().value?.spendableBalance()
@ -36,9 +39,10 @@ private fun WrapSend(
mySpendableBalance = spendableBalance,
goBack = goBack,
onCreateAndSend = {
synchronizer.send(spendingKey, it)
goBack()
scope.launch {
synchronizer.send(spendingKey, it)
goBack()
}
}
)
}

View File

@ -4,10 +4,8 @@
<string name="wallet_address_back_content_description">Back</string>
<string name="wallet_address_unified">Your Unified Address</string>
<string name="wallet_address_header_includes">which includes</string>
<string name="wallet_address_shielded_sapling">Shielded Sapling (NU1)</string>
<string name="wallet_address_sapling">Shielded Sapling (NU1)</string>
<string name="wallet_address_transparent">Transparent</string>
<string name="wallet_address_viewing_key">Viewing Key Only (Sapling)</string>
<string name="wallet_address_show">Show address</string>
<string name="wallet_address_hide">Hide address</string>
</resources>

View File

@ -475,7 +475,8 @@ private fun homeScreenshots(resContext: Context, tag: String, composeTestRule: A
}
private fun profileScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>) {
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.addresses.value != null }
// Note: increased timeout limit to satisfy time needed for SDK initialization
composeTestRule.waitUntil(2_000) { composeTestRule.activity.walletViewModel.addresses.value != null }
composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also {
it.assertExists()