diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 108efdb33c..dbe9d28ed2 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -18,6 +18,7 @@ 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.navigation.url.UrlOpener +import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError @@ -286,10 +287,9 @@ internal class GiveApprovalModel @Inject constructor( } private fun getApprovalAmount(): BigDecimal? { - return if (uiState.value.approveType == ApproveType.LIMITED) { - params.amount.toBigDecimalOrNull() - } else { - null + return when (uiState.value.approveType) { + ApproveType.LIMITED -> params.amount.parseBigDecimalOrNull() + ApproveType.UNLIMITED -> null } } @@ -302,7 +302,7 @@ internal class GiveApprovalModel @Inject constructor( val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left() - val amount = params.amount.toBigDecimalOrNull() + val amount = params.amount.parseBigDecimalOrNull() ?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left() val allowance = getAllowanceInfoUseCase( diff --git a/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt b/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt index cf09525c4c..acf4eaa4da 100644 --- a/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt +++ b/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt @@ -2,16 +2,21 @@ package com.tangem.features.approval.impl.model import arrow.core.right import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.MutableParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.AllowanceInfo +import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase @@ -23,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -30,6 +36,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) class GiveApprovalModelTest { @@ -48,31 +55,73 @@ class GiveApprovalModelTest { private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF") private val userWallet: UserWallet.Hot = mockk(relaxed = true) - private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk { - every { currency } returns mockk(relaxed = true) + private val tokenCurrency: CryptoCurrency.Token = mockk(relaxed = true) { + every { contractAddress } returns "0xContract" + every { network } returns mockk(relaxed = true) } - private val params = GiveApprovalComponent.Params( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = cryptoCurrencyStatus, - amount = "10", - spenderAddress = "0xSpender", - amountFooter = TextReference.EMPTY, - feeFooter = TextReference.EMPTY, - callback = mockk(relaxed = true), - ) + private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk { + every { currency } returns tokenCurrency + } + + private val approvalTx: TransactionData.Uncompiled = mockk(relaxed = true) + private val transactionFee: TransactionFee = mockk(relaxed = true) + private val transactionFeeExtended: TransactionFeeExtended = mockk(relaxed = true) { + every { transactionFee } returns this@GiveApprovalModelTest.transactionFee + } private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true) @BeforeEach fun setUp() { every { getUserWalletUseCase.invoke(userWalletId) } returns userWallet.right() + + coEvery { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = any(), + contractAddress = any(), + spenderAddress = any(), + ) + } returns approvalTx.right() + + coEvery { + getAllowanceInfoUseCase( + userWalletId = any(), + cryptoCurrency = any(), + spenderAddress = any(), + requiredAmount = any(), + ) + } returns AllowanceInfo.Enough(allowance = BigDecimal.ZERO).right() + + coEvery { + getFeeUseCase(transactionData = any(), userWallet = any(), network = any()) + } returns transactionFee.right() + + coEvery { + getFeeForGaslessUseCase(transactionData = any(), userWallet = any(), network = any()) + } returns transactionFeeExtended.right() + + coEvery { + getFeeForTokenUseCase(transactionData = any(), userWallet = any(), token = any()) + } returns transactionFeeExtended.right() } - private fun createModel(): GiveApprovalModel = GiveApprovalModel( + private fun createParams(amount: String): GiveApprovalComponent.Params = GiveApprovalComponent.Params( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCryptoCurrencyStatus = cryptoCurrencyStatus, + amount = amount, + spenderAddress = "0xSpender", + amountFooter = TextReference.EMPTY, + feeFooter = TextReference.EMPTY, + callback = mockk(relaxed = true), + ) + + private fun createModel(amount: String = "10"): GiveApprovalModel = GiveApprovalModel( dispatchers = TestingCoroutineDispatcherProvider(), - paramsContainer = MutableParamsContainer(params), + paramsContainer = MutableParamsContainer(createParams(amount)), createApprovalTransactionUseCase = createApprovalTransactionUseCase, getAllowanceInfoUseCase = getAllowanceInfoUseCase, sendTransactionUseCase = sendTransactionUseCase, @@ -109,4 +158,118 @@ class GiveApprovalModelTest { coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() } coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() } } + + @Test + fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "1,1") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN point decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "1.1") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN comma decimal amount and UNLIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() = + runTest { + val model = createModel(amount = "1,1") + model.onChangeApproveType(ApproveType.UNLIMITED) + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = isNull(), + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN unparseable amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() = + runTest { + val model = createModel(amount = "abc") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = isNull(), + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFee THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "2,5") + + val result = model.loadFee() + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("2.5")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN unparseable amount WHEN loadFee THEN returns DataError and does not check allowance`() = runTest { + val model = createModel(amount = "abc") + + val result = model.loadFee() + + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isInstanceOf(GetFeeError.DataError::class.java) + coVerify(exactly = 0) { + getAllowanceInfoUseCase( + userWalletId = any(), + cryptoCurrency = any(), + spenderAddress = any(), + requiredAmount = any(), + ) + } + } } \ No newline at end of file