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

@ -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

@ -0,0 +1,171 @@
package com.tangem.data.networks.utils
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 timber.log.Timber
/** Factory for creating [NetworkStatus] */
object NetworkStatusFactory {
/**
* Create [NetworkStatus]
*
* @param network network
* @param updatingResult result of updating wallet manager
* @param addedCurrencies added currencies
*/
fun create(
network: Network,
updatingResult: UpdateWalletManagerResult,
addedCurrencies: Set<CryptoCurrency>,
): NetworkStatus {
return NetworkStatus(
network = network,
value = when (updatingResult) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
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>,
): Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus> {
return currencies.associate { currency ->
val amount = when (currency) {
is CryptoCurrency.Coin -> {
amounts.singleOrNull { it is CryptoCurrencyAmount.Coin }
}
is CryptoCurrency.Token -> {
amounts.firstOrNull { amount ->
amount is CryptoCurrencyAmount.Token &&
currency.id.rawCurrencyId == amount.tokenId &&
currency.contractAddress.equals(amount.tokenContractAddress, ignoreCase = true)
}
}
}
if (amount == null) {
Timber.w("Unable to find amount for cryptocurrency: $currency")
currency.id to CryptoCurrencyAmountStatus.NotFound
} else {
currency.id to CryptoCurrencyAmountStatus.Loaded(amount.value)
}
}
}
private fun formatTransactions(
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<TxHistoryItem>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
.asSequence()
.map { currency ->
val currencyTransactions = when (currency) {
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Coin
}
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Token &&
transaction.tokenContractAddress.equals(currency.contractAddress, ignoreCase = true)
}
}
currency.id to createCurrentTransactions(currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(transactions: Set<CryptoCurrencyTransaction>): Set<TxHistoryItem> {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set<Address>): NetworkAddress {
val defaultAddress = availableAddresses
.firstOrNull { it.value == selectedAddress }
?.let(::mapToDomainAddress)
require(defaultAddress != null && defaultAddress.value.isNotBlank()) {
"Selected address must not be null"
}
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(
defaultAddress = defaultAddress,
availableAddresses = availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress),
)
} else {
NetworkAddress.Single(defaultAddress)
}
}
private fun mapToDomainAddress(address: Address): NetworkAddress.Address {
val type = when (address.type) {
Address.Type.Primary -> NetworkAddress.Address.Type.Primary
Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
}
if (address.value.isBlank()) {
Timber.w("Address value is blank")
}
return NetworkAddress.Address(address.value, type)
}
}

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),
)
}
}
}