From 09efb91aac6cb67650b380723906e0ffcadc8c16 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 19:40:06 +0000 Subject: [PATCH 1/7] fix: cache VSS store ID per walletIndex This change ensures that VSS store ID derivation maintains separate caches for each wallet index, preventing collisions when managing multiple wallets within the same application instance. Changes: - Replace single cachedStoreId with cachedStoreIds map keyed by walletIndex - Update getVssStoreId() to accept walletIndex parameter (defaults to 0) - Add clearCache() method to clear all cached store IDs - Add clearCache(walletIndex) method to clear cache for specific wallet - Update LightningService to pass walletIndex when getting store ID - Enhance logging to include walletIndex This matches the improvements from bitkit-ios PR #202, commit 91b47ba. --- .../bitkit/data/backup/VssStoreIdProvider.kt | 24 ++++++++++++++----- .../to/bitkit/services/LightningService.kt | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt index 3b4e1c81d..99e2eca00 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt @@ -13,13 +13,13 @@ class VssStoreIdProvider @Inject constructor( private val keychain: Keychain, ) { @Volatile - private var cachedStoreId: String? = null + private var cachedStoreIds: MutableMap = mutableMapOf() - fun getVssStoreId(): String { - cachedStoreId?.let { return it } + fun getVssStoreId(walletIndex: Int = 0): String { + cachedStoreIds[walletIndex]?.let { return it } return synchronized(this) { - cachedStoreId?.let { return it } + cachedStoreIds[walletIndex]?.let { return it } val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) @@ -30,12 +30,24 @@ class VssStoreIdProvider @Inject constructor( passphrase = passphrase, ) - Logger.info("VSS store id: '$storeId'", context = TAG) - cachedStoreId = storeId + Logger.info("VSS store id: '$storeId' for walletIndex: $walletIndex", context = TAG) + cachedStoreIds[walletIndex] = storeId storeId } } + fun clearCache() { + synchronized(this) { + cachedStoreIds.clear() + } + } + + fun clearCache(walletIndex: Int) { + synchronized(this) { + cachedStoreIds.remove(walletIndex) + } + } + companion object { private const val TAG = "VssStoreIdProvider" } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index b6bc78175..fc049e226 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -104,7 +104,7 @@ class LightningService @Inject constructor( Logger.debug("Building node…") - val vssStoreId = vssStoreIdProvider.getVssStoreId() + val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex) ServiceQueue.LDK.background { node = try { From 8dc99c711716eb766007fdd9723ed8ee74057786 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 20:08:08 +0000 Subject: [PATCH 2/7] fix: improve VssStoreIdProvider thread-safety Changes: - Remove @Volatile annotation (redundant with synchronized blocks) - Change cachedStoreIds from var to val (map reference never changes) - Move cache check inside synchronized block for proper thread-safety - MutableMap operations must be synchronized; @Volatile only affects reference visibility This ensures all map operations happen under synchronization, preventing potential race conditions when multiple threads access the cache. --- .../java/to/bitkit/data/backup/VssStoreIdProvider.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt index 99e2eca00..d551cfef2 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt @@ -12,13 +12,10 @@ import javax.inject.Singleton class VssStoreIdProvider @Inject constructor( private val keychain: Keychain, ) { - @Volatile - private var cachedStoreIds: MutableMap = mutableMapOf() + private val cachedStoreIds: MutableMap = mutableMapOf() fun getVssStoreId(walletIndex: Int = 0): String { - cachedStoreIds[walletIndex]?.let { return it } - - return synchronized(this) { + synchronized(this) { cachedStoreIds[walletIndex]?.let { return it } val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound @@ -32,7 +29,7 @@ class VssStoreIdProvider @Inject constructor( Logger.info("VSS store id: '$storeId' for walletIndex: $walletIndex", context = TAG) cachedStoreIds[walletIndex] = storeId - storeId + return storeId } } From 0b3be33737b7e14993b32df99c5a2828092954bb Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 22:40:57 +0100 Subject: [PATCH 3/7] fix: use ConcurrentHashMap for thread-safety --- .../java/to/bitkit/data/backup/VssStoreIdProvider.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt index d551cfef2..4ef6f31de 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt @@ -5,6 +5,7 @@ import to.bitkit.data.keychain.Keychain import to.bitkit.env.Env import to.bitkit.utils.Logger import to.bitkit.utils.ServiceError +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -12,7 +13,7 @@ import javax.inject.Singleton class VssStoreIdProvider @Inject constructor( private val keychain: Keychain, ) { - private val cachedStoreIds: MutableMap = mutableMapOf() + private val cachedStoreIds: MutableMap = ConcurrentHashMap() fun getVssStoreId(walletIndex: Int = 0): String { synchronized(this) { @@ -34,15 +35,11 @@ class VssStoreIdProvider @Inject constructor( } fun clearCache() { - synchronized(this) { - cachedStoreIds.clear() - } + cachedStoreIds.clear() } fun clearCache(walletIndex: Int) { - synchronized(this) { - cachedStoreIds.remove(walletIndex) - } + cachedStoreIds.remove(walletIndex) } companion object { From 7f6b7b3b32c13e0bd47b4986aad9254505989ec2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 22:55:20 +0100 Subject: [PATCH 4/7] fix: pass walletIndex to VssBackupClient setup --- app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt index 845510c18..76c74361c 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt @@ -26,12 +26,13 @@ class VssBackupClient @Inject constructor( ) { private val isSetup = CompletableDeferred() - suspend fun setup() = withContext(bgDispatcher) { + suspend fun setup(walletIndex: Int = 0) = withContext(bgDispatcher) { try { withTimeout(30.seconds) { Logger.debug("VSS client setting up…", context = TAG) val vssUrl = Env.vssServerUrl val lnurlAuthServerUrl = Env.lnurlAuthServerUrl + val vssStoreId = vssStoreIdProvider.getVssStoreId(walletIndex) Logger.verbose("Building VSS client with vssUrl: '$vssUrl'") Logger.verbose("Building VSS client with lnurlAuthServerUrl: '$lnurlAuthServerUrl'") if (lnurlAuthServerUrl.isNotEmpty()) { @@ -41,7 +42,7 @@ class VssBackupClient @Inject constructor( vssNewClientWithLnurlAuth( baseUrl = vssUrl, - storeId = vssStoreIdProvider.getVssStoreId(), + storeId = vssStoreId, mnemonic = mnemonic, passphrase = passphrase, lnurlAuthServerUrl = lnurlAuthServerUrl, @@ -49,7 +50,7 @@ class VssBackupClient @Inject constructor( } else { vssNewClient( baseUrl = vssUrl, - storeId = vssStoreIdProvider.getVssStoreId(), + storeId = vssStoreId, ) } isSetup.complete(Unit) From a5c0bc058349cd9026dec14c151020a98e800379 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 23:11:10 +0100 Subject: [PATCH 5/7] feat: clear cached vss store id on wipe --- .../main/java/to/bitkit/data/backup/VssStoreIdProvider.kt | 8 ++------ app/src/main/java/to/bitkit/repositories/WalletRepo.kt | 3 +++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt index 4ef6f31de..e6c297062 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt @@ -28,17 +28,13 @@ class VssStoreIdProvider @Inject constructor( passphrase = passphrase, ) - Logger.info("VSS store id: '$storeId' for walletIndex: $walletIndex", context = TAG) cachedStoreIds[walletIndex] = storeId + Logger.info("VSS store id setup for wallet[$walletIndex]: '$storeId'", context = TAG) return storeId } } - fun clearCache() { - cachedStoreIds.clear() - } - - fun clearCache(walletIndex: Int) { + fun clearCache(walletIndex: Int = 0) { cachedStoreIds.remove(walletIndex) } diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 3882c75cb..54ac93f6d 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -18,6 +18,7 @@ import org.lightningdevkit.ldknode.Event import to.bitkit.data.AppDb import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore +import to.bitkit.data.backup.VssStoreIdProvider import to.bitkit.data.entities.TagMetadataEntity import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher @@ -50,6 +51,7 @@ class WalletRepo @Inject constructor( private val lightningRepo: LightningRepo, private val cacheStore: CacheStore, private val deriveBalanceStateUseCase: DeriveBalanceStateUseCase, + private val vssStoreIdProvider: VssStoreIdProvider, ) { private val repoScope = CoroutineScope(bgDispatcher + SupervisorJob()) @@ -210,6 +212,7 @@ class WalletRepo @Inject constructor( suspend fun wipeWallet(walletIndex: Int = 0): Result = withContext(bgDispatcher) { try { keychain.wipe() + vssStoreIdProvider.clearCache(walletIndex) db.clearAllTables() settingsStore.reset() cacheStore.reset() From 85a8757b0e3b7352e2cbd5243fdeb5c7f6a66a8f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 23:27:20 +0100 Subject: [PATCH 6/7] refactor: rename to cacheMap --- .../main/java/to/bitkit/data/backup/VssStoreIdProvider.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt index e6c297062..ddcb202e9 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssStoreIdProvider.kt @@ -13,11 +13,11 @@ import javax.inject.Singleton class VssStoreIdProvider @Inject constructor( private val keychain: Keychain, ) { - private val cachedStoreIds: MutableMap = ConcurrentHashMap() + private val cacheMap: MutableMap = ConcurrentHashMap() fun getVssStoreId(walletIndex: Int = 0): String { synchronized(this) { - cachedStoreIds[walletIndex]?.let { return it } + cacheMap[walletIndex]?.let { return it } val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw ServiceError.MnemonicNotFound val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) @@ -28,14 +28,14 @@ class VssStoreIdProvider @Inject constructor( passphrase = passphrase, ) - cachedStoreIds[walletIndex] = storeId + cacheMap[walletIndex] = storeId Logger.info("VSS store id setup for wallet[$walletIndex]: '$storeId'", context = TAG) return storeId } } fun clearCache(walletIndex: Int = 0) { - cachedStoreIds.remove(walletIndex) + cacheMap.remove(walletIndex) } companion object { From c1a4a6c56ff2741d93146925295694588c46d958 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 4 Nov 2025 23:43:04 +0100 Subject: [PATCH 7/7] fix: wallet repo test --- app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt index 27259bb42..1fd6f2429 100644 --- a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt @@ -20,6 +20,7 @@ import to.bitkit.data.AppDb import to.bitkit.data.CacheStore import to.bitkit.data.SettingsData import to.bitkit.data.SettingsStore +import to.bitkit.data.backup.VssStoreIdProvider import to.bitkit.data.keychain.Keychain import to.bitkit.models.BalanceState import to.bitkit.services.CoreService @@ -46,6 +47,7 @@ class WalletRepoTest : BaseUnitTest() { private val lightningRepo: LightningRepo = mock() private val cacheStore: CacheStore = mock() private val deriveBalanceStateUseCase: DeriveBalanceStateUseCase = mock() + private val vssStoreIdProvider = mock() @Before fun setUp() { @@ -75,6 +77,7 @@ class WalletRepoTest : BaseUnitTest() { lightningRepo = lightningRepo, cacheStore = cacheStore, deriveBalanceStateUseCase = deriveBalanceStateUseCase, + vssStoreIdProvider = vssStoreIdProvider, ) @Test