Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-12 22:01:26 +05:00
parent f748b8539d
commit f1238f5ef8
20 changed files with 528 additions and 88 deletions

View file

@ -168,4 +168,16 @@ internal object StakingDomainModule {
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideGetConstructedStakingTransactionUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetConstructedStakingTransactionUseCase {
return GetConstructedStakingTransactionUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
}

View file

@ -100,4 +100,18 @@ internal object TransactionDomainModule {
): IsUtxoConsolidationAvailableUseCase {
return IsUtxoConsolidationAvailableUseCase(walletManagersFacade)
}
@Provides
@Singleton
fun provideCreateApproveTransactionUseCase(
transactionRepository: TransactionRepository,
): CreateApprovalTransactionUseCase {
return CreateApprovalTransactionUseCase(transactionRepository)
}
@Provides
@Singleton
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
return GetAllowanceUseCase(transactionRepository)
}
}

View file

@ -3,7 +3,11 @@ package com.tangem.data.staking
import android.util.Base64
import arrow.core.raise.catch
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.toCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.*
@ -25,6 +29,7 @@ import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
@ -231,14 +236,26 @@ internal class DefaultStakingRepository(
}
}
override suspend fun constructTransaction(transactionId: String): StakingTransaction {
override suspend fun constructTransaction(
networkId: String,
fee: Fee,
transactionId: String,
): Pair<StakingTransaction, TransactionData.Compiled> {
return withContext(dispatchers.io) {
val transactionResponse = stakeKitApi.constructTransaction(
transactionId = transactionId,
body = ConstructTransactionRequestBody(),
)
transactionConverter.convert(transactionResponse.getOrThrow())
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,
status = TransactionStatus.Unconfirmed,
)
transaction to transactionData
}
}
@ -530,10 +547,24 @@ internal class DefaultStakingRepository(
}
}
override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean {
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
return when (cryptoCurrency.id.getIntegrationKey()) {
Blockchain.Ethereum.id.plus(Blockchain.Polygon.toCoinId()) -> true
else -> false
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> {
StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
}
else -> StakingApproval.Empty
}
}
private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data {
val blockchain = Blockchain.fromId(networkId)
return when (blockchain) {
Blockchain.Solana,
Blockchain.Cosmos,
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
Blockchain.Ethereum,
-> TransactionData.Compiled.Data.RawString(unsignedTransaction)
else -> error("Unsupported blockchain")
}
}
@ -579,6 +610,8 @@ internal class DefaultStakingRepository(
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
// uncomment items as implementation is ready
val integrationIdMap = mapOf(
Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID,

View file

@ -16,6 +16,7 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.extensions.hexToBytes
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.models.TransactionType
@ -24,6 +25,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import java.math.BigInteger
import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType
@ -42,15 +44,15 @@ internal class DefaultTransactionRepository(
network: Network,
txExtras: TransactionExtras?,
hash: String?,
): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) {
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
) ?: error("Wallet manager not found")
return@withContext walletManager?.createTransactionDataInternal(
return@withContext walletManager.createTransactionDataInternal(
amount = amount,
fee = fee,
memo = memo,
@ -61,6 +63,48 @@ internal class DefaultTransactionRepository(
)
}
override suspend fun createApprovalTransaction(
amount: Amount,
fee: Fee,
contractAddress: String,
spenderAddress: String,
userWalletId: UserWalletId,
network: Network,
hash: String?,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
) ?: error("Wallet manager not found")
val approver = walletManager as? Approver ?: error("Cannot cast to Approver")
val approvalData = approver.getApproveData(
spenderAddress = spenderAddress,
value = amount,
)
val extras = createTransactionDataExtras(
data = approvalData,
network = network,
transactionType = TransactionType.APPROVE,
nonce = null,
gasLimit = null,
)
return@withContext createTransaction(
amount = amount,
fee = fee,
memo = null,
destination = contractAddress,
userWalletId = userWalletId,
network = network,
txExtras = extras,
hash = hash,
)
}
override suspend fun validateTransaction(
amount: Amount,
fee: Fee?,
@ -141,6 +185,28 @@ internal class DefaultTransactionRepository(
}
}
override suspend fun getAllowance(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency.Token,
spenderAddress: String,
): BigDecimal {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network)
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
val allowanceResult = (walletManager as? Approver)?.getAllowance(
spenderAddress,
Token(
symbol = blockchain.currency,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
) ?: error("Cannot cast to Approver")
return allowanceResult.fold(
onSuccess = { it },
onFailure = { error(it) },
)
}
private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType {
return when (transactionType) {
TransactionType.APPROVE -> SdkTransactionType.APPROVE

View file

@ -9,7 +9,6 @@ android {
namespace = "com.tangem.domain.staking"
}
dependencies {
api(projects.domain.staking.models)
@ -20,4 +19,8 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.features.staking.api)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.staking.model
sealed class StakingApproval {
data class Needed(val spenderAddress: String) : StakingApproval()
data object Empty : StakingApproval()
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
class GetConstructedStakingTransactionUseCase(
private val stakingRepository: StakingRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
suspend operator fun invoke(
networkId: String,
fee: Fee,
transactionId: String,
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
stakingRepository.constructTransaction(networkId, fee, transactionId)
}.mapLeft {
stakingErrorResolver.resolve(it)
}
}

View file

@ -22,19 +22,14 @@ class GetStakingTransactionUseCase(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Either<StakingError, StakingTransaction> {
): Either<StakingError, List<StakingTransaction>> {
return Either.catch {
val createAction = stakingRepository.createAction(userWalletId, network, params)
// workaround, sometimes transaction is not created immediately after actions/enter
delay(PATCH_TRANSACTION_REQUEST_DELAY)
val createdTransaction = createAction.transactions
?.get(createAction.currentStepIndex)
?: error("No available transaction to patch")
val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id)
patchedTransaction
createAction.transactions ?: error("No available transaction to patch")
}.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.StakingApproval
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
@ -10,9 +11,9 @@ class IsApproveNeededUseCase(
private val stakingRepository: StakingRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(cryptoCurrency: CryptoCurrency): Either<StakingError, Boolean> {
operator fun invoke(cryptoCurrency: CryptoCurrency): Either<StakingError, StakingApproval> {
return Either
.catch { stakingRepository.isApproveNeeded(cryptoCurrency) }
.catch { stakingRepository.getStakingApproval(cryptoCurrency) }
.mapLeft { stakingErrorResolver.resolve(it) }
}
}

View file

@ -1,7 +1,12 @@
package com.tangem.domain.staking.repositories
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
@ -66,7 +71,11 @@ interface StakingRepository {
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(transactionId: String): StakingTransaction
suspend fun constructTransaction(
networkId: String,
fee: Fee,
transactionId: String,
): Pair<StakingTransaction, TransactionData.Compiled>
suspend fun submitHash(transactionId: String, transactionHash: String)
@ -77,6 +86,6 @@ interface StakingRepository {
/** Returns whether additional staking is possible if there is already active staking */
fun isStakeMoreAvailable(networkId: Network.ID): Boolean
/** Returns whether approve is needed before staking */
fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean
/** Returns staking approval */
fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval
}

View file

@ -43,4 +43,7 @@ dependencies {
/** Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.domain.tokens.repository
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
@ -209,7 +213,11 @@ class MockStakingRepository : StakingRepository {
)
}
override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction(
override suspend fun constructTransaction(
networkId: String,
fee: Fee,
transactionId: String,
): Pair<StakingTransaction, TransactionData.Compiled> = StakingTransaction(
id = "id",
network = NetworkType.SOLANA,
status = StakingTransactionStatus.SIGNED,
@ -224,6 +232,9 @@ class MockStakingRepository : StakingRepository {
explorerUrl = null,
ledgerHwAppId = null,
isMessage = false,
) to TransactionData.Compiled(
value = TransactionData.Compiled.Data.RawString(""),
status = TransactionStatus.Unconfirmed,
)
override suspend fun submitHash(transactionId: String, transactionHash: String) {
@ -240,5 +251,5 @@ class MockStakingRepository : StakingRepository {
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true
override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean = true
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty
}

View file

@ -1,11 +1,16 @@
package com.tangem.domain.transaction
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.CommonSigner
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionSendResult
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.models.TransactionType
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
import java.math.BigInteger
interface TransactionRepository {
@ -20,7 +25,18 @@ interface TransactionRepository {
network: Network,
txExtras: TransactionExtras?,
hash: String?,
): TransactionData.Uncompiled?
): TransactionData.Uncompiled
@Suppress("LongParameterList")
suspend fun createApprovalTransaction(
amount: Amount,
fee: Fee,
contractAddress: String,
spenderAddress: String,
userWalletId: UserWalletId,
network: Network,
hash: String?,
): TransactionData.Uncompiled
@Suppress("LongParameterList")
suspend fun validateTransaction(
@ -49,4 +65,10 @@ interface TransactionRepository {
nonce: BigInteger?,
gasLimit: BigInteger?,
): TransactionExtras
suspend fun getAllowance(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency.Token,
spenderAddress: String,
): BigDecimal
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
class CreateApprovalTransactionUseCase(
private val transactionRepository: TransactionRepository,
) {
@Suppress("LongParameterList")
suspend operator fun invoke(
cryptoCurrency: CryptoCurrency.Token,
userWalletId: UserWalletId,
amount: BigDecimal,
fee: Fee,
contractAddress: String,
spenderAddress: String,
hash: String? = null,
) = Either.catch {
transactionRepository.createApprovalTransaction(
amount = amount.convertToAmount(cryptoCurrency),
contractAddress = contractAddress,
spenderAddress = spenderAddress,
userWalletId = userWalletId,
network = cryptoCurrency.network,
fee = fee,
hash = hash,
)
}
}

View file

@ -26,17 +26,15 @@ class CreateTransactionUseCase(
txExtras: TransactionExtras? = null,
hash: String? = null,
) = Either.catch {
requireNotNull(
transactionRepository.createTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
txExtras = txExtras,
hash = hash,
),
) { "Failed to create transaction" }
transactionRepository.createTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
txExtras = txExtras,
hash = hash,
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
class GetAllowanceUseCase(
private val transactionRepository: TransactionRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
spenderAddress: String,
): Either<Throwable, BigDecimal> {
return Either.catch {
transactionRepository.getAllowance(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency as CryptoCurrency.Token,
spenderAddress = spenderAddress,
)
}
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateAssentApprovalTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val fee: TransactionFee,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(),
bottomSheetConfig = null,
)
}
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
return if (this is StakingStates.ConfirmationState.Data) {
copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = fee.normal,
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(isClickable = true),
isPrimaryButtonEnabled = true,
isApprovalNeeded = true,
)
} else {
this
}
}
}

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.common.extensions.hexToBytes
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.amountScreen.models.AmountState
@ -18,15 +18,20 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
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.StakingTransactionType
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
@ -40,6 +45,7 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
import com.tangem.utils.Provider
@ -51,6 +57,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.properties.Delegates
@ -64,6 +71,7 @@ internal class StakingViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getStakingTransactionUseCase: GetStakingTransactionUseCase,
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
private val estimateGasUseCase: EstimateGasUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -71,6 +79,9 @@ internal class StakingViewModel @Inject constructor(
private val submitHashUseCase: SubmitHashUseCase,
private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase,
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
private val getAllowanceUseCase: GetAllowanceUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val isApproveNeededUseCase: IsApproveNeededUseCase,
private val clipboardManager: ClipboardManager,
private val vibratorHapticManager: VibratorHapticManager,
@ -101,7 +112,7 @@ internal class StakingViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private var isApprovalNeeded: Boolean = false
private var stakingApproval: StakingApproval = StakingApproval.Empty
private var approvalJobHolder: JobHolder = JobHolder()
@ -121,7 +132,7 @@ internal class StakingViewModel @Inject constructor(
}
stakingStateRouter.onNextClick()
if (isAssentState()) {
estimateGas(pendingActions)
getFee(pendingActions)
}
}
@ -139,6 +150,9 @@ internal class StakingViewModel @Inject constructor(
value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state")
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val amountState = value.amountState as? AmountState.Data ?: error("No amount provided")
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
@ -146,8 +160,7 @@ internal class StakingViewModel @Inject constructor(
params = ActionParams(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
amount = amountValue,
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
@ -159,19 +172,27 @@ internal class StakingViewModel @Inject constructor(
error(it)
}
stakingTransaction.unsignedTransaction?.let {
sendStakingTransaction(
transactionId = stakingTransaction.id,
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
txData = TransactionData.Compiled(value = it.hexToBytes()),
pendingActionList = confirmationState.pendingActions,
)
} ?: error("No unsigned transaction available")
stakingTransaction
.filterNot { it.type == StakingTransactionType.APPROVAL }
.forEach { transaction ->
val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase(
networkId = cryptoCurrencyStatus.currency.network.id.value,
fee = fee,
transactionId = transaction.id,
).getOrNull() ?: error("No constructed transaction")
sendStakingTransaction(
transactionId = constructedTransaction.id,
gasEstimate = constructedTransaction.gasEstimate ?: error("No gas estimate available"),
txData = transactionData,
pendingActionList = confirmationState.pendingActions,
)
}
}
}
}
private fun estimateGas(pendingActions: ImmutableList<PendingAction>) {
private fun getFee(pendingActions: ImmutableList<PendingAction>) {
viewModelScope.launch {
stateController.update(
SetConfirmationStateLoadingTransformer(
@ -184,38 +205,99 @@ internal class StakingViewModel @Inject constructor(
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse {
stateController.update(AddStakingErrorTransformer(it))
return@launch
}
val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value
?: error("No available address")
val validatorAddress = validatorState.chosenValidator.address
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
)
val approval = stakingApproval as? StakingApproval.Needed
if (approval != null) {
val allowance = getAllowanceUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
spenderAddress = approval.spenderAddress,
).getOrElse { BigDecimal.ZERO }
if (allowance < amount) {
getApproveFee(
amount = amount,
validatorAddress = validatorAddress,
)
} else {
estimateGas(
pendingActions = pendingActions,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
} else {
estimateGas(
pendingActions = pendingActions,
amount = amount,
sourceAddress = sourceAddress,
validatorAddress = validatorAddress,
)
}
}
}
private suspend fun estimateGas(
pendingActions: ImmutableList<PendingAction>,
amount: BigDecimal,
sourceAddress: String,
validatorAddress: String,
) {
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = value.actionType,
integrationId = yield.id,
amount = amount,
address = sourceAddress,
validatorAddress = validatorAddress,
token = yield.token,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse {
stateController.update(AddStakingErrorTransformer(it))
return
}
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
)
}
private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) {
val approvalFee = getFeeUseCase(
amount = amount,
destination = validatorAddress,
userWallet = userWallet,
cryptoCurrency = cryptoCurrencyStatus.currency,
).getOrElse {
// TODO staking error
return
}
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = approvalFee,
),
)
}
override fun onPrevClick() {
stakingStateRouter.onPrevClick()
}
@ -286,7 +368,60 @@ internal class StakingViewModel @Inject constructor(
},
)
// todo staking polygon
val tokenCryptoCurrency =
cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: error("No token currency")
val amountValue = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
?: error("No confirmation state")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val approval = stakingApproval as? StakingApproval.Needed ?: error("No staking approve spender address")
val approvalTransaction = createApprovalTransactionUseCase(
amount = amountValue,
contractAddress = tokenCryptoCurrency.contractAddress,
spenderAddress = approval.spenderAddress,
fee = fee,
cryptoCurrency = tokenCryptoCurrency,
userWalletId = userWalletId,
).fold(
ifLeft = { error ->
Timber.e(error.toString())
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
return@launch
},
ifRight = { it },
)
sendTransactionUseCase(
txData = approvalTransaction,
userWallet = userWallet,
network = tokenCryptoCurrency.network,
).fold(
ifLeft = { error ->
Timber.e(error.toString())
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
},
ifRight = {
getFee(confirmationState.pendingActions)
stateController.update(DismissBottomSheetStateTransformer())
},
)
}.saveIn(approvalJobHolder)
}
@ -319,7 +454,7 @@ internal class StakingViewModel @Inject constructor(
}
private fun setupApprovalNeeded() {
isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false }
stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty }
}
private fun subscribeOnCurrencyStatusUpdates() {

View file

@ -7,7 +7,6 @@ import arrow.core.raise.either
import arrow.core.right
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.datasource.api.common.response.ApiResponse
@ -362,10 +361,10 @@ internal class DefaultSwapRepository @Inject constructor(
),
) ?: error("Cannot cast to Approver")
return when (result) {
is Result.Success -> result.data
is Result.Failure -> error(result.error)
}
return result.fold(
onSuccess = { it },
onFailure = { error(it) },
)
}
override suspend fun getApproveData(

View file

@ -88,7 +88,7 @@ markdown = "0.7.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-733"
tangemBlockchainSdk = "develop-734"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-375"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^