Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-29 12:24:26 +03:00
parent b2189b3895
commit 4f3d1f10c7
11 changed files with 268 additions and 80 deletions

View file

@ -42,6 +42,21 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideSendMultipleTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
): SendMultipleTransactionUseCase {
return SendMultipleTransactionUseCase(
demoConfig = DemoConfig(),
cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideAssociateAssetUseCase(

View file

@ -158,6 +158,21 @@ internal class DefaultTransactionRepository(
(walletManager as TransactionSender).send(txData, signer)
}
override suspend fun sendMultipleTransactions(
txsData: List<TransactionData>,
signer: CommonSigner,
userWalletId: UserWalletId,
network: Network,
) = withContext(coroutineDispatcherProvider.io) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
(walletManager as TransactionSender).sendMultiple(txsData, signer)
}
override fun createTransactionDataExtras(
data: String,
network: Network,

View file

@ -86,10 +86,13 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Fee", error.fee ?: "Unable to receive")
}
fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) {
fun addStakingInfo(validatorName: String?, transactionTypes: List<String>, unsignedTransactions: List<String?>) {
builder.appendKeyValue("Validator", validatorName ?: "unknown")
builder.appendKeyValue("Action", transactionType ?: "unknown")
builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown")
builder.appendKeyValue("Action", transactionTypes.joinToString(separator = "\n").ifEmpty { "unknown" })
builder.appendKeyValue(
key = "Unsigned transaction",
value = unsignedTransactions.joinToString(separator = "\n") { it ?: "unknown" }.ifEmpty { "unknown" },
)
}
fun addDelimiter(): StringBuilder = builder.appendDelimiter()

View file

@ -27,7 +27,7 @@ sealed interface FeedbackEmailType {
data class StakingProblem(
override val cardInfo: CardInfo,
val validatorName: String?,
val transactionType: String?,
val unsignedTransaction: String?,
val transactionTypes: List<String>,
val unsignedTransactions: List<String?>,
) : FeedbackEmailType
}

View file

@ -26,8 +26,8 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.StakingProblem -> addStakingProblemBody(
type.cardInfo,
type.validatorName,
type.transactionType,
type.unsignedTransaction,
type.transactionTypes,
type.unsignedTransactions,
)
}
@ -81,8 +81,8 @@ internal class EmailMessageBodyResolver(
private suspend fun FeedbackDataBuilder.addStakingProblemBody(
cardInfo: CardInfo,
validatorName: String?,
transactionType: String?,
unsignedTransaction: String?,
transactionTypes: List<String>,
unsignedTransactions: List<String?>,
) {
addCardInfo(cardInfo)
addDelimiter()
@ -102,7 +102,7 @@ internal class EmailMessageBodyResolver(
addDelimiter()
}
addStakingInfo(validatorName, transactionType, unsignedTransaction)
addStakingInfo(validatorName, transactionTypes, unsignedTransactions)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionExtras
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionSendResult
import com.tangem.blockchain.common.transaction.TransactionsSendResult
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.models.TransactionType
@ -58,6 +59,13 @@ interface TransactionRepository {
network: Network,
): com.tangem.blockchain.extensions.Result<TransactionSendResult>
suspend fun sendMultipleTransactions(
txsData: List<TransactionData>,
signer: CommonSigner,
userWalletId: UserWalletId,
network: Network,
): com.tangem.blockchain.extensions.Result<TransactionsSendResult>
fun createTransactionDataExtras(
data: String,
network: Network,

View file

@ -0,0 +1,147 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.transaction.TransactionsSendResult
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.network.ResultChecker
import com.tangem.common.core.TangemSdkError
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.R
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.utils.toFormattedString
// TODO [REDACTED_TASK_KEY] merge with SendTransactionUseCase
class SendMultipleTransactionUseCase(
private val demoConfig: DemoConfig,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val transactionRepository: TransactionRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
txsData: List<TransactionData>,
userWallet: UserWallet,
network: Network,
): Either<SendTransactionError?, List<String>> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(cardId = card.cardId.takeIf { isCardNotBackedUp })
val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal()
if (userWallet.scanResponse.card.isStart2Coin) {
cardSdkConfigRepository.setLinkedTerminal(false)
}
val sendResult = try {
if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) {
sendDemo(
userWallet = userWallet,
network = network,
transactionsData = txsData,
signer = signer,
)
} else {
val sendResult = transactionRepository.sendMultipleTransactions(
txsData = txsData,
signer = signer,
userWalletId = userWallet.walletId,
network = network,
)
when (sendResult) {
is Result.Failure -> handleError(sendResult).left()
is Result.Success -> sendResult.data.right()
}
}
} catch (ex: Exception) {
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
SendTransactionError.DataError(ex.message).left()
}
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
return sendResult.fold(
ifRight = { result -> result.hashes.right() },
ifLeft = { it.left() },
)
}
private suspend fun sendDemo(
userWallet: UserWallet,
network: Network,
transactionsData: List<TransactionData>,
signer: TransactionSigner,
): Either<SendTransactionError, TransactionsSendResult> {
val demoTransactionSender = DemoTransactionSender(
walletManagersFacade
.getOrCreateWalletManager(userWallet.walletId, network)
?: error("WalletManager is null"),
)
val result = demoTransactionSender.sendMultiple(transactionDataList = transactionsData, signer = signer)
return if (result is Result.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) {
SendTransactionError.DemoCardError.left()
} else {
TransactionsSendResult(listOf("hash")).right()
}
}
private fun handleError(result: Result.Failure): SendTransactionError {
if (ResultChecker.isNetworkError(result)) {
return SendTransactionError.NetworkError(
code = result.error.message,
message = result.error.customMessage,
)
}
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
return when (error) {
is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error)
is BlockchainSdkError.CreateAccountUnderfunded -> {
val minAmount = error.minReserve
val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty()
SendTransactionError.CreateAccountUnderfunded(minValue)
}
else -> {
SendTransactionError.BlockchainSdkError(
code = error.code,
message = error.customMessage,
)
}
}
}
private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError {
return if (error.code == USER_CANCELLED_ERROR_CODE) {
SendTransactionError.UserCancelledError
} else {
when (val tangemError = error.tangemError) {
is TangemSdkError -> {
val resource = tangemError.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs))
}
is BlockchainSdkError.WrappedTangemError -> {
parseWrappedError(tangemError) // todo remove when sdk errors are revised
}
else -> {
SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage)
}
}
}
}
}

View file

@ -1,10 +1,8 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
@ -17,31 +15,23 @@ import kotlinx.collections.immutable.ImmutableList
internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val stakingGasEstimate: StakingGasEstimate,
private val fee: Fee,
private val pendingActionList: ImmutableList<PendingAction>,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate),
confirmationState = prevState.confirmationState.copyWrapped(fee),
)
}
private fun StakingStates.ConfirmationState.copyWrapped(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmationState {
private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = Fee.Common(
Amount(
currencySymbol = gasEstimate.token.symbol,
value = gasEstimate.amount,
decimals = gasEstimate.token.decimals,
),
),
fee = fee,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),

View file

@ -1,9 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -15,32 +13,24 @@ import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateCompletedTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val stakingGasEstimate: StakingGasEstimate,
private val fee: Fee,
private val txUrl: String,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate),
confirmationState = prevState.confirmationState.copyWrapped(fee),
)
}
private fun StakingStates.ConfirmationState.copyWrapped(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmationState {
private fun StakingStates.ConfirmationState.copyWrapped(fee: Fee): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return copy(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.COMPLETED,
feeState = FeeState.Content(
fee = Fee.Common(
Amount(
currencySymbol = gasEstimate.token.symbol,
value = gasEstimate.amount,
decimals = gasEstimate.token.decimals,
),
),
fee = fee,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),

View file

@ -6,7 +6,9 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
@ -28,7 +30,6 @@ import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
@ -37,10 +38,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
@ -70,6 +68,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.math.BigDecimal
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
import kotlin.properties.Delegates
@ -87,6 +86,7 @@ internal class StakingViewModel @Inject constructor(
private val getConstructedStakingTransactionUseCase: GetConstructedStakingTransactionUseCase,
private val estimateGasUseCase: EstimateGasUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val sendMultipleTransactionUseCase: SendMultipleTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val submitHashUseCase: SubmitHashUseCase,
@ -143,7 +143,7 @@ internal class StakingViewModel @Inject constructor(
private var stakingApproval: StakingApproval = StakingApproval.Empty
private val allowanceTaskScheduler = SingleTaskScheduler<BigDecimal>()
private var transactionInProgress: StakingTransaction? = null
private val transactionsInProgress: CopyOnWriteArrayList<StakingTransaction> = CopyOnWriteArrayList()
private var approvalJobHolder: JobHolder = JobHolder()
@ -187,7 +187,7 @@ internal class StakingViewModel @Inject constructor(
val defaultAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address")
val stakingTransaction = getStakingTransactionUseCase(
val stakingTransactions = getStakingTransactionUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
@ -206,9 +206,9 @@ internal class StakingViewModel @Inject constructor(
return@launch
}
stakingTransaction
val fullTransactionsData = stakingTransactions
.filterNot { it.type == StakingTransactionType.APPROVAL }
.forEach { transaction ->
.map { transaction ->
val (constructedTransaction, transactionData) = getConstructedStakingTransactionUseCase(
networkId = cryptoCurrencyStatus.currency.network.id.value,
fee = fee,
@ -218,17 +218,20 @@ internal class StakingViewModel @Inject constructor(
stakingEventFactory.createStakingErrorAlert(it)
return@launch
}
val gasEstimate = constructedTransaction.gasEstimate
?: return@launch stakingEventFactory.createGenericErrorAlert("Gas estimate is null")
transactionInProgress = constructedTransaction
sendStakingTransaction(
transactionId = constructedTransaction.id,
gasEstimate = gasEstimate,
txData = transactionData,
pendingActionList = confirmationState.pendingActions,
FullTransactionData(
stakeKitTransaction = constructedTransaction,
tangemTransaction = transactionData,
)
}
transactionsInProgress.addAll(fullTransactionsData.map { it.stakeKitTransaction })
sendStakingTransaction(
fullTransactionsData = fullTransactionsData,
fee = fee,
pendingActionList = confirmationState.pendingActions,
)
}
}
}
@ -291,7 +294,7 @@ internal class StakingViewModel @Inject constructor(
validatorAddress: String,
) {
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = estimateGasUseCase(
val gasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
@ -313,7 +316,13 @@ internal class StakingViewModel @Inject constructor(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = stakingGasEstimate,
fee = Fee.Common(
Amount(
currencySymbol = gasEstimate.token.symbol,
value = gasEstimate.amount,
decimals = gasEstimate.token.decimals,
),
),
pendingActionList = pendingActions,
),
)
@ -555,8 +564,8 @@ internal class StakingViewModel @Inject constructor(
val email = FeedbackEmailType.StakingProblem(
cardInfo = cardInfo,
validatorName = validator?.name,
transactionType = transactionInProgress?.type?.name,
unsignedTransaction = transactionInProgress?.unsignedTransaction,
transactionTypes = transactionsInProgress.map { it.type.name },
unsignedTransactions = transactionsInProgress.map { it.unsignedTransaction },
)
feedbackManager.sendEmail(email)
@ -632,13 +641,12 @@ internal class StakingViewModel @Inject constructor(
}
private suspend fun sendStakingTransaction(
transactionId: String,
gasEstimate: StakingGasEstimate,
txData: TransactionData,
fullTransactionsData: List<FullTransactionData>,
fee: Fee,
pendingActionList: ImmutableList<PendingAction>,
) {
sendTransactionUseCase(
txData = txData,
sendMultipleTransactionUseCase(
txsData = fullTransactionsData.map { it.tangemTransaction },
userWallet = userWallet,
network = cryptoCurrencyStatus.currency.network,
).fold(
@ -648,18 +656,21 @@ internal class StakingViewModel @Inject constructor(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = gasEstimate,
fee = fee,
pendingActionList = pendingActionList,
),
)
stakingEventFactory.createSendTransactionErrorAlert(error)
},
ifRight = { txHash ->
transactionInProgress = null
submitHash(transactionId, txHash)
ifRight = { transactionHashes ->
transactionsInProgress.clear()
submitHash(
transactionIds = fullTransactionsData.map { it.stakeKitTransaction.id },
transactionHashes = transactionHashes,
)
scheduleUpdates()
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
txHash = transactionHashes.last(),
networkId = cryptoCurrencyStatus.currency.network.id,
).getOrElse { "" }
@ -667,7 +678,7 @@ internal class StakingViewModel @Inject constructor(
SetConfirmationStateCompletedTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = gasEstimate,
fee = fee,
txUrl = txUrl,
),
)
@ -675,18 +686,22 @@ internal class StakingViewModel @Inject constructor(
)
}
private suspend fun submitHash(transactionId: String, transactionHash: String) {
submitHashUseCase.submitHash(
transactionId = transactionId,
transactionHash = transactionHash,
)
.onLeft {
saveUnsubmittedHashUseCase.invoke(
private suspend fun submitHash(transactionIds: List<String>, transactionHashes: List<String>) {
transactionIds
.zip(transactionHashes)
.forEach { (transactionId, transactionHash) ->
submitHashUseCase.submitHash(
transactionId = transactionId,
transactionHash = transactionHash,
)
}.onRight {
Timber.d("Successful hash submission")
.onLeft {
saveUnsubmittedHashUseCase.invoke(
transactionId = transactionId,
transactionHash = transactionHash,
)
}.onRight {
Timber.d("Successful hash submission")
}
}
}
@ -750,6 +765,11 @@ internal class StakingViewModel @Inject constructor(
InnerConfirmationStakingState.ASSENT
}
private data class FullTransactionData(
val stakeKitTransaction: StakingTransaction,
val tangemTransaction: TransactionData.Compiled,
)
private companion object {
const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/"
const val ALLOWANCE_UPDATE_DELAY = 10_000L

View file

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