Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-19 11:44:37 +03:00
commit 1146554992
1461 changed files with 62266 additions and 17096 deletions

View file

@ -21,6 +21,7 @@ dependencies {
// region Project - Data
implementation(projects.data.common)
implementation(projects.data.dynamicAddresses)
// endregion
// region Project - Domain

View file

@ -1,15 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:CommonNetworkStatusFetcher.kt$CommonNetworkStatusFetcher${ Timber.e("Failed to fetch network status for $userWalletId [${network.rawId}]: $it") networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) }</ID>
<ID>MultilineLambdaItParameter:DefaultMultiNetworkStatusFetcher.kt$DefaultMultiNetworkStatusFetcher${ networksStatusesStore.setSourceAsOnlyCache( userWalletId = params.userWalletId, networks = params.networks, ) raise(it) }</ID>
<ID>MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return emptyList() }</ID>
<ID>MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return@withContext }</ID>
<ID>MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null CurrencyAmount( id = currencyIdConverter.convertBack(value = it.key), amount = amount.value, ) }</ID>
<ID>MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val currencyId = currencyIdConverter.convert(value = it.id) val amount = NetworkStatus.Amount.Loaded(value = it.amount) currencyId to amount }</ID>
<ID>MultilineLambdaItParameter:NetworkStatusSupplierModule.kt$NetworkStatusSupplierModule.&lt;no name provided&gt;${ "single_network_status_${it.userWalletId.stringValue}_${it.network.rawId}_" + it.network.derivationPath.value }</ID>
<ID>MultilineLambdaItParameter:NetworkYieldSupplyStatusConverter.kt$NetworkYieldSupplyStatusConverter${ val id = currencyIdConverter.convert(value = it.id) val status = YieldSupplyStatus( isActive = it.isActive, isInitialized = it.isInitialized, isAllowedToSpend = it.isAllowedToSpend, effectiveProtocolBalance = it.effectiveProtocolBalance, ) id to status }</ID>
<ID>SuspendFunSwallowedCancellation:DefaultNetworksRepository.kt$DefaultNetworksRepository$runCatching</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -12,33 +12,33 @@ private typealias AmountsDomainModel = Map<CryptoCurrency.ID, NetworkStatus.Amou
/**
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
*
* @param rawNetworkId the raw network ID associated with the currency
* @param blockchainId the blockchain ID associated with the network (e.g. `Blockchain.id`)
* @param derivationPath the derivation path used for the network
*
[REDACTED_AUTHOR]
*/
internal class NetworkAmountsConverter(
rawNetworkId: String,
blockchainId: String,
derivationPath: Network.DerivationPath,
) : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath)
override fun convert(value: AmountsDataModel): AmountsDomainModel {
return value.associate {
val currencyId = currencyIdConverter.convert(value = it.id)
val amount = NetworkStatus.Amount.Loaded(value = it.amount)
return value.associate { currencyAmount ->
val currencyId = currencyIdConverter.convert(value = currencyAmount.id)
val amount = NetworkStatus.Amount.Loaded(value = currencyAmount.amount)
currencyId to amount
}
}
override fun convertBack(value: AmountsDomainModel): AmountsDataModel {
return value.mapNotNull {
val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null
return value.mapNotNull { (currencyId, networkAmount) ->
val amount = networkAmount as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null
CurrencyAmount(
id = currencyIdConverter.convertBack(value = it.key),
id = currencyIdConverter.convertBack(value = currencyId),
amount = amount.value,
)
}

View file

@ -1,7 +1,9 @@
package com.tangem.data.networks.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId.Companion.CONTRACT_ADDRESS_DELIMITER
import com.tangem.domain.models.currency.CryptoCurrency
@ -12,16 +14,22 @@ import com.tangem.domain.models.currency.CryptoCurrency.ID.Suffix as CurrencyIdS
/**
* Converts between [CurrencyId] and [CryptoCurrency.ID].
*
* @property rawNetworkId the raw network ID associated with the currency
* @property blockchainId the blockchain ID associated with the currency
* @property derivationPath the derivation path used for the network
*
[REDACTED_AUTHOR]
*/
internal class CurrencyIdConverter(
private val rawNetworkId: String,
internal class NetworkCurrencyIdConverter(
private val blockchainId: String,
private val derivationPath: Network.DerivationPath,
) : TwoWayConverter<CurrencyId, CryptoCurrency.ID> {
// Cache stores blockchainId in legacy format (e.g. "BTC"), but runtime
// CryptoCurrency.ID expects the new network rawId (e.g. "bitcoin") matching
// Network.rawId built from Blockchain.toNetworkId(). Convert once on construction
// so that IDs reconstructed from cache match those built at runtime.
private val networkRawId: String = Blockchain.fromId(blockchainId).toNetworkId()
override fun convert(value: CurrencyId): CryptoCurrency.ID {
val suffixParts = value.value.split(CONTRACT_ADDRESS_DELIMITER)
@ -31,7 +39,7 @@ internal class CurrencyIdConverter(
return if (contractAddress.isNullOrBlank()) {
getCoinId(
coinId = rawId.takeUnless { it.isNullOrBlank() }
?: error("Coin id is null for $rawNetworkId with $derivationPath"),
?: error("Coin id is null for $blockchainId with $derivationPath"),
)
} else {
getTokenId(
@ -43,14 +51,12 @@ internal class CurrencyIdConverter(
override fun convertBack(value: CryptoCurrency.ID): CurrencyId {
return if (value.isCoin) {
CurrencyId.createCoinId(
coinId = Blockchain.fromId(value.rawNetworkId).toCoinId(),
)
CurrencyId.createCoinId(coinId = value.toBlockchain().toCoinId())
} else {
CurrencyId.createTokenId(
rawTokenId = value.rawCurrencyId?.value,
contractAddress = requireNotNull(value.contractAddress) {
"Token contractAddress is null for token id: $this"
"Token contractAddress is null for token id: $value"
},
)
}
@ -82,17 +88,17 @@ internal class CurrencyIdConverter(
return when (derivationPath) {
is Network.DerivationPath.Card -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = rawNetworkId,
rawId = networkRawId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.Custom -> {
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
rawId = rawNetworkId,
rawId = networkRawId,
derivationPath = derivationPath.value,
)
}
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(rawNetworkId)
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(networkRawId)
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.networks.converters
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.network.NetworkStatus
import com.tangem.utils.converter.Converter
@ -15,17 +16,18 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
return when (val status = value.value) {
is NetworkStatus.Verified -> {
val address = NetworkAddressConverter.convertBack(value = status.address)
val blockchainId = value.network.toBlockchain().id
val amountsConverter = NetworkAmountsConverter(
rawNetworkId = value.network.rawId,
blockchainId = blockchainId,
derivationPath = value.network.derivationPath,
)
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
rawNetworkId = value.network.rawId,
blockchainId = blockchainId,
derivationPath = value.network.derivationPath,
)
NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID(value = value.network.rawId),
networkId = NetworkStatusDM.ID(value = blockchainId),
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
selectedAddress = address.selectedAddress,
availableAddresses = address.addresses,
@ -35,9 +37,10 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
}
is NetworkStatus.NoAccount -> {
val address = NetworkAddressConverter.convertBack(value = status.address)
val blockchainId = value.network.toBlockchain().id
NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID(value = value.network.rawId),
networkId = NetworkStatusDM.ID(value = blockchainId),
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
selectedAddress = address.selectedAddress,
availableAddresses = address.addresses,

View file

@ -10,20 +10,20 @@ private typealias YieldSupplyStatusDataModel = List<NetworkStatusDM.YieldSupplyS
private typealias YieldSupplyStatusDomainModel = Map<CryptoCurrency.ID, YieldSupplyStatus?>
internal class NetworkYieldSupplyStatusConverter(
rawNetworkId: String,
blockchainId: String,
derivationPath: Network.DerivationPath,
) : TwoWayConverter<YieldSupplyStatusDataModel, YieldSupplyStatusDomainModel> {
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath)
override fun convert(value: YieldSupplyStatusDataModel): YieldSupplyStatusDomainModel {
return value.associate {
val id = currencyIdConverter.convert(value = it.id)
return value.associate { yieldSupplyStatus ->
val id = currencyIdConverter.convert(value = yieldSupplyStatus.id)
val status = YieldSupplyStatus(
isActive = it.isActive,
isInitialized = it.isInitialized,
isAllowedToSpend = it.isAllowedToSpend,
effectiveProtocolBalance = it.effectiveProtocolBalance,
isActive = yieldSupplyStatus.isActive,
isInitialized = yieldSupplyStatus.isInitialized,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
effectiveProtocolBalance = yieldSupplyStatus.effectiveProtocolBalance,
)
id to status

View file

@ -1,5 +1,7 @@
package com.tangem.data.networks.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.networks.models.SimpleNetworkStatus
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
@ -23,14 +25,15 @@ internal object SimpleNetworkStatusConverter : Converter<NetworkStatusDM, Simple
)
val derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath)
val blockchainId = value.networkId.value
val amountsConverter = NetworkAmountsConverter(
rawNetworkId = value.networkId.value,
blockchainId = blockchainId,
derivationPath = derivationPath,
)
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
rawNetworkId = value.networkId.value,
blockchainId = blockchainId,
derivationPath = derivationPath,
)
@ -56,7 +59,7 @@ internal object SimpleNetworkStatusConverter : Converter<NetworkStatusDM, Simple
return SimpleNetworkStatus(
id = Network.ID(
value = value.networkId.value,
value = Blockchain.fromId(blockchainId).toNetworkId(),
derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath),
),
value = status,

View file

@ -17,21 +17,26 @@ internal object NetworkStatusSupplierModule {
@Provides
@Singleton
fun provideSingleNetworkStatusSupplier(factory: SingleNetworkStatusProducer.Factory): SingleNetworkStatusSupplier {
return object : SingleNetworkStatusSupplier(
return SingleNetworkStatusSupplier(
factory = factory,
keyCreator = {
"single_network_status_${it.userWalletId.stringValue}_${it.network.rawId}_" +
it.network.derivationPath.value
keyCreator = { params ->
listOf(
"single_network_status",
params.userWalletId.stringValue,
params.network.rawId,
params.network.derivationPath.value,
)
.joinToString(separator = "_")
},
) {}
)
}
@Provides
@Singleton
fun provideMultiNetworkStatusSupplier(factory: MultiNetworkStatusProducer.Factory): MultiNetworkStatusSupplier {
return object : MultiNetworkStatusSupplier(
return MultiNetworkStatusSupplier(
factory = factory,
keyCreator = { "multi_networks_statuses_${it.userWalletId.stringValue}" },
) {}
)
}
}

View file

@ -44,6 +44,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
userWalletId: UserWalletId,
network: Network,
networkCurrencies: Set<CryptoCurrency>,
xpub: String? = null,
): Either<Throwable, Unit> {
// Guard: empty networkCurrencies would result in NetworkStatus.Verified(amounts=emptyMap()),
// which overwrites any valid cached status and leaves all currencies in this network as Loading.
@ -60,6 +61,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
extraTokens = networkCurrencies
.filterIsInstance<CryptoCurrency.Token>()
.toSet(),
xpub = xpub,
)
}

View file

@ -2,16 +2,18 @@ package com.tangem.data.networks.multi
import arrow.core.raise.catch
import arrow.core.raise.ensure
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
import com.tangem.data.networks.store.setSourceAsCache
import com.tangem.data.networks.store.setSourceAsOnlyCache
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.domain.core.utils.eitherOn
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@ -27,11 +29,11 @@ import javax.inject.Inject
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
private val networksStatusesStore: NetworksStatusesStore,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
private val dynamicAddressesInitializer: DynamicAddressesInitializer,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiNetworkStatusFetcher {
@ -40,13 +42,21 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
val networksCurrencies = catch(
block = { createNetworksCurrenciesMap(params) },
catch = {
catch = { error ->
networksStatusesStore.setSourceAsOnlyCache(
userWalletId = params.userWalletId,
networks = params.networks,
)
raise(it)
raise(error)
},
)
val xpubByNetwork = catch(
block = { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) },
catch = { error ->
TangemLogger.e("Failed to build XPUBs for restore", error)
emptyMap()
},
)
@ -58,6 +68,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
userWalletId = params.userWalletId,
network = network,
networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
xpub = xpubByNetwork[network],
)
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.withContext
@ -33,13 +34,12 @@ internal class DefaultNetworksRepository(
override suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) {
withContext(dispatchers.default) {
val currencies = runCatching {
val currencies = runSuspendCatching {
cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network)
}.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return@withContext
}
.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return@withContext
}
fetchPendingTransactions(userWalletId = userWalletId, network = network, currencies = currencies)
}
@ -49,34 +49,34 @@ internal class DefaultNetworksRepository(
userWalletId: UserWalletId,
network: Network,
): List<CryptoCurrencyAddress> {
return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) }
.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return emptyList()
}
.map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = getDefaultAddress(userWalletId, network).orEmpty(),
)
}
return runSuspendCatching {
cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network)
}.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return emptyList()
}.map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = getDefaultAddress(userWalletId, network).orEmpty(),
)
}
}
override suspend fun getNetworkAddresses(
userWalletId: UserWalletId,
network: Network.RawID,
): List<CryptoCurrencyAddress> {
return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) }
.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return emptyList()
}
.map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = getDefaultAddress(userWalletId, currency.network).orEmpty(),
)
}
return runSuspendCatching {
cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network)
}.getOrElse { error ->
TangemLogger.e("Unable to create wallet currencies", error)
return emptyList()
}.map { currency ->
CryptoCurrencyAddress(
cryptoCurrency = currency,
address = getDefaultAddress(userWalletId, currency.network).orEmpty(),
)
}
}
override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? {

View file

@ -2,6 +2,7 @@ package com.tangem.data.networks.store
import android.content.Context
import androidx.datastore.core.DataStore
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.networks.converters.NetworkStatusDataModelConverter
import com.tangem.data.networks.converters.SimpleNetworkStatusConverter
import com.tangem.data.networks.models.SimpleNetworkStatus
@ -27,15 +28,15 @@ internal typealias WalletIdWithStatusDM = Map<String, Set<NetworkStatusDM>>
* Default implementation of [NetworksStatusesStore]
*
* @param context context
* @param scope app coroutine scope
* @property runtimeStore runtime store
* @property persistenceDataStore persistence store
* @param dispatchers dispatchers
*/
internal class DefaultNetworksStatusesStore(
context: Context,
scope: AppCoroutineScope,
private val runtimeStore: RuntimeSharedStore<WalletIdWithSimpleStatus>,
private val persistenceDataStore: DataStore<WalletIdWithStatusDM>,
private val scope: AppCoroutineScope,
) : NetworksStatusesStore {
init {
@ -112,7 +113,8 @@ internal class DefaultNetworksStatusesStore(
storedStatuses.toMutableMap().apply {
val updatedValues = this[userWalletId.stringValue].orEmpty().filterNot {
networks.any { network ->
it.networkId.value == network.rawId && it.derivationPath.value == network.derivationPath.value
it.networkId.value == network.toBlockchain().id &&
it.derivationPath.value == network.derivationPath.value
}
}

View file

@ -16,10 +16,14 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkAmountsConverterTest {
private val rawNetworkId = "ETH"
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
// The converter normalizes it to the canonical network rawId ("ethereum") via
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
// built at runtime from Network.rawId.
private val blockchainId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkAmountsConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
private val converter = NetworkAmountsConverter(blockchainId = blockchainId, derivationPath = derivationPath)
@Test
fun convert() {
@ -47,12 +51,12 @@ internal class NetworkAmountsConverterTest {
// Assert
val expected = mapOf(
ID.fromValue("coin⟨ETH$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue("coin⟨ethereum$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.ZERO),
ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.TEN),
)
@ -63,12 +67,12 @@ internal class NetworkAmountsConverterTest {
fun convertBack() {
// Arrange
val value = mapOf(
ID.fromValue("coin⟨ETH$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue("coin⟨ethereum$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.ZERO),
ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
) to Loaded(value = BigDecimal.TEN),
)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.test.core.ProvideTestModels
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
@ -13,12 +14,16 @@ import org.junit.jupiter.params.ParameterizedTest
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CurrencyIdConverterTest {
class NetworkCurrencyIdConverterTest {
private val rawNetworkId = "ETH"
// Legacy SDK format stored in cache (see NetworkStatusDataModelConverter:
// `value.network.toBlockchain().id`). Runtime CryptoCurrency.ID expects the canonical
// network rawId ("ethereum"), so the converter normalizes via Blockchain.fromId(...).toNetworkId().
private val blockchainId = "ETH"
private val canonicalNetworkRawId = "ethereum"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = CurrencyIdConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
private val converter = NetworkCurrencyIdConverter(blockchainId = blockchainId, derivationPath = derivationPath)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ -48,19 +53,19 @@ class CurrencyIdConverterTest {
ConvertModel(
value = CurrencyId.createCoinId("ethereum"),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH$derivationPathHashCode⟩ethereum"),
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum$derivationPathHashCode⟩ethereum"),
),
),
ConvertModel(
value = CurrencyId.createCoinId(""),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
),
),
ConvertModel(
value = CurrencyId.createCoinId(" "),
expected = Result.failure(
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
),
),
// create token id
@ -71,7 +76,7 @@ class CurrencyIdConverterTest {
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
@ -82,7 +87,7 @@ class CurrencyIdConverterTest {
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
@ -93,7 +98,7 @@ class CurrencyIdConverterTest {
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
@ -104,7 +109,7 @@ class CurrencyIdConverterTest {
),
expected = Result.success(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
),
),
@ -114,7 +119,7 @@ class CurrencyIdConverterTest {
contractAddress = "",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH$derivationPathHashCode⟩usdt"),
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum$derivationPathHashCode⟩usdt"),
),
),
ConvertModel(
@ -123,7 +128,7 @@ class CurrencyIdConverterTest {
contractAddress = " ",
),
expected = Result.success(
CryptoCurrency.ID.fromValue(value = "coin⟨ETH$derivationPathHashCode⟩usdt"),
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum$derivationPathHashCode⟩usdt"),
),
),
)
@ -154,14 +159,14 @@ class CurrencyIdConverterTest {
private fun provideTestModels(): Collection<ConvertBackModel> = listOf(
ConvertBackModel(
value = CryptoCurrency.ID.fromValue("coin⟨ETH$derivationPathHashCode⟩ethereum"),
value = CryptoCurrency.ID.fromValue("coin⟨ethereum$derivationPathHashCode⟩ethereum"),
expected = Result.success(
CurrencyId.createCoinId("ethereum"),
),
),
ConvertBackModel(
value = CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CurrencyId.createTokenId(
@ -172,7 +177,7 @@ class CurrencyIdConverterTest {
),
ConvertBackModel(
value = CryptoCurrency.ID.fromValue(
value = "token⟨ETH$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
value = "token⟨ethereum$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
),
expected = Result.success(
CurrencyId.createTokenId(
@ -184,6 +189,54 @@ class CurrencyIdConverterTest {
)
}
/**
* Regression coverage for [REDACTED_TASK_KEY]. Cache stores `blockchainId` in the legacy SDK format
* (`Blockchain.id`, e.g. "ETH"), but runtime [CryptoCurrency.ID] is built using the canonical
* network rawId (`Blockchain.toNetworkId()`, e.g. "ethereum"). The converter must bridge the
* two formats so that IDs reconstructed from cache equal those built at runtime otherwise
* `NetworkStatus.Verified.amounts[currency.id]` returns null and the wallet shimmer never clears.
*/
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class LegacyBlockchainIdNormalization {
@Test
fun `convert with legacy ETH blockchainId produces id with canonical ethereum rawId`() {
val cached = CurrencyId.createCoinId("ethereum")
val result = converter.convert(cached)
Truth.assertThat(result)
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨$canonicalNetworkRawId$derivationPathHashCode⟩ethereum"))
}
@Test
fun `convert with legacy BTC blockchainId produces id with canonical bitcoin rawId`() {
val btcDerivationPath = Network.DerivationPath.Card(value = "m/44'/0'/0'/0/0")
val btcDerivationHash = btcDerivationPath.value.hashCode()
val btcConverter = NetworkCurrencyIdConverter(
blockchainId = "BTC",
derivationPath = btcDerivationPath,
)
val cached = CurrencyId.createCoinId("bitcoin")
val result = btcConverter.convert(cached)
Truth.assertThat(result)
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨bitcoin→$btcDerivationHash⟩bitcoin"))
}
@Test
fun `convert and convertBack roundtrip preserves CurrencyId`() {
val cached = CurrencyId.createCoinId("ethereum")
val runtimeId = converter.convert(cached)
val roundTrip = converter.convertBack(runtimeId)
Truth.assertThat(roundTrip).isEqualTo(cached)
}
}
data class ConvertModel(val value: CurrencyId, val expected: Result<CryptoCurrency.ID>)
data class ConvertBackModel(val value: CryptoCurrency.ID, val expected: Result<CurrencyId>)

View file

@ -1,6 +1,7 @@
package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.*
@ -49,7 +50,7 @@ internal class NetworkStatusDataModelConverterTest {
),
),
amounts = mapOf(
ID.fromValue(value = "coin⟨ETH→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue(value = "coin⟨ethereum→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID(
prefix = Prefix.COIN_PREFIX,
body = Body.NetworkId(rawId = "BTC"),
@ -74,7 +75,7 @@ internal class NetworkStatusDataModelConverterTest {
),
),
expected = Verified(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "",
type = DerivationPath.Type.NONE,
@ -119,7 +120,7 @@ internal class NetworkStatusDataModelConverterTest {
),
),
expected = NoAccount(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "",
type = DerivationPath.Type.NONE,

View file

@ -13,10 +13,14 @@ import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class NetworkYieldSupplyStatusConverterTest {
private val rawNetworkId = "ETH"
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
// The converter normalizes it to the canonical network rawId ("ethereum") via
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
// built at runtime from Network.rawId.
private val blockchainId = "ETH"
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
private val derivationPathHashCode = "-1843072795"
private val converter = NetworkYieldSupplyStatusConverter(rawNetworkId, derivationPath)
private val converter = NetworkYieldSupplyStatusConverter(blockchainId, derivationPath)
private val domainStatus = YieldSupplyStatus(
isActive = true,
@ -38,8 +42,8 @@ internal class NetworkYieldSupplyStatusConverterTest {
// Assert
val expected = mapOf(
ID.fromValue("coin⟨ETH$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ETH$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
ID.fromValue("coin⟨ethereum$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ethereum$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
)
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
@ -49,9 +53,9 @@ internal class NetworkYieldSupplyStatusConverterTest {
fun convertBack() {
// Arrange
val value = mapOf(
ID.fromValue("coin⟨ETH$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ETH$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
ID.fromValue("token⟨ETH$derivationPathHashCode⟩usdc⚓0x1") to null,
ID.fromValue("coin⟨ethereum$derivationPathHashCode⟩ethereum") to domainStatus,
ID.fromValue("token⟨ethereum$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
ID.fromValue("token⟨ethereum$derivationPathHashCode⟩usdc⚓0x1") to null,
)
// Act

View file

@ -1,6 +1,7 @@
package com.tangem.data.networks.converters
import com.google.common.truth.Truth
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.networks.models.SimpleNetworkStatus
import com.tangem.datasource.local.network.entity.NetworkStatusDM
@ -48,7 +49,7 @@ internal class SimpleNetworkStatusConverterTest {
// region Verified
ConvertModel(
value = Verified(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -95,12 +96,12 @@ internal class SimpleNetworkStatusConverterTest {
),
),
amounts = mapOf(
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("token⟨ETH→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
ID.fromValue("token⟨ethereum→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
),
pendingTransactions = emptyMap(),
yieldSupplyStatuses = mapOf(
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to YieldSupplyStatus(
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to YieldSupplyStatus(
isActive = false,
isInitialized = false,
isAllowedToSpend = false,
@ -116,7 +117,7 @@ internal class SimpleNetworkStatusConverterTest {
// region NoAccount
ConvertModel(
value = NoAccount(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -168,7 +169,7 @@ internal class SimpleNetworkStatusConverterTest {
// region Error
ConvertModel(
value = Verified(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -189,7 +190,7 @@ internal class SimpleNetworkStatusConverterTest {
),
ConvertModel(
value = Verified(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -205,7 +206,7 @@ internal class SimpleNetworkStatusConverterTest {
),
ConvertModel(
value = Verified(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -230,7 +231,7 @@ internal class SimpleNetworkStatusConverterTest {
),
ConvertModel(
value = NoAccount(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -255,7 +256,7 @@ internal class SimpleNetworkStatusConverterTest {
),
ConvertModel(
value = NoAccount(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,
@ -276,7 +277,7 @@ internal class SimpleNetworkStatusConverterTest {
),
ConvertModel(
value = NoAccount(
networkId = ID(network.rawId),
networkId = ID(network.toBlockchain().id),
derivationPath = DerivationPath(
value = "card",
type = DerivationPath.Type.CARD,

View file

@ -52,7 +52,7 @@ internal class CommonNetworkStatusFetcherTest {
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
val extraTokens = setOf(
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
cryptoCurrencyFactory.createToken(Blockchain.Ethereum),
)
val updateException = IllegalStateException()
@ -106,7 +106,7 @@ internal class CommonNetworkStatusFetcherTest {
val userWalletId = UserWalletId("011")
val network = cryptoCurrencyFactory.ethereum.network
val extraTokens = setOf(
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
cryptoCurrencyFactory.createToken(Blockchain.Ethereum),
)
val updateResult = model.updateResult
val status = model.status
@ -169,15 +169,15 @@ internal class CommonNetworkStatusFetcherTest {
it.copy(
amounts = mapOf(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND",
value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND",
) to NetworkStatus.Amount.NotFound,
),
pendingTransactions = mapOf(
CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND") to emptySet(),
CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND") to emptySet(),
),
yieldSupplyStatuses = mapOf(
CryptoCurrency.ID.fromValue(
value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND",
value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND",
) to null,
),
)

View file

@ -3,6 +3,7 @@ package com.tangem.data.networks.multi
import arrow.core.Either
import arrow.core.left
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
import com.tangem.data.networks.store.NetworksStatusesStore
@ -27,17 +28,21 @@ internal class DefaultMultiNetworkStatusFetcherTest {
private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
private val dynamicAddressesInitializer: DynamicAddressesInitializer = mockk()
private val fetcher = DefaultMultiNetworkStatusFetcher(
networksStatusesStore = networksStatusesStore,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
commonNetworkStatusFetcher = commonNetworkStatusFetcher,
dynamicAddressesInitializer = dynamicAddressesInitializer,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@BeforeEach
fun resetMocks() {
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer)
// No dynamic addresses restore by default
coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap()
}
@Test
@ -62,6 +67,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
} returns ethereumFetcherResult
@ -70,6 +76,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns cardanoFetcherResult
@ -87,11 +94,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
}
@ -122,6 +131,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
} returns ethereumFetcherResult
@ -130,6 +140,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns cardanoFetcherResult
@ -147,11 +158,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
}
@ -182,6 +195,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
} returns ethereumFetcherResult
@ -190,6 +204,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns cardanoFetcherResult
@ -207,11 +222,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
}
@ -245,7 +262,220 @@ internal class DefaultMultiNetworkStatusFetcherTest {
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
}
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) }
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any(), any()) }
}
@Test
fun `fetch passes xpub to correct network and null to others`() = runTest {
// Arrange
val xpub = "xpub_test_eth"
val params = setupTwoNetworkParams()
coEvery { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) } returns mapOf(ethereum.network to xpub)
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = xpub,
)
} returns Either.Right(Unit)
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns Either.Right(Unit)
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Right(Unit)
assertEither(actual, expected)
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = xpub,
)
}
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
}
}
@Test
fun `fetch continues with null xpub for all networks if getXpubs throws`() = runTest {
// Arrange
val params = setupTwoNetworkParams()
coEvery {
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
} throws RuntimeException("XPUB derivation failed")
val fetchResult = Either.Right(Unit)
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
} returns fetchResult
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns fetchResult
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Right(Unit)
assertEither(actual, expected)
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = null,
)
}
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
}
// getXpubs failure must not degrade network status to OnlyCache
coVerify(inverse = true) { networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) }
}
@Test
fun `fetch passes xpubs to all networks returned by getXpubs`() = runTest {
// Arrange
val ethXpub = "xpub_eth"
val adaXpub = "xpub_ada"
val params = setupTwoNetworkParams()
coEvery {
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
} returns mapOf(ethereum.network to ethXpub, cardano.network to adaXpub)
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = ethXpub,
)
} returns Either.Right(Unit)
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = adaXpub,
)
} returns Either.Right(Unit)
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Right(Unit)
assertEither(actual, expected)
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = ethXpub,
)
}
coVerify(exactly = 1) {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = adaXpub,
)
}
}
@Test
fun `fetch calls getXpubs with exactly the networks from params`() = runTest {
// Arrange
val params = setupTwoNetworkParams()
coEvery {
commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any(), xpub = any())
} returns Either.Right(Unit)
// Act
fetcher(params)
// Assert
coVerify(exactly = 1) { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) }
}
@Test
fun `fetch failure if network fetch fails when xpub is provided`() = runTest {
// Arrange
val xpub = "xpub_eth"
val params = setupTwoNetworkParams()
coEvery {
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
} returns mapOf(ethereum.network to xpub)
val fetchFailure = Either.Left(IllegalStateException())
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = ethereum.network,
networkCurrencies = setOf(ethereum),
xpub = xpub,
)
} returns fetchFailure
coEvery {
commonNetworkStatusFetcher.fetch(
userWalletId = params.userWalletId,
network = cardano.network,
networkCurrencies = setOf(cardano),
xpub = null,
)
} returns Either.Right(Unit)
// Act
val actual = fetcher(params)
// Assert
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
assertEither(actual, expected)
}
private fun setupTwoNetworkParams(): MultiNetworkStatusFetcher.Params {
val params = MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOf(ethereum.network, cardano.network),
)
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns mapOf(
ethereum.network to listOf(ethereum),
cardano.network to listOf(cardano),
)
return params
}
private companion object {