Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-24 17:01:39 +04:00
parent df32ea7ab6
commit 4ef7082504
24 changed files with 214 additions and 185 deletions

View file

@ -2,10 +2,6 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:DefaultWalletsRepository.kt$DefaultWalletsRepository$val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull( key = PreferencesKeys.SAVE_USER_WALLETS_KEY, )</ID>
<ID>BooleanPropertyNaming:DefaultWalletsRepository.kt$DefaultWalletsRepository$val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull( key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, )</ID>
<ID>BooleanPropertyNaming:DefaultWalletsRepository.kt$DefaultWalletsRepository$val requireAccessCode = appPreferencesStore.getSyncOrNull( key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, )</ID>
<ID>BooleanPropertyNaming:DefaultWalletsRepository.kt$DefaultWalletsRepository$val useBiometricAuthentication = appPreferencesStore.getSyncOrNull( key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, )</ID>
<ID>MultilineLambdaItParameter:DefaultColdMapDerivationsRepository.kt$DefaultColdMapDerivationsRepository${ networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, extraDerivationPath = null, userWallet = userWallet, ) }</ID>
<ID>MultilineLambdaItParameter:DefaultDerivationsRepository.kt$DefaultDerivationsRepository${ userWallet.update(it.first) it.second }</ID>
<ID>MultilineLambdaItParameter:DefaultHotMapDerivationsRepository.kt$DefaultHotMapDerivationsRepository${ networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, extraDerivationPath = null, userWallet = userWallet, ) }</ID>
@ -14,19 +10,9 @@
<ID>MultilineLambdaItParameter:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository${ while (true) { emit(toState(id, it.attempts, it.deadline, it.bootCount)) val remaining = remainingSeconds(it.deadline, it.bootCount) if (remaining &lt;= 0) break delay(timeMillis = 1000) } }</ID>
<ID>MultilineLambdaItParameter:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor${ block(UnlockHotWallet(hotWalletId, it)).also { hotWalletPasswordRequester.successfulAuthentication() hotWalletPasswordRequester.dismiss() } }</ID>
<ID>MultilineLambdaItParameter:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor${ tangemHotSdk.getContextUnlock(it).also { unlockHotWallet -&gt; contextualUnlockHotWallet[hotWalletId] = unlockHotWallet } }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ if (it is HttpException &amp;&amp; it.code == HttpException.Code.NOT_FOUND) { Status.NOTIFIED } else { Status.NOT_NEEDED } }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ it.getOrDefault( key = userWalletId, defaultValue = SeedPhraseNotificationsStatus.NOT_NEEDED, ) }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ it.setObjectMap( key = PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY, value = it.getObjectMap&lt;Boolean&gt;(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) .plus(userWalletId.stringValue to isEnabled), ) }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ it.setObjectMap( key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY, value = it.getObjectMap&lt;Boolean&gt;(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .plus(userWalletId.stringValue to false), ) }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ it.setObjectMap( key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY, value = it.getObjectMap&lt;Boolean&gt;(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .plus(userWalletId.stringValue to true), ) }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ it.toMutableMap().apply { this[id] = value } }</ID>
<ID>MultilineLambdaItParameter:DefaultWalletsRepository.kt$DefaultWalletsRepository${ val added = it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY].orEmpty() it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue }</ID>
<ID>MultilineLambdaItParameter:TangemHotWalletSigner.kt$TangemHotWalletSigner${ Timber.e(it) return if (it is TangemSdkError) { CompletionResult.Failure(it) } else { CompletionResult.Failure(TangemSdkError.ExceptionError(it)) } }</ID>
<ID>MultilineLambdaItParameter:WalletIdBodyConverter.kt$WalletIdBodyConverter${ CardInfoBody( cardId = it.key, cardPublicKey = it.value, ) }</ID>
<ID>NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, count, deadline, boot)</ID>
<ID>NamedArguments:DefaultHotWalletAccessCodeAttemptsRepository.kt$DefaultHotWalletAccessCodeAttemptsRepository$toState(id, it.attempts, it.deadline, it.bootCount)</ID>
<ID>NoNameShadowing:DefaultWalletsRepository.kt$DefaultWalletsRepository$error</ID>
<ID>NoNameShadowing:DefaultWalletsRepository.kt$DefaultWalletsRepository${ UserWalletId(it.key) }</ID>
<ID>SuspendFunSwallowedCancellation:DefaultHotWalletAccessor.kt$DefaultHotWalletAccessor$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:TangemHotWalletSigner.kt$TangemHotWalletSigner$runCatching</ID>
<ID>UnnecessaryLet:MissedDerivationsFinder.kt$MissedDerivationsFinder$let(::findByNetworks)</ID>

View file

@ -4,15 +4,17 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
import com.tangem.data.wallets.converters.WalletIdBodyConverter
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.fold
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
import com.tangem.datasource.api.tangemTech.models.WalletIdBody.WalletType
import com.tangem.datasource.api.tangemTech.models.WalletBody
import com.tangem.datasource.api.tangemTech.models.WalletType
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -62,25 +64,25 @@ internal class DefaultWalletsRepository(
}
override suspend fun useBiometricAuthentication(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrNull(
val shouldUseBiometricAuth = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
)
if (useBiometricAuthentication != null) {
return useBiometricAuthentication
if (shouldUseBiometricAuth != null) {
return shouldUseBiometricAuth
}
val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull(
val isLegacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SAVE_USER_WALLETS_KEY,
)
if (legacySaveWalletsInTheApp != null) {
if (isLegacySaveWalletsInTheApp != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
value = legacySaveWalletsInTheApp,
value = isLegacySaveWalletsInTheApp,
)
return legacySaveWalletsInTheApp
return isLegacySaveWalletsInTheApp
} else {
// Default value for new users
setUseBiometricAuthentication(false)
@ -93,25 +95,25 @@ internal class DefaultWalletsRepository(
}
override suspend fun requireAccessCode(): Boolean {
val requireAccessCode = appPreferencesStore.getSyncOrNull(
val isRequireAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
)
if (requireAccessCode != null) {
return requireAccessCode
if (isRequireAccessCode != null) {
return isRequireAccessCode
}
val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull(
val isLegacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull(
key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY,
)
if (legacyShouldSaveAccessCode != null) {
if (isLegacyShouldSaveAccessCode != null) {
// Migrate legacy setting to new one
appPreferencesStore.store(
key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY,
value = legacyShouldSaveAccessCode.not(),
value = isLegacyShouldSaveAccessCode.not(),
)
return legacyShouldSaveAccessCode.not()
return isLegacyShouldSaveAccessCode.not()
} else {
// Default value for new users
setRequireAccessCode(true)
@ -130,10 +132,10 @@ internal class DefaultWalletsRepository(
}
override suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) {
appPreferencesStore.editData {
val added = it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY].orEmpty()
appPreferencesStore.editData { mutablePreferences ->
val added = mutablePreferences[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY].orEmpty()
it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue
mutablePreferences[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue
}
}
@ -141,8 +143,8 @@ internal class DefaultWalletsRepository(
return channelFlow {
launch {
seedPhraseNotificationVisibilityStore.get()
.map {
it.getOrDefault(
.map { map ->
map.getOrDefault(
key = userWalletId,
defaultValue = SeedPhraseNotificationsStatus.NOT_NEEDED,
)
@ -169,8 +171,8 @@ internal class DefaultWalletsRepository(
tangemTechApi.getSeedPhraseNotificationStatus(walletId = userWalletId.stringValue).getOrThrow()
}.fold(
onSuccess = { it.status },
onFailure = {
if (it is HttpException && it.code == HttpException.Code.NOT_FOUND) {
onFailure = { throwable ->
if (throwable is HttpException && throwable.code == HttpException.Code.NOT_FOUND) {
Status.NOTIFIED
} else {
Status.NOT_NEEDED
@ -245,27 +247,12 @@ internal class DefaultWalletsRepository(
updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED)
}
override suspend fun markWallet2WasCreated(userWalletId: UserWalletId) {
runCatching(dispatchers.io) {
tangemTechApi.markUserWallerWasCreated(
body = MarkUserWalletWasCreatedBody(userWalletId = userWalletId.stringValue),
)
}
}
override suspend fun createWallet(userWalletId: UserWalletId) {
withContext(dispatchers.io) {
runCatching(dispatchers.io) {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
tangemTechApi.createWallet(
body = WalletIdBody(
walletId = userWalletId.stringValue,
name = userWallet.name,
walletType = when (userWallet) {
is UserWallet.Cold -> WalletType.COLD
is UserWallet.Hot -> WalletType.HOT
},
),
body = WalletIdBodyConverter.convert(userWallet = userWallet),
)
}
}
@ -293,8 +280,8 @@ internal class DefaultWalletsRepository(
}
private suspend fun updateNotificationVisibility(id: UserWalletId, value: SeedPhraseNotificationsStatus) {
return seedPhraseNotificationVisibilityStore.update {
it.toMutableMap().apply {
return seedPhraseNotificationVisibilityStore.update { map ->
map.toMutableMap().apply {
this[id] = value
}
}
@ -306,23 +293,23 @@ internal class DefaultWalletsRepository(
override fun nftEnabledStatuses(): Flow<Map<UserWalletId, Boolean>> = appPreferencesStore
.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
.map { it.mapKeys { UserWalletId(it.key) } }
.map { map -> map.mapKeys { UserWalletId(it.key) } }
override suspend fun enableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
it.setObjectMap(
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY,
value = it.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
value = mutablePreferences.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
.plus(userWalletId.stringValue to true),
)
}
}
override suspend fun disableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
it.setObjectMap(
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY,
value = it.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
value = mutablePreferences.getObjectMap<Boolean>(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY)
.plus(userWalletId.stringValue to false),
)
}
@ -338,10 +325,10 @@ internal class DefaultWalletsRepository(
.firstOrNull() == true
override suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) {
appPreferencesStore.editData {
it.setObjectMap(
appPreferencesStore.editData { mutablePreferences ->
mutablePreferences.setObjectMap(
key = PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY,
value = it.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY)
value = mutablePreferences.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY)
.plus(userWalletId.stringValue to isEnabled),
)
}
@ -358,9 +345,11 @@ internal class DefaultWalletsRepository(
}
override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncOrNull(key = UserWalletId(walletId))
tangemTechApi.updateWallet(
walletId = walletId,
body = WalletBody(name = walletName),
body = WalletBody(name = walletName, type = WalletType.from(userWallet)),
).getOrThrow()
}
@ -419,18 +408,20 @@ internal class DefaultWalletsRepository(
promoCode = promoCode,
address = bitcoinAddress,
),
).fold({
return@fold it.status.right()
}, { error ->
val error = when (error) {
is HttpException -> when (error.code) {
HttpException.Code.NOT_FOUND -> ActivatePromoCodeError.InvalidPromoCode
HttpException.Code.CONFLICT -> ActivatePromoCodeError.PromocodeAlreadyUsed
).fold(
onSuccess = { it.status.right() },
onError = { apiResponseError ->
val error = when (apiResponseError) {
is HttpException -> when (apiResponseError.code) {
HttpException.Code.NOT_FOUND -> ActivatePromoCodeError.InvalidPromoCode
HttpException.Code.CONFLICT -> ActivatePromoCodeError.PromocodeAlreadyUsed
else -> ActivatePromoCodeError.ActivationFailed
}
else -> ActivatePromoCodeError.ActivationFailed
}
else -> ActivatePromoCodeError.ActivationFailed
}
return@fold error.left()
})
error.left()
},
)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.data.wallets.converters
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.domain.models.wallet.UserWallet
internal object WalletIdBodyConverter {
fun convert(userWallet: UserWallet, publicKeys: Map<String, String>): WalletIdBody {
return WalletIdBody(
walletId = userWallet.walletId.stringValue,
name = userWallet.name,
cards = publicKeys.map {
CardInfoBody(
cardId = it.key,
cardPublicKey = it.value,
)
},
)
}
}

View file

@ -1,80 +0,0 @@
package com.tangem.data.wallets.converters
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.mockk
import org.junit.Test
class WalletIdBodyConverterTest {
@Test
fun `GIVEN user wallet with cards WHEN convert THEN should return correct WalletIdBody`() {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
val walletName = "Test Wallet"
val userWallet = UserWallet.Cold(
walletId = walletId,
name = walletName,
cardsInWallet = setOf("card1", "card2"),
isMultiCurrency = true,
hasBackupError = false,
scanResponse = mockk(),
)
val publicKeys = mapOf(
"card1" to "public_key_1",
"card2" to "public_key_2",
)
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = listOf(
CardInfoBody(
cardId = "card1",
cardPublicKey = "public_key_1",
),
CardInfoBody(
cardId = "card2",
cardPublicKey = "public_key_2",
),
),
),
)
}
@Test
fun `GIVEN user wallet without cards WHEN convert THEN should return WalletIdBody with empty cards list`() {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
val walletName = "Test Wallet"
val userWallet = UserWallet.Cold(
walletId = walletId,
name = walletName,
cardsInWallet = emptySet(),
isMultiCurrency = true,
hasBackupError = false,
scanResponse = mockk(),
)
val publicKeys = emptyMap<String, String>()
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = emptyList(),
),
)
}
}