Updated on 2026-08-14
This commit is contained in:
parent
c3c5874060
commit
016b1e18d2
13 changed files with 858 additions and 421 deletions
|
|
@ -248,7 +248,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
||||
override suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
||||
return p2pEthPoolVaultsStore.getSync()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import java.math.BigDecimal
|
|||
* Strategy interface for staking integrations.
|
||||
* Abstracts over StakeKit and P2PEthPool staking providers.
|
||||
*/
|
||||
interface StakingIntegration {
|
||||
sealed interface StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,16 @@ interface P2PEthPoolRepository {
|
|||
*/
|
||||
fun getVaultsFlow(): Flow<List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Get cached vaults synchronously from local store.
|
||||
*
|
||||
* This returns vaults from the local cache/store without network call.
|
||||
* Call [fetchVaults] first to populate the cache from the network.
|
||||
*
|
||||
* @return List of cached vaults (empty if cache is not populated)
|
||||
*/
|
||||
suspend fun getVaultsSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Check P2PEthPool staking availability by finding public vault
|
||||
*
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ dependencies {
|
|||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.moshi)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM
|
|||
import com.tangem.features.staking.impl.presentation.state.events.StakingEvent
|
||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeTransactionLoader
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingOperationsFactory
|
||||
import com.tangem.features.staking.impl.presentation.state.helpers.StakingTransactionSender
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.*
|
||||
import com.tangem.features.staking.impl.presentation.state.transformers.amount.*
|
||||
|
|
@ -128,8 +129,7 @@ internal class StakingModel @Inject constructor(
|
|||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
||||
private val stakingTransactionLoader: StakingTransactionSender.Factory,
|
||||
private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory,
|
||||
private val stakingOperationsFactory: StakingOperationsFactory,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
|
|
@ -175,8 +175,7 @@ internal class StakingModel @Inject constructor(
|
|||
StakeKitIntegration(integrationId, yield)
|
||||
}
|
||||
StakingIntegrationID.P2PEthPool -> {
|
||||
// TODO p2p avoid network call
|
||||
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
|
||||
val vaults = p2pEthPoolRepository.getVaultsSync()
|
||||
P2PEthPoolIntegration(integrationId, vaults)
|
||||
}
|
||||
}
|
||||
|
|
@ -221,16 +220,16 @@ internal class StakingModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private val feeLoader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
stakingFeeTransactionLoader.create(
|
||||
private val feeLoader: StakingFeeLoader by lazy(LazyThreadSafetyMode.NONE) {
|
||||
stakingOperationsFactory.createFeeLoader(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
integration = integration,
|
||||
)
|
||||
}
|
||||
|
||||
private val transactionSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
stakingTransactionLoader.create(
|
||||
private val transactionSender: StakingTransactionSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
stakingOperationsFactory.createTransactionSender(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
integration = integration,
|
||||
|
|
@ -391,7 +390,8 @@ internal class StakingModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value)
|
||||
stateController.update(SetConfirmationStateInProgressTransformer())
|
||||
transactionSender.constructAndSendTransactions(
|
||||
transactionSender.send(
|
||||
StakingTransactionSender.Callbacks(
|
||||
onConstructSuccess = { constructedTransactions ->
|
||||
transactionsInProgress.addAll(constructedTransactions)
|
||||
},
|
||||
|
|
@ -405,7 +405,9 @@ internal class StakingModel @Inject constructor(
|
|||
cryptoCurrencyStatus,
|
||||
)
|
||||
transactionsInProgress.clear()
|
||||
stateController.update(SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus))
|
||||
stateController.update(
|
||||
SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus),
|
||||
)
|
||||
},
|
||||
onSendError = { error ->
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -434,6 +436,11 @@ internal class StakingModel @Inject constructor(
|
|||
)
|
||||
updateNotifications()
|
||||
},
|
||||
onTransactionExpired = {
|
||||
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
||||
getFee()
|
||||
},
|
||||
),
|
||||
)
|
||||
}.saveIn(sendTransactionJobHolder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class P2PEthPoolFeeLoader @AssistedInject constructor(
|
||||
private val transactionCreator: P2PEthPoolTransactionCreator,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val integration: P2PEthPoolIntegration,
|
||||
) : StakingFeeLoader {
|
||||
|
||||
override suspend fun getFee(
|
||||
onStakingFee: (Fee, Boolean) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
) {
|
||||
transactionCreator.createTransaction(cryptoCurrencyStatus).fold(
|
||||
ifLeft = onStakingFeeError,
|
||||
ifRight = { unsignedTx ->
|
||||
val fee = convertUnsignedTxToFee(unsignedTx)
|
||||
onStakingFee(fee, false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertUnsignedTxToFee(unsignedTx: P2PEthPoolUnsignedTx): Fee {
|
||||
val blockchain = integration.integrationId.blockchain
|
||||
val decimals = blockchain.decimals()
|
||||
val feeInWei = unsignedTx.gasLimit * unsignedTx.maxFeePerGas
|
||||
val feeValue = feeInWei.movePointLeft(decimals)
|
||||
return Fee.Common(
|
||||
Amount(
|
||||
currencySymbol = blockchain.currency,
|
||||
value = feeValue,
|
||||
decimals = decimals,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, integration: P2PEthPoolIntegration): P2PEthPoolFeeLoader
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class P2PEthPoolTransactionCreator @Inject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
) {
|
||||
|
||||
suspend fun createTransaction(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> {
|
||||
val params = extractParams(cryptoCurrencyStatus)
|
||||
?: return Either.Left(StakingError.DomainError("Invalid state for transaction creation"))
|
||||
|
||||
return createTransaction(
|
||||
actionType = params.actionType,
|
||||
amount = params.amount,
|
||||
vaultAddress = params.vaultAddress,
|
||||
sourceAddress = params.sourceAddress,
|
||||
)
|
||||
}
|
||||
|
||||
fun extractParams(cryptoCurrencyStatus: CryptoCurrencyStatus): TransactionParams? {
|
||||
val state = stateController.value
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: return null
|
||||
|
||||
val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
?: return null
|
||||
|
||||
val vaultAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
|
||||
?: state.balanceState?.targetAddress
|
||||
?: return null
|
||||
|
||||
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
?: return null
|
||||
|
||||
return TransactionParams(
|
||||
actionType = state.actionType,
|
||||
amount = amount,
|
||||
vaultAddress = vaultAddress,
|
||||
sourceAddress = sourceAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createTransaction(
|
||||
actionType: StakingActionCommonType,
|
||||
amount: BigDecimal,
|
||||
vaultAddress: String,
|
||||
sourceAddress: String,
|
||||
): Either<StakingError, P2PEthPoolUnsignedTx> {
|
||||
val network = P2PEthPoolStakingConfig.activeNetwork
|
||||
|
||||
return when (actionType) {
|
||||
is StakingActionCommonType.Enter -> {
|
||||
p2pEthPoolRepository.createDepositTransaction(
|
||||
network = network,
|
||||
delegatorAddress = sourceAddress,
|
||||
vaultAddress = vaultAddress,
|
||||
amount = amount.toPlainString(),
|
||||
)
|
||||
}
|
||||
is StakingActionCommonType.Exit -> {
|
||||
p2pEthPoolRepository.createWithdrawTransaction(
|
||||
network = network,
|
||||
stakerAddress = sourceAddress,
|
||||
)
|
||||
}
|
||||
is StakingActionCommonType.Pending -> {
|
||||
Either.Left(StakingError.DomainError("Pending actions not supported for P2PEthPool"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TransactionParams(
|
||||
val actionType: StakingActionCommonType,
|
||||
val amount: BigDecimal,
|
||||
val vaultAddress: String,
|
||||
val sourceAddress: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.blockchain.blockchains.ethereum.models.EthereumCompiledTransaction
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.formatHex
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.transaction.usecase.PrepareForSendUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.math.BigInteger
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class P2PEthPoolTransactionSender @AssistedInject constructor(
|
||||
private val transactionCreator: P2PEthPoolTransactionCreator,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val prepareForSendUseCase: PrepareForSendUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val integration: P2PEthPoolIntegration,
|
||||
) : StakingTransactionSender {
|
||||
|
||||
private val balanceUpdater: StakingBalanceUpdater
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
|
||||
|
||||
override suspend fun send(callbacks: StakingTransactionSender.Callbacks) {
|
||||
val params = transactionCreator.extractParams(cryptoCurrencyStatus)
|
||||
?: run {
|
||||
callbacks.onConstructError(
|
||||
StakingError.DomainError("Invalid state for transaction"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val unsignedTx = transactionCreator.createTransaction(cryptoCurrencyStatus).getOrElse { error ->
|
||||
callbacks.onConstructError(error)
|
||||
return
|
||||
}
|
||||
|
||||
val compiledTxJson = createCompiledTransactionJson(unsignedTx, params.sourceAddress)
|
||||
|
||||
val transactionData = TransactionData.Compiled(
|
||||
value = TransactionData.Compiled.Data.RawString(compiledTxJson),
|
||||
)
|
||||
|
||||
val signedTxBytes = prepareForSendUseCase(
|
||||
transactionData = transactionData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).getOrElse { error ->
|
||||
callbacks.onSendError(error)
|
||||
return
|
||||
}
|
||||
|
||||
val signedTxHex = signedTxBytes.toHexString().lowercase().formatHex()
|
||||
|
||||
p2pEthPoolRepository.broadcastTransaction(
|
||||
network = P2PEthPoolStakingConfig.activeNetwork,
|
||||
signedTransaction = signedTxHex,
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
callbacks.onConstructError(error)
|
||||
},
|
||||
ifRight = { broadcastResult ->
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = broadcastResult.hash,
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
).getOrNull().orEmpty()
|
||||
|
||||
balanceUpdater.updateAfterTransaction()
|
||||
callbacks.onSendSuccess(txUrl)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCompiledTransactionJson(unsignedTx: P2PEthPoolUnsignedTx, fromAddress: String): String {
|
||||
val compiledTx = EthereumCompiledTransaction(
|
||||
from = fromAddress,
|
||||
to = unsignedTx.to,
|
||||
data = unsignedTx.data,
|
||||
value = unsignedTx.value.toBigInteger().toHexString(),
|
||||
nonce = unsignedTx.nonce,
|
||||
chainId = unsignedTx.chainId,
|
||||
gasLimit = unsignedTx.gasLimit.toBigInteger().toHexString(),
|
||||
gasPrice = null, // EIP-1559: gasPrice is null
|
||||
maxFeePerGas = unsignedTx.maxFeePerGas.toBigInteger().toHexString(),
|
||||
maxPriorityFeePerGas = unsignedTx.maxPriorityFeePerGas.toBigInteger().toHexString(),
|
||||
type = EIP_1559_TX_TYPE,
|
||||
)
|
||||
return ethereumCompiledTxAdapter.toJson(compiledTx)
|
||||
}
|
||||
|
||||
private fun BigInteger.toHexString(): String {
|
||||
val hex = toString(HEX_RADIX)
|
||||
val paddedHex = if (hex.length % 2 != 0) "0$hex" else hex
|
||||
return HEX_PREFIX + paddedHex
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: P2PEthPoolIntegration,
|
||||
): P2PEthPoolTransactionSender
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val HEX_PREFIX = "0x"
|
||||
const val HEX_RADIX = 16
|
||||
const val EIP_1559_TX_TYPE = 2
|
||||
|
||||
val ethereumCompiledTxAdapter: com.squareup.moshi.JsonAdapter<EthereumCompiledTransaction> =
|
||||
Moshi.Builder().build().adapter(EthereumCompiledTransaction::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.EstimateGasUseCase
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.StakeKitIntegration
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
|
||||
|
|
@ -36,7 +36,7 @@ import kotlinx.coroutines.delay
|
|||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||
internal class StakeKitFeeLoader @AssistedInject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val estimateGasUseCase: EstimateGasUseCase,
|
||||
|
|
@ -44,10 +44,10 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val integration: StakingIntegration,
|
||||
) {
|
||||
@Assisted private val integration: StakeKitIntegration,
|
||||
) : StakingFeeLoader {
|
||||
|
||||
suspend fun getFee(
|
||||
override suspend fun getFee(
|
||||
onStakingFee: (Fee, Boolean) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
|
|
@ -57,21 +57,41 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("Illegal state")
|
||||
|
||||
val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
|
||||
val targetAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenTarget?.address
|
||||
?: state.balanceState?.targetAddress
|
||||
?: error("No target address provided")
|
||||
|
||||
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
?: error("No amount provided")
|
||||
|
||||
val pendingAction = confirmationState.pendingAction
|
||||
val pendingActions = confirmationState.pendingActions
|
||||
getStakeKitFee(
|
||||
confirmationState = confirmationState,
|
||||
actionType = state.actionType,
|
||||
amount = amount,
|
||||
validatorAddress = targetAddress,
|
||||
onStakingFee = onStakingFee,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
onApprovalFee = onApprovalFee,
|
||||
onFeeError = onFeeError,
|
||||
)
|
||||
}
|
||||
|
||||
val isEnter = state.actionType is StakingActionCommonType.Enter
|
||||
private suspend fun getStakeKitFee(
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
actionType: StakingActionCommonType,
|
||||
amount: BigDecimal,
|
||||
validatorAddress: String,
|
||||
onStakingFee: (Fee, Boolean) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
) {
|
||||
val isEnter = actionType is StakingActionCommonType.Enter
|
||||
val isApprovalNeeded = confirmationState.isApprovalNeeded
|
||||
val isAllowanceNotEnough = confirmationState.allowance < amount
|
||||
|
||||
if (isEnter && isApprovalNeeded && isAllowanceNotEnough) {
|
||||
getApproveFee(
|
||||
getApprovalFee(
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onApprovalFee = onApprovalFee,
|
||||
|
|
@ -79,8 +99,8 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
)
|
||||
} else {
|
||||
estimateGas(
|
||||
pendingAction = pendingAction,
|
||||
pendingActions = pendingActions,
|
||||
pendingAction = confirmationState.pendingAction,
|
||||
pendingActions = confirmationState.pendingActions,
|
||||
amount = amount,
|
||||
validatorAddress = validatorAddress,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
|
|
@ -105,19 +125,59 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
pendingActions = pendingActions,
|
||||
)
|
||||
) {
|
||||
estimateCompositeGas(
|
||||
pendingActions = pendingActions,
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
onStakingFeeError = onStakingFeeError,
|
||||
) ?: return
|
||||
} else {
|
||||
estimateSingleGas(
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
action = pendingAction,
|
||||
).getOrElse { error ->
|
||||
onStakingFeeError(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val feeAmount = Amount(
|
||||
currencySymbol = gasEstimate.token.symbol,
|
||||
value = gasEstimate.amount,
|
||||
decimals = gasEstimate.token.decimals,
|
||||
)
|
||||
onStakingFee(
|
||||
Fee.Common(feeAmount),
|
||||
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = feeAmount.type),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates gas for several staking transactions.
|
||||
*/
|
||||
private suspend fun estimateCompositeGas(
|
||||
pendingActions: ImmutableList<PendingAction>?,
|
||||
amount: BigDecimal,
|
||||
sourceAddress: String,
|
||||
validatorAddress: String,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
): StakingGasEstimate? {
|
||||
val result = coroutineScope {
|
||||
pendingActions?.map { action ->
|
||||
async {
|
||||
// Simultaneous or quick api calls can sometimes return ZERO fee
|
||||
estimateFeeRetry {
|
||||
estimateFee(
|
||||
// Simultaneous or quick API calls can sometimes return ZERO fee
|
||||
estimateGasRetry {
|
||||
estimateSingleGas(
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
action = action,
|
||||
)
|
||||
}.getOrElse {
|
||||
onStakingFeeError(it)
|
||||
}.getOrElse { gasError ->
|
||||
onStakingFeeError(gasError)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -126,40 +186,19 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
|
||||
if (result.isNullOrEmpty()) {
|
||||
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
val totalAmount = result.sumOf { it.amount }
|
||||
val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() }
|
||||
StakingGasEstimate(
|
||||
return StakingGasEstimate(
|
||||
amount = totalAmount,
|
||||
token = result.first().token,
|
||||
gasLimit = totalGasLimit.toPlainString().orEmpty(),
|
||||
)
|
||||
} else {
|
||||
estimateFee(
|
||||
amount = amount,
|
||||
sourceAddress = sourceAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
action = pendingAction,
|
||||
).getOrElse {
|
||||
onStakingFeeError(it)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val amount = Amount(
|
||||
currencySymbol = gasEstimate.token.symbol,
|
||||
value = gasEstimate.amount,
|
||||
decimals = gasEstimate.token.decimals,
|
||||
)
|
||||
onStakingFee(
|
||||
Fee.Common(amount),
|
||||
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun estimateFee(
|
||||
private suspend fun estimateSingleGas(
|
||||
amount: BigDecimal,
|
||||
sourceAddress: String,
|
||||
validatorAddress: String,
|
||||
|
|
@ -179,7 +218,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private suspend fun getApproveFee(
|
||||
private suspend fun getApprovalFee(
|
||||
amount: BigDecimal,
|
||||
validatorAddress: String,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
|
|
@ -187,6 +226,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
) {
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return onApprovalFeeError(GetFeeError.UnknownError)
|
||||
|
||||
val approvalTransactionData = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -196,34 +236,29 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
).getOrElse {
|
||||
return onApprovalFeeError(GetFeeError.DataError(it))
|
||||
}
|
||||
|
||||
getFeeUseCase(
|
||||
userWallet = userWallet,
|
||||
network = tokenCurrency.network,
|
||||
transactionData = approvalTransactionData,
|
||||
).fold(
|
||||
ifRight = { fee ->
|
||||
onApprovalFee(fee)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
onApprovalFeeError(error)
|
||||
},
|
||||
ifRight = onApprovalFee,
|
||||
ifLeft = onApprovalFeeError,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun estimateFeeRetry(
|
||||
times: Int = 3,
|
||||
delay: Long = 1000,
|
||||
private suspend fun estimateGasRetry(
|
||||
times: Int = RETRY_COUNT,
|
||||
delayMs: Long = RETRY_DELAY_MS,
|
||||
block: suspend () -> Either<StakingError, StakingGasEstimate>,
|
||||
): Either<StakingError, StakingGasEstimate> {
|
||||
repeat(times - 1) {
|
||||
val feeResult = block()
|
||||
feeResult.fold(
|
||||
ifLeft = { return feeResult },
|
||||
ifRight = {
|
||||
if (!it.amount.isZero()) return feeResult
|
||||
},
|
||||
ifRight = { estimate -> if (!estimate.amount.isZero()) return feeResult },
|
||||
)
|
||||
delay(delay)
|
||||
delay(delayMs)
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
|
@ -233,7 +268,12 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
|||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: StakingIntegration,
|
||||
): StakingFeeTransactionLoader
|
||||
integration: StakeKitIntegration,
|
||||
): StakeKitFeeLoader
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RETRY_COUNT = 3
|
||||
const val RETRY_DELAY_MS = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
|
||||
import com.tangem.domain.staking.GetStakingTransactionsUseCase
|
||||
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
|
||||
import com.tangem.domain.staking.SubmitHashUseCase
|
||||
import com.tangem.domain.staking.model.StakeKitIntegration
|
||||
import com.tangem.domain.staking.model.SubmitHashData
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
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.StakingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakeKitTransactionSender @AssistedInject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val getStakingTransactionsUseCase: GetStakingTransactionsUseCase,
|
||||
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val submitHashUseCase: SubmitHashUseCase,
|
||||
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val integration: StakeKitIntegration,
|
||||
@Assisted private val isAmountSubtractAvailable: Boolean,
|
||||
) : StakingTransactionSender {
|
||||
|
||||
private val balanceUpdater: StakingBalanceUpdater
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
|
||||
|
||||
override suspend fun send(callbacks: StakingTransactionSender.Callbacks) {
|
||||
constructAndSendTransactions(
|
||||
onConstructSuccess = callbacks.onConstructSuccess,
|
||||
onConstructError = callbacks.onConstructError,
|
||||
onSendSuccess = callbacks.onSendSuccess,
|
||||
onSendError = callbacks.onSendError,
|
||||
onFeeIncreased = callbacks.onFeeIncreased,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun constructAndSendTransactions(
|
||||
onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
onSendSuccess: (String) -> Unit,
|
||||
onSendError: (SendTransactionError) -> Unit,
|
||||
onFeeIncreased: (Fee, Boolean) -> Unit,
|
||||
) {
|
||||
val state = stateController.value
|
||||
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("No confirmation state")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val amountState = state.amountState as? AmountState.Data ?: error("No amount state")
|
||||
|
||||
val stakingTransactions = getStakingTransactions(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
val fullTransactionsData = getConstructedTransactions(
|
||||
stakingTransactions = stakingTransactions,
|
||||
fee = fee,
|
||||
amount = amountState.amountTextField.cryptoAmount.value.orZero(),
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
if (fullTransactionsData.isNullOrEmpty()) {
|
||||
onConstructError(StakingError.DomainError("fullTransactionsData is null or empty"))
|
||||
return
|
||||
}
|
||||
|
||||
val totalFee = fullTransactionsData.sumOf { it.stakeKitTransaction.gasEstimate?.amount.orZero() }
|
||||
|
||||
if (fee.amount.value.orZero() >= totalFee) {
|
||||
onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction })
|
||||
sendTransaction(
|
||||
fullTransactionsData = fullTransactionsData,
|
||||
onSendSuccess = onSendSuccess,
|
||||
onSendError = onSendError,
|
||||
)
|
||||
} else {
|
||||
val amount = fee.amount.copy(value = totalFee)
|
||||
onFeeIncreased(
|
||||
Fee.Common(amount),
|
||||
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransactions(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
val isComposePendingActions = isCompositePendingActions(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
pendingActions = confirmationState.pendingActions,
|
||||
)
|
||||
if (isComposePendingActions) {
|
||||
confirmationState.pendingActions?.map { action ->
|
||||
async {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
action = action,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}?.awaitAll()?.flatten()
|
||||
} else {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getConstructedTransactions(
|
||||
stakingTransactions: List<StakingTransaction>?,
|
||||
fee: Fee,
|
||||
amount: BigDecimal,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
stakingTransactions
|
||||
?.filterNot {
|
||||
it.type == StakingTransactionType.APPROVAL || it.status == StakingTransactionStatus.SKIPPED
|
||||
}
|
||||
?.map { transaction ->
|
||||
async {
|
||||
getConstructedStakingTransactionUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
fee = fee,
|
||||
amount = amount.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
transactionId = transaction.id,
|
||||
).fold(
|
||||
ifRight = { (constructedTransaction, transactionData) ->
|
||||
FullTransactionData(
|
||||
stakeKitTransaction = constructedTransaction,
|
||||
tangemTransaction = transactionData,
|
||||
)
|
||||
},
|
||||
ifLeft = { error ->
|
||||
onConstructError(error)
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
?.awaitAll()
|
||||
?.filterNotNull()
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransaction(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
action: PendingAction? = confirmationState.pendingAction,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
): List<StakingTransaction> {
|
||||
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
|
||||
?: error("No validator provided")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
?: error("No available address")
|
||||
val amountState = state.amountState as? AmountState.Data
|
||||
?: error("No amount provided")
|
||||
|
||||
val validatorAddress = validatorState.chosenTarget.address
|
||||
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
|
||||
|
||||
return getStakingTransactionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = state.actionType,
|
||||
integrationId = integration.integrationId.value,
|
||||
amount = amount,
|
||||
address = defaultAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
).getOrElse { error ->
|
||||
onConstructError(error)
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendTransaction(
|
||||
fullTransactionsData: List<FullTransactionData>,
|
||||
onSendSuccess: (txUrl: String) -> Unit,
|
||||
onSendError: (SendTransactionError) -> Unit,
|
||||
) {
|
||||
if (fullTransactionsData.isEmpty()) return
|
||||
|
||||
val sortedTransactions = fullTransactionsData.sortedBy { it.stakeKitTransaction.stepIndex }
|
||||
|
||||
val firstTransaction = sortedTransactions.first()
|
||||
val network = firstTransaction.stakeKitTransaction.network
|
||||
|
||||
val sendMode = if (network == NetworkType.SOLANA &&
|
||||
firstTransaction.stakeKitTransaction.type == StakingTransactionType.SPLIT
|
||||
) {
|
||||
TransactionSender.MultipleTransactionSendMode.WAIT_AFTER_FIRST
|
||||
} else {
|
||||
TransactionSender.MultipleTransactionSendMode.DEFAULT
|
||||
}
|
||||
|
||||
sendTransactionUseCase(
|
||||
txsData = sortedTransactions.map { it.tangemTransaction },
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
sendMode = sendMode,
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
onSendError(error)
|
||||
},
|
||||
ifRight = { transactionHashes ->
|
||||
submitHash(
|
||||
transactions = sortedTransactions.map { it.stakeKitTransaction },
|
||||
transactionHashes = transactionHashes,
|
||||
)
|
||||
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = transactionHashes.last(),
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
).getOrNull().orEmpty()
|
||||
|
||||
balanceUpdater.updateAfterTransaction()
|
||||
onSendSuccess(txUrl)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun submitHash(transactions: List<StakingTransaction>, transactionHashes: List<String>) {
|
||||
transactions
|
||||
.zip(transactionHashes)
|
||||
.forEach { (transaction, transactionHash) ->
|
||||
submitHashUseCase(
|
||||
SubmitHashData(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
),
|
||||
)
|
||||
.onLeft {
|
||||
saveUnsubmittedHashUseCase.invoke(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
)
|
||||
}.onRight {
|
||||
Timber.d("Successful hash submission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal {
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
|
||||
val feeValue = fee.amount.value ?: error("No fee value")
|
||||
val isEnterAction = stateController.value.actionType is StakingActionCommonType.Enter
|
||||
|
||||
return checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy.orZero(),
|
||||
)
|
||||
}
|
||||
|
||||
private data class FullTransactionData(
|
||||
val stakeKitTransaction: StakingTransaction,
|
||||
val tangemTransaction: TransactionData.Compiled,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: StakeKitIntegration,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
): StakeKitTransactionSender
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
|
||||
internal interface StakingFeeLoader {
|
||||
|
||||
suspend fun getFee(
|
||||
onStakingFee: (Fee, Boolean) -> Unit,
|
||||
onStakingFeeError: (StakingError) -> Unit,
|
||||
onApprovalFee: (TransactionFee) -> Unit,
|
||||
onFeeError: (GetFeeError) -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.model.P2PEthPoolIntegration
|
||||
import com.tangem.domain.staking.model.StakeKitIntegration
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class StakingOperationsFactory @Inject constructor(
|
||||
private val stakeKitFeeLoaderFactory: StakeKitFeeLoader.Factory,
|
||||
private val p2pEthPoolFeeLoaderFactory: P2PEthPoolFeeLoader.Factory,
|
||||
private val stakeKitTransactionSenderFactory: StakeKitTransactionSender.Factory,
|
||||
private val p2pEthPoolTransactionSenderFactory: P2PEthPoolTransactionSender.Factory,
|
||||
) {
|
||||
|
||||
fun createFeeLoader(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: StakingIntegration,
|
||||
): StakingFeeLoader {
|
||||
return when (integration) {
|
||||
is StakeKitIntegration -> stakeKitFeeLoaderFactory.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
integration = integration,
|
||||
)
|
||||
is P2PEthPoolIntegration -> p2pEthPoolFeeLoaderFactory.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
integration = integration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createTransactionSender(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: StakingIntegration,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
): StakingTransactionSender {
|
||||
return when (integration) {
|
||||
is StakeKitIntegration -> stakeKitTransactionSenderFactory.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
integration = integration,
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
)
|
||||
is P2PEthPoolIntegration -> p2pEthPoolTransactionSenderFactory.create(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWallet = userWallet,
|
||||
integration = integration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,312 +1,20 @@
|
|||
package com.tangem.features.staking.impl.presentation.state.helpers
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.PendingAction
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase
|
||||
import com.tangem.domain.staking.GetStakingTransactionsUseCase
|
||||
import com.tangem.domain.staking.SaveUnsubmittedHashUseCase
|
||||
import com.tangem.domain.staking.SubmitHashUseCase
|
||||
import com.tangem.domain.staking.model.StakingIntegration
|
||||
import com.tangem.domain.staking.model.SubmitHashData
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
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.StakingTransaction
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus
|
||||
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.FeeState
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStateController
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingStates
|
||||
import com.tangem.features.staking.impl.presentation.state.StakingUiState
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class StakingTransactionSender @AssistedInject constructor(
|
||||
private val stateController: StakingStateController,
|
||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||
private val getStakingTransactionsUseCase: GetStakingTransactionsUseCase,
|
||||
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val submitHashUseCase: SubmitHashUseCase,
|
||||
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
@Assisted private val integration: StakingIntegration,
|
||||
@Assisted private val isAmountSubtractAvailable: Boolean,
|
||||
) {
|
||||
internal interface StakingTransactionSender {
|
||||
|
||||
private val balanceUpdater: StakingBalanceUpdater
|
||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
|
||||
suspend fun send(callbacks: Callbacks)
|
||||
|
||||
suspend fun constructAndSendTransactions(
|
||||
onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
onSendSuccess: (String) -> Unit,
|
||||
onSendError: (SendTransactionError) -> Unit,
|
||||
onFeeIncreased: (Fee, Boolean) -> Unit,
|
||||
) {
|
||||
val state = stateController.value
|
||||
|
||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||
?: error("No confirmation state")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val amountState = state.amountState as? AmountState.Data ?: error("No amount state")
|
||||
|
||||
val stakingTransactions = getStakingTransactions(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
val fullTransactionsData = getConstructedTransactions(
|
||||
stakingTransactions = stakingTransactions,
|
||||
fee = fee,
|
||||
amount = amountState.amountTextField.cryptoAmount.value.orZero(),
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
|
||||
if (fullTransactionsData.isNullOrEmpty()) {
|
||||
onConstructError(StakingError.DomainError("fullTransactionsData is null or empty"))
|
||||
return
|
||||
}
|
||||
|
||||
val totalFee = fullTransactionsData.sumOf { it.stakeKitTransaction.gasEstimate?.amount.orZero() }
|
||||
|
||||
if (fee.amount.value.orZero() >= totalFee) {
|
||||
onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction })
|
||||
sendStakingTransaction(
|
||||
fullTransactionsData = fullTransactionsData,
|
||||
onSendSuccess = onSendSuccess,
|
||||
onSendError = onSendError,
|
||||
)
|
||||
} else {
|
||||
val amount = fee.amount.copy(value = totalFee)
|
||||
onFeeIncreased(
|
||||
Fee.Common(amount),
|
||||
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type),
|
||||
class Callbacks(
|
||||
val onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||
val onConstructError: (StakingError) -> Unit,
|
||||
val onSendSuccess: (String) -> Unit,
|
||||
val onSendError: (SendTransactionError) -> Unit,
|
||||
val onFeeIncreased: (Fee, Boolean) -> Unit,
|
||||
val onTransactionExpired: () -> Unit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransactions(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
val isComposePendingActions = isCompositePendingActions(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
pendingActions = confirmationState.pendingActions,
|
||||
)
|
||||
if (isComposePendingActions) {
|
||||
confirmationState.pendingActions?.map { action ->
|
||||
async {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
action = action,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}?.awaitAll()?.flatten()
|
||||
} else {
|
||||
getStakingTransaction(
|
||||
state = state,
|
||||
confirmationState = confirmationState,
|
||||
onConstructError = onConstructError,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getConstructedTransactions(
|
||||
stakingTransactions: List<StakingTransaction>?,
|
||||
fee: Fee,
|
||||
amount: BigDecimal,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
) = coroutineScope {
|
||||
stakingTransactions
|
||||
?.filterNot {
|
||||
it.type == StakingTransactionType.APPROVAL || it.status == StakingTransactionStatus.SKIPPED
|
||||
}
|
||||
?.map { transaction ->
|
||||
async {
|
||||
getConstructedStakingTransactionUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
fee = fee,
|
||||
amount = amount.convertToSdkAmount(cryptoCurrencyStatus),
|
||||
transactionId = transaction.id,
|
||||
).fold(
|
||||
ifRight = { (constructedTransaction, transactionData) ->
|
||||
FullTransactionData(
|
||||
stakeKitTransaction = constructedTransaction,
|
||||
tangemTransaction = transactionData,
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
onConstructError(it)
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
?.awaitAll()
|
||||
?.filterNotNull()
|
||||
}
|
||||
|
||||
private suspend fun getStakingTransaction(
|
||||
state: StakingUiState,
|
||||
confirmationState: StakingStates.ConfirmationState.Data,
|
||||
action: PendingAction? = confirmationState.pendingAction,
|
||||
onConstructError: (StakingError) -> Unit,
|
||||
): List<StakingTransaction> {
|
||||
val validatorState = state.validatorState as? StakingStates.ValidatorState.Data
|
||||
?: error("No validator provided")
|
||||
val fee = (confirmationState.feeState as? FeeState.Content)?.fee
|
||||
?: error("No fee provided")
|
||||
val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
?: error("No available address")
|
||||
val amountState = state.amountState as? AmountState.Data
|
||||
?: error("No amount provided")
|
||||
|
||||
val validatorAddress = validatorState.chosenTarget.address
|
||||
val amount = getAmount(amountState, fee, confirmationState.reduceAmountBy)
|
||||
|
||||
return getStakingTransactionsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
params = ActionParams(
|
||||
actionCommonType = state.actionType,
|
||||
integrationId = integration.integrationId.value,
|
||||
amount = amount,
|
||||
address = defaultAddress,
|
||||
validatorAddress = validatorAddress,
|
||||
token = integration.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId),
|
||||
passthrough = action?.passthrough,
|
||||
type = action?.type,
|
||||
),
|
||||
).getOrElse {
|
||||
onConstructError(it)
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendStakingTransaction(
|
||||
fullTransactionsData: List<FullTransactionData>,
|
||||
onSendSuccess: (txUrl: String) -> Unit,
|
||||
onSendError: (SendTransactionError) -> Unit,
|
||||
) {
|
||||
if (fullTransactionsData.isEmpty()) return
|
||||
|
||||
val sortedTransactions = fullTransactionsData.sortedBy { it.stakeKitTransaction.stepIndex }
|
||||
|
||||
val firstTransaction = sortedTransactions.first()
|
||||
val network = firstTransaction.stakeKitTransaction.network
|
||||
|
||||
val sendMode = if (network == NetworkType.SOLANA &&
|
||||
firstTransaction.stakeKitTransaction.type == StakingTransactionType.SPLIT
|
||||
) {
|
||||
TransactionSender.MultipleTransactionSendMode.WAIT_AFTER_FIRST
|
||||
} else {
|
||||
TransactionSender.MultipleTransactionSendMode.DEFAULT
|
||||
}
|
||||
|
||||
sendTransactionUseCase(
|
||||
txsData = sortedTransactions.map { it.tangemTransaction },
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
sendMode = sendMode,
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
onSendError(error)
|
||||
},
|
||||
ifRight = { transactionHashes ->
|
||||
submitHash(
|
||||
transactions = sortedTransactions.map { it.stakeKitTransaction },
|
||||
transactionHashes = transactionHashes,
|
||||
)
|
||||
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = transactionHashes.last(),
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
).getOrNull() ?: ""
|
||||
|
||||
balanceUpdater.updateAfterTransaction()
|
||||
onSendSuccess(txUrl)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun submitHash(transactions: List<StakingTransaction>, transactionHashes: List<String>) {
|
||||
transactions
|
||||
.zip(transactionHashes)
|
||||
.forEach { (transaction, transactionHash) ->
|
||||
submitHashUseCase(
|
||||
SubmitHashData(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
),
|
||||
)
|
||||
.onLeft {
|
||||
saveUnsubmittedHashUseCase.invoke(
|
||||
transactionId = transaction.id,
|
||||
transactionHash = transactionHash,
|
||||
)
|
||||
}.onRight {
|
||||
Timber.d("Successful hash submission")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal {
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value")
|
||||
val feeValue = fee.amount.value ?: error("No fee value")
|
||||
val isEnterAction = stateController.value.actionType is StakingActionCommonType.Enter
|
||||
|
||||
return checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy.orZero(),
|
||||
)
|
||||
}
|
||||
|
||||
private data class FullTransactionData(
|
||||
val stakeKitTransaction: StakingTransaction,
|
||||
val tangemTransaction: TransactionData.Compiled,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWallet: UserWallet,
|
||||
integration: StakingIntegration,
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
): StakingTransactionSender
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue