Skip to content

Commit 3092e32

Browse files
authored
Merge branch 'master' into ai/lsp-skill
2 parents 0b4df60 + 05d4aff commit 3092e32

16 files changed

Lines changed: 157 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.2.0] - 2026-04-07
11+
1012
### Fixed
1113
- Retouch Primary, Secondary, and Tertiary buttons styling #887
1214
- Avoid msat truncation when paying invoices and LNURL callbacks #879
@@ -43,4 +45,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4345
- About screen (content merged into Support) #857
4446
- Standalone General, Security, and Advanced settings screens (merged into tabs) #857
4547

46-
[Unreleased]: https://github.com/synonymdev/bitkit-android/compare/v2.1.2...HEAD
48+
[Unreleased]: https://github.com/synonymdev/bitkit-android/compare/v2.2.0...HEAD
49+
[2.2.0]: https://github.com/synonymdev/bitkit-android/compare/v2.1.2...v2.2.0

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ android {
5555
applicationId = "to.bitkit"
5656
minSdk = 28
5757
targetSdk = 36
58-
versionCode = 180
59-
versionName = "2.1.2"
58+
versionCode = 181
59+
versionName = "2.2.0"
6060
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
6161
vectorDrawables {
6262
useSupportLibrary = true

app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import to.bitkit.models.NotificationDetails
1818
import to.bitkit.models.PrimaryDisplay
1919
import to.bitkit.models.formatToModernDisplay
2020
import to.bitkit.repositories.ActivityRepo
21+
import to.bitkit.models.msatCeilOf
2122
import to.bitkit.repositories.CurrencyRepo
2223
import to.bitkit.utils.Logger
2324
import javax.inject.Inject
@@ -97,7 +98,7 @@ class NotifyPaymentReceivedHandler @Inject constructor(
9798
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
9899
},
99100
sats = when (command) {
100-
is NotifyPaymentReceived.Command.Lightning -> ((command.event.amountMsat + 999u) / 1000u).toLong()
101+
is NotifyPaymentReceived.Command.Lightning -> msatCeilOf(command.event.amountMsat).toLong()
101102
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
102103
},
103104
)

app/src/main/java/to/bitkit/ext/ChannelDetails.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.ext
33
import org.lightningdevkit.ldknode.ChannelConfig
44
import org.lightningdevkit.ldknode.ChannelDetails
55
import org.lightningdevkit.ldknode.MaxDustHtlcExposure
6+
import to.bitkit.models.msatFloorOf
67

78
/**
89
* Calculates our total balance in the channel (see `value_to_self_msat` in rust-lightning).
@@ -18,7 +19,7 @@ val ChannelDetails.amountOnClose: ULong
1819
@Suppress("ForbiddenComment")
1920
get() {
2021
// TODO: use channelDetails.claimableOnCloseSats
21-
val outboundCapacitySat = this.outboundCapacityMsat / 1000u
22+
val outboundCapacitySat = msatFloorOf(this.outboundCapacityMsat)
2223
val ourReserve = this.unspendablePunishmentReserve ?: 0u
2324

2425
return outboundCapacitySat + ourReserve
@@ -32,13 +33,13 @@ fun List<ChannelDetails>.filterPending(): List<ChannelDetails> = this.filterNot
3233

3334
/** Returns a limit in sats as close as possible to the HTLC limit we can currently send. */
3435
fun List<ChannelDetails>?.totalNextOutboundHtlcLimitSats(): ULong = this?.filter { it.isUsable }
35-
?.sumOf { it.nextOutboundHtlcLimitMsat / 1000u }
36+
?.sumOf { msatFloorOf(it.nextOutboundHtlcLimitMsat) }
3637
?: 0u
3738

3839
/** Calculates the total remote balance (inbound capacity) from open channels. */
3940
fun List<ChannelDetails>.calculateRemoteBalance(): ULong = this
4041
.filterOpen()
41-
.sumOf { it.inboundCapacityMsat / 1000u }
42+
.sumOf { msatFloorOf(it.inboundCapacityMsat) }
4243

4344
fun createChannelDetails(): ChannelDetails = ChannelDetails(
4445
channelId = "channelId",

app/src/main/java/to/bitkit/ext/Lnurl.kt

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,13 @@ package to.bitkit.ext
22

33
import com.synonym.bitkitcore.LnurlPayData
44
import com.synonym.bitkitcore.LnurlWithdrawData
5-
6-
private const val MSATS_PER_SAT: ULong = 1000u
7-
8-
/**
9-
* LNURL amounts are expressed in millisatoshis (msat).
10-
*
11-
* When converting a minimum bound to whole sats we must round up:
12-
* `minSendable = 100500 msat` means the minimum payable amount is `101 sat` (not `100 sat`).
13-
*/
14-
private fun msatsToSatsCeil(msats: ULong): ULong {
15-
val quotient = msats / MSATS_PER_SAT
16-
val remainder = msats % MSATS_PER_SAT
17-
return when (remainder) {
18-
0uL -> quotient
19-
else -> quotient + 1uL
20-
}
21-
}
5+
import to.bitkit.models.MSat
6+
import to.bitkit.models.msatCeilOf
7+
import to.bitkit.models.msatFloorOf
228

239
fun LnurlPayData.commentAllowed(): Boolean = commentAllowed?.let { it > 0u } == true
24-
fun LnurlPayData.maxSendableSat(): ULong = maxSendable / MSATS_PER_SAT
25-
fun LnurlPayData.minSendableSat(): ULong = msatsToSatsCeil(minSendable)
10+
fun LnurlPayData.maxSendableSat(): ULong = msatFloorOf(maxSendable)
11+
fun LnurlPayData.minSendableSat(): ULong = msatCeilOf(minSendable)
2612

2713
/**
2814
* True when the LNURL-pay endpoint specifies a single exact amount.
@@ -44,10 +30,10 @@ fun LnurlPayData.isFixedAmount(): Boolean =
4430
* For variable-amount requests the user-selected sat amount is converted to msats.
4531
*/
4632
fun LnurlPayData.callbackAmountMsats(userSats: ULong? = null): ULong =
47-
if (isFixedAmount()) minSendable else (userSats ?: minSendableSat()) * MSATS_PER_SAT
33+
if (isFixedAmount()) minSendable else (userSats ?: minSendableSat()) * MSat.PER_SAT
4834

49-
fun LnurlWithdrawData.minWithdrawableSat(): ULong = msatsToSatsCeil(minWithdrawable ?: 0u)
50-
fun LnurlWithdrawData.maxWithdrawableSat(): ULong = maxWithdrawable / MSATS_PER_SAT
35+
fun LnurlWithdrawData.minWithdrawableSat(): ULong = msatCeilOf(minWithdrawable ?: 0u)
36+
fun LnurlWithdrawData.maxWithdrawableSat(): ULong = msatFloorOf(maxWithdrawable)
5137

5238
/**
5339
* True when the LNURL-withdraw endpoint specifies a single exact amount,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package to.bitkit.ext
22

33
import org.lightningdevkit.ldknode.PaymentDetails
4+
import to.bitkit.models.msatCeilOf
45

56
val PaymentDetails.amountSats: ULong?
6-
get() = amountMsat?.let { (it + 999u) / 1000u }
7+
get() = amountMsat?.let { msatCeilOf(it) }

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import to.bitkit.ext.amountOnClose
2626
import to.bitkit.ext.toUserMessage
2727
import to.bitkit.models.BITCOIN_SYMBOL
2828
import to.bitkit.models.BlocktankNotificationType
29+
import to.bitkit.models.msatCeilOf
2930
import to.bitkit.models.BlocktankNotificationType.cjitPaymentArrived
3031
import to.bitkit.models.BlocktankNotificationType.incomingHtlc
3132
import to.bitkit.models.BlocktankNotificationType.mutualClose
@@ -192,7 +193,7 @@ class WakeNodeWorker @AssistedInject constructor(
192193
showDetails: Boolean,
193194
hiddenBody: String,
194195
) {
195-
val sats = (event.amountMsat + 999u) / 1000u
196+
val sats = msatCeilOf(event.amountMsat)
196197
// Save for UI to pick up
197198
cacheStore.setBackgroundReceive(
198199
NewTransactionSheetDetails(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package to.bitkit.models
2+
3+
/**
4+
* A non-boxing wrapper for millisatoshi [ULong] values that provides explicit sat conversions.
5+
*
6+
* Encapsulates the rounding intent directly in the API: [ceil] rounds up, [floor] truncates.
7+
*/
8+
@JvmInline
9+
value class MSat(val value: ULong) {
10+
11+
companion object {
12+
const val PER_SAT: ULong = 1000u
13+
}
14+
15+
/** Round up to the nearest whole sat. Use for payment/display amounts. */
16+
fun ceil(): ULong = (value + PER_SAT - 1u) / PER_SAT
17+
18+
/** Truncate sub-sat remainder. Use for fees and upper bounds. */
19+
fun floor(): ULong = value / PER_SAT
20+
}
21+
22+
/** Round [msat] up to the nearest whole sat. Use for payment/display amounts. */
23+
fun msatCeilOf(msat: ULong): ULong = MSat(msat).ceil()
24+
25+
/** Truncate sub-sat remainder from [msat]. Use for fees and upper bounds. */
26+
fun msatFloorOf(msat: ULong): ULong = MSat(msat).floor()

app/src/main/java/to/bitkit/repositories/WalletRepo.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import to.bitkit.ext.toHex
3232
import to.bitkit.models.ALL_ADDRESS_TYPE_STRINGS
3333
import to.bitkit.models.AddressModel
3434
import to.bitkit.models.BalanceState
35+
import to.bitkit.models.msatFloorOf
3536
import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING
3637
import to.bitkit.models.toDerivationPath
3738
import to.bitkit.services.CoreService
@@ -565,7 +566,7 @@ class WalletRepo @Inject constructor(
565566
val channels = lightningRepo.lightningState.value.channels
566567
if (channels.filterOpen().isEmpty()) return@runCatching false
567568

568-
val inboundBalanceSats = channels.sumOf { it.inboundCapacityMsat / 1000u }
569+
val inboundBalanceSats = channels.sumOf { msatFloorOf(it.inboundCapacityMsat) }
569570

570571
return@runCatching (_walletState.value.bip21AmountSats ?: 0uL) >= inboundBalanceSats
571572
}.onFailure {

app/src/main/java/to/bitkit/services/CoreService.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import to.bitkit.data.CacheStore
7272
import to.bitkit.data.SettingsStore
7373
import to.bitkit.env.Env
7474
import to.bitkit.ext.amountSats
75+
import to.bitkit.models.msatFloorOf
7576
import to.bitkit.ext.channelId
7677
import to.bitkit.ext.create
7778
import to.bitkit.ext.latestSpendingTxid
@@ -501,7 +502,7 @@ class ActivityService(
501502
value = payment.amountSats ?: 0u,
502503
invoice = kind.bolt11 ?: "Loading...",
503504
timestamp = payment.latestUpdateTimestamp,
504-
fee = (payment.feePaidMsat ?: 0u) / 1000u,
505+
fee = msatFloorOf(payment.feePaidMsat ?: 0u),
505506
message = kind.description.orEmpty(),
506507
preimage = kind.preimage,
507508
seenAt = null,
@@ -610,7 +611,7 @@ class ActivityService(
610611
ldkValue
611612
}
612613

613-
val ldkFeeSats = ldkFeeMsat / 1000u
614+
val ldkFeeSats = msatFloorOf(ldkFeeMsat)
614615
val updatedFee = if (existingActivity.v1.fee == 0uL && ldkFeeSats > 0uL) ldkFeeSats else existingActivity.v1.fee
615616

616617
val updatedOnChain = existingActivity.v1.copy(
@@ -649,7 +650,7 @@ class ActivityService(
649650
txType = payment.direction.toPaymentType(),
650651
txId = kind.txid,
651652
value = payment.amountSats ?: 0u,
652-
fee = (payment.feePaidMsat ?: 0u) / 1000u,
653+
fee = msatFloorOf(payment.feePaidMsat ?: 0u),
653654
address = resolvedAddress ?: "Loading...",
654655
timestamp = activityTimestamp,
655656
confirmed = confirmationData.isConfirmed,

0 commit comments

Comments
 (0)