[#319] Adopt Zatoshi API

This commit is contained in:
Carter Jernigan 2022-07-05 10:26:19 -04:00 committed by Carter Jernigan
parent 13ab487118
commit 362d2f94ab
19 changed files with 120 additions and 107 deletions

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.ext.tryWithWarning
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.CoroutineScope
@ -32,7 +33,7 @@ class ZcashWalletApp : Application(), CameraXConfig.Provider {
var creationMeasured: Boolean = false
/** The amount of transparent funds that need to accumulate before autoshielding is triggered */
val autoshieldThreshold: Long = ZcashSdk.ZATOSHI_PER_ZEC // 1 ZEC
val autoshieldThreshold: Long = Zatoshi.ZATOSHI_PER_ZEC // 1 ZEC
/**
* Intentionally private Scope for use with launching Feedback jobs. The feedback object has the

View File

@ -28,7 +28,7 @@ inline fun <reified VM : ViewModel> BaseFragment<*>.activityViewModel(isSynchron
return cached
?: scopedFactory<VM>(isSynchronizerScope)?.let { factory ->
ViewModelProvider(this@activityViewModel.mainActivity!!, factory)[VM::class.java]
}
}!!
}
}

View File

@ -5,6 +5,7 @@ import cash.z.ecc.android.ext.ConversionsUniform.LONG_SCALE
import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
@ -25,7 +26,7 @@ import java.util.Locale
*
*/
object ConversionsUniform {
val ONE_ZEC_IN_ZATOSHI = BigDecimal(ZcashSdk.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
val ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
val LONG_SCALE = 8
val SHORT_SCALE = 4
val SHORT_FORMATTER = from(SHORT_SCALE, SHORT_SCALE)
@ -47,11 +48,11 @@ object WalletZecFormmatter {
fun toZatoshi(zecString: String): Long? {
return toBigDecimal(zecString)?.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128)?.toLong()
}
fun toZecStringShort(zatoshi: Long?): String {
return SHORT_FORMATTER.format((zatoshi ?: 0).toZec())
fun toZecStringShort(amount: Zatoshi?): String {
return SHORT_FORMATTER.format((amount ?: Zatoshi(0)).toZec())
}
fun toZecStringFull(zatoshi: Long?): String {
return formatFull((zatoshi ?: 0).toZec())
fun toZecStringFull(amount: Zatoshi?): String {
return formatFull((amount ?: Zatoshi(0)).toZec())
}
fun formatFull(zec: BigDecimal): String {
return FULL_FORMATTER.format(zec)
@ -68,8 +69,8 @@ object WalletZecFormmatter {
}
// convert a zatoshi value to ZEC as a BigDecimal
private fun Long?.toZec(): BigDecimal =
BigDecimal(this ?: 0L, MathContext.DECIMAL128)
private fun Zatoshi?.toZec(): BigDecimal =
BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128)
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
.setScale(LONG_SCALE, ConversionsUniform.roundingMode)
}

View File

@ -7,6 +7,7 @@ import android.widget.EditText
import android.widget.TextView
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.util.twig
fun EditText.onEditorActionDone(block: (EditText) -> Unit) {
@ -72,9 +73,9 @@ inline fun EditText.limitDecimalPlaces(max: Int) {
})
}
fun TextView.convertZecToZatoshi(): Long? {
fun TextView.convertZecToZatoshi(): Zatoshi? {
return try {
text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi() ?: null
text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi()
} catch (t: Throwable) {
twig("Failed to convert text to Zatoshi: $text")
null

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.ext
import android.content.Context
import android.os.Build
import androidx.fragment.app.Fragment
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.util.Bush
import cash.z.ecc.android.util.Twig
@ -32,8 +33,8 @@ fun <T> String.distribute(chunks: Int, block: (Int, String) -> T) {
fun Boolean.asString(ifTrue: String = "", ifFalse: String = "") = if (this) ifTrue else ifFalse
inline val WalletBalance.pending: Long
get() = (this.totalZatoshi - this.availableZatoshi).coerceAtLeast(0)
inline val WalletBalance.pending: Zatoshi
get() = (this.total - this.available)
inline fun <R> tryWithWarning(message: String = "", block: () -> R): R? {
return try {

View File

@ -24,6 +24,7 @@ import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
@ -51,22 +52,22 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
override fun onResume() {
twig("HistoryFragment.onResume")
super.onResume()
viewModel.balance.collectWith(resumedScope) {
viewModel.balance.filterNotNull().collectWith(resumedScope) {
onBalanceUpdated(it)
}
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
}
private fun onBalanceUpdated(balance: WalletBalance) {
if (balance.availableZatoshi < 0) {
if (balance.available.value < 0) {
binding.textBalanceAvailable.text = "Updating"
return
}
binding.textBalanceAvailable.text = WalletZecFormmatter.toZecStringShort(balance.availableZatoshi)
binding.textBalanceAvailable.text = WalletZecFormmatter.toZecStringShort(balance.available)
val change = balance.pending
binding.textBalanceDescription.apply {
goneIf(change <= 0L)
goneIf(change.value <= 0L)
val changeString = WalletZecFormmatter.toZecStringFull(change)
val expecting = R.string.home_banner_expecting.toAppString(true)
val symbol = getString(R.string.symbol)

View File

@ -13,7 +13,9 @@ import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.util.MemoUtil
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.ecc.android.util.twig
@ -77,7 +79,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
else -> null
}
isMined = tx?.minedHeight != null && tx.minedHeight > synchronizer.network.saplingActivationHeight
topValue = if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.value)}"
topValue = if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.valueInZatoshi)}"
minedHeight = String.format("%,d", tx?.minedHeight ?: 0)
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
@ -124,7 +126,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
true -> {
topLabel = getString(R.string.transaction_story_inbound)
bottomLabel = getString(R.string.transaction_story_inbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.value)}"
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.valueInZatoshi)}"
iconRotation = 315f
source = getString(R.string.transaction_story_to_shielded)
address = MemoUtil.findAddressInMemo(tx, (synchronizer as SdkSynchronizer)::isValidAddress)
@ -132,7 +134,7 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
false -> {
topLabel = getString(R.string.transaction_story_outbound)
bottomLabel = getString(R.string.transaction_story_outbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.value?.plus(ZcashSdk.MINERS_FEE_ZATOSHI))}"
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(Zatoshi((tx?.valueInZatoshi?.value ?: 0) + ZcashSdk.MINERS_FEE.value))}"
iconRotation = 135f
fee = "+ 0.00001 network fee"
source = getString(R.string.transaction_story_from_shielded)

View File

@ -15,6 +15,7 @@ import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ext.toAppInt
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.isShielded
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
@ -59,7 +60,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
onTransactionLongPressed(this)
true
}
amountZec = WalletZecFormmatter.toZecStringShort(value)
amountZec = WalletZecFormmatter.toZecStringShort(valueInZatoshi)
// TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L)
val isMined = blockTimeInSeconds != 0L
@ -122,7 +123,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
}
// sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI * 10) amountDisplay = "< 0.0001"
if (value < ZcashSdk.MINERS_FEE.value * 10) amountDisplay = "< 0.0001"
else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
amountDisplay = str(R.string.transaction_instruction_tap)
}

View File

@ -87,7 +87,7 @@ class BalanceDetailFragment : BaseFragment<FragmentBalanceDetailBinding>() {
} else {
val toast = when {
// if funds exist but they're all unconfirmed
(viewModel.latestBalance?.transparentBalance?.totalZatoshi ?: 0) > 0 -> {
(viewModel.latestBalance?.transparentBalance?.total?.value ?: 0) > 0 -> {
"Please wait for more confirmations"
}
viewModel.latestBalance?.hasData() == true -> {

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ext.pending
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
@ -10,10 +9,10 @@ import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.WalletBalance
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.map
import javax.inject.Inject
class BalanceDetailViewModel @Inject constructor() : ViewModel() {
@ -53,40 +52,40 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
}
data class BalanceModel(
val shieldedBalance: WalletBalance = WalletBalance(),
val transparentBalance: WalletBalance = WalletBalance(),
val shieldedBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
var showAvailable: Boolean = false
) {
/** Whether to make calculations based on total or available zatoshi */
val canAutoShield: Boolean = transparentBalance.availableZatoshi > ZcashSdk.MINERS_FEE_ZATOSHI
val canAutoShield: Boolean = (transparentBalance?.available?.value ?: 0L) > ZcashSdk.MINERS_FEE.value
val balanceShielded: String
get() {
return if (showAvailable) shieldedBalance.availableZatoshi.toDisplay()
else shieldedBalance.totalZatoshi.toDisplay()
return if (showAvailable) shieldedBalance?.available.toDisplay()
else shieldedBalance?.total.toDisplay()
}
val balanceTransparent: String
get() {
return if (showAvailable) transparentBalance.availableZatoshi.toDisplay()
else transparentBalance.totalZatoshi.toDisplay()
return if (showAvailable) transparentBalance?.available.toDisplay()
else transparentBalance?.total.toDisplay()
}
val balanceTotal: String
get() {
return if (showAvailable) (shieldedBalance.availableZatoshi + transparentBalance.availableZatoshi).toDisplay()
else (shieldedBalance.totalZatoshi + transparentBalance.totalZatoshi).toDisplay()
return if (showAvailable) ((shieldedBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
else ((shieldedBalance?.total ?: Zatoshi(0)) + (transparentBalance?.total ?: Zatoshi(0))).toDisplay()
}
val paddedShielded get() = pad(balanceShielded)
val paddedTransparent get() = pad(balanceTransparent)
val paddedTotal get() = pad(balanceTotal)
val maxLength get() = maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val hasPending = shieldedBalance.availableZatoshi != shieldedBalance.totalZatoshi ||
transparentBalance.availableZatoshi != transparentBalance.totalZatoshi
private fun Long.toDisplay(): String {
return convertZatoshiToZecString(8, 8)
val hasPending = (null != shieldedBalance && shieldedBalance.available != shieldedBalance.total) ||
(null != transparentBalance && transparentBalance.available != transparentBalance.total)
private fun Zatoshi?.toDisplay(): String {
return this?.convertZatoshiToZecString(8, 8) ?: "0"
}
private fun pad(balance: String): String {
@ -100,11 +99,7 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
}
fun hasData(): Boolean {
val default = WalletBalance()
return shieldedBalance.availableZatoshi != default.availableZatoshi ||
shieldedBalance.totalZatoshi != default.totalZatoshi ||
shieldedBalance.availableZatoshi != default.availableZatoshi ||
shieldedBalance.totalZatoshi != default.totalZatoshi
return shieldedBalance != null || transparentBalance != null
}
}
@ -115,12 +110,12 @@ class BalanceDetailViewModel @Inject constructor() : ViewModel() {
) {
val pendingUnconfirmed = pending.filter { it.isSubmitSuccess() && it.isMined() && !it.isConfirmed(info.lastScannedHeight) }
val pendingUnmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pendingShieldedBalance = balances.shieldedBalance.pending
val pendingTransparentBalance = balances.transparentBalance.pending
val pendingShieldedBalance = balances.shieldedBalance?.pending
val pendingTransparentBalance = balances.transparentBalance?.pending
val hasUnconfirmed = pendingUnconfirmed.isNotEmpty()
val hasUnmined = pendingUnmined.isNotEmpty()
val hasPendingShieldedBalance = pendingShieldedBalance > 0L
val hasPendingTransparentBalance = pendingTransparentBalance > 0L
val hasPendingShieldedBalance = (pendingShieldedBalance?.value ?: 0L) > 0L
val hasPendingTransparentBalance = (pendingTransparentBalance?.value ?: 0L) > 0L
val missingBlocks = (info.networkBlockHeight - info.lastScannedHeight).coerceAtLeast(0)
private fun PendingTransaction.isConfirmed(networkBlockHeight: Int): Boolean {

View File

@ -42,6 +42,7 @@ import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.onFirstWith
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.CANCEL
import cash.z.ecc.android.ui.home.HomeFragment.BannerAction.CLEAR
@ -154,7 +155,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (::uiModel.isInitialized) {
twig("uiModel exists! it has pendingSend=${uiModel.pendingSend} ZEC while the sendViewModel=${sendViewModel.zatoshiAmount} zats")
// if the model already existed, cool but let the sendViewModel be the source of truth for the amount
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount.coerceAtLeast(0))))
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount ?: Zatoshi(0L))))
}
}
@ -278,8 +279,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
binding.buttonSendAmount.disabledIf(amount == "0")
}
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L, availableTransparentBalance: Long = -1L, unminedCount: Int = 0) {
val missingBalance = availableBalance < 0
fun setAvailable(availableBalance: Zatoshi?, totalBalance: Zatoshi?, availableTransparentBalance: Zatoshi?, unminedCount: Int = 0) {
val missingBalance = availableBalance == null
val availableString = if (missingBalance) getString(R.string.home_button_send_updating) else WalletZecFormmatter.toZecStringFull(availableBalance)
binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
@ -288,7 +289,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
goneIf(missingBalance)
text = when {
unminedCount > 0 -> "(excludes $unminedCount unconfirmed ${if (unminedCount > 1) "transactions" else "transaction"})"
availableBalance != -1L && (availableBalance < totalBalance) -> {
availableBalance != null && totalBalance != null && (availableBalance.value < totalBalance.value) -> {
val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
val symbol = getString(R.string.symbol)
"(${getString(R.string.home_banner_expecting)} +$change $symbol)".toColoredSpan(R.color.text_light, "+$change")
@ -296,7 +297,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
else -> getString(R.string.home_instruction_enter_amount)
}
}
binding.imageTransparentAvailable.goneIf(availableTransparentBalance <= 0)
binding.imageTransparentAvailable.goneIf(availableTransparentBalance == null)
}
fun setBanner(message: String = "", action: BannerAction = CLEAR) {
@ -348,8 +349,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (old.processorInfo.lastScanRange != new.processorInfo.lastScanRange) append("${innerComma()}lastScanRange=${new.processorInfo.lastScanRange}")
append(")")
}
if (old.saplingBalance.availableZatoshi != new.saplingBalance.availableZatoshi) append("${maybeComma()}availableBalance=${new.saplingBalance.availableZatoshi}")
if (old.saplingBalance.totalZatoshi != new.saplingBalance.totalZatoshi) append("${maybeComma()}totalBalance=${new.saplingBalance.totalZatoshi}")
if (old.saplingBalance?.available != new.saplingBalance?.available) append("${maybeComma()}availableBalance=${new.saplingBalance?.available}")
if (old.saplingBalance?.total != new.saplingBalance?.total) append("${maybeComma()}totalBalance=${new.saplingBalance?.total}")
if (old.pendingSend != new.pendingSend) append("${maybeComma()}pendingSend=${new.pendingSend}")
append(")")
}
@ -359,7 +360,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
private fun onSyncing(uiModel: HomeViewModel.UiModel) {
setAvailable()
setAvailable(null, null, null)
}
private fun onSynced(uiModel: HomeViewModel.UiModel) {
@ -368,7 +369,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
onNoFunds()
} else {
setBanner("")
setAvailable(uiModel.saplingBalance.availableZatoshi, uiModel.saplingBalance.totalZatoshi, uiModel.transparentBalance.availableZatoshi, uiModel.unminedCount)
setAvailable(uiModel.saplingBalance?.available, uiModel.saplingBalance?.total, uiModel.transparentBalance?.available, uiModel.unminedCount)
}
autoShield(uiModel)
}
@ -392,9 +393,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
// troubleshooting logs
if (uiModel.transparentBalance.availableZatoshi > 0) {
twig("Transparent funds are available but not enough to autoshield. Available: ${uiModel.transparentBalance.availableZatoshi.convertZatoshiToZecString(10)} Required: ${ZcashWalletApp.instance.autoshieldThreshold.convertZatoshiToZecString(8)}")
} else if (uiModel.transparentBalance.totalZatoshi > 0) {
if ((uiModel.transparentBalance?.available?.value ?: 0) > 0) {
twig("Transparent funds are available but not enough to autoshield. Available: ${uiModel.transparentBalance?.available.convertZatoshiToZecString(10)} Required: ${Zatoshi(ZcashWalletApp.instance.autoshieldThreshold).convertZatoshiToZecString(8)}")
} else if ((uiModel.transparentBalance?.total?.value ?: 0) > 0) {
twig("Transparent funds have been received but they require 10 confirmations for autoshielding.")
} else if (!canAutoshield) {
twig("Could not autoshield probably because the last one occurred too recently")
@ -437,6 +438,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// TODO: trigger banner / balance update
onNoFunds()
}
BannerAction.NONE -> TODO()
CLEAR -> TODO()
}
}
@ -445,9 +448,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
private fun monitorUiModelChanges() {
val existingAmount = sendViewModel.zatoshiAmount.coerceAtLeast(0)
val existingAmount = sendViewModel.zatoshiAmount ?: Zatoshi(0)
viewModel.initializeMaybe(WalletZecFormmatter.toZecStringFull(existingAmount))
if (existingAmount == 0L) onClearAmount()
if (existingAmount.value == 0L) onClearAmount()
viewModel.uiModels.runningReduce { old, new ->
onModelUpdated(old, new)
new

View File

@ -14,8 +14,8 @@ 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.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE_ZATOSHI
import cash.z.ecc.android.sdk.ext.ZcashSdk.ZATOSHI_PER_ZEC
import cash.z.ecc.android.sdk.ext.ZcashSdk.MINERS_FEE
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
@ -97,13 +97,13 @@ class HomeViewModel @Inject constructor() : ViewModel() {
UiModel(
status = flows[0] as Synchronizer.Status,
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
orchardBalance = flows[2] as WalletBalance,
saplingBalance = flows[3] as WalletBalance,
transparentBalance = flows[4] as WalletBalance,
orchardBalance = flows[2] as WalletBalance?,
saplingBalance = flows[3] as WalletBalance?,
transparentBalance = flows[4] as WalletBalance?,
pendingSend = flows[5] as String,
unminedCount = unminedCount
)
}.onStart { emit(UiModel()) }
}.onStart { emit(UiModel(orchardBalance = null, saplingBalance = null, transparentBalance = null)) }
}.conflate()
}
@ -119,16 +119,16 @@ class HomeViewModel @Inject constructor() : ViewModel() {
data class UiModel(
val status: Synchronizer.Status = DISCONNECTED,
val processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
val orchardBalance: WalletBalance = WalletBalance(),
val saplingBalance: WalletBalance = WalletBalance(),
val transparentBalance: WalletBalance = WalletBalance(),
val orchardBalance: WalletBalance?,
val saplingBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
val pendingSend: String = "0",
val unminedCount: Int = 0
) {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds: Boolean get() = saplingBalance.availableZatoshi > (MINERS_FEE_ZATOSHI.toDouble() / ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance: Boolean get() = saplingBalance.totalZatoshi > 0
val hasAutoshieldFunds: Boolean get() = transparentBalance.availableZatoshi >= ZcashWalletApp.instance.autoshieldThreshold
val hasFunds: Boolean get() = (saplingBalance?.available?.value ?: 0) > (MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance: Boolean get() = (saplingBalance?.total?.value ?: 0) > 0L
val hasAutoshieldFunds: Boolean get() = (transparentBalance?.available?.value ?: 0) >= ZcashWalletApp.instance.autoshieldThreshold
val isSynced: Boolean get() = status == SYNCED
val isSendEnabled: Boolean get() = isSynced && hasFunds

View File

@ -27,6 +27,7 @@ import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
@ -219,15 +220,15 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
}
private fun onBalanceUpdated(
balance: WalletBalance = WalletBalance(0, 0),
balance: WalletBalance = WalletBalance(Zatoshi(0), Zatoshi(0)),
utxoCount: Int = 0
) {
lastBalance = balance
twig("TRANSPARENT BALANCE: ${balance.availableZatoshi} / ${balance.totalZatoshi}")
binding.textStatus.text = if (balance.availableZatoshi > 0L) {
twig("TRANSPARENT BALANCE: ${balance.available} / ${balance.total}")
binding.textStatus.text = if (balance.available.value > 0L) {
binding.buttonAction.isActivated = true
binding.buttonAction.isEnabled = true
"Balance: ᙇ${balance.availableZatoshi.convertZatoshiToZecString(8)}"
"Balance: ᙇ${balance.available.convertZatoshiToZecString(8)}"
} else {
binding.buttonAction.isActivated = false
binding.buttonAction.isEnabled = true
@ -239,7 +240,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
appendStatus(if (utxoCount == 1) "transaction!" else "transactions!")
}
balance.pending.takeIf { it > 0 }?.let {
balance.pending.takeIf { it.value > 0 }?.let {
appendStatus("\n\n(ᙇ${it.convertZatoshiToZecString()} pending confirmation)")
}
}
@ -267,7 +268,7 @@ class AwesomeFragment : BaseFragment<FragmentAwesomeBinding>() {
model.primaryAction = { onShieldFundsAction() }
}
else -> {
model.status = "Shielding ᙇ${lastBalance?.availableZatoshi.convertZatoshiToZecString()}\n\nPlease do not exit this screen!"
model.status = "Shielding ᙇ${lastBalance?.available.convertZatoshiToZecString()}\n\nPlease do not exit this screen!"
model.showProgress = true
if (isCreating()) {
model.canCancel = true

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.util.twig
@ -45,7 +46,7 @@ class AutoShieldViewModel @Inject constructor() : ViewModel() {
val statuses get() = combineTransform(synchronizer.saplingBalances, synchronizer.pendingTransactions, synchronizer.processorInfo) { balance, pending, info ->
val unconfirmed = pending.filter { !it.isConfirmed(info.networkBlockHeight) }
val unmined = pending.filter { it.isSubmitSuccess() && !it.isMined() }
val pending = balance.pending
val pending = balance?.pending?.value ?: 0
emit(StatusModel(unmined, unconfirmed, pending, info.networkBlockHeight))
}
@ -96,21 +97,21 @@ class AutoShieldViewModel @Inject constructor() : ViewModel() {
}
data class BalanceModel(
val orchardBalance: WalletBalance = WalletBalance(),
val saplingBalance: WalletBalance = WalletBalance(),
val transparentBalance: WalletBalance = WalletBalance(),
val orchardBalance: WalletBalance?,
val saplingBalance: WalletBalance?,
val transparentBalance: WalletBalance?,
) {
val balanceShielded: String = saplingBalance.availableZatoshi.toDisplay()
val balanceTransparent: String = transparentBalance.availableZatoshi.toDisplay()
val balanceTotal: String = (saplingBalance.availableZatoshi + transparentBalance.availableZatoshi).toDisplay()
val canAutoShield: Boolean = transparentBalance.availableZatoshi > 0L
val balanceShielded: String = saplingBalance?.available.toDisplay()
val balanceTransparent: String = transparentBalance?.available.toDisplay()
val balanceTotal: String = ((saplingBalance?.available ?: Zatoshi(0)) + (transparentBalance?.available ?: Zatoshi(0))).toDisplay()
val canAutoShield: Boolean = (transparentBalance?.available?.value ?: 0) > 0L
val maxLength = maxOf(balanceShielded.length, balanceTransparent.length, balanceTotal.length)
val paddedShielded = pad(balanceShielded)
val paddedTransparent = pad(balanceTransparent)
val paddedTotal = pad(balanceTotal)
private fun Long.toDisplay(): String {
private fun Zatoshi?.toDisplay(): String {
return convertZatoshiToZecString(8, 8)
}

View File

@ -20,6 +20,7 @@ import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
import cash.z.ecc.android.sdk.db.entity.isFailure
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.util.twig
import kotlinx.coroutines.flow.launchIn
@ -144,7 +145,8 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
model.showSecondaryButton = true
}
else -> {
model.title = "${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(value)} ${getString(R.string.symbol)} ${getString(R.string.send_final_to)}\n${toAddress.toAbbreviatedAddress()}"
model.title = "${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(
Zatoshi(value))} ${getString(R.string.symbol)} ${getString(R.string.send_final_to)}\n${toAddress.toAbbreviatedAddress()}"
model.showProgress = true
if (isCreating()) {
model.showCloseIcon = false

View File

@ -41,6 +41,7 @@ import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -121,7 +122,7 @@ class SendFragment :
private fun applyViewModel(model: SendViewModel) {
// apply amount
val roundedAmount =
WalletZecFormmatter.toZecStringFull(model.zatoshiAmount.coerceAtLeast(0L))
WalletZecFormmatter.toZecStringFull(model.zatoshiAmount)
binding.textSendAmount.text = "\$$roundedAmount"
// apply address
binding.inputZcashAddress.setText(model.toAddress)
@ -243,7 +244,7 @@ class SendFragment :
override fun onResume() {
super.onResume()
onPrimaryClipChanged()
sendViewModel.synchronizer.saplingBalances.collectWith(resumedScope) {
sendViewModel.synchronizer.saplingBalances.filterNotNull().collectWith(resumedScope) {
onBalanceUpdated(it)
}
binding.inputZcashAddress.text.toString().let {
@ -254,8 +255,8 @@ class SendFragment :
private fun onBalanceUpdated(balance: WalletBalance) {
// binding.textLayoutAmount.helperText =
// "You have ${WalletZecFormmatter.toZecStringFull(balance.availableZatoshi.coerceAtLeast(0L))} available"
maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
availableZatoshi = balance.availableZatoshi
maxZatoshi = (balance.available - ZcashSdk.MINERS_FEE).value
availableZatoshi = balance.available.value
}
override fun onPrimaryClipChanged() {

View File

@ -31,6 +31,7 @@ import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
@ -62,7 +63,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
var fromAddress: String = ""
var toAddress: String = ""
var memo: String = ""
var zatoshiAmount: Long = -1L
var zatoshiAmount: Zatoshi? = null
var includeFromAddress: Boolean = false
set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) {
@ -86,7 +87,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
reportUserInputIssues(memoToSend)
return synchronizer.sendToAddress(
keys[0],
zatoshiAmount,
zatoshiAmount!!,
toAddress,
memoToSend.chunked(ZcashSdk.MAX_MEMO_SIZE).firstOrNull() ?: ""
).onEach {
@ -118,7 +119,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit(context.getString(R.string.send_validation_error_address_invalid))
}
zatoshiAmount < 1 -> {
zatoshiAmount?.let { it.value < 1L } ?: false -> {
emit(context.getString(R.string.send_validation_error_amount_minimum))
}
availableZatoshi == null -> {
@ -127,11 +128,12 @@ class SendViewModel @Inject constructor() : ViewModel() {
availableZatoshi == 0L -> {
emit(context.getString(R.string.send_validation_error_no_available_funds))
}
availableZatoshi > 0 && availableZatoshi < ZcashSdk.MINERS_FEE_ZATOSHI -> {
availableZatoshi > 0 && availableZatoshi.let { it < ZcashSdk.MINERS_FEE.value } ?: false -> {
emit(context.getString(R.string.send_validation_error_dust))
}
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit(context.getString(R.string.send_validation_error_too_much, WalletZecFormmatter.toZecStringFull(maxZatoshi), ZcashWalletApp.instance.getString(R.string.symbol)))
maxZatoshi != null && zatoshiAmount?.let { it.value > maxZatoshi } ?: false -> {
emit(context.getString(R.string.send_validation_error_too_much,
WalletZecFormmatter.toZecStringFull(Zatoshi((maxZatoshi))), ZcashWalletApp.instance.getString(R.string.symbol)))
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit(context.getString(R.string.send_validation_error_memo_length, ZcashSdk.MAX_MEMO_SIZE))
@ -151,7 +153,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
fromAddress = ""
toAddress = ""
memo = ""
zatoshiAmount = -1L
zatoshiAmount = null
includeFromAddress = false
}
@ -183,9 +185,9 @@ class SendViewModel @Inject constructor() : ViewModel() {
private fun reportUserInputIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when {
zatoshiAmount < ZcashSdk.MINERS_FEE_ZATOSHI -> feedback.report(Issue.TinyAmount)
zatoshiAmount < 100 -> feedback.report(Issue.MicroAmount)
zatoshiAmount == 1L -> feedback.report(Issue.MinimumAmount)
(zatoshiAmount?.value ?: 0L) < ZcashSdk.MINERS_FEE.value -> feedback.report(Issue.TinyAmount)
(zatoshiAmount?.value ?: 0L) < 100L -> feedback.report(Issue.MicroAmount)
(zatoshiAmount ?: 0L) == 1L -> feedback.report(Issue.MinimumAmount)
}
memoToSend.length.also {
when {

View File

@ -83,7 +83,7 @@ object Deps {
object Zcash {
const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0"
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.1"
const val SDK = "cash.z.ecc.android:zcash-android-sdk:1.5.0-beta01"
const val SDK = "cash.z.ecc.android:zcash-android-sdk:1.7.0-beta01"
}
object Misc {
const val LOTTIE = "com.airbnb.android:lottie:3.7.0"

View File

@ -4,7 +4,7 @@ pluginManagement {
}
plugins {
id("com.github.ben-manes.versions") version("0.39.0") apply(false)
id("com.github.ben-manes.versions") version ("0.39.0") apply (false)
}
}
@ -18,7 +18,7 @@ dependencyResolutionManagement {
maven("https://jitpack.io")
jcenter()
// Uncomment to use a snapshot version of the SDK, e.g. when the SDK version ends in -SNAPSHOT
// maven("https://oss.sonatype.org/content/repositories/snapshots") {
//maven("https://oss.sonatype.org/content/repositories/snapshots") {
// if (isRepoRestrictionEnabled) {
// content {
// includeGroup("cash.z.ecc.android")
@ -28,7 +28,7 @@ dependencyResolutionManagement {
}
}
rootProject.name="ecc-wallet"
rootProject.name = "ecc-wallet"
includeBuild("build-convention")