Skip to content

Commit a28add8

Browse files
committed
feat(accounts): add usd cost basis to support currency appreciation
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 36c659e commit a28add8

9 files changed

Lines changed: 100 additions & 10 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fun TokenBalanceRow(
6060
contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.inset),
6161
onClick: (() -> Unit)? = null,
6262
) {
63-
val (token, balance, displayName) = tokenWithBalance
63+
val (token, balance, _, displayName) = tokenWithBalance
6464
TokenBalanceRow(
6565
token = token,
6666
displayName = displayName,
@@ -92,7 +92,7 @@ fun TokenBalanceRow(
9292
contentPadding: PaddingValues = PaddingValues(vertical = CodeTheme.dimens.inset),
9393
onClick: (() -> Unit)? = null,
9494
) {
95-
val (token, balance, displayName) = tokenWithBalance
95+
val (token, balance, _, displayName) = tokenWithBalance
9696
TokenBalanceRow(
9797
token = token,
9898
displayName = displayName,

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,8 @@
400400

401401
<string name="error_title_buySellFailed">Something Went Wrong</string>
402402
<string name="error_description_buySellFailed">Please try again</string>
403+
404+
<string name="label_fromCurrencyAppreciation">from currency appreciation</string>
405+
<string name="label_fromCurrencyDepreciation">from currency depreciation</string>
406+
403407
</resources>

apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/BalanceScreenContent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ private fun BalanceScreenContent(
6363
BalanceHeader(
6464
modifier = Modifier
6565
.fillMaxWidth(),
66-
balance = tokenState.totalBalance
66+
balance = tokenState.totalBalance,
67+
appreciation = tokenState.aggregateAppreciation,
6768
) {
6869
dispatchEvent(BalanceViewModel.Event.OpenCurrencySelection)
6970
}

apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/BalanceHeader.kt

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
package com.flipcash.app.balance.internal.components
22

33
import androidx.compose.animation.Crossfade
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
46
import androidx.compose.foundation.layout.Box
57
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
69
import androidx.compose.foundation.layout.fillMaxWidth
710
import androidx.compose.foundation.layout.padding
11+
import androidx.compose.material.MaterialTheme
12+
import androidx.compose.material.Text
813
import androidx.compose.runtime.Composable
914
import androidx.compose.ui.Alignment
1015
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.draw.alpha
1117
import androidx.compose.ui.res.stringResource
18+
import androidx.compose.ui.unit.dp
19+
import com.flipcash.app.tokens.data.Period
1220
import com.flipcash.features.balance.R
1321
import com.getcode.opencode.compose.LocalExchange
1422
import com.getcode.opencode.model.financial.CurrencyCode
23+
import com.getcode.opencode.model.financial.Fiat
1524
import com.getcode.opencode.model.financial.LocalFiat
1625
import com.getcode.theme.CodeTheme
26+
import com.getcode.theme.extraSmall
1727
import com.getcode.ui.components.text.AmountArea
1828
import com.getcode.ui.theme.CodeCircularProgressIndicator
1929

2030
@Composable
2131
internal fun BalanceHeader(
2232
balance: LocalFiat?,
33+
appreciation: LocalFiat?,
2334
modifier: Modifier = Modifier,
2435
onClick: () -> Unit
2536
) {
@@ -28,6 +39,7 @@ internal fun BalanceHeader(
2839
modifier = modifier
2940
.padding(horizontal = CodeTheme.dimens.inset)
3041
.padding(vertical = CodeTheme.dimens.grid.x9),
42+
horizontalAlignment = Alignment.CenterHorizontally,
3143
) {
3244
if (balance == null) {
3345
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
@@ -45,6 +57,50 @@ internal fun BalanceHeader(
4557
onClick = onClick
4658
)
4759
}
60+
61+
if (appreciation != null) {
62+
Crossfade(appreciation.nativeAmount) { amount ->
63+
val hasAppreciation = amount.decimalValue >= 0
64+
val changeColor = if (hasAppreciation) {
65+
CodeTheme.colors.successText
66+
} else {
67+
CodeTheme.colors.errorText
68+
}
69+
70+
Row(
71+
verticalAlignment = Alignment.CenterVertically,
72+
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1),
73+
) {
74+
Text(
75+
modifier = Modifier
76+
.background(
77+
color = changeColor.copy(0.20f),
78+
shape = MaterialTheme.shapes.extraSmall,
79+
)
80+
.padding(
81+
vertical = 2.dp,
82+
horizontal = CodeTheme.dimens.grid.x1
83+
),
84+
text = amount.formatted(
85+
extraPrefix = if (hasAppreciation) "+" else "-",
86+
),
87+
style = CodeTheme.typography.textSmall,
88+
color = changeColor,
89+
)
90+
91+
val label = if (hasAppreciation) {
92+
stringResource(R.string.label_fromCurrencyAppreciation)
93+
} else {
94+
stringResource(R.string.label_fromCurrencyDepreciation)
95+
}
96+
Text(
97+
text = label,
98+
style = CodeTheme.typography.textSmall,
99+
color = changeColor,
100+
)
101+
}
102+
}
103+
}
48104
}
49105
}
50106
}

apps/flipcash/shared/tokens/src/main/kotlin/SelectTokenViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class SelectTokenViewModel @Inject constructor(
5757
) {
5858
val totalBalance: LocalFiat
5959
get() = tokens.orEmpty().map { it.balance }.sum()
60+
61+
val aggregateAppreciation: LocalFiat?
62+
get() = tokens?.map { it.appreciation }?.sum()
6063
}
6164

6265
sealed interface Event {
@@ -115,7 +118,11 @@ class SelectTokenViewModel @Inject constructor(
115118
balance = LocalFiat(
116119
usdf = it.balance,
117120
nativeAmount = it.balance.convertingTo(rate),
118-
)
121+
),
122+
appreciation = LocalFiat(
123+
usdf = it.appreciation,
124+
nativeAmount = it.appreciation.convertingTo(rate),
125+
),
119126
)
120127
}
121128
.sortedWith(compareByDescending { item ->

definitions/opencode/protos/src/main/proto/account/v1/account_service.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,8 @@ message TokenAccountInfo {
207207
// For REMOTE_SEND_GIFT_CARD, if requesting_owner was provided, was
208208
// requesting_owner the issuer of the account.
209209
bool is_gift_card_issuer = 14;
210+
211+
// The USD cost basis for this account, which can be used to compute currency
212+
// appreciation/depreciation
213+
double usd_cost_basis = 15;
210214
}

services/opencode/src/main/kotlin/com/getcode/opencode/controllers/TokenController.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import kotlinx.coroutines.flow.map
4444
import kotlinx.coroutines.flow.mapNotNull
4545
import kotlinx.coroutines.flow.onEach
4646
import kotlinx.coroutines.flow.update
47-
import org.checkerframework.checker.units.qual.min
4847
import javax.inject.Inject
4948
import javax.inject.Singleton
5049
import kotlin.concurrent.atomics.AtomicBoolean
@@ -82,13 +81,16 @@ class TokenController @Inject constructor(
8281
return tokenFetchState[mint]?.load() ?: false
8382
}
8483

84+
private val mintUsdAppreciationMap = MutableStateFlow(mapOf<Mint, Fiat>())
85+
8586
private val mintBalances = MutableStateFlow(mapOf<Mint, Fiat>())
8687
val tokens = MutableStateFlow(emptyList<Token>())
8788

8889
val tokenBalances: Flow<List<TokenWithBalance>> = mintBalances.map {
8990
it.mapNotNull { (mint, balance) ->
9091
val token = tokens.value.find { it.address == mint } ?: return@mapNotNull null
91-
TokenWithBalance(token, balance)
92+
val appreciation = mintUsdAppreciationMap.value[mint] ?: Fiat.Zero
93+
TokenWithBalance(token, balance, appreciation)
9294
}
9395
}
9496

@@ -242,18 +244,25 @@ class TokenController @Inject constructor(
242244
.firstOrNull() ?: return@mapNotNull null
243245

244246
val tokenBalance = Fiat.tokenBalance(account.balance, token = token)
247+
val costBasis = Fiat(fiat = account.usdCostBasis)
248+
249+
TokenWithBalance(
250+
token = token,
251+
balance = tokenBalance,
252+
appreciation = tokenBalance - costBasis
245253

246-
TokenWithBalance(token, tokenBalance)
254+
)
247255
}
248256
}?.onSuccess { tokensWithBalance ->
249-
tokensWithBalance.onEach { (token, balance) ->
257+
tokensWithBalance.onEach { (token, balance, appreciation) ->
250258
trace(
251259
tag = "Tokens",
252260
message = "Updated balance for ${token.symbol} is ${balance.formatted()} USD",
253261
type = TraceType.Process
254262
)
255263

256264
mintBalances.update { it + (token.address to balance) }
265+
mintUsdAppreciationMap.update { it + (token.address to appreciation) }
257266
tokens.update { (it + token).distinctBy { t -> t.address } }
258267
}
259268

services/opencode/src/main/kotlin/com/getcode/opencode/model/accounts/AccountInfo.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ data class AccountInfo(
9494
* For REMOTE_SEND_GIFT_CARD, if requesting_owner was provided, was
9595
* requesting_owner the issuer of the account.
9696
*/
97-
val isGiftCardIssuer: Boolean
97+
val isGiftCardIssuer: Boolean,
98+
99+
/**
100+
* The USD cost basis for this account, which can be used to compute currency appreciation/depreciation
101+
*/
102+
val usdCostBasis: Double,
98103
) {
99104
companion object {
100105
fun newInstance(info: AccountService.TokenAccountInfo): AccountInfo? {
@@ -126,7 +131,9 @@ data class AccountInfo(
126131
originalExchangeData = exchangeData,
127132
mint = info.mint.toMint(),
128133
createdAt = info.createdAt.seconds * 1000L,
129-
isGiftCardIssuer = info.isGiftCardIssuer
134+
isGiftCardIssuer = info.isGiftCardIssuer,
135+
usdCostBasis = info.usdCostBasis,
136+
130137
)
131138
}
132139
}

services/opencode/src/main/kotlin/com/getcode/opencode/model/financial/MintMetadata.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlin.time.Instant
1515
data class TokenWithBalance(
1616
val token: Token,
1717
val balance: Fiat,
18+
val appreciation: Fiat = Fiat.Zero,
1819
val displayName: String = token.name,
1920
) {
2021
val isReserves: Boolean
@@ -24,6 +25,7 @@ data class TokenWithBalance(
2425
data class TokenWithLocalizedBalance(
2526
val token: Token,
2627
val balance: LocalFiat,
28+
val appreciation: LocalFiat = LocalFiat.Zero,
2729
val displayName: String = token.name,
2830
) {
2931
val isReserves: Boolean

0 commit comments

Comments
 (0)