Improves the Redeeming feedback

Improves fee calculation from the mint
This commit is contained in:
Vitor Pamplona
2023-12-02 11:18:01 -05:00
parent 81a3baa41c
commit 61449774a1
2 changed files with 115 additions and 35 deletions
@@ -18,8 +18,6 @@ data class CashuToken(
val token: String,
val mint: String,
val totalAmount: Long,
val fees: Int,
val redeemInvoiceAmount: Long,
val proofs: JsonNode
)
@@ -38,10 +36,8 @@ class CashuProcessor {
for (proof in proofs) {
totalAmount += proof.get("amount").asLong()
}
val fees = Math.max(((totalAmount * 0.02).toInt()), 2)
val redeemInvoiceAmount = totalAmount - fees
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, fees, redeemInvoiceAmount, proofs))
return GenericLoadable.Loaded(CashuToken(cashuToken, mint, totalAmount, proofs))
} catch (e: Exception) {
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
}
@@ -53,10 +49,29 @@ class CashuProcessor {
runCatching {
LightningAddressResolver().lnAddressInvoice(
lnaddress = lud16,
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, onSuccess, onError, context)
milliSats = token.totalAmount * 1000, // Make invoice and leave room for fees
message = "Calculate Fees for Cashu",
onSuccess = { baseInvoice ->
feeCalculator(
token.mint,
baseInvoice,
onSuccess = { fees ->
LightningAddressResolver().lnAddressInvoice(
lnaddress = lud16,
milliSats = (token.totalAmount - fees) * 1000, // Make invoice and leave room for fees
message = "Redeem Cashu",
onSuccess = { invoice ->
meltInvoice(token, invoice, fees, onSuccess, onError, context)
},
onProgress = {
},
onError = onError,
context = context
)
},
onError = onError,
context
)
},
onProgress = {
},
@@ -66,7 +81,59 @@ class CashuProcessor {
}
}
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
fun feeCalculator(
mintAddress: String,
invoice: String,
onSuccess: (Int) -> Unit,
onError: (String, String) -> Unit,
context: Context
) {
checkNotInMainThread()
try {
val client = HttpClient.getHttpClient()
val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint
val factory = Event.mapper.nodeFactory
val jsonObject = factory.objectNode()
jsonObject.put("pr", invoice)
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonObject.toString().toRequestBody(mediaType)
val request = Request.Builder()
.url(url)
.post(requestBody)
.build()
client.newCall(request).execute().use {
val body = it.body.string()
val tree = jacksonObjectMapper().readTree(body)
val feeCost = tree?.get("fee")?.asInt()
if (feeCost != null) {
onSuccess(
feeCost
)
} else {
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
onError(
context.getString(R.string.cashu_failed_redemption),
if (msg != null) {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg, msg)
} else {
context.getString(R.string.cashu_failed_redemption_explainer_error_msg)
}
)
}
}
} catch (e: Exception) {
onError(context.getString(R.string.cashu_sucessful_redemption), context.getString(R.string.cashu_failed_redemption_explainer_error_msg, e.message))
}
}
private fun meltInvoice(token: CashuToken, invoice: String, fees: Int, onSuccess: (String, String) -> Unit, onError: (String, String) -> Unit, context: Context) {
try {
val client = HttpClient.getHttpClient()
val url = token.mint + "/melt" // Melt cashu tokens at Mint
@@ -93,7 +160,7 @@ class CashuProcessor {
if (successful) {
onSuccess(
context.getString(R.string.cashu_sucessful_redemption),
context.getString(R.string.cashu_sucessful_redemption_explainer, token.totalAmount.toString(), token.fees.toString())
context.getString(R.string.cashu_sucessful_redemption_explainer, token.totalAmount.toString(), fees.toString())
)
} else {
val msg = tree?.get("detail")?.asText()?.split('.')?.getOrNull(0)?.ifBlank { null }
@@ -6,6 +6,7 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -40,15 +41,19 @@ import androidx.core.content.ContextCompat.startActivity
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.CashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
var cachuData by remember { mutableStateOf<GenericLoadable<CashuToken>>(GenericLoadable.Loading<CashuToken>()) }
var cachuData by remember {
mutableStateOf<GenericLoadable<CashuToken>>(GenericLoadable.Loading())
}
LaunchedEffect(key1 = cashutoken) {
launch(Dispatchers.IO) {
@@ -59,7 +64,7 @@ fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
}
}
Crossfade(targetState = cachuData) {
Crossfade(targetState = cachuData, label = "CashuPreview(") {
when (it) {
is GenericLoadable.Loaded<CashuToken> -> CashuPreview(it.loaded, accountViewModel)
is GenericLoadable.Error<CashuToken> -> Text(
@@ -85,14 +90,14 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 30.dp, end = 30.dp)
.padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp)
.clip(shape = QuoteBorder)
.border(1.dp, MaterialTheme.colorScheme.subtleBorder, QuoteBorder)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(30.dp)
.padding(20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -117,37 +122,38 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Divider()
token.totalAmount.let {
Text(
text = "$it ${stringResource(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)
)
}
Row(
Text(
text = "${token.totalAmount} ${stringResource(id = R.string.sats)}",
fontSize = 25.sp,
fontWeight = FontWeight.W500,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
Button(
.padding(vertical = 10.dp)
)
modifier = Modifier
.padding(vertical = 10.dp).padding(horizontal = 2.dp),
Row(
modifier = Modifier
.padding(top = 5.dp)
.fillMaxWidth()
) {
var isRedeeming by remember {
mutableStateOf(false)
}
Button(
onClick = {
if (lud16 != null) {
scope.launch(Dispatchers.IO) {
isRedeeming = true
CashuProcessor().melt(
token,
lud16,
onSuccess = { title, message ->
isRedeeming = false
accountViewModel.toast(title, message)
},
onError = { title, message ->
isRedeeming = false
accountViewModel.toast(title, message)
},
context
@@ -165,15 +171,22 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
containerColor = MaterialTheme.colorScheme.primary
)
) {
if (isRedeeming) {
LoadingAnimation()
}
Spacer(modifier = StdHorzSpacer)
Text(
stringResource(R.string.cashu_redeem),
color = Color.White,
fontSize = 18.sp
)
}
Spacer(modifier = StdHorzSpacer)
Button(
modifier = Modifier
.padding(vertical = 10.dp).padding(horizontal = 1.dp),
onClick = {
if (useWebService) {
// In case we want to use the cashu.me webservice