From 1582c3a8ef8926bae55d614dee3375a4fc1ef316 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 11:53:56 +0500 Subject: [PATCH 1/5] Updated on 2026-08-14 --- .../state/confirm/SendNotificationFactory.kt | 13 ++--- .../presentation/viewmodel/SendViewModel.kt | 56 ++++--------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 16c1315e2d..7d19a4e8f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -41,7 +41,7 @@ import java.math.BigDecimal @Suppress("LongParameterList", "LargeClass") internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, - private val coinCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val currencyChecksRepository: CurrencyChecksRepository, @@ -307,6 +307,7 @@ internal class SendNotificationFactory( private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false val change = when (cryptoCurrencyStatus.currency) { is CryptoCurrency.Coin -> { @@ -314,7 +315,7 @@ internal class SendNotificationFactory( balance - (feeAmount + receivedAmount) } is CryptoCurrency.Token -> { - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val balance = feeCurrencyStatus.value.amount ?: BigDecimal.ZERO balance - feeAmount } } @@ -351,16 +352,14 @@ internal class SendNotificationFactory( val feeValue = fee?.amount?.value ?: BigDecimal.ZERO val userWalletId = userWalletProvider().walletId val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return val warning = getBalanceNotEnoughForFeeWarningUseCase( fee = feeValue, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, - coinStatus = coinCryptoCurrencyStatusProvider(), - ).fold( - ifLeft = { null }, - ifRight = { it }, - ) ?: return + coinStatus = feeCurrencyStatus, + ).getOrNull() ?: return val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() when (warning) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index f580eaa140..3d37fc0db7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -30,16 +30,14 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase -import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -71,7 +69,6 @@ internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, @@ -172,7 +169,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, @@ -203,7 +200,6 @@ internal class SendViewModel @Inject constructor( private var isAmountSubtractAvailable: Boolean = false private var isUtxoConsolidationAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false - private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null @@ -281,33 +277,18 @@ internal class SendViewModel @Inject constructor( } private suspend fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - val maybeCurrencyStatus = getCurrencyStatus( + getCurrencyStatus( isSingleWalletWithToken = isSingleWalletWithToken, isMultiCurrency = isMultiCurrency, + ).fold( + ifRight = { cryptoCurrencyStatus -> + onDataLoaded( + currencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(cryptoCurrencyStatus, isMultiCurrency), + ) + }, + ifLeft = { showErrorAlert() }, ) - val maybeCoinStatus = if (cryptoCurrency is CryptoCurrency.Coin) { - maybeCurrencyStatus - } else { - getCoinCurrencyStatusUpdates(isSingleWalletWithToken) - } - - if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { - val currencyStatus = maybeCurrencyStatus.getOrElse { - showErrorAlert() - return Timber.e("Currency status is unreachable") - } - val coinStatus = maybeCoinStatus.getOrElse { - showErrorAlert() - return Timber.e("Coin status is unreachable") - } - onDataLoaded( - currencyStatus = currencyStatus, - coinCurrencyStatus = coinStatus, - feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), - ) - } else { - showErrorAlert() - } } private fun getTapHelpPreviewAvailability() { @@ -316,14 +297,6 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase - .invokeSync( - userWalletId = userWalletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWalletWithToken, - ) - private suspend fun getCurrencyStatus( isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean, @@ -365,13 +338,8 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded( - currencyStatus: CryptoCurrencyStatus, - coinCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus?, - ) { + private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus?) { cryptoCurrencyStatus = currencyStatus - coinCryptoCurrencyStatus = coinCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus subscribeOnQRScannerResult() when { From 9c5490bcedf3aa56634a957b32440dffdd1781e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 20:25:24 +0500 Subject: [PATCH 2/5] Updated on 2026-08-14 --- app/build.gradle.kts | 5 +++++ app/src/main/AndroidManifest.xml | 1 - .../src/main/java/com/tangem/core/ui/res/TangemColors.kt | 1 - gradle/dependencies.toml | 7 +++---- .../extension/LibraryExtensionConfigurations.kt | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8615f2502..417f9ea050 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,6 +14,11 @@ android { testOptions { animationsDisabled = true } + packaging { + jniLibs { + useLegacyPackaging = true + } + } } configurations.all { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d52735ce7..1cd7e794eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,6 @@ android:largeHeap="@bool/largeHeap" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher" - android:extractNativeLibs="true" android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:allowBackup, android:fullBackupContent, android:label"> diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 451207be18..5ff9fc7e8d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -83,7 +83,6 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, - constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5063455aba..8a1c580d07 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -4,8 +4,8 @@ [versions] # region Classpath androidGradlePlugin = "8.2.2" -firebaseCrashlytics = "2.9.4" -googleServices = "4.3.10" +firebaseCrashlytics = "3.0.1" +googleServices = "4.4.1" kotlin = "1.9.22" # endregion Classpath @@ -44,7 +44,7 @@ coil = "2.1.0" compose-shimmer = "1.0.3" coroutine = "1.7.2" desugarJdkLibs = "1.1.5" -firebase = "26.0.0" +firebase = "33.1.0" googleMaterialComponent = "1.6.1" googlePlayCore = "1.10.3" googlePlayCoreKtx = "1.8.1" @@ -130,7 +130,6 @@ room = { id = "androidx.room", version.ref = "room" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } -gradle-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlytics" } # end region Classpath # region AndroidX diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt index 053151f9f1..f82cfcf71e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -39,7 +39,7 @@ private fun LibraryExtension.configureBuildTypes() { } private fun LibraryExtension.configurePackagingOptions() { - packagingOptions { + packaging { resources { excludes += "lib/x86_64/darwin/libscrypt.dylib" excludes += "lib/x86_64/freebsd/libscrypt.so" From c211d221ea49cc438bc8ae64f3ebcd809d92b83e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 12:09:55 +0400 Subject: [PATCH 3/5] Updated on 2026-08-14 --- .../tangem/data/visa/DefaultVisaRepository.kt | 36 +++----- .../com/tangem/data/visa/config/VisaConfig.kt | 25 ++++++ .../tangem/data/visa/config/VisaLibLoader.kt | 86 +++++++++++++++++++ .../data/visa/di/ImplementedVisaDataModule.kt | 35 ++------ .../tangem/data/visa/utils/CurrencyUtils.kt | 4 +- .../com/tangem/data/visa/utils/VisaConfig.kt | 16 ---- .../tangem/data/visa/utils/VisaConstants.kt | 44 ++++++++++ .../data/visa/utils/VisaCurrencyFactory.kt | 4 +- .../SetBalancesAndLimitsTransformer.kt | 31 +++---- .../TypedWalletStateTransformer.kt | 22 +++++ .../transformers/WalletStateTransformer.kt | 12 +-- .../lib/visa/VisaContractInfoProvider.kt | 22 ++--- .../com/tangem/lib/visa/api/VisaApiBuilder.kt | 19 +++- .../utils/{VisaConfig.kt => Constants.kt} | 8 +- 14 files changed, 251 insertions(+), 113 deletions(-) create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt delete mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt rename libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/{VisaConfig.kt => Constants.kt} (55%) diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index a817781b53..e558b2def0 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -10,10 +10,8 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.utils.VisaConfig -import com.tangem.data.visa.utils.VisaCurrencyFactory -import com.tangem.data.visa.utils.VisaTxDetailsFactory -import com.tangem.data.visa.utils.VisaTxHistoryPagingSource +import com.tangem.data.visa.config.VisaLibLoader +import com.tangem.data.visa.utils.* import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -24,8 +22,6 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.visa.repository.VisaRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApi import com.tangem.lib.visa.model.VisaTxHistoryResponse import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -35,9 +31,8 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal internal class DefaultVisaRepository( - private val visaContractInfoProvider: VisaContractInfoProvider, + private val visaLibLoader: VisaLibLoader, private val tangemTechApi: TangemTechApi, - private val visaApi: VisaApi, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, @@ -76,9 +71,11 @@ internal class DefaultVisaRepository( } private suspend fun fetchVisaCurrency(address: String) { + val contractInfoProvider = visaLibLoader.getOrCreateProvider() + parZip( dispatchers.io, - { visaContractInfoProvider.getContractInfo(address) }, + { contractInfoProvider.getContractInfo(address) }, { getFiatRate() }, { contractInfo, fiatRate -> fetchedCurrencies.update { value -> @@ -97,6 +94,7 @@ internal class DefaultVisaRepository( ): Flow> { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet) + val api = visaLibLoader.getOrCreateApi() val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -110,7 +108,7 @@ internal class DefaultVisaRepository( isRefresh = isRefresh, ), cacheRegistry = cacheRegistry, - visaApi = visaApi, + visaApi = api, fetchedItems = fetchedHistoryItems, dispatchers = dispatchers, ) @@ -137,7 +135,7 @@ internal class DefaultVisaRepository( } private suspend fun makeAddress(userWalletId: UserWalletId): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() val userWallet = findVisaUserWallet(userWalletId) val walletAddresses = makeWalletAddresses(userWallet) @@ -149,13 +147,13 @@ internal class DefaultVisaRepository( } private suspend fun getFiatRate(): BigDecimal? { - val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase() + val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() val quotes = tangemTechApi.getQuotes( currencyId = fiatCurrencyId, - coinIds = VisaConfig.TOKEN_ID, + coinIds = VisaConstants.TOKEN_ID, ).getOrThrow() - return quotes.quotes[VisaConfig.TOKEN_ID]?.price + return quotes.quotes[VisaConstants.TOKEN_ID]?.price } private fun makeWalletAddresses(userWallet: UserWallet): Set
{ @@ -165,7 +163,7 @@ internal class DefaultVisaRepository( } private fun getCardPubKey(userWallet: UserWallet): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey() val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 @@ -189,12 +187,4 @@ internal class DefaultVisaRepository( private fun getVisaCurrencyKey(address: String): String { return "visa_currency_$address" } - - private companion object { - // Must be `false` in production - const val IS_DEMO_MODE_ENABLED = false - - const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" - const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" - } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt new file mode 100644 index 0000000000..8c5dba275a --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt @@ -0,0 +1,25 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Json + +internal data class VisaConfig( + @Json(name = "testnet") + val testnet: Addresses, + @Json(name = "mainnet") + val mainnet: Addresses, + @Json(name = "txHistoryAPIAdditionalHeaders") + val header: Header, +) { + + data class Addresses( + @Json(name = "paymentAccountRegistry") + val paymentAccountRegistry: String, + @Json(name = "bridgeProcessor") + val bridgeProcessor: String, + ) + + data class Header( + @Json(name = "x-asn") + val xAsn: String, + ) +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt new file mode 100644 index 0000000000..e5062cf662 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt @@ -0,0 +1,86 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Moshi +import com.tangem.data.visa.BuildConfig +import com.tangem.data.visa.utils.VisaConstants +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApi +import com.tangem.lib.visa.api.VisaApiBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +internal class VisaLibLoader @Inject constructor( + private val assetLoader: AssetLoader, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val createMutex = Mutex() + + private var config: VisaConfig? = null + + private var provider: VisaContractInfoProvider? = null + private var api: VisaApi? = null + + suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider() + + suspend fun getOrCreateApi(): VisaApi = api ?: createApi() + + private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock { + val config = getOrLoadConfig() + + provider = VisaContractInfoProvider.Builder( + useTestnetRpc = VisaConstants.USE_TEST_ENV, + bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.bridgeProcessor + } else { + config.mainnet.bridgeProcessor + }, + paymentAccountRegistryAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.paymentAccountRegistry + } else { + config.mainnet.paymentAccountRegistry + }, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + dispatchers = dispatchers, + ).build() + + return requireNotNull(provider) { + "Visa provider is not created" + } + } + + private suspend fun createApi(): VisaApi = createMutex.withLock { + val config = getOrLoadConfig() + + api = VisaApiBuilder( + useDevApi = VisaConstants.USE_TEST_ENV, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + headers = mapOf( + X_ASN_HEADER_NAME to config.header.xAsn, + ), + ).build() + + return requireNotNull(api) { + "Visa API is not created" + } + } + + private suspend fun getOrLoadConfig(): VisaConfig { + config = assetLoader.load(VISA_CONFIG_FILE_NAME) + + return requireNotNull(config) { + "Visa config is not found" + } + } + + companion object { + private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config" + private const val X_ASN_HEADER_NAME = "x-asn" + } +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt index a3022a444a..60d487838e 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -1,15 +1,11 @@ package com.tangem.data.visa.di -import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.BuildConfig import com.tangem.data.visa.DefaultVisaRepository +import com.tangem.data.visa.config.VisaLibLoader import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.visa.repository.VisaRepository -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApiBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,29 +21,16 @@ internal object ImplementedVisaDataModule { @Singleton @ImplementedVisaRepository fun provideVisaRepository( - @NetworkMoshi moshi: Moshi, + visaLibLoader: VisaLibLoader, tangemTechApi: TangemTechApi, cacheRegistry: CacheRegistry, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, - ): VisaRepository { - val contractInfoProvider = VisaContractInfoProvider.Builder( - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - dispatchers = dispatchers, - ).build() - val visaApi = VisaApiBuilder( - useDevApi = true, - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - moshi = moshi, - ).build() - - return DefaultVisaRepository( - contractInfoProvider, - tangemTechApi, - visaApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) - } + ): VisaRepository = DefaultVisaRepository( + visaLibLoader, + tangemTechApi, + cacheRegistry, + userWalletsStore, + dispatchers, + ) } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt index b664ad4320..bcc9aeb311 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt @@ -7,9 +7,9 @@ import java.util.Currency internal fun findCurrencyByNumericCode(code: Int): Currency { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } - ?: Currency.getInstance(VisaConfig.fiatCurrency.code) + ?: Currency.getInstance(VisaConstants.fiatCurrency.code) } else { Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}") - Currency.getInstance(VisaConfig.fiatCurrency.code) + Currency.getInstance(VisaConstants.fiatCurrency.code) } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt deleted file mode 100644 index 6b3e3f3f8b..0000000000 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.data.visa.utils - -import com.tangem.domain.appcurrency.model.AppCurrency - -internal object VisaConfig { - - const val NETWORK_NAME = "Polygon PoS" - - const val TOKEN_ID = "tether" - - val fiatCurrency = AppCurrency( - code = "EUR", - name = "Euro", - symbol = "€", - ) -} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt new file mode 100644 index 0000000000..27cd336588 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt @@ -0,0 +1,44 @@ +package com.tangem.data.visa.utils + +import com.tangem.domain.appcurrency.model.AppCurrency + +internal object VisaConstants { + + const val NETWORK_NAME = "Polygon PoS" + + const val TOKEN_ID = "tether" + + val fiatCurrency = AppCurrency( + code = "EUR", + name = "Euro", + symbol = "€", + ) + + /* + * Must be `false` in production + * Don't forget to change CardTypesResolver.isVisaWallet + * */ + const val IS_DEMO_MODE_ENABLED = false + + const val USE_TEST_ENV = true + + const val DEMO_TESTNET_ADDRESS = "0x51d034eb1563d0d2e66379ef37756d3c14936c44" + const val DEMO_TESTNET_PUBLIC_KEY = "03FA1122B809079F79C4E0F657FE11337FEC88C3FB3C6341B2CE2E4F5D9241DD86" + + const val DEMO_MAINNET_ADDRESS = "0x927e3ef2b3d85bacf9e520379f64f6627d323fcd" + const val DEMO_MAINNET_PUBLIC_KEY = "02AC61CD57B8011BEE8BB489FB744845CC113AD379132C56015EE70528B6A88E92" +} + +internal fun getDemoAddress(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_ADDRESS + } else { + VisaConstants.DEMO_MAINNET_ADDRESS + } +} + +internal fun getDemoPublicKey(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_PUBLIC_KEY + } else VisaConstants.DEMO_MAINNET_PUBLIC_KEY +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 3c9f55e37f..6398dcec84 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -21,10 +21,10 @@ internal class VisaCurrencyFactory { return VisaCurrency( symbol = contractInfo.token.symbol, - networkName = VisaConfig.NETWORK_NAME, + networkName = VisaConstants.NETWORK_NAME, decimals = contractInfo.token.decimals, fiatRate = fiatRate, - fiatCurrency = VisaConfig.fiatCurrency, + fiatCurrency = VisaConstants.fiatCurrency, balances = with(contractInfo) { VisaCurrency.Balances( total = balances.total, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index ea53efb4c6..d42d27f165 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -19,24 +19,25 @@ internal class SetBalancesAndLimitsTransformer( private val userWallet: UserWallet, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, -) : WalletStateTransformer(userWallet.walletId) { +) : TypedWalletStateTransformer( + userWalletId = userWallet.walletId, + targetStateClass = WalletState.Visa.Content::class, +) { - override fun transform(prevState: WalletState): WalletState { - return prevState.transformWhenInState { state -> - val visaCurrency = maybeVisaCurrency.getOrElse { - return state.copy( - walletCardState = getErrorWalletCardState(state.walletCardState), - depositButtonState = state.depositButtonState.copy(isEnabled = false), - balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, - ) - } - - state.copy( - walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency), - depositButtonState = state.depositButtonState.copy(isEnabled = true), - balancesAndLimitBlockState = getContentBlockState(visaCurrency), + override fun transformTyped(prevState: WalletState.Visa.Content): WalletState { + val visaCurrency = maybeVisaCurrency.getOrElse { + return prevState.copy( + walletCardState = getErrorWalletCardState(prevState.walletCardState), + depositButtonState = prevState.depositButtonState.copy(isEnabled = false), + balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } + + return prevState.copy( + walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency), + depositButtonState = prevState.depositButtonState.copy(isEnabled = true), + balancesAndLimitBlockState = getContentBlockState(visaCurrency), + ) } private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt new file mode 100644 index 0000000000..2c72901066 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlin.reflect.KClass + +internal abstract class TypedWalletStateTransformer( + userWalletId: UserWalletId, + protected val targetStateClass: KClass, +) : WalletStateTransformer(userWalletId) { + + abstract fun transformTyped(prevState: S): WalletState + + @Suppress("UNCHECKED_CAST") + final override fun transform(prevState: WalletState): WalletState { + return if (prevState::class == targetStateClass) { + transformTyped(prevState as S) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index f02bd16531..bcbd84cf28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -4,7 +4,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber internal abstract class WalletStateTransformer( protected val userWalletId: UserWalletId, @@ -12,7 +11,7 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState - override fun transform(prevState: WalletScreenState): WalletScreenState { + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets .map { state -> @@ -21,13 +20,4 @@ internal abstract class WalletStateTransformer( .toImmutableList(), ) } - - protected inline fun WalletState.transformWhenInState( - transform: (state: S) -> WalletState, - ): WalletState = if (this is S) { - transform(this) - } else { - Timber.w("Impossible to transform ${this::class.simpleName} because current is ${S::class.simpleName}") - this - } } \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 7ae3b7e0dc..4696b698fb 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -3,8 +3,8 @@ package com.tangem.lib.visa import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.tangem.lib.visa.model.VisaContractInfo -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import com.tangem.lib.visa.utils.toHexString import com.tangem.utils.coroutines.CoroutineDispatcherProvider import okhttp3.OkHttpClient @@ -24,16 +24,16 @@ interface VisaContractInfoProvider { suspend fun getContractInfo(walletAddress: String): VisaContractInfo class Builder( + private val useTestnetRpc: Boolean, + private val bridgeProcessorAddress: String, + private val paymentAccountRegistryAddress: String, private val isNetworkLoggingEnabled: Boolean, private val dispatchers: CoroutineDispatcherProvider, - private val baseUrl: String = VisaConfig.BASE_RPC_URL, - private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS, - private val paymentAccountRegistryAddress: String = VisaConfig.PAYMENT_ACCOUNT_REGISTRY_ADDRESS, - private val chainId: Long = VisaConfig.CHAIN_ID, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, - private val decimals: Int = VisaConfig.DECIMALS, - private val gasLimit: Long = VisaConfig.GAS_LIMIT, - private val privateKey: String = ByteArray(VisaConfig.PRIVATE_KEY_LENGTH).toHexString(), + private val chainId: Long = Constants.CHAIN_ID, + private val decimals: Int = Constants.DECIMALS, + private val gasLimit: Long = Constants.GAS_LIMIT, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, + private val privateKey: String = ByteArray(Constants.PRIVATE_KEY_LENGTH).toHexString(), ) { fun build(): VisaContractInfoProvider { @@ -52,6 +52,8 @@ interface VisaContractInfoProvider { } private fun createWeb3J(): Web3j { + val baseUrl: String = if (useTestnetRpc) Constants.TESTNET_RPC_URL else Constants.MAINNET_RPC_URL + val httpClient = OkHttpClient.Builder().apply { connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt index 50f200249b..ef1a99dd5f 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt @@ -5,8 +5,8 @@ import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -17,7 +17,8 @@ class VisaApiBuilder( private val useDevApi: Boolean, private val isNetworkLoggingEnabled: Boolean, private val moshi: Moshi, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, + private val headers: Map, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, ) { fun build(): VisaApi { @@ -35,13 +36,23 @@ class VisaApiBuilder( if (isNetworkLoggingEnabled) { addInterceptor(createNetworkLoggingInterceptor()) } + + if (headers.isNotEmpty()) { + addInterceptor { chain -> + val request = chain.request().newBuilder().apply { + headers.forEach { (key, value) -> addHeader(key, value) } + }.build() + + chain.proceed(request) + } + } } return builder.build() } private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { - val baseUrl = if (useDevApi) VisaConfig.VISA_API_DEV_URL else VisaConfig.VISA_API_PROD_URL + val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt similarity index 55% rename from libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt rename to libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt index e3d0a9dab5..06e365e4ec 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt @@ -1,10 +1,10 @@ package com.tangem.lib.visa.utils -internal object VisaConfig { +internal object Constants { + + const val MAINNET_RPC_URL = "https://polygon-rpc.com/" + const val TESTNET_RPC_URL = "https://rpc-amoy.polygon.technology/" - const val BASE_RPC_URL = "https://polygon-mumbai.g.alchemy.com/v2/_1qqjXgBC_IikaXChnna8KTcV2eMMIQG/" - const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0xe32ecbbc1ec17fa9c160569cd613ad568ca50279" - const val PAYMENT_ACCOUNT_REGISTRY_ADDRESS = "0x3f4ae01073d1a9d5a92315fe118e57d1cdec7c44" const val CHAIN_ID = 80_001L const val DECIMALS = 9 const val GAS_LIMIT = 500_000_000L From 27a864a0a9b265f1238e9476a5c881656a40c312 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 14:41:05 +0200 Subject: [PATCH 4/5] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 13 +- .../tangem/tap/features/main/MainViewModel.kt | 11 ++ .../auth/DefaultStakeKitAuthProvider.kt | 2 +- core/datasource/build.gradle.kts | 1 + .../common/adapter/UnknownEnumMoshiAdapter.kt | 27 +++- .../datasource/api/stakekit/StakeKitApi.kt | 12 +- .../models/response/model/AddressArgument.kt | 4 +- .../response/model/StakingActionType.kt | 2 + .../stakekit/models/response/model/Yield.kt | 6 +- .../response/model/YieldBalanceWrapper.kt | 2 + .../models/response/model/YieldBalances.kt | 138 ------------------ ...eModule.kt => ExpressAssetsStoreModule.kt} | 10 +- .../com/tangem/datasource/di/MoshiModule.kt | 8 +- .../datasource/di/StakingTokensStoreModule.kt | 21 +++ ...sStore.kt => DefaultExpressAssetsStore.kt} | 4 +- .../local/token/DefaultStakingTokensStore.kt | 21 +++ .../{AssetsStore.kt => ExpressAssetsStore.kt} | 2 +- .../local/token/StakingTokensStore.kt | 10 ++ .../tangem/datasource/utils/RequestHeader.kt | 1 + .../data/staking/DefaultStakingRepository.kt | 16 ++ .../converters/StakingTokenConverter.kt | 22 +++ .../data/staking/di/StakingDataModule.kt | 6 + .../tangem/data/tokens/di/TokensDataModule.kt | 10 +- .../repository/DefaultCurrenciesRepository.kt | 6 +- .../DefaultMarketCryptoCurrencyRepository.kt | 6 +- .../staking/FetchStakingTokensUseCase.kt | 15 ++ .../domain/staking/model/StakingToken.kt | 9 ++ .../staking/model/StakingTokenWithYield.kt | 6 + .../staking/repositories/StakingRepository.kt | 2 + features/wallet/impl/build.gradle.kts | 1 + .../wallet/viewmodels/WalletViewModel.kt | 2 +- 31 files changed, 217 insertions(+), 179 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt rename core/datasource/src/main/java/com/tangem/datasource/di/{AssetsStoreModule.kt => ExpressAssetsStoreModule.kt} (52%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt rename core/datasource/src/main/java/com/tangem/datasource/local/token/{DefaultAssetsStore.kt => DefaultExpressAssetsStore.kt} (89%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt rename core/datasource/src/main/java/com/tangem/datasource/local/token/{AssetsStore.kt => ExpressAssetsStore.kt} (90%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 54bfe9d2b7..532e167dbd 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.settings.* import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -14,6 +15,14 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object StakingDomainModule { + @Provides + @Singleton + fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { + return GetStakingAvailabilityUseCase( + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase { @@ -24,8 +33,8 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { - return GetStakingAvailabilityUseCase( + fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { + return FetchStakingTokensUseCase( stakingRepository = stakingRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 53a3565731..23e161aaa8 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase +import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -44,6 +45,7 @@ internal class MainViewModel @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, + private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -69,6 +71,7 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() + fetchStakingTokens() viewModelScope.launch(dispatchers.main) { deleteDeprecatedLogsUseCase() @@ -115,6 +118,14 @@ internal class MainViewModel @Inject constructor( } } + private fun fetchStakingTokens() { + viewModelScope.launch(dispatchers.main) { + fetchStakingTokensUseCase() + .onLeft { Timber.e("Unable to fetch the staking tokens list") } + .onRight { Timber.d("Staking token list was fetched successfully") } + } + } + private fun updateSendFeatureToggle() { viewModelScope.launch(dispatchers.main) { sendFeatureToggles.fetchNewSendEnabled() diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index 7e953fdfd5..a5ef219c9b 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -8,6 +8,6 @@ internal class DefaultStakeKitAuthProvider( ) : StakeKitAuthProvider { override fun getApiKey(): String { - return configManager.config.express?.apiKey ?: error("No StakeKit api key provided") + return configManager.config.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4deb2329d1..b706481bfe 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.staking) // TODO staking create staking models module /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index f61191a104..633fd20cd2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -2,12 +2,35 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import com.squareup.moshi.adapters.EnumJsonAdapter +import com.tangem.datasource.api.stakekit.models.response.model.StakingActionType +import com.tangem.datasource.api.stakekit.models.response.model.Token +import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper /** * Object to create a adapter for enum types with support for unknown enum values. */ object UnknownEnumMoshiAdapter { - fun > create(enumType: Class, defaultValue: T): JsonAdapter = - EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue) + @Suppress("UNCHECKED_CAST") + fun > create(enumType: Class>, defaultValue: Enum<*>): JsonAdapter> { + return EnumJsonAdapter.create(enumType as Class).withUnknownFallback(defaultValue as T) + } +} + +fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { + val map = mapOf( + Token.NetworkType::class.java to Token.NetworkType.UNKNOWN, + StakingActionType::class.java to StakingActionType.UNKNOWN, + Yield.RewardType::class.java to Yield.RewardType.UNKNOWN, + YieldBalanceWrapper.Balance.BalanceType::class.java to YieldBalanceWrapper.Balance.BalanceType.UNKNOWN, + ) + + return apply { + map.forEach { entry -> + val enumClass = entry.key + val unknownValue = entry.value + add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue)) + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index c7d564c69a..4aae3792c7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -18,12 +18,12 @@ interface StakeKitApi { @GET("yields/enabled") suspend fun getMultipleYields( - @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean, - @Query("type") type: YieldType, - @Query("revenueOption") revenueOption: RevenueOption, - @Query("page") page: Int, - @Query("network") network: String, - @Query("limit") limit: Int, + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, + @Query("type") type: YieldType? = null, + @Query("revenueOption") revenueOption: RevenueOption? = null, + @Query("page") page: Int? = null, + @Query("network") network: String? = null, + @Query("limit") limit: Int? = null, ): ApiResponse @GET("yields/{integrationId}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt index 301b7b5805..80c6347e26 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt @@ -10,7 +10,7 @@ data class AddressArgument( @Json(name = "network") val network: String? = null, @Json(name = "minimum") - val minimum: Int? = null, + val minimum: Double? = null, @Json(name = "maximum") - val maximum: Int? = null, + val maximum: Double? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt index 5e60da84d9..2d71d2a50a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt @@ -47,4 +47,6 @@ enum class StakingActionType { @Json(name = "MIGRATE") MIGRATE, + + UNKNOWN, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt index c122022402..bdbea0868c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt @@ -139,8 +139,10 @@ data class Yield( enum class RewardType { @Json(name = "apy") - APY, // auto + APY, // compound rate @Json(name = "apr") - APR, // manual + APR, // simple rate, + + UNKNOWN, } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt index 01f0e26c16..adb7968856 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt @@ -61,6 +61,8 @@ data class YieldBalanceWrapper( @Json(name = "unlocking") UNLOCKING, + + UNKNOWN, } @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt deleted file mode 100644 index 98637444a4..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.datasource.api.stakekit.models.response.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime -import java.math.BigDecimal - -@JsonClass(generateAdapter = true) -data class YieldBalances( - @Json(name = "balances") - val balances: List, - @Json(name = "integrationId") - val integrationId: String, -) { - - @JsonClass(generateAdapter = true) - data class Balance( - @Json(name = "groupId") - val groupId: String, - @Json(name = "type") - val type: BalanceType, - @Json(name = "amount") - val amount: BigDecimal, - @Json(name = "date") - val date: DateTime?, - @Json(name = "pricePerShare") - val pricePerShare: BigDecimal, - @Json(name = "pendingActions") - val pendingActions: List, - @Json(name = "token") - val token: Token, - @Json(name = "validatorAddress") - val validatorAddress: String?, - @Json(name = "validatorAddresses") - val validatorAddresses: List?, - @Json(name = "providerId") - val providerId: String?, - ) { - - enum class BalanceType { - @Json(name = "available") - AVAILABLE, - - @Json(name = "staked") - STAKED, - - @Json(name = "unstaking") - UNSTAKING, - - @Json(name = "unstaked") - UNSTAKED, - - @Json(name = "preparing") - PREPARING, - - @Json(name = "rewards") - REWARDS, - - @Json(name = "locked") - LOCKED, - - @Json(name = "unlocking") - UNLOCKING, - } - - @JsonClass(generateAdapter = true) - data class PendingAction( - @Json(name = "type") - val type: StakingActionType, - @Json(name = "passthrough") - val passthrough: String, - @Json(name = "args") - val args: PendingActionArgs?, - ) { - @JsonClass(generateAdapter = true) - data class PendingActionArgs( - @Json(name = "amount") - val amount: Amount?, - @Json(name = "duration") - val duration: Duration?, - @Json(name = "validatorAddress") - val validatorAddress: Required?, - @Json(name = "validatorAddresses") - val validatorAddresses: Required?, - @Json(name = "nfts") - val nfts: List?, - @Json(name = "tronResource") - val tronResource: TronResource?, - @Json(name = "signatureVerification") - val signatureVerification: Required?, - ) { - @JsonClass(generateAdapter = true) - data class Amount( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: BigDecimal?, - @Json(name = "maximum") - val maximum: BigDecimal?, - ) - - @JsonClass(generateAdapter = true) - data class Duration( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: Int?, - @Json(name = "maximum") - val maximum: Int?, - ) - - @JsonClass(generateAdapter = true) - data class Nft( - @Json(name = "baycId") - val baycId: Required?, - @Json(name = "maycId") - val maycId: Required?, - @Json(name = "bakcId") - val bakcId: Required?, - ) - - @JsonClass(generateAdapter = true) - data class TronResource( - @Json(name = "required") - val required: Boolean, - @Json(name = "options") - val options: List, - ) - } - } - - @JsonClass(generateAdapter = true) - data class Required( - @Json(name = "required") - val required: Boolean, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt index 43b8e93f5c..cf51b2b006 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultAssetsStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.DefaultExpressAssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object AssetsStoreModule { +internal object ExpressAssetsStoreModule { @Provides @Singleton - fun provideAssetsStore(): AssetsStore { - return DefaultAssetsStore(dataStore = RuntimeDataStore()) + fun provideExpressAssetsStore(): ExpressAssetsStore { + return DefaultExpressAssetsStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index ddd3386e58..e04042a8b4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -4,11 +4,10 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter +import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.api.common.adapter.BigDecimalAdapter import com.tangem.datasource.api.common.adapter.DateTimeAdapter import com.tangem.datasource.api.common.adapter.LocalDateAdapter -import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter -import com.tangem.datasource.api.stakekit.models.response.model.Token import com.tangem.datasource.config.models.ProviderModel import dagger.Module import dagger.Provides @@ -35,10 +34,7 @@ class MoshiModule { .add(LocalDateAdapter()) .add(DateTimeAdapter()) .add(KotlinJsonAdapterFactory()) - .add( - Token.NetworkType::class.java, - UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN), - ) + .addStakeKitEnumFallbackAdapters() .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt new file mode 100644 index 0000000000..50b1bacb2b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultStakingTokensStore +import com.tangem.datasource.local.token.StakingTokensStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object StakingTokensStoreModule { + + @Provides + @Singleton + fun provideStakingTokensStore(): StakingTokensStore { + return DefaultStakingTokensStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt index 9177ec70b7..c67abc06e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt @@ -4,9 +4,9 @@ import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId -internal class DefaultAssetsStore( +internal class DefaultExpressAssetsStore( private val dataStore: StringKeyDataStore>, -) : AssetsStore { +) : ExpressAssetsStore { override suspend fun getSyncOrNull(userWalletId: UserWalletId): List? { return dataStore.getSyncOrNull(userWalletId.stringValue) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt new file mode 100644 index 0000000000..a74afc914e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.staking.model.StakingTokenWithYield + +internal class DefaultStakingTokensStore( + private val dataStore: StringKeyDataStore>, +) : StakingTokensStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_TOKENS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_TOKENS_KEY, items) + } + + companion object { + private const val STAKING_TOKENS_KEY = "STAKING_TOKENS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt index 11d6101843..353a15c7e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.express.models.response.Asset import com.tangem.domain.wallets.models.UserWalletId -interface AssetsStore { +interface ExpressAssetsStore { suspend fun getSyncOrNull(userWalletId: UserWalletId): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt new file mode 100644 index 0000000000..586674756b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.domain.staking.model.StakingTokenWithYield + +interface StakingTokensStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index c9fa81608f..215b56e9f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -35,5 +35,6 @@ sealed class RequestHeader(vararg pairs: Pair String>) { class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( "X-API-KEY" to { stakeKitAuthProvider.getApiKey() }, + "accept" to { "application/json" }, ) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 26f738de00..0c5b731554 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,18 +1,26 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain +import com.tangem.data.staking.converters.StakingTokenConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingTokensStore import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingFeatureToggles: StakingFeatureToggles, + private val stakingTokenStore: StakingTokensStore, + private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { + private val tokenWithYieldConverter = StakingTokenConverter() + override fun getStakingAvailability(blockchainId: String): StakingAvailability { if (!stakingFeatureToggles.isStakingEnabled) { return StakingAvailability.Unavailable @@ -33,6 +41,14 @@ internal class DefaultStakingRepository( ) } + override suspend fun fetchEnabledTokens() { + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getTokens().getOrThrow() + + stakingTokenStore.store(stakingTokensWithYields.map { tokenWithYieldConverter.convert(it) }) + } + } + companion object { private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt new file mode 100644 index 0000000000..3fa4fe1a33 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield +import com.tangem.domain.staking.model.StakingToken +import com.tangem.domain.staking.model.StakingTokenWithYield +import com.tangem.utils.converter.Converter + +class StakingTokenConverter : Converter { + + override fun convert(value: TokenWithYield): StakingTokenWithYield { + return StakingTokenWithYield( + token = StakingToken( + name = value.token.name, + symbol = value.token.symbol, + decimals = value.token.decimals, + contractAddress = value.token.address, + coinGeckoId = value.token.coinGeckoId, + ), + availableYieldIds = value.availableYieldIds, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0028f190b2..7663df6823 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,8 +2,10 @@ package com.tangem.data.staking.di import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingTokensStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,10 +21,14 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingFeatureToggles: StakingFeatureToggles, + stakingTokenStore: StakingTokensStore, + dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingFeatureToggles = stakingFeatureToggles, + stakingTokenStore = stakingTokenStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 062692cd06..5705476197 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* @@ -31,7 +31,7 @@ internal object TokensDataModule { userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, - assetsStore: AssetsStore, + expressAssetsStore: ExpressAssetsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { @@ -41,7 +41,7 @@ internal object TokensDataModule { userTokensStore = userTokensStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - assetsStore = assetsStore, + expressAssetsStore = expressAssetsStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) @@ -87,8 +87,8 @@ internal object TokensDataModule { @Provides @Singleton - fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(assetsStore) + fun provideDefaultMarketCoinsRepository(expressAssetsStore: ExpressAssetsStore): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(expressAssetsStore) } @Provides 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 cf29c3b4a3..ff644fd95c 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 @@ -15,7 +15,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider @@ -46,7 +46,7 @@ internal class DefaultCurrenciesRepository( private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -493,7 +493,7 @@ internal class DefaultCurrenciesRepository( ), ) - assetsStore.store(userWalletId, response.getOrThrow()) + expressAssetsStore.store(userWalletId, response.getOrThrow()) } } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index a210d96273..a3c7834ed9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -1,13 +1,13 @@ package com.tangem.data.tokens.repository import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId class DefaultMarketCryptoCurrencyRepository( - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, ) : MarketCryptoCurrencyRepository { override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { @@ -17,7 +17,7 @@ class DefaultMarketCryptoCurrencyRepository( private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - return assetsStore.getSyncOrNull(userWalletId)?.find { + return expressAssetsStore.getSyncOrNull(userWalletId)?.find { it.network == cryptoCurrency.network.backendId && it.contractAddress.equals(contractAddress, ignoreCase = true) }?.exchangeAvailable ?: false diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt new file mode 100644 index 0000000000..55c8d1b3fa --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for getting enabled tokens + */ +class FetchStakingTokensUseCase( + private val stakingRepository: StakingRepository, +) { + suspend operator fun invoke(): Either { + return Either.catch { stakingRepository.fetchEnabledTokens() } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt new file mode 100644 index 0000000000..5da29043d1 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.staking.model + +data class StakingToken( + val name: String, + val symbol: String, + val decimals: Int, + val contractAddress: String?, + val coinGeckoId: String?, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt new file mode 100644 index 0000000000..833cb6ae49 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.model + +data class StakingTokenWithYield( + val token: StakingToken, + val availableYieldIds: List, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 763377ac5e..5e84852870 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -8,4 +8,6 @@ interface StakingRepository { fun getStakingAvailability(blockchainId: String): StakingAvailability suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + + suspend fun fetchEnabledTokens() } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index dad4976a7d..6f499d7be1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) + implementation(projects.domain.staking) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 8507f48bbc..6d6961b577 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -55,12 +55,12 @@ internal class WalletViewModel @Inject constructor( private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState From 0e29df4ce9730aaa4d2d2b858debf8ba05a5a545 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 17:23:23 +0200 Subject: [PATCH 5/5] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 + .../tangem/tap/features/main/MainViewModel.kt | 2 +- .../common/adapter/UnknownEnumMoshiAdapter.kt | 17 +- .../datasource/api/stakekit/StakeKitApi.kt | 14 +- .../models/response/EnabledYieldsResponse.kt | 4 +- ...dressArgument.kt => AddressArgumentDTO.kt} | 2 +- ...gActionType.kt => StakingActionTypeDTO.kt} | 2 +- .../response/model/{Token.kt => TokenDTO.kt} | 6 +- ...TokenWithYield.kt => TokenWithYieldDTO.kt} | 4 +- ...ceWrapper.kt => YieldBalanceWrapperDTO.kt} | 10 +- .../response/model/{Yield.kt => YieldDTO.kt} | 50 ++--- .../datasource/di/StakingTokensStoreModule.kt | 8 +- .../local/token/DefaultStakingYieldsStore.kt | 21 ++ .../local/token/StakingYieldsStore.kt | 10 + core/res/src/main/res/values-ru/strings.xml | 11 +- core/res/src/main/res/values/strings.xml | 20 +- data/staking/build.gradle.kts | 2 + .../data/staking/DefaultStakingRepository.kt | 124 +++++++---- .../converters/StakingTokenConverter.kt | 6 +- .../data/staking/converters/YieldConverter.kt | 206 ++++++++++++++++++ .../data/staking/di/StakingDataModule.kt | 6 +- domain/staking/build.gradle.kts | 9 +- .../staking/FetchStakingTokensUseCase.kt | 10 +- .../staking/GetStakingAvailabilityUseCase.kt | 7 +- .../staking/error/StakingTokensError.kt | 6 + .../staking/model/StakingAvailability.kt | 2 + .../com/tangem/domain/staking/model/Yield.kt | 172 +++++++++++++++ .../staking/repositories/StakingRepository.kt | 10 +- domain/tokens/build.gradle.kts | 1 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 22 ++ .../model/ScenarioUnavailabilityReason.kt | 3 + .../domain/tokens/model/TokenActionsState.kt | 2 + .../tokendetails/TokenDetailsPreviewData.kt | 4 +- .../tokendetails/state/TokenDetailsState.kt | 2 +- .../components/TokenDetailsActionButton.kt | 15 ++ .../TokenDetailsActionButtonsConverter.kt | 6 + .../TokenDetailsSkeletonStateConverter.kt | 5 +- .../state/factory/TokenDetailsStateFactory.kt | 14 +- .../tokendetails/ui/TokenDetailsScreen.kt | 2 +- .../viewmodels/TokenDetailsClickIntents.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 13 +- .../wallet/state/model/WalletManageButton.kt | 13 ++ .../SetRefreshStateTransformer.kt | 1 + .../MultiWalletCurrencyActionsConverter.kt | 5 + .../WalletCurrencyActionsClickIntents.kt | 14 ++ 45 files changed, 731 insertions(+), 137 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{AddressArgument.kt => AddressArgumentDTO.kt} (92%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{StakingActionType.kt => StakingActionTypeDTO.kt} (95%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{Token.kt => TokenDTO.kt} (97%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{TokenWithYield.kt => TokenWithYieldDTO.kt} (75%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{YieldBalanceWrapper.kt => YieldBalanceWrapperDTO.kt} (96%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{Yield.kt => YieldDTO.kt} (81%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 40ba3c5084..ca10e45669 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -190,6 +191,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -199,6 +201,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 23e161aaa8..07650632f0 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -121,7 +121,7 @@ internal class MainViewModel @Inject constructor( private fun fetchStakingTokens() { viewModelScope.launch(dispatchers.main) { fetchStakingTokensUseCase() - .onLeft { Timber.e("Unable to fetch the staking tokens list") } + .onLeft { Timber.e(it, "Unable to fetch the staking tokens list") } .onRight { Timber.d("Staking token list was fetched successfully") } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 633fd20cd2..1d8fc5ffc1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -2,10 +2,10 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import com.squareup.moshi.adapters.EnumJsonAdapter -import com.tangem.datasource.api.stakekit.models.response.model.StakingActionType -import com.tangem.datasource.api.stakekit.models.response.model.Token -import com.tangem.datasource.api.stakekit.models.response.model.Yield -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper +import com.tangem.datasource.api.stakekit.models.response.model.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO /** * Object to create a adapter for enum types with support for unknown enum values. @@ -20,10 +20,11 @@ object UnknownEnumMoshiAdapter { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { val map = mapOf( - Token.NetworkType::class.java to Token.NetworkType.UNKNOWN, - StakingActionType::class.java to StakingActionType.UNKNOWN, - Yield.RewardType::class.java to Yield.RewardType.UNKNOWN, - YieldBalanceWrapper.Balance.BalanceType::class.java to YieldBalanceWrapper.Balance.BalanceType.UNKNOWN, + TokenDTO.NetworkTypeDTO::class.java to TokenDTO.NetworkTypeDTO.UNKNOWN, + StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, + YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, + YieldBalanceWrapperDTO.BalanceDTO.BalanceType::class.java to + YieldBalanceWrapperDTO.BalanceDTO.BalanceType.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 4aae3792c7..6f1df5b3f4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -5,9 +5,9 @@ import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.request.RevenueOption import com.tangem.datasource.api.stakekit.models.request.YieldType import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse -import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield -import com.tangem.datasource.api.stakekit.models.response.model.Yield -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Path @@ -30,19 +30,19 @@ interface StakeKitApi { suspend fun getSingleYield( @Path("integrationId") integrationId: String, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, - ): ApiResponse + ): ApiResponse @GET("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> @GET("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( @Path("integrationId") integrationId: String, @Body body: YieldBalanceRequestBody, - ): ApiResponse + ): ApiResponse @GET("tokens") - suspend fun getTokens(): ApiResponse> + suspend fun getTokens(): ApiResponse> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt index c4a958c435..15605bc27c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt @@ -2,12 +2,12 @@ package com.tangem.datasource.api.stakekit.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO @JsonClass(generateAdapter = true) data class EnabledYieldsResponse( @Json(name = "data") - val data: List, + val data: List, @Json(name = "hasNextPage") val hasNextPage: Boolean, @Json(name = "limit") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt index 80c6347e26..7a4f33b288 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class AddressArgument( +data class AddressArgumentDTO( @Json(name = "required") val required: Boolean, @Json(name = "network") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt index 2d71d2a50a..5f310f04a8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json -enum class StakingActionType { +enum class StakingActionTypeDTO { @Json(name = "STAKE") STAKE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt similarity index 97% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt index 88d5d12b59..6321b54ecb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt @@ -4,9 +4,9 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class Token( +data class TokenDTO( @Json(name = "name") val name: String, - @Json(name = "network") val network: NetworkType, + @Json(name = "network") val network: NetworkTypeDTO, @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "address") val address: String?, @@ -14,7 +14,7 @@ data class Token( @Json(name = "logoURI") val logoURI: String?, @Json(name = "isPoints") val isPoints: Boolean?, ) { - enum class NetworkType { + enum class NetworkTypeDTO { @Json(name = "avalanche-c") AVALANCHE_C, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt similarity index 75% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt index 2bc30185af..15182ef389 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class TokenWithYield( - @Json(name = "token") val token: Token, +data class TokenWithYieldDTO( + @Json(name = "token") val token: TokenDTO, @Json(name = "availableYields") val availableYieldIds: List, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt similarity index 96% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index adb7968856..c64bdd591d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -6,15 +6,15 @@ import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class YieldBalanceWrapper( +data class YieldBalanceWrapperDTO( @Json(name = "balances") - val balances: List, + val balances: List, @Json(name = "integrationId") val integrationId: String?, ) { @JsonClass(generateAdapter = true) - data class Balance( + data class BalanceDTO( @Json(name = "groupId") val groupId: String, @Json(name = "type") @@ -28,7 +28,7 @@ data class YieldBalanceWrapper( @Json(name = "pendingActions") val pendingActions: List, @Json(name = "token") - val token: Token, + val tokenDTO: TokenDTO, @Json(name = "validatorAddress") val validatorAddress: String?, @Json(name = "validatorAddresses") @@ -68,7 +68,7 @@ data class YieldBalanceWrapper( @JsonClass(generateAdapter = true) data class PendingAction( @Json(name = "type") - val type: StakingActionType, + val type: StakingActionTypeDTO, @Json(name = "passthrough") val passthrough: String, @Json(name = "args") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt similarity index 81% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index bdbea0868c..3e0c439e09 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -5,33 +5,33 @@ import com.squareup.moshi.JsonClass import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class Yield( +data class YieldDTO( @Json(name = "id") val id: String, @Json(name = "token") - val token: Token, + val token: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokens: List, @Json(name = "args") - val args: Args, + val args: ArgsDTO, @Json(name = "status") - val status: Status, + val status: StatusDTO, @Json(name = "apy") val apy: BigDecimal, @Json(name = "rewardRate") val rewardRate: Double, @Json(name = "rewardType") - val rewardType: RewardType, + val rewardType: RewardTypeDTO, @Json(name = "metadata") - val metadata: Metadata, + val metadata: MetadataDTO, @Json(name = "validators") - val validators: List, + val validators: List, @Json(name = "isAvailable") val isAvailable: Boolean, ) { @JsonClass(generateAdapter = true) - data class Status( + data class StatusDTO( @Json(name = "enter") val enter: Boolean, @Json(name = "exit") @@ -39,7 +39,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Args( + data class ArgsDTO( @Json(name = "enter") val enter: Enter, @Json(name = "exit") @@ -50,20 +50,20 @@ data class Yield( @Json(name = "addresses") val addresses: Addresses, @Json(name = "args") - val args: Map, + val args: Map, ) { @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "address") - val address: AddressArgument, + val address: AddressArgumentDTO, @Json(name = "additionalAddresses") - val additionalAddresses: Map? = null, + val additionalAddresses: Map? = null, ) } } @JsonClass(generateAdapter = true) - data class Validator( + data class ValidatorDTO( @Json(name = "address") val address: String, @Json(name = "status") @@ -87,7 +87,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Metadata( + data class MetadataDTO( @Json(name = "name") val name: String, @Json(name = "logoURI") @@ -97,19 +97,19 @@ data class Yield( @Json(name = "documentation") val documentation: String?, @Json(name = "gasFeeToken") - val gasFeeToken: Token, + val gasFeeTokenDTO: TokenDTO, @Json(name = "token") - val token: Token, + val tokenDTO: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokensDTO: List, @Json(name = "type") val type: String, @Json(name = "rewardSchedule") val rewardSchedule: String, @Json(name = "cooldownPeriod") - val cooldownPeriod: Period, + val cooldownPeriod: PeriodDTO, @Json(name = "warmupPeriod") - val warmupPeriod: Period, + val warmupPeriod: PeriodDTO, @Json(name = "rewardClaiming") val rewardClaiming: String, @Json(name = "defaultValidator") @@ -119,25 +119,25 @@ data class Yield( @Json(name = "supportsMultipleValidators") val supportsMultipleValidators: Boolean, @Json(name = "revshare") - val revshare: Enabled, + val revshare: EnabledDTO, @Json(name = "fee") - val fee: Enabled, + val fee: EnabledDTO, ) { @JsonClass(generateAdapter = true) - data class Period( + data class PeriodDTO( @Json(name = "days") val days: Int, ) @JsonClass(generateAdapter = true) - data class Enabled( + data class EnabledDTO( @Json(name = "enabled") val enabled: Boolean, ) } - enum class RewardType { + enum class RewardTypeDTO { @Json(name = "apy") APY, // compound rate @Json(name = "apr") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt index 50b1bacb2b..45e280e949 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultStakingTokensStore -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore +import com.tangem.datasource.local.token.StakingYieldsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +15,7 @@ internal object StakingTokensStoreModule { @Provides @Singleton - fun provideStakingTokensStore(): StakingTokensStore { - return DefaultStakingTokensStore(dataStore = RuntimeDataStore()) + fun provideStakingTokensStore(): StakingYieldsStore { + return DefaultStakingYieldsStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt new file mode 100644 index 0000000000..1cdb6c381c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultStakingYieldsStore( + private val dataStore: StringKeyDataStore>, +) : StakingYieldsStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_YIELDS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_YIELDS_KEY, items) + } + + companion object { + private const val STAKING_YIELDS_KEY = "STAKING_YIELDS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt new file mode 100644 index 0000000000..61f46cf15d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO + +interface StakingYieldsStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 1135d892dc..67531e72a0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -117,6 +117,7 @@ Отклонить Перезагрузить Переименовать + Сохранить Сохранить изменения Искать Поиск токенов @@ -128,6 +129,7 @@ Поделиться Подписать Подписать и отправить + Стейкинг Начать Отправить Успешно @@ -169,6 +171,7 @@ Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить Остерегайтесь мошеннических токенов, они могут ничего не стоить Токены могут быть созданы кем угодно + Купить кошелек Tangem Чат Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа. @@ -186,6 +189,7 @@ Скрывать балансы жестом переворота Эмитент Подписано + Отправить отзыв Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -538,6 +542,8 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Доступно + Стейкинг %s Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -579,9 +585,10 @@ У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. - Продажа средств станет доступной после завершения транзакции(-ий) в сети %s + Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. + В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -668,7 +675,7 @@ Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. Ошибка активации - По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain. + По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. Отключение сети BNB Beacon Chain Можно лучше Нравится diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8bbaa40db7..130168cb3a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ Reject Reload Rename + Save Save changes Search Search tokens @@ -127,6 +128,8 @@ Share Sign Sign and send + Stake + Staking Start Submit Success @@ -168,6 +171,7 @@ Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. Be aware of adding scam tokens, they can cost nothing Note that tokens can be created by anyone + Buy Tangem Wallet Chat Access code You will have to submit the correct access code before scanning the card @@ -185,6 +189,7 @@ Flip-to-Hide Balances Issuer Signed + Send feedback Details Check your internet connection or switch to a different network Terms of service @@ -531,6 +536,18 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + Available + Average Reward Rate + %s est. profit + Minimum Requirement + No rewards to claim + On stake + Reward claiming + Reward schedule + Staking %s + Unbonding Period + Warmup period + Rewards Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -575,6 +592,7 @@ Selling funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. + Staking %s is not available at the moment. Please check our updates. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -661,7 +679,7 @@ Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. Activation error - According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service to transfer them to the BNB Smart Chain network. + According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. BNB Beacon Chain will shut down Could be better Like it diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 83a8cccc87..3c16b7c878 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.domain.tokens.models) implementation(projects.domain.staking) implementation(projects.features.staking.api) @@ -29,6 +30,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(projects.libs.blockchainSdk) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 0c5b731554..be421d5a75 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,13 +1,16 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain -import com.tangem.data.staking.converters.StakingTokenConverter +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.data.staking.converters.YieldConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -15,65 +18,90 @@ import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingFeatureToggles: StakingFeatureToggles, - private val stakingTokenStore: StakingTokensStore, + private val stakingYieldsStore: StakingYieldsStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { - private val tokenWithYieldConverter = StakingTokenConverter() + private val yieldConverter = YieldConverter() + + override suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability { + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable - override fun getStakingAvailability(blockchainId: String): StakingAvailability { if (!stakingFeatureToggles.isStakingEnabled) { return StakingAvailability.Unavailable } + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable - return integrationIdMap[Blockchain.fromId(blockchainId)]?.let { - StakingAvailability.Available(it) - } ?: StakingAvailability.Unavailable - } + val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) + val isSupported = isStakingSupported(rawCurrencyId) - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { - val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() - - return StakingEntryInfo( - interestRate = yield.apy, - periodInDays = yield.metadata.cooldownPeriod.days, - tokenSymbol = yield.token.symbol, - ) - } - - override suspend fun fetchEnabledTokens() { - withContext(dispatchers.io) { - val stakingTokensWithYields = stakeKitApi.getTokens().getOrThrow() - - stakingTokenStore.store(stakingTokensWithYields.map { tokenWithYieldConverter.convert(it) }) + when { + prefetchedYield != null && isSupported -> { + StakingAvailability.Available(prefetchedYield.id) + } + prefetchedYield == null && isSupported -> { + StakingAvailability.TemporaryDisabled + } + else -> StakingAvailability.Unavailable + } } } - companion object { - private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" - private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - private const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" - private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - private const val NEAR_INTEGRATION_ID = "near-near-native-staking" - private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { + return yields + .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } + } - private val integrationIdMap = mapOf( - Blockchain.Solana to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos to COSMOS_INTEGRATION_ID, - Blockchain.Polkadot to POLKADOT_INTEGRATION_ID, - Blockchain.Polygon to ETHEREUM_INTEGRATION_ID, - Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID, - Blockchain.Tron to TRON_INTEGRATION_ID, - Blockchain.Cronos to CRONOS_INTEGRATION_ID, - Blockchain.Binance to BINANCE_INTEGRATION_ID, - Blockchain.Kava to KAVA_INTEGRATION_ID, - Blockchain.Near to NEAR_INTEGRATION_ID, - Blockchain.Tezos to TEZOS_INTEGRATION_ID, + override fun isStakingSupported(currencyId: String): Boolean { + return integrationIds.contains(currencyId) + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + return withContext(dispatchers.io) { + val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + + StakingEntryInfo( + interestRate = yield.apy, + periodInDays = yield.metadata.cooldownPeriod.days, + tokenSymbol = yield.token.symbol, + ) + } + } + + override suspend fun fetchEnabledYields() { + if (!stakingFeatureToggles.isStakingEnabled) { + return + } + + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + + stakingYieldsStore.store(stakingTokensWithYields.data) + } + } + + private suspend fun getEnabledYields(): List? { + val yields = stakingYieldsStore.getSyncOrNull() ?: return null + return yields.map { yieldConverter.convert(it) } + } + + companion object { + private val integrationIds = setOf( + Blockchain.Solana.toCoinId(), + Blockchain.Cosmos.toCoinId(), + Blockchain.Polkadot.toCoinId(), + Blockchain.Polygon.toCoinId(), + Blockchain.Avalanche.toCoinId(), + Blockchain.Tron.toCoinId(), + Blockchain.Cronos.toCoinId(), + Blockchain.Binance.toCoinId(), + Blockchain.Kava.toCoinId(), + Blockchain.Near.toCoinId(), + Blockchain.Tezos.toCoinId(), ) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt index 3fa4fe1a33..06cbb55869 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt @@ -1,13 +1,13 @@ package com.tangem.data.staking.converters -import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO import com.tangem.domain.staking.model.StakingToken import com.tangem.domain.staking.model.StakingTokenWithYield import com.tangem.utils.converter.Converter -class StakingTokenConverter : Converter { +class StakingTokenConverter : Converter { - override fun convert(value: TokenWithYield): StakingTokenWithYield { + override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield { return StakingTokenWithYield( token = StakingToken( name = value.token.name, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt new file mode 100644 index 0000000000..d7300234f0 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -0,0 +1,206 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.domain.staking.model.* +import com.tangem.utils.converter.Converter + +class YieldConverter : Converter { + + override fun convert(value: YieldDTO): Yield { + return Yield( + id = value.id, + token = convertToken(value.token), + tokens = value.tokens.map { convertToken(it) }, + args = convertArgs(value.args), + status = convertStatus(value.status), + apy = value.apy, + rewardRate = value.rewardRate, + rewardType = convertRewardType(value.rewardType), + metadata = convertMetadata(value.metadata), + validators = value.validators.map { convertValidator(it) }, + isAvailable = value.isAvailable, + ) + } + + private fun convertToken(tokenDTO: TokenDTO): Token { + return Token( + name = tokenDTO.name, + network = convertNetworkType(tokenDTO.network), + symbol = tokenDTO.symbol, + decimals = tokenDTO.decimals, + address = tokenDTO.address, + coinGeckoId = tokenDTO.coinGeckoId, + logoURI = tokenDTO.logoURI, + isPoints = tokenDTO.isPoints, + ) + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun convertNetworkType(networkTypeDTO: TokenDTO.NetworkTypeDTO): Token.NetworkType { + return when (networkTypeDTO) { + TokenDTO.NetworkTypeDTO.AVALANCHE_C -> Token.NetworkType.AVALANCHE_C + TokenDTO.NetworkTypeDTO.AVALANCHE_ATOMIC -> Token.NetworkType.AVALANCHE_ATOMIC + TokenDTO.NetworkTypeDTO.AVALANCHE_P -> Token.NetworkType.AVALANCHE_P + TokenDTO.NetworkTypeDTO.ARBITRUM -> Token.NetworkType.ARBITRUM + TokenDTO.NetworkTypeDTO.BINANCE -> Token.NetworkType.BINANCE + TokenDTO.NetworkTypeDTO.CELO -> Token.NetworkType.CELO + TokenDTO.NetworkTypeDTO.ETHEREUM -> Token.NetworkType.ETHEREUM + TokenDTO.NetworkTypeDTO.ETHEREUM_GOERLI -> Token.NetworkType.ETHEREUM_GOERLI + TokenDTO.NetworkTypeDTO.ETHEREUM_HOLESKY -> Token.NetworkType.ETHEREUM_HOLESKY + TokenDTO.NetworkTypeDTO.FANTOM -> Token.NetworkType.FANTOM + TokenDTO.NetworkTypeDTO.HARMONY -> Token.NetworkType.HARMONY + TokenDTO.NetworkTypeDTO.OPTIMISM -> Token.NetworkType.OPTIMISM + TokenDTO.NetworkTypeDTO.POLYGON -> Token.NetworkType.POLYGON + TokenDTO.NetworkTypeDTO.GNOSIS -> Token.NetworkType.GNOSIS + TokenDTO.NetworkTypeDTO.MOONRIVER -> Token.NetworkType.MOONRIVER + TokenDTO.NetworkTypeDTO.OKC -> Token.NetworkType.OKC + TokenDTO.NetworkTypeDTO.ZKSYNC -> Token.NetworkType.ZKSYNC + TokenDTO.NetworkTypeDTO.VICTION -> Token.NetworkType.VICTION + TokenDTO.NetworkTypeDTO.AGORIC -> Token.NetworkType.AGORIC + TokenDTO.NetworkTypeDTO.AKASH -> Token.NetworkType.AKASH + TokenDTO.NetworkTypeDTO.AXELAR -> Token.NetworkType.AXELAR + TokenDTO.NetworkTypeDTO.BAND_PROTOCOL -> Token.NetworkType.BAND_PROTOCOL + TokenDTO.NetworkTypeDTO.BITSONG -> Token.NetworkType.BITSONG + TokenDTO.NetworkTypeDTO.CANTO -> Token.NetworkType.CANTO + TokenDTO.NetworkTypeDTO.CHIHUAHUA -> Token.NetworkType.CHIHUAHUA + TokenDTO.NetworkTypeDTO.COMDEX -> Token.NetworkType.COMDEX + TokenDTO.NetworkTypeDTO.COREUM -> Token.NetworkType.COREUM + TokenDTO.NetworkTypeDTO.COSMOS -> Token.NetworkType.COSMOS + TokenDTO.NetworkTypeDTO.CRESCENT -> Token.NetworkType.CRESCENT + TokenDTO.NetworkTypeDTO.CRONOS -> Token.NetworkType.CRONOS + TokenDTO.NetworkTypeDTO.CUDOS -> Token.NetworkType.CUDOS + TokenDTO.NetworkTypeDTO.DESMOS -> Token.NetworkType.DESMOS + TokenDTO.NetworkTypeDTO.DYDX -> Token.NetworkType.DYDX + TokenDTO.NetworkTypeDTO.EVMOS -> Token.NetworkType.EVMOS + TokenDTO.NetworkTypeDTO.FETCH_AI -> Token.NetworkType.FETCH_AI + TokenDTO.NetworkTypeDTO.GRAVITY_BRIDGE -> Token.NetworkType.GRAVITY_BRIDGE + TokenDTO.NetworkTypeDTO.INJECTIVE -> Token.NetworkType.INJECTIVE + TokenDTO.NetworkTypeDTO.IRISNET -> Token.NetworkType.IRISNET + TokenDTO.NetworkTypeDTO.JUNO -> Token.NetworkType.JUNO + TokenDTO.NetworkTypeDTO.KAVA -> Token.NetworkType.KAVA + TokenDTO.NetworkTypeDTO.KI_NETWORK -> Token.NetworkType.KI_NETWORK + TokenDTO.NetworkTypeDTO.MARS_PROTOCOL -> Token.NetworkType.MARS_PROTOCOL + TokenDTO.NetworkTypeDTO.NYM -> Token.NetworkType.NYM + TokenDTO.NetworkTypeDTO.OKEX_CHAIN -> Token.NetworkType.OKEX_CHAIN + TokenDTO.NetworkTypeDTO.ONOMY -> Token.NetworkType.ONOMY + TokenDTO.NetworkTypeDTO.OSMOSIS -> Token.NetworkType.OSMOSIS + TokenDTO.NetworkTypeDTO.PERSISTENCE -> Token.NetworkType.PERSISTENCE + TokenDTO.NetworkTypeDTO.QUICKSILVER -> Token.NetworkType.QUICKSILVER + TokenDTO.NetworkTypeDTO.REGEN -> Token.NetworkType.REGEN + TokenDTO.NetworkTypeDTO.SECRET -> Token.NetworkType.SECRET + TokenDTO.NetworkTypeDTO.SENTINEL -> Token.NetworkType.SENTINEL + TokenDTO.NetworkTypeDTO.SOMMELIER -> Token.NetworkType.SOMMELIER + TokenDTO.NetworkTypeDTO.STAFI -> Token.NetworkType.STAFI + TokenDTO.NetworkTypeDTO.STARGAZE -> Token.NetworkType.STARGAZE + TokenDTO.NetworkTypeDTO.STRIDE -> Token.NetworkType.STRIDE + TokenDTO.NetworkTypeDTO.TERITORI -> Token.NetworkType.TERITORI + TokenDTO.NetworkTypeDTO.TGRADE -> Token.NetworkType.TGRADE + TokenDTO.NetworkTypeDTO.UMEE -> Token.NetworkType.UMEE + TokenDTO.NetworkTypeDTO.POLKADOT -> Token.NetworkType.POLKADOT + TokenDTO.NetworkTypeDTO.KUSAMA -> Token.NetworkType.KUSAMA + TokenDTO.NetworkTypeDTO.WESTEND -> Token.NetworkType.WESTEND + TokenDTO.NetworkTypeDTO.BINANCEBEACON -> Token.NetworkType.BINANCEBEACON + TokenDTO.NetworkTypeDTO.NEAR -> Token.NetworkType.NEAR + TokenDTO.NetworkTypeDTO.SOLANA -> Token.NetworkType.SOLANA + TokenDTO.NetworkTypeDTO.TEZOS -> Token.NetworkType.TEZOS + TokenDTO.NetworkTypeDTO.TRON -> Token.NetworkType.TRON + else -> Token.NetworkType.UNKNOWN + } + } + + private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { + return Yield.Args( + enter = convertEnter(argsDTO.enter), + exit = argsDTO.exit?.let { convertEnter(it) }, + ) + } + + private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { + return Yield.Args.Enter( + addresses = convertAddresses(enterDTO.addresses), + args = enterDTO.args.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { + return Yield.Args.Enter.Addresses( + address = convertAddressArgument(addressesDTO.address), + additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddressArgument(addressArgumentDTO: AddressArgumentDTO): AddressArgument { + return AddressArgument( + required = addressArgumentDTO.required, + network = addressArgumentDTO.network, + minimum = addressArgumentDTO.minimum, + maximum = addressArgumentDTO.maximum, + ) + } + + private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status { + return Yield.Status( + enter = statusDTO.enter, + exit = statusDTO.exit, + ) + } + + private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata { + return Yield.Metadata( + name = metadataDTO.name, + logoUri = metadataDTO.logoUri, + description = metadataDTO.description, + documentation = metadataDTO.documentation, + gasFeeToken = convertToken(metadataDTO.gasFeeTokenDTO), + token = convertToken(metadataDTO.tokenDTO), + tokens = metadataDTO.tokensDTO.map { convertToken(it) }, + type = metadataDTO.type, + rewardSchedule = metadataDTO.rewardSchedule, + cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod), + warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), + rewardClaiming = metadataDTO.rewardClaiming, + defaultValidator = metadataDTO.defaultValidator, + minimumStake = metadataDTO.minimumStake, + supportsMultipleValidators = metadataDTO.supportsMultipleValidators, + revshare = convertEnabled(metadataDTO.revshare), + fee = convertEnabled(metadataDTO.fee), + ) + } + + private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { + return Yield.Metadata.Period( + days = periodDTO.days, + ) + } + + private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled { + return Yield.Metadata.Enabled( + enabled = enabledDTO.enabled, + ) + } + + private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { + return Yield.Validator( + address = validatorDTO.address, + status = validatorDTO.status, + name = validatorDTO.name, + image = validatorDTO.image, + website = validatorDTO.website, + apr = validatorDTO.apr, + commission = validatorDTO.commission, + stakedBalance = validatorDTO.stakedBalance, + votingPower = validatorDTO.votingPower, + preferred = validatorDTO.preferred, + ) + } + + private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType { + return when (rewardTypeDTO) { + YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY + YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR + else -> Yield.RewardType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 7663df6823..092e8cfc62 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking.di import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -21,13 +21,13 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingFeatureToggles: StakingFeatureToggles, - stakingTokenStore: StakingTokensStore, + stakingTokenStore: StakingYieldsStore, dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingFeatureToggles = stakingFeatureToggles, - stakingTokenStore = stakingTokenStore, + stakingYieldsStore = stakingTokenStore, dispatchers = dispatchers, ) } diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index f6a93898bb..80ff7080b9 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -1,9 +1,16 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.staking" +} + + dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index 55c8d1b3fa..b8609b2d1d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -1,6 +1,9 @@ package com.tangem.domain.staking import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.staking.error.StakingTokensError import com.tangem.domain.staking.repositories.StakingRepository /** @@ -10,6 +13,11 @@ class FetchStakingTokensUseCase( private val stakingRepository: StakingRepository, ) { suspend operator fun invoke(): Either { - return Either.catch { stakingRepository.fetchEnabledTokens() } + return either { + catch( + block = { stakingRepository.fetchEnabledYields() }, + catch = { StakingTokensError.DataError(it) }, + ) + } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 18717a31f7..2cc658f615 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -2,15 +2,16 @@ package com.tangem.domain.staking import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency /** - * Use case for getting info about staking availability for certain blockchain. + * Use case for getting info about staking capability in tangem app. */ class GetStakingAvailabilityUseCase( private val stakingRepository: StakingRepository, ) { - operator fun invoke(blockchainNetworkId: String): StakingAvailability { - return stakingRepository.getStakingAvailability(blockchainNetworkId) + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingAvailability { + return stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt new file mode 100644 index 0000000000..d97a592cbd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.error + +sealed class StakingTokensError { + + data class DataError(val cause: Throwable) : StakingTokensError() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt index bf40ee3b58..d7eaa306e0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt @@ -5,4 +5,6 @@ sealed class StakingAvailability { data class Available(val integrationId: String) : StakingAvailability() data object Unavailable : StakingAvailability() + + data object TemporaryDisabled : StakingAvailability() } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt new file mode 100644 index 0000000000..3ef85e77dd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt @@ -0,0 +1,172 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +data class Yield( + val id: String, + val token: Token, + val tokens: List, + val args: Args, + val status: Status, + val apy: BigDecimal, + val rewardRate: Double, + val rewardType: RewardType, + val metadata: Metadata, + val validators: List, + val isAvailable: Boolean, +) { + data class Status( + val enter: Boolean, + val exit: Boolean?, + ) + + data class Args( + val enter: Enter, + val exit: Enter?, + ) { + data class Enter( + val addresses: Addresses, + val args: Map, + ) { + data class Addresses( + val address: AddressArgument, + val additionalAddresses: Map? = null, + ) + } + } + + data class Validator( + val address: String, + val status: String, + val name: String, + val image: String?, + val website: String?, + val apr: Double?, + val commission: Double?, + val stakedBalance: String?, + val votingPower: Double?, + val preferred: Boolean, + ) + + data class Metadata( + val name: String, + val logoUri: String, + val description: String, + val documentation: String?, + val gasFeeToken: Token, + val token: Token, + val tokens: List, + val type: String, + val rewardSchedule: String, + val cooldownPeriod: Period, + val warmupPeriod: Period, + val rewardClaiming: String, + val defaultValidator: String?, + val minimumStake: Int?, + val supportsMultipleValidators: Boolean, + val revshare: Enabled, + val fee: Enabled, + ) { + data class Period( + val days: Int, + ) + + data class Enabled( + val enabled: Boolean, + ) + } + + enum class RewardType { + APY, // compound rate + APR, // simple rate + UNKNOWN, + } +} + +data class Token( + val name: String, + val network: NetworkType, + val symbol: String, + val decimals: Int, + val address: String?, + val coinGeckoId: String?, + val logoURI: String?, + val isPoints: Boolean?, +) { + enum class NetworkType { + AVALANCHE_C, + AVALANCHE_ATOMIC, + AVALANCHE_P, + ARBITRUM, + BINANCE, + CELO, + ETHEREUM, + ETHEREUM_GOERLI, + ETHEREUM_HOLESKY, + FANTOM, + HARMONY, + OPTIMISM, + POLYGON, + GNOSIS, + MOONRIVER, + OKC, + ZKSYNC, + VICTION, + AGORIC, + AKASH, + AXELAR, + BAND_PROTOCOL, + BITSONG, + CANTO, + CHIHUAHUA, + COMDEX, + COREUM, + COSMOS, + CRESCENT, + CRONOS, + CUDOS, + DESMOS, + DYDX, + EVMOS, + FETCH_AI, + GRAVITY_BRIDGE, + INJECTIVE, + IRISNET, + JUNO, + KAVA, + KI_NETWORK, + MARS_PROTOCOL, + NYM, + OKEX_CHAIN, + ONOMY, + OSMOSIS, + PERSISTENCE, + QUICKSILVER, + REGEN, + SECRET, + SENTINEL, + SOMMELIER, + STAFI, + STARGAZE, + STRIDE, + TERITORI, + TGRADE, + UMEE, + POLKADOT, + KUSAMA, + WESTEND, + BINANCEBEACON, + NEAR, + SOLANA, + TEZOS, + TRON, + UNKNOWN, + } +} + +data class AddressArgument( + val required: Boolean, + val network: String? = null, + val minimum: Double? = null, + val maximum: Double? = null, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5e84852870..eafbb514fb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,12 +2,18 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.tokens.model.CryptoCurrency interface StakingRepository { - fun getStakingAvailability(blockchainId: String): StakingAvailability + suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability suspend fun getEntryInfo(integrationId: String): StakingEntryInfo - suspend fun fetchEnabledTokens() + suspend fun fetchEnabledYields() + + fun isStakingSupported(currencyId: String): Boolean } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index de3ef835c5..56b3e441ab 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.domain.staking) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e521d035af..ffef6ff04d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -2,6 +2,8 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,6 +30,7 @@ class GetCryptoCurrencyActionsUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -121,6 +124,17 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Receive(scenario)) } + // staking + if (isStakingAvailable(cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + ), + ) + } + // send val sendUnavailabilityReason = getSendUnavailabilityReason( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -234,6 +248,7 @@ class GetCryptoCurrencyActionsUseCase( } actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return actionsList } @@ -264,4 +279,11 @@ class GetCryptoCurrencyActionsUseCase( private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } + + private suspend fun isStakingAvailable(cryptoCurrency: CryptoCurrency): Boolean { + return stakingRepository.getStakingAvailabilityForActions( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) is StakingAvailability.Available + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 54db16bf92..ead2d06c56 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens.model sealed class ScenarioUnavailabilityReason { data object None : ScenarioUnavailabilityReason() + // staking-specific + data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + // send&sell-specific data class PendingTransaction( val withdrawalScenario: WithdrawalScenario, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 372f1e51d5..a3dbea0959 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -20,6 +20,8 @@ data class TokenActionsState( data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 470cc0ac26..4b3900f2c3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -260,7 +260,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isStakingAvailable = false, + isStakingBlockShown = false, event = consumedEvent(), ) @@ -296,7 +296,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isStakingAvailable = true, + isStakingBlockShown = true, event = consumedEvent(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 26f36f81ad..d1ec291d51 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -27,6 +27,6 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isStakingAvailable: Boolean, + val isStakingBlockShown: Boolean, val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index c5b6078527..d1ef7792c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -91,4 +91,19 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { dimContent = dimContent, ), ) + + /** + * Staking + * + * @property dimContent determines whether the button content will be dimmed + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Stake(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index dbf8a99f3c..afe2413ce6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -38,6 +38,12 @@ internal class TokenDetailsActionButtonsConverter( onLongClick = clickIntents::onCopyAddress, ) } + is TokenActionsState.ActionState.Stake -> { + TokenDetailsActionButton.Stake( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onStakeClick(action.unavailabilityReason) }, + ) + } is TokenActionsState.ActionState.Sell -> { TokenDetailsActionButton.Sell( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 2c29e74ce3..9cc42f3c2d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.tokendetails.presentation.tokendetails.state.* @@ -17,7 +16,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -27,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, - private val stakingAvailabilityProvider: Provider, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -67,7 +64,7 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available, + isStakingBlockShown = false, event = consumedEvent(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index af67c4e57d..cec107a630 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -44,7 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val stakingAvailabilityProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, symbol: String, @@ -55,7 +54,6 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, featureToggles = featureToggles, - stakingAvailabilityProvider = stakingAvailabilityProvider, ) } @@ -211,6 +209,12 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithUpdatedStakingAvailability(stakingAvailability: StakingAvailability): TokenDetailsState { + return currentStateProvider().copy( + isStakingBlockShown = stakingAvailability != StakingAvailability.Unavailable, + ) + } + fun getStateWithStaking(stakingEither: Either): TokenDetailsState { return currentStateProvider().copy( stakingBlockState = stakingStateConverter.convert(stakingEither), @@ -369,6 +373,12 @@ internal class TokenDetailsStateFactory( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index d9eb9ec18d..ddcb006eae 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -135,7 +135,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } - if (state.isStakingAvailable) { + if (state.isStakingBlockShown) { item( key = StakingBlockState::class.java, contentType = StakingBlockState::class.java, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 751b8dcd92..5de4fa2c87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -12,6 +12,8 @@ interface TokenDetailsClickIntents { fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 610f053855..9bcb6d6a55 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -142,9 +142,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - stakingAvailabilityProvider = Provider { - getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value) - }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, @@ -369,7 +366,11 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateStakingInfo() { viewModelScope.launch(dispatchers.io) { - val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value) + val stakingAvailability = getStakingAvailabilityUseCase( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + uiState = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) uiState = stateFactory.getStateWithStaking(stakingInfo) @@ -524,6 +525,10 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { + Timber.e("Not implemented yet") + } + override fun onGenerateExtendedKey() { viewModelScope.launch(dispatchers.main) { val extendedKey = getExtendedPublicKeyForCurrencyUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index afbd667405..19da0ca6a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -89,6 +89,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ), ) + data class Stake( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) + /** * Sell * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 0f6d520acf..58ceb2ac53 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -64,6 +64,7 @@ internal class SetRefreshStateTransformer( is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button + is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index fa7478a428..417ff93ede 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -59,6 +59,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_arrow_down_24 action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.Stake -> { + title = resourceReference(R.string.common_stake) + icon = R.drawable.ic_arrow_down_24 // TODO staking replace icon + action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } + } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) icon = R.drawable.ic_currency_24 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 318fddb6c3..606a27ad3e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -58,6 +59,8 @@ interface WalletCurrencyActionsClickIntents { fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -363,6 +366,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO staking + Timber.e("Not implemented yet") + } + private fun openExplorer() { val userWalletId = stateHolder.getSelectedWalletId() @@ -465,6 +473,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(