Skip to content

Commit cfbcf20

Browse files
authored
Merge pull request #529 from synonymdev/fix/lnurl-min-send-ceil
Fix/lnurl minSendable edge case when msats are not divisable by 1000
2 parents f0f8274 + 8f7c368 commit cfbcf20

File tree

3 files changed

+102
-6
lines changed

3 files changed

+102
-6
lines changed

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@ package to.bitkit.ext
33
import com.synonym.bitkitcore.LnurlPayData
44
import com.synonym.bitkitcore.LnurlWithdrawData
55

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+
}
22+
623
fun LnurlPayData.commentAllowed(): Boolean = commentAllowed?.let { it > 0u } == true
7-
fun LnurlPayData.maxSendableSat(): ULong = maxSendable / 1000u
8-
fun LnurlPayData.minSendableSat(): ULong = minSendable / 1000u
24+
fun LnurlPayData.maxSendableSat(): ULong = maxSendable / MSATS_PER_SAT
25+
fun LnurlPayData.minSendableSat(): ULong = msatsToSatsCeil(minSendable)
926

10-
fun LnurlWithdrawData.minWithdrawableSat(): ULong = (minWithdrawable ?: 0u) / 1000u
11-
fun LnurlWithdrawData.maxWithdrawableSat(): ULong = maxWithdrawable / 1000u
27+
fun LnurlWithdrawData.minWithdrawableSat(): ULong = msatsToSatsCeil(minWithdrawable ?: 0u)
28+
fun LnurlWithdrawData.maxWithdrawableSat(): ULong = maxWithdrawable / MSATS_PER_SAT

app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ fun ActivityDetailScreen(
249249
app.toast(
250250
type = Toast.ToastType.SUCCESS,
251251
title = context.getString(R.string.wallet__boost_success_title),
252-
description = context.getString(R.string.wallet__boost_success_msg)
252+
description = context.getString(R.string.wallet__boost_success_msg),
253+
testTag = "BoostSuccessToast"
253254
)
254255
listViewModel.resync()
255256
onCloseClick()
@@ -258,7 +259,8 @@ fun ActivityDetailScreen(
258259
app.toast(
259260
type = Toast.ToastType.ERROR,
260261
title = context.getString(R.string.wallet__boost_error_title),
261-
description = context.getString(R.string.wallet__boost_error_msg)
262+
description = context.getString(R.string.wallet__boost_error_msg),
263+
testTag = "BoostFailureToast"
262264
)
263265
detailViewModel.onDismissBoostSheet()
264266
},
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package to.bitkit.ext
2+
3+
import com.synonym.bitkitcore.LnurlPayData
4+
import com.synonym.bitkitcore.LnurlWithdrawData
5+
import org.junit.Test
6+
import to.bitkit.test.BaseUnitTest
7+
import kotlin.test.assertEquals
8+
9+
class LnurlExtTest : BaseUnitTest() {
10+
11+
@Test
12+
fun `minSendableSat rounds up when msats not divisible by 1000`() {
13+
val data = LnurlPayData(
14+
uri = "lnurl",
15+
callback = "callback",
16+
minSendable = 100_500u,
17+
maxSendable = 200_000u,
18+
metadataStr = "[]",
19+
commentAllowed = null,
20+
allowsNostr = false,
21+
nostrPubkey = null,
22+
)
23+
24+
assertEquals(101u, data.minSendableSat())
25+
}
26+
27+
@Test
28+
fun `minSendableSat keeps exact sat amounts`() {
29+
val data = LnurlPayData(
30+
uri = "lnurl",
31+
callback = "callback",
32+
minSendable = 100_000u,
33+
maxSendable = 200_000u,
34+
metadataStr = "[]",
35+
commentAllowed = null,
36+
allowsNostr = false,
37+
nostrPubkey = null,
38+
)
39+
40+
assertEquals(100u, data.minSendableSat())
41+
assertEquals(0u, data.copy(minSendable = 0u).minSendableSat())
42+
}
43+
44+
@Test
45+
fun `maxSendableSat floors when msats not divisible by 1000`() {
46+
val data = LnurlPayData(
47+
uri = "lnurl",
48+
callback = "callback",
49+
minSendable = 1_000u,
50+
maxSendable = 100_999u,
51+
metadataStr = "[]",
52+
commentAllowed = null,
53+
allowsNostr = false,
54+
nostrPubkey = null,
55+
)
56+
57+
assertEquals(100u, data.maxSendableSat())
58+
assertEquals(0u, data.copy(maxSendable = 0u).maxSendableSat())
59+
}
60+
61+
@Test
62+
fun `minWithdrawableSat rounds up and treats null as zero`() {
63+
val nullMin = LnurlWithdrawData(
64+
uri = "lnurl",
65+
callback = "callback",
66+
k1 = "k1",
67+
defaultDescription = "desc",
68+
minWithdrawable = null,
69+
maxWithdrawable = 1_000u,
70+
tag = "withdraw",
71+
)
72+
assertEquals(0u, nullMin.minWithdrawableSat())
73+
74+
val nonRoundMin = nullMin.copy(minWithdrawable = 1_500u)
75+
assertEquals(2u, nonRoundMin.minWithdrawableSat())
76+
}
77+
}

0 commit comments

Comments
 (0)