From e16b3fa46a6410f2b0507fe009877774f56be7ab Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 1 Apr 2026 13:22:53 +0300 Subject: [PATCH 1/2] fix: avoid msat truncation when paying invoices with built-in amounts Bump bitkit-core to v0.1.56 which rounds up sub-satoshi invoice amounts. Additionally, stop overriding the amount for invoices that already have one. Pass null so LDK uses the invoice's native msat precision instead of our truncated sats value converted back to msat. Only pass the amount for zero-amount invoices where the user specifies it. Closes #877 --- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 11 +++++++---- .../java/to/bitkit/viewmodels/QuickPayViewModel.kt | 12 +++++++----- gradle/libs.versions.toml | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index f40c21b58..41e88e17b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1756,8 +1756,11 @@ 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 + // When the invoice has a built-in amount, pass null so LDK uses the + // invoice's native msat precision (avoids truncation to whole sats). + val paymentAmount = if (decodedInvoice.amountSatoshis > 0uL) null else amount + // For display/UI purposes, use the invoice amount (in sats) when available. + val displayAmountSats = decodedInvoice.amountSatoshis.takeIf { it > 0uL } ?: amount ?: 0uL val tags = _sendUiState.value.selectedTags var createdMetadataPaymentId: String? = null @@ -1785,14 +1788,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 diff --git a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt index feda0ed8e..fd5209a1e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt @@ -39,10 +39,12 @@ class QuickPayViewModel @Inject constructor( fun pay(data: QuickPayData) { viewModelScope.launch { - val (bolt11, amount) = when (data) { + val (bolt11, amount, displaySats) = when (data) { is QuickPayData.Bolt11 -> { Logger.info("QuickPay: processing bolt11 invoice") - data.bolt11 to data.sats + // Pass null amount so LDK uses the invoice's native msat precision + // (avoids truncation to whole sats). data.sats is only for display. + Triple(data.bolt11, null, data.sats) } is QuickPayData.LnurlPay -> { @@ -54,7 +56,7 @@ class QuickPayViewModel @Inject constructor( } return@launch } - invoice.bolt11 to data.sats + Triple(invoice.bolt11, data.sats, data.sats) } } @@ -65,7 +67,7 @@ class QuickPayViewModel @Inject constructor( it.copy( result = QuickPayResult.Success( paymentHash = paymentHash, - amountWithFee = amount.toLong() // TODO GET FEE WHEN AVAILABLE + amountWithFee = displaySats.toLong() // TODO GET FEE WHEN AVAILABLE ) ) } @@ -77,7 +79,7 @@ class QuickPayViewModel @Inject constructor( it.copy( result = QuickPayResult.Pending( paymentHash = error.paymentHash, - amount = amount.toLong(), + amount = displaySats.toLong(), ) ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea7508a9a..8f19e76a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version = "1 appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } -bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.38" } +bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.56" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } From 55dee91420e9902ffac5fdb87d54b45296a5198e Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 1 Apr 2026 15:10:36 +0300 Subject: [PATCH 2/2] fix: remove inline comments per CLAUDE.md guidelines Co-Authored-By: Claude Opus 4.6 (1M context) --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 3 --- app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt | 2 -- 2 files changed, 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 41e88e17b..e47ffb414 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1756,10 +1756,7 @@ class AppViewModel @Inject constructor( val decodedInvoice = requireNotNull(_sendUiState.value.decodedInvoice) val bolt11 = decodedInvoice.bolt11 - // When the invoice has a built-in amount, pass null so LDK uses the - // invoice's native msat precision (avoids truncation to whole sats). val paymentAmount = if (decodedInvoice.amountSatoshis > 0uL) null else amount - // For display/UI purposes, use the invoice amount (in sats) when available. val displayAmountSats = decodedInvoice.amountSatoshis.takeIf { it > 0uL } ?: amount ?: 0uL val tags = _sendUiState.value.selectedTags diff --git a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt index fd5209a1e..4fcb69890 100644 --- a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt @@ -42,8 +42,6 @@ class QuickPayViewModel @Inject constructor( val (bolt11, amount, displaySats) = when (data) { is QuickPayData.Bolt11 -> { Logger.info("QuickPay: processing bolt11 invoice") - // Pass null amount so LDK uses the invoice's native msat precision - // (avoids truncation to whole sats). data.sats is only for display. Triple(data.bolt11, null, data.sats) }