Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-14 12:46:01 +04:00
parent a2480ff021
commit ba8797ec97
2 changed files with 183 additions and 20 deletions

View file

@ -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(

View file

@ -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<CryptoCurrency.Token>(relaxed = true)
private val tokenCurrency: CryptoCurrency.Token = mockk(relaxed = true) {
every { contractAddress } returns "0xContract"
every { network } returns mockk<Network>(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(),
)
}
}
}