diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 611d417b2a..da98e59b92 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -17,7 +17,6 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory -import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -49,7 +48,6 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -121,8 +119,6 @@ interface ApplicationEntryPoint { fun getOnboardingRepository(): OnboardingRepository - fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider - fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore @@ -155,6 +151,4 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory - - fun getWalletAccountsFetcher(): WalletAccountsFetcher } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 327b223c07..de3252872f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -31,7 +31,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory -import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor @@ -74,10 +73,8 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers @@ -90,7 +87,6 @@ import timber.log.Timber lateinit var store: Store val foregroundActivityObserver = ForegroundActivityObserver -internal lateinit var derivationsFinder: DerivationsFinder open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { @@ -191,9 +187,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val onboardingRepository: OnboardingRepository get() = entryPoint.getOnboardingRepository() - private val dispatchers: CoroutineDispatcherProvider - get() = entryPoint.getCoroutineDispatcherProvider() - private val excludedBlockchains: ExcludedBlockchains get() = entryPoint.getExcludedBlockchains() @@ -247,9 +240,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() - private val walletAccountsFetcher: WalletAccountsFetcher - get() = entryPoint.getWalletAccountsFetcher() - // endregion private val appScope = MainScope() @@ -352,11 +342,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - derivationsFinder = DerivationsFinder( - walletAccountsFetcher = walletAccountsFetcher, - dispatchers = dispatchers, - ) - appStateHolder.mainStore = store wcInitializeUseCase.init( diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 38e66db21b..21dfcaa7f1 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler @@ -40,6 +41,7 @@ internal class TangemSdkManagerModule { appFinisher: AppFinisher, sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, analyticsExceptionHandler: AnalyticsExceptionHandler, + blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -56,6 +58,7 @@ internal class TangemSdkManagerModule { appFinisher = appFinisher, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, analyticsExceptionHandler = analyticsExceptionHandler, + blockchainToDeriveFinder = blockchainToDeriveFinder, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 8639981110..3e6b36a6a5 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent -import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.CreateProductWalletTask -import com.tangem.tap.domain.tasks.product.ResetBackupCardTask -import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask -import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask @@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager( private val appFinisher: AppFinisher, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ) : TangemSdkManager { @@ -162,7 +159,7 @@ internal class DefaultTangemSdkManager( runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, - derivationsFinder = derivationsFinder, + blockchainToDeriveFinder = blockchainToDeriveFinder, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt new file mode 100644 index 0000000000..9118279751 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.tap.features.demo.DemoHelper +import javax.inject.Inject + +/** + * Finder of blockchains to derive. + * Returns only saved, default or demo blockchains without any additional logic + * (no cardano/ethereum additions or unnecessary blockchain removals). + */ +class BlockchainToDeriveFinder @Inject constructor( + private val walletAccountsFetcher: WalletAccountsFetcher, +) { + + suspend fun find(card: CardDTO): Set { + if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() + val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() + + val derivationStyle = card.derivationStyleProvider.getDerivationStyle() + + val blockchains = getBlockchains(userWalletId).ifEmpty { + if (DemoHelper.isDemoCardId(card.cardId)) { + getDemoBlockchains(derivationStyle, card.cardId) + } else { + getDefaultBlockchains(derivationStyle) + } + } + + return blockchains + } + + private suspend fun getBlockchains(userWalletId: UserWalletId): Set { + return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() + .flatMap { accountDTO -> + accountDTO.tokens.orEmpty() + .filter { it.contractAddress == null } + } + .mapNotNull { coin -> + val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null + val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + .toSet() + } + + private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set { + return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set { + val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun Set.mapToBlockchainsWithDerivations( + derivationStyle: DerivationStyle?, + ): Set { + return mapNotNullTo(hashSetOf()) { blockchain -> + val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null + BlockchainToDerive(blockchain, derivationPath) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt deleted file mode 100644 index dd5945533b..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.tap.domain.tasks.product - -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -internal data class BlockchainToDerive( - val blockchain: Blockchain, - val derivationPath: DerivationPath?, -) - -// FIXME: May be move to DI, currently unnecessary -internal class DerivationsFinder( - private val walletAccountsFetcher: WalletAccountsFetcher, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun findBlockchainsToDerive( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): Set { - if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() - val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() - val derivationStyle = derivationStyleProvider.getDerivationStyle() - - val blockchains = withContext(dispatchers.io) { - getBlockchains(userWalletId) - }.ifEmpty { - if (DemoHelper.isDemoCardId(card.cardId)) { - getDemoBlockchains(derivationStyle, card.cardId) - } else { - getDefaultBlockchains(derivationStyle) - } - } - - // we should generate second key for cardano - // because cardano address generation for wallet2 requires keys from 2 derivations - // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ - blockchains.addSecondCardanoDerivationIfPresent() - - if (card.settings.isHDWalletAllowed) { - blockchains.addEthereumBlockchains(derivationStyle) - } - - // pay attention to this - return if (!card.hasOldStyleDerivation) { - blockchains.removeUnnecessaryBlockchains(derivationStyle) - } else { - blockchains - } - } - - private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() - .flatMap { accountDTO -> - accountDTO.tokens.orEmpty() - .filter { it.contractAddress == null } - } - .mapNotNull { coin -> - val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null - val derivationPath = coin.derivationPath?.let(::DerivationPath) - - BlockchainToDerive(blockchain, derivationPath) - } - .toMutableSet() - } - - // TODO: Move to user wallet config - private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet { - return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) - } - - // TODO: Move to user wallet config - private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet { - val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) - - return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) - } -} - -private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { - val ethereumBlockchains = setOf(Blockchain.Ethereum) - .mapToBlockchainsWithDerivations(derivationStyle) - - addAll(ethereumBlockchains) -} - -private fun Set.removeUnnecessaryBlockchains( - derivationStyle: DerivationStyle?, -): Set { - val defaultEthereum = BlockchainToDerive( - blockchain = Blockchain.Ethereum, - derivationPath = Blockchain.Ethereum.derivationPath(derivationStyle), - ) - - val addedEthereum = this.firstOrNull { it == defaultEthereum } - - return if (addedEthereum != null) { - filterNot { it.derivationPath == defaultEthereum.derivationPath && it.blockchain != Blockchain.Ethereum } - } else { - // Impossible case because Ethereum was added at the last stage - distinctBy(BlockchainToDerive::derivationPath) - } - .toSet() -} - -private fun MutableSet.addSecondCardanoDerivationIfPresent() { - val cardanoDerivation = this - .firstOrNull { it.blockchain == Blockchain.Cardano } - ?.derivationPath - ?: return - - val secondCardanoBlockchain = BlockchainToDerive( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation), - ) - - add(secondCardanoBlockchain) -} - -private fun Set.mapToBlockchainsWithDerivations( - derivationStyle: DerivationStyle?, -): MutableSet { - return mapTo(hashSetOf()) { blockchain -> - BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 93a1c1f2be..e33ebc491c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TwinsHelper -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX @@ -44,11 +42,10 @@ import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlin.collections.set internal class ScanProductTask( private val card: Card?, - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, @@ -79,7 +76,7 @@ internal class ScanProductTask( readVisaCard( session = session, cardDto = cardDto, - scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder), callback = callback, ) return @@ -87,7 +84,7 @@ internal class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(derivationsFinder) + else -> ScanWalletProcessor(blockchainToDeriveFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { @@ -160,7 +157,7 @@ internal class ScanProductTask( } private class ScanWalletProcessor( - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -283,7 +280,6 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = getWalletProductType(card) - val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( card = card, @@ -291,8 +287,7 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = - collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = collectDerivations(card, scanResponse) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -322,32 +317,13 @@ private class ScanWalletProcessor( private suspend fun collectDerivations( card: CardDTO, - config: CardConfig, - derivationStyleProvider: DerivationStyleProvider, + scanResponse: ScanResponse, ): Map> { - val derivations = mutableMapOf>() - val blockchains = derivationsFinder - ?.findBlockchainsToDerive(card, derivationStyleProvider) - ?: return derivations + val blockchains = blockchainToDeriveFinder + ?.find(card) + ?: return emptyMap() - blockchains.forEach { blockchain -> - val curve = config.primaryCurve(blockchain.blockchain) - val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach - if (wallet.chainCode == null) return@forEach - - val key = wallet.publicKey.toMapKey() - val path = blockchain.derivationPath - if (path != null) { - val addedDerivations = derivations[key] - if (addedDerivations != null) { - derivations[key] = addedDerivations + path - } else { - derivations[key] = listOf(path) - } - } - } - - return derivations + return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains) } } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..792b932178 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -25,7 +25,7 @@ class FinalizeTwinTask( is CompletionResult.Success -> ScanProductTask( card = readResult.data, - derivationsFinder = null, + blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, onboardingV2FeatureToggles = null, diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt similarity index 63% rename from app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt rename to app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt index 64e3006815..5be7a5b371 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt @@ -1,19 +1,17 @@ package com.tangem.tap.domain.tasks.product import com.google.common.truth.Truth -import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -24,19 +22,16 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class DerivationsFinderTest { +class BlockchainToDeriveFinderTest { private val walletAccountsFetcher = mockk() - private val finder = DerivationsFinder( + private val finder = BlockchainToDeriveFinder( walletAccountsFetcher = walletAccountsFetcher, - dispatchers = TestingCoroutineDispatcherProvider(), ) - private val derivationStyleProvider = mockk() - @AfterEach fun tearDown() { - clearMocks(walletAccountsFetcher, derivationStyleProvider) + clearMocks(walletAccountsFetcher) } @Test @@ -47,7 +42,7 @@ class DerivationsFinderTest { } // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + val actual = finder.find(card) // Assert Truth.assertThat(actual).isEmpty() @@ -62,29 +57,26 @@ class DerivationsFinderTest { } // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + val actual = finder.find(card) // Assert Truth.assertThat(actual).isEmpty() } @Test - fun `GIVEN saved bitcoin THEN return bitcoin and ethereum`() = runTest { + fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - val response = createResponse(Blockchain.Bitcoin) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( createExpected(Blockchain.Bitcoin), - createExpected(Blockchain.Ethereum), ) Truth.assertThat(actual).containsExactlyElementsIn(expected) @@ -98,12 +90,10 @@ class DerivationsFinderTest { val demoCardId = "AC01000000045754" val card = createCardDTO(cardId = demoCardId) - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -124,12 +114,10 @@ class DerivationsFinderTest { val demoCardId = "DE00" val card = createCardDTO(cardId = demoCardId) - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -148,12 +136,10 @@ class DerivationsFinderTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -167,26 +153,19 @@ class DerivationsFinderTest { } @Test - fun `GIVEN saved cardano THEN return cardano and ethereum`() = runTest { + fun `GIVEN saved cardano THEN return only cardano`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - val response = createResponse(Blockchain.Cardano) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( createExpected(Blockchain.Cardano), - createExpected( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(Blockchain.Cardano.getDerivationPath()) - ), - createExpected(Blockchain.Ethereum), ) Truth.assertThat(actual).containsExactlyElementsIn(expected) @@ -195,53 +174,18 @@ class DerivationsFinderTest { } @Test - fun `GIVEN saved eth-like blockchains for v3 config wallet THEN return only unique evm derivations`() = runTest { + fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon) - val blockchains = Blockchain.entries - .filter { it.isEvm() && !it.isTestnet() } - .toTypedArray() - - val response = createResponse(*blockchains) + val response = createResponse(*blockchains.toTypedArray()) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) - - // Assert - val expected = setOf( - createExpected(Blockchain.Ethereum), - createExpected(Blockchain.EthereumClassic), - createExpected(Blockchain.Quai), - createExpected(Blockchain.XDC), - ) - - Truth.assertThat(actual).containsExactlyElementsIn(expected) - - coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } - } - - @Test - fun `GIVEN card has old style derivation (v1 config) THEN return all eth-like blockchains`() = runTest { - // Arrange - val oldBatchId = "AC01" - val card = createCardDTO(batchId = oldBatchId) - - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V1 - - val blockchains = Blockchain.entries - .filter { it.isEvm() && !it.isTestnet() } - .toTypedArray() - - val response = createResponse(*blockchains) - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response - - // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = blockchains.mapTo(hashSetOf(), ::createExpected) @@ -260,6 +204,13 @@ class DerivationsFinderTest { every { this@mockk.cardId } returns cardId every { this@mockk.batchId } returns batchId every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.settings.isKeysImportAllowed } returns true + every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release, + ) every { this@mockk.wallets } returns listOf(wallet) } } @@ -290,7 +241,7 @@ class DerivationsFinderTest { } private fun Blockchain.getDerivationPath(): DerivationPath { - return derivationPath(derivationStyleProvider.getDerivationStyle())!! + return derivationPath(DerivationStyle.V3)!! } private companion object { @@ -298,4 +249,4 @@ class DerivationsFinderTest { // for byteArrayOf(0) val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") } -} \ No newline at end of file +} diff --git a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt index a4a646a6ab..973f6c6895 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt @@ -73,7 +73,7 @@ object MockScanResponseFactory { CardDTO.Wallet( CardWallet( publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name - chainCode = null, + chainCode = ByteArray(32), // chainCode must not be null for HD wallets curve = curve, settings = createSettings(), totalSignedHashes = null, diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt new file mode 100644 index 0000000000..56cfcb4a13 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt @@ -0,0 +1,101 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation +import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.ColdCurvesConfig +import com.tangem.domain.wallets.config.CurvesConfig +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** + * Source of derivations data + */ +internal sealed interface DerivationsSource { + + val isHDWalletAllowed: Boolean + val hasOldStyleDerivation: Boolean + val curvesConfig: CurvesConfig + val derivationStyleProvider: DerivationStyleProvider + + fun getWalletPublicKey(curve: EllipticCurve): ByteArray? + fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap + + data class FromUserWallet(val userWallet: UserWallet) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.settings.isHDWalletAllowed + is UserWallet.Hot -> true + } + + override val hasOldStyleDerivation: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.hasOldStyleDerivation + is UserWallet.Hot -> false + } + + override val curvesConfig: CurvesConfig + get() = userWallet.curvesConfig + + override val derivationStyleProvider: DerivationStyleProvider + get() = userWallet.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getWalletPublicKey(curve) + is UserWallet.Hot -> userWallet.wallets + ?.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey + } + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getDerivedKeys(publicKey) + is UserWallet.Hot -> { + val derivedKeys = userWallet.wallets + ?.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) } + ?.derivedKeys + .orEmpty() + + ExtendedPublicKeysMap(derivedKeys) + } + } + } + } + + data class FromScanResponse(val scanResponse: ScanResponse) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = scanResponse.card.settings.isHDWalletAllowed + + override val hasOldStyleDerivation: Boolean + get() = scanResponse.card.hasOldStyleDerivation + + override val curvesConfig: CurvesConfig + get() = ColdCurvesConfig(scanResponse.card) + + override val derivationStyleProvider: DerivationStyleProvider + get() = scanResponse.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return scanResponse.getWalletPublicKey(curve) + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return scanResponse.getDerivedKeys(publicKey) + } + } +} + +private fun ScanResponse.getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return card.wallets.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey +} + +private fun ScanResponse.getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index ce12ab7516..3ea84acd9e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -3,45 +3,80 @@ package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.config.curvesConfig -import com.tangem.domain.wallets.derivations.derivationStyleProvider -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import kotlin.collections.forEach private typealias DerivationData = Pair> internal typealias Derivations = Map> +/** + * Data class representing a blockchain with its derivation path + */ +data class BlockchainToDerive( + val blockchain: Blockchain, + val derivationPath: DerivationPath, +) + /** * Finder of missed derivations * - * @property userWallet User wallet to find derivations for + * @property source Source of derivations data (UserWallet or ScanResponse) * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val userWallet: UserWallet) { +class MissedDerivationsFinder private constructor(private val source: DerivationsSource) { + + /** + * Secondary constructor for backward compatibility with UserWallet + */ + constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet)) + + /** + * Secondary constructor for ScanResponse + */ + constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse)) /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { return currencies.map { it.network }.let(::findByNetworks) } + /** Find missed derivations for given [Network] list */ fun findByNetworks(networks: List): Derivations { + val blockchainsToDerive = networks.mapNotNull { network -> + val blockchain = network.toBlockchain() + val derivationPath = network.derivationPath.value?.let(::DerivationPath) + ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + return findByBlockchainsToDerive(blockchainsToDerive) + } + + /** Find missed derivations for given [BlockchainToDerive] list */ + fun findByBlockchainsToDerive(blockchainsToDerive: Collection): Derivations { + val enrichedBlockchains = blockchainsToDerive.enrichBlockchains() + return findDerivationsInternal(enrichedBlockchains) + } + + /** + * Common implementation for finding derivations + */ + private fun findDerivationsInternal(items: Collection): Derivations { return buildMap> { - networks - .mapToNewDerivations() + items + .mapNotNull(::mapToNewDerivation) .forEach { data -> val current = this[data.first] if (current != null) { current.addAll(data.second) - current.distinct() + this[data.first] = current.distinct().toMutableList() } else { this[data.first] = data.second.toMutableList() } @@ -49,31 +84,17 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } } - private fun List.mapToNewDerivations(): List { - return mapNotNull { network -> - val blockchain = network.toBlockchain() - val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null + /** + * Maps a single BlockchainToDerive to derivation data (public key -> derivation paths) + */ + private fun mapToNewDerivation(input: BlockchainToDerive): DerivationData? { + val curve = source.curvesConfig.primaryCurve(input.blockchain) ?: return null + if (!input.blockchain.getSupportedCurves().contains(curve)) return null - val walletPublicKey = when (userWallet) { - is UserWallet.Cold -> { - val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } - wallet?.publicKey - } - is UserWallet.Hot -> { - val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } - wallet?.publicKey - } - } + val publicKey = source.getWalletPublicKey(curve) ?: return null - walletPublicKey?.let { - findNewDerivations(curve = curve, publicKey = it, network = network) - } - } - } - - private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { - val derivationCandidates = network - .getDerivationCandidates(curve) + val derivationCandidates = input.blockchain + .getDerivationCandidates(input.derivationPath) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } @@ -81,59 +102,63 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { return publicKey.toMapKey() to derivationCandidates } - private fun Network.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = this.toBlockchain() - + /** + * Gets all possible derivation paths for a blockchain + */ + private fun Blockchain.getDerivationCandidates(derivationPath: DerivationPath): List { return buildList { - add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) + // Default derivation path for blockchain + add(getDerivationPath()) + + // The specified derivation path (can be either default or custom) + add(derivationPath) + + // Extended Cardano derivation path if needed + add(getCardanoExtendedDerivationPath(derivationPath)) } .filterNotNull() .distinct() } - private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) - } else { - null - } + private fun Blockchain.getDerivationPath(): DerivationPath? { + return derivationPath(style = source.derivationStyleProvider.getDerivationStyle()) } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - network.derivationPath.value?.let(::DerivationPath) - } else { - null - } - } - - private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { - return if (this == Blockchain.Cardano) { - network.derivationPath.value?.let { - CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) - } - } else { - null - } + private fun Blockchain.getCardanoExtendedDerivationPath(customDerivationPath: DerivationPath): DerivationPath? { + if (this != Blockchain.Cardano) return null + return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath) } private fun List.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey) + val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList() return filterNot(alreadyDerivedPaths::contains) } - private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = when (userWallet) { - is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) - is UserWallet.Hot -> { - val wallets = userWallet.wallets ?: return emptyList() - wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys - ?: ExtendedPublicKeysMap(emptyMap()) - } + // region Blockchain enrichment logic + + /** + * Enriches blockchains collection: + * - Adds Ethereum if HD wallet is allowed + * - Removes unnecessary blockchains that share derivation path with Ethereum (for cards without old style derivation) + */ + private fun Collection.enrichBlockchains(): Collection { + if (!source.isHDWalletAllowed) return this + + val derivationStyle = source.derivationStyleProvider.getDerivationStyle() + val ethereumDerivationPath = Blockchain.Ethereum.derivationPath(derivationStyle) ?: return this + + val withEthereum = this + BlockchainToDerive(Blockchain.Ethereum, ethereumDerivationPath) + + // For cards with old style derivation, keep all blockchains + if (source.hasOldStyleDerivation) { + return withEthereum.distinct() } - return extendedPublicKeysMap.keys.toList() + // For new cards: filter out blockchains with same derivation path as Ethereum (except Ethereum itself) + return withEthereum + .filter { it.derivationPath != ethereumDerivationPath || it.blockchain == Blockchain.Ethereum } + .distinct() } + + // endregion } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index 426a1bc144..cd22c4c1f8 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -14,11 +14,13 @@ import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MissedDerivationsFinderTest { @Test @@ -97,9 +99,8 @@ internal class MissedDerivationsFinderTest { val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) - Truth.assertThat(actual).containsExactly( - ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()), - listOf( + val expected = mapOf( + ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()) to listOf( DerivationConfigV2.derivations(Blockchain.Cardano).values.first(), CardanoUtils.extendedDerivationPath( derivationPath = DerivationPath( @@ -108,7 +109,12 @@ internal class MissedDerivationsFinderTest { ), ), ), + ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()) to listOf( + DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(), + ), ) + + Truth.assertThat(actual).containsExactlyEntriesIn(expected) } @Test