Balance actions bugfixes

This commit is contained in:
Milan Cerovsky 2025-04-15 22:09:58 +02:00
parent 64af1a3d50
commit 2fe90bcc43
12 changed files with 134 additions and 84 deletions

View File

@ -25,29 +25,46 @@ sealed interface WalletAccount : Comparable<WalletAccount> {
val hdAccountIndex: Zip32AccountIndex val hdAccountIndex: Zip32AccountIndex
get() = sdkAccount.hdAccountIndex!! get() = sdkAccount.hdAccountIndex!!
/**
* Total transparent + total shielded balance.
*/
val totalBalance: Zatoshi val totalBalance: Zatoshi
/**
* Total shielded balance including non-spendable.
*/
val totalShieldedBalance: Zatoshi val totalShieldedBalance: Zatoshi
val spendableBalance: Zatoshi
val changePendingBalance: Zatoshi
val valuePendingBalance: Zatoshi
val pendingBalance: Zatoshi /**
get() = changePendingBalance + valuePendingBalance * Total spendable transparent balance.
*/
val totalTransparentBalance: Zatoshi
val hasChangePending: Boolean /**
val hasValuePending: Boolean * Spendable & available shielded balance. Might be smaller than total shielded balance.
val isPending: Boolean */
get() = pendingBalance > Zatoshi(0) val spendableShieldedBalance: Zatoshi
fun canSpend(amount: Zatoshi): Boolean = spendableBalance >= amount /**
* Pending shielded Balance.
*/
val pendingShieldedBalance: Zatoshi
fun isProcessingZeroAvailableBalance(): Boolean { val isShieldedPending: Boolean
if (totalShieldedBalance == Zatoshi(0) && transparent.balance > Zatoshi(0)) { get() = pendingShieldedBalance > Zatoshi(0)
val isShieldingAvailable: Boolean
get() = totalTransparentBalance > Zatoshi(100000L)
val isProcessingZeroSpendableBalance: Boolean
get() {
if (totalShieldedBalance == Zatoshi(0) && totalTransparentBalance > Zatoshi(0)) {
return false return false
} }
return totalBalance > Zatoshi(0) && totalShieldedBalance == Zatoshi(0)
return totalBalance != totalShieldedBalance && totalShieldedBalance == Zatoshi(0)
} }
fun canSpend(amount: Zatoshi): Boolean = spendableShieldedBalance >= amount
} }
data class ZashiAccount( data class ZashiAccount(
@ -59,22 +76,28 @@ data class ZashiAccount(
) : WalletAccount { ) : WalletAccount {
override val name: StringResource override val name: StringResource
get() = stringRes(co.electriccoin.zcash.ui.R.string.zashi_wallet_name) get() = stringRes(co.electriccoin.zcash.ui.R.string.zashi_wallet_name)
override val icon: Int override val icon: Int
get() = R.drawable.ic_item_zashi get() = R.drawable.ic_item_zashi
override val totalBalance: Zatoshi override val totalBalance: Zatoshi
get() = unified.balance.total + sapling.balance.total + transparent.balance get() = unified.balance.total + sapling.balance.total + transparent.balance
override val totalShieldedBalance: Zatoshi override val totalShieldedBalance: Zatoshi
get() = unified.balance.total + sapling.balance.total get() = unified.balance.total + sapling.balance.total
override val spendableBalance: Zatoshi
override val totalTransparentBalance: Zatoshi
get() = transparent.balance
override val spendableShieldedBalance: Zatoshi
get() = unified.balance.available + sapling.balance.available get() = unified.balance.available + sapling.balance.available
override val changePendingBalance: Zatoshi
get() = unified.balance.changePending + sapling.balance.changePending override val pendingShieldedBalance: Zatoshi
override val valuePendingBalance: Zatoshi get() {
get() = unified.balance.valuePending + sapling.balance.valuePending val changePendingShieldedBalance = unified.balance.changePending + sapling.balance.changePending
override val hasChangePending: Boolean val valuePendingShieldedBalance = unified.balance.valuePending + sapling.balance.valuePending
get() = changePendingBalance.value > 0L return changePendingShieldedBalance + valuePendingShieldedBalance
override val hasValuePending: Boolean }
get() = valuePendingBalance.value > 0L
override fun compareTo(other: WalletAccount) = override fun compareTo(other: WalletAccount) =
when (other) { when (other) {
@ -91,23 +114,26 @@ data class KeystoneAccount(
) : WalletAccount { ) : WalletAccount {
override val icon: Int override val icon: Int
get() = R.drawable.ic_item_keystone get() = R.drawable.ic_item_keystone
override val name: StringResource override val name: StringResource
get() = stringRes(co.electriccoin.zcash.ui.R.string.keystone_wallet_name) get() = stringRes(co.electriccoin.zcash.ui.R.string.keystone_wallet_name)
override val sapling: SaplingInfo? = null override val sapling: SaplingInfo? = null
override val totalBalance: Zatoshi override val totalBalance: Zatoshi
get() = unified.balance.total + transparent.balance get() = unified.balance.total + transparent.balance
override val totalShieldedBalance: Zatoshi override val totalShieldedBalance: Zatoshi
get() = unified.balance.total get() = unified.balance.total
override val spendableBalance: Zatoshi
override val totalTransparentBalance: Zatoshi
get() = transparent.balance
override val spendableShieldedBalance: Zatoshi
get() = unified.balance.available get() = unified.balance.available
override val changePendingBalance: Zatoshi
get() = unified.balance.changePending override val pendingShieldedBalance: Zatoshi
override val valuePendingBalance: Zatoshi get() = unified.balance.changePending + unified.balance.valuePending
get() = unified.balance.valuePending
override val hasChangePending: Boolean
get() = changePendingBalance.value > 0L
override val hasValuePending: Boolean
get() = valuePendingBalance.value > 0L
override fun compareTo(other: WalletAccount) = override fun compareTo(other: WalletAccount) =
when (other) { when (other) {
@ -124,10 +150,7 @@ data class UnifiedInfo(
data class TransparentInfo( data class TransparentInfo(
val address: WalletAddress.Transparent, val address: WalletAddress.Transparent,
val balance: Zatoshi val balance: Zatoshi
) { )
val isShieldingAvailable: Boolean
get() = balance > Zatoshi(100000L)
}
data class SaplingInfo( data class SaplingInfo(
val address: WalletAddress.Sapling, val address: WalletAddress.Sapling,

View File

@ -42,7 +42,7 @@ class FlexaRepositoryImpl(
Twig.info { "Flexa initialized" } Twig.info { "Flexa initialized" }
observeZashiAccountUseCase() observeZashiAccountUseCase()
.map { it?.totalShieldedBalance to it?.spendableBalance } .map { it?.totalShieldedBalance to it?.spendableShieldedBalance }
.collect { (total, available) -> .collect { (total, available) ->
val totalZec = total.convertZatoshiToZec().toDouble() val totalZec = total.convertZatoshiToZec().toDouble()
val availableZec = available.convertZatoshiToZec().toDouble() val availableZec = available.convertZatoshiToZec().toDouble()

View File

@ -29,7 +29,7 @@ class ShieldFundsRepositoryImpl(
account == null -> account == null ->
flowOf(ShieldFundsData.Unavailable) flowOf(ShieldFundsData.Unavailable)
account.transparent.isShieldingAvailable -> account.isShieldingAvailable ->
shieldFundsDataSource.observe(account.sdkAccount.accountUuid).map { shieldFundsDataSource.observe(account.sdkAccount.accountUuid).map {
when (it) { when (it) {
is ShieldFundsAvailability.Available -> ShieldFundsData.Available( is ShieldFundsAvailability.Available -> ShieldFundsData.Available(

View File

@ -40,17 +40,15 @@ class BalanceWidgetViewModel(
) )
) )
private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState) =
BalanceWidgetState(
private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState): BalanceWidgetState {
return BalanceWidgetState(
totalBalance = account?.totalBalance ?: Zatoshi(0), totalBalance = account?.totalBalance ?: Zatoshi(0),
exchangeRate = if (args.isExchangeRateButtonEnabled) exchangeRateUsd else null, exchangeRate = if (args.isExchangeRateButtonEnabled) exchangeRateUsd else null,
button = when { button = when {
!args.isBalanceButtonEnabled -> null !args.isBalanceButtonEnabled -> null
account == null -> null account == null -> null
account.totalBalance == account.spendableBalance -> null account.totalBalance == account.spendableShieldedBalance -> null
account.isProcessingZeroAvailableBalance() && !account.isPending -> account.totalBalance > account.spendableShieldedBalance && account.isShieldedPending ->
BalanceButtonState( BalanceButtonState(
icon = R.drawable.ic_balances_expand, icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable), text = stringRes(R.string.widget_balances_button_spendable),
@ -58,10 +56,10 @@ class BalanceWidgetViewModel(
onClick = ::onBalanceButtonClick onClick = ::onBalanceButtonClick
) )
account.totalBalance > account.spendableBalance -> BalanceButtonState( account.totalBalance > account.spendableShieldedBalance -> BalanceButtonState(
icon = R.drawable.ic_balances_expand, icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable), text = stringRes(R.string.widget_balances_button_spendable),
amount = account.spendableBalance, amount = account.spendableShieldedBalance,
onClick = ::onBalanceButtonClick onClick = ::onBalanceButtonClick
) )
@ -69,7 +67,6 @@ class BalanceWidgetViewModel(
}, },
showDust = args.showDust showDust = args.showDust
) )
}
private fun onBalanceButtonClick() = navigationRouter.forward(BalanceAction) private fun onBalanceButtonClick() = navigationRouter.forward(BalanceAction)
} }

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
@ -62,10 +63,12 @@ fun BottomSheetContent(state: BalanceActionState, modifier: Modifier = Modifier)
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(),
text = state.title.getValue(), text = state.title.getValue(),
color = ZashiColors.Text.textPrimary, color = ZashiColors.Text.textPrimary,
style = ZashiTypography.textXl, style = ZashiTypography.textXl,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center
) )
Spacer(12.dp) Spacer(12.dp)
Text( Text(
@ -80,8 +83,8 @@ fun BottomSheetContent(state: BalanceActionState, modifier: Modifier = Modifier)
} }
BalanceActionRow(state) BalanceActionRow(state)
} }
Spacer(32.dp)
state.shieldButton?.let { state.shieldButton?.let {
Spacer(32.dp)
BalanceShieldButton(it) BalanceShieldButton(it)
} }
Spacer(32.dp) Spacer(32.dp)

View File

@ -37,7 +37,7 @@ class BalanceActionViewModel(
if (account == null) return null if (account == null) return null
return BalanceActionState( return BalanceActionState(
title = stringRes("Spendable Balance"), title = stringRes(R.string.balance_action_title),
message = createMessage(account), message = createMessage(account),
positive = createPositiveButton(account), positive = createPositiveButton(account),
onBack = ::onBack, onBack = ::onBack,
@ -48,50 +48,58 @@ class BalanceActionViewModel(
private fun createMessage(account: WalletAccount): StringResource { private fun createMessage(account: WalletAccount): StringResource {
val pending = when { val pending = when {
account.totalBalance == account.spendableBalance && !account.isPending -> account.totalShieldedBalance == account.spendableShieldedBalance ->
stringRes("All your funds are shielded and spendable.") stringRes(R.string.balance_action_all_shielded)
account.totalShieldedBalance > account.spendableShieldedBalance ->
stringRes(R.string.balance_action_pending)
account.isPending || account.isProcessingZeroAvailableBalance() ->
stringRes("Pending transactions are getting mined and confirmed.")
else -> null else -> null
} }
val shielding = val shielding = stringRes(R.string.balance_action_shield_message).takeIf { account.isShieldingAvailable }
stringRes("Shield your transparent ZEC to make it spendable and private. Shielding transparent funds will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)")
.takeIf { account.transparent.isShieldingAvailable }
return if (pending != null && shielding != null) { return if (pending != null && shielding != null) {
pending + stringRes("\n\n") + shielding pending + stringRes("\n\n") + shielding
} else { } else {
listOfNotNull(pending, shielding).reduceOrNull { acc, stringResource -> acc + stringResource } ?: stringRes( pending ?: shielding ?: stringRes("")
""
)
} }
} }
private fun createPositiveButton(account: WalletAccount) = ButtonState( private fun createPositiveButton(account: WalletAccount) = ButtonState(
text = if (account.transparent.isShieldingAvailable) stringRes("Dismiss") else stringRes("Ok"), text = if (account.isShieldingAvailable) {
stringRes(R.string.general_dismiss)
} else {
stringRes(R.string.general_ok)
},
onClick = ::onBack onClick = ::onBack
) )
private fun createInfoRows(account: WalletAccount) = listOfNotNull( private fun createInfoRows(account: WalletAccount) = listOfNotNull(
BalanceActionRowState( BalanceActionRowState(
title = stringRes("Shielded ZEC (Spendable)"), title = stringRes(R.string.balance_action_info_shielded),
icon = imageRes(R.drawable.ic_balance_shield), icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes(R.string.general_zec, stringRes(account.spendableBalance)) value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance))
), ),
if (!account.isProcessingZeroAvailableBalance()) { when {
account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending ->
BalanceActionRowState( BalanceActionRowState(
title = stringRes("Pending"), title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(), icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.totalBalance)) value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance))
) )
} else {
account.totalShieldedBalance > account.spendableShieldedBalance ->
BalanceActionRowState( BalanceActionRowState(
title = stringRes("Pending"), title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(), icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.pendingBalance)) value = stringRes(
).takeIf { account.isPending } R.string.general_zec,
stringRes(account.totalShieldedBalance - account.spendableShieldedBalance)
)
)
else -> null
}, },
) )
@ -99,7 +107,7 @@ class BalanceActionViewModel(
return BalanceShieldButtonState( return BalanceShieldButtonState(
amount = account.transparent.balance, amount = account.transparent.balance,
onShieldClick = ::onShieldClick onShieldClick = ::onShieldClick
).takeIf { account.transparent.isShieldingAvailable } ).takeIf { account.isShieldingAvailable }
} }
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()

View File

@ -5,8 +5,6 @@ package co.electriccoin.zcash.ui.screen.restore.seed
*/ */
object RestoreSeedTag { object RestoreSeedTag {
const val SEED_WORD_TEXT_FIELD = "seed_text_field" const val SEED_WORD_TEXT_FIELD = "seed_text_field"
const val BIRTHDAY_TEXT_FIELD = "birthday_text_field"
const val CHIP_LAYOUT = "chip_group"
const val AUTOCOMPLETE_LAYOUT = "autocomplete_layout" const val AUTOCOMPLETE_LAYOUT = "autocomplete_layout"
const val AUTOCOMPLETE_ITEM = "autocomplete_item" const val AUTOCOMPLETE_ITEM = "autocomplete_item"
} }

View File

@ -430,10 +430,13 @@ fun SendButton(
AddressType.Tex -> AddressType.Tex ->
WalletAddress.Tex.new(recipientAddressState.address) WalletAddress.Tex.new(recipientAddressState.address)
AddressType.Transparent -> AddressType.Transparent ->
WalletAddress.Transparent.new(recipientAddressState.address) WalletAddress.Transparent.new(recipientAddressState.address)
AddressType.Unified -> AddressType.Unified ->
WalletAddress.Unified.new(recipientAddressState.address) WalletAddress.Unified.new(recipientAddressState.address)
null -> WalletAddress.Unified.new(recipientAddressState.address) null -> WalletAddress.Unified.new(recipientAddressState.address)
} }
) )
@ -604,7 +607,7 @@ fun SendFormAmountTextField(
} }
is AmountState.Valid -> { is AmountState.Valid -> {
if (selectedAccount.spendableBalance < amountState.zatoshi) { if (selectedAccount.spendableShieldedBalance < amountState.zatoshi) {
stringResource(id = R.string.send_amount_insufficient_balance) stringResource(id = R.string.send_amount_insufficient_balance)
} else { } else {
null null
@ -630,7 +633,9 @@ fun SendFormAmountTextField(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Row { Row(
modifier = Modifier.fillMaxWidth()
) {
ZashiTextField( ZashiTextField(
singleLine = true, singleLine = true,
maxLines = 1, maxLines = 1,
@ -648,7 +653,9 @@ fun SendFormAmountTextField(
) )
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
innerModifier = Modifier.testTag(SendTag.SEND_AMOUNT_FIELD), innerModifier = ZashiTextFieldDefaults
.innerModifier
.testTag(SendTag.SEND_AMOUNT_FIELD),
error = amountError, error = amountError,
placeholder = { placeholder = {
Text( Text(

View File

@ -34,4 +34,6 @@
<string name="general_remind_me_in_two_days">two days</string> <string name="general_remind_me_in_two_days">two days</string>
<string name="general_remind_me_in_two_weeks">two weeks</string> <string name="general_remind_me_in_two_weeks">two weeks</string>
<string name="general_remind_me_in_two_months">two months</string> <string name="general_remind_me_in_two_months">two months</string>
<string name="general_zec">%S ZEC</string>
<string name="general_dismiss">Dismiss</string>
</resources> </resources>

View File

@ -35,6 +35,6 @@
<string name="general_remind_me_in_two_days">two days</string> <string name="general_remind_me_in_two_days">two days</string>
<string name="general_remind_me_in_two_weeks">two weeks</string> <string name="general_remind_me_in_two_weeks">two weeks</string>
<string name="general_remind_me_in_two_months">two months</string> <string name="general_remind_me_in_two_months">two months</string>
<string name="general_zec">%S ZEC</string> <string name="general_zec">%S ZEC</string>
<string name="general_dismiss">Dismiss</string>
</resources> </resources>

View File

@ -70,4 +70,10 @@
<string name="home_message_crash_reporting_title">Help improve Zashi</string> <string name="home_message_crash_reporting_title">Help improve Zashi</string>
<string name="home_message_crash_reporting_subtitle">Allow sharing crash reports</string> <string name="home_message_crash_reporting_subtitle">Allow sharing crash reports</string>
<string name="home_message_crash_reporting_button">Review</string> <string name="home_message_crash_reporting_button">Review</string>
<string name="balance_action_title">Spendable Balance</string>
<string name="balance_action_all_shielded">All your funds are shielded and spendable.</string>
<string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string>
<string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Shielding transparent funds will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)</string>
<string name="balance_action_info_shielded">Shielded ZEC (Spendable)</string>
<string name="balance_action_info_pending">Pending</string>
</resources> </resources>

View File

@ -70,4 +70,10 @@
<string name="home_message_crash_reporting_title">Help improve Zashi</string> <string name="home_message_crash_reporting_title">Help improve Zashi</string>
<string name="home_message_crash_reporting_subtitle">Allow sharing crash reports</string> <string name="home_message_crash_reporting_subtitle">Allow sharing crash reports</string>
<string name="home_message_crash_reporting_button">Review</string> <string name="home_message_crash_reporting_button">Review</string>
<string name="balance_action_title">Spendable Balance</string>
<string name="balance_action_all_shielded">All your funds are shielded and spendable.</string>
<string name="balance_action_pending">Pending transactions are getting mined and confirmed.</string>
<string name="balance_action_shield_message">Shield your transparent ZEC to make it spendable and private. Shielding transparent funds will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)</string>
<string name="balance_action_info_shielded">Shielded ZEC (Spendable)</string>
<string name="balance_action_info_pending">Pending</string>
</resources> </resources>