diff --git a/ci_resources/Dockerfile.android b/ci_resources/Dockerfile.android index bdd3dd2f52..25edb6a0ec 100644 --- a/ci_resources/Dockerfile.android +++ b/ci_resources/Dockerfile.android @@ -8,6 +8,7 @@ ENV LC_ALL=en_US.UTF-8 ENV LANG=en_US.UTF-8 RUN apt-get update && apt-get install -y \ + locales \ openjdk-17-jdk \ wget \ unzip \ @@ -16,13 +17,15 @@ RUN apt-get update && apt-get install -y \ ruby \ ruby-dev \ build-essential \ + && locale-gen en_US.UTF-8 \ + && update-locale LANG=en_US.UTF-8 \ && apt-get clean RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \ - chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \ - echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \ - apt-get update && apt-get install -y gh && \ - apt-get clean + chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && apt-get install -y gh && \ + apt-get clean RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \ wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \ diff --git a/ci_resources/Dockerfile.lightweight b/ci_resources/Dockerfile.lightweight index 62c29262d9..c2b4b399b2 100644 --- a/ci_resources/Dockerfile.lightweight +++ b/ci_resources/Dockerfile.lightweight @@ -1,19 +1,26 @@ FROM ubuntu:24.04 +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV DEBIAN_FRONTEND=noninteractive + RUN apt-get update && apt-get install -y \ + locales \ curl \ git \ + && locale-gen en_US.UTF-8 \ + && update-locale LANG=en_US.UTF-8 \ && apt-get clean RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \ - chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \ - echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \ - apt-get update && apt-get install -y gh && \ - apt-get clean + chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && apt-get install -y gh && \ + apt-get clean RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \ - apt-get install -y git-lfs && \ - git lfs install && \ - apt-get clean + apt-get install -y git-lfs && \ + git lfs install && \ + apt-get clean CMD ["bash"] diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 2b30de2785..56fc636d4f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either -import arrow.core.toNonEmptyListOrNull import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency @@ -30,20 +29,6 @@ class AddCryptoCurrenciesUseCase( private val tokensFeatureToggles: TokensFeatureToggles, ) { - /** - * Adds a [currency] to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param currency Cryptocurrency to add. - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { - return invoke(userWalletId, listOf(currency)) - } - /** * Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId]. * @@ -61,34 +46,32 @@ class AddCryptoCurrenciesUseCase( network: Network, ): Either = either { val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) - invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) + invoke(userWalletId = userWalletId, currency = tokenToAdd) } /** - * Adds a list of [currencies] to the wallet identified by [userWalletId]. + * Adds a [currency] to the wallet identified by [userWalletId]. * - * After successfully adding currencies, it also refreshes the networks for tokens + * After successfully adding a currency, it also refreshes the networks for tokens * that are being added and have corresponding coins in the existing currencies list. * * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies to add. + * @param currency Cryptocurrency to add. * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ - suspend operator fun invoke( - userWalletId: UserWalletId, - currencies: List, - ): Either = either { - val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { - raise(it) - } - val currenciesToAdd = currencies - .filterNot(existingCurrencies::contains) - .toNonEmptyListOrNull() - ?: return@either + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = + either { + val existingCurrencies = catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }, + catch = ::raise, + ) + val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - addCurrencies(userWalletId, currenciesToAdd) - refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) - } + addCurrencies(userWalletId, currencyToAdd) + refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) + refreshUpdatedYieldBalances(userWalletId, currencyToAdd) + refreshUpdatedQuotes(currencyToAdd) + } suspend operator fun invoke( userWalletId: UserWalletId, @@ -110,10 +93,10 @@ class AddCryptoCurrenciesUseCase( return@either foundToken } val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - addCurrencies(userWalletId, listOf(tokenToAdd)) - refreshUpdatedNetworks(userWalletId, listOf(tokenToAdd), existingCurrencies) - refreshUpdatedYieldBalances(userWalletId, existingCurrencies) - refreshUpdatedQuotes(existingCurrencies) + addCurrencies(userWalletId, tokenToAdd) + refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) + refreshUpdatedYieldBalances(userWalletId, tokenToAdd) + refreshUpdatedQuotes(tokenToAdd) tokenToAdd } @@ -123,23 +106,24 @@ class AddCryptoCurrenciesUseCase( */ private suspend fun Raise.refreshUpdatedNetworks( userWalletId: UserWalletId, - currenciesToAdd: List, + currencyToAdd: CryptoCurrency, existingCurrencies: List, ) { - val networksToUpdate = currenciesToAdd - .asSequence() - .filterIsInstance() - .filter { hasCoinForToken(existingCurrencies, it) } - .mapTo(hashSetOf(), CryptoCurrency.Token::network) + val networksToUpdate = currencyToAdd.takeIf { currency -> + currency is CryptoCurrency.Token && hasCoinForToken(existingCurrencies, currency) + } + ?.network - val networkToUpdate = currenciesToAdd.map { it.network } - .subtract(existingCurrencies.map { it.network }.toSet()) + val networkToUpdate = currencyToAdd.takeIf { + !existingCurrencies.map(CryptoCurrency::network).contains(it.network) + } + ?.network if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, + networks = setOfNotNull(networksToUpdate, networkToUpdate), ), ) } else { @@ -147,7 +131,7 @@ class AddCryptoCurrenciesUseCase( { networksRepository.getNetworkStatusesSync( userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, + networks = setOfNotNull(networksToUpdate, networkToUpdate), refresh = true, ) }, @@ -157,20 +141,17 @@ class AddCryptoCurrenciesUseCase( } } - private suspend fun refreshUpdatedYieldBalances( - userWalletId: UserWalletId, - existingCurrencies: List, - ) { - stakingRepository.fetchMultiYieldBalance( + private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { + stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, + cryptoCurrency = addedCurrency, refresh = true, ) } - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { + private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { quotesRepository.fetchQuotes( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), refresh = true, ) } @@ -194,9 +175,9 @@ class AddCryptoCurrenciesUseCase( ) } - private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, tokens: List) { + private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, currency: CryptoCurrency) { catch( - { currenciesRepository.addCurrencies(userWalletId, tokens) }, + { currenciesRepository.addCurrencies(userWalletId, listOf(currency)) }, ) { raise(it) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index edceddb46d..822eb5efd4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.DelicateDecomposeApi import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.subscribeAsState @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.CustomTokenFormComponent @@ -171,6 +173,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( showForm(derivationPath = derivationPath) } + @OptIn(DelicateDecomposeApi::class) private fun showDerivationPathSelector(formValues: CustomTokenFormValues) { val currentConfig = contentStack.value.active.configuration @@ -181,6 +184,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( navigation.push(config) } + @OptIn(DelicateDecomposeApi::class) private fun showNetworkSelector(formValues: CustomTokenFormValues) { val currentConfig = contentStack.value.active.configuration @@ -194,14 +198,35 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( private fun showForm(network: SelectedNetwork? = null, derivationPath: SelectedDerivationPath? = null) { val currentConfig = contentStack.value.active.configuration + val selectedNetwork = network ?: currentConfig.selectedNetwork + val selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath + val config = currentConfig.copy( step = AddCustomTokenConfig.Step.FORM, - selectedNetwork = network ?: currentConfig.selectedNetwork, - selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath, + selectedNetwork = selectedNetwork, + selectedDerivationPath = defineSelectedDerivationPath(selectedNetwork, selectedDerivationPath), ) + navigation.replaceAll(config) } + private fun defineSelectedDerivationPath( + selectedNetwork: SelectedNetwork?, + selectedDerivationPath: SelectedDerivationPath?, + ): SelectedDerivationPath? { + // if default derivation path + if (selectedDerivationPath?.value == selectedNetwork?.derivationPath) return null + + // map to custom derivation path + return if (selectedDerivationPath != null && selectedDerivationPath.value.value != null) { + selectedDerivationPath.copy( + value = Network.DerivationPath.Custom(value = requireNotNull(selectedDerivationPath.value.value)), + ) + } else { + null + } + } + private fun dismissAndNotify() { dismiss() params.onCurrencyAdded() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index ed8f3c543a..97488cd589 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -71,7 +71,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor( addCryptoCurrenciesUseCase( userWalletId = params.userWalletId, - currencies = listOfNotNull(params.cryptoCurrency), + currency = params.cryptoCurrency, ) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } .onLeft { changeAddButtonProgressStatus(isProgress = false) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 4315d06992..c580236ad8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -40,6 +40,8 @@ internal class ReferralInteractorImpl( } val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData) + ?: error("Failed to create crypto currency") + derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { Timber.e("Failed to derive public keys: $it") throw it.mapToDomainError() @@ -47,12 +49,12 @@ internal class ReferralInteractorImpl( addCryptoCurrenciesUseCase( userWalletId = userWallet.walletId, - currencies = listOfNotNull(cryptoCurrency), + currency = cryptoCurrency, ) val publicAddress = userWalletManager.getWalletAddress( networkId = tokenData.networkId, - derivationPath = cryptoCurrency?.network?.derivationPath?.value, + derivationPath = cryptoCurrency.network.derivationPath.value, ) return repository.startReferral(