-
Notifications
You must be signed in to change notification settings - Fork 3
fix: avoid msat truncation when paying invoices with built-in amounts #879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
e16b3fa
fix: avoid msat truncation when paying invoices with built-in amounts
ben-kaufman 55dee91
fix: remove inline comments per CLAUDE.md guidelines
ben-kaufman 912fdca
Merge branch 'master' into fix/msat-invoice-precision
piotr-iohk 01ec250
Merge branch 'master' into fix/msat-invoice-precision
ovitrif 03403b3
fix: preserve msat precision for LNURL pay and withdraw callbacks
ben-kaufman 5d7614d
chore: add changelog entry and fix detekt line length
ben-kaufman 388c669
Merge branch 'master' into fix/msat-invoice-precision
ben-kaufman 7bfb20b
fix: address review — LNURL quickpay null amount, log quotes, changelog
ben-kaufman 74f3972
fix: log message should start with verb per CLAUDE.md
ben-kaufman 2a8037d
fix: use msat-precision invoices for fixed-amount LNURL withdraw
ben-kaufman 77e62a0
fix: use satsCeil for LNURL display amounts to match BOLT11 behavior
ben-kaufman 4750afb
fix: remove unused fixedWithdrawAmountSat import
ben-kaufman be28931
fix: remove dead fixedWithdrawAmountSat and simplify conditional
ben-kaufman c72e52c
fix: remove needless blank line flagged by detekt
ben-kaufman ae32b31
fix: use ceiling division for PaymentDetails.amountSats
ben-kaufman 2608e65
fix: use ceiling for received payment notification amounts
ben-kaufman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -77,6 +77,9 @@ import to.bitkit.ext.channelId | |||||
| import to.bitkit.ext.claimableAtHeight | ||||||
| import to.bitkit.ext.getClipboardText | ||||||
| import to.bitkit.ext.getSatsPerVByteFor | ||||||
| import to.bitkit.ext.callbackAmountMsats | ||||||
| import to.bitkit.ext.fixedWithdrawAmountSat | ||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||
| import to.bitkit.ext.isFixedAmount | ||||||
| import to.bitkit.ext.maxSendableSat | ||||||
| import to.bitkit.ext.maxWithdrawableSat | ||||||
| import to.bitkit.ext.minSendableSat | ||||||
|
|
@@ -1161,7 +1164,7 @@ class AppViewModel @Inject constructor( | |||||
| val maxSendable = maxSendableLightningSats() | ||||||
| when (val lnurl = _sendUiState.value.lnurl) { | ||||||
| null -> amount <= maxSendable && lightningRepo.canSend(amount) | ||||||
| is LnurlParams.LnurlWithdraw -> amount < lnurl.data.maxWithdrawableSat() | ||||||
| is LnurlParams.LnurlWithdraw -> amount <= lnurl.data.maxWithdrawableSat() | ||||||
| is LnurlParams.LnurlPay -> { | ||||||
| val maxSat = lnurl.data.maxSendableSat() | ||||||
| amount <= maxSat && amount <= maxSendable && lightningRepo.canSend(amount) | ||||||
|
|
@@ -1405,10 +1408,10 @@ class AppViewModel @Inject constructor( | |||||
| private suspend fun onScanLnurlPay(data: LnurlPayData) { | ||||||
| Logger.debug("LNURL: $data", context = TAG) | ||||||
|
|
||||||
| val minSendable = data.minSendableSat() | ||||||
| val maxSendable = data.maxSendableSat() | ||||||
| val isFixed = data.isFixedAmount() | ||||||
| val displaySats = if (isFixed) data.maxSendableSat() else data.minSendableSat() | ||||||
|
|
||||||
| if (!lightningRepo.canSend(minSendable)) { | ||||||
| if (!lightningRepo.canSend(displaySats.coerceAtLeast(1u))) { | ||||||
| toast( | ||||||
| type = Toast.ToastType.WARNING, | ||||||
| title = context.getString(R.string.other__lnurl_pay_error), | ||||||
|
|
@@ -1417,8 +1420,7 @@ class AppViewModel @Inject constructor( | |||||
| return | ||||||
| } | ||||||
|
|
||||||
| val hasAmount = minSendable == maxSendable && minSendable > 0u | ||||||
| val initialAmount = if (hasAmount) minSendable else 0u | ||||||
| val initialAmount = if (isFixed) displaySats else 0u | ||||||
|
|
||||||
| _sendUiState.update { | ||||||
| it.copy( | ||||||
|
|
@@ -1428,10 +1430,10 @@ class AppViewModel @Inject constructor( | |||||
| ) | ||||||
| } | ||||||
|
|
||||||
| if (hasAmount) { | ||||||
| Logger.info("Found amount $$minSendable in lnurlPay, proceeding with payment", context = TAG) | ||||||
| if (isFixed) { | ||||||
| Logger.info("Found fixed amount $displaySats sats in lnurlPay, proceeding with payment", context = TAG) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CLAUDE.md violation: log parameter values must be wrapped in single quotes Per CLAUDE.md:
Suggested change
|
||||||
|
|
||||||
| val quickPayHandled = handleQuickPayIfApplicable(amountSats = minSendable, lnurlPay = data) | ||||||
| val quickPayHandled = handleQuickPayIfApplicable(amountSats = displaySats, lnurlPay = data) | ||||||
| if (quickPayHandled) return | ||||||
|
|
||||||
| if (isMainScanner) { | ||||||
|
|
@@ -1453,10 +1455,11 @@ class AppViewModel @Inject constructor( | |||||
| private suspend fun onScanLnurlWithdraw(data: LnurlWithdrawData) { | ||||||
| Logger.debug("LNURL: $data", context = TAG) | ||||||
|
|
||||||
| val isFixed = data.isFixedAmount() | ||||||
| val minWithdrawable = data.minWithdrawableSat() | ||||||
| val maxWithdrawable = data.maxWithdrawableSat() | ||||||
|
|
||||||
| if (minWithdrawable > maxWithdrawable) { | ||||||
| if (!isFixed && minWithdrawable > maxWithdrawable) { | ||||||
| toast( | ||||||
| type = Toast.ToastType.WARNING, | ||||||
| title = context.getString(R.string.other__lnurl_withdr_error), | ||||||
|
|
@@ -1465,15 +1468,17 @@ class AppViewModel @Inject constructor( | |||||
| return | ||||||
| } | ||||||
|
|
||||||
| val displayAmount = if (isFixed) data.fixedWithdrawAmountSat() else minWithdrawable | ||||||
|
|
||||||
| _sendUiState.update { | ||||||
| it.copy( | ||||||
| payMethod = SendMethod.LIGHTNING, | ||||||
| amount = minWithdrawable, | ||||||
| amount = displayAmount, | ||||||
| lnurl = LnurlParams.LnurlWithdraw(data = data) | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| if (minWithdrawable == maxWithdrawable) { | ||||||
| if (isFixed || minWithdrawable == maxWithdrawable) { | ||||||
| delay(TRANSITION_SCREEN_MS) | ||||||
| if (isMainScanner) { | ||||||
| showSheet(Sheet.Send(SendRoute.WithdrawConfirm)) | ||||||
|
|
@@ -1582,7 +1587,11 @@ class AppViewModel @Inject constructor( | |||||
|
|
||||||
| val quickPayData: QuickPayData = when { | ||||||
| lnurlPay != null -> { | ||||||
| QuickPayData.LnurlPay(sats = amountSats, callback = lnurlPay.callback) | ||||||
| QuickPayData.LnurlPay( | ||||||
| sats = amountSats, | ||||||
| callback = lnurlPay.callback, | ||||||
| amountMsats = lnurlPay.callbackAmountMsats(amountSats), | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| else -> { | ||||||
|
|
@@ -1706,9 +1715,10 @@ class AppViewModel @Inject constructor( | |||||
| val isLnurlPay = lnurl is LnurlParams.LnurlPay | ||||||
|
|
||||||
| if (isLnurlPay) { | ||||||
| val amountMsats = lnurl.data.callbackAmountMsats(amount) | ||||||
| lightningRepo.fetchLnurlInvoice( | ||||||
| callbackUrl = lnurl.data.callback, | ||||||
| amountSats = amount, | ||||||
| amountMsats = amountMsats, | ||||||
| comment = _sendUiState.value.comment.takeIf { it.isNotEmpty() }, | ||||||
| ).onSuccess { invoice -> | ||||||
| _sendUiState.update { | ||||||
|
|
@@ -1756,8 +1766,8 @@ class AppViewModel @Inject constructor( | |||||
| val decodedInvoice = requireNotNull(_sendUiState.value.decodedInvoice) | ||||||
| val bolt11 = decodedInvoice.bolt11 | ||||||
|
|
||||||
| // Determine if we should override amount | ||||||
| val paymentAmount = decodedInvoice.amountSatoshis.takeIf { it > 0uL } ?: amount | ||||||
| val paymentAmount = if (decodedInvoice.amountSatoshis > 0uL) null else amount | ||||||
| val displayAmountSats = decodedInvoice.amountSatoshis.takeIf { it > 0uL } ?: amount ?: 0uL | ||||||
|
|
||||||
| val tags = _sendUiState.value.selectedTags | ||||||
| var createdMetadataPaymentId: String? = null | ||||||
|
|
@@ -1785,14 +1795,14 @@ class AppViewModel @Inject constructor( | |||||
| type = NewTransactionSheetType.LIGHTNING, | ||||||
| direction = NewTransactionSheetDirection.SENT, | ||||||
| paymentHashOrTxId = actualPaymentHash, | ||||||
| sats = paymentAmount.toLong(), // TODO Add fee when available | ||||||
| sats = displayAmountSats.toLong(), // TODO Add fee when available | ||||||
| ), | ||||||
| ) | ||||||
| }.onFailure { | ||||||
| if (it is PaymentPendingException) { | ||||||
| Logger.info("Lightning payment pending", context = TAG) | ||||||
| pendingPaymentRepo.track(it.paymentHash) | ||||||
| setSendEffect(SendEffect.NavigateToPending(it.paymentHash, paymentAmount.toLong())) | ||||||
| setSendEffect(SendEffect.NavigateToPending(it.paymentHash, displayAmountSats.toLong())) | ||||||
| return@onFailure | ||||||
| } | ||||||
| // Delete pre-activity metadata on failure | ||||||
|
|
@@ -1817,16 +1827,20 @@ class AppViewModel @Inject constructor( | |||||
| return@launch | ||||||
| } | ||||||
|
|
||||||
| _sendUiState.update { | ||||||
| it.copy( | ||||||
| amount = it.amount.coerceAtLeast( | ||||||
| (lnurl.data.minWithdrawable ?: 0u) / 1000u | ||||||
| ) | ||||||
| val withdrawAmountSats = if (lnurl.data.isFixedAmount()) { | ||||||
| lnurl.data.fixedWithdrawAmountSat() | ||||||
| } else { | ||||||
| _sendUiState.value.amount.coerceAtLeast( | ||||||
| (lnurl.data.minWithdrawable ?: 0u) / 1000u | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| _sendUiState.update { | ||||||
| it.copy(amount = withdrawAmountSats) | ||||||
| } | ||||||
|
|
||||||
| val invoice = lightningRepo.createInvoice( | ||||||
| amountSats = _sendUiState.value.amount, | ||||||
| amountSats = withdrawAmountSats, | ||||||
| description = lnurl.data.defaultDescription, | ||||||
| expirySeconds = 3600u, | ||||||
| ).getOrNull() | ||||||
|
|
@@ -2567,6 +2581,6 @@ sealed interface QuickPayData { | |||||
| data class Bolt11(override val sats: ULong, val bolt11: String) : QuickPayData | ||||||
|
|
||||||
| @Stable | ||||||
| data class LnurlPay(override val sats: ULong, val callback: String) : QuickPayData | ||||||
| data class LnurlPay(override val sats: ULong, val callback: String, val amountMsats: ULong) : QuickPayData | ||||||
| } | ||||||
| // endregion | ||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.