Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-14 23:42:14 +04:00
parent 040b5f0cf6
commit 505df4db90
13 changed files with 326 additions and 352 deletions

View file

@ -17,7 +17,6 @@ import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory 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.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage 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.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@ -121,8 +119,6 @@ interface ApplicationEntryPoint {
fun getOnboardingRepository(): OnboardingRepository fun getOnboardingRepository(): OnboardingRepository
fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
fun getExcludedBlockchains(): ExcludedBlockchains fun getExcludedBlockchains(): ExcludedBlockchains
fun getAppLogsStore(): AppLogsStore fun getAppLogsStore(): AppLogsStore
@ -155,6 +151,4 @@ interface ApplicationEntryPoint {
fun getABTestsManager(): ABTestsManager fun getABTestsManager(): ABTestsManager
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
fun getWalletAccountsFetcher(): WalletAccountsFetcher
} }

View file

@ -31,7 +31,6 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory 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.MoshiConverter
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor 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.AppState
import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles 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.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -90,7 +87,6 @@ import timber.log.Timber
lateinit var store: Store<AppState> lateinit var store: Store<AppState>
val foregroundActivityObserver = ForegroundActivityObserver val foregroundActivityObserver = ForegroundActivityObserver
internal lateinit var derivationsFinder: DerivationsFinder
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
@ -191,9 +187,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val onboardingRepository: OnboardingRepository private val onboardingRepository: OnboardingRepository
get() = entryPoint.getOnboardingRepository() get() = entryPoint.getOnboardingRepository()
private val dispatchers: CoroutineDispatcherProvider
get() = entryPoint.getCoroutineDispatcherProvider()
private val excludedBlockchains: ExcludedBlockchains private val excludedBlockchains: ExcludedBlockchains
get() = entryPoint.getExcludedBlockchains() get() = entryPoint.getExcludedBlockchains()
@ -247,9 +240,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val appsFlyerClientFactory: AppsFlyerClient.Factory private val appsFlyerClientFactory: AppsFlyerClient.Factory
get() = entryPoint.getAppsFlyerClientFactory() get() = entryPoint.getAppsFlyerClientFactory()
private val walletAccountsFetcher: WalletAccountsFetcher
get() = entryPoint.getWalletAccountsFetcher()
// endregion // endregion
private val appScope = MainScope() private val appScope = MainScope()
@ -352,11 +342,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
) )
} }
derivationsFinder = DerivationsFinder(
walletAccountsFetcher = walletAccountsFetcher,
dispatchers = dispatchers,
)
appStateHolder.mainStore = store appStateHolder.mainStore = store
wcInitializeUseCase.init( wcInitializeUseCase.init(

View file

@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager 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.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
@ -40,6 +41,7 @@ internal class TangemSdkManagerModule {
appFinisher: AppFinisher, appFinisher: AppFinisher,
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
analyticsExceptionHandler: AnalyticsExceptionHandler, analyticsExceptionHandler: AnalyticsExceptionHandler,
blockchainToDeriveFinder: BlockchainToDeriveFinder,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): TangemSdkManager { ): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) { return if (BuildConfig.MOCK_DATA_SOURCE) {
@ -56,6 +58,7 @@ internal class TangemSdkManagerModule {
appFinisher = appFinisher, appFinisher = appFinisher,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
analyticsExceptionHandler = analyticsExceptionHandler, analyticsExceptionHandler = analyticsExceptionHandler,
blockchainToDeriveFinder = blockchainToDeriveFinder,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }

View file

@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.*
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.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager(
private val appFinisher: AppFinisher, private val appFinisher: AppFinisher,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
) : TangemSdkManager { ) : TangemSdkManager {
@ -162,7 +159,7 @@ internal class DefaultTangemSdkManager(
runTaskAsyncReturnOnMain( runTaskAsyncReturnOnMain(
runnable = ScanProductTask( runnable = ScanProductTask(
card = null, card = null,
derivationsFinder = derivationsFinder, blockchainToDeriveFinder = blockchainToDeriveFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler, visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this, visaCoroutineScope = this,

View file

@ -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<BlockchainToDerive> {
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<BlockchainToDerive> {
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<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): Set<BlockchainToDerive> {
return mapNotNullTo(hashSetOf()) { blockchain ->
val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
BlockchainToDerive(blockchain, derivationPath)
}
}
}

View file

@ -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<BlockchainToDerive> {
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<BlockchainToDerive> {
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<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
// TODO: Move to user wallet config
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
}
private fun MutableSet<BlockchainToDerive>.addEthereumBlockchains(derivationStyle: DerivationStyle?) {
val ethereumBlockchains = setOf(Blockchain.Ethereum)
.mapToBlockchainsWithDerivations(derivationStyle)
addAll(ethereumBlockchains)
}
private fun Set<BlockchainToDerive>.removeUnnecessaryBlockchains(
derivationStyle: DerivationStyle?,
): Set<BlockchainToDerive> {
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<BlockchainToDerive>.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<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): MutableSet<BlockchainToDerive> {
return mapTo(hashSetOf()) { blockchain ->
BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle))
}
}

View file

@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath 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.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.card.common.TwinsHelper 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.common.visa.VisaUtilities
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO 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_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX 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 com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.set
internal class ScanProductTask( internal class ScanProductTask(
private val card: Card?, private val card: Card?,
private val derivationsFinder: DerivationsFinder?, private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val visaCardScanHandler: VisaCardScanHandler?, private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?, private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
@ -79,7 +76,7 @@ internal class ScanProductTask(
readVisaCard( readVisaCard(
session = session, session = session,
cardDto = cardDto, cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor(derivationsFinder), scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder),
callback = callback, callback = callback,
) )
return return
@ -87,7 +84,7 @@ internal class ScanProductTask(
val commandProcessor = when { val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor() cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(derivationsFinder) else -> ScanWalletProcessor(blockchainToDeriveFinder)
} }
commandProcessor.proceed(cardDto, session) { processorResult -> commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) { when (processorResult) {
@ -160,7 +157,7 @@ internal class ScanProductTask(
} }
private class ScanWalletProcessor( private class ScanWalletProcessor(
private val derivationsFinder: DerivationsFinder?, private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
) : ProductCommandProcessor<ScanResponse> { ) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null var primaryCard: PrimaryCard? = null
@ -283,7 +280,6 @@ private class ScanWalletProcessor(
callback: (result: CompletionResult<ScanResponse>) -> Unit, callback: (result: CompletionResult<ScanResponse>) -> Unit,
) { ) {
val productType = getWalletProductType(card) val productType = getWalletProductType(card)
val config = CardConfig.createConfig(card)
scope.launch { scope.launch {
val scanResponse = ScanResponse( val scanResponse = ScanResponse(
card = card, card = card,
@ -291,8 +287,7 @@ private class ScanWalletProcessor(
walletData = session.environment.walletData, walletData = session.environment.walletData,
primaryCard = primaryCard, primaryCard = primaryCard,
) )
val derivations = val derivations = collectDerivations(card, scanResponse)
collectDerivations(card, config, scanResponse.derivationStyleProvider)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse)) callback(CompletionResult.Success(scanResponse))
return@launch return@launch
@ -322,32 +317,13 @@ private class ScanWalletProcessor(
private suspend fun collectDerivations( private suspend fun collectDerivations(
card: CardDTO, card: CardDTO,
config: CardConfig, scanResponse: ScanResponse,
derivationStyleProvider: DerivationStyleProvider,
): Map<ByteArrayKey, List<DerivationPath>> { ): Map<ByteArrayKey, List<DerivationPath>> {
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>() val blockchains = blockchainToDeriveFinder
val blockchains = derivationsFinder ?.find(card)
?.findBlockchainsToDerive(card, derivationStyleProvider) ?: return emptyMap()
?: return derivations
blockchains.forEach { blockchain -> return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains)
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
} }
} }

View file

@ -25,7 +25,7 @@ class FinalizeTwinTask(
is CompletionResult.Success -> is CompletionResult.Success ->
ScanProductTask( ScanProductTask(
card = readResult.data, card = readResult.data,
derivationsFinder = null, blockchainToDeriveFinder = null,
visaCardScanHandler = null, visaCardScanHandler = null,
visaCoroutineScope = null, visaCoroutineScope = null,
onboardingV2FeatureToggles = null, onboardingV2FeatureToggles = null,

View file

@ -1,19 +1,17 @@
package com.tangem.tap.domain.tasks.product package com.tangem.tap.domain.tasks.product
import com.google.common.truth.Truth import com.google.common.truth.Truth
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher 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.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.* import io.mockk.*
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -24,19 +22,16 @@ import org.junit.jupiter.api.TestInstance
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DerivationsFinderTest { class BlockchainToDeriveFinderTest {
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>() private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
private val finder = DerivationsFinder( private val finder = BlockchainToDeriveFinder(
walletAccountsFetcher = walletAccountsFetcher, walletAccountsFetcher = walletAccountsFetcher,
dispatchers = TestingCoroutineDispatcherProvider(),
) )
private val derivationStyleProvider = mockk<DerivationStyleProvider>()
@AfterEach @AfterEach
fun tearDown() { fun tearDown() {
clearMocks(walletAccountsFetcher, derivationStyleProvider) clearMocks(walletAccountsFetcher)
} }
@Test @Test
@ -47,7 +42,7 @@ class DerivationsFinderTest {
} }
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) val actual = finder.find(card)
// Assert // Assert
Truth.assertThat(actual).isEmpty() Truth.assertThat(actual).isEmpty()
@ -62,29 +57,26 @@ class DerivationsFinderTest {
} }
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) val actual = finder.find(card)
// Assert // Assert
Truth.assertThat(actual).isEmpty() Truth.assertThat(actual).isEmpty()
} }
@Test @Test
fun `GIVEN saved bitcoin THEN return bitcoin and ethereum`() = runTest { fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest {
// Arrange // Arrange
val card = createCardDTO() val card = createCardDTO()
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3
val response = createResponse(Blockchain.Bitcoin) val response = createResponse(Blockchain.Bitcoin)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// Assert // Assert
val expected = setOf( val expected = setOf(
createExpected(Blockchain.Bitcoin), createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
) )
Truth.assertThat(actual).containsExactlyElementsIn(expected) Truth.assertThat(actual).containsExactlyElementsIn(expected)
@ -98,12 +90,10 @@ class DerivationsFinderTest {
val demoCardId = "AC01000000045754" val demoCardId = "AC01000000045754"
val card = createCardDTO(cardId = demoCardId) val card = createCardDTO(cardId = demoCardId)
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// Assert // Assert
val expected = setOf( val expected = setOf(
@ -124,12 +114,10 @@ class DerivationsFinderTest {
val demoCardId = "DE00" val demoCardId = "DE00"
val card = createCardDTO(cardId = demoCardId) val card = createCardDTO(cardId = demoCardId)
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// Assert // Assert
val expected = setOf( val expected = setOf(
@ -148,12 +136,10 @@ class DerivationsFinderTest {
// Arrange // Arrange
val card = createCardDTO() val card = createCardDTO()
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// Assert // Assert
val expected = setOf( val expected = setOf(
@ -167,26 +153,19 @@ class DerivationsFinderTest {
} }
@Test @Test
fun `GIVEN saved cardano THEN return cardano and ethereum`() = runTest { fun `GIVEN saved cardano THEN return only cardano`() = runTest {
// Arrange // Arrange
val card = createCardDTO() val card = createCardDTO()
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3
val response = createResponse(Blockchain.Cardano) val response = createResponse(Blockchain.Cardano)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// Assert // Assert
val expected = setOf( val expected = setOf(
createExpected(Blockchain.Cardano), createExpected(Blockchain.Cardano),
createExpected(
blockchain = Blockchain.Cardano,
derivationPath = CardanoUtils.extendedDerivationPath(Blockchain.Cardano.getDerivationPath())
),
createExpected(Blockchain.Ethereum),
) )
Truth.assertThat(actual).containsExactlyElementsIn(expected) Truth.assertThat(actual).containsExactlyElementsIn(expected)
@ -195,53 +174,18 @@ class DerivationsFinderTest {
} }
@Test @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 // Arrange
val card = createCardDTO() val card = createCardDTO()
every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon)
val blockchains = Blockchain.entries val response = createResponse(*blockchains.toTypedArray())
.filter { it.isEvm() && !it.isTestnet() }
.toTypedArray()
val response = createResponse(*blockchains)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act // Act
val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) val actual = finder.find(card)
// 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)
// Assert // Assert
val expected = blockchains.mapTo(hashSetOf(), ::createExpected) val expected = blockchains.mapTo(hashSetOf(), ::createExpected)
@ -260,6 +204,13 @@ class DerivationsFinderTest {
every { this@mockk.cardId } returns cardId every { this@mockk.cardId } returns cardId
every { this@mockk.batchId } returns batchId every { this@mockk.batchId } returns batchId
every { this@mockk.settings.isHDWalletAllowed } returns true 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) every { this@mockk.wallets } returns listOf(wallet)
} }
} }
@ -290,7 +241,7 @@ class DerivationsFinderTest {
} }
private fun Blockchain.getDerivationPath(): DerivationPath { private fun Blockchain.getDerivationPath(): DerivationPath {
return derivationPath(derivationStyleProvider.getDerivationStyle())!! return derivationPath(DerivationStyle.V3)!!
} }
private companion object { private companion object {
@ -298,4 +249,4 @@ class DerivationsFinderTest {
// for byteArrayOf(0) // for byteArrayOf(0)
val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7")
} }
} }

View file

@ -73,7 +73,7 @@ object MockScanResponseFactory {
CardDTO.Wallet( CardDTO.Wallet(
CardWallet( CardWallet(
publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name 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, curve = curve,
settings = createSettings(), settings = createSettings(),
totalSignedHashes = null, totalSignedHashes = null,

View file

@ -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())
}

View file

@ -3,45 +3,80 @@ package com.tangem.data.wallets.derivations
import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.KeyWalletPublicKey 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.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<ByteArrayKey, List<DerivationPath>> private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>> internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
/**
* Data class representing a blockchain with its derivation path
*/
data class BlockchainToDerive(
val blockchain: Blockchain,
val derivationPath: DerivationPath,
)
/** /**
* Finder of missed derivations * Finder of missed derivations
* *
* @property userWallet User wallet to find derivations for * @property source Source of derivations data (UserWallet or ScanResponse)
* *
[REDACTED_AUTHOR] [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] */ /** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations { fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks) return currencies.map { it.network }.let(::findByNetworks)
} }
/** Find missed derivations for given [Network] list */
fun findByNetworks(networks: List<Network>): Derivations { fun findByNetworks(networks: List<Network>): 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<BlockchainToDerive>): Derivations {
val enrichedBlockchains = blockchainsToDerive.enrichBlockchains()
return findDerivationsInternal(enrichedBlockchains)
}
/**
* Common implementation for finding derivations
*/
private fun findDerivationsInternal(items: Collection<BlockchainToDerive>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> { return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
networks items
.mapToNewDerivations() .mapNotNull(::mapToNewDerivation)
.forEach { data -> .forEach { data ->
val current = this[data.first] val current = this[data.first]
if (current != null) { if (current != null) {
current.addAll(data.second) current.addAll(data.second)
current.distinct() this[data.first] = current.distinct().toMutableList()
} else { } else {
this[data.first] = data.second.toMutableList() this[data.first] = data.second.toMutableList()
} }
@ -49,31 +84,17 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) {
} }
} }
private fun List<Network>.mapToNewDerivations(): List<DerivationData> { /**
return mapNotNull { network -> * Maps a single BlockchainToDerive to derivation data (public key -> derivation paths)
val blockchain = network.toBlockchain() */
val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null 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) { val publicKey = source.getWalletPublicKey(curve) ?: return null
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
}
}
walletPublicKey?.let { val derivationCandidates = input.blockchain
findNewDerivations(curve = curve, publicKey = it, network = network) .getDerivationCandidates(input.derivationPath)
}
}
}
private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? {
val derivationCandidates = network
.getDerivationCandidates(curve)
.ifEmpty { return null } .ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey.toMapKey()) .filterAlreadyDerivedKeys(publicKey.toMapKey())
.ifEmpty { return null } .ifEmpty { return null }
@ -81,59 +102,63 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) {
return publicKey.toMapKey() to derivationCandidates return publicKey.toMapKey() to derivationCandidates
} }
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> { /**
val blockchain = this.toBlockchain() * Gets all possible derivation paths for a blockchain
*/
private fun Blockchain.getDerivationCandidates(derivationPath: DerivationPath): List<DerivationPath> {
return buildList { return buildList {
add(blockchain.getDerivationPath(curve = curve)) // Default derivation path for blockchain
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) add(getDerivationPath())
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
// The specified derivation path (can be either default or custom)
add(derivationPath)
// Extended Cardano derivation path if needed
add(getCardanoExtendedDerivationPath(derivationPath))
} }
.filterNotNull() .filterNotNull()
.distinct() .distinct()
} }
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { private fun Blockchain.getDerivationPath(): DerivationPath? {
return if (getSupportedCurves().contains(curve)) { return derivationPath(style = source.derivationStyleProvider.getDerivationStyle())
derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle())
} else {
null
}
} }
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { private fun Blockchain.getCardanoExtendedDerivationPath(customDerivationPath: DerivationPath): DerivationPath? {
return if (getSupportedCurves().contains(curve)) { if (this != Blockchain.Cardano) return null
network.derivationPath.value?.let(::DerivationPath) return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath)
} 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 List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> { private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey) val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList()
return filterNot(alreadyDerivedPaths::contains) return filterNot(alreadyDerivedPaths::contains)
} }
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> { // region Blockchain enrichment logic
val extendedPublicKeysMap = when (userWallet) {
is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) /**
is UserWallet.Hot -> { * Enriches blockchains collection:
val wallets = userWallet.wallets ?: return emptyList() * - Adds Ethereum if HD wallet is allowed
wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys * - Removes unnecessary blockchains that share derivation path with Ethereum (for cards without old style derivation)
?: ExtendedPublicKeysMap(emptyMap()) */
} private fun Collection<BlockchainToDerive>.enrichBlockchains(): Collection<BlockchainToDerive> {
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
} }

View file

@ -14,11 +14,13 @@ import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig
import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.card.configs.Wallet2CardConfig
import com.tangem.domain.wallets.derivations.derivationStyleProvider 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] [REDACTED_AUTHOR]
*/ */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class MissedDerivationsFinderTest { internal class MissedDerivationsFinderTest {
@Test @Test
@ -97,9 +99,8 @@ internal class MissedDerivationsFinderTest {
val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf)
val actual = finder.find(currencies) val actual = finder.find(currencies)
Truth.assertThat(actual).containsExactly( val expected = mapOf(
ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()), ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()) to listOf(
listOf(
DerivationConfigV2.derivations(Blockchain.Cardano).values.first(), DerivationConfigV2.derivations(Blockchain.Cardano).values.first(),
CardanoUtils.extendedDerivationPath( CardanoUtils.extendedDerivationPath(
derivationPath = DerivationPath( 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 @Test