Updated on 2026-08-14
This commit is contained in:
commit
b43aa204d3
10 changed files with 811 additions and 49 deletions
|
|
@ -44,6 +44,7 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository
|
|||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
|
|
@ -70,7 +71,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultRoutingComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted val initialStack: List<AppRoute>?,
|
||||
|
|
@ -99,6 +100,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
||||
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
|
||||
) : RoutingComponent,
|
||||
AppComponentContext by context,
|
||||
SnackbarHandler {
|
||||
|
|
@ -229,18 +231,32 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
|
||||
FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
|
||||
)
|
||||
// Referral users skip the Home stories screen and land directly on the
|
||||
// mobile wallet creation flow.
|
||||
val afterEmptyRoute: AppRoute = if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
|
||||
} else {
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
}
|
||||
|
||||
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
|
||||
?: return AppRoute.Home(launchMode = launchMode)
|
||||
?: return afterEmptyRoute
|
||||
return if (shouldAskPushPermission) {
|
||||
notificationsRepository.setShouldShowNotifications(
|
||||
key = NotificationId.EnablePushesReminderNotification.key,
|
||||
value = false,
|
||||
)
|
||||
AppRoute.PushNotification(AppRoute.PushNotification.Source.Stories)
|
||||
AppRoute.PushNotification(
|
||||
source = AppRoute.PushNotification.Source.Stories,
|
||||
nextRoute = afterEmptyRoute,
|
||||
)
|
||||
} else {
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
afterEmptyRoute
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = PushNotificationsParams(
|
||||
modelCallbacks = PushNotificationsModelCallbacksStub(),
|
||||
source = route.source,
|
||||
nextRoute = AppRoute.Home(),
|
||||
nextRoute = route.nextRoute ?: AppRoute.Home(),
|
||||
),
|
||||
componentFactory = pushNotificationsComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data class PushNotification(
|
||||
val source: Source,
|
||||
val nextRoute: AppRoute? = null,
|
||||
) : AppRoute(path = "/push_notification") {
|
||||
enum class Source {
|
||||
Stories,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
},
|
||||
{
|
||||
"name": "DYNAMIC_ADDRESSES_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "VIRTUAL_ACCOUNTS_ENABLED",
|
||||
|
|
@ -37,11 +37,11 @@
|
|||
},
|
||||
{
|
||||
"name": "SOLANA_TX_HISTORY_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "HEDERA_ERC20_ENABLED",
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
},
|
||||
{
|
||||
"name": "SWAP_AB_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1403_PUSH_NOTIFICATION_SETTINGS_ENABLED",
|
||||
|
|
@ -77,32 +77,36 @@
|
|||
},
|
||||
{
|
||||
"name": "AND_15310_ADD_FUNDS_STAGE1",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15103_SWAP_RATE_EXPERIENCE_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15154_YIELD_PROMO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.express.models.*
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -1372,37 +1373,46 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
feeValue: BigDecimal,
|
||||
selectedFeeToken: CryptoCurrencyStatus? = null,
|
||||
): IncludeFeeInAmountInternal {
|
||||
val isFeeInSameCurrencyToken = selectedFeeToken != null &&
|
||||
fromSwapCurrencyStatus.currency.id == selectedFeeToken.currency.id &&
|
||||
selectedFeeToken.currency is CryptoCurrency.Token
|
||||
|
||||
return if (isFeeInSameCurrencyToken) {
|
||||
// we have a token selected for fee payment the same as sending token
|
||||
val fromBalance = fromSwapCurrencyStatus.status.value.amount
|
||||
val reducedBalance = fromBalance?.minus(reduceBalanceBy).orZero()
|
||||
when {
|
||||
amount.value > reducedBalance -> IncludeFeeInAmountInternal.BalanceNotEnough
|
||||
amount.value + feeValue <= reducedBalance -> IncludeFeeInAmountInternal.Excluded
|
||||
else -> {
|
||||
if (feeValue < amount.value) {
|
||||
IncludeFeeInAmountInternal.Included(
|
||||
amountSubtractFee = SwapAmount(
|
||||
value = reducedBalance - feeValue,
|
||||
decimals = fromSwapCurrencyStatus.currency.decimals,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
IncludeFeeInAmountInternal.Excluded
|
||||
}
|
||||
}
|
||||
return if (fromSwapCurrencyStatus.account is Account.Payment) {
|
||||
val fromBalance = fromSwapCurrencyStatus.status.value.amount.orZero()
|
||||
if (amount.value > fromBalance) {
|
||||
IncludeFeeInAmountInternal.BalanceNotEnough
|
||||
} else {
|
||||
IncludeFeeInAmountInternal.Excluded
|
||||
}
|
||||
} else {
|
||||
getIncludeFeeInAmountForNative(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
val isFeeInSameCurrencyToken = selectedFeeToken != null &&
|
||||
fromSwapCurrencyStatus.currency.id == selectedFeeToken.currency.id &&
|
||||
selectedFeeToken.currency is CryptoCurrency.Token
|
||||
|
||||
if (isFeeInSameCurrencyToken) {
|
||||
// we have a token selected for fee payment the same as sending token
|
||||
val fromBalance = fromSwapCurrencyStatus.status.value.amount
|
||||
val reducedBalance = fromBalance?.minus(reduceBalanceBy).orZero()
|
||||
when {
|
||||
amount.value > reducedBalance -> IncludeFeeInAmountInternal.BalanceNotEnough
|
||||
amount.value + feeValue <= reducedBalance -> IncludeFeeInAmountInternal.Excluded
|
||||
else -> {
|
||||
if (feeValue < amount.value) {
|
||||
IncludeFeeInAmountInternal.Included(
|
||||
amountSubtractFee = SwapAmount(
|
||||
value = reducedBalance - feeValue,
|
||||
decimals = fromSwapCurrencyStatus.currency.decimals,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
IncludeFeeInAmountInternal.Excluded
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getIncludeFeeInAmountForNative(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.feature.swap.domain.fee.TransactionFeeResult
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapBalanceStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Tests for the Tangem Pay early-exit branch in [SwapInteractorImpl].
|
||||
*
|
||||
* When the from-currency belongs to a [Account.Payment] account the fee must
|
||||
* never be included in the swap amount ([IncludeFeeInAmountInternal.Excluded]).
|
||||
*
|
||||
* The private [SwapInteractorImpl.getIncludeFeeInAmountInternal] function is exercised
|
||||
* through the public [SwapInteractorImpl.applySwapFee] entry point (CEX provider path),
|
||||
* which calls [computeBalanceStatus] → [getIncludeFeeInAmountInternal].
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@DisplayName("SwapInteractorImpl — Tangem Pay (Payment account) fee-inclusion behaviour")
|
||||
internal class SwapInteractorImplTangemPayTest : SwapInteractorImplTestBase() {
|
||||
|
||||
private val ethNetwork = Blockchain.Ethereum.toNetworkId()
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val lastReducedBalanceBy = BigDecimal.ZERO
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
// Shared stubs required by computeBalanceStatus / manageWarnings / manageTransactionValidationWarnings
|
||||
coEvery {
|
||||
getCurrencyCheckUseCase.invoke(
|
||||
userWalletId = any(),
|
||||
currencyStatus = any(),
|
||||
feeCurrencyStatus = any(),
|
||||
amount = any(),
|
||||
fee = any(),
|
||||
feeCurrencyBalanceAfterTransaction = any(),
|
||||
recipientAddress = any(),
|
||||
)
|
||||
} returns buildCurrencyCheck()
|
||||
|
||||
coEvery {
|
||||
validateTransactionUseCase.invoke(
|
||||
amount = any(),
|
||||
fee = any(),
|
||||
memo = any(),
|
||||
destination = any(),
|
||||
userWalletId = any(),
|
||||
network = any(),
|
||||
)
|
||||
} returns Unit.right()
|
||||
|
||||
coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("10")
|
||||
coEvery { getFeePaidCryptoCurrencyStatusSyncUseCase.invoke(any(), any()) } returns null.right()
|
||||
coEvery { currenciesRepository.createCoinCurrency(any()) } returns buildCoinCurrency()
|
||||
coEvery { currenciesRepository.getFeePaidCurrency(any(), any()) } returns FeePaidCurrency.Coin
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Payment account — fee always Excluded
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("Payment account (Tangem Pay withdrawal)")
|
||||
inner class PaymentAccountBranch {
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce Sufficient and not FeeAdjustedAmount when Payment account token amount within balance")
|
||||
fun `should produce Sufficient when Payment account token swap and amount within balance`() = runTest {
|
||||
// Token swap: balance=1, amount=0.95, fee=0.1 (amount + fee > balance).
|
||||
// On a CryptoPortfolio account with a same-currency token fee this triggers FeeAdjustedAmount.
|
||||
// On a Payment account the early-exit returns Excluded, so computeBalanceStatus falls through
|
||||
// to isBalanceEnough (token: checks balance >= amount only → true) → Sufficient.
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(BigDecimal("0.95"), 18),
|
||||
isCoin = false,
|
||||
fromBalance = BigDecimal("1"),
|
||||
account = Account.Payment(userWalletId),
|
||||
)
|
||||
val swapFee = buildTestSwapFee(feeValue = BigDecimal("0.001"))
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.Sufficient::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce Sufficient even when from-token and fee-token ids match (same-currency token path bypassed)")
|
||||
fun `should produce Sufficient when Payment account and same-currency token fee selected`() = runTest {
|
||||
// With a CryptoPortfolio account this scenario (same token for fee and swap) would trigger
|
||||
// the IncludeFeeInAmountInternal.Included / BalanceNotEnough paths.
|
||||
// Payment account must short-circuit before reaching that logic.
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(BigDecimal("1"), 18),
|
||||
isCoin = false,
|
||||
fromBalance = BigDecimal("1"),
|
||||
account = Account.Payment(userWalletId),
|
||||
)
|
||||
// Build a fee token that shares the same currency id as the from-token — triggers same-currency path
|
||||
// on non-Payment accounts.
|
||||
val fromCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus.status
|
||||
val swapFee = buildTestSwapFeeWithToken(
|
||||
feeValue = BigDecimal("0.5"),
|
||||
selectedFeeToken = fromCurrencyStatus,
|
||||
)
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
// Must be Sufficient, not FeeAdjustedAmount or InsufficientFee
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.Sufficient::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce InsufficientAmount when Payment account and amount exceeds balance")
|
||||
fun `should produce InsufficientAmount when Payment account and amount exceeds from-balance`() = runTest {
|
||||
// Even on a Payment account the basic amount-vs-balance check must still apply.
|
||||
coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("10")
|
||||
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(BigDecimal("5"), 18),
|
||||
isCoin = true,
|
||||
fromBalance = BigDecimal("1"),
|
||||
account = Account.Payment(userWalletId),
|
||||
)
|
||||
val swapFee = buildTestSwapFee(feeValue = BigDecimal("0.001"))
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.InsufficientAmount::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Non-Payment account — existing native-fee logic preserved
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Nested
|
||||
@DisplayName("CryptoPortfolio account (existing behaviour preserved)")
|
||||
inner class CryptoPortfolioAccountBranch {
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce Sufficient when CryptoPortfolio account and native balance covers fee")
|
||||
fun `should produce Sufficient when CryptoPortfolio account and native balance covers fee`() = runTest {
|
||||
coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("10")
|
||||
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(BigDecimal("1"), 18),
|
||||
isCoin = true,
|
||||
fromBalance = BigDecimal("10"),
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
)
|
||||
val swapFee = buildTestSwapFee(feeValue = BigDecimal("0.001"))
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.Sufficient::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce InsufficientFee when CryptoPortfolio account and native balance below fee")
|
||||
fun `should produce InsufficientFee when CryptoPortfolio and native balance below fee`() = runTest {
|
||||
coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("0.0001")
|
||||
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(BigDecimal("1"), 18),
|
||||
isCoin = false, // token → fee paid from native
|
||||
fromBalance = BigDecimal("10"),
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
)
|
||||
val swapFee = buildTestSwapFee(feeValue = BigDecimal("0.01"))
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.InsufficientFee::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should produce FeeAdjustedAmount when CryptoPortfolio account, same-currency token fee, and amount fills balance")
|
||||
fun `should produce FeeAdjustedAmount when CryptoPortfolio and same-currency token fee squeezes amount`() =
|
||||
runTest {
|
||||
// same-token fee path: amount fills the balance but amount + fee > balance → FeeAdjustedAmount
|
||||
val fromBalance = BigDecimal("1")
|
||||
val feeValue = BigDecimal("0.1")
|
||||
val amount = BigDecimal("0.95") // 0.95 + 0.1 = 1.05 > 1 → triggers Included
|
||||
|
||||
val state = buildCexQuotesLoadedState(
|
||||
fromAmount = SwapAmount(amount, 18),
|
||||
isCoin = false,
|
||||
fromBalance = fromBalance,
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
)
|
||||
val fromCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus.status
|
||||
val swapFee = buildTestSwapFeeWithToken(
|
||||
feeValue = feeValue,
|
||||
selectedFeeToken = fromCurrencyStatus,
|
||||
)
|
||||
|
||||
val patched = sut.applySwapFee(state, swapFee, lastReducedBalanceBy)
|
||||
|
||||
assertThat(patched.preparedSwapConfigState.balanceStatus)
|
||||
.isInstanceOf(SwapBalanceStatus.FeeAdjustedAmount::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private fun buildCurrencyCheck(): CryptoCurrencyCheck = CryptoCurrencyCheck(
|
||||
dustValue = null,
|
||||
reserveAmount = null,
|
||||
minimumSendAmount = null,
|
||||
existentialDeposit = null,
|
||||
utxoAmountLimit = null,
|
||||
isAccountFunded = true,
|
||||
rentWarning = null,
|
||||
isMemoRequired = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds a [SwapState.QuotesLoadedState] with a CEX provider so that [applySwapFee] routes
|
||||
* through [computeBalanceStatus] → [getIncludeFeeInAmountInternal].
|
||||
*
|
||||
* The [account] parameter is the real domain [Account] instance to put on [SwapCurrencyStatus].
|
||||
*/
|
||||
private fun buildCexQuotesLoadedState(
|
||||
fromAmount: SwapAmount,
|
||||
isCoin: Boolean,
|
||||
fromBalance: BigDecimal,
|
||||
account: Account,
|
||||
): SwapState.QuotesLoadedState {
|
||||
val from = buildSwapCurrencyStatus(
|
||||
networkRawId = ethNetwork,
|
||||
isCoin = isCoin,
|
||||
amount = fromBalance,
|
||||
).copy(account = account)
|
||||
val to = buildSwapCurrencyStatus(networkRawId = ethNetwork)
|
||||
return SwapState.QuotesLoadedState(
|
||||
fromTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = fromAmount,
|
||||
swapCurrencyStatus = from,
|
||||
amountFiat = BigDecimal.ZERO,
|
||||
),
|
||||
toTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(BigDecimal("0.5"), 18),
|
||||
swapCurrencyStatus = to,
|
||||
amountFiat = BigDecimal.ZERO,
|
||||
),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
preparedSwapConfigState = PreparedSwapConfigState(
|
||||
balanceStatus = SwapBalanceStatus.Pending,
|
||||
hasOutgoingTransaction = false,
|
||||
),
|
||||
permissionState = PermissionDataState.Empty,
|
||||
swapDataModel = null,
|
||||
currencyCheck = null,
|
||||
validationResult = null,
|
||||
minAdaValue = null,
|
||||
swapProvider = buildSwapProvider(ExchangeProviderType.CEX),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTestSwapFee(
|
||||
feeValue: BigDecimal,
|
||||
otherNativeFee: BigDecimal = BigDecimal.ZERO,
|
||||
): SwapFee {
|
||||
val feeAmount = mockk<Amount>(relaxed = true) {
|
||||
every { value } returns feeValue
|
||||
}
|
||||
val fee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { this@mockk.amount } returns feeAmount
|
||||
}
|
||||
val feeTokenStatus = mockk<CryptoCurrencyStatus>(relaxed = true) {
|
||||
every { currency } returns buildCoinCurrency()
|
||||
}
|
||||
return SwapFee(
|
||||
fee = fee,
|
||||
transactionFeeResult = TransactionFeeResult.Loaded(mockk<TransactionFee.Single>(relaxed = true)),
|
||||
selectedFeeToken = feeTokenStatus,
|
||||
otherNativeFee = otherNativeFee,
|
||||
feeBucket = FeeBucket.MARKET,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a [SwapFee] whose [SwapFee.selectedFeeToken] is the given [CryptoCurrencyStatus].
|
||||
* This triggers the same-currency-token path in [getIncludeFeeInAmountInternal] for
|
||||
* [Account.CryptoPortfolio] accounts.
|
||||
*/
|
||||
private fun buildTestSwapFeeWithToken(
|
||||
feeValue: BigDecimal,
|
||||
selectedFeeToken: CryptoCurrencyStatus,
|
||||
): SwapFee {
|
||||
val feeAmount = mockk<Amount>(relaxed = true) {
|
||||
every { value } returns feeValue
|
||||
}
|
||||
val fee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { this@mockk.amount } returns feeAmount
|
||||
}
|
||||
return SwapFee(
|
||||
fee = fee,
|
||||
transactionFeeResult = TransactionFeeResult.Loaded(mockk<TransactionFee.Single>(relaxed = true)),
|
||||
selectedFeeToken = selectedFeeToken,
|
||||
otherNativeFee = BigDecimal.ZERO,
|
||||
feeBucket = FeeBucket.MARKET,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,8 +160,9 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
val isPermissionNotReady = loadedState?.permissionState !is PermissionDataState.Empty
|
||||
val isInTransferMode = dataState.currentTransferState != null
|
||||
val isSwapNotReady = !isInTransferMode && (isProviderMissing || isPermissionNotReady)
|
||||
val isTangemPayWithdrawal = model.isTangemPayWithdrawal()
|
||||
|
||||
isAmountEmptyOrZero || isInsufficientFunds || isSwapNotReady
|
||||
isAmountEmptyOrZero || isInsufficientFunds || isSwapNotReady || isTangemPayWithdrawal
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1924,7 +1924,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun isTangemPayWithdrawal(): Boolean {
|
||||
fun isTangemPayWithdrawal(): Boolean {
|
||||
return tangemPayInput?.isWithdrawal == true || dataState.fromSwapCurrencyStatus?.account is Account.Payment
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -497,6 +497,7 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
val priceImpact = quoteModel.priceImpact
|
||||
val isTangemPayWithdrawal = fromSwapCurrencyStatus.account is Account.Payment
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = sendInput,
|
||||
|
|
@ -550,7 +551,12 @@ internal class StateBuilder(
|
|||
),
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(fromSwapCurrencyStatus.userWallet),
|
||||
isEnabled = getSwapButtonEnabled(notifications, priceImpact, swapFee),
|
||||
isEnabled = getSwapButtonEnabled(
|
||||
notifications = notifications,
|
||||
priceImpact = priceImpact,
|
||||
swapFee = swapFee,
|
||||
isTangemPayWithdrawal = isTangemPayWithdrawal,
|
||||
),
|
||||
isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
|
|
@ -631,8 +637,10 @@ internal class StateBuilder(
|
|||
notifications: ImmutableList<NotificationUM>,
|
||||
priceImpact: PriceImpact,
|
||||
swapFee: SwapFee?,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): Boolean {
|
||||
return swapFee != null && notifications.none { notification ->
|
||||
val isSwapTxReady = isTangemPayWithdrawal || swapFee != null
|
||||
return isSwapTxReady && notifications.none { notification ->
|
||||
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
|
||||
notification is SwapNotificationUM.Warning.ExpressErrorWarning ||
|
||||
notification is SwapNotificationUM.Warning.ExpressGeneralError ||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,386 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
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.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.feature.swap.domain.fee.TransactionFeeResult
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Tests for the [StateBuilder.getSwapButtonEnabled] path as exposed via
|
||||
* [StateBuilder.createQuotesLoadedState].
|
||||
*
|
||||
* The change under test:
|
||||
* val isSwapTxReady = isTangemPayWithdrawal || swapFee != null
|
||||
*
|
||||
* Truth table asserted here:
|
||||
* | isTangemPay | swapFee | blocking notification | expected isEnabled |
|
||||
* |-------------|---------|----------------------|--------------------|
|
||||
* | true | null | none | true |
|
||||
* | true | null | present | false |
|
||||
* | false | null | none | false |
|
||||
* | false | non-null| none | true |
|
||||
* | false | non-null| present | false |
|
||||
*/
|
||||
@DisplayName("StateBuilder — swap button enabled logic (isTangemPayWithdrawal gate)")
|
||||
internal class StateBuilderSwapButtonTest {
|
||||
|
||||
private val actions: UiActions = mockk(relaxed = true)
|
||||
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
|
||||
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
|
||||
private val isAccountsModeProvider: Provider<Boolean> = mockk()
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
|
||||
private val appRouter: AppRouter = mockk()
|
||||
|
||||
private lateinit var sut: StateBuilder
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
every { isBalanceHiddenProvider() } returns false
|
||||
every { appCurrencyProvider() } returns AppCurrency.Default
|
||||
every { isAccountsModeProvider() } returns false
|
||||
|
||||
sut = StateBuilder(
|
||||
actions = actions,
|
||||
isBalanceHiddenProvider = isBalanceHiddenProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
isAccountsModeProvider = isAccountsModeProvider,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
swapFeatureToggles = swapFeatureToggles,
|
||||
appRouter = appRouter,
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Tangem Pay withdrawal (Payment account)")
|
||||
inner class `Tangem Pay withdrawal` {
|
||||
|
||||
@Test
|
||||
@DisplayName("should enable swap button when Payment account, swapFee is null, and no blocking notifications")
|
||||
fun `should enable swap button when Payment account and swapFee null and no blocking notifications`() {
|
||||
val paymentAccount = Account.Payment(userWalletId)
|
||||
val state = buildQuotesLoadedStateFor(
|
||||
account = paymentAccount,
|
||||
hasOutgoingTransaction = false,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
)
|
||||
val baseHolder = buildInputtableHolder()
|
||||
|
||||
val result = sut.createQuotesLoadedState(
|
||||
uiStateHolder = baseHolder,
|
||||
quoteModel = state,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
bestRatedProviderId = "p",
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
swapFee = null,
|
||||
feeError = null,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should disable swap button when Payment account, swapFee is null, but a blocking notification is present")
|
||||
fun `should disable swap button when Payment account and swapFee null and blocking notification`() {
|
||||
val paymentAccount = Account.Payment(userWalletId)
|
||||
val state = buildQuotesLoadedStateFor(
|
||||
account = paymentAccount,
|
||||
// hasOutgoingTransaction=true produces a SwapNotificationUM.Error which blocks the button
|
||||
hasOutgoingTransaction = true,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
)
|
||||
val baseHolder = buildInputtableHolder()
|
||||
|
||||
val result = sut.createQuotesLoadedState(
|
||||
uiStateHolder = baseHolder,
|
||||
quoteModel = state,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
bestRatedProviderId = "p",
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
swapFee = null,
|
||||
feeError = null,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Non-Pay account (CryptoPortfolio)")
|
||||
inner class `Non-Pay account` {
|
||||
|
||||
@Test
|
||||
@DisplayName("should disable swap button when CryptoPortfolio account and swapFee is null")
|
||||
fun `should disable swap button when CryptoPortfolio account and swapFee null`() {
|
||||
val cryptoAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val state = buildQuotesLoadedStateFor(
|
||||
account = cryptoAccount,
|
||||
hasOutgoingTransaction = false,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
)
|
||||
val baseHolder = buildInputtableHolder()
|
||||
|
||||
val result = sut.createQuotesLoadedState(
|
||||
uiStateHolder = baseHolder,
|
||||
quoteModel = state,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
bestRatedProviderId = "p",
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
swapFee = null,
|
||||
feeError = null,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should enable swap button when CryptoPortfolio account, swapFee is non-null, and no blocking notifications")
|
||||
fun `should enable swap button when CryptoPortfolio account and swapFee non-null and no blocking notifications`() {
|
||||
val cryptoAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val state = buildQuotesLoadedStateFor(
|
||||
account = cryptoAccount,
|
||||
hasOutgoingTransaction = false,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
)
|
||||
val baseHolder = buildInputtableHolder()
|
||||
|
||||
val result = sut.createQuotesLoadedState(
|
||||
uiStateHolder = baseHolder,
|
||||
quoteModel = state,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
bestRatedProviderId = "p",
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
swapFee = buildSwapFee(),
|
||||
feeError = null,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should disable swap button when CryptoPortfolio account, swapFee is non-null, but a blocking notification is present")
|
||||
fun `should disable swap button when CryptoPortfolio account and swapFee non-null and blocking notification`() {
|
||||
val cryptoAccount = Account.CryptoPortfolio.createMainAccount(userWalletId)
|
||||
val state = buildQuotesLoadedStateFor(
|
||||
account = cryptoAccount,
|
||||
// PermissionRequired triggers SwapNotificationUM.Info.PermissionNeeded — in the blocking list
|
||||
hasOutgoingTransaction = false,
|
||||
permissionState = PermissionDataState.PermissionRequired(
|
||||
isResetApproval = false,
|
||||
spenderAddress = "0xspender",
|
||||
),
|
||||
)
|
||||
val baseHolder = buildInputtableHolder()
|
||||
|
||||
val result = sut.createQuotesLoadedState(
|
||||
uiStateHolder = baseHolder,
|
||||
quoteModel = state,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
bestRatedProviderId = "p",
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
swapFee = buildSwapFee(),
|
||||
feeError = null,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Builds a [SwapStateHolder] whose send/receive cards are [SwapCardState.SwapCardData] with
|
||||
* [TransactionCardType.Inputtable] type — required by [StateBuilder.createQuotesLoadedState].
|
||||
*/
|
||||
private fun buildInputtableHolder(): SwapStateHolder {
|
||||
val fromStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val toStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
val emptyAmountState = SwapState.EmptyAmountState(stringReference("$0.00"))
|
||||
val loading = sut.createInitialLoadingState()
|
||||
return sut.createInitialReadyState(
|
||||
uiStateHolder = loading,
|
||||
emptyAmountState = emptyAmountState,
|
||||
fromSwapCurrencyStatus = fromStatus,
|
||||
toSwapCurrencyStatus = toStatus,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a minimal [SwapState.QuotesLoadedState] with the given [account] on the from-currency
|
||||
* and configurable notification triggers.
|
||||
*
|
||||
* @param hasOutgoingTransaction when true, [SwapNotificationsFactory] adds a
|
||||
* [SwapNotificationUM.Error.TransactionInProgressWarning] — a blocking Error notification.
|
||||
* @param permissionState when [PermissionDataState.PermissionRequired], adds a
|
||||
* [SwapNotificationUM.Info.PermissionNeeded] — also in the blocking list.
|
||||
*/
|
||||
private fun buildQuotesLoadedStateFor(
|
||||
account: Account,
|
||||
hasOutgoingTransaction: Boolean,
|
||||
permissionState: PermissionDataState,
|
||||
): SwapState.QuotesLoadedState {
|
||||
val networkRawId = Blockchain.Ethereum.toNetworkId()
|
||||
|
||||
val networkId = mockk<Network.ID>(relaxed = true) {
|
||||
every { rawId } returns Network.RawID(networkRawId)
|
||||
}
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { rawId } returns networkRawId
|
||||
every { id } returns networkId
|
||||
every { currencySymbol } returns "ETH"
|
||||
every { name } returns "Ethereum"
|
||||
}
|
||||
val currency = mockk<CryptoCurrency.Coin>(relaxed = true) {
|
||||
every { this@mockk.network } returns network
|
||||
every { this@mockk.symbol } returns "ETH"
|
||||
every { this@mockk.decimals } returns 18
|
||||
}
|
||||
val networkAddress = mockk<NetworkAddress>(relaxed = true) {
|
||||
every { defaultAddress } returns NetworkAddress.Address(
|
||||
value = "0xTest",
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
)
|
||||
}
|
||||
val statusValue = mockk<CryptoCurrencyStatus.Loaded>(relaxed = true) {
|
||||
every { amount } returns BigDecimal("1")
|
||||
every { this@mockk.networkAddress } returns networkAddress
|
||||
every { pendingTransactions } returns emptySet()
|
||||
}
|
||||
val cryptoCurrencyStatus = CryptoCurrencyStatus(currency = currency, value = statusValue)
|
||||
|
||||
val userWallet = mockk<UserWallet>(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
val fromSwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = cryptoCurrencyStatus,
|
||||
account = account,
|
||||
)
|
||||
val toSwapCurrencyStatus = buildSwapCurrencyStatusWithCryptoPortfolio(coldWallet)
|
||||
|
||||
return SwapState.QuotesLoadedState(
|
||||
fromTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(BigDecimal("0.5"), 18),
|
||||
swapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
amountFiat = BigDecimal.ZERO,
|
||||
),
|
||||
toTokenInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(BigDecimal("0.5"), 18),
|
||||
swapCurrencyStatus = toSwapCurrencyStatus,
|
||||
amountFiat = BigDecimal.ZERO,
|
||||
),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
preparedSwapConfigState = PreparedSwapConfigState(
|
||||
balanceStatus = SwapBalanceStatus.Sufficient,
|
||||
hasOutgoingTransaction = hasOutgoingTransaction,
|
||||
),
|
||||
permissionState = permissionState,
|
||||
swapDataModel = null,
|
||||
currencyCheck = null,
|
||||
validationResult = null,
|
||||
minAdaValue = null,
|
||||
swapProvider = buildProvider(ExchangeProviderType.CEX),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildSwapCurrencyStatusWithCryptoPortfolio(userWallet: UserWallet): SwapCurrencyStatus {
|
||||
val walletId = userWallet.walletId
|
||||
val account = Account.CryptoPortfolio.createMainAccount(walletId)
|
||||
val currency: CryptoCurrency = mockk(relaxed = true) {
|
||||
every { symbol } returns "BTC"
|
||||
every { decimals } returns 8
|
||||
every { network } returns mockk(relaxed = true) {
|
||||
every { id } returns mockk(relaxed = true)
|
||||
every { name } returns "Bitcoin"
|
||||
every { currencySymbol } returns "BTC"
|
||||
}
|
||||
}
|
||||
val statusValue: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { amount } returns BigDecimal("1.0")
|
||||
}
|
||||
val cryptoCurrencyStatus = CryptoCurrencyStatus(currency = currency, value = statusValue)
|
||||
return SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = cryptoCurrencyStatus,
|
||||
account = account,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildProvider(type: ExchangeProviderType): SwapProvider = SwapProvider(
|
||||
providerId = "p",
|
||||
rateTypes = listOf(RateType.FLOAT),
|
||||
name = "Provider",
|
||||
type = type,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
isRecommended = false,
|
||||
slippage = null,
|
||||
isExtraIdSupported = false,
|
||||
)
|
||||
|
||||
private fun buildSwapFee(): SwapFee {
|
||||
val amount = mockk<Amount>(relaxed = true) {
|
||||
every { value } returns BigDecimal("0.001")
|
||||
}
|
||||
val fee = mockk<Fee.Common>(relaxed = true) {
|
||||
every { this@mockk.amount } returns amount
|
||||
}
|
||||
val feeTokenStatus = mockk<CryptoCurrencyStatus>(relaxed = true)
|
||||
return SwapFee(
|
||||
fee = fee,
|
||||
transactionFeeResult = TransactionFeeResult.Loaded(mockk<TransactionFee.Single>(relaxed = true)),
|
||||
selectedFeeToken = feeTokenStatus,
|
||||
otherNativeFee = BigDecimal.ZERO,
|
||||
feeBucket = FeeBucket.MARKET,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue