Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-06 11:05:49 +05:00
commit 18b6ef368e
84 changed files with 1191 additions and 452 deletions

View file

@ -14,6 +14,11 @@ android {
testOptions {
animationsDisabled = true
}
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}
configurations.all {

View file

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

View file

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

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

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

View file

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

View file

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

View file

@ -2,12 +2,36 @@ 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.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.
*/
object UnknownEnumMoshiAdapter {
fun <T : Enum<T>> create(enumType: Class<T>, defaultValue: T): JsonAdapter<T> =
EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue)
@Suppress("UNCHECKED_CAST")
fun <T : Enum<T>> create(enumType: Class<out Enum<*>>, defaultValue: Enum<*>): JsonAdapter<out Enum<*>> {
return EnumJsonAdapter.create(enumType as Class<T>).withUnknownFallback(defaultValue as T)
}
}
fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
val map = mapOf(
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 {
map.forEach { entry ->
val enumClass = entry.key
val unknownValue = entry.value
add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue))
}
}
}

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
@ -18,31 +18,31 @@ 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<EnabledYieldsResponse>
@GET("yields/{integrationId}")
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,13 +4,13 @@ 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")
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,
)

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,
@ -47,4 +47,6 @@ enum class StakingActionType {
@Json(name = "MIGRATE")
MIGRATE,
UNKNOWN,
}

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")
@ -61,12 +61,14 @@ data class YieldBalanceWrapper(
@Json(name = "unlocking")
UNLOCKING,
UNKNOWN,
}
@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

@ -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<Balance>,
@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<PendingAction>,
@Json(name = "token")
val token: Token,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>?,
@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<Nft>?,
@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<String>,
)
}
}
@JsonClass(generateAdapter = true)
data class Required(
@Json(name = "required")
val required: Boolean,
)
}
}

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,28 +119,30 @@ 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, // auto
APY, // compound rate
@Json(name = "apr")
APR, // manual
APR, // simple rate,
UNKNOWN,
}
}

View file

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

View file

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

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
import com.tangem.datasource.local.token.StakingYieldsStore
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(): StakingYieldsStore {
return DefaultStakingYieldsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -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<List<Asset>>,
) : AssetsStore {
) : ExpressAssetsStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)

View file

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

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

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

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.token
import com.tangem.domain.staking.model.StakingTokenWithYield
interface StakingTokensStore {
suspend fun getSyncOrNull(): List<StakingTokenWithYield>?
suspend fun store(items: List<StakingTokenWithYield>)
}

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

@ -35,5 +35,6 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader(
"X-API-KEY" to { stakeKitAuthProvider.getApiKey() },
"accept" to { "application/json" },
)
}

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

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

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,63 +1,107 @@
package com.tangem.data.staking
import com.tangem.blockchain.common.Blockchain
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.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
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingFeatureToggles: StakingFeatureToggles,
private val stakingYieldsStore: StakingYieldsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingRepository {
override fun getStakingAvailability(blockchainId: String): StakingAvailability {
private val yieldConverter = YieldConverter()
override suspend fun getStakingAvailabilityForActions(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
): StakingAvailability {
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable
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)
when {
prefetchedYield != null && isSupported -> {
StakingAvailability.Available(prefetchedYield.id)
}
prefetchedYield == null && isSupported -> {
StakingAvailability.TemporaryDisabled
}
else -> StakingAvailability.Unavailable
}
}
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
return yields
.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
}
override fun isStakingSupported(currencyId: String): Boolean {
return integrationIds.contains(currencyId)
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
return withContext(dispatchers.io) {
val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow()
return StakingEntryInfo(
interestRate = yield.apy,
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
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 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 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,
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

@ -0,0 +1,22 @@
package com.tangem.data.staking.converters
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<TokenWithYieldDTO, StakingTokenWithYield> {
override fun convert(value: TokenWithYieldDTO): 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,
)
}
}

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,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.StakingYieldsStore
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: StakingYieldsStore,
dispatchers: CoroutineDispatcherProvider,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingFeatureToggles = stakingFeatureToggles,
stakingYieldsStore = stakingTokenStore,
dispatchers = dispatchers,
)
}
}

View file

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

View file

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

View file

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

View file

@ -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<PagingData<VisaTxHistoryItem>> {
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<Address> {
@ -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"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

@ -0,0 +1,23 @@
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
/**
* Use case for getting enabled tokens
*/
class FetchStakingTokensUseCase(
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
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,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?,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.staking.model
data class StakingTokenWithYield(
val token: StakingToken,
val availableYieldIds: List<String>,
)

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,10 +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 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

@ -41,7 +41,7 @@ import java.math.BigDecimal
@Suppress("LongParameterList", "LargeClass")
internal class SendNotificationFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val currentStateProvider: Provider<SendUiState>,
private val userWalletProvider: Provider<UserWallet>,
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) {

View file

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

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

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

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

@ -19,24 +19,25 @@ internal class SetBalancesAndLimitsTransformer(
private val userWallet: UserWallet,
private val maybeVisaCurrency: Either<Throwable, VisaCurrency>,
private val clickIntents: WalletClickIntents,
) : WalletStateTransformer(userWallet.walletId) {
) : TypedWalletStateTransformer<WalletState.Visa.Content>(
userWalletId = userWallet.walletId,
targetStateClass = WalletState.Visa.Content::class,
) {
override fun transform(prevState: WalletState): WalletState {
return prevState.transformWhenInState<WalletState.Visa.Content> { 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(

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

@ -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<S : WalletState>(
userWalletId: UserWalletId,
protected val targetStateClass: KClass<S>,
) : 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
}
}
}

View file

@ -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 <reified S : WalletState> 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
}
}

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

@ -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<WalletScreenState> = stateHolder.uiState

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(

View file

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

View file

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

View file

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

View file

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

View file

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