Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 13:57:57 +03:00
commit f3dfb262c2
527 changed files with 12898 additions and 3301 deletions

View file

@ -80,7 +80,7 @@ class ArchiveCryptoPortfolioUseCase(
val hasNotReferralToken = statuses.none { status ->
val currency = status.currency
currency.network.backendId == referralToken.networkId &&
currency.network.rawId == referralToken.networkId &&
(currency as? CryptoCurrency.Token)?.contractAddress == referralToken.contractAddress &&
status.value.networkAddress?.availableAddresses?.any { it.value == address } == true
}

View file

@ -168,7 +168,7 @@ class ManageCryptoCurrenciesUseCase(
val foundToken = accountStatus.tokenList.flattenCurrencies()
.mapNotNull { it.currency as? CryptoCurrency.Token }
.firstOrNull { token ->
token.network.backendId == networkId &&
token.network.rawId == networkId &&
!token.isCustom &&
token.contractAddress.equals(contractAddress, true)
}
@ -361,7 +361,7 @@ class ManageCryptoCurrenciesUseCase(
launch {
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
ExpressAsset.ID(
networkId = currency.network.backendId,
networkId = currency.network.rawId,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
}
@ -388,19 +388,19 @@ class ManageCryptoCurrenciesUseCase(
) {
constructor(network: Network) : this(
networkId = network.backendId,
networkId = network.rawId,
derivationPath = network.derivationPath,
contractAddress = null,
)
constructor(currency: CryptoCurrency) : this(
networkId = currency.network.backendId,
networkId = currency.network.rawId,
derivationPath = currency.network.derivationPath,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
constructor(status: CryptoCurrencyStatus) : this(
networkId = status.currency.network.backendId,
networkId = status.currency.network.rawId,
derivationPath = status.currency.network.derivationPath,
contractAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress,
)

View file

@ -1,6 +1,6 @@
package com.tangem.domain.account.status.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
@ -180,7 +180,7 @@ internal object AccountCryptoCurrencyStatusFinder {
contractAddress: String?,
): AccountCryptoCurrency? {
return accountList.getExpectedAccounts(
rawNetworkId = networkId.rawId.value,
rawNetworkId = networkId.rawId,
derivationPath = derivationPath,
)
.asSequence()
@ -220,7 +220,7 @@ internal object AccountCryptoCurrencyStatusFinder {
internal fun AccountStatusList.getExpectedAccountStatuses(networkId: Network.ID): List<AccountStatus> {
val possibleAccountIndex = getAccountIndexOrNull(
rawNetworkId = networkId.rawId.value,
rawNetworkId = networkId.rawId,
derivationPath = networkId.derivationPath,
)
@ -239,7 +239,7 @@ internal object AccountCryptoCurrencyStatusFinder {
}
internal fun AccountStatusList.getExpectedAccountStatuses(networks: List<Network>): List<AccountStatus> {
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.rawId, it.derivationPath) }
val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.id.rawId, it.derivationPath) }
if (possibleAccountIndexes.isEmpty()) return accountStatuses
@ -256,16 +256,14 @@ internal object AccountCryptoCurrencyStatusFinder {
// region AccountList helpers
internal fun AccountList.getExpectedAccounts(network: Network?): List<Account> {
return getExpectedAccounts(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath)
return getExpectedAccounts(rawNetworkId = network?.id?.rawId, derivationPath = network?.derivationPath)
}
private fun AccountList.getExpectedAccounts(
rawNetworkId: String?,
rawNetworkId: Network.RawID?,
derivationPath: Network.DerivationPath?,
): List<Account> {
val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)
return when (possibleAccountIndex) {
return when (val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)) {
null -> accounts
DerivationIndex.Main.value -> listOf(mainAccount)
// currency only in the account with specific derivation index or in the main account
@ -283,10 +281,10 @@ internal object AccountCryptoCurrencyStatusFinder {
// region Common helpers
private fun getAccountIndexOrNull(rawNetworkId: String?, derivationPath: Network.DerivationPath?): Int? {
private fun getAccountIndexOrNull(rawNetworkId: Network.RawID?, derivationPath: Network.DerivationPath?): Int? {
if (rawNetworkId == null || derivationPath == null) return null
val blockchain = Blockchain.fromId(id = rawNetworkId)
val blockchain = rawNetworkId.toBlockchain()
val recognizer = AccountNodeRecognizer(blockchain)
return recognizer.recognize(derivationPath)?.toInt()

View file

@ -143,7 +143,7 @@ class ArchiveCryptoPortfolioUseCaseTest {
val defaultAddress = "0xABC"
val cryptoCurrency = mockk<CryptoCurrency.Token> {
every { this@mockk.network.backendId } returns token.networkId
every { this@mockk.network.rawId } returns token.networkId
every { this@mockk.contractAddress } returns token.contractAddress!!
}

View file

@ -5,7 +5,7 @@ plugins {
}
android {
namespace = "com.tangem.domain.tokensync"
namespace = "com.tangem.domain.assetsdiscovery"
}
dependencies {
@ -14,6 +14,9 @@ dependencies {
implementation(projects.domain.account.status)
implementation(projects.core.utils)
implementation(projects.libs.blockchainSdk)
implementation(tangemDeps.blockchain)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.assetsdiscovery
import com.tangem.blockchain.assetsdiscovery.AssetsDiscoveryService
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
interface AssetsDiscoveryFacade {
suspend fun getAssetsDiscoveryService(userWalletId: UserWalletId, network: Network): AssetsDiscoveryServiceInfo?
data class AssetsDiscoveryServiceInfo(
val address: String,
val service: AssetsDiscoveryService,
)
}

View file

@ -1,13 +1,13 @@
package com.tangem.domain.tokensync.model
package com.tangem.domain.assetsdiscovery.model
sealed class TokenSyncProgress {
sealed class AssetsDiscoveryProgress {
data object Idle : TokenSyncProgress()
data object Idle : AssetsDiscoveryProgress()
data class InProgress(
val completedNetworks: Int,
val totalNetworks: Int,
) : TokenSyncProgress() {
) : AssetsDiscoveryProgress() {
val progressPercent: Int
get() = if (totalNetworks > 0) {
completedNetworks * 100 / totalNetworks
@ -16,5 +16,5 @@ sealed class TokenSyncProgress {
}
}
data object Completed : TokenSyncProgress()
data object Completed : AssetsDiscoveryProgress()
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.assetsdiscovery.repository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import kotlinx.coroutines.flow.Flow
interface AssetsDiscoveryRepository {
suspend fun runDiscovery(userWalletId: UserWalletId)
suspend fun completeDiscovery(userWalletId: UserWalletId)
suspend fun getPendingDiscoveryWalletIds(): List<UserWalletId>
fun observeDiscoveryProgress(userWalletId: UserWalletId): Flow<AssetsDiscoveryProgress>
fun acknowledgeCompletion(userWalletId: UserWalletId)
suspend fun clearPendingFlag(userWalletId: UserWalletId)
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.assetsdiscovery.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
class AcknowledgeAssetsDiscoveryCompletionUseCase(
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
) {
operator fun invoke(userWalletId: UserWalletId) {
assetsDiscoveryRepository.acknowledgeCompletion(userWalletId)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.assetsdiscovery.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
import kotlinx.coroutines.flow.Flow
class ObserveAssetsDiscoveryUseCase(
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<AssetsDiscoveryProgress> {
return assetsDiscoveryRepository.observeDiscoveryProgress(userWalletId)
}
}

View file

@ -1,18 +1,18 @@
package com.tangem.domain.tokensync.usecase
package com.tangem.domain.assetsdiscovery.usecase
import arrow.core.Either
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokensync.repository.TokenSyncRepository
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
class StartTokenSyncUseCase(
private val tokenSyncRepository: TokenSyncRepository,
class StartAssetsDiscoveryUseCase(
private val assetsDiscoveryRepository: AssetsDiscoveryRepository,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val appCoroutineScope: AppCoroutineScope,
) {
@ -23,9 +23,9 @@ class StartTokenSyncUseCase(
activeSyncJobs[userWalletId]?.cancel()
activeSyncJobs[userWalletId] = appCoroutineScope.launch {
try {
tokenSyncRepository.runSync(userWalletId)
assetsDiscoveryRepository.runDiscovery(userWalletId)
applyDiscoveredTokens(userWalletId)
tokenSyncRepository.completeSync(userWalletId)
assetsDiscoveryRepository.completeDiscovery(userWalletId)
} catch (e: Exception) {
TangemLogger.e("Token sync failed for wallet: $userWalletId", e)
} finally {
@ -36,18 +36,18 @@ class StartTokenSyncUseCase(
suspend fun cancel(userWalletId: UserWalletId): Either<Throwable, Unit> = Either.catch {
activeSyncJobs.remove(userWalletId)?.cancel()
tokenSyncRepository.clearPendingFlag(userWalletId)
tokenSyncRepository.clearDiscoveredTokens(userWalletId)
assetsDiscoveryRepository.clearPendingFlag(userWalletId)
assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId)
}
fun applyPendingSyncs() {
fun applyPendingAssetsDiscovery() {
appCoroutineScope.launch {
try {
val pendingIds = tokenSyncRepository.getPendingSyncWalletIds()
val pendingIds = assetsDiscoveryRepository.getPendingDiscoveryWalletIds()
for (walletId in pendingIds) {
val isApplied = applyDiscoveredTokens(walletId)
if (isApplied) {
tokenSyncRepository.clearPendingFlag(walletId)
assetsDiscoveryRepository.clearPendingFlag(walletId)
}
}
} catch (e: Exception) {
@ -57,7 +57,7 @@ class StartTokenSyncUseCase(
}
private suspend fun applyDiscoveredTokens(userWalletId: UserWalletId): Boolean {
val currencies = tokenSyncRepository.getDiscoveredCurrencies(userWalletId)
val currencies = assetsDiscoveryRepository.getDiscoveredCurrencies(userWalletId)
if (currencies.isEmpty()) return true
@ -67,7 +67,7 @@ class StartTokenSyncUseCase(
add = currencies,
).fold(
ifRight = {
tokenSyncRepository.clearDiscoveredTokens(userWalletId)
assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId)
true
},
ifLeft = { error ->

View file

@ -13,8 +13,12 @@ dependencies {
api(projects.domain.dynamicAddresses.models)
implementation(projects.domain.models)
implementation(projects.domain.walletManager)
implementation(projects.domain.wallets)
implementation(projects.libs.blockchainSdk)
implementation(tangemDeps.blockchain) {
exclude(module = "joda-time")
}
implementation(tangemDeps.card.core)
}

View file

@ -11,7 +11,7 @@ class DisableDynamicAddressesUseCase(
/**
* Returns true when consolidation is required before disabling (non-base balances exist),
* or false when DA was disabled immediately.
* or false when dynamic addresses was disabled immediately.
*/
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> =
Either.catch {

View file

@ -0,0 +1,54 @@
package com.tangem.domain.dynamicaddresses
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
/**
* List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode).
* Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred).
*
* Dynamic addresses is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses.
* Only the default derivation style per blockchain is supported.
*/
object DynamicAddressesSupportedBlockchains {
private const val BIP44_PURPOSE = 44L
private const val BIP84_PURPOSE = 84L
private val supported = setOf(
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.BitcoinCash,
Blockchain.BitcoinCashTestnet,
Blockchain.Litecoin,
Blockchain.Dogecoin,
Blockchain.Dash,
Blockchain.Ravencoin,
Blockchain.RavencoinTestnet,
)
private val supportedNetworkIds = supported.map { it.toNetworkId() }.toSet()
/**
* Allowed BIP purpose nodes per network ID.
* BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH).
*/
private val allowedPurposeByNetworkId: Map<String, Long> = buildMap {
put(Blockchain.Bitcoin.toNetworkId(), BIP84_PURPOSE)
put(Blockchain.BitcoinTestnet.toNetworkId(), BIP84_PURPOSE)
put(Blockchain.Litecoin.toNetworkId(), BIP84_PURPOSE)
put(Blockchain.BitcoinCash.toNetworkId(), BIP44_PURPOSE)
put(Blockchain.BitcoinCashTestnet.toNetworkId(), BIP44_PURPOSE)
put(Blockchain.Dogecoin.toNetworkId(), BIP44_PURPOSE)
put(Blockchain.Dash.toNetworkId(), BIP44_PURPOSE)
put(Blockchain.Ravencoin.toNetworkId(), BIP44_PURPOSE)
put(Blockchain.RavencoinTestnet.toNetworkId(), BIP44_PURPOSE)
}
fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported
fun isSupportedByNetworkId(networkId: String): Boolean = networkId in supportedNetworkIds
/** Returns the allowed BIP purpose node for the given network, or null if not supported */
fun getAllowedPurpose(networkId: String): Long? = allowedPurposeByNetworkId[networkId]
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.dynamicaddresses
sealed class EnableDynamicAddressesError {
data object ConflictingCustomTokens : EnableDynamicAddressesError()
data class ServiceError(val cause: Throwable) : EnableDynamicAddressesError()
}

View file

@ -1,6 +1,8 @@
package com.tangem.domain.dynamicaddresses
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
@ -9,8 +11,19 @@ class EnableDynamicAddressesUseCase(
private val dynamicAddressesRepository: DynamicAddressesRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either<Throwable, Unit> =
Either.catch {
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
xpub: String,
): Either<EnableDynamicAddressesError, Unit> {
return try {
if (dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)) {
return EnableDynamicAddressesError.ConflictingCustomTokens.left()
}
dynamicAddressesRepository.enable(userWalletId, network, xpub)
Unit.right()
} catch (e: Throwable) {
EnableDynamicAddressesError.ServiceError(e).left()
}
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.dynamicaddresses
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.calculateRipemd160
import com.tangem.common.extensions.calculateSha256
import com.tangem.crypto.NetworkType
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
/**
* Returns the XPUB string if account-level keys are already derived (no card scan needed),
* or null if keys are not available.
*/
class GetDerivedXpubUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val derivationsRepository: DerivationsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String? {
val blockchain = network.toBlockchain()
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return null
if (!blockchain.isBip44DerivationStyleXPUB()) return null
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return null
val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return null
if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return null
val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey)
val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey)
val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT))
val parentPath = DerivationPath(accountPath.nodes.dropLast(1))
val childExtKey = existingKeys[accountPath] ?: return null
val parentExtKey = existingKeys[parentPath] ?: return null
val parentFingerprint = parentExtKey.publicKey
.calculateSha256().calculateRipemd160()
.take(PARENT_FINGERPRINT_SIZE).toByteArray()
val net = if (blockchain.isTestnet()) NetworkType.Testnet else NetworkType.Mainnet
return ExtendedPublicKey(
publicKey = childExtKey.publicKey,
chainCode = childExtKey.chainCode,
depth = accountPath.nodes.size,
parentFingerprint = parentFingerprint,
childNumber = accountPath.nodes.last().index,
).serialize(net)
}
private companion object {
const val ACCOUNT_PATH_DROP_COUNT = 2
const val PARENT_FINGERPRINT_SIZE = 4
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.dynamicaddresses
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletmanager.WalletManagersFacade
/**
* Checks if XPUB generation is supported for the given wallet and network (hardware capability check).
*/
class IsXpubSupportedUseCase(
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean {
val blockchain = network.toBlockchain()
if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false
return walletManager.wallet.publicKey.derivationType?.hdKey != null
}
}

View file

@ -19,4 +19,7 @@ interface DynamicAddressesRepository {
suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String?
suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean
/** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */
suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean
}

View file

@ -56,7 +56,7 @@ class GetEarnNetworksUseCase(
accountLists
.filter { it.userWalletId in unlockedWalletsId }
.flatMap(AccountList::flattenCurrencies)
.mapTo(HashSet()) { it.network.backendId }
.mapTo(HashSet()) { it.network.rawId }
}
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.models.network.Network
/**
* Information about blockchain's operation error
*
* @property errorMessage message about error
* @property blockchainId blockchain id
* @property derivationPath derivation path
* @property networkId network ID
* @property destinationAddress destination address
* @property tokenSymbol token symbol or null, if it isn't operation with token
* @property amount amount
@ -13,8 +14,7 @@ package com.tangem.domain.feedback.models
*/
data class BlockchainErrorInfo(
val errorMessage: String,
val blockchainId: String,
val derivationPath: String?,
val networkId: Network.ID?,
val destinationAddress: String,
val tokenSymbol: String?,
val amount: String,

View file

@ -1,6 +1,7 @@
package com.tangem.domain.feedback.repository
import com.tangem.domain.feedback.models.*
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import java.io.File
@ -17,11 +18,7 @@ interface FeedbackRepository {
fun getPhoneInfo(): PhoneInfo
suspend fun getBlockchainInfo(
userWalletId: UserWalletId,
blockchainId: String,
derivationPath: String?,
): BlockchainInfo?
suspend fun getBlockchainInfo(userWalletId: UserWalletId, networkId: Network.ID): BlockchainInfo?
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)

View file

@ -94,11 +94,10 @@ class EmailMessageBodyResolver(
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
networkId = networkId,
)
}
@ -159,11 +158,10 @@ class EmailMessageBodyResolver(
val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
networkId = networkId,
)
}
@ -181,11 +179,10 @@ class EmailMessageBodyResolver(
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
networkId = networkId,
)
}
@ -210,11 +207,10 @@ class EmailMessageBodyResolver(
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
val blockchainInfo = blockchainError?.networkId?.let { networkId ->
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
networkId = networkId,
)
}

View file

@ -1,8 +0,0 @@
package com.tangem.domain.redux
import org.rekotlin.Action
sealed interface LegacyAction : Action {
data object PrepareDetailsScreen : LegacyAction
}

View file

@ -18,11 +18,11 @@ data class TokenReceiveConfig(
@Serializable
data class ReceiveAddressModel(
val nameService: NameService,
val displayType: DisplayType,
val value: String,
) {
enum class NameService {
Default, Legacy, Ens
enum class DisplayType {
Default, Legacy, Ens, Dynamic,
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.domain.models.account
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.serialization.Serializable
@Serializable
@ConsistentCopyVisibility
data class CardDisplayName private constructor(val value: String) {
@Serializable
sealed interface Error {
@Serializable
data object Empty : Error
@Serializable
data object ExceedsMaxLength : Error
@Serializable
data object InvalidCharacters : Error
}
companion object {
const val MAX_LENGTH = 20
private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$")
operator fun invoke(name: String): Either<Error, CardDisplayName> = either {
val trimmed = name.trim()
ensure(trimmed.isNotEmpty()) { Error.Empty }
ensure(trimmed.length <= MAX_LENGTH) { Error.ExceedsMaxLength }
ensure(allowedPattern.matches(trimmed)) { Error.InvalidCharacters }
CardDisplayName(trimmed)
}
}
}

View file

@ -2,9 +2,13 @@ package com.tangem.domain.models.account
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
import java.math.BigDecimal
/**
* Represents the various states a payment account can have, encapsulating different information based on the state.
@ -104,7 +108,30 @@ sealed class PaymentAccountStatusValue {
val isPinSet: Boolean,
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
) : PaymentAccountStatusValue()
val cryptoCurrency: CryptoCurrency.Token,
val displayName: CardDisplayName?,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded(
amount = cryptoBalance.balance,
fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = cryptoBalance.depositAddress,
),
),
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
),
)
}
/**
* Represents a state where the payment account is successfully loaded with complete information.
@ -130,7 +157,30 @@ sealed class PaymentAccountStatusValue {
val isPinSet: Boolean,
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
) : PaymentAccountStatusValue()
val cryptoCurrency: CryptoCurrency.Token,
val displayName: CardDisplayName?,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = CryptoCurrencyStatus.Loaded(
amount = cryptoBalance.balance,
fiatAmount = fiatBalance.availableBalance,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
type = NetworkAddress.Address.Type.Primary,
value = cryptoBalance.depositAddress,
),
),
sources = CryptoCurrencyStatus.Sources(),
pendingTransactions = emptySet(),
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
),
)
}
/** Represents an error state for the payment account status. */
@Serializable

View file

@ -3,7 +3,7 @@ package com.tangem.domain.models.currency
import java.math.BigDecimal
fun CryptoCurrency.Token.yieldSupplyKey(): String {
return "${network.backendId}_$contractAddress"
return "${network.rawId}_$contractAddress"
}
fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean {

View file

@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable
* (e.g., ERC20, BEP20).
*
* @property id the unique identifier of the network
* @property backendId the name of this network in the Tangem backend
* @property name the human-readable name of the network, such as "Ethereum" or "Bitcoin"
* @property currencySymbol the symbol of the currency associated with the network
* @property derivationPath the path used to derive keys for this network
@ -25,7 +24,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class Network(
val id: ID,
val backendId: String,
val name: String,
val currencySymbol: String,
val derivationPath: DerivationPath,
@ -49,7 +47,7 @@ data class Network(
/**
* Represents a unique identifier for a blockchain network
*
* @property rawId raw network ID
* @property rawId raw network ID (backend id)
* @property derivationPath derivation path
*/
@Serializable

View file

@ -1,8 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus
*
[REDACTED_AUTHOR]
*/
abstract class MultiNetworkStatusSupplier(
open class MultiNetworkStatusSupplier(
override val factory: MultiNetworkStatusProducer.Factory,
override val keyCreator: (MultiNetworkStatusProducer.Params) -> String,
) : FlowCachingSupplier<MultiNetworkStatusProducer, MultiNetworkStatusProducer.Params, Set<NetworkStatus>>()

View file

@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus
*
[REDACTED_AUTHOR]
*/
abstract class SingleNetworkStatusSupplier(
open class SingleNetworkStatusSupplier(
override val factory: SingleNetworkStatusProducer.Factory,
override val keyCreator: (SingleNetworkStatusProducer.Params) -> String,
) : FlowCachingSupplier<SingleNetworkStatusProducer, SingleNetworkStatusProducer.Params, NetworkStatus>()

View file

@ -3,5 +3,5 @@ package com.tangem.domain.search.model
data class SearchResult(
val textHints: List<SearchTextHint>,
val recentTokens: List<RecentSearchToken>,
val userAssets: List<UserAssetSearchEntry>,
val userAssets: List<UserAssetSearchItem>,
)

View file

@ -2,6 +2,7 @@ package com.tangem.domain.search.model
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
@ -10,5 +11,6 @@ data class UserAssetSearchEntry(
val userWalletName: String,
val accountId: AccountId,
val accountName: AccountName,
val accountIcon: CryptoPortfolioIcon,
val currencyStatus: CryptoCurrencyStatus,
)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.search.model
sealed interface UserAssetSearchItem {
data class Single(val entry: UserAssetSearchEntry) : UserAssetSearchItem
data class Grouped(
val tokenName: String,
val tokenSymbol: String,
val tokenIconUrl: String?,
val entries: List<UserAssetSearchEntry>,
) : UserAssetSearchItem
}

View file

@ -9,10 +9,12 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.search.model.SearchResult
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.search.model.UserAssetSearchItem
import com.tangem.domain.search.repository.SearchRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import java.math.BigDecimal
/**
* Primary search use case that produces [SearchResult] based on the current query.
@ -70,9 +72,12 @@ class GetSearchResultsUseCase(
if (unlockedWallets.isEmpty()) return@combine emptyList()
statusLists
val entries = statusLists
.filter { it.userWalletId in unlockedWallets }
.flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) }
val shouldGroup = needsGrouping(unlockedWallets.values, statusLists)
groupAndSort(entries, shouldGroup)
}.map { userAssets ->
SearchResult(
textHints = emptyList(),
@ -82,6 +87,44 @@ class GetSearchResultsUseCase(
}
}
private fun needsGrouping(unlockedWallets: Collection<UserWallet>, statusLists: List<AccountStatusList>): Boolean {
if (unlockedWallets.size > 1) return true
val totalAccounts = statusLists
.filter { sl -> unlockedWallets.any { it.walletId == sl.userWalletId } }
.sumOf { it.accountStatuses.filterCryptoPortfolio().size }
return totalAccounts > 1
}
private fun groupAndSort(entries: List<UserAssetSearchEntry>, shouldGroup: Boolean): List<UserAssetSearchItem> {
if (!shouldGroup) {
return entries
.map { UserAssetSearchItem.Single(it) }
.sortedByDescending { it.entry.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
}
val grouped = entries.groupBy { entry ->
val rawId = entry.currencyStatus.currency.id.rawCurrencyId
rawId?.value ?: "${entry.currencyStatus.currency.name}|${entry.currencyStatus.currency.symbol}"
}
return grouped.map { (_, groupEntries) ->
val assetInfo = groupEntries.first()
UserAssetSearchItem.Grouped(
tokenName = assetInfo.currencyStatus.currency.name,
tokenSymbol = assetInfo.currencyStatus.currency.symbol,
tokenIconUrl = assetInfo.currencyStatus.currency.iconUrl,
entries = groupEntries,
)
}.sortedByDescending { item ->
when (item) {
is UserAssetSearchItem.Grouped ->
item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
}
}
}
private fun extractMatchingAssets(
statusList: AccountStatusList,
wallets: Map<UserWalletId, UserWallet>,
@ -103,6 +146,7 @@ class GetSearchResultsUseCase(
userWalletName = wallet.name,
accountId = accountStatus.accountId,
accountName = accountStatus.account.accountName,
accountIcon = accountStatus.account.icon,
currencyStatus = currencyStatus,
)
}

View file

@ -1,13 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -&gt; raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -&gt; Unit.right() } return@either }</ID>
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending &amp;&amp; action.amount &lt; it.amount &amp;&amp; it.type == BalanceType.STAKED &amp;&amp; it.validatorAddress == action.validatorAddress }</ID>
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase(
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
)
.getOrElse {
when (it) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
.getOrElse { error ->
when (error) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error"))
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
}

View file

@ -4,10 +4,11 @@ import arrow.core.Either
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
class GetConstructedStakingTransactionUseCase(
private val stakeKitRepository: StakeKitRepository,
@ -15,12 +16,17 @@ class GetConstructedStakingTransactionUseCase(
) {
suspend operator fun invoke(
networkId: String,
networkId: Network.RawID,
fee: Fee,
amount: Amount,
transactionId: String,
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId)
stakeKitRepository.constructTransaction(
networkId = networkId,
fee = fee,
amount = amount,
transactionId = transactionId,
)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase(
type = BalanceType.STAKED,
amount = action.amount,
rawCurrencyId = null,
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "",
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(),
date = null,
pendingActions = emptyList(),
pendingActionsConstraints = emptyList(),
@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase(
}
private fun findPartialUnstake(balances: MutableList<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
val index = balances.indexOfFirst {
!it.isPending && action.amount < it.amount &&
it.type == BalanceType.STAKED &&
it.validatorAddress == action.validatorAddress
val index = balances.indexOfFirst { balance ->
!balance.isPending && action.amount < balance.amount &&
balance.type == BalanceType.STAKED &&
balance.validatorAddress == action.validatorAddress
}
return index to action.amount
}

View file

@ -2,23 +2,27 @@ package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
/**
* Factory class for creating instances of [StakingID]
*
* @property walletManagersFacade wallet manager facade
* @property walletManagersFacade wallet manager facade
* @property stakingFeatureToggles staking feature toggles
*
[REDACTED_AUTHOR]
*/
class StakingIdFactory(
private val walletManagersFacade: WalletManagersFacade,
private val stakingFeatureToggles: StakingFeatureToggles,
) {
/**
@ -72,6 +76,8 @@ class StakingIdFactory(
ensureNotNull(integrationId) { Error.UnsupportedCurrency }
ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency }
val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() }
ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) }

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType
sealed class StakingAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(
category = "Staking",
event = event,

View file

@ -147,7 +147,7 @@ sealed interface StakingIntegrationID {
* @return a [StakingIntegrationID] if supported, or `null` if not supported.
*/
fun create(currencyId: CryptoCurrency.ID): StakingIntegrationID? {
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
val blockchain = currencyId.toBlockchain()
return if (currencyId.contractAddress.isNullOrBlank()) {
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class MultiStakingBalanceSupplier(
open class MultiStakingBalanceSupplier(
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()

View file

@ -5,11 +5,11 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.models.staking.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
@ -55,7 +55,7 @@ interface StakeKitRepository {
suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate
suspend fun constructTransaction(
networkId: String,
networkId: Network.RawID,
fee: Fee,
amount: Amount,
transactionId: String,

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class SingleStakingBalanceSupplier(
open class SingleStakingBalanceSupplier(
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()

View file

@ -1,5 +1,8 @@
package com.tangem.domain.staking.toggles
import com.tangem.domain.staking.model.StakingIntegrationID
interface StakingFeatureToggles {
val isEthStakingEnabled: Boolean
fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean
}

View file

@ -5,17 +5,16 @@ import arrow.core.left
import arrow.core.right
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.test.core.ProvideTestModels
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
@ -30,11 +29,16 @@ import org.junit.jupiter.params.ParameterizedTest
internal class StakingIdFactoryTest {
private val walletManagersFacade: WalletManagersFacade = mockk()
private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade)
private val stakingFeatureToggles: StakingFeatureToggles = mockk()
private val factory = StakingIdFactory(
walletManagersFacade = walletManagersFacade,
stakingFeatureToggles = stakingFeatureToggles,
)
@BeforeEach
fun resetMocks() {
clearMocks(walletManagersFacade)
clearMocks(walletManagersFacade, stakingFeatureToggles)
every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true
}
@Nested
@ -66,6 +70,33 @@ internal class StakingIdFactoryTest {
}
}
@Test
fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest {
// Arrange
val userWalletId = UserWalletId(stringValue = "011")
val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON)
every {
stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton)
} returns false
// Act
val actual = factory.create(
userWalletId = userWalletId,
currencyId = currency.id,
network = currency.network,
)
// Assert
val expected = StakingIdFactory.Error.UnsupportedCurrency
Truth.assertThat(actual.leftOrNull()).isEqualTo(expected)
coVerify(inverse = true) {
walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any())
}
}
@Test
fun `create returns UnableToGetAddress if address is null`() = runTest {
// Arrange
@ -154,7 +185,7 @@ internal class StakingIdFactoryTest {
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(
value = "token⟨ETH⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
value = "token⟨ethereum⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0",
),
expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon),
),
@ -168,6 +199,6 @@ internal class StakingIdFactoryTest {
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: Either<StakingIdFactory.Error, StakingID>)
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}")
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}")
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.staking
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegrationID
@ -144,11 +145,11 @@ class StakingIntegrationIDTest {
expected = StakingIntegrationID.P2PEthPool,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩polygon-ecosystem-token⚓1234567890"),
expected = StakingIntegrationID.StakeKit.EthereumToken.Polygon,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"),
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨solana⟩solana⚓1234567890"),
expected = null,
),
)
@ -157,6 +158,6 @@ class StakingIntegrationIDTest {
data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingIntegrationID?)
private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID {
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}")
return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}")
}
}

View file

@ -190,7 +190,7 @@ class WalletBalanceFetcher internal constructor(
private suspend fun fetchExpressAssets(userWallet: UserWallet, currencies: Set<CryptoCurrency>) {
val assetIds = currencies.mapTo(hashSetOf()) { currency ->
ExpressAsset.ID(
networkId = currency.network.backendId,
networkId = currency.network.rawId,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
}

View file

@ -18,7 +18,6 @@ internal object MockNetworks {
name = "Network One",
isTestnet = false,
standardType = Network.StandardType.ERC20,
backendId = "network1",
currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
hasFiatFeeRate = true,
@ -32,7 +31,6 @@ internal object MockNetworks {
name = "Network Two",
isTestnet = false,
standardType = Network.StandardType.ERC20,
backendId = "network1",
currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
hasFiatFeeRate = true,
@ -46,7 +44,6 @@ internal object MockNetworks {
name = "Network Three",
isTestnet = false,
standardType = Network.StandardType.ERC20,
backendId = "network1",
currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
hasFiatFeeRate = true,

View file

@ -1,25 +0,0 @@
package com.tangem.domain.tokensync.repository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokensync.model.TokenSyncProgress
import kotlinx.coroutines.flow.Flow
interface TokenSyncRepository {
suspend fun runSync(userWalletId: UserWalletId)
suspend fun completeSync(userWalletId: UserWalletId)
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
fun acknowledgeCompletion(userWalletId: UserWalletId)
suspend fun clearPendingFlag(userWalletId: UserWalletId)
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
}

View file

@ -1,13 +0,0 @@
package com.tangem.domain.tokensync.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokensync.repository.TokenSyncRepository
class AcknowledgeTokenSyncCompletionUseCase(
private val tokenSyncRepository: TokenSyncRepository,
) {
operator fun invoke(userWalletId: UserWalletId) {
tokenSyncRepository.acknowledgeCompletion(userWalletId)
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.domain.tokensync.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokensync.model.TokenSyncProgress
import com.tangem.domain.tokensync.repository.TokenSyncRepository
import kotlinx.coroutines.flow.Flow
class ObserveTokenSyncUseCase(
private val tokenSyncRepository: TokenSyncRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<TokenSyncProgress> {
return tokenSyncRepository.observeSyncProgress(userWalletId)
}
}

View file

@ -27,6 +27,8 @@ dependencies {
implementation(projects.libs.crypto)
implementation(projects.domain.account.status)
implementation(projects.domain.dynamicAddresses)
implementation(projects.domain.dynamicAddresses.models)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.walletManager)

View file

@ -40,8 +40,8 @@ class GetEthSpecificFeeUseCase(
?: (walletManager as? EthereumWalletManager)?.getGasPriceValue()
?: error("not supported for ${cryptoCurrency.network}")
val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.backendId)
?: error("unknown networkId ${cryptoCurrency.network.backendId}")
val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.rawId)
?: error("unknown networkId ${cryptoCurrency.network.rawId}")
val minimalFee = getEthLegacyFee(
gasPrice = gasPriceResult,

View file

@ -1,5 +1,9 @@
package com.tangem.domain.transaction.usecase
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
import com.tangem.domain.models.Asset
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
@ -12,10 +16,15 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.transaction.R
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.firstOrNull
class ReceiveAddressesFactory(
private val getEnsNameUseCase: GetEnsNameUseCase,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase,
private val dynamicAddressesRepository: DynamicAddressesRepository,
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
) {
suspend fun create(
@ -32,27 +41,14 @@ class ReceiveAddressesFactory(
address = addresses.defaultAddress.value,
)
val receiveAddresses = buildList {
ensName?.let { ens ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
),
)
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
),
)
}
val dynamicAddress = getDynamicAddressIfEnabled(userWalletId, cryptoCurrency)
val receiveAddresses = if (dynamicAddress != null) {
buildDynamicAddressList(ensName, dynamicAddress)
} else {
buildStandardAddressList(ensName, addresses)
}
return TokenReceiveConfig(
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
cryptoCurrency = cryptoCurrency,
@ -64,6 +60,52 @@ class ReceiveAddressesFactory(
)
}
private suspend fun getDynamicAddressIfEnabled(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): String? {
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return null
if (cryptoCurrency !is CryptoCurrency.Coin) return null
val status = dynamicAddressesRepository.getStatus(userWalletId, cryptoCurrency.network).firstOrNull()
if (status != DynamicAddressesStatus.ENABLED) return null
return getDynamicReceiveAddressUseCase(userWalletId, cryptoCurrency.network)
.onLeft { TangemLogger.e("Failed to get dynamic receive address: ${it.message}") }
.getOrNull()
}
private fun buildDynamicAddressList(ensName: String?, dynamicAddress: String): List<ReceiveAddressModel> =
buildList {
ensName?.let { ens ->
add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens))
}
add(
ReceiveAddressModel(
displayType = ReceiveAddressModel.DisplayType.Dynamic,
value = dynamicAddress,
),
)
}
private fun buildStandardAddressList(ensName: String?, addresses: NetworkAddress): List<ReceiveAddressModel> =
buildList {
ensName?.let { ens ->
add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens))
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
displayType = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy
},
value = address.value,
),
)
}
}
suspend fun createForNft(
userWalletId: UserWalletId,
addresses: NetworkAddress,
@ -82,7 +124,7 @@ class ReceiveAddressesFactory(
ensName?.let { ens ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
displayType = ReceiveAddressModel.DisplayType.Ens,
value = ens,
),
)
@ -90,9 +132,9 @@ class ReceiveAddressesFactory(
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
displayType = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy
},
value = address.value,
),

View file

@ -1,5 +1,6 @@
package com.tangem.domain.pay
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.serialization.Serializable
@ -11,4 +12,5 @@ data class TangemPayDetailsConfig(
val cardFrozenState: TangemPayCardFrozenState,
val cardNumberEnd: String,
val chainId: Int,
val displayName: CardDisplayName?,
)

View file

@ -5,8 +5,8 @@ import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
@Deprecated("TangemPayCurrencyFactory")
interface TangemPayCryptoCurrencyFactory {
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token>
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.pay.model
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.domain.visa.model.TangemPayCardFrozenState
@ -49,6 +50,7 @@ data class CustomerInfo(
val id: String,
val cardId: String,
val frozenState: TangemPayCardFrozenState,
val displayName: CardDisplayName?,
)
data class CardInfo(

View file

@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
@ -31,4 +32,9 @@ interface TangemPayCardDetailsRepository {
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
suspend fun updateCardDisplayName(
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit>
}

View file

@ -0,0 +1,34 @@
package com.tangem.domain.pay.usecase
import arrow.core.Option
import arrow.core.none
import arrow.core.some
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import kotlinx.coroutines.flow.firstOrNull
class GetPaymentAccountCryptoCurrencyStatusUseCase(
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Option<Pair<Account.Payment, CryptoCurrencyStatus>> {
val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none()
val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus
else -> return none()
}
return if (cryptoCurrencyStatus.currency == cryptoCurrency) {
(accountStatus.account to cryptoCurrencyStatus).some()
} else {
none()
}
}
}

View file

@ -12,7 +12,6 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.blockchain.tokenbalance.models.TokenBalance
import com.tangem.blockchainsdk.models.UpdateWalletManagerResult
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -39,12 +38,14 @@ interface WalletManagersFacade {
* @param userWalletId The ID of the user's wallet.
* @param network The network.
* @param extraTokens Additional tokens.
* @param xpub XPUB string to restore dynamic addresses mode if not yet active.
* @return The result of updating the wallet manager.
*/
suspend fun update(
userWalletId: UserWalletId,
network: Network,
extraTokens: Set<CryptoCurrency.Token>,
xpub: String? = null,
): UpdateWalletManagerResult
/**
@ -282,8 +283,6 @@ interface WalletManagersFacade {
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
suspend fun getTokenBalances(userWalletId: UserWalletId, network: Network): List<TokenBalance>
/**
* If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized]
* value. Otherwise always return true

View file

@ -0,0 +1,6 @@
package com.tangem.domain.wallets.models
enum class WalletSyncResult {
AlreadyExists,
Created,
}

View file

@ -29,6 +29,9 @@ interface DerivationsRepository {
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Map<ByteArrayKey, ExtendedPublicKeysMap>
/** Returns already derived extended public keys for the given [seedKey] */
suspend fun getExistingDerivedKeys(userWalletId: UserWalletId, seedKey: ByteArrayKey): ExtendedPublicKeysMap
/** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */
suspend fun hasMissedDerivations(
userWalletId: UserWalletId,

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.domain.wallets.models.WalletSyncResult
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
import kotlinx.coroutines.flow.Flow
@ -22,7 +23,7 @@ interface WalletsRepository {
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)
suspend fun createWallet(userWalletId: UserWalletId)
suspend fun createWallet(userWalletId: UserWalletId): WalletSyncResult
fun nftEnabledStatus(userWalletId: UserWalletId): Flow<Boolean>

View file

@ -1,7 +1,6 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.right
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.ByteArrayKey
@ -38,23 +37,37 @@ class GetExtendedPublicKeyForCurrencyUseCase(
error("No derivation found")
}
val seedKey = walletManager.wallet.publicKey.seedKey
val existingKeys = derivationsRepository.getExistingDerivedKeys(
userWalletId = userWalletId,
seedKey = ByteArrayKey(seedKey),
)
var childKey = makeChildKey(
isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(),
extendedPublicKey = hdKey.extendedPublicKey,
derivationPath = hdKey.path,
)
// Fill from already derived keys if available
if (childKey.extendedPublicKey == null) {
existingKeys[childKey.derivationPath]?.let {
childKey = childKey.copy(extendedPublicKey = it)
}
}
val parentPath = childKey.derivationPath.dropLastNodes(1)
var parentKey = Key(
derivationPath = childKey.derivationPath.dropLastNodes(1),
extendedPublicKey = null,
derivationPath = parentPath,
extendedPublicKey = existingKeys[parentPath],
)
val pendingDerivations = getPendingDerivations(childKey, parentKey)
val derivedKeys = deriveKeys(
userWalletId = userWalletId,
seedKey = walletManager.wallet.publicKey.seedKey,
paths = pendingDerivations,
)
val derivedKeys = if (pendingDerivations.isNotEmpty()) {
deriveKeys(userWalletId = userWalletId, seedKey = seedKey, paths = pendingDerivations)
} else {
ExtendedPublicKeysMap(emptyMap())
}
if (childKey.extendedPublicKey == null) {
childKey = childKey.copy(
@ -72,22 +85,6 @@ class GetExtendedPublicKeyForCurrencyUseCase(
}
}
/**
* @return true if xpub generation is supported, false otherwise
*/
suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> = Either.catch {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found for user wallet $userWalletId and network ${network.id}")
val blockchain = network.toBlockchain()
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey
val isSupported = isSecp256k1Blockchain && isHdKey != null
return isSupported.right()
}
private suspend fun deriveKeys(
userWalletId: UserWalletId,
seedKey: ByteArray,

View file

@ -3,9 +3,9 @@ package com.tangem.domain.wallets.usecase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.LinkedHashMap
/**
* Use case for getting list of user wallets
@ -22,9 +22,13 @@ class GetWalletsUseCase(
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListRepository.userWallets.map { requireNotNull(it) }
@Throws(IllegalArgumentException::class)
fun invokeAsMap(): Flow<LinkedHashMap<UserWalletId, UserWallet>> = userWalletsListRepository.userWallets
.map { requireNotNull(it) }
.map { wallets ->
fun invokeAsMap(isOnlyMultiCurrency: Boolean = true): Flow<LinkedHashMap<UserWalletId, UserWallet>> = invoke()
.map { list ->
val wallets = if (isOnlyMultiCurrency) {
list.filter { wallet -> wallet.isMultiCurrency }
} else {
list
}
wallets.associateByTo(
destination = linkedMapOf(),
keySelector = { wallet -> wallet.walletId },

View file

@ -1,6 +1,7 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.models.WalletSyncResult
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
@ -14,8 +15,9 @@ class SyncWalletWithRemoteUseCase(
private val walletsRepository: WalletsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId) {
runSuspendCatching { walletsRepository.createWallet(userWalletId) }
suspend operator fun invoke(userWalletId: UserWalletId): WalletSyncResult {
return runSuspendCatching { walletsRepository.createWallet(userWalletId) }
.onFailure { TangemLogger.e("Error", it) }
.getOrDefault(WalletSyncResult.AlreadyExists)
}
}

View file

@ -16,6 +16,7 @@ dependencies {
/** Core */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.libs.blockchainSdk)
/** Domain */
implementation(projects.domain.account.status)

View file

@ -5,6 +5,7 @@ import arrow.core.Either.Companion.catch
import arrow.core.getOrElse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getCoin
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -67,8 +68,7 @@ class YieldSupplyGetCurrentFeeUseCase(
val tokenValue = rateRatio.multiply(nativeGas.amount.value)
val isEthereum = cryptoCurrencyStatus.currency
.network.id.rawId.value == Blockchain.Ethereum.id
val isEthereum = cryptoCurrencyStatus.currency.network.rawId == Blockchain.Ethereum.toNetworkId()
val isHighFee = if (isEthereum) {
val maxFeePerGas = (feeWithoutGas as? Fee.Ethereum.EIP1559)?.maxFeePerGas ?: 0.toBigInteger()

View file

@ -165,8 +165,7 @@ class YieldSupplyMinAmountUseCaseTest {
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
id = Network.ID(value = "polygon", derivationPath = derivationPath),
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,

View file

@ -7,8 +7,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
import io.mockk.coEvery
import io.mockk.coVerify
@ -324,7 +324,6 @@ class YieldSupplyEnterStatusUseCaseTest {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,

View file

@ -2,7 +2,7 @@ package com.tangem.domain.yield.supply.usecase
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
@ -48,7 +48,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN valid inputs on non-ethereum WHEN invoke THEN returns fee value and not high`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val tokenDecimals = 8
val nativeDecimals = 18
val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals)
@ -100,7 +100,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN ethereum with high gas WHEN invoke THEN returns high fee flag`() = runTest {
val rawNetworkId = Blockchain.Ethereum.id
val rawNetworkId = "ethereum"
val tokenDecimals = 8
val nativeDecimals = 18
val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals)
@ -152,7 +152,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN token fiat rate missing WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = null)
@ -175,7 +175,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN native quotes unavailable WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
@ -204,7 +204,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN empty quotes list WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
@ -233,7 +233,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN native fiat rate non-positive WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00"))
val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18)
@ -274,7 +274,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
@Test
fun `GIVEN token fiat rate non-positive WHEN invoke THEN returns error`() = runTest {
val rawNetworkId = Blockchain.BSC.id
val rawNetworkId = "binance-smart-chain"
val token = createToken(rawNetworkId = rawNetworkId, decimals = 8)
val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal.ZERO) // non-positive
@ -299,7 +299,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,
@ -331,7 +330,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
backendId = rawNetworkId,
name = rawNetworkId,
currencySymbol = rawNetworkId.take(3).uppercase(),
derivationPath = derivationPath,

View file

@ -61,8 +61,7 @@ class YieldSupplyGetDustMinAmountUseCaseTest {
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
id = Network.ID(value = "polygon", derivationPath = derivationPath),
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,

View file

@ -238,8 +238,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest {
@Test
fun `GIVEN polygon USDT0 Loaded status WHEN invoke THEN emit formatted balances`() = runTest {
val network = Network(
id = Network.ID(Network.RawID("POLYGON"), Network.DerivationPath.Card("m/44'/60'/0'/0/0")),
backendId = "polygon-pos",
id = Network.ID(value = "polygon-pos", derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0")),
name = "Polygon",
currencySymbol = "POL",
derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0"),
@ -365,8 +364,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest {
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
id = Network.ID(value = "polygon", derivationPath = derivationPath),
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,

View file

@ -279,7 +279,6 @@ class YieldSupplyPendingTrackerTest {
val derivationPath = Network.DerivationPath.None
val network = Network(
id = Network.ID(value = networkId, derivationPath = derivationPath),
backendId = networkId,
name = networkId,
currencySymbol = networkId.take(3).uppercase(),
derivationPath = derivationPath,