Updated on 2026-08-14
This commit is contained in:
parent
e599995315
commit
f39054b5a9
7 changed files with 788 additions and 19 deletions
|
|
@ -21,12 +21,7 @@ inline fun <T> catchApiResponseError(onError: (ApiResponseError) -> Unit, block:
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the [ApiResponseError] is a network-related error.
|
||||
* You can provide a custom predicate [codePredicate] to check for specific HTTP status codes.
|
||||
*/
|
||||
inline fun ApiResponseError.isNetworkError(
|
||||
codePredicate: (ApiResponseError.HttpException.Code) -> Boolean = { true },
|
||||
): Boolean {
|
||||
return this is ApiResponseError.HttpException && codePredicate(this.code)
|
||||
/** Checks if the ApiResponseError is a network error with the specified HTTP status [code] */
|
||||
fun ApiResponseError.isNetworkError(code: ApiResponseError.HttpException.Code): Boolean {
|
||||
return this is ApiResponseError.HttpException && this.code == code
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ data class GetWalletAccountsResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@Json(name = "totalAccounts") val totalAccounts: Int,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.common.response.isNetworkError
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.Provider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Handles errors that occur during the fetching of wallet accounts
|
||||
*
|
||||
* @property userTokensSaver saves user tokens to the storage
|
||||
* @property userWalletsStore provides access to user wallet data
|
||||
* @property userTokensResponseStore provides access to user token responses.
|
||||
* @property cryptoPortfolioCF factory for converting crypto portfolios
|
||||
* @property userTokensResponseFactory factory for creating user token responses
|
||||
* @property cardCryptoCurrencyFactory factory for creating default cryptocurrencies for multi-currency wallets
|
||||
*
|
||||
* @see DefaultWalletAccountsFetcher
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Handles the error that occurred during the fetching of wallet accounts.
|
||||
* [pushWalletAccounts] and [storeWalletAccounts] are functions that passed as parameters to avoid
|
||||
* cyclic dependencies.
|
||||
*
|
||||
* @param error the error that occurred
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param savedAccountsResponse the previously saved wallet accounts response, if available
|
||||
* @param pushWalletAccounts function to push wallet accounts to the server
|
||||
* @param storeWalletAccounts function to store wallet accounts locally
|
||||
*/
|
||||
suspend fun handle(
|
||||
error: ApiResponseError,
|
||||
userWalletId: UserWalletId,
|
||||
savedAccountsResponse: GetWalletAccountsResponse?,
|
||||
pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List<WalletAccountDTO>) -> Unit,
|
||||
storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit,
|
||||
) {
|
||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||
if (isResponseUpToDate) {
|
||||
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||
return
|
||||
}
|
||||
|
||||
val userWalletProvider = Provider { userWalletsStore.getSyncStrict(key = userWalletId) }
|
||||
val accountDTOs = savedAccountsResponse?.accounts.orDefault(userWalletProvider = userWalletProvider)
|
||||
|
||||
val userTokensResponse = savedAccountsResponse?.toUserTokensResponse()
|
||||
.orFromLegacyStore(userWalletProvider = userWalletProvider)
|
||||
.orDefault(userWalletProvider = userWalletProvider)
|
||||
|
||||
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
||||
if (isNotFoundError) {
|
||||
pushWalletAccounts(userWalletId, accountDTOs)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse)
|
||||
}
|
||||
|
||||
val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse)
|
||||
storeWalletAccounts(userWalletId, response)
|
||||
}
|
||||
|
||||
private fun List<WalletAccountDTO>?.orDefault(userWalletProvider: Provider<UserWallet>): List<WalletAccountDTO> {
|
||||
if (this != null) return this
|
||||
|
||||
val userWallet = userWalletProvider()
|
||||
|
||||
val accounts = AccountList.empty(userWallet).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
val converter = cryptoPortfolioCF.create(userWallet = userWallet)
|
||||
|
||||
return converter.convertListBack(input = accounts)
|
||||
}
|
||||
|
||||
private suspend fun UserTokensResponse?.orFromLegacyStore(
|
||||
userWalletProvider: Provider<UserWallet>,
|
||||
): UserTokensResponse? {
|
||||
if (this != null) return this
|
||||
|
||||
return userTokensResponseStore.getSyncOrNull(
|
||||
userWalletId = userWalletProvider().walletId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse?.orDefault(userWalletProvider: Provider<UserWallet>): UserTokensResponse {
|
||||
if (this != null) return this
|
||||
|
||||
return userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(
|
||||
userWallet = userWalletProvider(),
|
||||
),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun GetWalletAccountsResponse?.orDefault(
|
||||
userWalletId: UserWalletId,
|
||||
accountDTOs: List<WalletAccountDTO>,
|
||||
userTokensResponse: UserTokensResponse,
|
||||
): GetWalletAccountsResponse {
|
||||
if (this != null) return this
|
||||
|
||||
return GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = userTokensResponse.group,
|
||||
sort = userTokensResponse.sort,
|
||||
totalAccounts = accountDTOs.size,
|
||||
),
|
||||
accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.data.account.utils
|
||||
|
||||
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/** Flattens the tokens from all wallet accounts into a single list */
|
||||
internal fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
|
||||
return accounts.flatMap { it.tokens.orEmpty() }
|
||||
}
|
||||
|
||||
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
|
||||
internal fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = wallet.group,
|
||||
sort = wallet.sort,
|
||||
tokens = flattenTokens(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns tokens from a [UserTokensResponse] to the wallet accounts in the [GetWalletAccountsResponse]
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
*
|
||||
* @return a new [GetWalletAccountsResponse]` with tokens assigned to the wallet accounts
|
||||
*/
|
||||
internal fun GetWalletAccountsResponse.assignTokens(userWalletId: UserWalletId): GetWalletAccountsResponse {
|
||||
return copy(
|
||||
accounts = accounts.assignTokens(userWalletId = userWalletId, tokens = unassignedTokens),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns tokens from a [UserTokensResponse] to a list of wallet accounts
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param tokens tokens to be assigned
|
||||
*
|
||||
* @return a new list of [WalletAccountDTO] with tokens assigned to each account
|
||||
*/
|
||||
internal fun List<WalletAccountDTO>.assignTokens(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): List<WalletAccountDTO> {
|
||||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.groupBy { it.accountId }
|
||||
|
||||
return map { accountDTO ->
|
||||
accountDTO.copy(
|
||||
tokens = enrichedTokens[accountDTO.id].orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
package com.tangem.data.account.fetcher
|
||||
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.utils.toUserTokensResponse
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class FetchWalletAccountsErrorHandlerTest {
|
||||
|
||||
private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true)
|
||||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val userTokensResponseStore: UserTokensResponseStore = mockk()
|
||||
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk()
|
||||
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
|
||||
private val userTokensResponseFactory: UserTokensResponseFactory = mockk()
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
|
||||
private val handler = FetchWalletAccountsErrorHandler(
|
||||
userTokensSaver = userTokensSaver,
|
||||
userWalletsStore = userWalletsStore,
|
||||
userTokensResponseStore = userTokensResponseStore,
|
||||
cryptoPortfolioCF = cryptoPortfolioCF,
|
||||
userTokensResponseFactory = userTokensResponseFactory,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
)
|
||||
|
||||
private val userWallet = mockk<UserWallet> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setupEach() {
|
||||
clearMocks(
|
||||
userTokensSaver,
|
||||
userWalletsStore,
|
||||
userTokensResponseStore,
|
||||
cryptoPortfolioCF,
|
||||
cryptoPortfolioConverter,
|
||||
cardCryptoCurrencyFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not update accounts when response is up to date`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.HttpException(
|
||||
code = Code.NOT_MODIFIED,
|
||||
message = "Not Modified",
|
||||
errorBody = null,
|
||||
)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk()
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk()
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify(inverse = true) {
|
||||
userWalletsStore.getSyncStrict(key = any())
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = any())
|
||||
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
|
||||
cryptoPortfolioCF.create(any())
|
||||
cryptoPortfolioConverter.convertListBack(any())
|
||||
pushWalletAccounts(any(), any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
storeWalletAccounts(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pushes and stores accounts when NOT_FOUND error occurs`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.HttpException(
|
||||
code = Code.NOT_FOUND,
|
||||
message = "Not Found",
|
||||
errorBody = null,
|
||||
)
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
name = "Michael Dotson",
|
||||
derivationIndex = 7135,
|
||||
icon = "consectetuer",
|
||||
iconColor = "ferri",
|
||||
tokens = listOf(),
|
||||
totalTokens = 7738,
|
||||
totalNetworks = 3348,
|
||||
)
|
||||
|
||||
val savedAccountsResponse = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = savedAccountsResponse,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
pushWalletAccounts(userWalletId, listOf(accountDTO))
|
||||
userTokensSaver.push(userWalletId, response = savedAccountsResponse.toUserTokensResponse())
|
||||
storeWalletAccounts(userWalletId, savedAccountsResponse)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
userWalletsStore.getSyncStrict(key = any())
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId = any())
|
||||
userTokensResponseFactory.createUserTokensResponse(any(), any(), any())
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any())
|
||||
cryptoPortfolioCF.create(any())
|
||||
cryptoPortfolioConverter.convertListBack(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uses default accounts when savedAccountsResponse is null`() = runTest {
|
||||
// Arrange
|
||||
val error = ApiResponseError.TimeoutException
|
||||
|
||||
val accounts = AccountList.empty(userWallet).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
val accountDTO = WalletAccountDTO(
|
||||
id = "nibh",
|
||||
name = "Michael Dotson",
|
||||
derivationIndex = 7135,
|
||||
icon = "consectetuer",
|
||||
iconColor = "ferri",
|
||||
tokens = listOf(),
|
||||
totalTokens = 7738,
|
||||
totalNetworks = 3348,
|
||||
)
|
||||
|
||||
val savedAccountsResponse = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountDTO),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
val userTokensResponse = savedAccountsResponse.toUserTokensResponse()
|
||||
|
||||
every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet
|
||||
every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter
|
||||
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO)
|
||||
coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
} returns userTokensResponse
|
||||
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
|
||||
|
||||
val pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> Unit = mockk(relaxed = true)
|
||||
val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true)
|
||||
|
||||
// Act
|
||||
handler.handle(
|
||||
error = error,
|
||||
userWalletId = userWalletId,
|
||||
savedAccountsResponse = null,
|
||||
pushWalletAccounts = pushWalletAccounts,
|
||||
storeWalletAccounts = storeWalletAccounts,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
userWalletsStore.getSyncStrict(userWalletId)
|
||||
cryptoPortfolioCF.create(userWallet)
|
||||
cryptoPortfolioConverter.convertListBack(accounts)
|
||||
userTokensResponseStore.getSyncOrNull(userWalletId)
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = emptyList(),
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
|
||||
storeWalletAccounts(userWalletId, any())
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
pushWalletAccounts(any(), any())
|
||||
userTokensSaver.push(userWalletId = any(), response = any())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package com.tangem.data.account.utils
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class GetWalletAccountsResponseExtTest {
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class FlattenTokens {
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns empty list when accounts are empty`() {
|
||||
// Arrange
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns empty list when single account has empty tokens`() {
|
||||
// Arrange
|
||||
val account = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flattenTokens returns all tokens from multiple accounts`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(id = "0")
|
||||
val token2 = createUserToken(id = "1")
|
||||
val token3 = createUserToken(id = "2")
|
||||
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1, token2))
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1, tokens = listOf(token3))
|
||||
val account3 = createWalletAccountDTO(derivationIndex = 2)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(account1, account2, account3),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.flattenTokens()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly(token1, token2, token3)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ToUserTokensResponse {
|
||||
|
||||
@Test
|
||||
fun `toUserTokensResponse returns correct UserTokensResponse for empty accounts and unassignedTokens`() {
|
||||
// Arrange
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 0,
|
||||
),
|
||||
accounts = emptyList(),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.toUserTokensResponse()
|
||||
|
||||
// Assert
|
||||
val expected = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserTokensResponse includes tokens from accounts and unassignedTokens`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(id = "0")
|
||||
val token2 = createUserToken(id = "1")
|
||||
val account = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1))
|
||||
val unassignedToken = token2
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.toUserTokensResponse()
|
||||
|
||||
// Assert
|
||||
val expected = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = listOf(token1),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetWalletAccountsResponseAssignTokens {
|
||||
|
||||
@Test
|
||||
fun `assignTokens correctly assigns tokens to accounts`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = null)
|
||||
val token2 = createUserToken(id = "1", accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(account1, account2),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.assignTokens(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = GetWalletAccountsResponse(
|
||||
wallet = response.wallet,
|
||||
accounts = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assignTokens does not change accounts if there are no unassignedTokens`() {
|
||||
// Arrange
|
||||
val account = createWalletAccountDTO(derivationIndex = 0)
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.assignTokens(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = GetWalletAccountsResponse(
|
||||
wallet = response.wallet,
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class WalletAccountDTOListAssignTokens {
|
||||
|
||||
@Test
|
||||
fun `assignTokens correctly assigns tokens to accounts`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = null)
|
||||
val token2 = createUserToken(id = "1", accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
|
||||
// Act
|
||||
val actual = listOf(account1, account2).assignTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(token1, token2),
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `assignTokens does not change accounts if there are no unassignedTokens`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = accountId)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
// Act
|
||||
val actual = listOf(account1).assignTokens(
|
||||
userWalletId = userWalletId,
|
||||
tokens = listOf(token1),
|
||||
)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWalletAccountDTO(derivationIndex: Int, tokens: List<UserTokensResponse.Token> = emptyList()) =
|
||||
WalletAccountDTO(
|
||||
id = AccountId.forCryptoPortfolio(
|
||||
userWalletId = userWalletId,
|
||||
derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!,
|
||||
).value,
|
||||
name = "Name #$derivationIndex",
|
||||
derivationIndex = derivationIndex,
|
||||
icon = "icon",
|
||||
iconColor = "color",
|
||||
tokens = tokens,
|
||||
totalTokens = tokens.size,
|
||||
totalNetworks = 1,
|
||||
)
|
||||
|
||||
private fun createUserToken(id: String, accountId: String? = "account_id") = UserTokensResponse.Token(
|
||||
id = id,
|
||||
accountId = accountId,
|
||||
networkId = "ethereum",
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
name = "Token",
|
||||
symbol = "T",
|
||||
contractAddress = "0x$id",
|
||||
decimals = 18,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -24,23 +24,36 @@ object UserTokensResponseAccountIdEnricher {
|
|||
* @param response the [UserTokensResponse] containing tokens to be enriched
|
||||
*/
|
||||
operator fun invoke(userWalletId: UserWalletId, response: UserTokensResponse): UserTokensResponse {
|
||||
val hasUnassignedTokens = response.tokens.any { it.accountId == null }
|
||||
if (!hasUnassignedTokens) return response
|
||||
val enrichedTokens = invoke(userWalletId = userWalletId, tokens = response.tokens)
|
||||
|
||||
val enrichedTokens = response.tokens
|
||||
return response.copy(tokens = enrichedTokens)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the given list of tokens with accountId values
|
||||
*
|
||||
* @param userWalletId the ID of the user wallet
|
||||
* @param tokens the list of tokens to be enriched
|
||||
*/
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): List<UserTokensResponse.Token> {
|
||||
val hasUnassignedTokens = tokens.any { it.accountId == null }
|
||||
if (!hasUnassignedTokens) return tokens
|
||||
|
||||
val enrichedTokens = tokens
|
||||
.filter { it.accountId == null }
|
||||
.groupByAccountIndex()
|
||||
.mapKeysToAccountId(userWalletId)
|
||||
.mapToEnrichedTokens()
|
||||
|
||||
if (enrichedTokens.isEmpty()) return response
|
||||
if (enrichedTokens.isEmpty()) return tokens
|
||||
|
||||
return response.copy(
|
||||
tokens = response.tokens.map { token ->
|
||||
val enrichedToken = enrichedTokens.find { it == token }
|
||||
enrichedToken ?: token
|
||||
},
|
||||
)
|
||||
return tokens.map { token ->
|
||||
val enrichedToken = enrichedTokens.firstOrNull { it == token }
|
||||
enrichedToken ?: token
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<UserTokensResponse.Token>.groupByAccountIndex(): Map<Long?, List<UserTokensResponse.Token>> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue