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 @@ -31,6 +31,7 @@ import org.bitcoinppl.cove.flows.CoinControlFlow.displayDate
import org.bitcoinppl.cove.flows.CoinControlFlow.displayName
import org.bitcoinppl.cove.ui.theme.CoveColor
import org.bitcoinppl.cove_core.SendFlowManagerAction
import org.bitcoinppl.cove_core.ffiConservativeDustLimitSats
import org.bitcoinppl.cove_core.types.Amount
import org.bitcoinppl.cove_core.types.Utxo
import org.bitcoinppl.cove_core.types.UtxoType
Expand Down Expand Up @@ -66,21 +67,26 @@ fun CoinControlCustomAmountSheet(
var debounceJob: Job? by remember { mutableStateOf(null) }

// min/max calculations
val minSendSats = 546L
val minSend = if (isSats) minSendSats.toDouble() else minSendSats / 100_000_000.0
val conservativeDustLimitSatsU = remember { ffiConservativeDustLimitSats() }
val conservativeDustLimitSats = conservativeDustLimitSatsU.toLong()
val minSend = if (isSats) conservativeDustLimitSats.toDouble() else conservativeDustLimitSats.toDouble() / 100_000_000.0

val step = if (isSats) 10.0 else 0.0000001

val maxSend =
remember(sendFlowManager.amount, unit) {
val amount = sendFlowManager.maxSendMinusFees() ?: Amount.fromSat(minSendSats.toULong() + 1000u)
var amount = sendFlowManager.maxSendMinusFees() ?: Amount.fromSat(conservativeDustLimitSatsU + 1_000uL)
if (amount.asSats() <= conservativeDustLimitSatsU) {
amount = Amount.fromSat(conservativeDustLimitSatsU + 1_000uL)
}

val value = if (isSats) amount.asSats().toDouble() else amount.asBtc()
value.coerceAtLeast(minSend)
}
Comment thread
praveenperera marked this conversation as resolved.

val softMaxSend =
remember(sendFlowManager.amount, unit) {
val amount = sendFlowManager.maxSendMinusFeesAndSmallUtxo() ?: Amount.fromSat(minSendSats.toULong())
val amount = sendFlowManager.maxSendMinusFeesAndSmallUtxo() ?: Amount.fromSat(conservativeDustLimitSatsU)
val value = if (isSats) amount.asSats().toDouble() else amount.asBtc()
value.coerceAtLeast(minSend)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,8 @@ private fun SendFlowRouteToScreen(

// validation alert dialog (shown across all send routes)
if (presenter.isShowingAlert) {
val alert = presenter.alertState?.item

AlertDialog(
onDismissRequest = {
presenter.setDisappearing()
Expand All @@ -651,13 +653,42 @@ private fun SendFlowRouteToScreen(
title = { Text(presenter.alertTitle()) },
text = { Text(presenter.alertMessage()) },
confirmButton = {
TextButton(
onClick = {
presenter.setDisappearing()
presenter.alertButtonAction()?.invoke()
},
) {
Text("OK")
when (alert) {
is SendFlowAlertState.Warning -> {
TextButton(
onClick = {
presenter.setDisappearing()
presenter.alertState = null
sendFlowManager.dispatch(
SendFlowManagerAction.AcknowledgeWarningAndFinalize(alert.kind),
)
},
) {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Text("Send Anyway")
}
}
else -> {
TextButton(
onClick = {
presenter.setDisappearing()
presenter.alertButtonAction()?.invoke()
},
) {
Text("OK")
}
}
}
},
dismissButton = {
if (alert is SendFlowAlertState.Warning) {
TextButton(
onClick = {
presenter.setDisappearing()
presenter.alertState = null
},
) {
Text("Cancel")
}
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ class SendFlowManager internal constructor(
*/
fun validate(displayAlert: Boolean = false): Boolean {
if (isClosed.get()) return false
return validateAmount(displayAlert) &&
validateAddress(displayAlert) &&
validateFeePercentage(displayAlert)
return validateAmount(displayAlert) && validateAddress(displayAlert)
}

suspend fun waitForInit(): Boolean =
Expand Down Expand Up @@ -224,11 +222,6 @@ class SendFlowManager internal constructor(
validateAmount(displayAlert)
}

fun validateFeePercentage(displayAlert: Boolean = false): Boolean =
withRustOr(false) {
validateFeePercentage(displayAlert)
}

fun updateAddress(address: Address) {
if (isClosed.get()) return
_enteringAddress = address.unformatted()
Expand Down Expand Up @@ -316,25 +309,24 @@ class SendFlowManager internal constructor(
is SendFlowManagerReconcileMessage.SetAlert -> {
logWarn("setAlert: ${message.v1}")

// capture previous state before modifying
val hadSheet = presenter.sheetState != null
val hadAlert = presenter.alertState != null
val isDismissingAlert = presenter.isDisappearing

presenter.alertState = TaggedItem(message.v1)

// handle alert/sheet conflict - delay only if there was a previous conflict
if (hadSheet || hadAlert) {
presenter.alertState = null
if (hadSheet || hadAlert || isDismissingAlert) {
presenter.clearAlert()
presenter.sheetState = null
mainScope.launch {
delay(ALERT_PRESENTATION_DELAY_MS)
presenter.alertState = TaggedItem(message.v1)
}
} else {
presenter.alertState = TaggedItem(message.v1)
}
}

is SendFlowManagerReconcileMessage.ClearAlert -> {
presenter.alertState = null
presenter.clearAlert()
}

is SendFlowManagerReconcileMessage.SetMaxSelected -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
Expand All @@ -24,6 +25,7 @@ class SendFlowPresenter(
) : Closeable {
// prevents alerts from reappearing during dismissal animations
private var disappearing by mutableStateOf(false)
private var disappearingResetJob: Job? = null

private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private val isClosed = AtomicBoolean(false)
Expand All @@ -35,6 +37,9 @@ class SendFlowPresenter(
val isShowingAlert: Boolean
get() = alertState != null && !disappearing

val isDisappearing: Boolean
get() = disappearing

var lastWorkingFeeRate by mutableStateOf<Float?>(null)
var erroredFeeRate by mutableStateOf<Float?>(null)

Expand All @@ -58,20 +63,30 @@ class SendFlowPresenter(
}

fun setDisappearing() {
disappearingResetJob?.cancel()
disappearing = true
mainScope.launch {
disappearingResetJob = mainScope.launch {
delay(500)
disappearing = false
}
}

fun clearAlert() {
if (alertState != null) {
setDisappearing()
}

alertState = null
}

/**
* get alert title based on alert state
*/
fun alertTitle(): String =
when (val state = alertState?.item) {
is SendFlowAlertState.Error -> errorAlertTitle(state.v1)
is SendFlowAlertState.General -> state.title
is SendFlowAlertState.Warning -> state.title
null -> ""
}

Expand All @@ -90,7 +105,7 @@ class SendFlowPresenter(
is SendFlowException.NoBalance,
-> "Insufficient Funds"

is SendFlowException.SendAmountToLow -> "Send Amount Too Low"
is SendFlowException.SendBelowDustLimit -> "Amount Below Dust Limit"
is SendFlowException.UnableToGetFeeRate -> "Unable to get fee rate"
is SendFlowException.UnableToBuildTxn -> "Unable to build transaction"
is SendFlowException.UnableToGetMaxSend -> "Unable to get max send"
Expand All @@ -110,6 +125,7 @@ class SendFlowPresenter(
when (val state = alertState?.item) {
is SendFlowAlertState.Error -> errorAlertMessage(state.v1)
is SendFlowAlertState.General -> state.message
is SendFlowAlertState.Warning -> state.message
null -> ""
}

Expand All @@ -136,8 +152,8 @@ class SendFlowPresenter(
is SendFlowException.InsufficientFunds ->
"You do not have enough bitcoin in your wallet to cover the amount plus fees"

is SendFlowException.SendAmountToLow ->
"Send amount is too low. Please send at least 5000 sats"
is SendFlowException.SendBelowDustLimit ->
"This amount is below Bitcoin's dust limit for this address type. The network would reject it. Please send a bit more."

is SendFlowException.UnableToGetFeeRate ->
"Are you connected to the internet?"
Expand Down Expand Up @@ -171,6 +187,9 @@ class SendFlowPresenter(
is SendFlowAlertState.General -> {
{ alertState = null }
}
is SendFlowAlertState.Warning -> {
{ alertState = null }
}
null -> null
}

Expand All @@ -195,7 +214,7 @@ class SendFlowPresenter(

is SendFlowException.InvalidNumber,
is SendFlowException.InsufficientFunds,
is SendFlowException.SendAmountToLow,
is SendFlowException.SendBelowDustLimit,
is SendFlowException.ZeroAmount,
is SendFlowException.WalletManager,
is SendFlowException.UnableToGetFeeDetails,
Expand All @@ -213,6 +232,8 @@ class SendFlowPresenter(

override fun close() {
if (!isClosed.compareAndSet(false, true)) return
disappearingResetJob?.cancel()
disappearingResetJob = null
mainScope.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.bitcoinppl.cove.ui.theme.title3
import org.bitcoinppl.cove.utils.toColor
import org.bitcoinppl.cove_core.SendFlowAlertState
import org.bitcoinppl.cove_core.SendFlowException
import org.bitcoinppl.cove_core.WalletManagerException
import org.bitcoinppl.cove_core.types.Amount
import org.bitcoinppl.cove_core.types.FeeRate
import org.bitcoinppl.cove_core.types.FeeRateOptionWithTotalFee
Expand All @@ -53,6 +54,13 @@ private object CustomFeeRateConstants {
const val ALERT_DELAY_MS = 850L
}

internal fun isTooHighCustomFeeError(error: SendFlowException): Boolean =
when (error) {
is SendFlowException.InsufficientFunds -> true
is SendFlowException.WalletManager -> error.v1 is WalletManagerException.InsufficientFunds
else -> false
}

/** custom fee rate sheet - allows user to set custom sats/vbyte with slider */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand Down Expand Up @@ -141,8 +149,16 @@ fun CustomFeeRateSheet(
updatedFeeOptions = updatedFeeOptions.addCustomFeeRate(feeRateOption)
presenter.lastWorkingFeeRate = feeRate
}
} catch (e: SendFlowException.WalletManager) {
// handle insufficient funds - set max fee rate
} catch (e: SendFlowException) {
if (!isTooHighCustomFeeError(e)) {
android.util.Log.e(
"CustomFeeRateSheet",
"Unexpected error calculating fee: ${e.javaClass.simpleName} - ${e.message}",
e,
)
return@withContext
}

withContext(Dispatchers.Main) {
presenter.erroredFeeRate = feeRate

Expand Down
Loading
Loading