Skip to content

Commit b41b715

Browse files
committed
feat: validate custom fee limits & disable invalid speeds
1 parent be2b744 commit b41b715

File tree

3 files changed

+81
-7
lines changed

3 files changed

+81
-7
lines changed

app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeCustomScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fun SendFeeCustomScreen(
5353
onBack = onBack,
5454
onContinue = {
5555
uiState.custom?.let {
56-
viewModel.validateAndProceed()
56+
viewModel.validateCustomFee()
5757
}
5858
},
5959
)

app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ private fun Content(
107107
feeRate = feeRate,
108108
sats = sats,
109109
isSelected = uiState.selected == feeRate,
110-
isDisabled = false, // TODO
111-
onClick = { onSelect(feeRate) },
110+
isDisabled = feeRate in uiState.disabledRates,
111+
onClick = { if (feeRate !in uiState.disabledRates) onSelect(feeRate) },
112112
modifier = Modifier.testTag("fee_${feeRate.name}_button"),
113113
)
114114
}

app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,35 @@ import kotlinx.coroutines.launch
1212
import to.bitkit.R
1313
import to.bitkit.ext.getSatsPerVByteFor
1414
import to.bitkit.models.FeeRate
15+
import to.bitkit.models.Toast
1516
import to.bitkit.models.TransactionSpeed
1617
import to.bitkit.repositories.CurrencyRepo
1718
import to.bitkit.repositories.LightningRepo
19+
import to.bitkit.repositories.WalletRepo
1820
import to.bitkit.ui.components.KEY_DELETE
21+
import to.bitkit.ui.shared.toast.ToastEventBus
1922
import to.bitkit.viewmodels.SendUiState
2023
import javax.inject.Inject
2124

25+
const val MAX_INPUT_VALUE = 999u
26+
2227
@HiltViewModel
2328
class SendFeeViewModel @Inject constructor(
2429
private val lightningRepo: LightningRepo,
2530
private val currencyRepo: CurrencyRepo,
31+
private val walletRepo: WalletRepo,
2632
@ApplicationContext private val context: Context,
2733
) : ViewModel() {
2834
private val _uiState = MutableStateFlow(SendFeeUiState())
2935
val uiState = _uiState.asStateFlow()
3036

3137
private lateinit var sendUiState: SendUiState
38+
private var maxSatsPerVByte: UInt = MAX_INPUT_VALUE
39+
private var maxFee: ULong = 0u
3240

3341
fun init(sendUiState: SendUiState) {
3442
this.sendUiState = sendUiState
43+
this.maxFee = getFeeLimit()
3544
val selected = FeeRate.fromSpeed(sendUiState.speed)
3645
val fees = sendUiState.fees
3746

@@ -42,15 +51,25 @@ class SendFeeViewModel @Inject constructor(
4251
TransactionSpeed.Custom(satsPerVByte)
4352
}
4453
}
54+
calculateMaxSatPerVByte()
55+
val disabledRates = fees.filter { it.value.toULong() > maxFee }.keys.toSet()
4556
_uiState.update {
4657
it.copy(
4758
selected = selected,
4859
fees = fees,
4960
custom = custom,
5061
input = custom.satsPerVByte.toString().takeIf { custom.satsPerVByte > 0u } ?: "",
62+
disabledRates = disabledRates,
5163
)
5264
}
53-
recalculateFee()
65+
updateTotalFeeText()
66+
}
67+
68+
private fun getFeeLimit(): ULong {
69+
val totalBalance = walletRepo.balanceState.value.totalOnchainSats
70+
val halfBalance = (totalBalance.toDouble() * 0.5).toULong()
71+
val remainingFunds = maxOf(0u, totalBalance - sendUiState.amount)
72+
return minOf(halfBalance, remainingFunds)
5473
}
5574

5675
fun onKeyPress(key: String) {
@@ -60,16 +79,51 @@ class SendFeeViewModel @Inject constructor(
6079
else -> if (currentInput.length < 3) (currentInput + key).trimStart('0') else currentInput
6180
}
6281

82+
val satsPerVByte = newInput.toUIntOrNull() ?: 0u
83+
6384
_uiState.update {
6485
it.copy(
6586
input = newInput,
66-
custom = TransactionSpeed.Custom(newInput.toUIntOrNull() ?: 0u)
87+
custom = TransactionSpeed.Custom(satsPerVByte),
6788
)
6889
}
69-
recalculateFee()
90+
updateTotalFeeText()
7091
}
7192

72-
private fun recalculateFee() {
93+
fun validateCustomFee() {
94+
viewModelScope.launch {
95+
val isValid = performValidation()
96+
_uiState.update { it.copy(isCustomFeeValid = isValid) }
97+
}
98+
}
99+
100+
private suspend fun performValidation(): Boolean {
101+
val satsPerVByte = _uiState.value.custom?.satsPerVByte ?: 0u
102+
103+
// TODO update to use minimum instead of slow when using mempool api
104+
val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u
105+
if (satsPerVByte < minSatsPerVByte) {
106+
ToastEventBus.send(
107+
type = Toast.ToastType.INFO,
108+
title = context.getString(R.string.wallet__min_possible_fee_rate),
109+
description = context.getString(R.string.wallet__min_possible_fee_rate_msg)
110+
)
111+
return false
112+
}
113+
114+
if (satsPerVByte > maxSatsPerVByte) {
115+
ToastEventBus.send(
116+
type = Toast.ToastType.INFO,
117+
title = context.getString(R.string.wallet__max_possible_fee_rate),
118+
description = context.getString(R.string.wallet__max_possible_fee_rate_msg)
119+
)
120+
return false
121+
}
122+
123+
return true
124+
}
125+
126+
private fun updateTotalFeeText() {
73127
viewModelScope.launch {
74128
val satsPerVByte = _uiState.value.custom?.satsPerVByte ?: 0u
75129
val totalFee = if (satsPerVByte > 0u) calculateTotalFee(satsPerVByte).toLong() else 0L
@@ -90,6 +144,24 @@ class SendFeeViewModel @Inject constructor(
90144
}
91145
}
92146

147+
private fun calculateMaxSatPerVByte() {
148+
viewModelScope.launch {
149+
val feeFor1SatPerVByte = lightningRepo.calculateTotalFee(
150+
amountSats = sendUiState.amount,
151+
address = sendUiState.address,
152+
speed = TransactionSpeed.Custom(1u),
153+
utxosToSpend = sendUiState.selectedUtxos,
154+
feeRates = sendUiState.feeRates,
155+
).getOrDefault(0uL)
156+
157+
maxSatsPerVByte = if (feeFor1SatPerVByte > 0uL) {
158+
(maxFee / feeFor1SatPerVByte).toUInt().coerceAtLeast(1u)
159+
} else {
160+
MAX_INPUT_VALUE
161+
}
162+
}
163+
}
164+
93165
suspend fun calculateTotalFee(satsPerVByte: UInt): ULong {
94166
return lightningRepo.calculateTotalFee(
95167
amountSats = sendUiState.amount,
@@ -107,4 +179,6 @@ data class SendFeeUiState(
107179
val custom: TransactionSpeed.Custom? = null,
108180
val input: String = "",
109181
val totalFeeText: String = "",
182+
val disabledRates: Set<FeeRate> = emptySet(),
183+
val isCustomFeeValid: Boolean? = null,
110184
)

0 commit comments

Comments
 (0)