Updated on 2026-08-14
This commit is contained in:
parent
d5299f8094
commit
2c487afef2
7 changed files with 63 additions and 134 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.transaction.usecase
|
|||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.transaction.WalletAddressServiceRepository
|
||||
|
|
@ -43,4 +44,29 @@ class ValidateWalletAddressUseCase(
|
|||
else -> AddressValidation.Success.Valid.right()
|
||||
}
|
||||
}
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
address: String,
|
||||
senderAddresses: List<CryptoCurrencyAddress>,
|
||||
): AddressValidationResult {
|
||||
val isUtxoConsolidationAvailable =
|
||||
walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network)
|
||||
val isCurrentAddress = senderAddresses.any { it.address == address }
|
||||
|
||||
val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable
|
||||
|
||||
val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value)
|
||||
val addressToValidate = decodedXAddress?.address ?: address
|
||||
|
||||
val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate)
|
||||
|
||||
return when {
|
||||
!isValidAddress -> AddressValidation.Error.InvalidAddress.left()
|
||||
isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left()
|
||||
decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right()
|
||||
else -> AddressValidation.Success.Valid.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
|
||||
userWallet = model.userWallet,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.currency,
|
||||
callback = model,
|
||||
isEditMode = route.isEditMode,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ internal class SendConfirmComponent(
|
|||
params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWallet = params.userWallet,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
cryptoCurrency = params.cryptoCurrencyStatus.currency,
|
||||
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
|
||||
isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
|
|
@ -12,14 +12,14 @@ internal sealed class SendDestinationComponentParams {
|
|||
|
||||
abstract val state: DestinationUM
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val userWallet: UserWallet
|
||||
abstract val userWalletId: UserWalletId
|
||||
abstract val cryptoCurrency: CryptoCurrency
|
||||
|
||||
data class DestinationParams(
|
||||
override val state: DestinationUM,
|
||||
override val analyticsCategoryName: String,
|
||||
override val userWallet: UserWallet,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
override val userWalletId: UserWalletId,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val currentRoute: Flow<SendRoute.Destination>,
|
||||
val callback: ModelCallback,
|
||||
|
|
@ -29,7 +29,7 @@ internal sealed class SendDestinationComponentParams {
|
|||
data class DestinationBlockParams(
|
||||
override val state: DestinationUM,
|
||||
override val analyticsCategoryName: String,
|
||||
override val userWallet: UserWallet,
|
||||
override val userWalletId: UserWalletId,
|
||||
override val cryptoCurrency: CryptoCurrency,
|
||||
val blockClickEnableFlow: StateFlow<Boolean>,
|
||||
val predefinedAddressValue: String?,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
|
|
@ -10,23 +9,10 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
|
|
@ -38,7 +24,7 @@ import com.tangem.features.send.v2.impl.R
|
|||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationAlertFactory
|
||||
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
|
|
@ -56,7 +42,6 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
|
|
@ -73,15 +58,7 @@ internal class SendDestinationModel @Inject constructor(
|
|||
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val sendDestinationAlertFactory: SendDestinationAlertFactory,
|
||||
) : Model(), SendDestinationClickIntents {
|
||||
private val params: SendDestinationComponentParams = paramsContainer.require()
|
||||
|
||||
|
|
@ -89,17 +66,26 @@ internal class SendDestinationModel @Inject constructor(
|
|||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private val userWallet = params.userWallet
|
||||
private val cryptoCurrency = params.cryptoCurrency
|
||||
private var cryptoCurrencyStatus by Delegates.notNull<CryptoCurrencyStatus>()
|
||||
private val userWalletId = params.userWalletId
|
||||
|
||||
private val senderAddresses = MutableStateFlow<List<CryptoCurrencyAddress>>(emptyList())
|
||||
|
||||
private var validationJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
initSenderAddress()
|
||||
configDestinationNavigation()
|
||||
initialState()
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
subscribeOnQRScannerResult()
|
||||
}
|
||||
|
||||
private fun initSenderAddress() {
|
||||
modelScope.launch {
|
||||
senderAddresses.value = getNetworkAddressesUseCase.invokeSync(userWalletId, cryptoCurrency.network)
|
||||
}
|
||||
senderAddresses.onEach {
|
||||
getWalletsAndRecent()
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
|
|
@ -157,95 +143,13 @@ internal class SendDestinationModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyStatusUpdates() {
|
||||
val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
|
||||
val isMultiCurrency = userWallet.isMultiCurrency
|
||||
getCurrenciesStatusUpdates(
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
|
||||
getCurrencyStatus(
|
||||
isSingleWalletWithToken = isSingleWalletWithToken,
|
||||
isMultiCurrency = isMultiCurrency,
|
||||
).onEach { maybeCryptoCurrency ->
|
||||
maybeCryptoCurrency.fold(
|
||||
ifRight = { cryptoStatus ->
|
||||
cryptoCurrencyStatus = cryptoStatus
|
||||
getWalletsAndRecent()
|
||||
},
|
||||
ifLeft = {
|
||||
sendDestinationAlertFactory.getGenericErrorState {
|
||||
onFailedTxEmailClick(it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getCurrencyStatus(
|
||||
isSingleWalletWithToken: Boolean,
|
||||
isMultiCurrency: Boolean,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return if (isMultiCurrency) {
|
||||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
isSingleWalletWithTokens = isSingleWalletWithToken,
|
||||
)
|
||||
} else {
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFailedTxEmailClick(errorMessage: String? = null) {
|
||||
saveBlockchainErrorUseCase(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = errorMessage.orEmpty(),
|
||||
blockchainId = cryptoCurrency.network.id.value,
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
destinationAddress = "",
|
||||
tokenSymbol = "",
|
||||
amount = "",
|
||||
fee = "",
|
||||
),
|
||||
)
|
||||
|
||||
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
|
||||
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnQRScannerResult() {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
.onEach(::onQrCodeScanned)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onQrCodeScanned(address: String) {
|
||||
parseQrCodeUseCase(address, cryptoCurrency).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
ifLeft = {
|
||||
onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getWalletsUseCase().conflate().map {
|
||||
waitForDelay(RECENT_LOAD_DELAY) { it.toAvailableWallets() }
|
||||
},
|
||||
flow2 = getFixedTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
pageSize = RECENT_TX_SIZE,
|
||||
).getOrElse { flowOf(emptyList()) }.map {
|
||||
|
|
@ -253,15 +157,17 @@ internal class SendDestinationModel @Inject constructor(
|
|||
}.conflate(),
|
||||
) { destinationWalletList, txHistoryList ->
|
||||
val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
_uiState.update(
|
||||
SendDestinationRecentListTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
senderAddress = senderAddresses.value.firstOrNull()?.address,
|
||||
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable,
|
||||
destinationWalletList = destinationWalletList,
|
||||
txHistoryList,
|
||||
txHistoryList = txHistoryList,
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
|
|
@ -308,10 +214,10 @@ internal class SendDestinationModel @Inject constructor(
|
|||
_uiState.update(SendDestinationValidationStartedTransformer)
|
||||
|
||||
val addressValidationResult = validateWalletAddressUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = address,
|
||||
currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses,
|
||||
senderAddresses = senderAddresses.value,
|
||||
)
|
||||
val memoValidationResult = validateWalletMemoUseCase(
|
||||
memo = memo.orEmpty(),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model.converters
|
|||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG
|
||||
|
|
@ -13,7 +12,7 @@ import kotlinx.collections.immutable.PersistentList
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientWalletListConverter(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val senderAddress: String?,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
) :
|
||||
Converter<List<DestinationWalletUM?>, PersistentList<DestinationRecipientListUM>> {
|
||||
|
|
@ -25,14 +24,11 @@ internal class SendRecipientWalletListConverter(
|
|||
|
||||
private fun List<DestinationWalletUM?>.filterWallets(): PersistentList<DestinationRecipientListUM> {
|
||||
var walletsCounter = 0
|
||||
val currentAddress: String = runCatching {
|
||||
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
}.getOrNull().orEmpty()
|
||||
|
||||
return this.filterNotNull()
|
||||
.filter {
|
||||
val isCoin = it.cryptoCurrency is CryptoCurrency.Coin
|
||||
val isNotSameAddress = it.address != currentAddress
|
||||
val isNotSameAddress = it.address != senderAddress
|
||||
val isNotBlankAddress = it.address.isNotBlank()
|
||||
|
||||
isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
|
||||
|
|
@ -9,7 +9,8 @@ import com.tangem.features.send.v2.subcomponents.destination.ui.state.Destinatio
|
|||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationRecentListTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val senderAddress: String?,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
private val destinationWalletList: List<DestinationWalletUM>,
|
||||
private val txHistoryList: List<TxHistoryItem>,
|
||||
|
|
@ -19,11 +20,11 @@ internal class SendDestinationRecentListTransformer(
|
|||
|
||||
return state.copy(
|
||||
wallets = SendRecipientWalletListConverter(
|
||||
cryptoCurrencyStatus,
|
||||
isUtxoConsolidationAvailable,
|
||||
senderAddress = senderAddress,
|
||||
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable,
|
||||
).convert(destinationWalletList),
|
||||
recent = SendRecipientHistoryListConverter(
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
).convert(txHistoryList),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue