Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-05 17:23:23 +02:00
parent 27a864a0a9
commit 0e29df4ce9
45 changed files with 731 additions and 137 deletions

View file

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

View file

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

View file

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

View file

@ -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<Yield>
): ApiResponse<YieldDTO>
@GET("yields/balances")
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapper>>
): ApiResponse<List<YieldBalanceWrapperDTO>>
@GET("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(
@Path("integrationId") integrationId: String,
@Body body: YieldBalanceRequestBody,
): ApiResponse<YieldBalanceWrapper>
): ApiResponse<YieldBalanceWrapperDTO>
@GET("tokens")
suspend fun getTokens(): ApiResponse<List<TokenWithYield>>
suspend fun getTokens(): ApiResponse<List<TokenWithYieldDTO>>
}

View file

@ -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<Yield>,
val data: List<YieldDTO>,
@Json(name = "hasNextPage")
val hasNextPage: Boolean,
@Json(name = "limit")

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Balance>,
val balances: List<BalanceDTO>,
@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<PendingAction>,
@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")

View file

@ -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<Token>,
val tokens: List<TokenDTO>,
@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<Validator>,
val validators: List<ValidatorDTO>,
@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<String, AddressArgument>,
val args: Map<String, AddressArgumentDTO>,
) {
@JsonClass(generateAdapter = true)
data class Addresses(
@Json(name = "address")
val address: AddressArgument,
val address: AddressArgumentDTO,
@Json(name = "additionalAddresses")
val additionalAddresses: Map<String, AddressArgument>? = null,
val additionalAddresses: Map<String, AddressArgumentDTO>? = 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<Token>,
val tokensDTO: List<TokenDTO>,
@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")

View file

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

View file

@ -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<List<YieldDTO>>,
) : StakingYieldsStore {
override suspend fun getSyncOrNull(): List<YieldDTO>? {
return dataStore.getSyncOrNull(STAKING_YIELDS_KEY)
}
override suspend fun store(items: List<YieldDTO>) {
dataStore.store(STAKING_YIELDS_KEY, items)
}
companion object {
private const val STAKING_YIELDS_KEY = "STAKING_YIELDS_KEY"
}
}

View file

@ -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<YieldDTO>?
suspend fun store(items: List<YieldDTO>)
}

View file

@ -117,6 +117,7 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_save">Сохранить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -128,6 +129,7 @@
<string name="common_share">Поделиться</string>
<string name="common_sign">Подписать</string>
<string name="common_sign_and_send">Подписать и отправить</string>
<string name="common_staking">Стейкинг</string>
<string name="common_start">Начать</string>
<string name="common_submit">Отправить</string>
<string name="common_success">Успешно</string>
@ -169,6 +171,7 @@
<string name="custom_token_validation_error_not_found">Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_description">Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_title">Токены могут быть созданы кем угодно</string>
<string name="details_buy_wallet">Купить кошелек Tangem</string>
<string name="details_chat">Чат</string>
<string name="details_manage_security_access_code">Код доступа</string>
<string name="details_manage_security_access_code_description">Перед сканированием карты вам нужно будет ввести правильный код доступа.</string>
@ -186,6 +189,7 @@
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
<string name="details_row_title_issuer">Эмитент</string>
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_send_feedback">Отправить отзыв</string>
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
@ -538,6 +542,8 @@
<string name="settings_forget_wallet">Забыть кошелек</string>
<string name="settings_forget_wallet_footer">Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова.</string>
<string name="settings_wallet_name_title">Имя</string>
<string name="staking_details_available">Доступно</string>
<string name="staking_details_title">Стейкинг %s</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
<string name="story_awe_title">Революционный аппаратный кошелек</string>
<string name="story_backup_description">До трех карт с одним кошельком</string>
@ -579,9 +585,10 @@
<string name="token_button_unavailability_reason_empty_balance_sell">У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства.</string>
<string name="token_button_unavailability_reason_empty_balance_send">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
<string name="token_button_unavailability_reason_not_exchangeable">В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Отправка средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_staking_unavailable">В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
@ -668,7 +675,7 @@
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string>
<string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string>
<string name="warning_backup_errors_title">Ошибка активации</string>
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_title">Отключение сети BNB Beacon Chain</string>
<string name="warning_button_could_be_better">Можно лучше</string>
<string name="warning_button_like_it">Нравится</string>

View file

@ -116,6 +116,7 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_save">Save</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -127,6 +128,8 @@
<string name="common_share">Share</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_stake">Stake</string>
<string name="common_staking">Staking</string>
<string name="common_start">Start</string>
<string name="common_submit">Submit</string>
<string name="common_success">Success</string>
@ -168,6 +171,7 @@
<string name="custom_token_validation_error_not_found">Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.</string>
<string name="custom_token_validation_error_not_found_description">Be aware of adding scam tokens, they can cost nothing</string>
<string name="custom_token_validation_error_not_found_title">Note that tokens can be created by anyone</string>
<string name="details_buy_wallet">Buy Tangem Wallet</string>
<string name="details_chat">Chat</string>
<string name="details_manage_security_access_code">Access code</string>
<string name="details_manage_security_access_code_description">You will have to submit the correct access code before scanning the card</string>
@ -185,6 +189,7 @@
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_send_feedback">Send feedback</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of service</string>
@ -531,6 +536,18 @@
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_details_available">Available</string>
<string name="staking_details_average_reward_rate">Average Reward Rate</string>
<string name="staking_details_estimated_profit">%s est. profit</string>
<string name="staking_details_minimum_requirement">Minimum Requirement</string>
<string name="staking_details_no_rewards_to_claim">No rewards to claim</string>
<string name="staking_details_on_stake">On stake</string>
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_rewards">Rewards</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to 3 physical cards to one wallet</string>
@ -575,6 +592,7 @@
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Sending funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Staking %s is not available at the moment. Please check our updates.</string>
<string name="token_details_generate_xpub">Generate XPUB</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
@ -661,7 +679,7 @@
<string name="warning_access_denied_message">Use %s or scan a card to unlock access to your wallet</string>
<string name="warning_backup_errors_message">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.</string>
<string name="warning_backup_errors_title">Activation error</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain will shut down</string>
<string name="warning_button_could_be_better">Could be better</string>
<string name="warning_button_like_it">Like it</string>

View file

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

View file

@ -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<Yield>, 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<Yield>? {
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(),
)
}
}

View file

@ -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<TokenWithYield, StakingTokenWithYield> {
class StakingTokenConverter : Converter<TokenWithYieldDTO, StakingTokenWithYield> {
override fun convert(value: TokenWithYield): StakingTokenWithYield {
override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield {
return StakingTokenWithYield(
token = StakingToken(
name = value.token.name,

View file

@ -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<YieldDTO, Yield> {
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
}
}
}

View file

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

View file

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

View file

@ -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<Throwable, Unit> {
return Either.catch { stakingRepository.fetchEnabledTokens() }
return either {
catch(
block = { stakingRepository.fetchEnabledYields() },
catch = { StakingTokensError.DataError(it) },
)
}
}
}

View file

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

View file

@ -0,0 +1,6 @@
package com.tangem.domain.staking.error
sealed class StakingTokensError {
data class DataError(val cause: Throwable) : StakingTokensError()
}

View file

@ -5,4 +5,6 @@ sealed class StakingAvailability {
data class Available(val integrationId: String) : StakingAvailability()
data object Unavailable : StakingAvailability()
data object TemporaryDisabled : StakingAvailability()
}

View file

@ -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<Token>,
val args: Args,
val status: Status,
val apy: BigDecimal,
val rewardRate: Double,
val rewardType: RewardType,
val metadata: Metadata,
val validators: List<Validator>,
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<String, AddressArgument>,
) {
data class Addresses(
val address: AddressArgument,
val additionalAddresses: Map<String, AddressArgument>? = 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<Token>,
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,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<StakingAvailability>,
) : Converter<CryptoCurrency, TokenDetailsState> {
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(),
)
}

View file

@ -44,7 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val stakingAvailabilityProvider: Provider<StakingAvailability>,
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<Throwable, StakingEntryInfo>): 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(

View file

@ -135,7 +135,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
)
}
if (state.isStakingAvailable) {
if (state.isStakingBlockShown) {
item(
key = StakingBlockState::class.java,
contentType = StakingBlockState::class.java,

View file

@ -12,6 +12,8 @@ interface TokenDetailsClickIntents {
fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason)

View file

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

View file

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

View file

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

View file

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

View file

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