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()
|
return p2pEthPoolVaultsStore.getSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import java.math.BigDecimal
|
||||||
* Strategy interface for staking integrations.
|
* Strategy interface for staking integrations.
|
||||||
* Abstracts over StakeKit and P2PEthPool staking providers.
|
* Abstracts over StakeKit and P2PEthPool staking providers.
|
||||||
*/
|
*/
|
||||||
interface StakingIntegration {
|
sealed interface StakingIntegration {
|
||||||
|
|
||||||
// Basic
|
// Basic
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,16 @@ interface P2PEthPoolRepository {
|
||||||
*/
|
*/
|
||||||
fun getVaultsFlow(): Flow<List<P2PEthPoolVault>>
|
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
|
* Check P2PEthPool staking availability by finding public vault
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ dependencies {
|
||||||
implementation(deps.lifecycle.compose)
|
implementation(deps.lifecycle.compose)
|
||||||
implementation(deps.jodatime)
|
implementation(deps.jodatime)
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
|
implementation(deps.moshi)
|
||||||
|
|
||||||
/** Compose */
|
/** Compose */
|
||||||
implementation(deps.compose.accompanist.systemUiController)
|
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.StakingEvent
|
||||||
import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory
|
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.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.helpers.StakingTransactionSender
|
||||||
import com.tangem.features.staking.impl.presentation.state.transformers.*
|
import com.tangem.features.staking.impl.presentation.state.transformers.*
|
||||||
import com.tangem.features.staking.impl.presentation.state.transformers.amount.*
|
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 isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||||
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
private val isAnyTokenStakedUseCase: IsAnyTokenStakedUseCase,
|
||||||
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
private val invalidatePendingTransactionsUseCase: InvalidatePendingTransactionsUseCase,
|
||||||
private val stakingTransactionLoader: StakingTransactionSender.Factory,
|
private val stakingOperationsFactory: StakingOperationsFactory,
|
||||||
private val stakingFeeTransactionLoader: StakingFeeTransactionLoader.Factory,
|
|
||||||
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
private val stakingBalanceUpdater: StakingBalanceUpdater.Factory,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
|
|
@ -175,8 +175,7 @@ internal class StakingModel @Inject constructor(
|
||||||
StakeKitIntegration(integrationId, yield)
|
StakeKitIntegration(integrationId, yield)
|
||||||
}
|
}
|
||||||
StakingIntegrationID.P2PEthPool -> {
|
StakingIntegrationID.P2PEthPool -> {
|
||||||
// TODO p2p avoid network call
|
val vaults = p2pEthPoolRepository.getVaultsSync()
|
||||||
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
|
|
||||||
P2PEthPoolIntegration(integrationId, vaults)
|
P2PEthPoolIntegration(integrationId, vaults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -221,16 +220,16 @@ internal class StakingModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val feeLoader by lazy(LazyThreadSafetyMode.NONE) {
|
private val feeLoader: StakingFeeLoader by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
stakingFeeTransactionLoader.create(
|
stakingOperationsFactory.createFeeLoader(
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
integration = integration,
|
integration = integration,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val transactionSender by lazy(LazyThreadSafetyMode.NONE) {
|
private val transactionSender: StakingTransactionSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
stakingTransactionLoader.create(
|
stakingOperationsFactory.createTransactionSender(
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
integration = integration,
|
integration = integration,
|
||||||
|
|
@ -391,49 +390,57 @@ internal class StakingModel @Inject constructor(
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value)
|
stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value)
|
||||||
stateController.update(SetConfirmationStateInProgressTransformer())
|
stateController.update(SetConfirmationStateInProgressTransformer())
|
||||||
transactionSender.constructAndSendTransactions(
|
transactionSender.send(
|
||||||
onConstructSuccess = { constructedTransactions ->
|
StakingTransactionSender.Callbacks(
|
||||||
transactionsInProgress.addAll(constructedTransactions)
|
onConstructSuccess = { constructedTransactions ->
|
||||||
},
|
transactionsInProgress.addAll(constructedTransactions)
|
||||||
onConstructError = { error ->
|
},
|
||||||
stakingEventFactory.createStakingErrorAlert(error)
|
onConstructError = { error ->
|
||||||
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
stakingEventFactory.createStakingErrorAlert(error)
|
||||||
},
|
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
||||||
onSendSuccess = { txUrl ->
|
},
|
||||||
stakingAnalyticSender.sendTransactionStakingAnalytics(
|
onSendSuccess = { txUrl ->
|
||||||
stateController.value,
|
stakingAnalyticSender.sendTransactionStakingAnalytics(
|
||||||
cryptoCurrencyStatus,
|
stateController.value,
|
||||||
)
|
cryptoCurrencyStatus,
|
||||||
transactionsInProgress.clear()
|
)
|
||||||
stateController.update(SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus))
|
transactionsInProgress.clear()
|
||||||
},
|
stateController.update(
|
||||||
onSendError = { error ->
|
SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus),
|
||||||
analyticsEventHandler.send(
|
)
|
||||||
StakingAnalyticsEvent.TransactionError(
|
},
|
||||||
errorCode = error.getAnalyticsDescription(),
|
onSendError = { error ->
|
||||||
),
|
analyticsEventHandler.send(
|
||||||
)
|
StakingAnalyticsEvent.TransactionError(
|
||||||
stakingEventFactory.createSendTransactionErrorAlert(error)
|
errorCode = error.getAnalyticsDescription(),
|
||||||
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
),
|
||||||
},
|
)
|
||||||
onFeeIncreased = { increasedFee, isFeeApproximate ->
|
stakingEventFactory.createSendTransactionErrorAlert(error)
|
||||||
stateController.updateAll(
|
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
||||||
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus),
|
},
|
||||||
SetConfirmationStateAssentTransformer(
|
onFeeIncreased = { increasedFee, isFeeApproximate ->
|
||||||
appCurrencyProvider = Provider { appCurrency },
|
stateController.updateAll(
|
||||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus),
|
||||||
fee = increasedFee,
|
SetConfirmationStateAssentTransformer(
|
||||||
isFeeApproximate = isFeeApproximate,
|
appCurrencyProvider = Provider { appCurrency },
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||||
),
|
fee = increasedFee,
|
||||||
)
|
isFeeApproximate = isFeeApproximate,
|
||||||
stateController.updateEvent(
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
StakingEvent.ShowAlert(
|
),
|
||||||
StakingAlertUM.FeeIncreased(stateController::dismissAlert),
|
)
|
||||||
),
|
stateController.updateEvent(
|
||||||
)
|
StakingEvent.ShowAlert(
|
||||||
updateNotifications()
|
StakingAlertUM.FeeIncreased(stateController::dismissAlert),
|
||||||
},
|
),
|
||||||
|
)
|
||||||
|
updateNotifications()
|
||||||
|
},
|
||||||
|
onTransactionExpired = {
|
||||||
|
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
|
||||||
|
getFee()
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}.saveIn(sendTransactionJobHolder)
|
}.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.staking.PendingAction
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.staking.EstimateGasUseCase
|
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.StakingError
|
||||||
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
|
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.ActionParams
|
||||||
|
|
@ -36,7 +36,7 @@ import kotlinx.coroutines.delay
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
internal class StakeKitFeeLoader @AssistedInject constructor(
|
||||||
private val stateController: StakingStateController,
|
private val stateController: StakingStateController,
|
||||||
private val getFeeUseCase: GetFeeUseCase,
|
private val getFeeUseCase: GetFeeUseCase,
|
||||||
private val estimateGasUseCase: EstimateGasUseCase,
|
private val estimateGasUseCase: EstimateGasUseCase,
|
||||||
|
|
@ -44,10 +44,10 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||||
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
@Assisted private val userWallet: UserWallet,
|
@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,
|
onStakingFee: (Fee, Boolean) -> Unit,
|
||||||
onStakingFeeError: (StakingError) -> Unit,
|
onStakingFeeError: (StakingError) -> Unit,
|
||||||
onApprovalFee: (TransactionFee) -> Unit,
|
onApprovalFee: (TransactionFee) -> Unit,
|
||||||
|
|
@ -57,21 +57,41 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data
|
||||||
?: error("Illegal state")
|
?: 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
|
?: state.balanceState?.targetAddress
|
||||||
?: error("No target address provided")
|
?: error("No target address provided")
|
||||||
|
|
||||||
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||||
?: error("No amount provided")
|
?: error("No amount provided")
|
||||||
|
|
||||||
val pendingAction = confirmationState.pendingAction
|
getStakeKitFee(
|
||||||
val pendingActions = confirmationState.pendingActions
|
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 isApprovalNeeded = confirmationState.isApprovalNeeded
|
||||||
val isAllowanceNotEnough = confirmationState.allowance < amount
|
val isAllowanceNotEnough = confirmationState.allowance < amount
|
||||||
|
|
||||||
if (isEnter && isApprovalNeeded && isAllowanceNotEnough) {
|
if (isEnter && isApprovalNeeded && isAllowanceNotEnough) {
|
||||||
getApproveFee(
|
getApprovalFee(
|
||||||
amount = amount,
|
amount = amount,
|
||||||
validatorAddress = validatorAddress,
|
validatorAddress = validatorAddress,
|
||||||
onApprovalFee = onApprovalFee,
|
onApprovalFee = onApprovalFee,
|
||||||
|
|
@ -79,8 +99,8 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
estimateGas(
|
estimateGas(
|
||||||
pendingAction = pendingAction,
|
pendingAction = confirmationState.pendingAction,
|
||||||
pendingActions = pendingActions,
|
pendingActions = confirmationState.pendingActions,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
validatorAddress = validatorAddress,
|
validatorAddress = validatorAddress,
|
||||||
onStakingFeeError = onStakingFeeError,
|
onStakingFeeError = onStakingFeeError,
|
||||||
|
|
@ -105,61 +125,80 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
pendingActions = pendingActions,
|
pendingActions = pendingActions,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val result = coroutineScope {
|
estimateCompositeGas(
|
||||||
pendingActions?.map { action ->
|
pendingActions = pendingActions,
|
||||||
async {
|
amount = amount,
|
||||||
// Simultaneous or quick api calls can sometimes return ZERO fee
|
sourceAddress = sourceAddress,
|
||||||
estimateFeeRetry {
|
validatorAddress = validatorAddress,
|
||||||
estimateFee(
|
onStakingFeeError = onStakingFeeError,
|
||||||
amount = amount,
|
) ?: return
|
||||||
sourceAddress = sourceAddress,
|
|
||||||
validatorAddress = validatorAddress,
|
|
||||||
action = action,
|
|
||||||
)
|
|
||||||
}.getOrElse {
|
|
||||||
onStakingFeeError(it)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.awaitAll()?.filterNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isNullOrEmpty()) {
|
|
||||||
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalAmount = result.sumOf { it.amount }
|
|
||||||
val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() }
|
|
||||||
StakingGasEstimate(
|
|
||||||
amount = totalAmount,
|
|
||||||
token = result.first().token,
|
|
||||||
gasLimit = totalGasLimit.toPlainString().orEmpty(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
estimateFee(
|
estimateSingleGas(
|
||||||
amount = amount,
|
amount = amount,
|
||||||
sourceAddress = sourceAddress,
|
sourceAddress = sourceAddress,
|
||||||
validatorAddress = validatorAddress,
|
validatorAddress = validatorAddress,
|
||||||
action = pendingAction,
|
action = pendingAction,
|
||||||
).getOrElse {
|
).getOrElse { error ->
|
||||||
onStakingFeeError(it)
|
onStakingFeeError(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val amount = Amount(
|
val feeAmount = Amount(
|
||||||
currencySymbol = gasEstimate.token.symbol,
|
currencySymbol = gasEstimate.token.symbol,
|
||||||
value = gasEstimate.amount,
|
value = gasEstimate.amount,
|
||||||
decimals = gasEstimate.token.decimals,
|
decimals = gasEstimate.token.decimals,
|
||||||
)
|
)
|
||||||
onStakingFee(
|
onStakingFee(
|
||||||
Fee.Common(amount),
|
Fee.Common(feeAmount),
|
||||||
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = amount.type),
|
isFeeApproximateUseCase(networkId = cryptoCurrencyStatus.currency.network.id, amountType = feeAmount.type),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun estimateFee(
|
/**
|
||||||
|
* 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
|
||||||
|
estimateGasRetry {
|
||||||
|
estimateSingleGas(
|
||||||
|
amount = amount,
|
||||||
|
sourceAddress = sourceAddress,
|
||||||
|
validatorAddress = validatorAddress,
|
||||||
|
action = action,
|
||||||
|
)
|
||||||
|
}.getOrElse { gasError ->
|
||||||
|
onStakingFeeError(gasError)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}?.awaitAll()?.filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isNullOrEmpty()) {
|
||||||
|
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalAmount = result.sumOf { it.amount }
|
||||||
|
val totalGasLimit = result.sumOf { it.gasLimit?.toBigDecimalOrNull().orZero() }
|
||||||
|
return StakingGasEstimate(
|
||||||
|
amount = totalAmount,
|
||||||
|
token = result.first().token,
|
||||||
|
gasLimit = totalGasLimit.toPlainString().orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun estimateSingleGas(
|
||||||
amount: BigDecimal,
|
amount: BigDecimal,
|
||||||
sourceAddress: String,
|
sourceAddress: String,
|
||||||
validatorAddress: String,
|
validatorAddress: String,
|
||||||
|
|
@ -179,7 +218,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun getApproveFee(
|
private suspend fun getApprovalFee(
|
||||||
amount: BigDecimal,
|
amount: BigDecimal,
|
||||||
validatorAddress: String,
|
validatorAddress: String,
|
||||||
onApprovalFee: (TransactionFee) -> Unit,
|
onApprovalFee: (TransactionFee) -> Unit,
|
||||||
|
|
@ -187,6 +226,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
) {
|
) {
|
||||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||||
?: return onApprovalFeeError(GetFeeError.UnknownError)
|
?: return onApprovalFeeError(GetFeeError.UnknownError)
|
||||||
|
|
||||||
val approvalTransactionData = createApprovalTransactionUseCase(
|
val approvalTransactionData = createApprovalTransactionUseCase(
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
userWalletId = userWallet.walletId,
|
userWalletId = userWallet.walletId,
|
||||||
|
|
@ -196,34 +236,29 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
).getOrElse {
|
).getOrElse {
|
||||||
return onApprovalFeeError(GetFeeError.DataError(it))
|
return onApprovalFeeError(GetFeeError.DataError(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeeUseCase(
|
getFeeUseCase(
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
network = tokenCurrency.network,
|
network = tokenCurrency.network,
|
||||||
transactionData = approvalTransactionData,
|
transactionData = approvalTransactionData,
|
||||||
).fold(
|
).fold(
|
||||||
ifRight = { fee ->
|
ifRight = onApprovalFee,
|
||||||
onApprovalFee(fee)
|
ifLeft = onApprovalFeeError,
|
||||||
},
|
|
||||||
ifLeft = { error ->
|
|
||||||
onApprovalFeeError(error)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun estimateFeeRetry(
|
private suspend fun estimateGasRetry(
|
||||||
times: Int = 3,
|
times: Int = RETRY_COUNT,
|
||||||
delay: Long = 1000,
|
delayMs: Long = RETRY_DELAY_MS,
|
||||||
block: suspend () -> Either<StakingError, StakingGasEstimate>,
|
block: suspend () -> Either<StakingError, StakingGasEstimate>,
|
||||||
): Either<StakingError, StakingGasEstimate> {
|
): Either<StakingError, StakingGasEstimate> {
|
||||||
repeat(times - 1) {
|
repeat(times - 1) {
|
||||||
val feeResult = block()
|
val feeResult = block()
|
||||||
feeResult.fold(
|
feeResult.fold(
|
||||||
ifLeft = { return feeResult },
|
ifLeft = { return feeResult },
|
||||||
ifRight = {
|
ifRight = { estimate -> if (!estimate.amount.isZero()) return feeResult },
|
||||||
if (!it.amount.isZero()) return feeResult
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
delay(delay)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
return block()
|
return block()
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +268,12 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
|
||||||
fun create(
|
fun create(
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
integration: StakingIntegration,
|
integration: StakeKitIntegration,
|
||||||
): StakingFeeTransactionLoader
|
): 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
|
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.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.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.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.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 interface StakingTransactionSender {
|
||||||
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,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val balanceUpdater: StakingBalanceUpdater
|
suspend fun send(callbacks: Callbacks)
|
||||||
get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, integration)
|
|
||||||
|
|
||||||
suspend fun constructAndSendTransactions(
|
class Callbacks(
|
||||||
onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
val onConstructSuccess: (List<StakingTransaction>) -> Unit,
|
||||||
onConstructError: (StakingError) -> Unit,
|
val onConstructError: (StakingError) -> Unit,
|
||||||
onSendSuccess: (String) -> Unit,
|
val onSendSuccess: (String) -> Unit,
|
||||||
onSendError: (SendTransactionError) -> Unit,
|
val onSendError: (SendTransactionError) -> Unit,
|
||||||
onFeeIncreased: (Fee, Boolean) -> Unit,
|
val onFeeIncreased: (Fee, Boolean) -> Unit,
|
||||||
) {
|
val onTransactionExpired: () -> 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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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