Keep "currency to Sats" empty when the BTC rate is, and whole-Sats only

Clearing the Manual rate cleared pricedCurrencyCodes but left
rateDisplays stale, so "currency to Sats" kept showing a value
computed from the old rate even after "BTC to currency" went empty.
refresh()'s early-return path now clears rateDisplays too.

oneCurrencyToSats() also now derives from defaultCurrencyRate()
directly instead of re-reading rateDisplays independently, so the
two rates structurally can't disagree about whether a rate is known,
and formats to a whole number of Sats (0 decimals) instead of 2 —
fractional Sats aren't a meaningful unit to show.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXcrACM5sok3M1KQJ8kByy
This commit is contained in:
2026-09-10 10:21:26 +03:00
co-authored by Claude Sonnet 5
parent 161c219d60
commit bf91abdd12
3 changed files with 51 additions and 5 deletions
@@ -26,10 +26,14 @@ fun ConverterUiState.exceedsMaxSupply(): Boolean {
fun ConverterUiState.defaultCurrencyRate(): String =
rateDisplays[defaultCurrencyCode]?.takeIf { it.isNotEmpty() } ?: ""
/** The current "1 [ConverterUiState.defaultCurrencyCode] = ? Sats" rate, as a plain number. */
/**
* The current "1 [ConverterUiState.defaultCurrencyCode] = ? Sats" rate, as a whole (never
* fractional) number of Sats — empty whenever [defaultCurrencyRate] is, so the two rates never
* disagree about whether a rate is currently known.
*/
fun ConverterUiState.oneCurrencyToSats(): String {
val rate = rateDisplays[defaultCurrencyCode]?.toBigDecimalOrNull() ?: return ""
return CurrencyConverter.satsPerCurrencyUnit(rate)?.let { formatAmount(it, 2) } ?: ""
val rate = defaultCurrencyRate().toBigDecimalOrNull() ?: return ""
return CurrencyConverter.satsPerCurrencyUnit(rate)?.let { formatAmount(it, 0) } ?: ""
}
/** The pinned, non-removable "Current Currency" shown in the currency picker's own section. */
@@ -143,10 +143,16 @@ class PriceViewModel(
fun refresh() {
if (currentSource === manualSource && manualSource.rate == null) {
// No rate typed in (yet): nothing to price fiat currencies with, so clear any stale
// rate this might otherwise still compute fiat amounts from.
// rate this might otherwise still compute fiat amounts (or the derived "currency to
// Sats" rate) from.
rates = null
_uiState.update {
it.copy(isLoading = false, errorMessage = null, pricedCurrencyCodes = emptySet())
it.copy(
isLoading = false,
errorMessage = null,
pricedCurrencyCodes = emptySet(),
rateDisplays = emptyMap(),
)
}
return
}
@@ -0,0 +1,36 @@
package xyz.tyiu.satsprice.ui
import kotlin.test.Test
import kotlin.test.assertEquals
class ConverterUiStateDisplayTest {
@Test
fun oneCurrencyToSats_isInverseOfTheBtcRate() {
val state = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("USD" to "50000.00"))
// 1 BTC = 50,000 USD, so 1 USD is worth 100,000,000 / 50,000 = 2,000 Sats.
assertEquals("2000", state.oneCurrencyToSats())
}
@Test
fun oneCurrencyToSats_roundsToAWholeNumberOfSats() {
// 1 BTC = 78,293.19 USD, so 1 USD is worth ~1,277.24 Sats — never shown fractionally.
val state = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("USD" to "78293.19"))
assertEquals("1277", state.oneCurrencyToSats())
}
@Test
fun oneCurrencyToSats_isEmptyWheneverDefaultCurrencyRateIs() {
// No rate for the default currency at all (e.g. Manual with nothing typed in yet).
val noRate = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = emptyMap())
assertEquals("", noRate.defaultCurrencyRate())
assertEquals("", noRate.oneCurrencyToSats())
// A rate is known, just not for the default currency.
val otherCurrencyOnly = ConverterUiState(defaultCurrencyCode = "USD", rateDisplays = mapOf("EUR" to "45000"))
assertEquals("", otherCurrencyOnly.defaultCurrencyRate())
assertEquals("", otherCurrencyOnly.oneCurrencyToSats())
}
}