Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-07 14:44:58 +03:00
parent 88dbdbc7f6
commit a10c1a8505
24 changed files with 132 additions and 1215 deletions

View file

@ -29,7 +29,6 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -38,10 +37,8 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
@ -119,9 +116,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var scanCardProcessor: ScanCardProcessor
@Inject
lateinit var blockchainExceptionHandler: BlockchainExceptionHandler
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@ -155,12 +149,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var oneTimeEventFilter: OneTimeEventFilter
@Inject
lateinit var derivationsRepository: DerivationsRepository
@Inject
lateinit var testerFeatureToggles: TesterFeatureToggles
@Inject
lateinit var blockchainDataStorage: BlockchainDataStorage
// endregion Injected
@ -238,8 +226,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
derivationsRepository = derivationsRepository,
testerFeatureToggles = testerFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
),
),

View file

@ -33,7 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -91,7 +90,6 @@ data class AppState(
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware.tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,

View file

@ -8,8 +8,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -62,14 +60,8 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideDerivePublicKeysUseCase(
tangemSdkManager: TangemSdkManager,
derivationsRepository: DerivationsRepository,
): DerivePublicKeysUseCase {
return DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = derivationsRepository,
)
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
}
@Provides

View file

@ -1,41 +0,0 @@
package com.tangem.tap.domain.card
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.TangemSdkManager
internal class DefaultDerivePublicKeysUseCase(
private val tangemSdkManager: TangemSdkManager,
private val derivationsRepository: DerivationsRepository,
) : DerivePublicKeysUseCase {
override suspend fun invoke(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Either<Unit, DerivationTaskResponse> {
tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations)
.doOnSuccess { return it.right() }
.doOnFailure { return Unit.left() }
return Unit.left()
}
override suspend fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
@ -30,7 +29,6 @@ internal object CustomTokenInteractorModule {
reduxStateHolder: AppStateHolder,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
testerFeatureToggles: TesterFeatureToggles,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -40,7 +38,6 @@ internal object CustomTokenInteractorModule {
),
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
testerFeatureToggles = testerFeatureToggles,
)
}
}

View file

@ -1,40 +1,19 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import timber.log.Timber
/**
@ -48,7 +27,6 @@ class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val testerFeatureToggles: TesterFeatureToggles,
) : CustomTokenInteractor {
// TODO: Move to DI
@ -70,136 +48,12 @@ class DefaultCustomTokenInteractor(
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
val currency = Currency.fromCustomCurrency(customCurrency)
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
}
}
private fun isNeedToDerive(userWallet: UserWallet, currency: Currency): Boolean {
val scanResponse = userWallet.scanResponse
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
}
private suspend fun deriveMissingBlockchains(
userWallet: UserWallet,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
onFailure: suspend (TangemError) -> Unit,
) {
val scanResponse = userWallet.scanResponse
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
onFailure.invoke(result.error)
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) {
val scanResponse = userWallet.scanResponse
val userWalletId = userWallet.walletId
val currencyList = listOfNotNull(element = currency.toCryptoCurrency(scanResponse))
userWalletsListManager.update(userWalletId) {
it.copy(scanResponse = scanResponse)
}
addCryptoCurrenciesUseCase(userWalletId, currencyList)
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {

View file

@ -3,14 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
import kotlin.properties.Delegates
@ -24,6 +30,8 @@ import kotlin.properties.Delegates
internal class TokensListMigration(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
) {
private var currentNewCoins: List<CryptoCurrency.Coin> by Delegates.notNull()
@ -76,31 +84,72 @@ internal class TokensListMigration(
}
}
fun onSaveButtonClick(
suspend fun onSaveButtonClick(
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
store.dispatch(
action = TokensAction.SaveChanges(
currentTokens = currentNewTokens,
currentCoins = currentNewCoins,
changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
userWallet = currentUserWallet,
),
val changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val blockchainsToAdd = changedCoins.filterNot(currentNewCoins::contains)
val blockchainsToRemove = currentNewCoins.filterNot(changedCoins::contains)
val tokensToAdd = changedTokens.filterNot(currentNewTokens::contains)
val tokensToRemove = currentNewTokens.filterNot { token -> changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = currentUserWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return
}
val currencyList = blockchainsToAdd + tokensToAdd
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -13,12 +13,14 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -67,6 +69,8 @@ internal class TokensListViewModel @Inject constructor(
private val router: TokensListRouter,
private val dispatchers: AppCoroutineDispatcherProvider,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
analyticsEventHandler: AnalyticsEventHandler,
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) : ViewModel(), DefaultLifecycleObserver {
@ -88,6 +92,8 @@ internal class TokensListViewModel @Inject constructor(
private val tokensListMigration = TokensListMigration(
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getCurrenciesUseCase = getCurrenciesUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
)
init {
@ -303,10 +309,13 @@ internal class TokensListViewModel @Inject constructor(
fun onSaveButtonClick() {
analyticsSender.sendWhenSaveButtonClicked()
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
viewModelScope.launch(dispatchers.main) {
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
}
}
private fun onSearchValueChange(newValue: String) {

View file

@ -1,250 +0,0 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.core.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
object TokensMiddleware {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
}
}
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
scope.launch {
val scanResponse = action.userWallet.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentCoins
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = blockchainsToAdd + tokensToAdd
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::testerFeatureToggles)
if (featureToggles.isDerivePublicKeysRefactoringEnabled) {
val derivePublicKeys = DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = store.state.daggerGraphState.get(DaggerGraphState::derivationsRepository),
)
derivePublicKeys(userWalletId = action.userWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(
userWalletId = action.userWallet.walletId,
currencies = currencyList,
)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitAdd(
userWallet = action.userWallet,
updatedScanResponse = it,
currencyList = currencyList,
)
}
} else {
submitAdd(action.userWallet, scanResponse, currencyList)
}
}
}
}
@Deprecated(message = "Use DerivePublicKeysUseCase instead")
private fun deriveMissingCoins(
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
onSuccess: (ScanResponse) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val blockchain = Blockchain.fromId(currency.network.id.value)
val supportedCurves = blockchain.getSupportedCurves()
val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.network.derivationPath.value?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(
userWallet: UserWallet,
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
scope.launch {
userWalletsListManager.update(
userWalletId = userWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
).doOnSuccess {
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList)
}
}
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -1,298 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.tangem.tap.domain.model.Currency as WalletModelCurrency
class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
) : DerivationManager {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation ->
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "selectedUserWallet shouldn't be null" }
val scanResponse = selectedUserWallet.scanResponse
val blockchain = requireNotNull(
Blockchain.fromNetworkId(currency.networkId),
) { "unsupported blockchain" }
val derivationStyleProvider = scanResponse.derivationStyleProvider
val derivationPath = requireNotNull(
blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath,
) { "derivationPath shouldn't be null" }
val hasDerivation = scanResponse.hasDerivation(
blockchain,
derivationPath,
)
if (hasDerivation) {
scope.launch {
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
} else {
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
val appCurrency = com.tangem.tap.domain.model.Currency.fromBlockchainNetwork(
blockchainNetwork,
getAppToken(currency),
)
deriveMissingBlockchains(
scanResponse = scanResponse,
currencyList = listOf(appCurrency),
onSuccess = { updatedScanResponse ->
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
},
onFailure = {
continuation.resumeWith(Result.failure(it))
},
)
}
}
private suspend fun addToken(
userWalletId: UserWalletId,
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
) {
val cryptoCurrency = convertCurrency(
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
addCryptoCurrenciesUseCase(userWalletId, cryptoCurrency)
}
private fun convertCurrency(
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
return when (currency) {
is Currency.NativeToken -> {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
is NonNativeToken -> {
val sdkToken = Token(
name = currency.name,
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
id = currency.id,
)
cryptoCurrencyFactory.createToken(
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
} as CryptoCurrency
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<WalletModelCurrency>,
onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
cardId = null, // always ignore cardId in derive task
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
if (selectedUserWallet != null) {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
}
appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
appStateHolder.mainStore?.dispatchDebugErrorNotification(
TapError.CustomError(
"Error derivation",
),
)
onFailure.invoke(handleTangemError(result.error))
}
else -> {
error("result result is null")
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: com.tangem.tap.domain.model.Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is WalletModelCurrency.Blockchain &&
currency.blockchain == Blockchain.Cardano
) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private fun getAppToken(currency: Currency): Token? {
return if (currency is NonNativeToken) {
Token(
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
)
} else {
null
}
}
/**
* Simple error handler
* for now specifically handle only UserCancelled
*
* @param error [TangemError]
*/
private fun handleTangemError(error: TangemError): Exception {
if (error is TangemSdkError.UserCancelled) {
return UserCancelledException()
}
return IllegalStateException(error.customMessage)
}
}

View file

@ -4,9 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.*
@ -56,18 +54,4 @@ internal object ProxyModule {
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideDerivationManager(
appStateHolder: AppStateHolder,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): DerivationManager {
return DerivationManagerImpl(
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
}
}

View file

@ -8,7 +8,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -18,7 +17,6 @@ import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggle
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -53,11 +51,7 @@ data class DaggerGraphState(
val sendFeatureToggles: SendFeatureToggles? = null,
val sendRouter: SendRouter? = null,
val qrScanningRouter: QrScanningRouter? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,
val derivationsRepository: DerivationsRepository? = null,
val testerFeatureToggles: TesterFeatureToggles? = null,
val blockchainDataStorage: BlockchainDataStorage? = null,
) : StateType {