Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.flipcash.shared.tokens.R
import com.getcode.opencode.exchange.Exchange
import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.rounded
import com.getcode.opencode.model.financial.Rate
import com.getcode.opencode.model.financial.TokenWithLocalizedBalance
import com.getcode.opencode.model.financial.sum
Expand Down Expand Up @@ -63,7 +62,11 @@ class SelectTokenViewModel @Inject constructor(
)
}

return set.map { it.balance.rounded() }.sum()
// Sum the UNROUNDED per-token values, letting display formatting round the total —
// i.e. round(sum(x)), not sum(round(x)). Rounding each token to cents first drifts the
// total by up to a penny and diverged from iOS (ExchangedFiat.total sums unrounded) and
// from our own aggregateAppreciation (which already sums unrounded).
return set.map { it.balance }.sum()
}

val aggregateAppreciation: LocalFiat?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.flipcash.app.tokens.ui

import com.flipcash.app.core.tokens.TokenPurpose
import com.getcode.opencode.model.financial.CurrencyCode
import com.getcode.opencode.model.financial.Fiat
import com.getcode.opencode.model.financial.LocalFiat
import com.getcode.opencode.model.financial.Rate
import com.getcode.opencode.model.financial.Token
import com.getcode.opencode.model.financial.TokenWithLocalizedBalance
import com.getcode.opencode.model.financial.usdf
import com.getcode.solana.keys.Mint
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test

class SelectTokenViewModelStateTest {

private fun tokenWorth(nativeUsd: Double): TokenWithLocalizedBalance {
val amount = Fiat(nativeUsd, CurrencyCode.USD)
return TokenWithLocalizedBalance(
token = Token.usdf,
balance = LocalFiat(
underlyingTokenAmount = amount,
nativeAmount = amount,
rate = Rate.oneToOne,
mint = Mint.usdf,
),
)
}

/**
* The wallet total must be round(sum(x)), not sum(round(x)): rounding each token to cents first
* drifts the total by up to a penny and diverged from iOS (ExchangedFiat.total sums unrounded).
*
* Two tokens each worth $0.014 display as $0.01 apiece — so sum(round) is $0.02 — but their true
* sum is $0.028, which rounds to $0.03.
*/
@Test
fun `total sums unrounded values, then rounds for display`() {
val state = SelectTokenViewModel.State(
purpose = TokenPurpose.Balance,
rate = Rate.oneToOne,
tokens = listOf(tokenWorth(0.014), tokenWorth(0.014)),
)

val total = state.totalBalance!!.nativeAmount

// Unrounded sum is retained; display formatting rounds it to $0.03.
assertEquals(0.028, total.decimalValue, 1e-3)
assertEquals(0.03, total.rounded(2).decimalValue, 1e-6)

// Regression guard: rounding per token first (the old behaviour) would have shown $0.02.
val sumOfRounded = state.tokens!!.sumOf { it.balance.nativeAmount.rounded(2).decimalValue }
assertEquals(0.02, sumOfRounded, 1e-6)
assertNotEquals(sumOfRounded, total.rounded(2).decimalValue, 1e-6)
}
}
Loading