Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-29 18:44:22 +05:00
parent 473a936863
commit df938519a2
13 changed files with 155 additions and 32 deletions

View file

@ -48,6 +48,14 @@ internal object ManageTokensDomainModule {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideRemoveCustomManagedCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): RemoveCustomManagedCryptoCurrencyUseCase {
return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(

View file

@ -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")

View file

@ -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<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
}
}

View file

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

View file

@ -77,6 +77,7 @@ internal class ManagedCryptoCurrencyFactory {
iconUrl = token.id?.let { getIconUrl(it, imageHost) },
contractAddress = contractAddress,
network = network,
decimals = token.decimals,
)
}
}

View file

@ -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"
}
}
}

View file

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

View file

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

View file

@ -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<Throwable, Unit> {
return Either.catch {
repository.removeCurrency(userWalletId, customCurrency)
}
}
}

View file

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

View file

@ -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,

View file

@ -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

View file

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