Skip to content

Commit 56dc586

Browse files
authored
Merge branch 'master' into ai/lsp-skill
2 parents 6713961 + 4fe3125 commit 56dc586

56 files changed

Lines changed: 1737 additions & 594 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
240240

241241
### Changelog
242242

243-
- ALWAYS add an entry under `## [Unreleased]` in `CHANGELOG.md` for `feat:` and `fix:` PRs; skip for `chore:`, `ci:`, `refactor:`, `test:`, `docs:` unless the change is user-facing
243+
- ALWAYS add exactly ONE entry per PR under `## [Unreleased]` in `CHANGELOG.md` for `feat:` and `fix:` PRs; skip for `chore:`, `ci:`, `refactor:`, `test:`, `docs:` unless the change is user-facing
244+
- NEVER add multiple changelog lines for the same PR — summarize all changes in a single concise entry
244245
- USE standard Keep a Changelog categories: `### Added`, `### Changed`, `### Deprecated`, `### Removed`, `### Fixed`, `### Security`
245246
- ALWAYS append `#PR_NUMBER` at the end of each changelog entry when the PR number is known
246247
- ALWAYS place new entries at the top of their category section (newest first)

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Fixed
11+
- Retouch Primary, Secondary, and Tertiary buttons styling #887
12+
- Avoid msat truncation when paying invoices and LNURL callbacks #879
1113
- Fix ANR on RGS server settings screen caused by catastrophic regex backtracking #880
1214
- Fix crash when returning app to foreground on Receive screen #875
1315
- Show loading state on Spending tab when node is not running #875
@@ -22,6 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2224
- Mnemonic warning text transitions on reveal #857
2325

2426
### Changed
27+
- Show end of address on Receive Bitcoin screen using middle ellipsis truncation #886
28+
- Update funding screen: replace Advanced with Manual Setup, fix Use Other Wallet navigation to open amount entry, and add Fund Wallet button to no-funds dialog #885
29+
- Updated design of the success screen in the manual channel setup flow #883
30+
- Unified send flow with payment method switcher, details toggle, Lightning support for BIP21 payments, and improved fee rate defaults #863
2531
- Settings redesigned with tabbed navigation (General/Security/Advanced) with swipe support #857
2632
- Icons added to all settings rows for faster scanning #857
2733
- Selected values displayed on right side of settings rows #857

app/src/main/java/to/bitkit/di/HttpModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import io.ktor.http.contentType
1818
import io.ktor.http.isSuccess
1919
import io.ktor.serialization.kotlinx.json.json
2020
import kotlinx.serialization.json.Json
21-
import to.bitkit.utils.UrlValidator
2221
import to.bitkit.utils.AppError
2322
import to.bitkit.utils.Logger
23+
import to.bitkit.utils.UrlValidator
2424
import javax.inject.Singleton
2525
import io.ktor.client.plugins.logging.Logger as KtorLogger
2626

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class NotifyPaymentReceivedHandler @Inject constructor(
9797
is NotifyPaymentReceived.Command.Onchain -> command.event.txid
9898
},
9999
sats = when (command) {
100-
is NotifyPaymentReceived.Command.Lightning -> (command.event.amountMsat / 1000u).toLong()
100+
is NotifyPaymentReceived.Command.Lightning -> ((command.event.amountMsat + 999u) / 1000u).toLong()
101101
is NotifyPaymentReceived.Command.Onchain -> command.event.details.amountSats
102102
},
103103
)

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,34 @@ fun Long.toRelativeTimeString(
109109
}
110110
}
111111

112+
fun formatInvoiceExpiryRelative(
113+
expirySeconds: ULong,
114+
locale: Locale = Locale.getDefault(),
115+
): String {
116+
val seconds = expirySeconds.toLong()
117+
if (seconds <= 0) return ""
118+
119+
val uLocale = ULocale.forLocale(locale)
120+
val numberFormat = NumberFormat.getNumberInstance(uLocale)?.apply { maximumFractionDigits = 0 }
121+
val formatter = RelativeDateTimeFormatter.getInstance(
122+
uLocale,
123+
numberFormat,
124+
RelativeDateTimeFormatter.Style.LONG,
125+
DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
126+
) ?: return ""
127+
128+
val minutes = seconds / Factor.SECONDS_TO_MINUTES.toLong()
129+
val hours = minutes / Factor.MINUTES_TO_HOURS.toLong()
130+
val days = hours / Factor.HOURS_TO_DAYS.toLong()
131+
132+
return when {
133+
minutes < 1 -> formatter.format(seconds.toDouble(), Direction.NEXT, RelativeUnit.SECONDS)
134+
hours < 1 -> formatter.format(minutes.toDouble(), Direction.NEXT, RelativeUnit.MINUTES)
135+
days < 1 -> formatter.format(hours.toDouble(), Direction.NEXT, RelativeUnit.HOURS)
136+
else -> formatter.format(days.toDouble(), Direction.NEXT, RelativeUnit.DAYS)
137+
}
138+
}
139+
112140
fun getDaysInMonth(month: LocalDate): List<LocalDate?> {
113141
val firstDayOfMonth = LocalDate(month.year, month.month, Constants.FIRST_DAY_OF_MONTH)
114142
val daysInMonth = month.month.toJavaMonth().length(isLeapYear(month.year))

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,36 @@ fun LnurlPayData.commentAllowed(): Boolean = commentAllowed?.let { it > 0u } ==
2424
fun LnurlPayData.maxSendableSat(): ULong = maxSendable / MSATS_PER_SAT
2525
fun LnurlPayData.minSendableSat(): ULong = msatsToSatsCeil(minSendable)
2626

27+
/**
28+
* True when the LNURL-pay endpoint specifies a single exact amount.
29+
*
30+
* This also covers the sub-sat edge case where `minSendable` and `maxSendable` differ
31+
* in their sub-sat fraction but map to the same (or inverted) sat range after rounding,
32+
* e.g. `minSendable = 500500, maxSendable = 500500` → `minSendableSat() = 501, maxSendableSat() = 500`.
33+
*/
34+
fun LnurlPayData.isFixedAmount(): Boolean =
35+
minSendable == maxSendable || (minSendable > 0u && minSendableSat() > maxSendableSat())
36+
37+
/**
38+
* Returns the amount in millisatoshis to send in the LNURL-pay callback.
39+
*
40+
* For fixed-amount requests (including sub-sat ranges) the original msat value
41+
* from the server is returned verbatim, avoiding precision loss from the
42+
* msat→sat→msat round-trip.
43+
*
44+
* For variable-amount requests the user-selected sat amount is converted to msats.
45+
*/
46+
fun LnurlPayData.callbackAmountMsats(userSats: ULong? = null): ULong =
47+
if (isFixedAmount()) minSendable else (userSats ?: minSendableSat()) * MSATS_PER_SAT
48+
2749
fun LnurlWithdrawData.minWithdrawableSat(): ULong = msatsToSatsCeil(minWithdrawable ?: 0u)
2850
fun LnurlWithdrawData.maxWithdrawableSat(): ULong = maxWithdrawable / MSATS_PER_SAT
51+
52+
/**
53+
* True when the LNURL-withdraw endpoint specifies a single exact amount,
54+
* including the sub-sat edge case where rounding causes `min > max` in whole sats.
55+
*/
56+
fun LnurlWithdrawData.isFixedAmount(): Boolean {
57+
val min = minWithdrawable ?: 0u
58+
return min == maxWithdrawable || (min > 0u && minWithdrawableSat() > maxWithdrawableSat())
59+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ package to.bitkit.ext
22

33
import org.lightningdevkit.ldknode.PaymentDetails
44

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class WakeNodeWorker @AssistedInject constructor(
192192
showDetails: Boolean,
193193
hiddenBody: String,
194194
) {
195-
val sats = event.amountMsat / 1000u
195+
val sats = (event.amountMsat + 999u) / 1000u
196196
// Save for UI to pick up
197197
cacheStore.setBackgroundReceive(
198198
NewTransactionSheetDetails(

app/src/main/java/to/bitkit/models/FeeRate.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ enum class FeeRate(
1515
@DrawableRes val icon: Int,
1616
val color: Color,
1717
) {
18+
INSTANT(
19+
title = R.string.fee__instant__title,
20+
description = R.string.fee__instant__description,
21+
shortDescription = R.string.fee__instant__shortDescription,
22+
color = Colors.Purple,
23+
icon = R.drawable.ic_speed_fast,
24+
),
1825
FAST(
1926
title = R.string.fee__fast__title,
2027
description = R.string.fee__fast__description,
@@ -53,7 +60,7 @@ enum class FeeRate(
5360

5461
fun toSpeed(): TransactionSpeed {
5562
return when (this) {
56-
FAST -> TransactionSpeed.Fast
63+
INSTANT, FAST -> TransactionSpeed.Fast
5764
NORMAL -> TransactionSpeed.Medium
5865
MINIMUM, SLOW -> TransactionSpeed.Slow
5966
CUSTOM -> TransactionSpeed.Custom(0u)

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -897,20 +897,29 @@ class LightningRepo @Inject constructor(
897897
runCatching { lightningService.receive(amountSats, description, expirySeconds) }
898898
}
899899

900+
suspend fun createInvoiceMsats(
901+
amountMsats: ULong,
902+
description: String,
903+
expirySeconds: UInt = 86_400u,
904+
): Result<String> = executeWhenNodeRunning("createInvoiceMsats") {
905+
updateGeoBlockState()
906+
runCatching { lightningService.receiveMsats(amountMsats, description, expirySeconds) }
907+
}
908+
900909
@Suppress("ForbiddenComment")
901910
suspend fun fetchLnurlInvoice(
902911
callbackUrl: String,
903-
amountSats: ULong,
912+
amountMsats: ULong,
904913
comment: String? = null,
905914
): Result<LightningInvoice> {
906915
return runCatching {
907916
// TODO use bitkit-core getLnurlInvoice if it works with callbackUrl
908-
val bolt11 = lnurlService.fetchLnurlInvoice(callbackUrl, amountSats, comment).getOrThrow().pr
917+
val bolt11 = lnurlService.fetchLnurlInvoice(callbackUrl, amountMsats, comment).getOrThrow().pr
909918
val decoded = (coreService.decode(bolt11) as Scanner.Lightning).invoice
910919
return@runCatching decoded
911920
}.onFailure {
912921
Logger.error(
913-
"fetchLnurlInvoice error, url: $callbackUrl, amount: $amountSats, comment: $comment",
922+
"Failed to fetch LNURL invoice, url: '$callbackUrl', amountMsats: '$amountMsats', comment: '$comment'",
914923
it,
915924
context = TAG,
916925
)

0 commit comments

Comments
 (0)