Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 18:55:37 +03:00
parent 330ae73357
commit e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions

1
data/staking/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,64 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.data.staking"
}
dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
/** Common modules */
implementation(projects.data.common)
/** Domain modules */
implementation(projects.core.configToggles)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
/** Feature Api modules */
implementation(projects.features.staking.api)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Others dependencies
implementation(deps.androidx.datastore)
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.immutable.collections)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(deps.firebase.crashlytics)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain) {
exclude(module = "joda-time")
}
// endregion
}

View file

@ -0,0 +1,30 @@
package com.tangem.data.staking
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
internal class DefaultStakingActionRepository(
private val stakingActionsStore: StakingActionsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingActionRepository {
override suspend fun store(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
actions: List<StakingAction>,
) {
withContext(dispatchers.io) {
stakingActionsStore.store(userWalletId, cryptoCurrencyId, actions)
}
}
override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<StakingAction>> {
return stakingActionsStore.get(userWalletId, cryptoCurrencyId)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.data.staking
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
internal class DefaultStakingErrorResolver(
private val analyticsEventHandler: AnalyticsEventHandler,
private val stakeKitErrorConverter: StakeKitErrorConverter,
) : StakingErrorResolver {
override fun resolve(throwable: Throwable): StakingError {
val error = if (throwable is ApiResponseError.HttpException) {
stakeKitErrorConverter.convert(throwable.errorBody.orEmpty())
} else {
StakingError.DomainError(throwable.message)
}
when (error) {
is StakingError.StakeKitApiError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.StakeKitApiError(error))
}
is StakingError.StakeKitUnknownError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.StakeKitApiUnknownError(error))
}
is StakingError.DomainError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
}
}
return error
}
}

View file

@ -0,0 +1,739 @@
package com.tangem.data.staking
import android.util.Base64
import arrow.core.getOrElse
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toMigratedCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.YieldBalanceListConverter
import com.tangem.data.staking.converters.YieldConverter
import com.tangem.data.staking.converters.action.ActionStatusConverter
import com.tangem.data.staking.converters.action.EnterActionResponseConverter
import com.tangem.data.staking.converters.transaction.GasEstimateConverter
import com.tangem.data.staking.converters.transaction.StakingTransactionConverter
import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter
import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.TokenConverter
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.stakekit.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val stakingBalanceStore: StakingBalanceStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val stakingFeatureToggles: StakingFeatureToggles,
moshi: Moshi,
) : StakingRepository {
private val transactionStatusConverter = StakingTransactionStatusConverter()
private val transactionTypeConverter = StakingTransactionTypeConverter()
private val actionStatusConverter = ActionStatusConverter()
private val transactionConverter = StakingTransactionConverter(
transactionStatusConverter = transactionStatusConverter,
transactionTypeConverter = transactionTypeConverter,
)
private val enterActionResponseConverter = EnterActionResponseConverter(
actionStatusConverter = actionStatusConverter,
transactionConverter = transactionConverter,
)
private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) }
private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) }
private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) }
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
rawNetworkId.plus(rawCurrencyId)
}
override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? {
return integrationIdMap.getOrDefault(getIntegrationKey(cryptoCurrencyId), null)
}
override suspend fun fetchEnabledYields(refresh: Boolean) {
withContext(dispatchers.io) {
cacheRegistry.invokeOnExpire(
key = YIELDS_STORE_KEY,
skipCache = refresh,
block = {
when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) {
is ApiResponse.Success -> stakingYieldsStore.store(
stakingTokensWithYields.data.data.filter {
it.isAvailable ?: false
},
)
else -> {
stakingYieldsStore.store(emptyList())
throw (stakingTokensWithYields as ApiResponse.Error).cause
}
}
},
)
}
}
override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield {
return withContext(dispatchers.io) {
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available")
val prefetchedYield = findPrefetchedYield(
yields = getEnabledYieldsSync(),
currencyId = rawCurrencyId,
symbol = symbol,
)
prefetchedYield ?: error("Staking is unavailable")
}
}
override suspend fun getYield(yieldId: String): Yield {
return withContext(dispatchers.io) {
getEnabledYieldsSync().find { it.id == yieldId } ?: error("Staking is unavailable")
}
}
override suspend fun getActions(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
networkType: NetworkType,
stakingActionStatus: StakingActionStatus,
): List<StakingAction> {
return withContext(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val networkTypeDto = StakingNetworkTypeConverter.convertBack(networkType)
val networkTypeString = networkTypeDto.extractJsonName()
val actionStatusDTO = actionStatusConverter.convertBack(stakingActionStatus)
val actionStatusString = actionStatusDTO.extractJsonName()
enterActionResponseConverter.convertListIgnoreErrors(
input = stakeKitApi.getActions(
walletAddress = address,
network = networkTypeString,
status = actionStatusString,
).getOrThrow().data,
onError = { Timber.e("Error converting staking actions list: $it") },
)
}
}
private fun NetworkTypeDTO.extractJsonName(): String {
return networkTypeAdapter.toJson(this).replace("\"", "")
}
private fun StakingActionStatusDTO.extractJsonName(): String {
return stakingActionStatusAdapter.toJson(this).replace("\"", "")
}
override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo {
return withContext(dispatchers.io) {
val yield = getYield(cryptoCurrencyId, symbol)
StakingEntryInfo(
apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr),
rewardSchedule = yield.metadata.rewardSchedule,
tokenSymbol = yield.token.symbol,
)
}
}
override fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<StakingAvailability> {
return channelFlow {
if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
if (rawCurrencyId == null) {
send(StakingAvailability.Unavailable)
return@channelFlow
}
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
getEnabledYields()
.distinctUntilChanged()
.onEach { yields ->
if (yields.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null && isSupportedInMobileApp -> {
send(StakingAvailability.Available(prefetchedYield.id))
}
prefetchedYield == null && isSupportedInMobileApp -> {
send(StakingAvailability.TemporaryUnavailable)
}
else -> send(StakingAvailability.Unavailable)
}
}
.launchIn(this)
}
}
private fun checkFeatureToggleEnabled(networkId: Network.ID): Boolean {
return when (Blockchain.fromId(networkId.value)) {
Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled
else -> true
}
}
private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet")
}
return when {
isSolana(cryptoCurrency.network.id.value) -> {
INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId)
}
else -> {
false
}
}
}
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
is StakingActionCommonType.Exit -> stakeKitApi.createExitAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
is StakingActionCommonType.Pending -> stakeKitApi.createPendingAction(
createPendingActionRequestBody(params),
)
}
enterActionResponseConverter.convert(response.getOrThrow())
}
}
override suspend fun estimateGas(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingGasEstimate {
return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
createActionRequestBody(
userWalletId,
network,
params,
),
)
is StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit(
createActionRequestBody(
userWalletId,
network,
params,
),
)
is StakingActionCommonType.Pending -> stakeKitApi.estimateGasOnPending(
createPendingActionRequestBody(params),
)
}
GasEstimateConverter.convert(gasEstimateDTO.getOrThrow())
}
}
override suspend fun constructTransaction(
networkId: String,
fee: Fee,
amount: Amount,
transactionId: String,
): Pair<StakingTransaction, TransactionData.Compiled> {
return withContext(dispatchers.io) {
val transactionResponse = stakeKitApi.constructTransaction(
transactionId = transactionId,
body = ConstructTransactionRequestBody(),
)
val transaction = transactionConverter.convert(transactionResponse.getOrThrow())
val unsignedTransaction = transaction.unsignedTransaction ?: error("No unsigned transaction available")
val transactionData = TransactionData.Compiled(
value = getTransactionDataType(networkId, unsignedTransaction),
fee = fee,
amount = amount,
status = TransactionStatus.Unconfirmed,
)
transaction to transactionData
}
}
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network)
if (integrationId == null || address.isNullOrBlank()) {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
Timber.w(
"IntegrationId or address is null fetching ${cryptoCurrency.name} staking balance",
)
return@invokeOnExpire
}
val requestBody = getBalanceRequestData(address, integrationId)
val result = stakeKitApi.getSingleYieldBalance(
integrationId = requestBody.integrationId,
body = requestBody,
).getOrThrow()
stakingBalanceStore.store(
userWalletId = userWalletId,
integrationId = requestBody.integrationId,
address = address,
item = YieldBalanceWrapperDTO(
balances = result,
integrationId = requestBody.integrationId,
addresses = requestBody.addresses,
),
)
},
)
}
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
launch(dispatchers.io) {
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
if (it != null) {
send(it)
} else {
FirebaseCrashlytics.getInstance()
.log("No yield balance available for currency ${cryptoCurrency.id.value}")
send(YieldBalance.Error(integrationId, address))
}
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
cryptoCurrency,
)
}
}.cancellable()
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId)
?: YieldBalance.Error(integrationId, address)
}
@Suppress("LongMethod")
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (refresh) {
stakingBalanceStore.refresh(
userWalletId = userWalletId,
addressWithIntegrationIdMap = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address -> integrationId to address.value }
}
.toMap(),
)
}
val yieldDTOs = withTimeoutOrNull(YIELDS_WATITING_TIMEOUT) {
runCatching { stakingYieldsStore.get().firstOrNull() }.getOrNull()
}
if (yieldDTOs == null) {
Timber.i("No enabled yields for $userWalletId")
stakingBalanceStore.store(userWalletId, emptySet())
return@withContext
}
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val yields = YieldConverter.convertListIgnoreErrors(
input = yieldDTOs,
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null && yields.any { it.id == integrationId }) {
addresses to integrationId
} else {
null
}
}
.flatMap { (addresses, integrationId) ->
addresses.map { address -> address to integrationId }
}
.map { getBalanceRequestData(it.first.value, it.second) }
.ifEmpty {
stakingBalanceStore.store(userWalletId, emptySet())
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
return@invokeOnExpire
}
val yieldBalances = safeApiCall(
call = {
stakeKitApi
.getMultipleYieldBalances(availableCurrencies)
.bind()
},
onError = {
Timber.e(it, "Unable to fetch yield balances")
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
emptySet()
},
)
stakingBalanceStore.store(userWalletId, yieldBalances)
},
)
}
override fun getMultiYieldBalanceUpdates(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> {
return stakingBalanceStore.get(userWalletId)
.map(YieldBalanceListConverter::convert)
.flowOn(dispatchers.io)
}
override fun getMultiYieldBalanceUpdatesLegacy(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
stakingBalanceStore.get(userWalletId)
.onEach {
val balances = YieldBalanceListConverter.convert(it)
send(balances)
}
.launchIn(scope = this + dispatchers.io)
withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false)
}
}
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
stakingBalanceStore.getSyncOrNull(userWalletId)?.let(YieldBalanceListConverter::convert)
?: YieldBalanceList.Error
}
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.io) {
stakingBalanceStore.getSyncOrNull(userWalletId)
?.let {
it.isNotEmpty() &&
it.any { yieldBalance ->
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
}
}
?: false
}
}
private suspend fun createActionRequestBody(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): ActionRequestBody {
return ActionRequestBody(
integrationId = params.integrationId,
addresses = Address(
address = params.address,
additionalAddresses = createAdditionalAddresses(userWalletId, network, params),
),
args = ActionRequestBodyArgs(
amount = params.amount.toPlainString(),
inputToken = TokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress,
validatorAddresses = listOf(params.validatorAddress), // check on other networks
tronResource = getTronResource(network),
),
)
}
private fun createPendingActionRequestBody(params: ActionParams): PendingActionRequestBody {
return PendingActionRequestBody(
integrationId = params.integrationId,
type = params.type ?: StakingActionType.UNKNOWN,
passthrough = params.passthrough.orEmpty(),
args = ActionRequestBodyArgs(
amount = params.amount.toPlainString(),
validatorAddress = params.validatorAddress,
validatorAddresses = listOf(params.validatorAddress),
),
)
}
private suspend fun createAdditionalAddresses(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Address.AdditionalAddresses? {
val selectedWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
return when (params.token.network) {
NetworkType.COSMOS -> Address.AdditionalAddresses(
cosmosPubKey = Base64.encodeToString(
/* input = */ selectedWallet?.wallet?.publicKey?.blockchainKey?.toCompressedPublicKey(),
/* flags = */ Base64.NO_WRAP,
),
)
else -> null
}
}
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
return when (getIntegrationKey(cryptoCurrency.id)) {
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(),
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(),
-> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
else -> StakingApproval.Empty
}
}
private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data {
return when (Blockchain.fromId(networkId)) {
Blockchain.Solana,
Blockchain.Cosmos,
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
Blockchain.BSC,
Blockchain.Ethereum,
-> TransactionData.Compiled.Data.RawString(unsignedTransaction)
Blockchain.Tron -> {
val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction)
?: error("Failed to parse Tron StakeKit transaction")
TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex)
}
Blockchain.TON -> TransactionData.Compiled.Data.RawString(unsignedTransaction)
else -> error("Unsupported blockchain")
}
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: CryptoCurrency.RawID, symbol: String): Yield? {
return yields.find { yield ->
yield.tokens.any { it.coinGeckoId == currencyId.value && it.symbol == symbol }
}
}
private suspend fun getEnabledYieldsSync(): List<Yield> {
return YieldConverter.convertListIgnoreErrors(
input = stakingYieldsStore.getSync(),
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
}
private fun getEnabledYields(): Flow<List<Yield>> {
return stakingYieldsStore.get().map {
YieldConverter.convertListIgnoreErrors(
input = it,
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
}
}
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
return YieldBalanceRequestBody(
addresses = Address(
address = address,
additionalAddresses = null, // todo fill additional addresses metadata if needed
explorerUrl = "", // todo fill exporer url [REDACTED_JIRA]
),
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
),
integrationId = integrationId,
)
}
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) {
TronResource.ENERGY
} else {
null
}
}
private companion object {
const val YIELDS_STORE_KEY = "yields"
const val TON_INTEGRATION_ID = "ton-ton-tonwhales-pools-staking"
const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking"
const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking"
const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
val YIELDS_WATITING_TIMEOUT = 15.seconds
val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79")
// uncomment items as implementation is ready
val integrationIdMap = mapOf(
Blockchain.TON.run { id + toCoinId() } to TON_INTEGRATION_ID,
Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID,
Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID,
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
// Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
// Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID,
// Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID,
// Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID,
// Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID,
// Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID,
)
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.data.staking
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultStakingTransactionHashRepository(
private val stakeKitApi: StakeKitApi,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingTransactionHashRepository {
override suspend fun submitHash(transactionId: String, transactionHash: String) {
withContext(dispatchers.io) {
stakeKitApi.submitTransactionHash(
transactionId = transactionId,
body = SubmitTransactionHashRequestBody(
hash = transactionHash,
),
)
}
}
override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) {
withContext(dispatchers.io) {
appPreferencesStore.editData { preferences ->
val savedTransactions = preferences.getObjectListOrDefault<UnsubmittedTransactionMetadata>(
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
default = emptyList(),
)
preferences.setObjectList(
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
value = savedTransactions + unsubmittedTransactionMetadata,
)
}
}
}
override suspend fun sendUnsubmittedHashes() {
withContext(dispatchers.io) {
val savedTransactions = appPreferencesStore.getObjectListSync<UnsubmittedTransactionMetadata>(
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
)
savedTransactions.forEach { transaction ->
try {
stakeKitApi.submitTransactionHash(
transactionId = transaction.transactionId,
body = SubmitTransactionHashRequestBody(hash = transaction.transactionHash),
)
appPreferencesStore.editData { mutablePreferences ->
val updatedTransactions =
mutablePreferences.getObjectListOrDefault<UnsubmittedTransactionMetadata>(
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
default = emptyList(),
).filterNot { it.transactionId == transaction.transactionId }
mutablePreferences.setObjectList(
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
value = updatedTransactions,
)
}
} catch (e: Exception) {
val logMessage = buildString {
append("Error while submitting transaction with\n")
append("StakeKit id = ${transaction.transactionId} and\n")
append("transaction hash = ${transaction.transactionHash}")
}
Timber.e(logMessage)
}
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.staking.converters
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.utils.converter.Converter
internal object YieldBalanceListConverter : Converter<Set<YieldBalance>, YieldBalanceList> {
override fun convert(value: Set<YieldBalance>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {
YieldBalanceList.Data(balances = value.toList())
}
}
}

View file

@ -0,0 +1,192 @@
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.YieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.local.token.converter.TokenConverter
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
internal object YieldConverter : Converter<YieldDTO, Yield> {
private val PARTNERS = listOf(
"cosmosvaloper1wrx0x9m9ykdhw9sg04v7uljme53wuj03aa5d4f",
"H2tJNyMHnRF6ahCQLQ1sSycM4FGchymuzyYzUqKEuydk",
)
private val PARTNERS_NAMES = listOf("Meria")
override fun convert(value: YieldDTO): Yield {
return Yield(
id = value.id.asMandatory("id"),
token = TokenConverter.convert(value.token.asMandatory("token")),
tokens = value.tokens.asMandatory("tokens").map(TokenConverter::convert),
args = convertArgs(value.args.asMandatory("args")),
status = convertStatus(value.status.asMandatory("status")),
apy = value.apy.asMandatory("apy"),
rewardRate = value.rewardRate.asMandatory("rewardRate"),
rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")),
metadata = convertMetadata(value.metadata.asMandatory("metadata")),
validators = value.validators.asMandatory("validators")
.asSequence()
.distinctBy { it.address }
.filter { it.status == ValidatorStatusDTO.ACTIVE }
.map { convertValidator(it) }
.sortedByDescending { it.apr }
.sortedByDescending { it.isStrategicPartner }
.toImmutableList(),
isAvailable = value.isAvailable.asMandatory("isAvailable"),
)
}
private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args {
return Yield.Args(
enter = convertEnter(argsDTO.enter.asMandatory("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.asMandatory("addresses")),
args = enterDTO.args.asMandatory("args")
.mapKeys { convertArgType(it.key) }
.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.asMandatory("address")),
additionalAddresses = addressesDTO.additionalAddresses
?.mapKeys { convertArgType(it.key) }
?.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.asMandatory("enter"),
exit = statusDTO.exit,
)
}
private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata {
return Yield.Metadata(
name = metadataDTO.name.asMandatory("name"),
logoUri = metadataDTO.logoUri.asMandatory("logoUri"),
description = metadataDTO.description.asMandatory("description"),
documentation = metadataDTO.documentation,
gasFeeToken = TokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")),
token = TokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")),
tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(TokenConverter::convert),
type = metadataDTO.type.asMandatory("type"),
rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")),
cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) },
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod.asMandatory("warmupPeriod")),
rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")),
defaultValidator = metadataDTO.defaultValidator,
minimumStake = metadataDTO.minimumStake,
supportsMultipleValidators = metadataDTO.supportsMultipleValidators,
revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")),
fee = convertEnabled(metadataDTO.fee.asMandatory("fee")),
)
}
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
return Yield.Metadata.Period(
days = periodDTO.days.asMandatory("days"),
)
}
private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled {
return Yield.Metadata.Enabled(
enabled = enabledDTO.enabled.asMandatory("enabled"),
)
}
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator {
val address = validatorDTO.address.asMandatory("address")
return Yield.Validator(
address = address,
status = convertValidatorStatus(validatorDTO.status.asMandatory("status")),
name = validatorDTO.name.asMandatory("name"),
image = validatorDTO.image,
website = validatorDTO.website,
apr = validatorDTO.apr,
commission = validatorDTO.commission,
stakedBalance = validatorDTO.stakedBalance,
votingPower = validatorDTO.votingPower,
preferred = validatorDTO.preferred.asMandatory("preferred"),
isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name.asMandatory("name")),
)
}
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
}
}
private fun convertValidatorStatus(validatorStatusDTO: ValidatorStatusDTO): ValidatorStatus {
return when (validatorStatusDTO) {
ValidatorStatusDTO.ACTIVE -> ValidatorStatus.ACTIVE
ValidatorStatusDTO.DEACTIVATING -> ValidatorStatus.DEACTIVATING
ValidatorStatusDTO.INACTIVE -> ValidatorStatus.INACTIVE
ValidatorStatusDTO.JAILED -> ValidatorStatus.JAILED
ValidatorStatusDTO.FULL -> ValidatorStatus.FULL
else -> ValidatorStatus.UNKNOWN
}
}
private fun convertRewardSchedule(rewardTypeDTO: RewardScheduleDTO): RewardSchedule {
return when (rewardTypeDTO) {
RewardScheduleDTO.BLOCK -> RewardSchedule.BLOCK
RewardScheduleDTO.WEEK -> RewardSchedule.WEEK
RewardScheduleDTO.HOUR -> RewardSchedule.HOUR
RewardScheduleDTO.DAY -> RewardSchedule.DAY
RewardScheduleDTO.MONTH -> RewardSchedule.MONTH
RewardScheduleDTO.ERA -> RewardSchedule.ERA
RewardScheduleDTO.EPOCH -> RewardSchedule.EPOCH
else -> RewardSchedule.UNKNOWN
}
}
private fun convertRewardClaiming(
rewardClaimingDTO: YieldDTO.MetadataDTO.RewardClaimingDTO,
): Yield.Metadata.RewardClaiming {
return when (rewardClaimingDTO) {
YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO -> Yield.Metadata.RewardClaiming.AUTO
YieldDTO.MetadataDTO.RewardClaimingDTO.MANUAL -> Yield.Metadata.RewardClaiming.MANUAL
else -> Yield.Metadata.RewardClaiming.UNKNOWN
}
}
private fun convertArgType(value: String): Yield.Args.ArgType {
return when (value) {
"address" -> Yield.Args.ArgType.ADDRESS
"amount" -> Yield.Args.ArgType.AMOUNT
else -> Yield.Args.ArgType.UNKNOWN
}
}
private fun isStrategicPartner(validatorAddress: String?, validatorName: String): Boolean {
return PARTNERS.any { it == validatorAddress } || PARTNERS_NAMES.any { it.equals(validatorName, true) }
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.data.staking.converters.action
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.utils.converter.TwoWayConverter
class ActionStatusConverter : TwoWayConverter<StakingActionStatusDTO, StakingActionStatus> {
override fun convert(value: StakingActionStatusDTO): StakingActionStatus {
return when (value) {
StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED
StakingActionStatusDTO.CREATED -> StakingActionStatus.CREATED
StakingActionStatusDTO.WAITING_FOR_NEXT -> StakingActionStatus.WAITING_FOR_NEXT
StakingActionStatusDTO.PROCESSING -> StakingActionStatus.PROCESSING
StakingActionStatusDTO.FAILED -> StakingActionStatus.FAILED
StakingActionStatusDTO.SUCCESS -> StakingActionStatus.SUCCESS
else -> StakingActionStatus.UNKNOWN
}
}
override fun convertBack(value: StakingActionStatus): StakingActionStatusDTO {
return when (value) {
StakingActionStatus.CANCELED -> StakingActionStatusDTO.CANCELED
StakingActionStatus.CREATED -> StakingActionStatusDTO.CREATED
StakingActionStatus.WAITING_FOR_NEXT -> StakingActionStatusDTO.WAITING_FOR_NEXT
StakingActionStatus.PROCESSING -> StakingActionStatusDTO.PROCESSING
StakingActionStatus.FAILED -> StakingActionStatusDTO.FAILED
StakingActionStatus.SUCCESS -> StakingActionStatusDTO.SUCCESS
else -> StakingActionStatusDTO.UNKNOWN
}
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.staking.converters.action
import com.tangem.data.staking.converters.transaction.StakingTransactionConverter
import com.tangem.datasource.api.stakekit.models.response.ActionDTO
import com.tangem.datasource.local.token.converter.StakingActionTypeConverter
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.utils.converter.Converter
class EnterActionResponseConverter(
private val actionStatusConverter: ActionStatusConverter,
private val transactionConverter: StakingTransactionConverter,
) : Converter<ActionDTO, StakingAction> {
override fun convert(value: ActionDTO): StakingAction {
return StakingAction(
id = value.id,
integrationId = value.integrationId,
status = actionStatusConverter.convert(value.status),
type = StakingActionTypeConverter.convert(value.type),
currentStepIndex = value.currentStepIndex,
amount = value.amount,
validatorAddress = value.validatorAddress,
validatorAddresses = value.validatorAddresses,
transactions = value.transactions?.map(transactionConverter::convert),
createdAt = value.createdAt,
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.data.staking.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.converter.Converter
internal class StakeKitErrorConverter(
private val jsonAdapter: JsonAdapter<StakeKitErrorResponse>,
) : Converter<String, StakingError> {
override fun convert(value: String): StakingError {
return try {
val stakeKitErrorResponse = jsonAdapter.fromJson(value)
?: return StakingError.StakeKitUnknownError(value)
return StakingError.StakeKitApiError(
message = stakeKitErrorResponse.message,
code = stakeKitErrorResponse.code,
methodName = stakeKitErrorResponse.path,
)
} catch (e: Exception) {
StakingError.StakeKitUnknownError(value)
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.data.staking.converters.transaction
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
import com.tangem.datasource.local.token.converter.TokenConverter
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.utils.converter.Converter
internal object GasEstimateConverter : Converter<StakingGasEstimateDTO, StakingGasEstimate> {
override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate {
return StakingGasEstimate(
amount = value.amount,
token = TokenConverter.convert(value.token),
gasLimit = value.gasLimit,
)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.data.staking.converters.transaction
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.utils.converter.Converter
class StakingTransactionConverter(
private val transactionStatusConverter: StakingTransactionStatusConverter,
private val transactionTypeConverter: StakingTransactionTypeConverter,
) : Converter<StakingTransactionDTO, StakingTransaction> {
override fun convert(value: StakingTransactionDTO): StakingTransaction {
return StakingTransaction(
id = value.id,
network = StakingNetworkTypeConverter.convert(value.network),
status = transactionStatusConverter.convert(value.status),
type = transactionTypeConverter.convert(value.type),
hash = value.hash,
signedTransaction = value.signedTransaction,
unsignedTransaction = value.unsignedTransaction,
stepIndex = value.stepIndex,
error = value.error,
gasEstimate = value.gasEstimate?.let(GasEstimateConverter::convert),
stakeId = value.stakeId,
explorerUrl = value.explorerUrl,
ledgerHwAppId = value.ledgerHwAppId,
isMessage = value.isMessage,
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.staking.converters.transaction
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
import com.tangem.utils.converter.Converter
class StakingTransactionStatusConverter : Converter<StakingTransactionStatusDTO, StakingTransactionStatus> {
override fun convert(value: StakingTransactionStatusDTO): StakingTransactionStatus {
return when (value) {
StakingTransactionStatusDTO.NOT_FOUND -> StakingTransactionStatus.NOT_FOUND
StakingTransactionStatusDTO.CREATED -> StakingTransactionStatus.CREATED
StakingTransactionStatusDTO.BLOCKED -> StakingTransactionStatus.BLOCKED
StakingTransactionStatusDTO.WAITING_FOR_SIGNATURE -> StakingTransactionStatus.WAITING_FOR_SIGNATURE
StakingTransactionStatusDTO.SIGNED -> StakingTransactionStatus.SIGNED
StakingTransactionStatusDTO.BROADCASTED -> StakingTransactionStatus.BROADCASTED
StakingTransactionStatusDTO.PENDING -> StakingTransactionStatus.PENDING
StakingTransactionStatusDTO.CONFIRMED -> StakingTransactionStatus.CONFIRMED
StakingTransactionStatusDTO.FAILED -> StakingTransactionStatus.FAILED
StakingTransactionStatusDTO.SKIPPED -> StakingTransactionStatus.SKIPPED
StakingTransactionStatusDTO.UNKNOWN -> StakingTransactionStatus.UNKNOWN
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.data.staking.converters.transaction
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.utils.converter.Converter
@Suppress("CyclomaticComplexMethod")
class StakingTransactionTypeConverter : Converter<StakingTransactionTypeDTO, StakingTransactionType> {
override fun convert(value: StakingTransactionTypeDTO): StakingTransactionType {
return when (value) {
StakingTransactionTypeDTO.SWAP -> StakingTransactionType.SWAP
StakingTransactionTypeDTO.DEPOSIT -> StakingTransactionType.DEPOSIT
StakingTransactionTypeDTO.APPROVAL -> StakingTransactionType.APPROVAL
StakingTransactionTypeDTO.STAKE -> StakingTransactionType.STAKE
StakingTransactionTypeDTO.CLAIM_UNSTAKED -> StakingTransactionType.CLAIM_UNSTAKED
StakingTransactionTypeDTO.CLAIM_REWARDS -> StakingTransactionType.CLAIM_REWARDS
StakingTransactionTypeDTO.RESTAKE_REWARDS -> StakingTransactionType.RESTAKE_REWARDS
StakingTransactionTypeDTO.UNSTAKE -> StakingTransactionType.UNSTAKE
StakingTransactionTypeDTO.SPLIT -> StakingTransactionType.SPLIT
StakingTransactionTypeDTO.MERGE -> StakingTransactionType.MERGE
StakingTransactionTypeDTO.LOCK -> StakingTransactionType.LOCK
StakingTransactionTypeDTO.UNLOCK -> StakingTransactionType.UNLOCK
StakingTransactionTypeDTO.SUPPLY -> StakingTransactionType.SUPPLY
StakingTransactionTypeDTO.BRIDGE -> StakingTransactionType.BRIDGE
StakingTransactionTypeDTO.VOTE -> StakingTransactionType.VOTE
StakingTransactionTypeDTO.REVOKE -> StakingTransactionType.REVOKE
StakingTransactionTypeDTO.RESTAKE -> StakingTransactionType.RESTAKE
StakingTransactionTypeDTO.REBOND -> StakingTransactionType.REBOND
StakingTransactionTypeDTO.WITHDRAW -> StakingTransactionType.WITHDRAW
StakingTransactionTypeDTO.CREATE_ACCOUNT -> StakingTransactionType.CREATE_ACCOUNT
StakingTransactionTypeDTO.REVEAL -> StakingTransactionType.REVEAL
StakingTransactionTypeDTO.MIGRATE -> StakingTransactionType.MIGRATE
StakingTransactionTypeDTO.UTXO_P_TO_C_IMPORT -> StakingTransactionType.UTXO_P_TO_C_IMPORT
StakingTransactionTypeDTO.UTXO_C_TO_P_IMPORT -> StakingTransactionType.UTXO_C_TO_P_IMPORT
StakingTransactionTypeDTO.UNFREEZE_LEGACY -> StakingTransactionType.UNFREEZE_LEGACY
StakingTransactionTypeDTO.UNFREEZE_LEGACY_BANDWIDTH -> StakingTransactionType.UNFREEZE_LEGACY_BANDWIDTH
StakingTransactionTypeDTO.UNFREEZE_LEGACY_ENERGY -> StakingTransactionType.UNFREEZE_LEGACY_ENERGY
StakingTransactionTypeDTO.UNFREEZE_BANDWIDTH -> StakingTransactionType.UNFREEZE_BANDWIDTH
StakingTransactionTypeDTO.UNFREEZE_ENERGY -> StakingTransactionType.UNFREEZE_ENERGY
StakingTransactionTypeDTO.FREEZE_BANDWIDTH -> StakingTransactionType.FREEZE_BANDWIDTH
StakingTransactionTypeDTO.FREEZE_ENERGY -> StakingTransactionType.FREEZE_ENERGY
StakingTransactionTypeDTO.UNDELEGATE_BANDWIDTH -> StakingTransactionType.UNDELEGATE_BANDWIDTH
StakingTransactionTypeDTO.UNDELEGATE_ENERGY -> StakingTransactionType.UNDELEGATE_ENERGY
StakingTransactionTypeDTO.P2P_NODE_REQUEST -> StakingTransactionType.P2P_NODE_REQUEST
StakingTransactionTypeDTO.LUGANODES_PROVISION -> StakingTransactionType.LUGANODES_PROVISION
StakingTransactionTypeDTO.LUGANODES_EXIT_REQUEST -> StakingTransactionType.LUGANODES_EXIT_REQUEST
StakingTransactionTypeDTO.INFSTONES_PROVISION -> StakingTransactionType.INFSTONES_PROVISION
StakingTransactionTypeDTO.INFSTONES_EXIT_REQUEST -> StakingTransactionType.INFSTONES_EXIT_REQUEST
StakingTransactionTypeDTO.INFSTONES_CLAIM_REQUEST -> StakingTransactionType.INFSTONES_CLAIM_REQUEST
StakingTransactionTypeDTO.UNKNOWN -> StakingTransactionType.UNKNOWN
}
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.data.staking.di
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.DefaultStakingActionRepository
import com.tangem.data.staking.DefaultStakingErrorResolver
import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.data.staking.DefaultStakingTransactionHashRepository
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 StakingDataModule {
@Provides
@Singleton
fun provideStakingRepository(
stakeKitApi: StakeKitApi,
stakingYieldsStore: StakingYieldsStore,
stakingBalanceStore: StakingBalanceStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
getUserWalletUseCase: GetUserWalletUseCase,
stakingFeatureToggles: StakingFeatureToggles,
@NetworkMoshi moshi: Moshi,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
stakingYieldsStore = stakingYieldsStore,
stakingBalanceStore = stakingBalanceStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
walletManagersFacade = walletManagersFacade,
getUserWalletUseCase = getUserWalletUseCase,
stakingFeatureToggles = stakingFeatureToggles,
moshi = moshi,
)
}
@Provides
@Singleton
fun provideStakingTransactionHashRepository(
stakeKitApi: StakeKitApi,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): StakingTransactionHashRepository {
return DefaultStakingTransactionHashRepository(
stakeKitApi = stakeKitApi,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideStakingActionRepository(
stakingActionsStore: StakingActionsStore,
dispatchers: CoroutineDispatcherProvider,
): StakingActionRepository {
return DefaultStakingActionRepository(
stakingActionsStore = stakingActionsStore,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
internal fun provideStakingErrorResolver(
@NetworkMoshi moshi: Moshi,
analyticsEventHandler: AnalyticsEventHandler,
): StakingErrorResolver {
val jsonAdapter = moshi.adapter(StakeKitErrorResponse::class.java)
return DefaultStakingErrorResolver(
stakeKitErrorConverter = StakeKitErrorConverter(jsonAdapter),
analyticsEventHandler = analyticsEventHandler,
)
}
@Provides
@Singleton
internal fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles {
return DefaultStakingFeatureToggles(featureTogglesManager)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.data.staking.toggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.staking.toggles.StakingFeatureToggles
internal class DefaultStakingFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : StakingFeatureToggles {
override val isTonStakingEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED")
}