From b6a6078be969d8fa43f82edffc78f8d861ffdaf5 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 21 Oct 2026 06:37:07 -0600 Subject: [PATCH] perf: skip unloaded accounts in address search --- .../java/to/bitkit/services/CoreService.kt | 17 ++- .../ActivityServicePaykitContactsTest.kt | 103 +++++++++++++++++- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index b59c70edd1..61a0967610 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -1037,8 +1037,16 @@ class ActivityService( return currentWalletAddress } + val accounts = runSuspendCatching { lightningService.listOnchainWalletAccounts() } + .onFailure { Logger.warn("Failed to list onchain wallet accounts", it, context = TAG) } + .getOrNull() + val loadedPrimaryTypes = accounts?.filter { it.accountIndex == 0u } + ?.map { it.addressType.toBitkitAddressType() }?.toSet() + val primaryTypes = prioritizedAddressTypes(selectedAddressType) + .filter { loadedPrimaryTypes == null || it in loadedPrimaryTypes } + for (isChange in listOf(false, true)) { - for (addressType in prioritizedAddressTypes(selectedAddressType)) { + for (addressType in primaryTypes) { searchReceivingAddressForType( details = details, value = value, @@ -1049,12 +1057,9 @@ class ActivityService( } } - val accounts = runSuspendCatching { lightningService.listOnchainWalletAccounts() } - .onFailure { Logger.warn("Failed to list onchain wallet accounts", it, context = TAG) } - .getOrDefault(emptyList()) - .filter { it.accountIndex != 0u } + val companionAccounts = accounts.orEmpty().filter { it.accountIndex != 0u } for (isChange in listOf(false, true)) { - for (account in accounts) { + for (account in companionAccounts) { searchReceivingAddressForType( details = details, value = value, diff --git a/app/src/test/java/to/bitkit/services/ActivityServicePaykitContactsTest.kt b/app/src/test/java/to/bitkit/services/ActivityServicePaykitContactsTest.kt index 2874cd3c9d..f69baba29d 100644 --- a/app/src/test/java/to/bitkit/services/ActivityServicePaykitContactsTest.kt +++ b/app/src/test/java/to/bitkit/services/ActivityServicePaykitContactsTest.kt @@ -14,6 +14,7 @@ import com.synonym.bitkitcore.getActivityByTxId import com.synonym.bitkitcore.getTransactionDetails import com.synonym.bitkitcore.updateActivity import com.synonym.bitkitcore.upsertActivity +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -27,8 +28,11 @@ import org.lightningdevkit.ldknode.PaymentStatus import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import to.bitkit.async.ServiceQueue @@ -399,7 +403,10 @@ class ActivityServicePaykitContactsTest : BaseUnitTest() { addressSearchLastUsedReceiveIndexes = mapOf("nativeSegwit" to 600), ) whenever(lightningService.listOnchainWalletAccounts()).thenReturn( - listOf(OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 5u)), + listOf( + OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 5u), + OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 0u), + ), ) whenever(lightningService.addressInfosForType(AddressType.P2WPKH, false, 200, 200, 5u)) .thenReturn(listOf(AddressDerivationInfo("server-address", 203))) @@ -413,6 +420,100 @@ class ActivityServicePaykitContactsTest : BaseUnitTest() { mapOf("nativeSegwit" to 600, "nativeSegwit:account:5" to 203), cacheData.value.addressSearchLastUsedReceiveIndexes, ) + for (type in listOf(AddressType.P2PKH, AddressType.P2SH, AddressType.P2TR)) { + verify(lightningService, never()).addressInfosForType(eq(type), any(), any(), any(), any()) + } + inOrder(lightningService) { + verify(lightningService).listOnchainWalletAccounts() + verify(lightningService).addressInfosForType(AddressType.P2WPKH, false, 1600, 1, 0u) + verify(lightningService).addressInfosForType(AddressType.P2WPKH, true, 800, 200, 0u) + verify(lightningService).addressInfosForType(AddressType.P2WPKH, false, 200, 200, 5u) + } + } + + @Test + fun `loaded primaries retain selected type and change priority over companion receive`() = coreTest { + whenever(lightningService.listOnchainWalletAccounts()).thenReturn( + listOf( + OnchainWalletAccount(LdkAddressType.LEGACY, 0u), + OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 5u), + OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 0u), + ), + ) + whenever(lightningService.addressInfosForType(AddressType.P2WPKH, true, 0, 200, 0u)) + .thenReturn(listOf(AddressDerivationInfo("primary-change", 3))) + whenever(lightningService.addressInfosForType(AddressType.P2PKH, true, 0, 200, 0u)) + .thenReturn(listOf(AddressDerivationInfo("legacy-change", 4))) + whenever(lightningService.addressInfosForType(AddressType.P2WPKH, false, 0, 200, 5u)) + .thenReturn(listOf(AddressDerivationInfo("companion-receive", 5))) + + receive("companion-receive", "legacy-change", "primary-change") + + assertEquals("primary-change", (updates.single() as Activity.Onchain).v1.address) + assertEquals(mapOf("nativeSegwit" to 3), cacheData.value.addressSearchLastUsedChangeIndexes) + inOrder(lightningService) { + verify(lightningService).addressInfosForType(AddressType.P2WPKH, false, 0, 200, 0u) + verify(lightningService).addressInfosForType(AddressType.P2PKH, false, 0, 200, 0u) + verify(lightningService).addressInfosForType(AddressType.P2WPKH, true, 0, 200, 0u) + } + verify(lightningService, times(11)).addressInfosForType(any(), any(), any(), any(), any()) + } + + @Test + fun `account listing failure preserves primary address discovery`() = coreTest { + whenever(lightningService.listOnchainWalletAccounts()).thenAnswer { throw AppError("Unavailable") } + whenever(lightningService.addressInfosForType(AddressType.P2WPKH, false, 0, 200, 0u)) + .thenReturn(listOf(AddressDerivationInfo("primary-receive", 8))) + + receive("primary-receive") + + assertEquals("primary-receive", (updates.single() as Activity.Onchain).v1.address) + assertEquals(mapOf("nativeSegwit" to 8), cacheData.value.addressSearchLastUsedReceiveIndexes) + } + + @Test + fun `account listing cancellation does not fall back to derivation or write activity`() = coreTest { + whenever(lightningService.listOnchainWalletAccounts()).thenThrow(CancellationException("Cancelled")) + whenever(lightningService.listPayments()).thenReturn(listOf(payment())) + val outputs = listOf(output("primary-receive")) + details = mock { on { this.outputs }.thenReturn(outputs) } + + assertFailsWith { sut.handlePaymentEvent("received") } + + verify(lightningService, never()).addressInfosForType(any(), any(), any(), any(), any()) + assertTrue(updates.isEmpty()) + assertTrue(cacheData.value.addressSearchLastUsedReceiveIndexes.isEmpty()) + assertTrue(cacheData.value.addressSearchLastUsedChangeIndexes.isEmpty()) + } + + @Test + fun `empty loaded accounts skip derivation and the next search reads newly loaded accounts`() = coreTest { + whenever(lightningService.listOnchainWalletAccounts()).thenReturn( + emptyList(), + listOf(OnchainWalletAccount(LdkAddressType.NATIVE_SEGWIT, 0u)), + ) + whenever(lightningService.addressInfosForType(AddressType.P2WPKH, false, 0, 200, 0u)) + .thenReturn(listOf(AddressDerivationInfo("primary-receive", 8))) + + receive("primary-receive") + + assertEquals("Loading...", (updates.single() as Activity.Onchain).v1.address) + verify(lightningService, never()).addressInfosForType(any(), any(), any(), any(), any()) + + receive("primary-receive") + + assertEquals("primary-receive", (updates.last() as Activity.Onchain).v1.address) + verify(lightningService, times(2)).listOnchainWalletAccounts() + verify(lightningService).addressInfosForType(AddressType.P2WPKH, false, 0, 200, 0u) + } + + @Test + fun `current receiving address resolves without listing accounts`() = coreTest { + receive("wallet-address") + + assertEquals("wallet-address", (updates.single() as Activity.Onchain).v1.address) + verify(lightningService, never()).listOnchainWalletAccounts() + verify(lightningService, never()).addressInfosForType(any(), any(), any(), any(), any()) } @Test