Skip to content

Commit ca6504c

Browse files
committed
chore: merge master
2 parents a896d9f + d25de50 commit ca6504c

13 files changed

Lines changed: 209 additions & 98 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ jobs:
126126
E2E: true
127127
E2E_BACKEND: network
128128
GEO: false
129+
TREZOR_BRIDGE: true
130+
TREZOR_BRIDGE_URL: http://10.0.2.2:21325
129131
run: ./gradlew assembleDevDebug
130132

131133
- name: Rename APK
@@ -320,6 +322,7 @@ jobs:
320322
shard:
321323
- { name: multi_address_2_regtest, grep: "@multi_address_2" }
322324
- { name: pubky_paykit, grep: "@pubky" }
325+
- { name: hardware_wallet, grep: "@hardware_wallet" }
323326

324327
name: e2e-tests-staging - ${{ matrix.shard.name }}
325328

@@ -390,6 +393,13 @@ jobs:
390393
working-directory: bitkit-e2e-tests
391394
run: npm ci
392395

396+
- name: Pull Trezor emulator image
397+
if: matrix.shard.name == 'hardware_wallet'
398+
working-directory: bitkit-e2e-tests
399+
run: |
400+
cd docker
401+
docker compose --profile trezor pull --quiet trezor-user-env
402+
393403
- name: Run E2E Tests 1 (${{ matrix.shard.name }})
394404
continue-on-error: true
395405
id: test1

CHANGELOG.md

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

88
## [Unreleased]
99

10+
## [2.3.1] - 2026-06-26
11+
12+
### Fixed
13+
- Improved LNURL-pay invoice validation. #1048
14+
- Improved LNURL-pay payment handling. #1051
15+
1016
## [2.3.0] - 2026-06-05
1117

1218
### Added
@@ -94,6 +100,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
94100
- About screen (content merged into Support) #857
95101
- Standalone General, Security, and Advanced settings screens (merged into tabs) #857
96102

97-
[Unreleased]: https://github.com/synonymdev/bitkit-android/compare/v2.3.0...HEAD
103+
[Unreleased]: https://github.com/synonymdev/bitkit-android/compare/v2.3.1...HEAD
104+
[2.3.1]: https://github.com/synonymdev/bitkit-android/compare/v2.3.0...v2.3.1
98105
[2.3.0]: https://github.com/synonymdev/bitkit-android/compare/v2.2.0...v2.3.0
99106
[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
@@ -169,8 +169,8 @@ android {
169169
applicationId = "to.bitkit"
170170
minSdk = 28
171171
targetSdk = 36
172-
versionCode = 182
173-
versionName = "2.3.0"
172+
versionCode = 184
173+
versionName = "2.3.1"
174174
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
175175
bitkitAndroidTestAnnotation?.let {
176176
testInstrumentationRunnerArguments["annotation"] = it

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import kotlinx.coroutines.Job
55
import to.bitkit.utils.Logger
66

77
@Suppress("TooGenericExceptionCaught")
8-
suspend inline fun <R> runSuspendCatching(block: () -> R): Result<R> =
8+
suspend inline fun <T> runSuspendCatching(crossinline block: suspend () -> T): Result<T> =
99
try {
1010
Result.success(block())
1111
} catch (c: CancellationException) {

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.synonym.bitkitcore.AddressType
66
import com.synonym.bitkitcore.ClosedChannelDetails
77
import com.synonym.bitkitcore.FeeRates
88
import com.synonym.bitkitcore.LightningInvoice
9+
import com.synonym.bitkitcore.LnurlException
10+
import com.synonym.bitkitcore.LnurlPayData
911
import com.synonym.bitkitcore.PreActivityMetadata
1012
import com.synonym.bitkitcore.Scanner
1113
import com.synonym.bitkitcore.createChannelRequestUrl
@@ -993,20 +995,20 @@ class LightningRepo @Inject constructor(
993995
runCatching { lightningService.receiveMsats(amountMsats, description, expirySeconds) }
994996
}
995997

996-
@Suppress("ForbiddenComment")
997998
suspend fun fetchLnurlInvoice(
998-
callbackUrl: String,
999+
data: LnurlPayData,
9991000
amountMsats: ULong,
10001001
comment: String? = null,
10011002
): Result<LightningInvoice> {
10021003
return runCatching {
1003-
// TODO use bitkit-core getLnurlInvoice if it works with callbackUrl
1004-
val bolt11 = lnurlService.fetchLnurlInvoice(callbackUrl, amountMsats, comment).getOrThrow().pr
1004+
val bolt11 = coreService.getLnurlInvoiceForPayData(data, amountMsats, comment)
10051005
val decoded = (coreService.decode(bolt11) as Scanner.Lightning).invoice
10061006
return@runCatching decoded
1007+
}.recoverCatching {
1008+
throw it.toLnurlPayInvoiceError()
10071009
}.onFailure {
10081010
Logger.error(
1009-
"Failed to fetch LNURL invoice, url: '$callbackUrl', amountMsats: '$amountMsats', comment: '$comment'",
1011+
"Failed to fetch LNURL invoice, uri: '${data.uri}', amountMsats: '$amountMsats', comment: '$comment'",
10101012
it,
10111013
context = TAG,
10121014
)
@@ -1717,11 +1719,27 @@ class NodeStopTimeoutError : AppError("Timeout waiting for node to stop")
17171719
class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node to run and execute: '$opName'")
17181720
class GetPaymentsError : AppError("It wasn't possible get the payments")
17191721
class SyncUnhealthyError : AppError("Wallet sync failed before send")
1722+
class LnurlPayInvoiceMismatchError : AppError("The invoice did not match the requested payment. Payment cancelled.")
17201723
sealed class ProbeError(message: String) : AppError(message) {
17211724
class NoProbeHandles : ProbeError("No probe handles returned")
17221725
class TimedOut : ProbeError("Probe timed out")
17231726
}
17241727

1728+
private fun Throwable.toLnurlPayInvoiceError(): Throwable {
1729+
val lnurlPayValidationError = generateSequence(this) { it.cause }
1730+
.firstOrNull { it.isLnurlPayValidationError() }
1731+
1732+
return if (lnurlPayValidationError != null) LnurlPayInvoiceMismatchError() else this
1733+
}
1734+
1735+
private fun Throwable.isLnurlPayValidationError(): Boolean = when (this) {
1736+
is LnurlException.InvalidAmount,
1737+
is LnurlException.AmountMismatch,
1738+
is LnurlException.MetadataMismatch -> true
1739+
1740+
else -> false
1741+
}
1742+
17251743
@Stable
17261744
data class LightningState(
17271745
val nodeId: String = "",

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.synonym.bitkitcore.IcJitEntry
2020
import com.synonym.bitkitcore.LegacyRnCloseRecoveryScanResult
2121
import com.synonym.bitkitcore.LegacyRnCloseRecoverySweepPreview
2222
import com.synonym.bitkitcore.LightningActivity
23+
import com.synonym.bitkitcore.LnurlPayData
2324
import com.synonym.bitkitcore.OnchainActivity
2425
import com.synonym.bitkitcore.PaymentState
2526
import com.synonym.bitkitcore.PaymentType
@@ -108,6 +109,7 @@ import kotlin.random.Random
108109
import com.synonym.bitkitcore.TransactionDetails as BitkitCoreTransactionDetails
109110
import com.synonym.bitkitcore.TxInput as BitkitCoreTxInput
110111
import com.synonym.bitkitcore.TxOutput as BitkitCoreTxOutput
112+
import com.synonym.bitkitcore.getLnurlInvoiceForPayData as coreGetLnurlInvoiceForPayData
111113
import com.synonym.bitkitcore.getTransactionDetails as getBitkitCoreTransactionDetails
112114

113115
// region Core
@@ -220,6 +222,14 @@ class CoreService @Inject constructor(
220222
com.synonym.bitkitcore.decode(input)
221223
}
222224

225+
suspend fun getLnurlInvoiceForPayData(
226+
data: LnurlPayData,
227+
amountMsats: ULong,
228+
comment: String? = null,
229+
): String = ServiceQueue.CORE.background {
230+
coreGetLnurlInvoiceForPayData(data, amountMsats, comment)
231+
}
232+
223233
companion object {
224234
private const val TAG = "CoreService"
225235
}

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

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,6 @@ class LnurlService @Inject constructor(
3939
Logger.warn("Failed to request LNURL withdraw", it, context = TAG)
4040
}
4141

42-
suspend fun fetchLnurlInvoice(
43-
callbackUrl: String,
44-
amountMsats: ULong,
45-
comment: String? = null,
46-
): Result<LnurlPayResponse> = runCatching {
47-
Logger.debug("Fetching LNURL pay invoice from: $callbackUrl", context = TAG)
48-
49-
val response = client.get(callbackUrl) {
50-
url {
51-
parameters["amount"] = "$amountMsats"
52-
comment?.takeIf { it.isNotBlank() }?.let {
53-
parameters["comment"] = it
54-
}
55-
}
56-
}
57-
Logger.debug("Http call: $response", context = TAG)
58-
59-
if (!response.status.isSuccess()) {
60-
throw HttpError("fetchLnurlInvoice error: '${response.status.description}'", response.status.value)
61-
}
62-
63-
return@runCatching response.body<LnurlPayResponse>()
64-
}
65-
6642
suspend fun requestLnurlChannel(url: String): Result<LnurlChannelResponse> = runCatching {
6743
Logger.debug("Requesting LNURL channel request via: '$url'", context = TAG)
6844

@@ -101,12 +77,6 @@ data class LnurlWithdrawResponse(
10177
val balanceCheck: String? = null,
10278
)
10379

104-
@Serializable
105-
data class LnurlPayResponse(
106-
val pr: String,
107-
val routes: List<String>,
108-
)
109-
11080
@Serializable
11181
data class LnurlChannelResponse(
11282
val status: String? = null,

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

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -706,41 +706,40 @@ private fun ActivityDetailContent(
706706
}
707707
)
708708
)
709-
if (isTransfer && channelId != null && onChannelClick != null) {
710-
PrimaryButton(
711-
text = stringResource(R.string.lightning__connection),
712-
size = ButtonSize.Small,
713-
onClick = { onChannelClick(channelId) },
714-
icon = {
715-
Icon(
716-
painter = painterResource(R.drawable.ic_lightning),
717-
contentDescription = null,
718-
tint = accentColor,
719-
modifier = Modifier.size(16.dp)
720-
)
721-
},
722-
modifier = Modifier
723-
.weight(1f)
724-
.testTag("ChannelButton")
725-
)
726-
} else {
727-
PrimaryButton(
728-
text = stringResource(R.string.wallet__activity_explore),
729-
size = ButtonSize.Small,
730-
onClick = { onExploreClick(item) },
731-
icon = {
732-
Icon(
733-
painter = painterResource(R.drawable.ic_git_branch),
734-
contentDescription = null,
735-
tint = accentColor,
736-
modifier = Modifier.size(16.dp)
737-
)
738-
},
739-
modifier = Modifier
740-
.weight(1f)
741-
.testTag("ActivityTxDetails")
742-
)
743-
}
709+
PrimaryButton(
710+
text = stringResource(R.string.wallet__activity_explore),
711+
size = ButtonSize.Small,
712+
onClick = { onExploreClick(item) },
713+
icon = {
714+
Icon(
715+
painter = painterResource(R.drawable.ic_git_branch),
716+
contentDescription = null,
717+
tint = accentColor,
718+
modifier = Modifier.size(16.dp)
719+
)
720+
},
721+
modifier = Modifier
722+
.weight(1f)
723+
.testTag("ActivityTxDetails")
724+
)
725+
}
726+
if (isTransfer && channelId != null && onChannelClick != null) {
727+
PrimaryButton(
728+
text = stringResource(R.string.lightning__connection),
729+
size = ButtonSize.Small,
730+
onClick = { onChannelClick(channelId) },
731+
icon = {
732+
Icon(
733+
painter = painterResource(R.drawable.ic_lightning),
734+
contentDescription = null,
735+
tint = accentColor,
736+
modifier = Modifier.size(16.dp)
737+
)
738+
},
739+
modifier = Modifier
740+
.fillMaxWidth()
741+
.testTag("ChannelButton")
742+
)
744743
}
745744
}
746745
}

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ import to.bitkit.repositories.CurrencyRepo
128128
import to.bitkit.repositories.HealthRepo
129129
import to.bitkit.repositories.HwWalletRepo
130130
import to.bitkit.repositories.LightningRepo
131+
import to.bitkit.repositories.LnurlPayInvoiceMismatchError
131132
import to.bitkit.repositories.PaymentPendingException
132133
import to.bitkit.repositories.PendingPaymentNotification
133134
import to.bitkit.repositories.PendingPaymentRepo
@@ -2147,8 +2148,7 @@ class AppViewModel @Inject constructor(
21472148
lnurlPay != null -> {
21482149
QuickPayData.LnurlPay(
21492150
sats = amountSats,
2150-
callback = lnurlPay.callback,
2151-
amountMsats = lnurlPay.callbackAmountMsats(amountSats),
2151+
data = lnurlPay,
21522152
)
21532153
}
21542154

@@ -2271,15 +2271,16 @@ class AppViewModel @Inject constructor(
22712271
if (isLnurlPay) {
22722272
val amountMsats = lnurl.data.callbackAmountMsats(amount)
22732273
lightningRepo.fetchLnurlInvoice(
2274-
callbackUrl = lnurl.data.callback,
2274+
data = lnurl.data,
22752275
amountMsats = amountMsats,
22762276
comment = _sendUiState.value.comment.takeIf { it.isNotEmpty() },
22772277
).onSuccess { invoice ->
22782278
_sendUiState.update {
22792279
it.copy(decodedInvoice = invoice)
22802280
}
22812281
}.onFailure {
2282-
toast(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch)))
2282+
val message = getLnurlInvoiceFetchErrorMessage(it)
2283+
toast(Exception(message))
22832284
hideSheet()
22842285
return
22852286
}
@@ -2380,6 +2381,11 @@ class AppViewModel @Inject constructor(
23802381
}
23812382
}
23822383

2384+
private fun getLnurlInvoiceFetchErrorMessage(error: Throwable): String = when (error) {
2385+
is LnurlPayInvoiceMismatchError -> context.getString(R.string.lightning__order_state__payment_canceled)
2386+
else -> context.getString(R.string.wallet__error_lnurl_invoice_fetch)
2387+
}
2388+
23832389
fun onConfirmWithdraw() {
23842390
_sendUiState.update { it.copy(isLoading = true) }
23852391
viewModelScope.launch {
@@ -3401,7 +3407,7 @@ sealed interface QuickPayData {
34013407
data class Bolt11(override val sats: ULong, val bolt11: String) : QuickPayData
34023408

34033409
@Stable
3404-
data class LnurlPay(override val sats: ULong, val callback: String, val amountMsats: ULong) : QuickPayData
3410+
data class LnurlPay(override val sats: ULong, val data: LnurlPayData) : QuickPayData
34053411
}
34063412
// endregion
34073413

0 commit comments

Comments
 (0)