Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-28 16:59:16 +04:00
parent 1c8b3ff14b
commit 2d6ead6d92
4 changed files with 131 additions and 3 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.domain.account.status.usecase
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.merge
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
@ -9,10 +10,12 @@ import arrow.core.raise.ensure
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.utils.AccountNameIndexer
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWalletId
/**
@ -45,8 +48,7 @@ class RecoverCryptoPortfolioUseCase(
val recoveredAccount = archivedAccount.recover()
val updatedAccountList = (accountList + recoveredAccount)
.getOrElse { raise(Error.AccountListRequirementsNotMet(cause = it)) }
val updatedAccountList = add(accountList, recoveredAccount)
saveAccounts(updatedAccountList)
@ -97,6 +99,25 @@ class RecoverCryptoPortfolioUseCase(
)
}
private fun Raise<Error>.add(accountList: AccountList, recoveredAccount: Account.CryptoPortfolio): AccountList {
val maybeAccountList = accountList + recoveredAccount
return maybeAccountList.mapLeft {
val recoveredAccountName = recoveredAccount.accountName as? AccountName.Custom
?: raise(Error.DataOperationFailed(message = "Recovered account should have a custom name"))
val indexedName = AccountNameIndexer.transform(from = recoveredAccountName.value)
.let(AccountName::invoke).getOrNull()
?: raise(Error.DataOperationFailed(message = "Failed to generate indexed account name"))
val renamedAccount = recoveredAccount.copy(accountName = indexedName)
// recursively try to add the account with the new name
add(accountList = accountList, recoveredAccount = renamedAccount)
}
.merge()
}
private suspend fun refreshBalances(accountId: AccountId): Either<Error, Unit> = either {
val currencies = catch(
block = { crudRepository.getAccountSync(accountId = accountId) },

View file

@ -0,0 +1,63 @@
package com.tangem.domain.account.status.utils
import com.tangem.domain.models.account.AccountName
/**
* Utility object for indexing account names to ensure uniqueness.
*
* @see [iOS](https://github.com/tangem-developments/tangem-app-ios/blob/32c56f1a566ecec00364ffeaf6b3c9929419ad15/Tangem/Domain/Accounts/Archiving/Utils/UnarchivedCryptoAccountNameIndexer.swift#L11)
*
[REDACTED_AUTHOR]
*/
internal object AccountNameIndexer {
private const val INITIAL_INDEX = 1
private const val INDEX_PREFIX = "("
private const val INDEX_SUFFIX = ")"
/**
* Transforms the given account name by appending or incrementing an index suffix to ensure uniqueness.
*
* @param from the original account name
*
* @return the transformed account name with an updated index suffix
*/
fun transform(from: String): String {
val (newIndex, currentIndex) = extractIndices(from)
val accountNameSuffix = makeStringFromIndex(newIndex)
val accountNamePrefixLength = (AccountName.MAX_LENGTH - accountNameSuffix.length).coerceAtLeast(0)
var accountNamePrefix = from
if (currentIndex != null) {
val accountNameCurrentSuffix = makeStringFromIndex(currentIndex)
val accountNameCurrentSuffixLength = accountNameCurrentSuffix.length
accountNamePrefix = accountNamePrefix.dropLast(accountNameCurrentSuffixLength)
}
accountNamePrefix = accountNamePrefix.take(accountNamePrefixLength)
return accountNamePrefix + accountNameSuffix
}
private fun extractIndices(string: String): Pair<Int, Int?> {
val pattern = """\${INDEX_PREFIX}(\d+)\${INDEX_SUFFIX}$"""
val regex = Regex(pattern)
val match = regex.find(string)
return if (match != null && match.groupValues.size > 1) {
val currentIndex = match.groupValues[1].toIntOrNull()
if (currentIndex != null) {
Pair(currentIndex + 1, currentIndex)
} else {
Pair(INITIAL_INDEX, null)
}
} else {
Pair(INITIAL_INDEX, null)
}
}
private fun makeStringFromIndex(index: Int): String {
return "${INDEX_PREFIX}$index${INDEX_SUFFIX}"
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.domain.account.status.utils
import com.google.common.truth.Truth
import com.tangem.common.test.utils.ProvideTestModels
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AccountNameIndexerTest {
@ParameterizedTest
@ProvideTestModels
fun transform(model: TestModel) {
// Act
val actual = AccountNameIndexer.transform(model.input)
// Assert
Truth.assertThat(actual).isEqualTo(model.expected)
}
private fun provideTestModels() = listOf(
TestModel(input = "Bitcoin", expected = "Bitcoin(1)"),
TestModel(input = "Bitcoin(5)", expected = "Bitcoin(6)"),
TestModel(input = "Ethereum(123)", expected = "Ethereum(124)"),
TestModel(input = "Account(0)", expected = "Account(1)"),
TestModel(input = "Bitcoin(old)new", expected = "Bitcoin(old)new(1)"),
TestModel(input = "Bitcoin(10)new", expected = "Bitcoin(10)new(1)"),
TestModel(input = "", expected = "(1)"),
TestModel(input = "(42)", expected = "(43)"),
TestModel(input = "Account(999999)", expected = "Account(1000000)"),
TestModel(input = "Wallet(9)", expected = "Wallet(10)"),
TestModel(input = "My Account (5)", expected = "My Account (6)"),
TestModel(input = "Bitcoin💰(2)", expected = "Bitcoin💰(3)"),
TestModel(input = "Bitcoin(abc)", expected = "Bitcoin(abc)(1)"),
TestModel(input = "Bitcoin()", expected = "Bitcoin()(1)"),
TestModel(input = "Bitcoin(", expected = "Bitcoin((1)"),
TestModel(input = "Bitcoin)", expected = "Bitcoin)(1)"),
TestModel(input = "EthereumEthereumEthereum(999)", expected = "EthereumEthere(1000)"),
TestModel(input = "a".repeat(100), expected = "a".repeat(17) + "(1)"),
)
data class TestModel(val input: String, val expected: String)
}

View file

@ -74,7 +74,8 @@ sealed interface AccountName {
companion object {
private const val MAX_LENGTH = 20
/** Maximum allowed length for an account name */
const val MAX_LENGTH = 20
/**
* Factory method to create an [AccountName] instance.