diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 1513f4d484..576da07163 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -48,6 +48,14 @@ internal object ManageTokensDomainModule { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } + @Provides + @Singleton + fun provideRemoveCustomManagedCryptoCurrencyUseCase( + customTokensRepository: CustomTokensRepository, + ): RemoveCustomManagedCryptoCurrencyUseCase { + return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository) + } + @Provides @Singleton fun provideSaveManagedTokensUseCase( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index c4b95bba88..dcd3107e06 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.tangem.common.extensions.calculateHashCode data class UserTokensResponse( @Json(name = "version") val version: Int = 0, @@ -17,7 +18,23 @@ data class UserTokensResponse( @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, - ) + ) { + override fun equals(other: Any?): Boolean { + val otherToken = other as? Token ?: return false + + return otherToken.contractAddress == this.contractAddress && + otherToken.networkId == this.networkId && + otherToken.derivationPath == this.derivationPath && + otherToken.decimals == this.decimals + } + + override fun hashCode(): Int = calculateHashCode( + contractAddress.hashCode(), + networkId.hashCode(), + derivationPath.hashCode(), + decimals.hashCode(), + ) + } enum class GroupType { @Json(name = "none") diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index eabde921a1..0ec95b565f 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -1,37 +1,46 @@ package com.tangem.data.managetokens -import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter -import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.getBlockchain +import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getNetwork +import com.tangem.data.managetokens.utils.TokenAddressesConverter +import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val appPreferencesStore: AppPreferencesStore, + private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, ) : CustomTokensRepository { private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val userTokensResponseFactory = UserTokensResponseFactory() + private val tokenAddressConverter = TokenAddressesConverter() + private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = withContext(dispatchers.io) { @@ -73,7 +82,7 @@ internal class DefaultCustomTokensRepository( val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, contractAddress, symbol = null, @@ -112,10 +121,7 @@ internal class DefaultCustomTokensRepository( } } - override suspend fun createCoin( - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { + override fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin { val network = getNetwork(networkId, derivationPath) return cryptoCurrencyFactory.createCoin(network) @@ -127,7 +133,7 @@ internal class DefaultCustomTokensRepository( formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { val network = getNetwork(networkId, derivationPath) - val tokenAddress = convertTokenAddress( + val tokenAddress = tokenAddressConverter.convertTokenAddress( networkId, formValues.contractAddress, formValues.symbol, @@ -143,20 +149,54 @@ internal class DefaultCustomTokensRepository( ) } - private fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { - val convertedAddress = when (getBlockchain(networkId)) { - Blockchain.Hedera, - Blockchain.HederaTestnet, - -> HederaTokenAddressConverter().convertToTokenId(contractAddress) - Blockchain.Cardano -> { - // TODO: [REDACTED_JIRA] - CardanoTokenAddressConverter().convertToFingerprint(contractAddress, symbol) + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = + withContext(dispatchers.io) { + val cryptoCurrency = when (currency) { + is ManagedCryptoCurrency.Custom.Coin -> createCoin(currency.network.id, currency.network.derivationPath) + is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken( + network = currency.network, + rawId = currency.currencyId.rawCurrencyId, + name = currency.name, + symbol = currency.symbol, + decimals = currency.decimals, + contractAddress = currency.contractAddress, + ) + } + + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, + ) + val token = userTokensResponseFactory.createResponseToken(cryptoCurrency) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), + ) + when (cryptoCurrency) { + is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network)) + is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency)) } - else -> contractAddress } - return requireNotNull(convertedAddress) { - "Token contract address is invalid" + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response) + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + value = compatibleUserTokensResponse, + ) + + pushTokens(userWalletId, response) + } + + private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) { + Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}") } } + + private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(key.stringValue), + ) + } } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 37d4a334fb..64317a96e8 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -52,12 +52,14 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, appPreferencesStore: AppPreferencesStore, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi, userWalletsStore, appPreferencesStore, + walletManagersFacade, dispatchers, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index c848b36699..5089f51294 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -77,6 +77,7 @@ internal class ManagedCryptoCurrencyFactory { iconUrl = token.id?.let { getIconUrl(it, imageHost) }, contractAddress = contractAddress, network = network, + decimals = token.decimals, ) } } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt new file mode 100644 index 0000000000..9503e4a6c7 --- /dev/null +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/TokenAddressesConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.managetokens.utils + +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.common.currency.getBlockchain +import com.tangem.domain.tokens.model.Network + +internal class TokenAddressesConverter { + private val hederaTokenAddressConverter = HederaTokenAddressConverter() + private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() + + fun convertTokenAddress(networkId: Network.ID, contractAddress: String, symbol: String?): String { + val convertedAddress = when (getBlockchain(networkId)) { + Blockchain.Hedera, + Blockchain.HederaTestnet, + -> hederaTokenAddressConverter.convertToTokenId(contractAddress) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + cardanoTokenAddressConverter.convertToFingerprint(contractAddress, symbol) + } + else -> contractAddress + } + + return requireNotNull(convertedAddress) { + "Token contract address is invalid" + } + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 93ad36b66f..8fda73fff1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -153,15 +153,7 @@ internal class DefaultCurrenciesRepository( val token = userTokensResponseFactory.createResponseToken(currency) storeAndPushTokens( userWalletId = userWalletId, - response = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filterNot { - // it's better to compare by fields, to support renaming and etc - it.contractAddress == token.contractAddress && - it.networkId == token.networkId && - it.derivationPath == token.derivationPath && - it.decimals == token.decimals - }, - ), + response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }), ) } diff --git a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt index 595a8e1077..848616a5b2 100644 --- a/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt +++ b/domain/manage-tokens/models/src/main/kotlin/com/tangem/domain/managetokens/model/ManagedCryptoCurrency.kt @@ -25,6 +25,7 @@ sealed class ManagedCryptoCurrency { override val iconUrl: String?, override val network: Network, val contractAddress: String, + val decimals: Int, ) : Custom() data class Coin( diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt new file mode 100644 index 0000000000..61e33c875c --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.managetokens + +import arrow.core.Either +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.wallets.models.UserWalletId + +class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + customCurrency: ManagedCryptoCurrency.Custom, + ): Either { + return Either.catch { + repository.removeCurrency(userWalletId, customCurrency) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index b578627da0..5f84209200 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.managetokens.repository import com.tangem.domain.managetokens.model.AddCustomTokenForm +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -23,11 +24,13 @@ interface CustomTokensRepository { derivationPath: Network.DerivationPath, ): CryptoCurrency.Token? - suspend fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin + fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin suspend fun createCustomToken( networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token + + suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 92dc53736c..12c248b792 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.GetManagedTokensUseCase +import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase import com.tangem.domain.managetokens.model.ManageTokensListBatchingContext import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManageTokensUpdateAction @@ -29,6 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,6 +38,7 @@ import javax.inject.Inject internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, + private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensUiActions { @@ -160,6 +163,12 @@ internal class ManageTokensListManager @Inject constructor( sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) } + override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { + scope.launch { + removeCustomCurrencyUseCase.invoke(userWalletId, currency) + } + } + override fun checkNeedToShowRemoveNetworkWarning( currency: ManagedCryptoCurrency.Token, network: Network, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt index 7b9ab64f3f..7b7548289a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiActions.kt @@ -10,6 +10,8 @@ internal interface ManageTokensUiActions { fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) + fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) + fun checkNeedToShowRemoveNetworkWarning(currency: ManagedCryptoCurrency.Token, network: Network): Boolean suspend fun checkHasLinkedTokens(userWalletId: UserWalletId, network: Network): Boolean diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 3ca2075789..78e3d1011b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -104,7 +104,8 @@ internal class ManageTokensUiManager( network = currency.network, isCoin = currency is ManagedCryptoCurrency.Custom.Coin, onConfirm = { - // TODO: [REDACTED_JIRA] + val userWalletId = requireNotNull(state.value.userWalletId) { "UserWalletId is null. Can not remove" } + actions.removeCustomCurrency(userWalletId = userWalletId, currency = currency) }, ) }