Updated on 2026-08-14
This commit is contained in:
parent
b3138bf560
commit
3088258c74
14 changed files with 339 additions and 97 deletions
|
|
@ -3,6 +3,9 @@ package com.tangem.datasource.api.tangemTech.models
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.common.extensions.calculateHashCode
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType.NONE
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
|
|
@ -68,4 +71,8 @@ data class UserTokensResponse(
|
|||
@Json(name = "marketcap")
|
||||
MARKETCAP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun GroupType?.orDefault(): GroupType = this ?: NONE
|
||||
|
||||
fun SortType?.orDefault(): SortType = this ?: SortType.MANUAL
|
||||
|
|
@ -15,9 +15,9 @@ data class GetWalletAccountsResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@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,
|
||||
)
|
||||
}
|
||||
|
|
@ -30,8 +30,8 @@ fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
|
|||
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
|
||||
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = wallet.group,
|
||||
sort = wallet.sort,
|
||||
group = wallet.group ?: GroupType.NONE,
|
||||
sort = wallet.sort ?: SortType.MANUAL,
|
||||
tokens = flattenTokens(),
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package com.tangem.data.account.converter
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.models.TokensGroupType
|
||||
import com.tangem.domain.models.TokensSortType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.utils.converter.Converter
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -27,12 +29,15 @@ internal class AccountListConverter @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun convert(value: GetWalletAccountsResponse): AccountList {
|
||||
val sortType = value.wallet.sort?.let(TokensSortTypeConverter::convert) ?: TokensSortType.NONE
|
||||
val groupType = value.wallet.group?.let(TokensGroupTypeConverter::convert) ?: TokensGroupType.NONE
|
||||
|
||||
return AccountList(
|
||||
userWalletId = userWallet.walletId,
|
||||
accounts = value.accounts.map(cryptoPortfolioConverter::convert),
|
||||
totalAccounts = value.wallet.totalAccounts,
|
||||
sortType = TokensSortTypeConverter.convert(value.wallet.sort),
|
||||
groupType = TokensGroupTypeConverter.convert(value.wallet.group),
|
||||
sortType = sortType,
|
||||
groupType = groupType,
|
||||
)
|
||||
.getOrElse {
|
||||
error("Failed to convert GetWalletAccountsResponse to AccountList: $it")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
|
|||
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.orDefault
|
||||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -159,18 +160,18 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
val response = defaultWalletAccountsResponseFactory.create(
|
||||
userWalletId = userWalletId,
|
||||
userTokensResponse = UserTokensResponse(
|
||||
group = accountsResponse.wallet.group,
|
||||
sort = accountsResponse.wallet.sort,
|
||||
group = accountsResponse.wallet.group.orDefault(),
|
||||
sort = accountsResponse.wallet.sort.orDefault(),
|
||||
tokens = accountsResponse.unassignedTokens,
|
||||
),
|
||||
)
|
||||
|
||||
store(userWalletId = userWalletId, response = response)
|
||||
|
||||
push(userWalletId = userWalletId, accounts = response.accounts)
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
|
||||
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts) ?: response
|
||||
|
||||
store(userWalletId = userWalletId, response = syncedResponse)
|
||||
|
||||
return syncedResponse
|
||||
return response
|
||||
}
|
||||
|
||||
private suspend fun assignTokens(
|
||||
|
|
|
|||
|
|
@ -32,13 +32,30 @@ internal fun List<WalletAccountDTO>.assignTokens(
|
|||
userWalletId: UserWalletId,
|
||||
tokens: List<UserTokensResponse.Token>,
|
||||
): List<WalletAccountDTO> {
|
||||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
val enrichedTokensByAccountId = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.groupBy { it.accountId }
|
||||
|
||||
return map { accountDTO ->
|
||||
accountDTO.copy(
|
||||
tokens = enrichedTokens[accountDTO.id].orEmpty(),
|
||||
)
|
||||
val accountTokens = enrichedTokensByAccountId[accountDTO.id].orEmpty()
|
||||
val isMainAccount = accountDTO.derivationIndex == 0
|
||||
|
||||
val tokens = if (isMainAccount) {
|
||||
val existingAccountIds = map(WalletAccountDTO::id).toSet()
|
||||
val unexistingAccountIds = enrichedTokensByAccountId.keys - existingAccountIds
|
||||
|
||||
val customTokens = unexistingAccountIds.flatMap {
|
||||
enrichedTokensByAccountId[it].orEmpty().map { token ->
|
||||
// Tokens from unexisting accounts should be copied to the main account
|
||||
token.copy(accountId = accountDTO.id)
|
||||
}
|
||||
}
|
||||
|
||||
accountTokens + customTokens
|
||||
} else {
|
||||
accountTokens
|
||||
}
|
||||
|
||||
accountDTO.copy(tokens = accountDTO.tokens.orEmpty() + tokens)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,5 +66,5 @@ internal fun WalletAccountDTO.assignTokens(
|
|||
val enrichedTokens = UserTokensResponseAccountIdEnricher(userWalletId, tokens)
|
||||
.filter { it.accountId == this.id }
|
||||
|
||||
return copy(tokens = enrichedTokens)
|
||||
return copy(tokens = this.tokens.orEmpty() + enrichedTokens)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.account.utils
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.data.account.converter.CryptoPortfolioConverter
|
||||
import com.tangem.data.account.converter.createWalletAccountDTO
|
||||
import com.tangem.data.account.utils.GetWalletAccountsResponseExtTest.Companion.createUserToken
|
||||
import com.tangem.data.common.currency.UserTokensResponseFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
|
|
@ -10,7 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
|
|||
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.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.*
|
||||
|
|
@ -104,15 +104,16 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
|
||||
val token = createUserToken(accountIndex = 0)
|
||||
val defaultResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = listOf(mockk(relaxed = true)),
|
||||
tokens = listOf(token),
|
||||
)
|
||||
every {
|
||||
userTokensResponseFactory.createDefaultResponse(
|
||||
|
|
@ -135,7 +136,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
sort = defaultResponse.sort,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountsDTO),
|
||||
accounts = listOf(accountsDTO.copy(tokens = listOf(token))),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
|
|
@ -202,24 +203,20 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
val userWallet = mockk<UserWallet>(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
val assignedTokens = listOf(mockk<CryptoCurrency.Token>(), mockk<CryptoCurrency.Token>())
|
||||
coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet
|
||||
|
||||
val userTokensResponse = UserTokensResponse(
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = listOf(mockk(relaxed = true)),
|
||||
tokens = listOf(
|
||||
createUserToken(accountIndex = 0),
|
||||
createUserToken(accountIndex = 1),
|
||||
),
|
||||
)
|
||||
every {
|
||||
userTokensResponseFactory.createUserTokensResponse(
|
||||
currencies = assignedTokens,
|
||||
isGroupedByNetwork = false,
|
||||
isSortedByBalance = false,
|
||||
)
|
||||
} returns userTokensResponse
|
||||
|
||||
val accounts = AccountList.empty(userWallet.walletId).accounts
|
||||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
val accountsDTO = createWalletAccountDTO(userWalletId)
|
||||
val accountsDTO = createWalletAccountDTO(userWalletId = userWalletId)
|
||||
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
|
||||
|
||||
// Act
|
||||
|
|
@ -232,7 +229,7 @@ class DefaultWalletAccountsResponseFactoryTest {
|
|||
sort = userTokensResponse.sort,
|
||||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(accountsDTO),
|
||||
accounts = listOf(accountsDTO.copy(tokens = userTokensResponse.tokens)),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ class GetWalletAccountsResponseExtTest {
|
|||
@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 token1 = createUserToken(accountIndex = 0)
|
||||
val token2 = createUserToken(accountIndex = 1)
|
||||
val token3 = createUserToken(accountIndex = 2)
|
||||
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1, token2))
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1, tokens = listOf(token3))
|
||||
|
|
@ -131,10 +131,9 @@ class GetWalletAccountsResponseExtTest {
|
|||
@Test
|
||||
fun `toUserTokensResponse includes tokens from accounts and unassignedTokens`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(id = "0")
|
||||
val token2 = createUserToken(id = "1")
|
||||
val token1 = createUserToken(accountIndex = 0)
|
||||
val token2 = createUserToken(accountIndex = 1)
|
||||
val account = createWalletAccountDTO(derivationIndex = 0, tokens = listOf(token1))
|
||||
val unassignedToken = token2
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
|
|
@ -144,7 +143,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
totalAccounts = 1,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(unassignedToken),
|
||||
unassignedTokens = listOf(token2),
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
@ -169,8 +168,8 @@ class GetWalletAccountsResponseExtTest {
|
|||
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 token1 = createUserToken(accountIndex = 0, accountId = null)
|
||||
val token2 = createUserToken(accountIndex = 1, accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
val response = GetWalletAccountsResponse(
|
||||
|
|
@ -194,10 +193,13 @@ class GetWalletAccountsResponseExtTest {
|
|||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2.copy(
|
||||
tokens = listOf(
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
|
@ -232,6 +234,44 @@ class GetWalletAccountsResponseExtTest {
|
|||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unassignedTokens contain tokens for unexisting accounts`() {
|
||||
// Arrange
|
||||
val token1 = createUserToken(accountIndex = 0, accountId = null)
|
||||
val token2 = createUserToken(accountIndex = 3, accountId = null)
|
||||
val account = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
val response = GetWalletAccountsResponse(
|
||||
wallet = GetWalletAccountsResponse.Wallet(
|
||||
version = 1,
|
||||
group = UserTokensResponse.GroupType.NONE,
|
||||
sort = UserTokensResponse.SortType.MANUAL,
|
||||
totalAccounts = 2,
|
||||
),
|
||||
accounts = listOf(account),
|
||||
unassignedTokens = listOf(token1, token2),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = response.assignTokens(userWalletId)
|
||||
|
||||
// Assert
|
||||
val expected = GetWalletAccountsResponse(
|
||||
wallet = response.wallet,
|
||||
accounts = listOf(
|
||||
account.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"),
|
||||
token2.copy(accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"),
|
||||
),
|
||||
),
|
||||
),
|
||||
unassignedTokens = emptyList(),
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
@ -242,8 +282,8 @@ class GetWalletAccountsResponseExtTest {
|
|||
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 token1 = createUserToken(accountIndex = 0, accountId = null)
|
||||
val token2 = createUserToken(accountIndex = 1, accountId = null)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
val account2 = createWalletAccountDTO(derivationIndex = 1)
|
||||
|
||||
|
|
@ -258,10 +298,13 @@ class GetWalletAccountsResponseExtTest {
|
|||
account1.copy(
|
||||
tokens = listOf(
|
||||
token1.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2.copy(
|
||||
tokens = listOf(
|
||||
token2.copy(accountId = accountId),
|
||||
),
|
||||
),
|
||||
account2,
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
|
@ -271,7 +314,7 @@ class GetWalletAccountsResponseExtTest {
|
|||
fun `assignTokens does not change accounts if there are no unassignedTokens`() {
|
||||
// Arrange
|
||||
val accountId = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027"
|
||||
val token1 = createUserToken(id = "0", accountId = accountId)
|
||||
val token1 = createUserToken(accountIndex = 0, accountId = accountId)
|
||||
val account1 = createWalletAccountDTO(derivationIndex = 0)
|
||||
|
||||
// Act
|
||||
|
|
@ -308,19 +351,19 @@ class GetWalletAccountsResponseExtTest {
|
|||
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,
|
||||
)
|
||||
companion object {
|
||||
|
||||
private companion object {
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
||||
val userWalletId = UserWalletId("011")
|
||||
fun createUserToken(accountIndex: Int, accountId: String? = "account_id") = UserTokensResponse.Token(
|
||||
id = accountIndex.toString(),
|
||||
accountId = accountId,
|
||||
networkId = "ethereum",
|
||||
derivationPath = "m/44'/60'/0'/0/$accountIndex",
|
||||
name = "Token",
|
||||
symbol = "T",
|
||||
contractAddress = "0x$accountIndex",
|
||||
decimals = 18,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -97,9 +97,9 @@ class UserTokensResponseAccountIdEnricherTest {
|
|||
val validNetworkId = mockCryptoCurrencyFactory.ethereum.network.rawId
|
||||
|
||||
val tokenWithUnknownNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
accountId = null,
|
||||
networkId = unknownNetworkId,
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
accountId = null,
|
||||
)
|
||||
|
||||
val tokenWithValidNetworkId = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
|
|
@ -122,6 +122,32 @@ class UserTokensResponseAccountIdEnricherTest {
|
|||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `skips tokens with existing account ids`() {
|
||||
// Arrange
|
||||
val tokenWithAccountId = mockCryptoCurrencyFactory.ethereum
|
||||
.toResponseToken(derivationPath = "m/44'/60'/0'/0/0")
|
||||
.enrichWithAccountId(accountIndex = 0)
|
||||
|
||||
val tokenWithoutAccountId = mockCryptoCurrencyFactory.ethereum.toResponseToken(
|
||||
accountId = null,
|
||||
derivationPath = "m/44'/60'/0'/0/0",
|
||||
)
|
||||
|
||||
val response = listOf(tokenWithAccountId, tokenWithoutAccountId).toResponse()
|
||||
|
||||
// Act
|
||||
val actual = UserTokensResponseAccountIdEnricher(userWalletId, response)
|
||||
|
||||
// Assert
|
||||
val expected = listOf(
|
||||
tokenWithAccountId,
|
||||
tokenWithoutAccountId.enrichWithAccountId(accountIndex = 0),
|
||||
).toResponse()
|
||||
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toResponseToken(
|
||||
accountId: AccountId? = null,
|
||||
networkId: String? = null,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.orDefault
|
||||
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -197,8 +198,8 @@ internal class DefaultManageTokensRepository(
|
|||
|
||||
val tokensResponse = response?.let {
|
||||
UserTokensResponse(
|
||||
group = response.wallet.group,
|
||||
sort = response.wallet.sort,
|
||||
group = response.wallet.group.orDefault(),
|
||||
sort = response.wallet.sort.orDefault(),
|
||||
tokens = accountDTO?.tokens.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -295,8 +296,8 @@ internal class DefaultManageTokensRepository(
|
|||
|
||||
val tokensResponse = response?.let {
|
||||
UserTokensResponse(
|
||||
group = response.wallet.group,
|
||||
sort = response.wallet.sort,
|
||||
group = response.wallet.group.orDefault(),
|
||||
sort = response.wallet.sort.orDefault(),
|
||||
tokens = accountDTO?.tokens.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ data class AccountList private constructor(
|
|||
val canAddMoreAccounts: Boolean
|
||||
get() = accounts.size < MAX_ACCOUNTS_COUNT
|
||||
|
||||
/** Returns the number of active accounts in the list */
|
||||
val activeAccounts: Int
|
||||
get() = accounts.size
|
||||
|
||||
/**
|
||||
* Adds an account to the account list.
|
||||
* If an account with the same identifier already exists, it will be replaced.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,23 @@ class GetAccountCurrencyStatusUseCase(
|
|||
.toOption()
|
||||
}
|
||||
|
||||
suspend fun invokeSync(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
contractAddress: String?,
|
||||
): Option<AccountCryptoCurrencyStatus> {
|
||||
val accountStatusList = getAccountStatusListSync(userWalletId) ?: return none()
|
||||
|
||||
return AccountCryptoCurrencyStatusFinder(
|
||||
accountStatusList = accountStatusList,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
.toOption()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of a specific cryptocurrency for a given user wallet as a [Flow].
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.account.status.utils
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
|
||||
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatuses
|
||||
|
|
@ -34,6 +33,15 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the [AccountCryptoCurrencyStatus] for the specified [currencyId] and [network]
|
||||
* from the given [accountStatusList].
|
||||
*
|
||||
* @param accountStatusList the list of account statuses to search within.
|
||||
* @param currencyId the ID of the cryptocurrency whose status is to be retrieved.
|
||||
* @param network the network associated with the cryptocurrency.
|
||||
* @return the [AccountCryptoCurrencyStatus] if found, otherwise null.
|
||||
*/
|
||||
operator fun invoke(
|
||||
accountStatusList: AccountStatusList,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
|
|
@ -51,6 +59,14 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a map of accounts to their corresponding list of [AccountCryptoCurrencyStatus] for the specified
|
||||
* list of [currencies] from the given [accountStatusList].
|
||||
*
|
||||
* @param accountStatusList the list of account statuses to search within.
|
||||
* @param currencies the list of cryptocurrencies whose statuses are to be retrieved.
|
||||
* @return a map where the key is the account and the value is a list of [AccountCryptoCurrencyStatus].
|
||||
*/
|
||||
operator fun invoke(
|
||||
accountStatusList: AccountStatusList,
|
||||
currencies: List<CryptoCurrency>,
|
||||
|
|
@ -72,15 +88,66 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected account statuses based on the provided [network].
|
||||
* If the [network] is null, all account statuses are returned.
|
||||
* If the network has a specific derivation index, it filters the accounts accordingly.
|
||||
* Retrieves the [AccountCryptoCurrencyStatus] for the specified [networkId], [derivationPath],
|
||||
* and optional [contractAddress] from the given [accountStatusList].
|
||||
*
|
||||
* @param network the network to filter accounts by, can be null.
|
||||
* @return a set of [AccountStatus] that match the expected criteria.
|
||||
* @param accountStatusList the list of account statuses to search within.
|
||||
* @param networkId the ID of the network associated with the cryptocurrency.
|
||||
* @param derivationPath the derivation path of the account.
|
||||
* @param contractAddress the optional contract address of the token (if applicable).
|
||||
* @return the [AccountCryptoCurrencyStatus] if found, otherwise null.
|
||||
*/
|
||||
operator fun invoke(
|
||||
accountStatusList: AccountStatusList,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
contractAddress: String?,
|
||||
): AccountCryptoCurrencyStatus? {
|
||||
return accountStatusList.getExpectedAccountStatuses(
|
||||
rawNetworkId = networkId.rawId.value,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
.asSequence()
|
||||
.filterIsInstance<AccountStatus.CryptoPortfolio>()
|
||||
.mapNotNull { accountStatus ->
|
||||
val status = accountStatus.flattenCurrencies().firstOrNull {
|
||||
val currency = it.currency
|
||||
val isContractAddressMatch = contractAddress == null ||
|
||||
currency.id.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
|
||||
currency.network.rawId == networkId.rawId.value &&
|
||||
currency.network.derivationPath.value == derivationPath.value &&
|
||||
isContractAddressMatch
|
||||
}
|
||||
?: return@mapNotNull null
|
||||
|
||||
AccountCryptoCurrencyStatus(account = accountStatus.account, status = status)
|
||||
}
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): List<AccountStatus> {
|
||||
val possibleAccountIndex = network?.getAccountIndexOrNull()
|
||||
return getExpectedAccountStatuses(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the expected account statuses based on the provided [rawNetworkId] and [derivationPath].
|
||||
* If either parameter is null, all account statuses are returned.
|
||||
* If both parameters are provided, it filters the accounts based on the derivation index.
|
||||
*
|
||||
* @param rawNetworkId the raw ID of the network to filter accounts by, can be null.
|
||||
* @param derivationPath the derivation path of the network to filter accounts by, can be null.
|
||||
* @return a list of [AccountStatus] that match the expected criteria.
|
||||
*/
|
||||
private fun AccountStatusList.getExpectedAccountStatuses(
|
||||
rawNetworkId: String?,
|
||||
derivationPath: Network.DerivationPath?,
|
||||
): List<AccountStatus> {
|
||||
val possibleAccountIndex = if (rawNetworkId != null && derivationPath != null) {
|
||||
getAccountIndexOrNull(rawNetworkId, derivationPath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return when (possibleAccountIndex) {
|
||||
// currency can be in any account
|
||||
|
|
@ -115,7 +182,11 @@ internal object AccountCryptoCurrencyStatusFinder {
|
|||
}
|
||||
|
||||
private fun Network.getAccountIndexOrNull(): Int? {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = rawId) ?: return null
|
||||
return getAccountIndexOrNull(rawNetworkId = rawId, derivationPath = derivationPath)
|
||||
}
|
||||
|
||||
private fun getAccountIndexOrNull(rawNetworkId: String, derivationPath: Network.DerivationPath): Int? {
|
||||
val blockchain = Blockchain.fromId(id = rawNetworkId)
|
||||
val recognizer = AccountNodeRecognizer(blockchain)
|
||||
|
||||
return recognizer.recognize(derivationPath)?.toInt()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.domain.notifications)
|
||||
|
||||
// region Project - Libs
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.crypto)
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,46 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenMode
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
||||
@Assisted private val mode: AddCustomTokenMode,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val singleAccountSupplier: SingleAccountSupplier,
|
||||
@Assisted private val mode: AddCustomTokenMode,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) {
|
||||
|
||||
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> {
|
||||
manageCryptoCurrenciesUseCase(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = mode.userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
),
|
||||
add = currency,
|
||||
)
|
||||
is AddCustomTokenMode.Account -> either {
|
||||
val accountId = getAccountId(currency)
|
||||
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind()
|
||||
}
|
||||
is AddCustomTokenMode.Wallet -> {
|
||||
addCryptoCurrenciesUseCase.invoke(
|
||||
|
|
@ -59,16 +64,13 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
contractAddress: String?,
|
||||
): Either<Throwable, Boolean> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> {
|
||||
val account = singleAccountSupplier.getSyncOrNull(
|
||||
params = SingleAccountProducer.Params(accountId = mode.accountId),
|
||||
getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = mode.accountId.userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
?: return IllegalStateException("Account not found").left()
|
||||
|
||||
account.cryptoCurrencies.none { currency ->
|
||||
networkId == currency.network.id &&
|
||||
derivationPath == currency.network.derivationPath &&
|
||||
contractAddress.equals(currency.id.contractAddress, ignoreCase = true)
|
||||
}
|
||||
.fold(ifEmpty = { true }, ifSome = { false })
|
||||
.right()
|
||||
}
|
||||
is AddCustomTokenMode.Wallet -> checkIsCurrencyNotAddedUseCase.invoke(
|
||||
|
|
@ -79,6 +81,56 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.getAccountId(currency: CryptoCurrency): AccountId {
|
||||
val accountList = singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = mode.userWalletId),
|
||||
)
|
||||
|
||||
ensureNotNull(accountList) {
|
||||
IllegalStateException("Account list not found: ${mode.userWalletId}")
|
||||
}
|
||||
|
||||
if (accountList.activeAccounts == 1) {
|
||||
return AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId)
|
||||
}
|
||||
|
||||
val currencyAccountIndex = currency.getAccountIndex().bind()
|
||||
|
||||
val account = accountList.accounts.firstOrNull { account ->
|
||||
val cryptoPortfolioAccount = account as? Account.CryptoPortfolio
|
||||
|
||||
cryptoPortfolioAccount?.derivationIndex?.value == currencyAccountIndex
|
||||
}
|
||||
|
||||
return account?.accountId ?: AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.getAccountIndex(): Either<Throwable, Int> = either {
|
||||
val currency = this@getAccountIndex
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = currency.network.backendId)
|
||||
if (blockchain == null) {
|
||||
val exception = IllegalStateException("Token has unknown networkId: ${currency.id}")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
}
|
||||
|
||||
val derivationPathValue = currency.network.derivationPath.value
|
||||
if (derivationPathValue == null) {
|
||||
val exception = IllegalStateException("Token has no derivation path: ${currency.id}")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
}
|
||||
|
||||
val accountNodeRecognizer = AccountNodeRecognizer(blockchain)
|
||||
val index = accountNodeRecognizer.recognize(derivationPathValue)?.toInt()
|
||||
|
||||
ensureNotNull(index) {
|
||||
val exception = IllegalStateException("Token has unrecognized derivation path: ${currency.id}")
|
||||
Timber.e(exception)
|
||||
exception
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue