Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-16 13:26:37 +03:00
parent 0cd5575681
commit 0391e50dc8
12 changed files with 198 additions and 106 deletions

View file

@ -87,11 +87,23 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideCreateEnterActionUseCase(
fun provideInitializeStakingProcessUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): InitializeStakingProcessUseCase {
return InitializeStakingProcessUseCase(
): GetStakingTransactionUseCase {
return GetStakingTransactionUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGasEstimateUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): EstimateGasUseCase {
return EstimateGasUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.transaction.Stak
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
import retrofit2.http.*
@Suppress("LongParameterList")
@ -45,7 +46,13 @@ interface StakeKitApi {
suspend fun getTokens(): ApiResponse<List<TokenWithYieldDTO>>
@POST("actions/enter")
suspend fun createEnterAction(@Body body: EnterActionRequestBody): ApiResponse<EnterActionResponse>
suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse<EnterActionResponse>
@POST("actions/enter/estimate-gas")
suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse<StakingGasEstimateDTO>
@POST("actions/exit/estimate-gas")
suspend fun estimateGasOnExit(@Body body: ActionRequestBody): ApiResponse<StakingGasEstimateDTO>
@PATCH("transactions/{transactionId}")
suspend fun constructTransaction(

View file

@ -1,10 +1,11 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
data class EnterActionRequestBody(
data class ActionRequestBody(
@Json(name = "integrationId")
val integrationId: String,
@Json(name = "addresses")
@ -13,6 +14,8 @@ data class EnterActionRequestBody(
val args: EnterActionRequestBodyArgs,
@Json(name = "referralCode")
val referralCode: String? = null,
@Json(name = "gasArgs")
val gasArgs: GasArgs? = null, // used only in estimate_gas request
) {
data class EnterActionRequestBodyArgs(

View file

@ -25,6 +25,9 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.action.StakingActionCommonType
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.transaction.ActionParams
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
@ -37,7 +40,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@Suppress("LargeClass", "LongParameterList")
internal class DefaultStakingRepository(
@ -153,29 +155,28 @@ internal class DefaultStakingRepository(
}
}
override suspend fun createEnterAction(
integrationId: String,
amount: BigDecimal,
address: String,
validatorAddress: String,
token: Token,
): EnterAction {
override suspend fun createEnterAction(params: ActionParams): EnterAction {
return withContext(dispatchers.io) {
val body = EnterActionRequestBody(
integrationId = integrationId,
addresses = Address(address),
args = EnterActionRequestBody.EnterActionRequestBodyArgs(
amount = amount.toFormattedString(token.decimals),
inputToken = tokenConverter.convertBack(token),
validatorAddress = validatorAddress,
),
)
val body = createActionRequestBody(params)
val response = stakeKitApi.createEnterAction(body)
enterActionResponseConverter.convert(response.getOrThrow())
}
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
return withContext(dispatchers.io) {
val body = createActionRequestBody(params)
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(body)
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(body)
}
gasEstimateConverter.convert(gasEstimateDTO.getOrThrow())
}
}
override suspend fun constructTransaction(transactionId: String): StakingTransaction {
return withContext(dispatchers.io) {
val transactionResponse = stakeKitApi.constructTransaction(
@ -423,6 +424,18 @@ internal class DefaultStakingRepository(
}
}
private fun createActionRequestBody(params: ActionParams): ActionRequestBody {
return ActionRequestBody(
integrationId = params.integrationId,
addresses = Address(params.address),
args = ActionRequestBody.EnterActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals),
inputToken = tokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress,
),
)
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.staking.model.action
enum class StakingActionCommonType {
ENTER,
EXIT,
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.staking.model.transaction
import com.tangem.domain.staking.model.Token
import com.tangem.domain.staking.model.action.StakingActionCommonType
import java.math.BigDecimal
data class ActionParams(
val actionCommonType: StakingActionCommonType,
val integrationId: String,
val amount: BigDecimal,
val address: String,
val validatorAddress: String,
val token: Token,
)

View file

@ -0,0 +1,25 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.StakingError
import com.tangem.domain.staking.model.transaction.ActionParams
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
/**
* Use case for staking gas estimation.
*/
class EstimateGasUseCase(
private val stakingRepository: StakingRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingGasEstimate> {
return Either.catch {
stakingRepository.estimateGas(params)
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}
}

View file

@ -2,37 +2,23 @@ package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.StakingError
import com.tangem.domain.staking.model.Token
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.transaction.ActionParams
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import kotlinx.coroutines.delay
import java.math.BigDecimal
/**
* Use case for creating enter action
*/
class InitializeStakingProcessUseCase(
class GetStakingTransactionUseCase(
private val stakingRepository: StakingRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(
integrationId: String,
amount: BigDecimal,
address: String,
validatorAddress: String,
token: Token,
): Either<StakingError, Pair<EnterAction, StakingTransaction>> {
suspend operator fun invoke(params: ActionParams): Either<StakingError, StakingTransaction> {
return Either.catch {
val createAction = stakingRepository.createEnterAction(
integrationId = integrationId,
amount = amount,
address = address,
validatorAddress = validatorAddress,
token = token,
)
val createAction = stakingRepository.createEnterAction(params)
// workaround, sometimes transaction is not created immediately after actions/enter
delay(PATCH_TRANSACTION_REQUEST_DELAY)
@ -40,7 +26,7 @@ class InitializeStakingProcessUseCase(
val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch")
val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id)
createAction to patchedTransaction
patchedTransaction
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -2,13 +2,13 @@ package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.Token
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.transaction.StakingTransaction
import java.math.BigDecimal
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.transaction.ActionParams
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
@ -60,13 +60,9 @@ interface StakingRepository {
addresses: List<CryptoCurrencyAddress>,
): YieldBalanceList
suspend fun createEnterAction(
integrationId: String,
amount: BigDecimal,
address: String,
validatorAddress: String,
token: Token,
): EnterAction
suspend fun createEnterAction(params: ActionParams): EnterAction
suspend fun estimateGas(params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(transactionId: String): StakingTransaction

View file

@ -6,9 +6,7 @@ import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.action.StakingActionStatus
import com.tangem.domain.staking.model.action.StakingActionType
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.model.transaction.StakingTransactionStatus
import com.tangem.domain.staking.model.transaction.StakingTransactionType
import com.tangem.domain.staking.model.transaction.*
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
@ -21,7 +19,8 @@ import java.math.BigDecimal
class MockStakingRepository : StakingRepository {
override fun isStakingSupported(currencyId: String): Boolean = true
override suspend fun fetchEnabledYields(refresh: Boolean) { /* no-op */
override suspend fun fetchEnabledYields(refresh: Boolean) {
/* no-op */
}
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo(
@ -165,24 +164,37 @@ class MockStakingRepository : StakingRepository {
balances = listOf(YieldBalance.Error),
)
override suspend fun createEnterAction(
integrationId: String,
amount: BigDecimal,
address: String,
validatorAddress: String,
token: Token,
): EnterAction = EnterAction(
id = "quis",
integrationId = "persequeris",
status = StakingActionStatus.PROCESSING,
type = StakingActionType.CLAIM_REWARDS,
currentStepIndex = 8701,
amount = BigDecimal.ZERO,
validatorAddress = null,
validatorAddresses = listOf(),
transactions = listOf(),
createdAt = DateTime.now(),
)
override suspend fun createEnterAction(params: ActionParams): EnterAction {
return EnterAction(
id = "quis",
integrationId = "persequeris",
status = StakingActionStatus.PROCESSING,
type = StakingActionType.CLAIM_REWARDS,
currentStepIndex = 8701,
amount = BigDecimal.ZERO,
validatorAddress = null,
validatorAddresses = listOf(),
transactions = listOf(),
createdAt = DateTime.now(),
)
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
return StakingGasEstimate(
amount = BigDecimal(0.0001),
token = Token(
name = "Solana",
network = NetworkType.SOLANA,
symbol = "SOL",
decimals = 18,
address = null,
coinGeckoId = "solana",
logoURI = null,
isPoints = null,
),
gasLimit = null,
)
}
override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction(
id = "id",
@ -202,11 +214,14 @@ class MockStakingRepository : StakingRepository {
)
override suspend fun submitHash(transactionId: String, transactionHash: String) {
/* no-op */
}
override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) {
/* no-op */
}
override suspend fun sendUnsubmittedHashes() {
/* no-op */
}
}

View file

@ -113,7 +113,7 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (buttonClick != null) buttonClick()
},
showProgress = false,
showProgress = isInProgressInnerState,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)

View file

@ -14,11 +14,14 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.InitializeStakingProcessUseCase
import com.tangem.domain.staking.GetStakingTransactionUseCase
import com.tangem.domain.staking.EstimateGasUseCase
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
import com.tangem.domain.staking.SubmitHashUseCase
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.model.action.StakingActionCommonType
import com.tangem.domain.staking.model.transaction.ActionParams
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -57,7 +60,8 @@ internal class StakingViewModel @Inject constructor(
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase,
private val getStakingTransactionUseCase: GetStakingTransactionUseCase,
private val estimateGasUseCase: EstimateGasUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
@ -71,8 +75,6 @@ internal class StakingViewModel @Inject constructor(
var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private set
private var stakingTransaction: StakingTransaction? = null
private val cryptoCurrencyId: CryptoCurrency.ID =
savedStateHandle.get<Bundle>(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY)
?.unbundle(CryptoCurrency.ID.serializer())
@ -113,8 +115,29 @@ internal class StakingViewModel @Inject constructor(
private fun handleOnNextConfirmationClick() {
if (isAssentState()) {
viewModelScope.launch {
stakingTransaction?.unsignedTransaction?.let {
sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes()))
stateController.update(SetConfirmationStateInProgressTransformer())
val stakingTransaction = getStakingTransactionUseCase(
params = ActionParams(
actionCommonType = StakingActionCommonType.ENTER,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
token = yield.token,
),
).getOrElse {
error(it)
}
stakingTransaction.unsignedTransaction?.let {
sendStakingTransaction(
transactionId = stakingTransaction.id,
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
TransactionData.Compiled(value = it.hexToBytes()),
)
} ?: error("No unsigned transaction available")
}
}
@ -128,23 +151,18 @@ internal class StakingViewModel @Inject constructor(
),
)
val actionWithTransaction = initializeStakingProcessUseCase(
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
token = yield.token,
)
val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse {
error(it)
}
this@StakingViewModel.stakingTransaction = stakingTransaction
val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info")
val stakingGasEstimate = estimateGasUseCase(
params = ActionParams(
actionCommonType = StakingActionCommonType.ENTER,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
token = yield.token,
),
).getOrElse { error("Can't get fee info") }
stateController.update(
SetConfirmationStateAssentTransformer(
@ -270,9 +288,11 @@ internal class StakingViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private suspend fun sendStakingTransaction(txData: TransactionData) {
stateController.update(SetConfirmationStateInProgressTransformer())
private suspend fun sendStakingTransaction(
transactionId: String,
gasEstimate: StakingGasEstimate,
txData: TransactionData,
) {
sendTransactionUseCase(
txData = txData,
userWallet = userWallet,
@ -280,7 +300,6 @@ internal class StakingViewModel @Inject constructor(
).fold(
ifLeft = { error ->
Timber.e(error.toString())
val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -290,17 +309,13 @@ internal class StakingViewModel @Inject constructor(
)
},
ifRight = { txHash ->
val transaction = stakingTransaction ?: return@fold
submitHash(transaction.id, txHash)
submitHash(transactionId, txHash)
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrencyStatus.currency.network.id,
).getOrElse { "" }
val gasEstimate = transaction.gasEstimate ?: error("No gas for transaction")
stateController.update(
SetConfirmationStateCompletedTransformer(
appCurrencyProvider = Provider { appCurrency },