Updated on 2026-08-14
This commit is contained in:
parent
2e8d0bc03b
commit
298e7a5565
29 changed files with 687 additions and 43 deletions
|
|
@ -47,6 +47,9 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
* @param userWalletId the wallet to resolve currencies for
|
||||
* @param initialCryptoCurrency pre-selected currency, or null to auto-select
|
||||
* @param swapCurrencyPosition preferred position for the initial currency
|
||||
* @param initialToCryptoCurrency optional currency to pre-select as TO. It is placed into the TO slot
|
||||
* ONLY if it already exists in the user's crypto portfolio (and the TO slot wasn't filled otherwise);
|
||||
* if the currency is not added to the wallet, the TO slot stays empty.
|
||||
* @return pair of (from, to) [SwapCurrencyStatus]; either or both may be null
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -54,6 +57,7 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
initialCryptoCurrency: CryptoCurrency?,
|
||||
swapCurrencyPosition: CurrencyPosition,
|
||||
isPaymentAccount: Boolean,
|
||||
initialToCryptoCurrency: CryptoCurrency? = null,
|
||||
): Pair<SwapCurrencyStatus?, SwapCurrencyStatus?> {
|
||||
val walletAccountList = getWalletAccountCurrencyStatusList(userWalletId)
|
||||
val cryptoPortfolioAccounts = walletAccountList.filterKeys { accountStatus ->
|
||||
|
|
@ -65,7 +69,7 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
|
||||
val cryptoCurrencyList = cryptoPortfolioAccounts.values.flatten()
|
||||
|
||||
return if (initialCryptoCurrency != null) {
|
||||
val (from, to) = if (initialCryptoCurrency != null) {
|
||||
val selectedSwapCurrencyStatus = if (isPaymentAccount) {
|
||||
cryptoPaymentAccounts
|
||||
} else {
|
||||
|
|
@ -91,6 +95,43 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
cryptoCurrencyList = cryptoCurrencyList,
|
||||
) to null
|
||||
}
|
||||
|
||||
val resolvedTo = to ?: resolveExplicitToCurrency(
|
||||
initialToCryptoCurrency = initialToCryptoCurrency,
|
||||
from = from,
|
||||
cryptoPortfolioAccountsMap = cryptoPortfolioAccounts,
|
||||
)
|
||||
|
||||
return from to resolvedTo
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the optional explicit TO currency, but only if it is already present in the user's crypto
|
||||
* portfolio. Matches by token identity ([isSameTokenAs]) rather than full id, since the passed currency
|
||||
* may come from a different account/derivation. Prefers the instance from the FROM account, then falls
|
||||
* back to the first match across the portfolio. Never returns the same token as FROM.
|
||||
*/
|
||||
private fun resolveExplicitToCurrency(
|
||||
initialToCryptoCurrency: CryptoCurrency?,
|
||||
from: SwapCurrencyStatus?,
|
||||
cryptoPortfolioAccountsMap: Map<AccountStatus.CryptoPortfolio, List<SwapCurrencyStatus>>,
|
||||
): SwapCurrencyStatus? {
|
||||
if (initialToCryptoCurrency == null) return null
|
||||
|
||||
val fromCurrency = from?.currency
|
||||
fun matches(status: SwapCurrencyStatus): Boolean {
|
||||
return status.currency.isSameTokenAs(initialToCryptoCurrency) &&
|
||||
(fromCurrency == null || !status.currency.isSameTokenAs(fromCurrency))
|
||||
}
|
||||
|
||||
val fromAccountMatch = from?.account?.accountId?.let { fromAccountId ->
|
||||
cryptoPortfolioAccountsMap.entries
|
||||
.firstOrNull { (accountStatus, _) -> accountStatus.account.accountId == fromAccountId }
|
||||
?.value
|
||||
?.firstOrNull(::matches)
|
||||
}
|
||||
|
||||
return fromAccountMatch ?: cryptoPortfolioAccountsMap.values.flatten().firstOrNull(::matches)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
||||
private val initialCryptoCurrency = params.cryptoCurrency
|
||||
private val initialCryptoCurrency = params.fromCryptoCurrency
|
||||
private val tangemPayInput = params.tangemPayInput
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
|
@ -407,8 +407,9 @@ internal class SwapModel @Inject constructor(
|
|||
val (fromSwapCurrencyStatus, toSwapCurrencyStatus) = initialCurrenciesResolver(
|
||||
userWalletId = params.userWalletId,
|
||||
initialCryptoCurrency = initialCryptoCurrency,
|
||||
swapCurrencyPosition = params.currencyPosition,
|
||||
swapCurrencyPosition = params.fromCurrencyPosition,
|
||||
isPaymentAccount = params.tangemPayInput != null,
|
||||
initialToCryptoCurrency = params.toCryptoCurrency,
|
||||
)
|
||||
|
||||
preselectedFromCurrency = fromSwapCurrencyStatus?.currency
|
||||
|
|
@ -2316,7 +2317,7 @@ internal class SwapModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
val transaction = dataState.getCurrentLoadedSwapState()?.swapDataModel?.transaction
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val fromCurrency = fromSwapCurrencyStatus?.currency ?: params.cryptoCurrency
|
||||
val fromCurrency = fromSwapCurrencyStatus?.currency ?: params.fromCryptoCurrency
|
||||
val fromWalletId = fromSwapCurrencyStatus?.userWalletId ?: params.userWalletId
|
||||
val network = fromCurrency?.network
|
||||
val fee = getSelectedSwapFee()?.fee
|
||||
|
|
|
|||
|
|
@ -1151,6 +1151,161 @@ internal class DefaultInitialCurrenciesResolverTest {
|
|||
|
||||
// endregion
|
||||
|
||||
// region explicit TO currency
|
||||
|
||||
@Test
|
||||
fun `GIVEN explicit TO currency present in portfolio WHEN invoke with position FROM THEN it is placed as TO`() =
|
||||
runTest {
|
||||
val fromId = mockk<CryptoCurrency.ID>(relaxed = true)
|
||||
val initialFrom = mockCryptoCurrency(id = fromId)
|
||||
val accountFrom = mockCryptoCurrency(id = fromId)
|
||||
|
||||
// Same token (network + contract), different id instance from the passed one.
|
||||
val accountTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
val explicitTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
|
||||
val fromStatus = createCurrencyStatus(accountFrom, fiatAmount = BigDecimal("100"))
|
||||
val toStatus = createCurrencyStatus(accountTo, fiatAmount = BigDecimal("50"))
|
||||
val accountStatus = createCryptoPortfolioAccountStatus(listOf(fromStatus, toStatus))
|
||||
setupSupplier(listOf(accountStatus))
|
||||
setupAvailability(linkedMapOf(accountFrom to true, accountTo to true))
|
||||
|
||||
val (from, to) = resolver.invoke(
|
||||
userWalletId,
|
||||
initialCryptoCurrency = initialFrom,
|
||||
swapCurrencyPosition = CurrencyPosition.FROM,
|
||||
isPaymentAccount = false,
|
||||
initialToCryptoCurrency = explicitTo,
|
||||
)
|
||||
|
||||
assertThat(from?.status).isSameInstanceAs(fromStatus)
|
||||
assertThat(to?.status).isSameInstanceAs(toStatus)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN explicit TO currency not present in portfolio WHEN invoke THEN TO stays null`() = runTest {
|
||||
val fromId = mockk<CryptoCurrency.ID>(relaxed = true)
|
||||
val initialFrom = mockCryptoCurrency(id = fromId)
|
||||
val accountFrom = mockCryptoCurrency(id = fromId)
|
||||
|
||||
val explicitTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xNOT_IN_WALLET"))
|
||||
|
||||
val fromStatus = createCurrencyStatus(accountFrom, fiatAmount = BigDecimal("100"))
|
||||
val accountStatus = createCryptoPortfolioAccountStatus(listOf(fromStatus))
|
||||
setupSupplier(listOf(accountStatus))
|
||||
setupAvailability(linkedMapOf(accountFrom to true))
|
||||
|
||||
val (from, to) = resolver.invoke(
|
||||
userWalletId,
|
||||
initialCryptoCurrency = initialFrom,
|
||||
swapCurrencyPosition = CurrencyPosition.FROM,
|
||||
isPaymentAccount = false,
|
||||
initialToCryptoCurrency = explicitTo,
|
||||
)
|
||||
|
||||
assertThat(from?.status).isSameInstanceAs(fromStatus)
|
||||
assertThat(to).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN explicit TO currency is the same token as FROM WHEN invoke THEN TO stays null`() = runTest {
|
||||
val sharedId = mockCurrencyId("ethereum", "0xUSDT")
|
||||
val initialFrom = mockCryptoCurrency(id = sharedId)
|
||||
val accountFrom = mockCryptoCurrency(id = sharedId)
|
||||
// Passed TO is the same token as FROM (different id instance, same network + contract).
|
||||
val explicitTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
|
||||
val fromStatus = createCurrencyStatus(accountFrom, fiatAmount = BigDecimal("100"))
|
||||
val accountStatus = createCryptoPortfolioAccountStatus(listOf(fromStatus))
|
||||
setupSupplier(listOf(accountStatus))
|
||||
setupAvailability(linkedMapOf(accountFrom to true))
|
||||
|
||||
val (from, to) = resolver.invoke(
|
||||
userWalletId,
|
||||
initialCryptoCurrency = initialFrom,
|
||||
swapCurrencyPosition = CurrencyPosition.FROM,
|
||||
isPaymentAccount = false,
|
||||
initialToCryptoCurrency = explicitTo,
|
||||
)
|
||||
|
||||
assertThat(from?.status).isSameInstanceAs(fromStatus)
|
||||
assertThat(to).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN TO slot already filled by position TO WHEN explicit TO provided THEN explicit TO is ignored`() =
|
||||
runTest {
|
||||
val sharedId = mockk<CryptoCurrency.ID>(relaxed = true)
|
||||
val initialCurrency = mockCryptoCurrency(id = sharedId)
|
||||
val accountCurrency = mockCryptoCurrency(id = sharedId)
|
||||
|
||||
// A different token that exists in the portfolio and would match the explicit TO.
|
||||
val otherTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
val explicitTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
|
||||
val status = createCurrencyStatus(accountCurrency, fiatAmount = BigDecimal("100"))
|
||||
val otherStatus = createCurrencyStatus(otherTo, fiatAmount = BigDecimal("50"))
|
||||
val accountStatus = createCryptoPortfolioAccountStatus(listOf(status, otherStatus))
|
||||
setupSupplier(listOf(accountStatus))
|
||||
setupAvailability(linkedMapOf(accountCurrency to true, otherTo to true))
|
||||
|
||||
val (from, to) = resolver.invoke(
|
||||
userWalletId,
|
||||
initialCryptoCurrency = initialCurrency,
|
||||
swapCurrencyPosition = CurrencyPosition.TO,
|
||||
isPaymentAccount = false,
|
||||
initialToCryptoCurrency = explicitTo,
|
||||
)
|
||||
|
||||
// Position TO already placed the selected currency in TO; explicit TO must not override it.
|
||||
assertThat(from).isNull()
|
||||
assertThat(to?.status).isSameInstanceAs(status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN explicit TO currency exists in multiple accounts WHEN invoke THEN instance from FROM account is used`() =
|
||||
runTest {
|
||||
// FROM lives in account 1.
|
||||
val fromId = mockk<CryptoCurrency.ID>(relaxed = true)
|
||||
val initialFrom = mockCryptoCurrency(id = fromId)
|
||||
val accountFrom = mockCryptoCurrency(id = fromId)
|
||||
|
||||
// Same TO token present in both accounts (different id instances).
|
||||
val toInAccount1 = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
val toInAccount2 = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
val explicitTo = mockCryptoCurrency(id = mockCurrencyId("ethereum", "0xUSDT"))
|
||||
|
||||
val fromStatus = createCurrencyStatus(accountFrom, fiatAmount = BigDecimal("100"))
|
||||
val toInAccount1Status = createCurrencyStatus(toInAccount1, fiatAmount = BigDecimal("10"))
|
||||
val toInAccount2Status = createCurrencyStatus(toInAccount2, fiatAmount = BigDecimal("5000"))
|
||||
|
||||
val account1 = createCryptoPortfolioAccountStatus(
|
||||
currencies = listOf(fromStatus, toInAccount1Status),
|
||||
derivationIndexValue = 0,
|
||||
)
|
||||
val account2 = createCryptoPortfolioAccountStatus(
|
||||
currencies = listOf(toInAccount2Status),
|
||||
derivationIndexValue = 1,
|
||||
)
|
||||
setupSupplier(listOf(account1, account2))
|
||||
setupAvailability(linkedMapOf(accountFrom to true, toInAccount1 to true))
|
||||
setupAvailability(linkedMapOf(toInAccount2 to true))
|
||||
|
||||
val (from, to) = resolver.invoke(
|
||||
userWalletId,
|
||||
initialCryptoCurrency = initialFrom,
|
||||
swapCurrencyPosition = CurrencyPosition.FROM,
|
||||
isPaymentAccount = false,
|
||||
initialToCryptoCurrency = explicitTo,
|
||||
)
|
||||
|
||||
// TO must be the instance from the FROM account, not the higher-balance duplicate in account 2.
|
||||
assertThat(from?.status).isSameInstanceAs(fromStatus)
|
||||
assertThat(to?.status).isSameInstanceAs(toInAccount1Status)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region helpers
|
||||
|
||||
private fun mockCryptoCurrency(
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ internal abstract class SwapModelTestBase {
|
|||
|
||||
protected fun createParams(): SwapComponent.Params = SwapComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = null,
|
||||
fromCryptoCurrency = null,
|
||||
screenSource = "Test",
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue