Skip to content

Commit 05d4aff

Browse files
authored
Merge pull request #889 from synonymdev/refactor/add-msat-model
refactor: add MSat value class for msat-to-sat conversions
2 parents 4a804d4 + d6ccbe7 commit 05d4aff

14 files changed

Lines changed: 151 additions & 48 deletions

File tree

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,

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import to.bitkit.env.Env
4949
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
5050
import to.bitkit.ext.uByteList
5151
import to.bitkit.ext.uri
52+
import to.bitkit.models.msatFloorOf
5253
import to.bitkit.models.OpenChannelResult
5354
import to.bitkit.models.toAddressType
5455
import to.bitkit.utils.AppError
@@ -694,7 +695,7 @@ class LightningService @Inject constructor(
694695
return@background runCatching {
695696
val invoice = Bolt11Invoice.fromStr(bolt11)
696697
val feesMsat = node.bolt11Payment().estimateRoutingFees(invoice)
697-
val feeSat = feesMsat / 1000u
698+
val feeSat = msatFloorOf(feesMsat)
698699
Result.success(feeSat)
699700
}.getOrElse {
700701
Result.failure(if (it is NodeException) LdkError(it) else it)
@@ -710,7 +711,7 @@ class LightningService @Inject constructor(
710711
val invoice = Bolt11Invoice.fromStr(bolt11)
711712
val amountMsat = amountSats * 1000u
712713
val feesMsat = node.bolt11Payment().estimateRoutingFeesUsingAmount(invoice, amountMsat)
713-
val feeSat = feesMsat / 1000u
714+
val feeSat = msatFloorOf(feesMsat)
714715
Result.success(feeSat)
715716
}.getOrElse {
716717
Result.failure(if (it is NodeException) LdkError(it) else it)
@@ -728,7 +729,7 @@ class LightningService @Inject constructor(
728729

729730
val invoiceAmountMsat = bolt11Invoice.amountMilliSatoshis()
730731
Logger.debug(
731-
"sendProbes: invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { it / 1000u }} sats)",
732+
"sendProbes: invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { msatFloorOf(it) }} sats)",
732733
context = TAG
733734
)
734735

@@ -751,8 +752,8 @@ class LightningService @Inject constructor(
751752

752753
val invoiceAmountMsat = bolt11Invoice.amountMilliSatoshis()
753754
Logger.debug(
754-
"sendProbesUsingAmount: customAmountMsat=$amountMsat (${amountMsat / 1000u} sats), " +
755-
"invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { it / 1000u }} sats)",
755+
"sendProbesUsingAmount: customAmountMsat=$amountMsat (${msatFloorOf(amountMsat)} sats), " +
756+
"invoiceAmountMsat=$invoiceAmountMsat (${invoiceAmountMsat?.let { msatFloorOf(it) }} sats)",
756757
context = TAG
757758
)
758759

app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import to.bitkit.ext.createChannelDetails
5050
import to.bitkit.ext.ellipsisMiddle
5151
import to.bitkit.ext.formatToString
5252
import to.bitkit.ext.uri
53+
import to.bitkit.models.msatFloorOf
5354
import to.bitkit.models.NodeLifecycleState
5455
import to.bitkit.models.NodePeer
5556
import to.bitkit.models.alias
@@ -336,8 +337,8 @@ private fun ChannelsSection(
336337
}
337338
LightningChannel(
338339
capacity = (channel.channelValueSats).toLong(),
339-
localBalance = (channel.outboundCapacityMsat / 1000u).toLong(),
340-
remoteBalance = (channel.inboundCapacityMsat / 1000u).toLong(),
340+
localBalance = msatFloorOf(channel.outboundCapacityMsat).toLong(),
341+
remoteBalance = msatFloorOf(channel.inboundCapacityMsat).toLong(),
341342
status = if (channel.isChannelReady) ChannelStatusUi.OPEN else ChannelStatusUi.PENDING,
342343
)
343344
VerticalSpacer(8.dp)
@@ -356,23 +357,25 @@ private fun ChannelsSection(
356357
)
357358
ChannelDetailRow(
358359
title = stringResource(R.string.lightning__inbound_capacity),
359-
value = "${(channel.inboundCapacityMsat / 1000u).formatToModernDisplay()}",
360+
value = "${msatFloorOf(channel.inboundCapacityMsat).formatToModernDisplay()}",
360361
)
361362
ChannelDetailRow(
362363
title = stringResource(R.string.lightning__inbound_htlc_max),
363-
value = "${(channel.inboundHtlcMaximumMsat?.div(1000u) ?: 0u).formatToModernDisplay()}",
364+
value = "${
365+
(channel.inboundHtlcMaximumMsat?.let { msatFloorOf(it) } ?: 0u).formatToModernDisplay()
366+
}",
364367
)
365368
ChannelDetailRow(
366369
title = stringResource(R.string.lightning__inbound_htlc_min),
367-
value = "${(channel.inboundHtlcMinimumMsat / 1000u).formatToModernDisplay()}",
370+
value = "${msatFloorOf(channel.inboundHtlcMinimumMsat).formatToModernDisplay()}",
368371
)
369372
ChannelDetailRow(
370373
title = stringResource(R.string.lightning__next_outbound_htlc_limit),
371-
value = "${(channel.nextOutboundHtlcLimitMsat / 1000u).formatToModernDisplay()}",
374+
value = "${msatFloorOf(channel.nextOutboundHtlcLimitMsat).formatToModernDisplay()}",
372375
)
373376
ChannelDetailRow(
374377
title = stringResource(R.string.lightning__next_outbound_htlc_min),
375-
value = "${(channel.nextOutboundHtlcMinimumMsat / 1000u).formatToModernDisplay()}",
378+
value = "${msatFloorOf(channel.nextOutboundHtlcMinimumMsat).formatToModernDisplay()}",
376379
)
377380
ChannelDetailRow(
378381
title = stringResource(R.string.common__confirmations),

0 commit comments

Comments
 (0)