Updated on 2026-08-14
This commit is contained in:
parent
bbcdc778fa
commit
8c6372e398
13 changed files with 136 additions and 82 deletions
|
|
@ -105,4 +105,12 @@ internal object WalletsDomainModule {
|
|||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesValidateWalletMemoUseCase(
|
||||
walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
): ValidateWalletMemoUseCase {
|
||||
return ValidateWalletMemoUseCase(walletAddressServiceRepository = walletAddressServiceRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,12 @@ class DefaultTxHistoryRepository(
|
|||
|
||||
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return blockchain.getExploreTxUrl(txHash)
|
||||
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
|
||||
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
|
||||
""
|
||||
} else {
|
||||
blockchain.getExploreTxUrl(txHash)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
package com.tangem.data.wallets
|
||||
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.tangem.blockchain.blockchains.near.NearWalletManager
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
import java.math.BigInteger
|
||||
|
||||
class DefaultWalletAddressServiceRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) : WalletAddressServiceRepository {
|
||||
override suspend fun validate(userWalletId: UserWalletId, network: Network, address: String): Boolean {
|
||||
|
||||
override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return if (blockchain.isNear()) {
|
||||
|
|
@ -25,7 +28,43 @@ class DefaultWalletAddressServiceRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun validateMemo(network: Network, memo: String): Boolean {
|
||||
if (memo.isEmpty()) return true
|
||||
return when (network.id.value) {
|
||||
Blockchain.XRP.id -> {
|
||||
val tag = memo.toLongOrNull()
|
||||
tag != null && tag <= XRP_TAG_MAX_NUMBER
|
||||
}
|
||||
Blockchain.Stellar.id -> {
|
||||
isAssignableXlmValue(memo)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.isNear(): Boolean {
|
||||
return this == Blockchain.Near || this == Blockchain.NearTestnet
|
||||
}
|
||||
|
||||
private fun isAssignableXlmValue(value: String): Boolean {
|
||||
return when {
|
||||
value.isNotEmpty() && value.isDigitsOnly() -> {
|
||||
try {
|
||||
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
|
||||
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
|
||||
} catch (ex: NumberFormatException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// from org.stellar.sdk.MemoText
|
||||
value.toByteArray().size <= XLM_MEMO_MAX_LENGTH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val XLM_MEMO_MAX_LENGTH = 28
|
||||
private const val XRP_TAG_MAX_NUMBER = 4294967295
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
sealed class GetFeeError {
|
||||
object DataError : GetFeeError()
|
||||
data class DataError(val cause: Throwable?) : GetFeeError()
|
||||
|
||||
object UnknownError : GetFeeError()
|
||||
}
|
||||
|
|
@ -30,20 +30,26 @@ class GetFeeUseCase(
|
|||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<Either<GetFeeError.DataError, TransactionFee>> {
|
||||
): Flow<Either<GetFeeError, TransactionFee>> {
|
||||
return flow {
|
||||
val result = walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
try {
|
||||
val result = requireNotNull(
|
||||
walletManagersFacade.getFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
),
|
||||
) { "Fee is null" }
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
else -> GetFeeError.DataError.left()
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
is Result.Failure -> GetFeeError.DataError(result.error).left()
|
||||
}
|
||||
emit(maybeFee)
|
||||
} catch (e: Exception) {
|
||||
emit(GetFeeError.DataError(e.cause).left())
|
||||
}
|
||||
emit(maybeFee)
|
||||
}.flowOn(dispatcher.io)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
*/
|
||||
interface WalletAddressServiceRepository {
|
||||
|
||||
suspend fun validate(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean
|
||||
|
||||
fun validateMemo(network: Network, memo: String): Boolean
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class ValidateWalletAddressUseCase(
|
|||
address: String,
|
||||
): Either<Throwable, Boolean> = withContext(dispatchers.io) {
|
||||
Either.catch {
|
||||
walletAddressServiceRepository.validate(userWalletId, network, address)
|
||||
walletAddressServiceRepository.validateAddress(userWalletId, network, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.wallets.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
|
||||
|
||||
/**
|
||||
* Use case for validating wallet memo.
|
||||
*/
|
||||
class ValidateWalletMemoUseCase(
|
||||
private val walletAddressServiceRepository: WalletAddressServiceRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(network: Network, memo: String): Either<Throwable, Boolean> = Either.catch {
|
||||
walletAddressServiceRepository.validateMemo(network, memo)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(deps.arrow.core)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -10,33 +10,32 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.calculateReceiveAmount
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val walletAddressesProvider: Provider<Set<Address>>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
|
||||
|
|
@ -138,29 +137,32 @@ internal class SendStateFactory(
|
|||
}
|
||||
|
||||
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
memo = recipientState.addressTextField.value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
address = value,
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
)
|
||||
val isValidMemo = validateWalletMemoUseCase(
|
||||
memo = recipientState.memoTextField?.value.orEmpty(),
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e("Failed to validateWalletMemoUseCase: $it")
|
||||
false
|
||||
}
|
||||
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
|
||||
?.any { it.value == value } ?: true
|
||||
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
|
||||
isValidating = false,
|
||||
addressTextField = recipientState.addressTextField.copy(
|
||||
error = when {
|
||||
!isValidAddress || !isAddressInWallet -> resourceReference(
|
||||
!isValidAddress || isAddressInWallet -> resourceReference(
|
||||
R.string.send_recipient_address_error,
|
||||
)
|
||||
else -> null
|
||||
},
|
||||
isError = value.isNotEmpty() && !isValidAddress || !isAddressInWallet,
|
||||
isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -185,23 +187,26 @@ internal class SendStateFactory(
|
|||
}
|
||||
|
||||
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
val isValidMemo = validateMemo(
|
||||
val isValidMemo = validateWalletMemoUseCase(
|
||||
memo = value,
|
||||
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
|
||||
)
|
||||
val isAddressInWallet = isNotAddressInWallet(
|
||||
walletAddresses = walletAddressesProvider(),
|
||||
address = recipientState.addressTextField.value,
|
||||
)
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e("Failed to validateWalletMemoUseCase: $it")
|
||||
false
|
||||
}
|
||||
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
|
||||
?.any { it.value == value } ?: true
|
||||
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
|
||||
isValidating = false,
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
isError = value.isNotEmpty() || isValidMemo && isValidAddress && isAddressInWallet,
|
||||
isError = value.isNotEmpty() && !isValidMemo,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -85,12 +85,10 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier
|
|||
)
|
||||
}
|
||||
|
||||
val isButtonEnabled = remember {
|
||||
isButtonEnabled(
|
||||
currentState = currentState,
|
||||
uiState = uiState,
|
||||
)
|
||||
}
|
||||
val isButtonEnabled = isButtonEnabled(
|
||||
currentState = currentState,
|
||||
uiState = uiState,
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = buttonTextId,
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
internal fun verifyAddress(address: String, cryptoCurrency: CryptoCurrency?): Boolean {
|
||||
if (address.isEmpty()) return true
|
||||
val blockchain = cryptoCurrency?.let {
|
||||
Blockchain.fromId(cryptoCurrency.id.rawNetworkId)
|
||||
} ?: return false
|
||||
|
||||
return blockchain.validateAddress(address)
|
||||
}
|
||||
|
||||
internal fun isNotAddressInWallet(walletAddresses: Set<Address>, address: String): Boolean {
|
||||
return walletAddresses.all { it.value != address }
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
|||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -38,6 +37,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
|
|
@ -78,6 +78,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
|
||||
|
||||
|
|
@ -97,10 +98,10 @@ internal class SendViewModel @Inject constructor(
|
|||
clickIntents = this,
|
||||
currentStateProvider = Provider { uiState },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
walletAddressesProvider = Provider { walletAddresses },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
)
|
||||
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
|
|
@ -109,16 +110,13 @@ internal class SendViewModel @Inject constructor(
|
|||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
private var walletAddresses = emptySet<Address>()
|
||||
|
||||
private var balanceJobHolder = JobHolder()
|
||||
private var recipientsJobHolder = JobHolder()
|
||||
private var walletAddressesJobHolder = JobHolder()
|
||||
private var feeJobHolder = JobHolder()
|
||||
private var addressValidationJobHolder = JobHolder()
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
getWalletAddresses()
|
||||
subscribeOnCurrencyStatusUpdates(owner)
|
||||
getFee()
|
||||
}
|
||||
|
|
@ -310,15 +308,6 @@ internal class SendViewModel @Inject constructor(
|
|||
}.saveIn(feeJobHolder)
|
||||
}
|
||||
|
||||
private fun getWalletAddresses() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
walletAddresses = walletManagersFacade.getAddresses(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
}.saveIn(walletAddressesJobHolder)
|
||||
}
|
||||
|
||||
// region screen state navigation
|
||||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() = stateRouter.onBackClick()
|
||||
|
|
@ -370,7 +359,7 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = stateFactory.getOnRecipientAddressValidationStarted()
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
val isValidAddress = validateAddress(value)
|
||||
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
|
||||
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
|
||||
}
|
||||
}.saveIn(addressValidationJobHolder)
|
||||
|
|
@ -508,7 +497,7 @@ internal class SendViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
// endregion
|
||||
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue