Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-12 17:44:40 +04:00
parent 4e69a47c32
commit c470e8ef27
8 changed files with 429 additions and 98 deletions

View file

@ -102,7 +102,13 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default
fun createToken(blockchain: Blockchain): CryptoCurrency {
return factory.createToken(
sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8),
sdkToken = Token(
name = "NEVER-MIND",
symbol = "NEVER-MIND",
contractAddress = "NEVER-MIND",
decimals = 8,
id = "NEVER-MIND",
),
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,

View file

@ -10,36 +10,52 @@ android {
}
dependencies {
implementation(projects.libs.blockchainSdk)
// region Project - Core
implementation(projects.core.datasource)
implementation(projects.core.utils)
// endregion
// region Project - Data
implementation(projects.data.common)
implementation(projects.data.tokens)
// endregion
// region Project - Domain
implementation(projects.domain.core)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.networks)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
// endregion
// region Project - Libs
implementation(projects.libs.blockchainSdk)
// endregion
// region Tangem libraries
implementation(tangemDeps.blockchain)
// endregion
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Other libraries
implementation(deps.androidx.datastore)
implementation(deps.moshi)
implementation(deps.timber)
// endregion
implementation(tangemDeps.blockchain)
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(tangemDeps.card.core)
testImplementation(projects.common.test)
testImplementation(tangemDeps.card.core)
// endregion
}

View file

@ -3,7 +3,7 @@ package com.tangem.data.networks.single
import arrow.core.Either
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.data.networks.utils.NetworkStatusFactory
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
@ -31,8 +31,6 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : SingleNetworkStatusFetcher {
private val networkStatusFactory = NetworkStatusFactory()
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
val networkCurrencies = when (params) {
is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies
@ -56,10 +54,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
)
}
val status = networkStatusFactory.createNetworkStatus(
val status = NetworkStatusFactory.create(
network = params.network,
result = result,
currencies = networkCurrencies.toSet(),
updatingResult = result,
addedCurrencies = networkCurrencies.toSet(),
)
val statusValue = status.value

View file

@ -1,4 +1,4 @@
package com.tangem.data.tokens.utils
package com.tangem.data.networks.utils
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.*
@ -9,39 +9,79 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
class NetworkStatusFactory {
/** Factory for creating [NetworkStatus] */
object NetworkStatusFactory {
fun createNetworkStatus(
/**
* Create [NetworkStatus]
*
* @param network network
* @param updatingResult result of updating wallet manager
* @param addedCurrencies added currencies
*/
fun create(
network: Network,
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency>,
updatingResult: UpdateWalletManagerResult,
addedCurrencies: Set<CryptoCurrency>,
): NetworkStatus {
return NetworkStatus(
network = network,
value = when (result) {
value = when (updatingResult) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable(
address = getNetworkAddressOrNull(result.selectedAddress, result.addresses),
)
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
source = StatusSource.ACTUAL,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.selectedAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
transactions = result.currentTransactions,
currencies = currencies,
),
source = StatusSource.ACTUAL,
)
is UpdateWalletManagerResult.Unreachable -> createUnreachableStatus(result = updatingResult)
is UpdateWalletManagerResult.NoAccount -> createNoAccount(result = updatingResult)
is UpdateWalletManagerResult.Verified -> {
createVerifiedStatus(result = updatingResult, addedCurrencies = addedCurrencies)
}
},
)
}
private fun createUnreachableStatus(result: UpdateWalletManagerResult.Unreachable): NetworkStatus.Unreachable {
return NetworkStatus.Unreachable(
address = getNetworkAddressOrNull(
selectedAddress = result.selectedAddress,
availableAddresses = result.addresses,
),
)
}
private fun createNoAccount(result: UpdateWalletManagerResult.NoAccount): NetworkStatus.NoAccount {
return NetworkStatus.NoAccount(
address = getNetworkAddress(
selectedAddress = result.selectedAddress,
availableAddresses = result.addresses,
),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
source = StatusSource.ACTUAL,
)
}
private fun createVerifiedStatus(
result: UpdateWalletManagerResult.Verified,
addedCurrencies: Set<CryptoCurrency>,
): NetworkStatus.Verified {
return NetworkStatus.Verified(
address = getNetworkAddress(
selectedAddress = result.selectedAddress,
availableAddresses = result.addresses,
),
amounts = formatAmounts(amounts = result.currenciesAmounts, currencies = addedCurrencies),
pendingTransactions = formatTransactions(
transactions = result.currentTransactions,
currencies = addedCurrencies,
),
source = StatusSource.ACTUAL,
)
}
private fun getNetworkAddressOrNull(selectedAddress: String?, availableAddresses: Set<Address>?): NetworkAddress? {
if (selectedAddress.isNullOrBlank() || availableAddresses == null) return null
return getNetworkAddress(selectedAddress, availableAddresses)
}
private fun formatAmounts(
amounts: Set<CryptoCurrencyAmount>,
currencies: Set<CryptoCurrency>,
@ -97,23 +137,20 @@ class NetworkStatusFactory {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddressOrNull(selectedAddress: String?, availableAddresses: Set<Address>?): NetworkAddress? {
if (selectedAddress == null || availableAddresses == null) {
return null
}
return getNetworkAddress(selectedAddress, availableAddresses)
}
private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set<Address>): NetworkAddress {
val defaultAddress = availableAddresses
.firstOrNull { it.value == selectedAddress }
?.let(::mapToDomainAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
require(defaultAddress != null && defaultAddress.value.isNotBlank()) {
"Selected address must not be null"
}
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress))
NetworkAddress.Selectable(
defaultAddress = defaultAddress,
availableAddresses = availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress),
)
} else {
NetworkAddress.Single(defaultAddress)
}

View file

@ -0,0 +1,274 @@
package com.tangem.data.networks.utils
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@RunWith(Parameterized::class)
internal class NetworkStatusFactoryTest(private val model: Model) {
@Test
fun test() {
val actual = runCatching {
NetworkStatusFactory.create(
network = model.network,
updatingResult = model.result,
addedCurrencies = model.currencies,
)
}
actual
.onSuccess {
Truth.assertThat(actual).isEqualTo(model.expected)
}
.onFailure {
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(it::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(it.message)
}
}
data class Model(
val network: Network,
val result: UpdateWalletManagerResult,
val currencies: Set<CryptoCurrency>,
val expected: Result<NetworkStatus>,
)
private companion object {
val selectedAddressThrowable = IllegalArgumentException("Selected address must not be null")
val currencies = with(MockCryptoCurrencyFactory()) { setOf(ethereum, createToken(Blockchain.Ethereum)) }
val txHistoryItem = TxHistoryItem(
txHash = "erroribus",
timestampInMillis = 2771,
isOutgoing = false,
destinationType = TxHistoryItem.DestinationType.Single(
addressType = TxHistoryItem.AddressType.User("0x1"),
),
sourceType = TxHistoryItem.SourceType.Single("0x2"),
interactionAddressType = null,
status = TxHistoryItem.TransactionStatus.Confirmed,
type = TxHistoryItem.TransactionType.Transfer,
amount = BigDecimal.ONE,
)
@JvmStatic
@Parameterized.Parameters
fun data(): Collection<Model> = listOf(
// region MissedDerivation
createSuccess(
result = UpdateWalletManagerResult.MissedDerivation,
status = NetworkStatus.MissedDerivation,
),
// endregion
// region Unreachable
createSuccess(
result = UpdateWalletManagerResult.Unreachable(selectedAddress = null, addresses = null),
status = NetworkStatus.Unreachable(address = null),
),
createSuccess(
result = UpdateWalletManagerResult.Unreachable(
selectedAddress = "",
addresses = emptySet(),
),
status = NetworkStatus.Unreachable(address = null),
),
createSuccess(
result = UpdateWalletManagerResult.Unreachable(
selectedAddress = "",
addresses = setOf(Address(value = "", type = Address.Type.Primary)),
),
status = NetworkStatus.Unreachable(address = null),
),
createSuccess(
result = UpdateWalletManagerResult.Unreachable(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
),
status = NetworkStatus.Unreachable(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
),
),
createFailure(
result = UpdateWalletManagerResult.Unreachable(
selectedAddress = "0x1",
addresses = emptySet(),
),
throwable = selectedAddressThrowable,
),
// endregion
// region NoAccount
createFailure(
result = UpdateWalletManagerResult.NoAccount(
selectedAddress = "",
addresses = emptySet(),
amountToCreateAccount = BigDecimal.ZERO,
errorMessage = "",
),
throwable = selectedAddressThrowable,
),
createFailure(
result = UpdateWalletManagerResult.NoAccount(
selectedAddress = "",
addresses = setOf(Address(value = "", type = Address.Type.Primary)),
amountToCreateAccount = BigDecimal.ZERO,
errorMessage = "",
),
throwable = selectedAddressThrowable,
),
createFailure(
result = UpdateWalletManagerResult.NoAccount(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)),
amountToCreateAccount = BigDecimal.ZERO,
errorMessage = "",
),
throwable = selectedAddressThrowable,
),
createSuccess(
result = UpdateWalletManagerResult.NoAccount(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
amountToCreateAccount = BigDecimal.ZERO,
errorMessage = "",
),
status = NetworkStatus.NoAccount(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
amountToCreateAccount = BigDecimal.ZERO,
errorMessage = "",
source = StatusSource.ACTUAL,
),
),
// endregion
// region Verified
createFailure(
result = UpdateWalletManagerResult.Verified(
selectedAddress = "",
addresses = emptySet(),
currenciesAmounts = emptySet(),
currentTransactions = emptySet(),
),
throwable = selectedAddressThrowable,
),
createFailure(
result = UpdateWalletManagerResult.Verified(
selectedAddress = "",
addresses = setOf(Address(value = "", type = Address.Type.Primary)),
currenciesAmounts = emptySet(),
currentTransactions = emptySet(),
),
throwable = selectedAddressThrowable,
),
createFailure(
result = UpdateWalletManagerResult.Verified(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)),
currenciesAmounts = emptySet(),
currentTransactions = emptySet(),
),
throwable = selectedAddressThrowable,
),
createFailure(
result = UpdateWalletManagerResult.Verified(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)),
currenciesAmounts = emptySet(),
currentTransactions = emptySet(),
),
throwable = selectedAddressThrowable,
),
createSuccess(
result = UpdateWalletManagerResult.Verified(
selectedAddress = "0x1",
addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)),
currenciesAmounts = setOf(
CryptoCurrencyAmount.Coin(value = BigDecimal.ONE),
),
currentTransactions = setOf(
CryptoCurrencyTransaction.Coin(txHistoryItem),
),
),
currencies = currencies,
status = NetworkStatus.Verified(
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "0x1",
type = NetworkAddress.Address.Type.Primary,
),
),
amounts = mapOf(
currencies.first().id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.ONE),
currencies.last().id to CryptoCurrencyAmountStatus.NotFound,
),
pendingTransactions = mapOf(
currencies.first().id to setOf(txHistoryItem),
currencies.last().id to setOf(),
),
source = StatusSource.ACTUAL,
),
),
// endregion
)
fun createSuccess(
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency> = emptySet(),
status: NetworkStatus.Value,
): Model {
val network = MockCryptoCurrencyFactory().ethereum.network
return Model(
network = network,
result = result,
currencies = currencies,
expected = Result.success(
value = NetworkStatus(network = network, value = status),
),
)
}
fun createFailure(
result: UpdateWalletManagerResult,
currencies: Set<CryptoCurrency> = emptySet(),
throwable: Throwable,
): Model {
val network = MockCryptoCurrencyFactory().ethereum.network
return Model(
network = network,
result = result,
currencies = currencies,
expected = Result.failure(throwable),
)
}
}
}

View file

@ -14,44 +14,52 @@ android {
dependencies {
/** Project - Domain */
// region Project - Data
implementation(projects.data.common)
implementation(projects.data.networks)
// endregion
// region Project - Domain
implementation(projects.domain.core)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.staking)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
// endregion
/** Project - Data */
// region Project - Utils
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Utils */
implementation(projects.core.utils)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
// endregion
/** Tangem SDKs */
// region Tangem SDKs
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
// endregion
/** AndroidX */
// region AndroidX
implementation(deps.androidx.datastore)
implementation(deps.androidx.paging.runtime)
// endregion
/** DI */
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
/** Other */
// region Other
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.moshi.kotlin)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.retrofit) // For HttpException
implementation(deps.androidx.paging.runtime)
implementation(deps.timber)
ksp(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
}

View file

@ -7,7 +7,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.data.networks.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -43,7 +43,6 @@ internal class DefaultNetworksRepository(
) : NetworksRepository {
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val networkStatusFactory = NetworkStatusFactory()
override fun getNetworkStatusesUpdates(
userWalletId: UserWalletId,
@ -168,10 +167,10 @@ internal class DefaultNetworksRepository(
invalidateCacheKeyIfNeeded(userWalletId, network, result)
}
val networkStatus = networkStatusFactory.createNetworkStatus(
val networkStatus = NetworkStatusFactory.create(
network = network,
result = result,
currencies = networkCurrencies.toSet(),
updatingResult = result,
addedCurrencies = networkCurrencies.toSet(),
)
networksStatusesStore.store(userWalletId, networkStatus)
@ -191,10 +190,10 @@ internal class DefaultNetworksRepository(
invalidateCacheKeyIfNeeded(userWalletId, network, result)
}
val networkStatus = networkStatusFactory.createNetworkStatus(
val networkStatus = NetworkStatusFactory.create(
network = network,
result = result,
currencies = currencies.toSet(),
updatingResult = result,
addedCurrencies = currencies.toSet(),
)
networksStatusesStore.store(userWalletId, networkStatus)

View file

@ -5,23 +5,18 @@ import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal
/**
* Represents the status of a specific blockchain network.
* Represents the status of a specific blockchain network
*
* @property network The network for which the status is provided.
* @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network.
* @property network the network for which the status is provided
* @property value the specific status value, represented as a sealed class to encapsulate the various possible
* states of the network
*/
data class NetworkStatus(
val network: Network,
val value: Value,
) {
data class NetworkStatus(val network: Network, val value: Value) {
/**
* Represents the various possible statuses of a network.
*
* This sealed class includes different states like unreachable, missed derivation, verified, and no account.
*/
/** Represents the various possible statuses of a network */
sealed class Value {
/** Status source */
abstract val source: StatusSource
fun copySealed(source: StatusSource): Value {
@ -36,30 +31,28 @@ data class NetworkStatus(
}
/**
* Represents the state where the network is unreachable.
* Represents the state where the network is unreachable
*
* @property address Network addresses.
* @property address network address
*/
data class Unreachable(val address: NetworkAddress?) : Value() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents the state where a derivation has been missed.
*/
/** Represents the state where a derivation has been missed */
data object MissedDerivation : Value() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies
* and whether there are transactions in progress.
* and whether there are transactions in progress
*
* @property address Network addresses.
* @property amounts A map containing the amounts associated with different cryptocurrencies within the network.
* @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies
* @property source source of data
* within the network.
* @property address network address
* @property amounts a map containing the amounts associated with different cryptocurrencies within the
* network
* @property pendingTransactions a map containing pending transactions associated with different cryptocurrencies
* @property source source of data
*/
data class Verified(
val address: NetworkAddress,
@ -69,12 +62,12 @@ data class NetworkStatus(
) : Value()
/**
* Represents the state where there is no account, and an amount is required to create one.
* Represents the state where there is no account, and an amount is required to create one
*
* @property address Network addresses.
* @property amountToCreateAccount The amount required to create an account within the network.
* @property errorMessage error message
* @property source source of data
* @property address network address
* @property amountToCreateAccount the amount required to create an account within the network
* @property errorMessage error message
* @property source source of data
*/
data class NoAccount(
val address: NetworkAddress,