@@ -12,26 +12,35 @@ import kotlinx.coroutines.launch
1212import to.bitkit.R
1313import to.bitkit.ext.getSatsPerVByteFor
1414import to.bitkit.models.FeeRate
15+ import to.bitkit.models.Toast
1516import to.bitkit.models.TransactionSpeed
1617import to.bitkit.repositories.CurrencyRepo
1718import to.bitkit.repositories.LightningRepo
19+ import to.bitkit.repositories.WalletRepo
1820import to.bitkit.ui.components.KEY_DELETE
21+ import to.bitkit.ui.shared.toast.ToastEventBus
1922import to.bitkit.viewmodels.SendUiState
2023import javax.inject.Inject
2124
25+ const val MAX_INPUT_VALUE = 999u
26+
2227@HiltViewModel
2328class 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