Updated on 2026-08-14
This commit is contained in:
parent
ede2066c64
commit
4a726e436c
31 changed files with 421 additions and 37 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -98,6 +98,7 @@ internal sealed class StakingStates {
|
|||
val transactionDoneState: TransactionDoneState,
|
||||
val isApprovalNeeded: Boolean,
|
||||
val reduceAmountBy: BigDecimal?,
|
||||
val balanceState: BalanceState?,
|
||||
) : ConfirmationState()
|
||||
|
||||
data class Empty(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,5 +93,6 @@ internal object ConfirmationStatePreviewData {
|
|||
isApprovalNeeded = false,
|
||||
reduceAmountBy = null,
|
||||
pendingActions = null,
|
||||
balanceState = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
|
|||
actionTypeToOverwrite: StakingActionCommonType?,
|
||||
pendingAction: PendingAction?,
|
||||
pendingActions: ImmutableList<PendingAction>?,
|
||||
) {
|
||||
}
|
||||
balanceState: BalanceState?,
|
||||
) { }
|
||||
|
||||
override fun onActionClick() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ internal class SetConfirmationStateLoadingTransformer(
|
|||
pendingActions = possibleConfirmationState?.pendingActions,
|
||||
isApprovalNeeded = false,
|
||||
reduceAmountBy = null,
|
||||
balanceState = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
|
|||
actionTypeToOverwrite: StakingActionCommonType? = null,
|
||||
pendingAction: PendingAction? = null,
|
||||
pendingActions: ImmutableList<PendingAction>? = null,
|
||||
balanceState: BalanceState? = null,
|
||||
)
|
||||
|
||||
fun onActionClick()
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue