Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-24 13:48:52 +02:00
parent aa6c1c95f2
commit df32ea7ab6
32 changed files with 704 additions and 637 deletions

View file

@ -12,10 +12,15 @@ import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.ethpool.*
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -192,7 +197,12 @@ internal class DefaultP2PEthPoolRepository(
period: Int?,
): Either<StakingError, List<P2PEthPoolReward>> = either {
withContext(dispatchers.io) {
val response = p2pApi.getRewards(network.value, delegatorAddress, vaultAddress, period)
val response = p2pApi.getRewards(
network = network.value,
delegatorAddress = delegatorAddress,
vaultAddress = vaultAddress,
period = period,
)
when (response) {
is ApiResponse.Success -> {
val data = response.data
@ -206,4 +216,49 @@ internal class DefaultP2PEthPoolRepository(
}
}
}
override fun getStakingAvailability(): Flow<StakingAvailability> {
return getVaultsFlow()
.distinctUntilChanged()
.map { vaults ->
if (vaults.isEmpty()) {
return@map StakingAvailability.TemporaryUnavailable
}
val vault = findPublicVault(vaults = vaults)
if (vault != null) {
StakingAvailability.Available(StakingOption.P2P(vault))
} else {
StakingAvailability.TemporaryUnavailable
}
}
}
override suspend fun getStakingAvailabilitySync(): StakingAvailability {
val vaults = getVaultsSync()
if (vaults.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
val vault = findPublicVault(vaults = vaults)
return if (vault != null) {
StakingAvailability.Available(StakingOption.P2P(vault))
} else {
StakingAvailability.TemporaryUnavailable
}
}
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
return p2pEthPoolVaultsStore.getSync()
}
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
private fun findPublicVault(vaults: List<P2PEthPoolVault>): P2PEthPoolVault? {
return vaults.firstOrNull { vault -> !vault.isPrivate }
}
}

View file

@ -4,15 +4,15 @@ import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakeKitActionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
internal class DefaultStakingActionRepository(
internal class DefaultStakeKitActionRepository(
private val stakingActionsStore: StakingActionsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingActionRepository {
) : StakeKitActionRepository {
override suspend fun store(
userWalletId: UserWalletId,

View file

@ -0,0 +1,421 @@
package com.tangem.data.staking
import android.util.Base64
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.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
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.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
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.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.YieldTokenConverter
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.stakekit.Yield
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.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.StakeKitRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakeKitRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val stakingFeatureToggles: StakingFeatureToggles,
moshi: Moshi,
) : StakeKitRepository {
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 suspend fun fetchYields() {
withContext(dispatchers.io) {
val yieldsResponses = getAvailableStakeKitIntegrationsIds().map {
async { it.getYieldRequest() }
}.awaitAll()
val yields = yieldsResponses.flatMap { response ->
when (response) {
is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true }
is ApiResponse.Error -> {
Timber.e("Error fetching enabled yields: ${response.cause}")
emptyList()
}
}
}
stakingYieldsStore.store(yields)
}
}
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 suspend fun StakingIntegrationID.StakeKit.getYieldRequest(): ApiResponse<EnabledYieldsResponse> {
return when (this) {
is StakingIntegrationID.StakeKit.Coin -> stakeKitApi.getEnabledYields(
preferredValidatorsOnly = false,
network = networkId,
)
is StakingIntegrationID.StakeKit.EthereumToken -> stakeKitApi.getEnabledYields(
preferredValidatorsOnly = false,
yieldId = value,
network = networkId,
)
}
}
private fun getAvailableStakeKitIntegrationsIds(): List<StakingIntegrationID.StakeKit> {
return StakingIntegrationID.StakeKit.entries.filterNot {
it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled
}
}
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(
tokenSymbol = yield.token.symbol,
)
}
}
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
is 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) {
is 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
}
}
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 = YieldTokenConverter.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
}
}
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,
Blockchain.TON,
Blockchain.Cardano,
-> 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)
}
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") },
)
}
override fun getEnabledYields(): Flow<List<Yield>> {
return stakingYieldsStore.get().map { yields ->
YieldConverter.convertListIgnoreErrors(
input = yields,
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)
}
}
override fun getStakingAvailability(
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): Flow<StakingAvailability> {
return getEnabledYields()
.distinctUntilChanged()
.map { yields ->
if (yields.isEmpty()) {
return@map StakingAvailability.TemporaryUnavailable
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = symbol,
)
if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}
}
}
override suspend fun getStakingAvailabilitySync(
rawCurrencyId: CryptoCurrency.RawID,
symbol: String,
): StakingAvailability {
val yields = getEnabledYieldsSync()
if (yields.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = symbol,
)
return if (prefetchedYield != null) {
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
} else {
StakingAvailability.TemporaryUnavailable
}
}
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) {
TronResource.ENERGY
} else {
null
}
}
}

View file

@ -6,16 +6,16 @@ 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.domain.staking.repositories.StakeKitTransactionHashRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultStakingTransactionHashRepository(
internal class DefaultStakeKitTransactionHashRepository(
private val stakeKitApi: StakeKitApi,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : StakingTransactionHashRepository {
) : StakeKitTransactionHashRepository {
override suspend fun submitHash(transactionId: String, transactionHash: String) {
withContext(dispatchers.io) {

View file

@ -1,204 +1,38 @@
package com.tangem.data.staking
import android.util.Base64
import arrow.core.getOrElse
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.toBlockchain
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
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.data.staking.store.YieldsBalancesStore
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.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
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.P2PEthPoolVaultsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter
import com.tangem.datasource.local.token.converter.YieldTokenConverter
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import com.tangem.domain.staking.model.stakekit.Yield
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.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.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.lib.crypto.BlockchainUtils.isCardano
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val stakingYieldsStore: StakingYieldsStore,
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
private val stakeKitRepository: StakeKitRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingBalanceStoreV2: YieldsBalancesStore,
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 suspend fun fetchYields() {
withContext(dispatchers.io) {
val yieldsResponses = getAvailableStakeKitIntegrationsIds().map {
async { it.getYieldRequest() }
}.awaitAll()
val yields = yieldsResponses.flatMap { response ->
when (response) {
is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true }
else -> {
Timber.e("Error fetching enabled yields: ${(response as? ApiResponse.Error)?.cause}")
emptyList()
}
}
}
stakingYieldsStore.store(yields)
}
}
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 suspend fun StakingIntegrationID.StakeKit.getYieldRequest(): ApiResponse<EnabledYieldsResponse> {
return when (this) {
is StakingIntegrationID.StakeKit.Coin -> stakeKitApi.getEnabledYields(
preferredValidatorsOnly = false,
network = networkId,
)
is StakingIntegrationID.StakeKit.EthereumToken -> stakeKitApi.getEnabledYields(
preferredValidatorsOnly = false,
yieldId = value,
network = networkId,
)
}
}
private fun getAvailableStakeKitIntegrationsIds(): List<StakingIntegrationID.StakeKit> {
return StakingIntegrationID.StakeKit.entries.filterNot {
it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled
}
}
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(
tokenSymbol = yield.token.symbol,
)
}
}
override fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
@ -222,14 +56,16 @@ internal class DefaultStakingRepository(
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
when (stakingIntegration) {
is StakingIntegrationID.P2P -> subscribeToP2PStakingAvailability()
is StakingIntegrationID.StakeKit -> subscribeToStakeKitStakingAvailability(
val availabilityFlow = when (stakingIntegration) {
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
rawCurrencyId,
cryptoCurrency,
cryptoCurrency.symbol,
)
null -> send(StakingAvailability.Unavailable)
null -> flowOf(StakingAvailability.Unavailable)
}
availabilityFlow.collect { send(it) }
}
}
@ -254,39 +90,25 @@ internal class DefaultStakingRepository(
?: return StakingAvailability.Unavailable
return when (stakingIntegration) {
is StakingIntegrationID.P2P -> {
val vaults = getP2PEthPoolVaultsSync()
if (vaults.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync()
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
rawCurrencyId,
cryptoCurrency.symbol,
)
}
}
val vault = findP2PEthPoolVault(
vaults = vaults,
)
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.default) {
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
if (vault != null) {
StakingAvailability.Available(StakingOption.P2P(vault))
} else {
StakingAvailability.TemporaryUnavailable
val hasDataYieldBalance by lazy {
balances.any { yieldBalance ->
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
}
}
is StakingIntegrationID.StakeKit -> {
val yields = getEnabledYieldsSync()
if (yields.isEmpty()) {
return StakingAvailability.TemporaryUnavailable
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null -> StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
else -> StakingAvailability.TemporaryUnavailable
}
}
balances.isNotEmpty() && hasDataYieldBalance
}
}
@ -316,267 +138,6 @@ internal class DefaultStakingRepository(
}
}
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
is 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) {
is 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 isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.default) {
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
val hasDataYieldBalance by lazy {
balances.any { yieldBalance ->
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
}
}
balances.isNotEmpty() && hasDataYieldBalance
}
}
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 = YieldTokenConverter.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
}
}
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,
Blockchain.TON,
Blockchain.Cardano,
-> 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)
}
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") },
)
}
override 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 suspend fun getP2PEthPoolVaultsSync(): List<P2PEthPoolVault> {
return p2pEthPoolVaultsStore.getSync()
}
private fun getP2PEthPoolVaults(): Flow<List<P2PEthPoolVault>> {
return p2pEthPoolVaultsStore.get()
}
private fun findP2PEthPoolVault(vaults: List<P2PEthPoolVault>): P2PEthPoolVault? {
return vaults.firstOrNull { vault -> !vault.isPrivate }
}
private fun ProducerScope<StakingAvailability>.subscribeToP2PStakingAvailability() {
getP2PEthPoolVaults()
.distinctUntilChanged()
.onEach { vaults ->
if (vaults.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val vault = findP2PEthPoolVault(vaults = vaults)
if (vault != null) {
send(StakingAvailability.Available(StakingOption.P2P(vault)))
} else {
send(StakingAvailability.TemporaryUnavailable)
}
}
.launchIn(this)
}
private fun ProducerScope<StakingAvailability>.subscribeToStakeKitStakingAvailability(
rawCurrencyId: CryptoCurrency.RawID,
cryptoCurrency: CryptoCurrency,
) {
getEnabledYields()
.distinctUntilChanged()
.onEach { yields ->
if (yields.isEmpty()) {
send(StakingAvailability.TemporaryUnavailable)
return@onEach
}
val prefetchedYield = findPrefetchedYield(
yields = yields,
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
if (prefetchedYield != null) {
send(StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield)))
} else {
send(StakingAvailability.TemporaryUnavailable)
}
}
.launchIn(this)
}
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) {
TronResource.ENERGY
} else {
null
}
}
companion object {
private val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79")
}

View file

@ -4,10 +4,11 @@ import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.staking.DefaultP2PEthPoolRepository
import com.tangem.data.staking.DefaultStakingActionRepository
import com.tangem.data.staking.DefaultStakeKitActionRepository
import com.tangem.data.staking.DefaultStakingErrorResolver
import com.tangem.data.staking.DefaultStakeKitRepository
import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.data.staking.DefaultStakingTransactionHashRepository
import com.tangem.data.staking.DefaultStakeKitTransactionHashRepository
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.data.staking.store.YieldsBalancesStore
import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles
@ -21,10 +22,11 @@ import com.tangem.datasource.local.token.P2PEthPoolVaultsStore
import com.tangem.datasource.local.token.StakingActionsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakingActionRepository
import com.tangem.domain.staking.repositories.StakeKitActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.utils.StakingCleaner
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -42,30 +44,44 @@ internal object StakingDataModule {
@Provides
@Singleton
fun provideStakingRepository(
fun provideStakeKitRepository(
stakeKitApi: StakeKitApi,
stakingYieldsStore: StakingYieldsStore,
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
getUserWalletUseCase: GetUserWalletUseCase,
stakingFeatureToggles: StakingFeatureToggles,
@NetworkMoshi moshi: Moshi,
): StakingRepository {
return DefaultStakingRepository(
): StakeKitRepository {
return DefaultStakeKitRepository(
stakeKitApi = stakeKitApi,
stakingYieldsStore = stakingYieldsStore,
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
stakingBalanceStoreV2 = yieldsBalancesStore,
dispatchers = dispatchers,
walletManagersFacade = walletManagersFacade,
getUserWalletUseCase = getUserWalletUseCase,
stakingFeatureToggles = stakingFeatureToggles,
moshi = moshi,
)
}
@Provides
@Singleton
fun provideStakingRepository(
stakeKitRepository: StakeKitRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
yieldsBalancesStore: YieldsBalancesStore,
dispatchers: CoroutineDispatcherProvider,
getUserWalletUseCase: GetUserWalletUseCase,
stakingFeatureToggles: StakingFeatureToggles,
): StakingRepository {
return DefaultStakingRepository(
stakeKitRepository = stakeKitRepository,
p2pEthPoolRepository = p2pEthPoolRepository,
stakingBalanceStoreV2 = yieldsBalancesStore,
dispatchers = dispatchers,
getUserWalletUseCase = getUserWalletUseCase,
stakingFeatureToggles = stakingFeatureToggles,
)
}
@Provides
@Singleton
fun provideP2PEthPoolRepository(
@ -86,8 +102,8 @@ internal object StakingDataModule {
stakeKitApi: StakeKitApi,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): StakingTransactionHashRepository {
return DefaultStakingTransactionHashRepository(
): StakeKitTransactionHashRepository {
return DefaultStakeKitTransactionHashRepository(
stakeKitApi = stakeKitApi,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
@ -99,8 +115,8 @@ internal object StakingDataModule {
fun provideStakingActionRepository(
stakingActionsStore: StakingActionsStore,
dispatchers: CoroutineDispatcherProvider,
): StakingActionRepository {
return DefaultStakingActionRepository(
): StakeKitActionRepository {
return DefaultStakeKitActionRepository(
stakingActionsStore = stakingActionsStore,
dispatchers = dispatchers,
)

View file

@ -69,8 +69,8 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
fetch(userWalletId = params.userWalletId, stakingIds = availableStakingIds)
}
.onLeft {
Timber.e(it, "Unable to fetch yield balances $params")
.onLeft { throwable ->
Timber.e(throwable, "Unable to fetch yield balances $params")
yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds)
}
@ -163,21 +163,21 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
if (!allResponsesReceived(requests, yieldBalances)) {
val values = stakingIds.filter { stakingId ->
yieldBalances.none {
stakingId.integrationId == it.integrationId &&
stakingId.address == it.addresses.address
yieldBalances.none { balanceWrapper ->
stakingId.integrationId == balanceWrapper.integrationId &&
stakingId.address == balanceWrapper.addresses.address
}
}
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = values.toSet())
}
},
onError = {
Timber.e(it, "Unable to fetch yield balances $userWalletId")
onError = { throwable ->
Timber.e(throwable, "Unable to fetch yield balances $userWalletId")
yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
throw it
throw throwable
},
)
}
@ -187,9 +187,9 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor(
yieldBalances: Set<YieldBalanceWrapperDTO>,
): Boolean {
return requests.all { request ->
yieldBalances.any {
request.integrationId == it.integrationId &&
request.addresses.address == it.addresses.address
yieldBalances.any { balance ->
request.integrationId == balance.integrationId &&
request.addresses.address == balance.addresses.address
}
}
}

View file

@ -14,7 +14,7 @@ internal object YieldBalanceRequestBodyFactory {
return YieldBalanceRequestBody(
addresses = YieldBalanceRequestBodyAddressFactory.create(stakingID),
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
validatorAddresses = emptyList(), // todo add validators [REDACTED_JIRA]
),
integrationId = stakingID.integrationId,
)