Skip to content

Commit 9450857

Browse files
committed
fix: bip21 wallet state management on events
1 parent a510992 commit 9450857

File tree

5 files changed

+107
-133
lines changed

5 files changed

+107
-133
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class LightningRepo @Inject constructor(
257257
_isRecoveryMode.value = enabled
258258
}
259259

260-
suspend fun updateGeoBlockState() {
260+
suspend fun updateGeoBlockState() = withContext(bgDispatcher) {
261261
val (isGeoBlocked, shouldBlockLightning) = coreService.checkGeoBlock()
262262
_lightningState.update {
263263
it.copy(isGeoBlocked = isGeoBlocked, shouldBlockLightningReceive = shouldBlockLightning)

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

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -98,39 +98,15 @@ class WalletRepo @Inject constructor(
9898
}
9999
}
100100

101-
suspend fun refreshBip21(force: Boolean = false): Result<Unit> = withContext(bgDispatcher) {
102-
Logger.debug("Refreshing bip21 (force: $force)", context = TAG)
101+
suspend fun refreshBip21(): Result<Unit> = withContext(bgDispatcher) {
102+
Logger.debug("Refreshing bip21", context = TAG)
103103

104-
val shouldBlockLightningReceive = coreService.checkGeoBlock().second
104+
val (_, shouldBlockLightningReceive) = coreService.checkGeoBlock()
105105
_walletState.update {
106106
it.copy(receiveOnSpendingBalance = !shouldBlockLightningReceive)
107107
}
108-
109-
// Reset invoice state
110-
_walletState.update {
111-
it.copy(
112-
selectedTags = emptyList(),
113-
bip21Description = "",
114-
bip21 = "",
115-
bip21AmountSats = null,
116-
)
117-
}
118-
119-
// Check current address or generate new one
120-
val currentAddress = getOnchainAddress()
121-
if (force || currentAddress.isEmpty()) {
122-
newAddress()
123-
} else {
124-
// Check if current address has been used
125-
checkAddressUsage(currentAddress)
126-
.onSuccess { hasTransactions ->
127-
if (hasTransactions) {
128-
// Address has been used, generate a new one
129-
newAddress()
130-
}
131-
}
132-
}
133-
108+
clearBip21State()
109+
refreshAddressIfNeeded()
134110
updateBip21Invoice()
135111
return@withContext Result.success(Unit)
136112
}
@@ -172,11 +148,66 @@ class WalletRepo @Inject constructor(
172148

173149
suspend fun refreshBip21ForEvent(event: Event) {
174150
when (event) {
175-
is Event.PaymentReceived, is Event.ChannelReady, is Event.ChannelClosed -> refreshBip21()
151+
is Event.ChannelReady -> {
152+
// Only refresh bolt11 if we can now receive on lightning
153+
if (lightningRepo.canReceive()) {
154+
lightningRepo.createInvoice(
155+
amountSats = _walletState.value.bip21AmountSats,
156+
description = _walletState.value.bip21Description,
157+
).onSuccess { bolt11 ->
158+
setBolt11(bolt11)
159+
updateBip21Url()
160+
}
161+
}
162+
}
163+
164+
is Event.ChannelClosed -> {
165+
// Clear bolt11 if we can no longer receive on lightning
166+
if (!lightningRepo.canReceive()) {
167+
setBolt11("")
168+
updateBip21Url()
169+
}
170+
}
171+
172+
is Event.PaymentReceived -> {
173+
// Check if onchain address was used, generate new one if needed
174+
refreshAddressIfNeeded()
175+
updateBip21Url()
176+
}
177+
176178
else -> Unit
177179
}
178180
}
179181

182+
private suspend fun refreshAddressIfNeeded() = withContext(bgDispatcher) {
183+
val address = getOnchainAddress()
184+
if (address.isEmpty()) {
185+
newAddress()
186+
} else {
187+
checkAddressUsage(address).onSuccess { wasUsed ->
188+
if (wasUsed) {
189+
newAddress()
190+
}
191+
}
192+
}
193+
}
194+
195+
private suspend fun updateBip21Url(
196+
amountSats: ULong? = _walletState.value.bip21AmountSats,
197+
message: String = _walletState.value.bip21Description,
198+
): String {
199+
val address = getOnchainAddress()
200+
val newBip21 = buildBip21Url(
201+
bitcoinAddress = address,
202+
amountSats = amountSats,
203+
message = message.ifBlank { Env.DEFAULT_INVOICE_MESSAGE },
204+
lightningInvoice = getBolt11(),
205+
)
206+
setBip21(newBip21)
207+
208+
return newBip21
209+
}
210+
180211
suspend fun createWallet(bip39Passphrase: String?): Result<Unit> = withContext(bgDispatcher) {
181212
lightningRepo.setRecoveryMode(enabled = false)
182213
try {
@@ -310,12 +341,19 @@ class WalletRepo @Inject constructor(
310341
}
311342

312343
// BIP21 state management
313-
fun updateBip21AmountSats(amount: ULong?) {
314-
_walletState.update { it.copy(bip21AmountSats = amount) }
315-
}
344+
fun setBip21AmountSats(amount: ULong?) = _walletState.update { it.copy(bip21AmountSats = amount) }
345+
346+
fun setBip21Description(description: String) = _walletState.update { it.copy(bip21Description = description) }
316347

317-
fun updateBip21Description(description: String) {
318-
_walletState.update { it.copy(bip21Description = description) }
348+
fun clearBip21State() {
349+
_walletState.update {
350+
it.copy(
351+
bip21 = "",
352+
selectedTags = emptyList(),
353+
bip21AmountSats = null,
354+
bip21Description = "",
355+
)
356+
}
319357
}
320358

321359
suspend fun toggleReceiveOnSpendingBalance(): Result<Unit> = withContext(bgDispatcher) {
@@ -348,37 +386,23 @@ class WalletRepo @Inject constructor(
348386

349387
// BIP21 invoice creation
350388
suspend fun updateBip21Invoice(
351-
amountSats: ULong? = null,
352-
description: String = "",
389+
amountSats: ULong? = walletState.value.bip21AmountSats,
390+
description: String = walletState.value.bip21Description,
353391
): Result<Unit> = withContext(bgDispatcher) {
354392
try {
355-
updateBip21AmountSats(amountSats)
356-
updateBip21Description(description)
393+
setBip21AmountSats(amountSats)
394+
setBip21Description(description)
357395

358396
val canReceive = lightningRepo.canReceive()
359397
if (canReceive && _walletState.value.receiveOnSpendingBalance) {
360-
lightningRepo.createInvoice(
361-
amountSats = _walletState.value.bip21AmountSats,
362-
description = _walletState.value.bip21Description,
363-
).onSuccess { bolt11 ->
364-
setBolt11(bolt11)
398+
lightningRepo.createInvoice(amountSats, description).onSuccess {
399+
setBolt11(it)
365400
}
366401
} else {
367402
setBolt11("")
368403
}
369-
val address = getOnchainAddress()
370-
val newBip21 = buildBip21Url(
371-
bitcoinAddress = address,
372-
amountSats = _walletState.value.bip21AmountSats,
373-
message = description.ifBlank { Env.DEFAULT_INVOICE_MESSAGE },
374-
lightningInvoice = getBolt11()
375-
)
376-
setBip21(newBip21)
377-
saveInvoiceWithTags(
378-
bip21Invoice = newBip21,
379-
onChainAddress = address,
380-
tags = _walletState.value.selectedTags
381-
)
404+
val newBip21Url = updateBip21Url(amountSats, description)
405+
persistTagsMetadata(newBip21Url)
382406
Result.success(Unit)
383407
} catch (e: Throwable) {
384408
Logger.error("Update BIP21 invoice error", e, context = TAG)
@@ -404,14 +428,16 @@ class WalletRepo @Inject constructor(
404428
}
405429
}
406430

407-
suspend fun saveInvoiceWithTags(bip21Invoice: String, onChainAddress: String, tags: List<String>) =
431+
private suspend fun persistTagsMetadata(bip21Url: String) =
408432
withContext(bgDispatcher) {
433+
val tags = _walletState.value.selectedTags
409434
if (tags.isEmpty()) return@withContext
410435

436+
val onChainAddress = getOnchainAddress()
437+
411438
try {
412-
deleteExpiredInvoices()
413-
val decoded = decode(bip21Invoice)
414-
val paymentHash = when (decoded) {
439+
deleteExpiredTagMetadata()
440+
val paymentHash = when (val decoded = decode(bip21Url)) {
415441
is Scanner.Lightning -> decoded.invoice.paymentHash.toHex()
416442
is Scanner.OnChain -> decoded.extractLightningHash()
417443
else -> null
@@ -432,15 +458,7 @@ class WalletRepo @Inject constructor(
432458
}
433459
}
434460

435-
suspend fun deleteAllInvoices() = withContext(bgDispatcher) {
436-
try {
437-
db.tagMetadataDao().deleteAll()
438-
} catch (e: Throwable) {
439-
Logger.error("deleteAllInvoices error", e, context = TAG)
440-
}
441-
}
442-
443-
suspend fun deleteExpiredInvoices() = withContext(bgDispatcher) {
461+
private suspend fun deleteExpiredTagMetadata() = withContext(bgDispatcher) {
444462
try {
445463
val twoDaysAgoMillis = Clock.System.now().minus(2.days).toEpochMilliseconds()
446464
db.tagMetadataDao().deleteExpired(expirationTimeStamp = twoDaysAgoMillis)
@@ -451,9 +469,8 @@ class WalletRepo @Inject constructor(
451469

452470
private suspend fun Scanner.OnChain.extractLightningHash(): String? {
453471
val lightningInvoice: String = this.invoice.params?.get("lightning") ?: return null
454-
val decoded = decode(lightningInvoice)
455472

456-
return when (decoded) {
473+
return when (val decoded = decode(lightningInvoice)) {
457474
is Scanner.Lightning -> decoded.invoice.paymentHash.toHex()
458475
else -> null
459476
}

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

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,10 @@ class WalletViewModel @Inject constructor(
202202
}
203203

204204
fun updateBip21Invoice(
205-
amountSats: ULong? = null,
205+
amountSats: ULong? = walletState.value.bip21AmountSats,
206206
) {
207207
viewModelScope.launch {
208-
walletRepo.updateBip21Invoice(
209-
amountSats = amountSats,
210-
description = walletState.value.bip21Description,
211-
).onFailure { error ->
208+
walletRepo.updateBip21Invoice(amountSats).onFailure { error ->
212209
ToastEventBus.send(
213210
type = Toast.ToastType.ERROR,
214211
title = "Error updating invoice",
@@ -220,33 +217,23 @@ class WalletViewModel @Inject constructor(
220217

221218
fun toggleReceiveOnSpending() {
222219
viewModelScope.launch {
223-
walletRepo.toggleReceiveOnSpendingBalance().onSuccess {
224-
updateBip21Invoice(
225-
amountSats = walletState.value.bip21AmountSats,
226-
)
227-
}.onFailure { e ->
228-
if (e is ServiceError.GeoBlocked) {
229-
walletEffect(WalletViewModelEffects.NavigateGeoBlockScreen)
230-
return@launch
220+
walletRepo.toggleReceiveOnSpendingBalance()
221+
.onSuccess {
222+
updateBip21Invoice()
223+
}.onFailure { e ->
224+
if (e is ServiceError.GeoBlocked) {
225+
walletEffect(WalletViewModelEffects.NavigateGeoBlockScreen)
226+
return@launch
227+
}
228+
updateBip21Invoice()
231229
}
232-
233-
updateBip21Invoice(
234-
amountSats = walletState.value.bip21AmountSats,
235-
)
236-
}
237230
}
238231
}
239232

240-
fun refreshReceiveState() = viewModelScope.launch(bgDispatcher) {
241-
launch { lightningRepo.updateGeoBlockState() }
242-
launch { walletRepo.refreshBip21() }
233+
fun refreshReceiveState() = viewModelScope.launch {
243234
launch { blocktankRepo.refreshInfo() }
244-
}
245-
246-
fun refreshBip21() {
247-
viewModelScope.launch {
248-
walletRepo.refreshBip21()
249-
}
235+
lightningRepo.updateGeoBlockState()
236+
walletRepo.refreshBip21()
250237
}
251238

252239
fun wipeWallet() {
@@ -290,7 +277,7 @@ class WalletViewModel @Inject constructor(
290277
if (newText.isEmpty()) {
291278
Logger.warn("Empty")
292279
}
293-
walletRepo.updateBip21Description(newText)
280+
walletRepo.setBip21Description(newText)
294281
}
295282

296283
suspend fun handleHideBalanceOnOpen() {

app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -200,19 +200,6 @@ class WalletRepoTest : BaseUnitTest() {
200200
verify(lightningRepo, never()).newAddress()
201201
}
202202

203-
@Test
204-
fun `refreshBip21 forced should always generate new address`() = test {
205-
val existingAddress = "existingAddress"
206-
whenever(cacheStore.data).thenReturn(flowOf(AppCacheData(onchainAddress = existingAddress)))
207-
whenever(lightningRepo.newAddress()).thenReturn(Result.success("newAddress"))
208-
whenever(addressChecker.getAddressInfo(any())).thenReturn(mockAddressInfo())
209-
210-
val result = sut.refreshBip21(force = true)
211-
212-
assertTrue(result.isSuccess)
213-
verify(lightningRepo).newAddress()
214-
}
215-
216203
@Test
217204
fun `syncBalances should update balance cache and state`() = test {
218205
val expectedState = BalanceState(

app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ class WalletViewModelTest : BaseUnitTest() {
6969
fun `refreshReceiveState should refresh receive state`() = test {
7070
sut.refreshReceiveState()
7171

72+
verify(blocktankRepo).refreshInfo()
7273
verify(lightningRepo).updateGeoBlockState()
7374
verify(walletRepo).refreshBip21()
74-
verify(blocktankRepo).refreshInfo()
7575
}
7676

7777
@Test
@@ -104,23 +104,6 @@ class WalletViewModelTest : BaseUnitTest() {
104104
// Add verification for ToastEventBus.send if you have a way to capture those events
105105
}
106106

107-
@Test
108-
fun `updateBip21Invoice should call walletRepo updateBip21Invoice and send failure toast`() = test {
109-
val testError = Exception("Test error")
110-
whenever(walletRepo.updateBip21Invoice(anyOrNull(), any())).thenReturn(Result.failure(testError))
111-
112-
sut.updateBip21Invoice()
113-
114-
verify(walletRepo).updateBip21Invoice(anyOrNull(), any())
115-
// Add verification for ToastEventBus.send
116-
}
117-
118-
@Test
119-
fun `refreshBip21 should call walletRepo refreshBip21`() = test {
120-
sut.refreshBip21()
121-
verify(walletRepo).refreshBip21()
122-
}
123-
124107
@Test
125108
fun `wipeWallet should call walletRepo wipeWallet`() =
126109
test {
@@ -170,7 +153,7 @@ class WalletViewModelTest : BaseUnitTest() {
170153
fun `updateBip21Description should call walletRepo updateBip21Description`() = test {
171154
sut.updateBip21Description("test_description")
172155

173-
verify(walletRepo).updateBip21Description("test_description")
156+
verify(walletRepo).setBip21Description("test_description")
174157
}
175158

176159
@Test

0 commit comments

Comments
 (0)