Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-20 16:40:25 +03:00
parent ede2066c64
commit 4a726e436c
31 changed files with 421 additions and 37 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import dagger.Module
@ -122,6 +123,42 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideSavePendingTransactionUseCase(
stakingPendingTransactionRepository: StakingPendingTransactionRepository,
stakingErrorResolver: StakingErrorResolver,
): SavePendingTransactionUseCase {
return SavePendingTransactionUseCase(
stakingPendingTransactionRepository = stakingPendingTransactionRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideInvalidatePendingTransactionsUseCase(
stakingPendingTransactionRepository: StakingPendingTransactionRepository,
stakingErrorResolver: StakingErrorResolver,
): InvalidatePendingTransactionsUseCase {
return InvalidatePendingTransactionsUseCase(
stakingPendingTransactionRepository = stakingPendingTransactionRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetPendingTransactionsUseCase(
stakingPendingTransactionRepository: StakingPendingTransactionRepository,
stakingErrorResolver: StakingErrorResolver,
): GetPendingTransactionsUseCase {
return GetPendingTransactionsUseCase(
stakingPendingTransactionRepository = stakingPendingTransactionRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideSendUnsubmittedHashesUseCase(

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -38,10 +39,12 @@ import com.tangem.core.ui.res.TangemThemePreview
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isGrayscaleImage whether to display grayscale image
* @param subtitleEndIconRes icon to show after subtitle
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
* @param extraContent extra content
*/
@Suppress("LongMethod")
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
@ -53,7 +56,9 @@ internal fun InputRowImageBase(
captionColor: Color = TangemTheme.colors.text.tertiary,
iconTint: Color = TangemTheme.colors.icon.informative,
isGrayscaleImage: Boolean = false,
iconEndRes: Int? = null,
@DrawableRes subtitleEndIconRes: Int? = null,
subtitleEndIconTint: Color = TangemColorPalette.Azure,
@DrawableRes iconEndRes: Int? = null,
onImageError: (@Composable () -> Unit)? = null,
extraContent: (@Composable RowScope.() -> Unit)? = null,
) {
@ -89,11 +94,17 @@ internal fun InputRowImageBase(
SpacerW12()
}
Column {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Row {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
SubtitleEndIconRes(
subtitleEndIconRes = subtitleEndIconRes,
subtitleEndIconTint = subtitleEndIconTint,
)
}
if (caption != null) {
Text(
text = caption.resolveAnnotatedReference(),
@ -113,11 +124,28 @@ internal fun InputRowImageBase(
}
}
@Composable
private fun RowScope.SubtitleEndIconRes(subtitleEndIconRes: Int?, subtitleEndIconTint: Color) {
AnimatedVisibility(
visible = subtitleEndIconRes != null,
label = "Subtitle end icon visibility animation",
modifier = Modifier.align(Alignment.CenterVertically),
) {
val icon = remember(this) { requireNotNull(subtitleEndIconRes) }
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = icon)),
tint = subtitleEndIconTint,
contentDescription = null,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
}
@Composable
private fun RowScope.InputRowEndIcon(iconRes: Int?) {
AnimatedVisibility(
visible = iconRes != null,
label = "Icon visibility animation",
label = "End icon visibility animation",
) {
val icon = remember(this) { requireNotNull(iconRes) }
Icon(

View file

@ -34,6 +34,7 @@ import com.tangem.core.ui.res.TangemThemePreview
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isGrayscaleImage whether to display grayscale image
* @param showPendingIcon whether to show pending icon
* @param iconEndRes icon to end of row
* @param onImageError composable to show if image loading failed
*/
@ -52,6 +53,7 @@ fun InputRowImageInfo(
captionColor: Color = TangemTheme.colors.text.tertiary,
iconTint: Color = TangemTheme.colors.icon.informative,
isGrayscaleImage: Boolean = false,
@DrawableRes subtitleEndIconRes: Int? = null,
@DrawableRes iconEndRes: Int? = null,
onImageError: (@Composable () -> Unit)? = null,
) {
@ -76,6 +78,7 @@ fun InputRowImageInfo(
subtitleColor = subtitleColor,
captionColor = captionColor,
isGrayscaleImage = isGrayscaleImage,
subtitleEndIconRes = subtitleEndIconRes,
iconEndRes = iconEndRes,
onImageError = onImageError,
) {
@ -132,6 +135,7 @@ private fun InputRowImageInfo_Preview(
infoTitle = data.infoTitle,
infoSubtitle = data.infoSubtitle,
imageUrl = "",
subtitleEndIconRes = data.subtitleEndIconRes,
iconEndRes = R.drawable.ic_chevron_right_24,
)
}
@ -162,6 +166,22 @@ private class InputRowImageInfoPreviewDataProvider : PreviewParameterProvider<In
infoTitle = stringReference("5431231231231231231231232 USD"),
infoSubtitle = null,
),
InputRowImageInfoPreviewData(
title = stringReference("Validator"),
subtitle = stringReference("Binance"),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
append("3,54%")
},
),
),
infoTitle = stringReference("5431231231231231231231232 USD"),
infoSubtitle = stringReference("5 SOL"),
subtitleEndIconRes = R.drawable.ic_staking_pending_transaction,
),
)
}
@ -171,5 +191,6 @@ private data class InputRowImageInfoPreviewData(
val caption: TextReference?,
val infoTitle: TextReference,
val infoSubtitle: TextReference?,
@DrawableRes val subtitleEndIconRes: Int? = null,
)
// endregion

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="15dp"
android:height="4dp"
android:viewportWidth="15"
android:viewportHeight="4">
<path
android:pathData="M13.5,3.5C12.672,3.5 12,2.828 12,2C12,1.172 12.672,0.5 13.5,0.5C14.328,0.5 15,1.172 15,2C15,2.828 14.328,3.5 13.5,3.5Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M7.5,3.5C6.672,3.5 6,2.828 6,2C6,1.172 6.672,0.5 7.5,0.5C8.328,0.5 9,1.172 9,2C9,2.828 8.328,3.5 7.5,3.5Z"
android:strokeAlpha="0.55"
android:fillColor="#0099FF"
android:fillAlpha="0.55"/>
<path
android:pathData="M1.5,3.5C0.672,3.5 0,2.828 0,2C0,1.172 0.672,0.5 1.5,0.5C2.328,0.5 3,1.172 3,2C3,2.828 2.328,3.5 1.5,3.5Z"
android:strokeAlpha="0.25"
android:fillColor="#0099FF"
android:fillAlpha="0.25"/>
</vector>

View file

@ -0,0 +1,27 @@
package com.tangem.data.staking
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import java.util.concurrent.CopyOnWriteArrayList
internal class DefaultStakingPendingTransactionRepository : StakingPendingTransactionRepository {
private val pendingTransactions = CopyOnWriteArrayList<PendingTransaction>()
override fun saveTransaction(transaction: PendingTransaction) {
pendingTransactions.add(transaction)
}
override fun removeTransactions(transactions: Set<PendingTransaction>) {
pendingTransactions.removeAll(transactions)
}
override fun getTransactionsWithBalanceItems(): List<Pair<PendingTransaction, BalanceItem>> {
return pendingTransactions.mapNotNull { pendingTransaction ->
PendingTransactionItemConverter.convert(pendingTransaction)?.let { balanceItem ->
pendingTransaction to balanceItem
}
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.data.staking
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.utils.converter.Converter
import org.joda.time.DateTime
internal object PendingTransactionItemConverter : Converter<PendingTransaction, BalanceItem?> {
override fun convert(value: PendingTransaction): BalanceItem? {
return BalanceItem(
groupId = value.groupId ?: return null,
type = value.type ?: return null,
amount = value.amount ?: return null,
rawCurrencyId = value.rawCurrencyId,
validatorAddress = value.validator?.address,
date = DateTime.now(),
pendingActions = emptyList(),
isPending = true,
)
}
}

View file

@ -19,12 +19,10 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
balance = YieldBalanceItem(
items = value.balances.map { item ->
BalanceItem(
id = item.groupId,
groupId = item.groupId,
type = BalanceType.valueOf(item.type.name),
amount = item.amount,
pricePerShare = item.pricePerShare,
rawCurrencyId = item.tokenDTO.coinGeckoId,
rawNetworkId = item.tokenDTO.network.name,
// tron-specific. operates validatorAddresses instead of validatorAddress
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toDateTime(),
@ -32,6 +30,7 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
.convertList(item.pendingActions)
// temporarily exclude VOTE_LOCKED, it will be implemented in future iterations
.filterNot { it.type == StakingActionType.VOTE_LOCKED },
isPending = false,
)
},
integrationId = value.integrationId,

View file

@ -3,6 +3,7 @@ package com.tangem.data.staking.di
import com.squareup.moshi.Moshi
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.DefaultStakingErrorResolver
import com.tangem.data.staking.DefaultStakingPendingTransactionRepository
import com.tangem.data.staking.DefaultStakingRepository
import com.tangem.data.staking.DefaultStakingTransactionHashRepository
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
@ -13,6 +14,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -57,7 +59,7 @@ internal object StakingDataModule {
@Provides
@Singleton
fun StakingTransactionHashRepository(
fun provideStakingTransactionHashRepository(
stakeKitApi: StakeKitApi,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
@ -69,6 +71,12 @@ internal object StakingDataModule {
)
}
@Provides
@Singleton
fun provideStakingPendingTransactionRepository(): StakingPendingTransactionRepository {
return DefaultStakingPendingTransactionRepository()
}
@Provides
@Singleton
internal fun provideStakingErrorResolver(@NetworkMoshi moshi: Moshi): StakingErrorResolver {

View file

@ -0,0 +1,13 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
data class PendingTransaction(
val groupId: String?,
val type: BalanceType?,
val amount: BigDecimal?,
val rawCurrencyId: String?,
val validator: Yield.Validator?,
)

View file

@ -0,0 +1,14 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
data class SubmitHashData(
val transactionHash: String,
val transactionId: String,
val validator: Yield.Validator?,
val amount: BigDecimal?,
val balanceType: BalanceType?,
val rawCurrencyId: String?,
)

View file

@ -45,15 +45,14 @@ data class YieldBalanceItem(
)
data class BalanceItem(
val id: String,
val groupId: String,
val type: BalanceType,
val amount: BigDecimal,
val pricePerShare: BigDecimal,
val rawCurrencyId: String?,
val rawNetworkId: String,
val validatorAddress: String?,
val date: DateTime?,
val pendingActions: List<PendingAction>,
val isPending: Boolean,
)
data class PendingAction(

View file

@ -0,0 +1,24 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
/**
* Use case for getting saved staking pending transactions.
*/
class GetPendingTransactionsUseCase(
private val stakingPendingTransactionRepository: StakingPendingTransactionRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(): Either<StakingError, List<BalanceItem>> {
return Either.catch {
stakingPendingTransactionRepository.getTransactionsWithBalanceItems().map { it.second }
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
class InvalidatePendingTransactionsUseCase(
private val stakingPendingTransactionRepository: StakingPendingTransactionRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(yieldBalance: YieldBalance): Either<StakingError, List<BalanceItem>> {
return Either.catch {
if (yieldBalance is YieldBalance.Data) {
val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions(
real = yieldBalance.balance.items,
pending = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(),
)
stakingPendingTransactionRepository.removeTransactions(transactionsToRemove.toSet())
balancesToDisplay
} else {
emptyList()
}
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}
private fun mergeRealAndPendingTransactions(
real: List<BalanceItem>,
pending: List<Pair<PendingTransaction, BalanceItem>>,
): Pair<List<BalanceItem>, List<PendingTransaction>> {
val map = real.associateBy { Triple(it.groupId, it.type, it.amount) }.toMutableMap()
val toRemove = mutableListOf<PendingTransaction>()
pending.forEach { (pendingTransaction, balanceItem) ->
val key = Triple(balanceItem.groupId, balanceItem.type, balanceItem.amount)
if (map.containsKey(key)) {
map[key] = balanceItem
} else {
toRemove.add(pendingTransaction)
}
}
return map.values.toList() to toRemove
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository
/**
* Use case for saving hash that failed to submit during staking confirmation
*/
class SavePendingTransactionUseCase(
private val stakingPendingTransactionRepository: StakingPendingTransactionRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(pendingTransaction: PendingTransaction): Either<StakingError, Unit> {
return Either.catch {
stakingPendingTransactionRepository.saveTransaction(pendingTransaction)
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
@ -13,12 +14,12 @@ class SubmitHashUseCase(
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend fun submitHash(transactionId: String, transactionHash: String): Either<StakingError, Unit> {
suspend operator fun invoke(submitHashData: SubmitHashData): Either<StakingError, Unit> {
return Either
.catch {
stakingTransactionHashRepository.submitHash(
transactionId = transactionId,
transactionHash = transactionHash,
transactionId = submitHashData.transactionId,
transactionHash = submitHashData.transactionHash,
)
}.mapLeft {
stakingErrorResolver.resolve(it)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.stakekit.BalanceItem
interface StakingPendingTransactionRepository {
fun getTransactionsWithBalanceItems(): List<Pair<PendingTransaction, BalanceItem>>
fun saveTransaction(transaction: PendingTransaction)
fun removeTransactions(transactions: Set<PendingTransaction>)
}

View file

@ -22,7 +22,7 @@ internal sealed class InnerYieldBalanceState {
@Immutable
internal data class BalanceState(
val id: String,
val groupId: String,
val title: TextReference,
val type: BalanceType,
val subtitle: TextReference?,
@ -34,4 +34,5 @@ internal data class BalanceState(
val rawCurrencyId: String?,
val validator: Yield.Validator?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
)

View file

@ -98,6 +98,7 @@ internal sealed class StakingStates {
val transactionDoneState: TransactionDoneState,
val isApprovalNeeded: Boolean,
val reduceAmountBy: BigDecimal?,
val balanceState: BalanceState?,
) : ConfirmationState()
data class Empty(

View file

@ -22,6 +22,7 @@ internal class BalanceItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
@ -37,7 +38,7 @@ internal class BalanceItemConverter(
val title = value.type.getTitle(validator?.name)
return title?.let {
BalanceState(
id = value.id,
groupId = value.groupId,
validator = validator,
title = title,
subtitle = getSubtitle(value),
@ -59,7 +60,8 @@ internal class BalanceItemConverter(
),
rawCurrencyId = value.rawCurrencyId,
pendingActions = value.pendingActions.toPersistentList(),
isClickable = value.type.isClickable(),
isClickable = value.type.isClickable() && !value.isPending,
isPending = value.isPending,
)
}
}

View file

@ -78,7 +78,7 @@ internal class RewardsValidatorStateConverter(
)
return BalanceState(
id = balance.id,
groupId = balance.groupId,
validator = this,
title = stringReference(this.name),
subtitle = null,
@ -90,6 +90,7 @@ internal class RewardsValidatorStateConverter(
pendingActions = balance.pendingActions.toPersistentList(),
isClickable = true,
type = balance.type,
isPending = balance.isPending,
)
}
}

View file

@ -14,6 +14,7 @@ import kotlinx.collections.immutable.toPersistentList
internal class YieldBalancesConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield,
) : Converter<Unit, InnerYieldBalanceState> {
@ -32,6 +33,8 @@ internal class YieldBalancesConverter(
val cryptoRewardsValue = yieldBalance.getRewardStakingBalance()
val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue)
val balanceToShowItems = balancesToShowProvider()
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
@ -43,7 +46,7 @@ internal class YieldBalancesConverter(
fiatCurrencySymbol = appCurrency.symbol,
),
rewardBlockType = getRewardBlockType(),
balance = yieldBalance.balance.items.mapBalances(),
balance = balanceToShowItems.mapBalances(),
)
} else {
InnerYieldBalanceState.Empty

View file

@ -5,10 +5,10 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
import com.tangem.domain.staking.GetStakingTransactionUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.PendingTransaction
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Yield
@ -45,6 +45,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val submitHashUseCase: SubmitHashUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val savePendingTransactionUseCase: SavePendingTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@ -89,6 +90,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
sendStakingTransaction(
fullTransactionsData = fullTransactionsData,
balanceState = confirmationState.balanceState,
onSendSuccess = onSendSuccess,
onSendError = onSendError,
)
@ -191,6 +193,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
private suspend fun sendStakingTransaction(
fullTransactionsData: List<FullTransactionData>,
balanceState: BalanceState?,
onSendSuccess: (txUrl: String) -> Unit,
onSendError: (SendTransactionError?) -> Unit,
) {
@ -206,6 +209,11 @@ internal class StakingTransactionSender @AssistedInject constructor(
submitHash(
transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id },
transactionHashes = transactionHashes,
groupId = balanceState?.groupId,
validator = balanceState?.validator,
amount = balanceState?.cryptoDecimal,
balanceType = balanceState?.type,
rawCurrencyId = balanceState?.rawCurrencyId,
)
val txUrl = getExplorerTransactionUrlUseCase(
txHash = transactionHashes.last(),
@ -218,13 +226,27 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
}
private suspend fun submitHash(transactionIds: List<String>, transactionHashes: List<String>) {
private suspend fun submitHash(
transactionIds: List<String>,
transactionHashes: List<String>,
groupId: String?,
validator: Yield.Validator?,
amount: BigDecimal?,
balanceType: BalanceType?,
rawCurrencyId: String?,
) {
transactionIds
.zip(transactionHashes)
.forEach { (transactionId, transactionHash) ->
submitHashUseCase.submitHash(
transactionId = transactionId,
transactionHash = transactionHash,
submitHashUseCase(
SubmitHashData(
transactionId = transactionId,
transactionHash = transactionHash,
validator = validator,
amount = amount,
balanceType = balanceType,
rawCurrencyId = rawCurrencyId,
),
)
.onLeft {
analyticsEventHandler.send(
@ -236,6 +258,15 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
}.onRight {
Timber.d("Successful hash submission")
savePendingTransactionUseCase.invoke(
PendingTransaction(
groupId = groupId,
type = balanceType,
amount = amount,
rawCurrencyId = rawCurrencyId,
validator = validator,
),
)
}
}
}

View file

@ -93,5 +93,6 @@ internal object ConfirmationStatePreviewData {
isApprovalNeeded = false,
reduceAmountBy = null,
pendingActions = null,
balanceState = null,
)
}

View file

@ -68,7 +68,7 @@ internal object InitialStakingStatePreview {
rewardBlockType = RewardBlockType.RewardUnavailable,
balance = persistentListOf(
BalanceState(
id = "id",
groupId = "groupId",
title = stringReference("Binance"),
cryptoValue = "100",
cryptoAmount = stringReference("100 SOL"),
@ -91,6 +91,7 @@ internal object InitialStakingStatePreview {
isClickable = true,
type = BalanceType.STAKED,
subtitle = null,
isPending = false,
),
),
),

View file

@ -20,8 +20,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
actionTypeToOverwrite: StakingActionCommonType?,
pendingAction: PendingAction?,
pendingActions: ImmutableList<PendingAction>?,
) {
}
balanceState: BalanceState?,
) { }
override fun onActionClick() {}

View file

@ -0,0 +1,18 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
internal class SetBalanceStateTransformer(
private val balanceState: BalanceState? = null,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
?: return prevState
return prevState.copy(
confirmationState = possibleConfirmationState.copy(balanceState = balanceState),
)
}
}

View file

@ -41,6 +41,7 @@ internal class SetConfirmationStateLoadingTransformer(
pendingActions = possibleConfirmationState?.pendingActions,
isApprovalNeeded = false,
reduceAmountBy = null,
balanceState = null,
),
)
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
@ -37,6 +38,7 @@ internal class SetInitialDataStateTransformer(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
) : Transformer<StakingUiState> {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@ -59,6 +61,7 @@ internal class SetInitialDataStateTransformer(
YieldBalancesConverter(
cryptoCurrencyStatusProvider,
appCurrencyProvider,
balancesToShowProvider,
yield,
)
}
@ -228,6 +231,7 @@ internal class SetInitialDataStateTransformer(
pendingActions = null,
isApprovalNeeded = isApprovalNeeded,
reduceAmountBy = null,
balanceState = null,
)
}

View file

@ -89,7 +89,7 @@ internal fun StakingInitialInfoContent(
if (state.showBanner) {
item(key = BANNER_BLOCK_KEY) {
Column(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier.animateItem(),
) {
BannerBlock(onClick = clickIntents::onInitialInfoBannerClick)
SpacerH12()
@ -105,7 +105,7 @@ internal fun StakingInitialInfoContent(
if (state.yieldBalance is InnerYieldBalanceState.Data) {
item(key = STAKING_REWARD_BLOCK_KEY) {
Column(modifier = Modifier.animateItemPlacement()) {
Column(modifier = Modifier.animateItem()) {
StakingRewardBlock(
rewardCrypto = state.yieldBalance.rewardsCrypto,
rewardFiat = state.yieldBalance.rewardsFiat,
@ -138,7 +138,6 @@ internal fun StakingInitialInfoContent(
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.activeStakingBlock(
state: StakingStates.InitialInfoState.Data,
clickIntents: StakingClickIntents,
@ -175,7 +174,7 @@ private fun LazyListScope.activeStakingBlock(
onClick = clickIntents::onActiveStake,
onAnalytic = clickIntents::onActiveStakeAnalytic,
modifier = Modifier
.animateItemPlacement()
.animateItem()
.then(
if (state.yieldBalance.balance.last() == balance) {
Modifier.clip(CornersToRound.BOTTOM_2.getShape())
@ -279,6 +278,7 @@ private fun ActiveStakingBlock(
imageUrl = balance.getImage(),
iconRes = icon,
iconTint = iconTint,
subtitleEndIconRes = R.drawable.ic_staking_pending_transaction.takeIf { balance.isPending },
onImageError = { ValidatorImagePlaceholder() },
modifier = modifier
.background(TangemTheme.colors.background.action)

View file

@ -20,6 +20,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
actionTypeToOverwrite: StakingActionCommonType? = null,
pendingAction: PendingAction? = null,
pendingActions: ImmutableList<PendingAction>? = null,
balanceState: BalanceState? = null,
)
fun onActionClick()

View file

@ -23,9 +23,11 @@ import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.staking.InvalidatePendingTransactionsUseCase
import com.tangem.domain.staking.IsAnyTokenStakedUseCase
import com.tangem.domain.staking.IsApproveNeededUseCase
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
@ -98,6 +100,7 @@ internal class StakingViewModel @Inject constructor(
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
private val stakingTransactionLoader: StakingTransactionSender.Factory,
private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory,
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
@ -131,6 +134,13 @@ internal class StakingViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private val balancesToShow: List<BalanceItem>
get() {
return cryptoCurrencyStatus.value.yieldBalance?.let {
invalidatePendingTransactionsUseCase(it).getOrElse { emptyList() }
} ?: emptyList()
}
private var isInitialInfoAnalyticSent: Boolean = false
private val balanceUpdater by lazy(LazyThreadSafetyMode.NONE) {
@ -200,6 +210,7 @@ internal class StakingViewModel @Inject constructor(
actionTypeToOverwrite: StakingActionCommonType?,
pendingAction: PendingAction?,
pendingActions: ImmutableList<PendingAction>?,
balanceState: BalanceState?,
) {
if (actionTypeToOverwrite != null) {
stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions))
@ -208,10 +219,12 @@ internal class StakingViewModel @Inject constructor(
when {
isInitState() -> {
stateController.update(SetConfirmationStateLoadingTransformer(yield, appCurrency))
stateController.update(SetBalanceStateTransformer(balanceState))
onRefreshSwipe(isRefreshing = false)
}
isAssentState() -> {
getFee(pendingAction, pendingActions)
stateController.update(SetBalanceStateTransformer(balanceState))
val amountState = value.amountState as? AmountState.Data
if (amountState?.amountTextField?.isWarning == true) {
stateController.update(
@ -413,6 +426,7 @@ internal class StakingViewModel @Inject constructor(
onNextClick(
actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER,
pendingAction = action,
balanceState = activeStake,
)
stateController.update(DismissBottomSheetStateTransformer)
},
@ -428,6 +442,7 @@ internal class StakingViewModel @Inject constructor(
actionTypeToOverwrite = null,
pendingAction = activeStake.pendingActions.firstOrNull(),
pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions },
balanceState = activeStake,
)
}
}
@ -749,6 +764,7 @@ internal class StakingViewModel @Inject constructor(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider { appCurrency },
balancesToShowProvider = Provider { balancesToShow },
),
)
},