Updated on 2026-08-14
This commit is contained in:
parent
d3dbc2c3d0
commit
1a4405fd28
5 changed files with 382 additions and 16 deletions
|
|
@ -41,7 +41,9 @@ class UserTokensSaver(
|
|||
return@withContext
|
||||
}
|
||||
|
||||
val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
val enrichedResponse = response
|
||||
.withoutDuplicates()
|
||||
.enrichIf(userWalletId = userWalletId, condition = useEnricher)
|
||||
|
||||
push(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend)
|
||||
}
|
||||
|
|
@ -85,7 +87,46 @@ class UserTokensSaver(
|
|||
apiResponse.bind()
|
||||
}
|
||||
},
|
||||
onError = { onFailSend() },
|
||||
onError = { error ->
|
||||
TangemLogger.e("Failed to push ${response.tokens.size} user tokens", error)
|
||||
onFailSend()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops fully identical tokens from the list.
|
||||
*
|
||||
* The API rejects the whole list with `400 All tokens's elements must be unique` if it contains two equal
|
||||
* elements, so a single duplicate discards the update of the entire wallet's token list. Duplicates are not
|
||||
* expected here, hence the error log.
|
||||
*
|
||||
* Tokens are compared by every field of the request instead of [UserTokensResponse.Token.equals], which
|
||||
* deliberately ignores most of them: dropping an element that differs in any way would silently change what the
|
||||
* user has saved.
|
||||
*/
|
||||
private fun UserTokensResponse.withoutDuplicates(): UserTokensResponse {
|
||||
val uniqueTokens = tokens.distinctBy { it.toRequestKey() }
|
||||
|
||||
if (uniqueTokens.size == tokens.size) return this
|
||||
|
||||
TangemLogger.e("Dropped ${tokens.size - uniqueTokens.size} duplicated tokens before pushing them")
|
||||
|
||||
return copy(tokens = uniqueTokens)
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.toRequestKey(): List<Any?> {
|
||||
return listOf(
|
||||
id,
|
||||
accountId,
|
||||
networkId,
|
||||
derivationPath,
|
||||
name,
|
||||
symbol,
|
||||
decimals,
|
||||
contractAddress,
|
||||
addresses,
|
||||
dynamicAddressesEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,4 +95,75 @@ class UserTokensSaverTest {
|
|||
|
||||
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN response with duplicated tokens WHEN push THEN duplicates are dropped before the api call`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val userWallet = mockk<UserWallet.Cold> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.name } returns ""
|
||||
}
|
||||
|
||||
val token = createToken()
|
||||
val response = createResponse(tokens = listOf(token, token))
|
||||
val uniqueResponse = createResponse(tokens = listOf(token))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet))
|
||||
coEvery { enricher(userWalletId, uniqueResponse) } returns uniqueResponse
|
||||
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = response)
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) { tangemTechApi.saveTokens(userWalletId.stringValue, uniqueResponse) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tokens differing in account id only WHEN push THEN both of them are pushed`() = runTest {
|
||||
// GIVEN
|
||||
val userWalletId = UserWalletId("1234567890abcdef")
|
||||
val userWallet = mockk<UserWallet.Cold> {
|
||||
every { this@mockk.walletId } returns userWalletId
|
||||
every { this@mockk.name } returns ""
|
||||
}
|
||||
|
||||
val token = createToken()
|
||||
val response = createResponse(tokens = listOf(token, token.copy(accountId = "other-account")))
|
||||
|
||||
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet))
|
||||
coEvery { enricher(userWalletId, response) } returns response
|
||||
coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Success(Unit)
|
||||
|
||||
// WHEN
|
||||
userTokensSaver.push(userWalletId = userWalletId, response = response)
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 1) { tangemTechApi.saveTokens(userWalletId.stringValue, response) }
|
||||
}
|
||||
|
||||
private fun createResponse(tokens: List<UserTokensResponse.Token>): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
version = 0,
|
||||
group = UserTokensResponse.GroupType.NETWORK,
|
||||
sort = UserTokensResponse.SortType.BALANCE,
|
||||
tokens = tokens,
|
||||
walletName = null,
|
||||
walletType = WalletType.COLD,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createToken(): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = "ethereum",
|
||||
accountId = "account",
|
||||
networkId = "ethereum",
|
||||
derivationPath = "m/44'/60'/0'/0/1",
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
contractAddress = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,11 +20,13 @@ import com.tangem.domain.common.wallets.getSyncStrict
|
|||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||
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.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.config.curvesConfig
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import com.tangem.lib.crypto.derivation.supportsDerivationPath
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -139,11 +141,7 @@ internal class DefaultCustomTokensRepository(
|
|||
): CryptoCurrency.Coin {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
val network = requireNotNull(
|
||||
networkFactory.create(
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
userWallet = userWallet,
|
||||
),
|
||||
createNetwork(networkId = networkId, derivationPath = derivationPath, userWallet = userWallet),
|
||||
) {
|
||||
"Network [$networkId] not found while creating coin"
|
||||
}
|
||||
|
|
@ -151,6 +149,38 @@ internal class DefaultCustomTokensRepository(
|
|||
return cryptoCurrencyFactory.createCoin(network)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [Network] for the given [derivationPath].
|
||||
*
|
||||
* A derivation path typed in by the user always arrives as [Network.DerivationPath.Custom], even when it is the
|
||||
|
||||
* currency shares its network with the currencies already present in the account instead of getting a separate
|
||||
* "custom" network for the very same address.
|
||||
*/
|
||||
private fun createNetwork(
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
userWallet: UserWallet,
|
||||
): Network? {
|
||||
val pathValue = derivationPath.value
|
||||
val blockchain = networkId.toBlockchain()
|
||||
|
||||
val accountIndex = pathValue
|
||||
?.let { AccountNodeRecognizer(blockchain).recognize(derivationPathValue = it) }
|
||||
?.let { DerivationIndex(value = it.toInt()).getOrNull() }
|
||||
|
||||
return if (accountIndex == null) {
|
||||
networkFactory.create(networkId = networkId, derivationPath = derivationPath, userWallet = userWallet)
|
||||
} else {
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = pathValue,
|
||||
userWallet = userWallet,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createToken(
|
||||
managedCryptoCurrency: ManagedCryptoCurrency.Token,
|
||||
sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default,
|
||||
|
|
@ -174,11 +204,7 @@ internal class DefaultCustomTokensRepository(
|
|||
): CryptoCurrency.Token {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
val network = requireNotNull(
|
||||
networkFactory.create(
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
userWallet = userWallet,
|
||||
),
|
||||
createNetwork(networkId = networkId, derivationPath = derivationPath, userWallet = userWallet),
|
||||
) {
|
||||
"Network [$networkId] not found while creating custom token"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue